Author SHA1 Message Date
Jordan Petridis 064d877205 Appdata: add release notes 2018-08-31 21:00:18 +03:00
Jordan Petridis fbf8cc87c9 Version bump 2018-08-31 14:14:17 +00:00
Jordan Petridis bebabf84a0 Update Changelog 2018-08-31 14:11:29 +00:00
Seong-ho Cho 36f169635a Add Korean translation 2018-08-31 00:56:44 +00:00
Jordan Petridis 3f509f44a1 Update dependancies. 2018-08-30 20:45:53 +03:00
Jordan Petridis 9bc8a8ac2b Release: include meson_options.txt 2018-08-30 20:21:11 +03:00
Jordan Petridis abfe98283b EpisodeWidget: Allow size labels to elipsize 2018-08-30 20:21:10 +03:00
Kukuh Syafaat ded0224f51 Add Indonesian translation 2018-08-30 10:43:20 +00:00
Fran Dieguez 0060a634d2 Add Galician translation 2018-08-30 08:45:45 +00:00
Jordan Petridis d34005e04f Merge branch 'chng-readme' into 'master'
README.md: Change instructions for translators

See merge request World/podcasts!68
2018-08-29 08:43:48 +00:00
Rafael Fontenelle c734bd48b5 README.md: Change instructions for translators
Change translation instructions to steer the translator to the translation platform D-L, to follow the proper workflow of the language teams.
2018-08-29 07:29:38 +00:00
Jordan Petridis 174c814541 Merge branch 'alatiera/cover_load_perf' into 'master'
Cover loading performance improvments

See merge request World/podcasts!67
2018-08-29 07:29:17 +00:00
Jordan Petridis 993b6e9d0a Utils: only queue a single cover download
Before we were inserting the id of the cover into the registry
from a rayon thread. But rayon will only execute N threads at the
same time and let the rest into a queue. This would casue mutliple
jobs being queued since the cover id was not inserted in the
registry until the downloading had started.

This fixes said behavior by having the main thread block and write
in the id in the registry.
2018-08-28 21:15:52 +00:00
Jordan Petridis 273c9f7b99 Utils: Change the priority of the cover caches
Since loadign a pixbuf from the pre-rendered cache is the most
common operation and it does not affect the behavior we can
first check that and then if the cover is midway downloading.

This avoids a mutex lock for the most common path.
2018-08-28 21:15:52 +00:00
Jordan Petridis 822deb2867 Utils: do not block the cover_dl registry
Accidently after f21398357b when a download would start,
it would lock the cover_dl_registry hashmap till it had finished.

Since the registry.read() happens on the main thread this would
cause the UI to block until the download was and the mutex guard
from the download thread dropped.
2018-08-28 21:15:52 +00:00
Merge Bot 132e2afce0 Merge branch 'wip/piotrdrag/update-potfiles-180828' into 'master'
Update POTFILES.in

See merge request World/podcasts!66
2018-08-28 20:54:09 +00:00
Piotr Drąg 2a888f0bce Update POTFILES.in 2018-08-28 19:44:34 +02:00
Jordan Petridis f8202a7add Merge branch 'alatiera/meson-stuff' into 'master'
Parallel installation and meson cleanups

See merge request World/podcasts!64
2018-08-28 16:13:31 +00:00
Jordan Petridis 87e8d0b775 Fix the test-suite 2018-08-28 17:50:57 +03:00
Jordan Petridis 569c00ff5f Allow for parallel development instance
This adds a configuration option in meson, if set it changes the
application ID allowing for stable and development version to be
run at the same time.
2018-08-28 17:22:13 +03:00
Jordan Petridis 15457e1db4 App/Build: Use env! macro to fetch the variable
env! is resolved at compile time which means we don't need to read
and set LOCALDI from build.rs
2018-08-28 17:22:13 +03:00
Jordan Petridis 0ae1eb9578 build: Translate desktop and appdata files 2018-08-28 17:22:13 +03:00
Jordan Petridis aa1d0161d3 cargo fmt 2018-08-27 21:08:29 +03:00
Emin Tufan Çetin a2d8b88337 Update Turkish translation 2018-08-27 08:43:08 +00:00
Jordan Petridis a1b4306954 Merge remote-tracking branch 'zander/master'
See !65 for more.
2018-08-27 10:53:07 +03:00
Zander Brown 88e07031a6 Workaround for FileChooserNative oddness 2018-08-27 07:52:09 +00:00
Piotr Drąg cb4daa1ba1 Update Polish translation 2018-08-26 16:31:44 +00:00
Jordan Petridis 46cfa79e89 Update .gitignore 2018-08-26 12:39:26 +03:00
Jordan Petridis a480f47cea cargo fmt 2018-08-26 12:38:36 +03:00
Jordan Petridis 4ef789e7b9 Meson: Fix build_always deprication warning
build_always has been replaced by build_always_stale and
build_by_default is assumed now.

https://mesonbuild.com/Reference-manual.html#custom_target
2018-08-26 12:38:36 +03:00
Mario Blättermann 05d6d8399d Update German translation 2018-08-25 21:27:45 +00:00
Jordan Petridis 8bc81d6b5e Update pot files 2018-08-22 08:29:30 +03:00
Jordan Petridis 6c5cb8f07d AboutDialog: Translate website label 2018-08-22 08:29:12 +03:00
Jordan Petridis 70e79e50d6 Change the website to point to the wiki page
Thanks a lot to @svito for creating the page!
2018-08-21 20:11:38 +03:00
Jordan Petridis d2c6c6cc4d Update pot files 2018-08-20 14:09:34 +03:00
Jordan Petridis ba5e22bd21 README: Add flathub banner 2018-08-20 13:55:15 +03:00
Jordan Petridis 9d0bfdea44 README: Update build instructions 2018-08-20 13:41:19 +03:00
Jordan Petridis 92ae681517 Add gtk tests
Currently we only test the GtkBuilder files.
Also I can't find a way to get gtk to uninitialize and reinitialize
in a different thread.

Close #56
2018-08-20 13:14:41 +03:00
Jordan Petridis 3afa8c4441 Flatpak: avoid the git redirect 2018-08-20 05:27:31 +03:00
Emin Tufan Çetin 547fdef9c4 Update Turkish translation 2018-08-19 14:54:27 +00:00
Merge Bot 152c250300 Merge branch 'alatiera/content_states' into 'master'
content states

Closes #71

See merge request World/podcasts!63
2018-08-19 11:56:06 +00:00
Jordan Petridis 04161284a7 Headerbar: Make the switcher insensitive if empty
If there are no shows/episodes to display, there isn't any point
to being able to hit the switcher.
2018-08-19 14:31:27 +03:00
Jordan Petridis 14d4818867 App: Disable refresh action while in empty state
Close #71
2018-08-19 14:25:41 +03:00
Jordan Petridis 9f42e91088 Refactor content state with Application actions
Instead of each view/widget determening if its populated on its own,
make add Application Actions and apply the state globally.
2018-08-19 13:44:11 +03:00
Jordan Petridis 79ac3b9700 Update dependancies 2018-08-18 18:33:26 +03:00
Jordan Petridis 7a3178896b Remove criterion
Haven't been able to use it effectivly with futures. Maybe will
revisit it again at a later time. For now it just adds extra
build time.
2018-08-18 18:02:28 +03:00
Jordan Petridis ee95512321 Update .gitignore 2018-08-18 17:37:06 +03:00
Jordan Petridis 55519b1855 scripts/test.sh: avoid conlfict with vanila cargo
I am not sure why, but cargo does not like it if flatpak-builder
and gnome Builder try to use the same target/ directory, even
though the environment should be almost identical.

The only difference is that Builder uses flatpak build instead of
flatpak-builder as far as I can see.
2018-08-18 17:34:49 +03:00
Jordan Petridis 89b99614a0 Refactor the tests to use ? operator
As of ructc 1.27 #[test] can return errors. This improves a great
deal the ergonomics.
2018-08-18 17:02:31 +03:00
Jordan Petridis 7bbd9a1a4f Update the test script 2018-08-18 16:25:43 +03:00
Tobias Bernard 0dfb48593e App icon: orange -> red 2018-08-17 12:49:44 +02:00
Jordan Petridis 3abe0803d6 Source: Remove reduntant save() call
Source::clear_etags does a save already, so it's not needed to call
it twice.
2018-08-17 09:57:49 +03:00
Merge Bot 1e0a919dc7 Merge branch 'alatiera/source-redirects-85' into 'master'
Source: Improve http redirections handling

Closes #85

See merge request World/podcasts!62
2018-08-17 04:33:12 +00:00
Jordan Petridis 489e8aa4b3 Source: Improve http redirections handling
We should follow all redirects, and update the Source uri for
301, 302 and 308 codes.

Closes #85
2018-08-17 07:09:12 +03:00
Merge Bot 5058f2f8d8 Merge branch 'alatiera/source-etags-71' into 'master'
Source: Only save Etag headers upon succesful requests

Closes #64

See merge request World/podcasts!61
2018-08-17 02:56:04 +00:00
Jordan Petridis 49aff9f22e Source: Only save Etag headers upon succesful requests
Additionally clear the Etags if the returned code is not 200 or 304.
Just to be extra safe. This is not as clean as it should, as this is
a temporary workaround until the API is reworked.

Related to #64
2018-08-17 05:36:34 +03:00
Jordan Petridis 775d4accf7 Flatpak: switch to master branch of the runtime 2018-08-16 08:30:46 +03:00
Jordan Petridis 703a1baf8b CI: Move repo export right before the bundle generation 2018-08-16 08:30:42 +03:00
Jordan Petridis 51fdad2ae2 CI: Use variables instread of hardcoding things 2018-08-16 08:30:38 +03:00
Jordan Petridis 86545e5f99 CI: Add git commit sha in the bundle name 2018-08-16 08:30:34 +03:00
Jordan Petridis 5a0413b3e4 CI: Remove --libdir=/app/libdir from meson config
This is not needed anymore I think.
2018-08-16 08:30:29 +03:00
Jordan Petridis a0ff2b8ae4 CI: set CARGO_HOME before cargo test
This is set in the scripts/cargo.sh during building, but since we
invoke cargo test manually we need to also set it here.
2018-08-16 08:30:24 +03:00
Mario Blättermann 6b6c390cb8 Add German translation 2018-08-15 17:19:11 +00:00
Jordan Petridis cd937c4844 utils: Refactor refresh_feed
Move channel creation inside the thread.
Drop the Result return type as its not needed anymore.
2018-08-14 15:24:25 +03:00
Jordan Petridis cc1a5783fd App: Do not update the db if its empty
If the source table is empty skipp the database refresh.
2018-08-14 15:19:31 +03:00
Jordan Petridis 5631caad36 Gitlab: Fix the bug issue template
Forgot to change this when it was copied from nautilus.
2018-08-14 14:23:35 +03:00
Jordan Petridis d0144fb0d0 CI: Do not spin up review enviorment for tags 2018-08-14 14:11:16 +03:00
Jordan Petridis 5d1870c1cf CI: Lower flatpak bundle lifespan 2018-08-14 14:03:06 +03:00
Jordan Petridis fb87460bc7 CI: Clear unused stuff
No point on keeping them around since libhandy was added as a dep.
No distro packages libhandy currently, so only the flatpak will be
buildable for the foreseeable future.
2018-08-14 14:01:14 +03:00
Jordan Petridis cb122cbc61 ShowWidget: Change description wrap mode 2018-08-14 13:59:42 +03:00
Jordan Petridis efc4e299ac CI: Add env viariable for the tests 2018-08-14 13:58:09 +03:00
Jordan Petridis 848caa275b CI: re-enable the test-suite 2018-08-14 13:41:12 +03:00
Jordan Petridis 03754c56c6 Fix the test-suite 2018-08-14 13:40:37 +03:00
Jordan Petridis 471f6ff93b Source: Remove ignore_etags option
This is never used anywhere else apart from the testsuite. Instead
of ignoring etags we should instead not save them if the feed does
not return 200 or 304. See #64.
2018-08-14 13:40:33 +03:00
Jordan Petridis c53ad56a6d Remove TODOs and FIXMEs
They are either no longer relevant or just forgotten
2018-08-14 13:40:28 +03:00
Tobias Bernard 646439d86a Merge branch 'alatiera/move-update-notif' into 'master'
Move the update indication into an In-app Notification

Closes #72

See merge request World/podcasts!60
2018-08-14 10:11:28 +00:00
Jordan Petridis ae7f65e938 InAppNotif: Switch the timer to milliseconds
This allows for more responsive updates. The implementation still
sucks though. Ideally we would pass a receiver in the callback
and have an even lower timeout_add.
2018-08-14 07:58:29 +03:00
Jordan Petridis b2d71a037c Headerbar: Remove the update indicator 2018-08-14 07:52:41 +03:00
Jordan Petridis 019ec8972f InAppNotif: Add a spinner 2018-08-14 07:41:58 +03:00
Jordan Petridis e25e411ebe App: Use the new updater notif
Initial wiring of the new InAppNotif update indicator. This still
misses a spinner, and its overall teribly implemented!
2018-08-14 07:28:10 +03:00
Jordan Petridis 911dcbac9f InAppNotif: Pass revealer to the callback
Let the callback handle if/when the visibility of the notification
2018-08-14 06:22:02 +03:00
Jordan Petridis 25195c972c InAppNotif: add a method to show/hide the close button
This will enable us to create persistant notifications.
2018-08-14 05:36:17 +03:00
Jordan Petridis 304c92f733 InAppNotification: Allow to set a custom timer
This allows for a custom timer to be set before the
callback will be run. Currently all the callbacks only
run once and then retunr glib::Continue(false) but this
would allow for setting a low timer and have a callback
that would determine if it needs to be run again, Continue(true),
in a relative responsive way.
2018-08-13 09:13:39 +03:00
Jordan Petridis 336b9a126e InAppNotif: Fix ref cycles 2018-08-13 09:13:10 +03:00
Jordan Petridis 01efbf5c79 InAppNotif: Refactor to infer the undo state
If we use an Optional instead of passing empty closures, we
can infer if the Undo button needs to be shown.
2018-08-13 08:31:52 +03:00
Merge Bot, Bors Wannabe 866fa6a758 Merge branch 'fix-ref-cycles' into 'master'
Fix more refference cycles

See merge request World/podcasts!59
2018-08-13 04:28:49 +00:00
Jordan Petridis b8bb5e6d82 ShowMenu: Fix a reference cycle 2018-08-13 06:48:39 +03:00
Jordan Petridis cc4b3cce55 Player: Fix a refference cycle 2018-08-13 06:35:19 +03:00
Jordan Petridis 5699562133 App: Fix more refference cycles 2018-08-13 03:34:37 +00:00
Piotr Drąg dae064d2bb Update Polish translation 2018-08-13 01:45:08 +00:00
Jordan Petridis d54e15cd15 EmptyView: Fix a typo 2018-08-13 04:05:28 +03:00
Jordan Petridis dbdf56d494 App: Do not placeholder strings as translatable
Also add license metadata, and update the targeted gtk3 version
2018-08-13 02:34:53 +03:00
Jordan Petridis b07cd5515a Refactor Empty states 2018-08-13 02:05:09 +03:00
Emin Tufan Çetin 73929f2d25 Add Turkish translation 2018-08-12 11:05:29 +00:00
Jordan Petridis acaa06749e ShowWidget: Keep the Frame from filling available the space 2018-08-12 02:41:22 +03:00
Piotr Drąg 1bd6efc0c1 Add Polish translation 2018-08-11 16:43:15 +00:00
Jiri Grönroos 936960269d Add Finnish translation 2018-08-11 14:10:14 +00:00
Jordan Petridis 5780df20ad Merge branch 'wip/piotrdrag/gschema-gettext' into 'master'
gschema: Add gettext-domain

See merge request World/podcasts!56
2018-08-11 09:24:05 +00:00
Piotr Drąg 14e5f33f2a gschema: Add gettext-domain
Without it DConf Editor won’t show localized schemas.
2018-08-11 01:35:28 +02:00
Jordan Petridis 7aa86bcec4 Merge branch 'wip/piotrdrag/update-potfiles-180810' into 'master'
Update POTFILES.in

See merge request World/podcasts!54
2018-08-10 23:20:38 +00:00
Piotr Drąg c77a1e85a4 Update POTFILES.in 2018-08-10 20:58:18 +00:00
Jordan Petridis 093b8cb6df Merge branch 'wip/piotrdrag/desktop-comments' into 'master'
desktop: Add translator comments

See merge request World/podcasts!55
2018-08-10 20:56:30 +00:00
Piotr Drąg 6460198e1d desktop: Add translator comments 2018-08-10 22:03:04 +02:00
Jordan Petridis 662dc3fa85 HomeView: Properly align the frames
d5ea0d5a17 broke alignment
between the listboxes and the frame labels.
2018-08-10 22:49:54 +03:00
Merge Bot, Bors Wannabe 5a4bce2816 Merge branch 'alatiera/libhandy' into 'master'
Refactor views and use HdyColumn for views with that display `EpisodeWidget`s

Closes #70

See merge request World/podcasts!48
2018-08-10 18:35:58 +00:00
Jordan Petridis d5ea0d5a17 HoveView and ShowWiget: Add margins around the listboxes 2018-08-10 21:14:49 +03:00
Jordan Petridis 9e525727fd InAppNotif: allow the text to elipsize
Also fixe deprications in the glae file, add license
2018-08-10 20:49:59 +03:00
Jordan Petridis 95b6995649 Headerbar: Elipsisize update label 2018-08-10 15:54:12 +03:00
Jordan Petridis 745064c5ce AddPopOver: Remove deprication warning 2018-08-10 15:47:20 +03:00
Jordan Petridis 223a3b46bf Headerbar: Elipsisize show title 2018-08-10 15:46:28 +03:00
Jordan Petridis fee3e320ab ShowWidget: Move the listbox back to the glade file 2018-08-10 15:43:19 +03:00
Jordan Petridis e068cff37b ShowWidget: Increase the description char limit 2018-08-10 14:44:02 +03:00
Jordan Petridis 28f08ed196 HomeView: handle vadjustment with BaseView
Instead of using lazy_static to save the adjustment,
pass it to the widget upon creation. If its the first instance
created, pass None instead.
2018-08-10 14:43:58 +03:00
Jordan Petridis 056d971000 HomeView: make base view field publick
There is no point to re-export BaseView's methods.
2018-08-10 14:42:57 +03:00
Jordan Petridis aa5195e5a9 ShowsView: handle vadjustment with BaseView
Instead of using lazy_static to save the adjustment,
pass it to the widget upon creation. If its the first instance
created, pass None instead.
2018-08-10 14:42:52 +03:00
Jordan Petridis 85aaaf80ac ShowsView: make base view field publick
There is no point to re-export BaseView's methods.
2018-08-10 13:54:19 +03:00
Jordan Petridis 5d467a22d0 ShowWidget: make base view field publick
There is no point to re-export BaseView's methods.
2018-08-10 13:54:14 +03:00
Jordan Petridis bcc1cfb67b ShowWidget: handle vadjustment with BaseView
Instead of using lazy_static to save the adjustment,
pass it to the widget upon creation. If previous it doesn't
exists pass None instead.
2018-08-10 13:54:07 +03:00
Jordan Petridis 70a24fba69 BaseView: implement a set_adjustments method
Ment to replace the individual set_vadjustment of widgets.
Also remove unused method from ShowWiget.
2018-08-10 13:53:25 +03:00
Jordan Petridis c3121bef84 ShowsChild: Remove the need for a .ui file
Its a simple enough widget that can be written by hand
and does not need a .ui builder file.
2018-08-10 13:53:12 +03:00
Jordan Petridis 8c25be7d05 BaseView: Set scroll policy 2018-08-10 13:53:00 +03:00
Jordan Petridis 7463e9d42c ShowsView: Use BaseView abstraction 2018-08-10 13:52:56 +03:00
Jordan Petridis e4dd9f5bb3 HomeView: Use libhandy::Column for the main widget 2018-08-10 13:52:52 +03:00
Jordan Petridis 4e59d648ef HomeView: Use the new BaseView abstraction 2018-08-10 13:52:48 +03:00
Jordan Petridis 7538e76537 Gtk: Abstract a BaseView Widget
Currently it just re-exports its children with getters,
but the idea is that it will also be able to handle
the saving the state of its height adjustment later.
2018-08-10 13:52:44 +03:00
Jordan Petridis 20ddc54edc ShowWidget: Put the whole thing inside HdyColumn 2018-08-10 13:52:35 +03:00
Jordan Petridis ac75205933 ShowWidget: Initial prototype w/ libhandy::Column 2018-08-10 13:47:59 +03:00
Jordan Petridis 17b58b159a ShowWidget: Remove unsused menu from the glade file 2018-08-10 13:47:12 +03:00
Jordan Petridis 191cf445ef ShowWidget: Do not hardcode the episode listbox in the glade file 2018-08-10 13:47:07 +03:00
Jordan Petridis 65a0c08cb3 Flatpak: fix previous commit that broke it
I broke both the manifest and the CI in 4da68ff89c
2018-08-10 13:44:48 +03:00
Jordan Petridis 4da68ff89c Flatpak: switch to using the 3.28 runtime again
Gtk was upgraded yesterday to 3.24. This causes a warning about
widgets being drawn without prior allocation. I am not sure yet if
the bug is in the bindings or if gtk's ABI broke.
2018-08-10 12:11:08 +03:00
Jordan Petridis a7da4525fd Flatpak: Grant access to thee dri
A warning suggested it, I assume it was coming from Gtk.
2018-08-10 12:08:54 +03:00
Jordan Petridis 92dfbce45a Merge branch 'wip/piotrdrag/review-strings' into 'master'
podcasts-gtk: Review ALL the strings!

See merge request World/podcasts!53
2018-08-10 05:58:11 +00:00
Piotr Drąg bfdd6b5f7c podcasts-gtk: Review ALL the strings! 2018-08-10 05:23:15 +00:00
Jordan Petridis a8a8c09b90 Merge branch 'wip/piotrdrag/appdata-fix' into 'master'
appdata: Remove duplicate tag

See merge request World/podcasts!49
2018-08-10 05:21:47 +00:00
Piotr Drąg 4bd13b81cf appdata: Remove duplicate tag 2018-08-10 08:00:32 +03:00
Jordan Petridis 5a07600664 Merge branch 'wip/piotrdrag/unmark-placeholders' into 'master'
episode_widget: Unmark placeholder strings from translation

See merge request World/podcasts!50
2018-08-10 04:59:39 +00:00
Piotr Drąg f2bc4b21cb episode_widget: Unmark placeholder strings from translation 2018-08-10 04:37:32 +00:00
Jordan Petridis 778975fbde Merge branch 'wip/piotrdrag/unmark-copyright' into 'master'
aboutdialog: Unmark copyright string from translation

See merge request World/podcasts!51
2018-08-10 04:36:54 +00:00
Piotr Drąg 57f920624f aboutdialog: Unmark copyright string from translation 2018-08-10 04:05:31 +00:00
Jordan Petridis 3d87940da9 Merge branch 'wip/piotrdrag/translate-appname' into 'master'
aboutdialog: Translate the app name

See merge request World/podcasts!52
2018-08-10 04:04:04 +00:00
Piotr Drąg c33d0836eb app: Translate the app name 2018-08-10 06:34:02 +03:00
Jordan Petridis a01cf21f2c Flatpak: use master runtime of GNOME sdk 2018-08-09 10:25:02 +03:00
Jordan Petridis 10fc1d4a2a Gitlab: Add merge request template 2018-08-09 10:23:34 +03:00
Jordan Petridis 4fa973007d App: Make sure to quit on delete event.
Since 9a76c6428a I've noticed
the applciation does not quit properly, and then it's unable
to be launched again till it's killed manually.

This patch is an attept to correct/workaroud that by calling
explcitly gio::ApplicationExt::quit().
2018-08-06 01:01:21 +03:00
Jordan Petridis c27f5ec02e Merge branch 'readme_improvements' into 'master'
Fix some typos in README.md

See merge request World/podcasts!47
2018-08-05 08:57:26 +00:00
Matthew 5d6cf3d17d Fix some typos in README.md 2018-08-05 11:32:42 +03:00
Jordan Petridis f6c7731377 Fix Rust 2018 edition warnings 2018-08-05 11:24:20 +03:00
Jordan Petridis bcd739da76 Merge branch 'i18n' into 'master'
Translation support and initial spanish translation

See merge request World/podcasts!46
2018-08-02 14:54:41 +00:00
Daniel García Moreno bea4915317 Translation support and initial spanish translation
Added translation support based on the Fractal i18n. To do this I've
added the gettext-rs crate dep. I'm using my own fork because the
official gettext-rs release includes the gettext source files and that
increase the distribution package a lot and for distribution with
flatkap we don't need to build gettext, the lib is in the gnome sdk. So
this gettext-rs fork is the same, but removing the not needed gettext
source files.

The i18n.rs file adds some useful functions to translate strings. These
functions wraps the original gettext and adds more functionality, to be
able to translate compound strings, something that's not supported by
the gettext function.

The 'i18n' function works like the gettext, receives a plain string
without params.

The 'i18n_f' function receives a string with "{}" and a ref to an array
of &str with substitutions for the "{}" in the original string. The
substitution is done by order.

The 'i18n_k' function receives a string with "{named}" and a ref to an
array of (&str, &str) with substitutions for the "{named}" in the
original string. The substitution is done by name, where the first &str
in the tuple is the name and the second the string to use for the
replace.

This mod also include ni18n variants of the three functions for plural
and singular translations.

I've also created the spanish translation.

See #61

https://gitlab.gnome.org/World/podcasts/issues/61
2018-08-02 15:24:19 +02:00
Jordan Petridis f695ba4605 cargo fmt 2018-08-02 07:10:22 +03:00
Jordan Petridis 24983ba3af Readme.md: Improve wording and formatting 2018-08-02 02:44:29 +00:00
Jordan Petridis 0a2a3b3377 Remove unused permission. 2018-08-01 02:14:27 +03:00
Jordan Petridis d8d7193cbc README: Add flahub instructions. 2018-07-31 11:40:05 +03:00
Jordan Petridis 042c9eed9c Appdata: Remove 'lang=' metadata tag from the description.
This seems to confuse the flathub store website.
2018-07-31 11:22:15 +03:00
Jordan Petridis b0c94dd998 Appdata: Add OARS Rating
This help with content age rating. Usefull for store
clients like gnome-software.

https://hughsie.github.io/oars/
2018-07-31 10:58:04 +03:00
Jordan Petridis 4c8cc9e823 Version bump. 2018-07-31 00:31:44 +03:00
Jordan Petridis 132c9bbdff ShowModel: Remove dead unused methods. 2018-07-30 23:23:58 +03:00
Jordan Petridis cc0caff8d0 EpisodeModels: Remove unused methods. 2018-07-30 23:20:10 +03:00
Jordan Petridis 3496df24f8 App: Fix refference cycles in actions. 2018-07-28 19:52:37 +03:00
Jordan Petridis e4e35e4c57 App: Replace action macro with a generic fuction. 2018-07-28 19:52:27 +03:00
Jordan Petridis 838320785e Replace SendCell with fragile.
SendCell is depricated now and its advised to use the Fragile
crate instead.

https://github.com/sdroege/send-cell/issues/5
2018-07-28 19:46:01 +03:00
Jordan Petridis 91bea85519 Update dependancies. 2018-07-28 19:15:26 +03:00
Jordan Petridis 88ea081661 Version bump. 2018-07-28 00:12:08 +03:00
Jordan Petridis 39c0a0dba5 Fix cargo vendor config. 2018-07-28 00:08:48 +03:00
Jordan Petridis 9d64d3e30d AppStream: captions were too short. 2018-07-27 21:45:33 +03:00
Jordan Petridis 39ff238716 Version bump. 2018-07-27 20:21:28 +03:00
Jordan Petridis 7c96152f3f Gtk: Change the minimum requests of the views. 2018-07-27 19:46:18 +03:00
Jordan Petridis a2440c19e1 Appdata: Fix screenshots metadata. 2018-07-27 00:42:54 +03:00
Jordan Petridis 24dff5ce85 Update the release script. 2018-07-27 00:27:42 +03:00
Tobias Bernard 1ca8e15d19 Update org.gnome.Podcasts.appdata.xml 2018-07-26 21:15:10 +00:00
Tobias Bernard f77a8f09bb Update podcasts.doap 2018-07-26 21:13:04 +00:00
Jordan Petridis 48a7c8140f Update screenshots. 2018-07-26 23:53:08 +03:00
Jordan Petridis b03ff46767 Version bump. 2018-07-26 08:33:45 +03:00
Jordan Petridis e66e6364c3 Update Changelog 2018-07-26 05:30:46 +00:00
Jordan Petridis 89ef7ac4f6 Rename the .doap file. 2018-07-26 07:56:00 +03:00
Jordan Petridis 27e74ee064 Update the appdata summary. 2018-07-26 07:53:29 +03:00
Jordan Petridis 83c44aa12c Replace links again 2018-07-26 07:41:43 +03:00
Jordan Petridis 17da62d53b App: Fix some refference cycles. 2018-07-26 07:19:43 +03:00
Jordan Petridis d43fc268f4 App: Only execute .show_all() at startup.
If you would had an instance running and activate the app again
the UI would break visually in all kind of ways.
2018-07-26 07:07:56 +03:00
Jordan Petridis 5a7ab9795d Headerbar: rename menu-button to hamburger. 2018-07-25 04:57:00 +03:00
Jordan Petridis cfcdba5aea Headerbar: Avoid code duplication and ref cycles. 2018-07-25 04:38:24 +03:00
Jordan Petridis aaca6a6704 Prefs: Use weak refs to avoid refference cycle. 2018-07-25 04:25:40 +03:00
Jordan Petridis 5f7c822deb gtk: Rename left over home_view .ui files. 2018-07-25 03:55:42 +03:00
Jordan Petridis 53be091a31 Replace links 2018-07-25 03:26:41 +03:00
Jordan Petridis 04c68ba013 Initial massive renaming. 2018-07-25 03:26:35 +03:00
Jordan Petridis 518ea9c8b5 EmptyView: Improve wording of strings and Center stuff. 2018-07-24 15:07:13 +03:00
Jordan Petridis 6bb2142f35 Prefs: Fix typo. 2018-07-24 15:06:56 +03:00
Jordan Petridis 6aa931c866 Update deps. 2018-07-24 09:28:25 +03:00
Jordan Petridis 67ab54f820 Headerbar: Try to improve the add button behavior. 2018-07-24 09:05:55 +03:00
Jordan Petridis cc9fc80328 Remove some unwraps. 2018-07-24 08:04:31 +03:00
Jordan Petridis 3e8a8a6b85 Hamburger: connect the MenuModel during initialization. 2018-07-24 07:36:41 +03:00
Jordan Petridis 70a2d0e5f3 EpisodeWidget: Improve total_size label wording. 2018-07-24 07:29:52 +03:00
Jordan Petridis 09e8d7e1da AboutDialog: Update authors list. 2018-07-24 07:25:57 +03:00
Jordan Petridis 3c3d6c1e7f EmptyView: Style improvments.
Suggest using the OPML import action from the hamburger menu.

Minor rework of the over style.
2018-07-24 07:23:00 +03:00
Jordan Petridis 0dcc95cd34 Hamburger: Hide unimplemented menu actions. 2018-07-24 06:47:43 +03:00
Jordan Petridis edae1b0480 Add Application Icons.
Huge thanks to Tobias and Sam for these!
2018-07-24 06:27:34 +03:00
Jordan Petridis 5fb2cb7e76 Merge branch 'prefs' into 'master'
Prefrences dialog

See merge request World/hammond!34
2018-07-24 02:57:29 +00:00
Jordan Petridis 403bb71c5d Prefs: Bikeshed on the naming of things. 2018-07-24 05:05:52 +03:00
Jordan Petridis 9e23b16ae7 App: Remove no longer needed allow(unused). 2018-07-24 05:05:43 +03:00
Jordan Petridis 60a93a2433 App: Use the gio::prelude 2018-07-24 05:05:39 +03:00
Zander Brown 14dfafcb7c Sort out accelerators 2018-07-24 05:05:34 +03:00
Zander Brown dcc06cf8c6 Initial dialog 2018-07-24 05:05:30 +03:00
Zander Brown 54c084040c Minor rustfmt update 2018-07-24 05:05:26 +03:00
Zander Brown a9c38f5a03 Remove the update section 2018-07-24 05:05:22 +03:00
Zander Brown bce80cca0b Fix some style issues 2018-07-24 05:05:19 +03:00
Zander Brown f56fac6877 Bind GtkSettings dark property to GSettings
Now you can switch between dark and light on the fly
2018-07-24 05:05:15 +03:00
Zander Brown ef2286dca4 Bind refresh/cleanup rate 2018-07-24 05:05:11 +03:00
Zander Brown a4a012368e Bind switches to keys 2018-07-24 05:05:08 +03:00
Zander Brown 1b623ef346 Initial dialog 2018-07-24 05:05:00 +03:00
Jordan Petridis f661d24544 Merge branch 'icons-are-weird' into 'master'
Get the symbolics to work

See merge request World/hammond!45
2018-07-24 01:53:01 +00:00
Zander Brown cfe10553b2 Stop overriding the images 2018-07-24 04:01:16 +03:00
Jordan Petridis cf1042f40d Player: Improve the tooltips of buttons. 2018-07-23 23:36:52 +03:00
Jordan Petridis e77000076b Player: Add custom icons for the forward and rewind buttons. 2018-07-23 21:28:29 +03:00
Jordan Petridis 49241664dc Merge branch 'alatiera-master-patch-41773' into 'master'
Add Sam to the artists credits.

See merge request World/hammond!44
2018-07-21 23:19:11 +00:00
Jordan Petridis ffa3e9ec9a Add Sam to the artists credits. 2018-07-21 23:02:56 +00:00
Jordan Petridis fbbe0d9ca9 EpisodeWidget: Tweak padding and spacing. 2018-07-21 21:14:07 +03:00
Jordan Petridis 001eeecc09 ShowMenu: Add a separator and set alignment left. 2018-07-21 20:12:02 +03:00
Jordan Petridis 454a9c7076 ShowWidget: Fix description padding. 2018-07-21 19:41:15 +03:00
Jordan Petridis fc934ce8e1 ShowsView/Stack: Add some assertions. 2018-07-21 10:39:42 +03:00
Jordan Petridis b9bcc28e0f ShowMenu: Add debug assertions here too. 2018-07-21 10:30:46 +03:00
Jordan Petridis b5ddca65f5 ShowWidget: Add some assertions. 2018-07-21 10:24:48 +03:00
Jordan Petridis 536805791e ShowWidget: Move controls into a headerbar menu.
This fits better the HIG and allows for more flexibility in the way
the ShowWidget is implemented/designed.
2018-07-21 09:47:08 +03:00
Jordan Petridis 5a6c73c4c1 Headerbar: Move/Rename the menus.ui to hamburger.ui 2018-07-21 07:20:48 +03:00
Jordan Petridis d50f5a0488 Player: Remove no longer relavant FIXME annotations. 2018-07-19 22:14:17 +03:00
Jordan Petridis 671a31a95a Player: Improver human-facing error message. 2018-07-18 16:16:33 +03:00
Jordan Petridis 5e38f41530 Merge branch 'lazy_drawing' into 'master'
Lazy drawing

See merge request World/hammond!43
2018-07-18 13:14:44 +00:00
Jordan Petridis 7569465a61 App: Remove the imposed delay before refresh_on_startup runs.
The application is even lazier now and this is no longer an issue.
2018-07-17 20:42:57 +03:00
Jordan Petridis 5913166a13 EpisodeWidget: Avoid Refference Cycles.
When passing an Rc into a gtk callback it causes
the Rust struct to be kept in memory even if the gtk+ wiget
was dropped.

Should have been using Weak refferences all along.
2018-07-17 20:42:45 +03:00
Jordan Petridis 6036562af2 HomeEpisode: Do not initialize the Image until it needs to be drawn. 2018-07-17 16:08:20 +03:00
Jordan Petridis 39e6c258d5 ShowsChild: do not initialize the cover until it needs to be drawn. 2018-07-17 16:04:04 +03:00
Jordan Petridis 1ab0291483 EpisodeWidget: Only initialize the episode once it's first drawn. 2018-07-17 15:57:52 +03:00
Jordan Petridis fe024502d4 EpisodeWidget: Pass the EpisodeModel by value.
This is preparationg for being able to pass the model to
the connect_craw callback directly.
2018-07-16 16:01:32 +03:00
Jordan Petridis 9a76c6428a Merge branch 'hammond-service-launch'
See merge request !42
2018-07-15 22:21:18 +03:00
Jordan Petridis 2d4053c792 cargo fmt 2018-07-15 22:20:45 +03:00
Zander Brown a69254612c Install as a DBus service 2018-07-15 22:20:31 +03:00
Zander Brown 09a14c1270 Delay showing window until ::activate
Bit of a hack as we are still creating the window in ::startup but it's good compromise

Pass the arguments to GApplication so we can be launched as a service
2018-07-15 22:20:28 +03:00
Jordan Petridis b343068805 cargo fmt 2018-07-15 22:17:00 +03:00
Jordan Petridis ecf50dde2b Update .gitignore 2018-07-15 22:16:06 +03:00
Jordan Petridis 008404ffb3 Update Changelog. 2018-07-04 18:40:03 +03:00
Jordan Petridis 2b6cca6bab Version bump. 2018-07-04 17:26:56 +03:00
Jordan Petridis fe968e19c0 Update Changelog. 2018-07-04 16:39:19 +03:00
Jordan Petridis 479498d8be EpisodeMinimal: add lenght as well. 2018-07-03 16:38:08 +03:00
Jordan Petridis af9669acd0 Add a bash script to run the test-suite. 2018-07-01 00:55:57 +03:00
Jordan Petridis d2eb98f859 Update gitignore file. 2018-07-01 00:54:28 +03:00
Jordan Petridis ae11084f48 Merge branch 'db-cleanup' into 'master'
Database cleanup

See merge request World/hammond!41
2018-06-30 21:25:51 +00:00
Jordan Petridis b02b554105 Models: Change the Query suffix to Model.
This is maps better to the MVC naming convention.
2018-06-30 23:02:13 +03:00
Jordan Petridis 2d66ba918a Models: Rename Podcat Models to Show to better match the UI code. 2018-06-30 22:47:58 +03:00
Jordan Petridis 400c0f35f0 PodcastModel: Remove dead code. 2018-06-30 22:20:07 +03:00
Jordan Petridis 5b8b265371 Database: Add diesel_cli config. 2018-06-30 21:47:57 +03:00
Jordan Petridis f3fb27005a Database: Rename the tables to better match the userfacing strings 2018-06-30 21:47:49 +03:00
Jordan Petridis 79bb9bdde8 Database: Drop dead fields/columns.
If and when such featured are going to be implemented, it should be
trivial to re-add them. Till then there is no reason for them to exist.
2018-06-30 18:05:52 +03:00
Jordan Petridis 4b983e401d PlayerWidget: Use weak ref counting for callbacks.
When we pass strong reference to callback closures, it prevents
the object to be dropped even if they gtk Widget was destroyed.
2018-06-27 23:06:27 +03:00
Jordan Petridis 5f2f0a9a57 Headerbar: Hide the hamburger button when not in a global view. 2018-06-27 20:27:16 +03:00
Jordan Petridis 8b2ae6d464 Headerbar: Remove the requirment of a window to construct it. 2018-06-27 20:19:03 +03:00
Jordan Petridis 91aae6a9f5 Headerbar: Factor out the AddPopover widget. 2018-06-27 20:15:42 +03:00
Jordan Petridis b0fc9ef05e Headerbar: Factor out the Update Indicator widget. 2018-06-27 18:51:56 +03:00
Jordan Petridis 5d6fbb6f04 Headerbar: ::new() method now returns Rc<Self>. 2018-06-27 18:20:45 +03:00
Jordan Petridis 301ebdbcd8 Content: ::new() method now returns Rc<Self>. 2018-06-27 18:18:11 +03:00
Jordan Petridis 49bcf46b4f app.rs: Group the gactions declarations. 2018-06-27 17:11:32 +03:00
Jordan Petridis f7263c8ab8 app.rs: Move the action channel to it's own function. 2018-06-27 16:48:35 +03:00
Jordan Petridis c69772131a app.rs: Refactor into an object/struct again. 2018-06-27 16:32:08 +03:00
Jordan Petridis e8c025b898 app.rs: Minor style change. 2018-06-26 23:37:14 +03:00
Jordan Petridis 8fb5c16bce Upgrade crossbeam-channel. 2018-06-26 23:37:06 +03:00
Jordan Petridis f4551ddf3a Update deps. 2018-06-26 23:37:02 +03:00
Jordan Petridis f337488951 Readme: Update the dependencies needed 2018-06-25 17:35:54 +00:00
Jordan Petridis f104f11613 Fix trivial_cast lint warning. 2018-06-24 20:01:27 +03:00
Jordan Petridis 32e8f952fd Even more lints! 2018-06-24 02:21:27 +03:00
Jordan Petridis c7cfc81c6f Merge branch 'embedded-player' into 'master'
Embedded player

Closes #38

See merge request World/hammond!40
2018-06-23 23:03:39 +00:00
Jordan Petridis faeafc329c PlayerWidget: Tweak rewind on pause behavior.
Only rewind on pause if the stream position is passed a certain point.
Else it can feel a bit weird if you just started the stream and it
immediatly rewinds.
2018-06-23 22:45:21 +00:00
Jordan Petridis eeef0d13ff PlayerWidget: Use the Gtk Main Context for the gst_player as well.
There is no longer a need for sending stuff to the main-thread `Action`
channel anymore thanks to this.
2018-06-23 22:45:21 +00:00
Jordan Petridis 0686fca3b0 h-gtk: Increase the polling rate of the main thread channel.
When dragging the PlayerWidget.timer.slider widget PlayerDurationChanged
messaged pile up in the channel and get out of sync with the
slider.connect_value_changed() signal.
2018-06-23 22:45:21 +00:00
Jordan Petridis 79b425326b Update Changelog. 2018-06-23 22:45:20 +00:00
Jordan Petridis 2d879b9604 PlayerRate: Change the container widget to GtkBox and add padding. 2018-06-23 22:45:20 +00:00
Jordan Petridis 38eb14b013 Delete commented out code. 2018-06-23 22:45:19 +00:00
Jordan Petridis ff2f43766e PlayerWidget: Add a widget to change the playback speed of the stream.
Only 3 options are offered currently since the design of the feature
is still in progress and this is only a throw a away prototype.
2018-06-23 22:45:19 +00:00
Jordan Petridis 593d66ea54 EpisodeWidget: Mark an episode as played when the play button is hit.
Ideally episodes would be marked as played only when they have
passed a cerain point in their duration, but till thats ready
we should keep marking them.
2018-06-23 22:45:19 +00:00
Jordan Petridis ee8cbbf7ef PlayerWidget: Delete commented out stuff. 2018-06-23 22:45:19 +00:00
Jordan Petridis 474cb49d2c PlayerInfo: Increase the size of the cover. 2018-06-23 22:45:19 +00:00
Jordan Petridis 590f815dc0 PlayerInfo: Limit label widths and add tooltips. 2018-06-23 22:45:18 +00:00
Jordan Petridis a93d5246d2 PlayerInfo: Swap bold properties of the labels. 2018-06-23 22:45:18 +00:00
Jordan Petridis 3e2ab8e7ee PlayerExt: Add a stop method.
Also connect on the gst_player::Player::connect_on_stream_end method.

The main reason for this is to be able to reset the slider bar to 0
upon a stream ends.
2018-06-23 22:45:18 +00:00
Jordan Petridis a83270699f PlayerWidget: refactor seek method. 2018-06-23 22:45:17 +00:00
Jordan Petridis 745afb32a3 PlayerWidget: Rewind on pause. 2018-06-23 22:45:17 +00:00
Jordan Petridis 2fcb8d915d PlayerWidget: Remove an .expect() occurrence. 2018-06-23 22:45:17 +00:00
Jordan Petridis a596b62a5f PlayerWidget: Tweak gst_player config. 2018-06-23 22:45:16 +00:00
Jordan Petridis 55b1504aab PlayerTimes: Display human-friendly values. 2018-06-23 22:45:16 +00:00
Jordan Petridis c42822669b PlayerTimes: Replace unnecessary Arc with Rc. 2018-06-23 22:45:16 +00:00
Jordan Petridis b58d28c723 PlayerTimes: create wrapper struct of gst::ClockTime.
Now on_duration_changed requires a `Duration` type and
on_position_updated requires a `Position` type as oppose to
both accepting `ClockTime` as their argument.
2018-06-23 22:45:16 +00:00
Jordan Petridis a6a34d8246 PlayerWidget: Group the button connect_clicked methods. 2018-06-23 22:45:16 +00:00
Jordan Petridis 0080399db2 PlayerWidget: Move on_duration_change and on_postion_updated methods.
Previously they depended on the player/pipeline to get the ClockTime
values, and only `PlayerWidget` had access to the `gst_player::Player`
object.

Now that it uses the gst_player methods instead of the raw pipeline
methods to get the ClockTime values it no longer needs access to the whole
PlayerWidget object.
2018-06-23 22:45:15 +00:00
Jordan Petridis 50b480ee23 PlayerWidget: Refactor the position_changed/updated callback.
It now uses gst_player::Player::connect_position_updated callback
to send, cross threads, the `position` value to the gtk main loop
which then updates the widget.
2018-06-23 22:45:15 +00:00
Jordan Petridis da467b7837 PlayerWidget::seek handle the case where the offset might be none. 2018-06-23 22:45:15 +00:00
Jordan Petridis 70914b6c3e PlayerWigdet: Refactor the way the duration label is updated.
This now connect's directly to gst_player::Player::connect_duration_changed
method.

The method then sends a cross-thread msg to the Action channel in the main loop that
then updates the widget.
2018-06-23 22:45:14 +00:00
Jordan Petridis 6c3fbfe0ca PlayerWiget: refactor the seekbar connect signal. 2018-06-23 22:45:14 +00:00
Jordan Petridis 8e4b705e60 PlayerWidget: Log the gst warnings. 2018-06-23 22:45:14 +00:00
Jordan Petridis 48d80d3194 PlayerWidget: Remove unused vars an Enum. 2018-06-23 22:45:14 +00:00
Jordan Petridis 38768c777d PlayerWidget: Connect the fast-forward and rewind buttons, sort of.
There appears to be a bug where it seeks 17 seconds instead of 10.
2018-06-23 22:45:13 +00:00
Jordan Petridis 1daa841f31 PlayerWidget: Connect to the errors callback. 2018-06-23 22:45:13 +00:00
Jordan Petridis a9f81d0ad3 PlayerWidget: Refactor the timers callbacks.
Should use the gst_player::Player callbacks instead but they require
the Send Trait which means we would need to use SendCell and that's
not something I am going to deal with right now.
2018-06-23 22:45:13 +00:00
Jordan Petridis 76720424ab PlayerWidget: Set a custom config for the gst Player. 2018-06-23 22:45:13 +00:00
Jordan Petridis a7b639a66b PlayerWidget: Wire the PlayerTimes labels and scale.
Adapted from gstreamer basic-tutorial-5.
https://gstreamer.freedesktop.org/documentation/tutorials/basic/toolkit-integration.html
2018-06-23 22:45:12 +00:00
Jordan Petridis 1b78d221b6 PlayerWidget: Wire the play and pause buttons and add style classes to the Info Labels.
This also includes the yak shaving of a ::new and ::inti methods.
2018-06-23 22:45:12 +00:00
Jordan Petridis ac7b1a3c66 CI: disable debian builds fow now.
Debian stable provides gst 1.10 but the gst-rs bindings requiere
v 1.12 to build.

I will make custom images Soon™
2018-06-23 22:45:12 +00:00
Jordan Petridis 039c3182aa h-gtk: Remove unused .ui file. 2018-06-23 22:45:12 +00:00
Jordan Petridis 55d94b1844 CI: Add gstreamer as a dep for the debian build. 2018-06-23 22:45:11 +00:00
Jordan Petridis 3baa69b43b cargo fmt 2018-06-23 22:45:11 +00:00
Jordan Petridis 5f92df97e6 PlayerWidget: Wire the widget to the GUI.
This commit also removes the majority of the playback widget,
though most of it's code will make it to the PlayerWidget once
it starts to get wired to the gtreamer_plaer::Player.
2018-06-23 22:45:11 +00:00
Jordan Petridis 47f297c495 PlayerWidget: Intial draft of the the PlayerExt trait. 2018-06-23 22:45:11 +00:00
Jordan Petridis 58f09ba150 h-gtk: Bind the new player widget to code. 2018-06-23 22:45:10 +00:00
Jordan Petridis 1142948945 Rework the player widget. 2018-06-23 22:45:10 +00:00
Zander Brown 9528160b03 Start hooking things up
Still doesn't accept input
2018-06-23 22:45:10 +00:00
Zander Brown 4afdc54914 Initial playback control area
(not plumbed in)
2018-06-23 22:45:10 +00:00
Zander Brown 09973a6a56 Initial playback
... and not a lot more. Hit play and the podcast will play, press play on something else and that will play instead
2018-06-23 22:45:09 +00:00
Jordan Petridis f56bf3afef Enable more rustc lints 2018-06-23 21:02:19 +03:00
Jordan Petridis c4c7bbf46b Content: Change the user-facing string of the home stack. 2018-06-23 16:11:00 +03:00
Jordan Petridis 7fdd374911 Update the gitlab bug issue template 2018-06-23 13:01:34 +00:00
Jordan Petridis fb9ad9870d Update the gitlab feature issue template. 2018-06-23 12:59:35 +00:00
Jordan Petridis bbabc6f5e9 h-gtk: Add resources/ to the cargo check list. 2018-06-11 14:23:29 +03:00
Jordan Petridis 2060579bb4 gitlab templates: Fix markdown formatting. 2018-06-09 16:16:57 +03:00
Jordan Petridis b0f0940605 Merge branch 'master' into 'master'
Our GActions don't need to be in the app namespace

See merge request World/hammond!39
2018-06-07 17:37:12 +00:00
Zander Brown ee23df176d Our GActions don't need to be in the app namespace 2018-06-07 18:06:36 +01:00
Jordan Petridis d53865d81b Update deps. 2018-06-07 16:14:40 +03:00
Jordan Petridis 7becfd8adb Commit Updated Cargo.lock.
This was part of a9feed56fe but
forgot to commit it.
2018-06-07 15:47:41 +03:00
Jordan Petridis a9feed56fe Replace html2pango with html2text. 2018-06-06 15:28:44 +03:00
159 changed files with 11868 additions and 5403 deletions
+13 -3
View File
@@ -1,9 +1,19 @@
target/
**/*.rs.bk
Cargo.lock
.vscode
*.ui~
resources.gresource
_build
_build/
build/
vendor/
.criterion
.criterion/
org.gnome.*.json~
podcasts-gtk/po/gnome-podcasts.pot
# scripts/test.sh
target_*/
# flatpak-builder stuff
.flatpak-builder/
app/
repo/
+38 -86
View File
@@ -4,90 +4,56 @@ stages:
- review
variables:
BUNDLE: "hammond-dev.flatpak"
.cargo_cache_template: &cargo_cache
cache:
# JOB_NAME - Each job will have it's own cache
# COMMIT_REF_SLUG = Lowercase name of the branch
# ^ Keep diffrerent caches for each branch
key: "$CI_JOB_NAME"
paths:
- target/
- .cargo_cache/
.cargo_test_template: &cargo_test
stage: test
variables:
RUSTFLAGS: "--cfg rayon_unstable"
RUST_BACKTRACE: "FULL"
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
- mkdir -p .cargo_cache
# Only stuff inside the repo directory can be cached
# Override the CARGO_HOME variable to force it location
- export CARGO_HOME="${PWD}/.cargo_cache"
script:
- rustc -Vv && cargo -Vv
# Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo build
- cargo test -- --test-threads=1
- cargo test -- --test-threads=1 --ignored
<<: *cargo_cache
rust:stable:
# https://hub.docker.com/_/rust/
image: "rust"
<<: *cargo_test
rust:nightly:
# https://hub.docker.com/r/rustlang/rust/
image: "rustlang/rust:nightly"
<<: *cargo_test
only:
- schedule
- web
BUNDLE: "org.gnome.Podcasts.${CI_COMMIT_SHA}.flatpak"
flatpak:
image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.28
stage: test
image: "registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:master"
stage: "test"
variables:
MANIFEST_PATH: "org.gnome.PodcastsDevel.json"
FLATPAK_MODULE: "gnome-podcasts"
CONFIGURE_ARGS: "-Dprofile=development"
DBUS_ID: "org.gnome.PodcastsDevel"
script:
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
# https://gitlab.gnome.org/World/hammond/issues/55
- flatpak-builder --stop-at=${FLATPAK_MODULE} app ${MANIFEST_PATH}
# https://gitlab.gnome.org/World/podcasts/issues/55
# Force regeneration of gresources regardless of artifacts chage
- flatpak-builder --run app org.gnome.Hammond.json glib-compile-resources --sourcedir=hammond-gtk/resources/ hammond-gtk/resources/resources.xml
- flatpak-builder --run app ${MANIFEST_PATH} glib-compile-resources --sourcedir=podcasts-gtk/resources/ podcasts-gtk/resources/resources.xml
# Build the flatpak repo
- flatpak-builder --run app org.gnome.Hammond.json meson --prefix=/app --libdir=/app/lib _build
- flatpak-builder --run app org.gnome.Hammond.json ninja -C _build install
- flatpak-builder --finish-only app org.gnome.Hammond.json
- flatpak build-export repo app
- flatpak-builder --run app ${MANIFEST_PATH} meson --prefix=/app ${CONFIGURE_ARGS} _build
- flatpak-builder --run app ${MANIFEST_PATH} ninja -C _build install
# Run the tests
- |
xvfb-run -a -s "-screen 0 1024x768x24" \
flatpak-builder --run \
--env=APP_ID="org.gnome.PodcastsDevel" \
--env=LOCALEDIR="./podcasts-gtk/po" \
--env=VERSION="0.0.0" \
--env=RUSTFLAGS="--cfg rayon_unstable" \
--env=CARGO_HOME="target/cargo-home" \
--env=CARGO_TARGET_DIR="target_test/" \
app ${MANIFEST_PATH} \
cargo test -j 1 -- --test-threads=1
# Create a flatpak bundle
- flatpak build-bundle repo ${BUNDLE} org.gnome.Hammond
# Run the tests
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1 --ignored
- flatpak-builder --finish-only app ${MANIFEST_PATH}
- flatpak build-export repo app
- flatpak build-bundle repo ${BUNDLE} ${DBUS_ID}
artifacts:
paths:
- $BUNDLE
expire_in: 30 days
expire_in: 2 days
cache:
# JOB_NAME - Each job will have it's own cache
# COMMIT_REF_SLUG = Lowercase name of the branch
# ^ Keep diffrerent caches for each branch
key: "$CI_JOB_NAME"
key: "flatpak"
paths:
- .flatpak-builder/cache/
- target/
- target_test/
review:
stage: review
@@ -104,7 +70,8 @@ review:
url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
on_stop: stop_review
except:
- master@World/hammond
- master@World/podcasts
- tags
stop_review:
stage: review
@@ -115,7 +82,8 @@ stop_review:
name: review/$CI_COMMIT_REF_NAME
action: stop
except:
- master@World/hammond
- master@World/podcasts
- tags
# Configure and run rustfmt on nightly
# Exits and builds fails if on bad format
@@ -125,20 +93,4 @@ rustfmt:
script:
- rustc -Vv && cargo -Vv
- cargo fmt --version
- cargo fmt --all -- --check
# Configure and run clippy on nightly
# Only fails on errors atm.
clippy:
image: "registry.gitlab.gnome.org/alatiera/hammond-container-images/clippy:nightly"
stage: lint
variables:
RUSTFLAGS: "--cfg rayon_unstable"
CARGO_HOME: ".cargo_cache"
script:
- rustc --version && cargo --version
- cargo clippy --version
# Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo clippy --all
<<: *cargo_cache
- cargo fmt --all -- --check
+1 -1
View File
@@ -18,7 +18,7 @@ Some common cases might be:
Steps to reproduce:
1. Open Hammond
1. Open GNOME Podcasts
2. Do an action
3. ...
+37 -6
View File
@@ -1,9 +1,40 @@
Detailed description of the issue. Put as much information as you can, potentially
with images showing the issue.
# Steps to reproduce
<!--
Explain in detail the steps on how the issue can be reproduced.
-->
1.
2.
3.
Steps to reproduce:
Reproducible in:
<!--
Please test if the issue was already fixed in the unstable version of the app.
For that, follow these steps:
1. Make sure Flatpak is installed or install it following these steps https://flatpak.org/setup
2. Install the unstable version of the app following, flatpak bundles can be found in the CI artifacts.
1. Open Hammond
2. Do an action
3. ...
If these steps failed, write in 'Other' the distribution youre using and
the version of the app.
-->
- Flatpak unstable: (yes or no) <!-- Write "yes" or "no" after the semicolon. -->
- Other:
# Current behavior
<!-- Describe the current behavior. -->
# Expected behavior
<!-- Describe the expected behavior. -->
# Additional information
<!--
Provide more information that could be relevant.
If the issue is a crash, provide a stack trace following the steps in:
https://wiki.gnome.org/Community/GettingInTouch/Bugzilla/GettingTraces
-->
<!-- Ignore the text under this line. -->
/label ~"Bug"
+33 -34
View File
@@ -1,42 +1,41 @@
Current problems
<!--
What are the problems that the current project has?
# Current problems
<!--
What are the problems that the current project has?
For example:
* User cannot use the keyboard to perform most common actions
or
* User cannot see documents from cloud services
-->
For example:
* User cannot use the keyboard to perform most common actions
or
* User cannot see documents from cloud services
-->
# Goals & use cases
<!--
What are the use cases that this proposal will cover? What are the end goals?
# Goals & use cases
<!--
What are the use cases that this proposal will cover? What are the end goals?
For example:
* User needs to share a file with their friends.
or
* It should be easy to edit a picture within the app.
-->
For example:
* User needs to share a file with their friends.
or
* It should be easy to edit a picture within the app.
-->
# Requirements
<!--
What does the solution needs to ensure for being succesful?
# Requirements
<!--
What does the solution needs to ensure for being succesful?
For example:
* Work on small form factors and touch
or
* Use the Meson build system and integrate with it
-->
For example:
* Work on small form factors and touch
or
* Use the Meson build system and integrate with it
-->
# Relevant art
<!--
Is there any product that has implemented something similar? Put links to other
projects, pictures, links to other code, etc.
-->
# Relevant art
<!--
Is there any product that has implemented something similar? Put links to other
projects, pictures, links to other code, etc.
-->
# Proposal & plan
<!-- What's the solution and how should be achieved? It can be split in smaller
tasks of minimum change, so they can be delivered across several releases. -->
/label ~"Epic"
# Proposal & plan
<!-- What's the solution and how should be achieved? It can be split in smaller
tasks of minimum change, so they can be delivered across several releases. -->
/label ~"Epic"
+17 -10
View File
@@ -1,17 +1,24 @@
Detailed description of the feature. Put as much information as you can.
### Use cases
<!--
Describe what problem(s) the user is experiencing and that this request
is trying to solve.
-->
Proposed Mockups:
(Add mockups of the proposed feature)
### Desired behavior
<!-- Describe the desired functionality. -->
## Design Tasks
* [ ] design tasks
### Benefits of the solution
<!-- List the possible benefits of the solution and how it fits in the project. -->
## Development Tasks
* [ ] development tasks
### Possible drawbacks
<!--
Describe possible drawbacks of the feature and list how it could affect
the project i.e. UI discoverability, complexity, impact in more or less
number of users, etc.
-->
## QA Tasks
* [ ] qa (quality assurance) tasks
<!-- Ignore the text under this line. -->
/label ~"Feature"
+1
View File
@@ -0,0 +1 @@
### Please attach a relevant issue to this MR, if this doesn't exist please create one.
+115 -17
View File
@@ -6,18 +6,116 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
### Added:
- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33)
### Changed:
- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33)
- The `EpisdeWidget` was refactored. [!38](https://gitlab.gnome.org/World/hammond/merge_requests/38)
- `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012
### Fixed:
- Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/hammond/merge_requests/37)
### Removed:
### Translations:
## [0.4.5] - 2018-08-31
### Added:
- [OARS](https://hughsie.github.io/oars/) Tags where added for compatibility with Store clients b0c94dd9
- Daniel added support for Translations !46
- Svitozar Cherepii(@svito) created a [wiki page](https://wiki.gnome.org/Apps/Podcasts) 70e79e50
- Libhandy was added as a dependancy #70
- Development builds can now be installed in parallel with stable builds !64
### Changed:
- The update indication was moved to an In-App notification #72
- The app icon's accent color was changed from orange to red 0dfb4859
- The stack switcher in the Headerbar is now insesitive on Empty Views !63
### Fixed:
- Improved handling of HTTP redirections #64 !61 !62
- Fixed a major performance regression when loading show covers !67
- More refference cycles have been fixed !59
- OPML import dialog now exits properly and no longer keeps the application from shuting down !65
- Update action is disabled if there isn't something to update #71
### Translations:
- Added Finish 93696026
- Added Polish 1bd6efc0
- Added Turkish 73929f2d
- Added Spanish !46
- Added German 6b6c390c
- Added Galician 0060a634
- Added Indonesian ded0224f
- Added Korean 36f16963
## [0.4.4] - 2018-07-31
### Changed:
- `SendCell` crate was replaced with `Fragile`. (Jorda Petridis) 838320785ebbea94e009698b473495cfec076f54
- Update dependancies (Jorda Petridis) 91bea8551998b16e44e5358fdd43c53422bcc6f3
### Fixed:
- Fix more refference cycles. (Jorda Petridis) 3496df24f8d8bfa8c8a53d8f00262d42ee39b41c
- Actually fix cargo-vendor (Jorda Petridis)
## [0.4.3] - 2018-07-27
### Fixed:
- Fix the cargo vendor config for the tarball releash script. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
## [0.4.2] - 2018-07-27
### Changed:
- Minimum size requested by the Views. (Jorda Petridis) 7c96152f3f53f271247230dccf1c9cd5947b685f
### Fixed:
- Screenshot metadata in appstream data. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
## [0.4.1] - 2018-07-26
### Added:
- Custom icons for the fast-forward and rewind actions in the Player were added. (Tobias Bernard) e77000076b3d78b8625f4c7ef367376d0130ece6
- Hicolor and symbolic icons for the Application. (Tobias Bernard and Sam Hewitt) edae1b04801dba9d91d5d4145db79b287f0eec2c
- Basic prefferences dialog (Zander Brown). [34](https://gitlab.gnome.org/World/podcasts/merge_requests/34)
- Dbus service preperation. Not used till the MPRIS2 integration has landed. (Zander Brown) [42](https://gitlab.gnome.org/World/podcasts/merge_requests/42)
- Episodes and Images will only get drawn when needed. Big Performance impact. (Jordan Petridis) [43](https://gitlab.gnome.org/World/podcasts/merge_requests/43)
### Changed:
- The `ShowWidget` control button were moved to a secondary menu in the Headerbar. (Jordan Petridis) 536805791e336a3e112799be554706bb804d2bef
- EmptyView layout improvements. (Jorda Petridis) 3c3d6c1e7f15b88308a9054b15a6ca0d8fa233ce 518ea9c8b57885c44bda9c418b19fef26ae0e55d
- Improved the `AddButton` behavior. (Jorda Petridis) 67ab54f8203f19aad198dc49e935127d25432b41
### Fixed:
- A couple reffence cycles where fixed. (Jorda Petridis)
### Removed:
- The delay between the application startup and the `update_on_startup` action. (Jorda Petridis) 7569465a612ee5ef84d0e58f4e1010c8d14080d4
## [0.4.0] - 2018-07-04
### Added:
- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown)
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
### Changed:
- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown)
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
- The `EpisdeWidget` was refactored.
[!38](https://gitlab.gnome.org/World/podcasts/merge_requests/38)
- `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012
- An embeded audio player was implemented!
[!40](https://gitlab.gnome.org/World/podcasts/merge_requests/40)
- Various Database changes.
[!41](https://gitlab.gnome.org/World/podcasts/merge_requests/41)
### Fixed:
- Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/podcasts/merge_requests/37)
## [0.3.4] - 2018-05-20
### Fixed:
- Flatpak can now access the Home folder. This fixes the OPML import feature from
@@ -42,9 +140,9 @@ not being able to access any file.
### Changed:
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
[#7](https://gitlab.gnome.org/World/hammond/issues/7)
[#7](https://gitlab.gnome.org/World/podcasts/issues/7)
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
[#25](https://gitlab.gnome.org/World/hammond/issues/25)
[#25](https://gitlab.gnome.org/World/podcasts/issues/25)
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
- `EpisdeWidget`s are now loaded asynchronously accross views.
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
@@ -52,7 +150,7 @@ not being able to access any file.
- `ShowWidget` Description is inside a scrolled window now
### Fixed:
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/hammond/issues/57)
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/podcasts/issues/57)
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
### Removed:
@@ -61,27 +159,27 @@ not being able to access any file.
## [0.3.1] - 2018-03-28
### Added:
- Ability to mark all episodes of a Show as watched.
[#47](https://gitlab.gnome.org/World/hammond/issues/47)
[#47](https://gitlab.gnome.org/World/podcasts/issues/47)
- Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
[#49](https://gitlab.gnome.org/World/hammond/issues/49)
[#49](https://gitlab.gnome.org/World/podcasts/issues/49)
- Hammond now remembers the window size and position. (Rowan Lewis)
[#50](https://gitlab.gnome.org/World/hammond/issues/50)
[#50](https://gitlab.gnome.org/World/podcasts/issues/50)
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
[!22](https://gitlab.gnome.org/World/hammond/merge_requests/22) [!23](https://gitlab.gnome.org/World/hammond/merge_requests/23)
[!22](https://gitlab.gnome.org/World/podcasts/merge_requests/22) [!23](https://gitlab.gnome.org/World/podcasts/merge_requests/23)
- Shows without episodes now display an empty message similar to EmptyView.
[#44](https://gitlab.gnome.org/World/hammond/issues/44)
[#44](https://gitlab.gnome.org/World/podcasts/issues/44)
### Changed:
- EpisdeWidget has been reimplemented as a compile time state machine.
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18)
[!18](https://gitlab.gnome.org/World/podcasts/merge_requests/18)
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
[#35](https://gitlab.gnome.org/World/hammond/issues/35)
[#35](https://gitlab.gnome.org/World/podcasts/issues/35)
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
[#53](https://gitlab.gnome.org/World/hammond/issues/53)
[#53](https://gitlab.gnome.org/World/podcasts/issues/53)
### Fixed:
- Double border aroun the main window was fixed. (Rowan Lewis)
[#52](https://gitlab.gnome.org/World/hammond/issues/52)
[#52](https://gitlab.gnome.org/World/podcasts/issues/52)
## [0.3.0] - 2018-02-11
- Tobias Bernard Redesigned the whole Gtk+ client.
+8 -8
View File
@@ -1,18 +1,18 @@
## Contributing to Hammond
## Contributing to GNOME Podcasts
Thank you for looking in this file!
When contributing to the development of Hammond, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
When contributing to the development of GNOME Podcasts, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
If you have any questions regarding the use or development of Hammond,
If you have any questions regarding the use or development of GNOME Podcasts,
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOfConduc), please follow it in all your interactions with the project.
## Source repository
Hammond's main source repository is at gitlab.gnome.org. You can view
the web interface [here](https://gitlab.gnome.org/World/hammond)
GNOME Podcasts's main source repository is at gitlab.gnome.org. You can view
the web interface [here](https://gitlab.gnome.org/World/podcasts)
Development happens in the master branch.
@@ -50,8 +50,8 @@ In order to run the test suite use the following: `cargo test -- --test-threads=
# Issues, issues and more issues!
There are many ways you can contribute to Hammond, and all of them involve creating issues
in [Hammond issue tracker](https://gitlab.gnome.org/World/hammond/issues). This is the entry point for your contribution.
There are many ways you can contribute to GNOME Podcasts, and all of them involve creating issues
in [GNOME Podcasts issue tracker](https://gitlab.gnome.org/World/podcasts/issues). This is the entry point for your contribution.
To create an effective and high quality ticket, try to put the following information on your
ticket:
@@ -74,7 +74,7 @@ If it's an issue, add the steps to reproduce like this:
Steps to reproduce:
1. Open Hammond
1. Open GNOME Podcasts
2. Do an Action
3. ...
Generated
+1133 -815
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,8 +1,8 @@
[workspace]
members = [
"hammond-data",
"hammond-downloader",
"hammond-gtk"
"podcasts-data",
"podcasts-downloader",
"podcasts-gtk"
]
[profile.release]
+2 -2
View File
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Hammond
GNOME Podcasts
Copyright (C) 2017 Jordan Petridis
This program is free software: you can redistribute it and/or modify
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Hammond Copyright (C) 2017 Jordan Petridis
GNOME Podcasts Copyright (C) 2017 Jordan Petridis
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
+60 -62
View File
@@ -1,66 +1,63 @@
# Hammond
# GNOME Podcasts
### A Podcast Client for GNOME written in Rust.
### A Podcast application for GNOME.
Listen to your favorite podcasts, right from your desktop.
![episdes_view](./screenshots/episodes_view.png)
![episdes_view](./screenshots/home_view.png)
![shows_view](./screenshots/shows_view.png)
![show_widget](./screenshots/show_widget.png)
## Available on Flathub
[![Get it from Flathub!](https://flathub.org/assets/badges/flathub-badge-en.svg)](https://flathub.org/apps/details/org.gnome.Podcasts)
## Quick start
Hammond can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
GNOME Podcasts can be built and run with [Gnome Builder][builder] >= 3.28.
Just clone the repo and hit the run button!
Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
You can get Builder from [here][get_builder].
## Broken Feeds
Found a feed that does not work in Hammond?
Please [open an issue](https://gitlab.gnome.org/World/hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
Found a feed that does not work in GNOME Podcasts?
Please [open an issue][new_issue] and choose the `BrokenFeed` template so we will know and fix it!
## Getting in Touch
If you have any questions regarding the use or development of Hammond,
want to discuss design or simply hang out, please join us in `#hammond` on
[irc.gnome.org.][irc] or [matrix][matrix].
Note:
There isn't much documentation yet, so you will probably have question about parts of the Code.
If you have any questions regarding the use or development of GNOME Podcasts,
want to discuss design or simply hang out, please join us on our [irc][irc] or [matrix][matrix] channel.
## Building
### Flatpak
Flatpak is the reccomended way of building and installing Hammond.
Flatpak is the recommended way of building and installing GNOME Podcasts.
Here are the depencancies you will need.
#### Building a Flatpak
```sh
# Add flathub and the gnome-nightly repo
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists gnome-nightly https://sdk.gnome.org/gnome-nightly.flatpakrepo
Download the `org.gnome.Hammond.json` flatpak manifest from this repo.
# Install the gnome-nightly Sdk and Platform runtime
flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform
```bash
# Add flathub repo
flatpak --user remote-add flathub --if-not-exists https://dl.flathub.org/repo/flathub.flatpakrepo
# Add the gnome-nightly repo
flatpak --user remote-add gnome-nightly --if-not-exists https://sdk.gnome.org/gnome-nightly.flatpakrepo
# Install the gnome-nightly Sdk and Platform runtim
flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
# Install the required rust-stable extension from flathub
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//18.08
```
To install the resulting flatpak you can do:
```bash
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
flatpak install --user --bundle hammond.flatpak
flatpak-builder --user --install --force-clean --repo=repo podcasts org.gnome.Podcasts.json
```
### Building from soure
### Building from source
```sh
git clone https://gitlab.gnome.org/World/hammond.git
cd hammond/
git clone https://gitlab.gnome.org/World/podcasts.git
cd gnome-podcasts/
meson --prefix=/usr build
ninja -C build
sudo ninja -C build install
@@ -68,41 +65,33 @@ sudo ninja -C build install
#### Dependencies
* Rust stable 1.26 or later along with cargo.
* Rust stable 1.27 or later along with cargo.
* Gtk+ 3.22 or later
* Gstreamer 1.12 or later
* libhandy
* Meson
* A network connection
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
**Debian/Ubuntu**
```sh
apt-get update -yqq
apt-get install -yqq --no-install-recommends build-essential
apt-get install -yqq --no-install-recommends rustc cargo libgtk-3-dev meson
```
**Fedora**
```sh
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson
```
If you happen to build it on other distributions please let me know the names
of the corresponding libraries. Feel free to open a MR or an Issue to note it.
## Contributing
There alot of thins yet to be done.
There are a lot of things yet to be done.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/World/hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
You can start by taking a look at [Issues][issues] or by opening a [New issue][new_issue].
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
[contribution-guidelines]: https://gitlab.gnome.org/World/hammond/blob/master/CONTRIBUTING.md
[contribution-guidelines]: https://gitlab.gnome.org/World/podcasts/blob/master/CONTRIBUTING.md
### Translations
Translation of this project takes place on the GNOME translation platform,
[Damned Lies](https://l10n.gnome.org/module/podcasts). For further
information on how to join a language team, or even to create one, please see
[GNOME Translation Project wiki page](https://wiki.gnome.org/TranslationProject).
## Overview
@@ -110,15 +99,15 @@ There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source c
```sh
$ tree -d
├── screenshots # png's used in the README.md
├── hammond-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
├── podcasts-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
│   ├── migrations # Diesel SQL migrations.
│   │   └── ...
│   ├── src
│   └── tests
│   └── feeds # Raw RSS Feeds used for tests.
├── hammond-downloader # Really basic, Really crappy downloader.
├── podcasts-downloader # Really basic, Really crappy downloader.
│   └── src
├── hammond-gtk # The Gtk+ Client
├── podcasts-gtk # The Gtk+ Client
│   ├── resources # GResources folder
│   │   └── gtk # Contains the glade.ui files.
│   └── src
@@ -128,18 +117,27 @@ $ tree -d
## A note about the project's name
The project was named after Allan Moore's character [Evey Hammond](https://en.wikipedia.org/wiki/Evey_Hammond) from the graphic novel V for Vendetta.
It has nothing to do with the horrible headlines on the news.
The project used to be called Hammond, after Allan Moore's character [Evey Hammond][hammond] from the graphic novel V for Vendetta.
It was renamed to GNOME Podcasts on 2018/07/24 shortly before its first public release.
## Acknowledgments
Hammond's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Design/Apps/Music) and [Vocal](http://vocalproject.net/).
GNOME Podcasts's design is heavily inspired by [GNOME Music][music] and [Vocal][vocal].
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
We also copied some elements from [GNOME News][news].
And almost the entirety of the build system is copied from the [Fractal](https://gitlab.gnome.org/danigm/fractal) project.
And almost the entirety of the build system is copied from the [Fractal][fractal] project.
[vendor]: https://github.com/alexcrichton/cargo-vendor
[irc]: irc://irc.gnome.org/#hammond
[matrix]: https://matrix.to/#/#hammond:matrix.org
[matrix]: https://matrix.to/#/#gnome-podcasts:matrix.org
[flatpak_setup]: https://flatpak.org/setup/
[music]: https://wiki.gnome.org/Design/Apps/Music
[vocal]: http://vocalproject.net/
[news]: https://wiki.gnome.org/Design/Apps/Potential/News
[fractal]: https://gitlab.gnome.org/World/fractal
[hammond]: https://en.wikipedia.org/wiki/Evey_Hammond
[issues]: https://gitlab.gnome.org/World/podcasts/issues
[new_issue]: https://gitlab.gnome.org/World/podcasts/issues/new
[builder]: https://wiki.gnome.org/Apps/Builder
[get_builder]: https://wiki.gnome.org/Apps/Builder/Downloads
-104
View File
@@ -1,104 +0,0 @@
#![allow(unused)]
#[macro_use]
extern crate criterion;
use criterion::Criterion;
// extern crate futures;
extern crate hammond_data;
extern crate hyper;
extern crate hyper_tls;
extern crate rand;
extern crate tokio_core;
// extern crate rayon;
extern crate rss;
// use rayon::prelude::*;
// use futures::future::*;
use tokio_core::reactor::Core;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
use hammond_data::FeedBuilder;
use hammond_data::Source;
// use hammond_data::errors::*;
use std::io::BufReader;
// RSS feeds
const INTERCEPTED: &[u8] = include_bytes!("../tests/feeds/2018-01-20-Intercepted.xml");
const INTERCEPTED_URL: &str = "https://web.archive.org/web/20180120083840if_/https://feeds.\
feedburner.com/InterceptedWithJeremyScahill";
const UNPLUGGED: &[u8] = include_bytes!("../tests/feeds/2018-01-20-LinuxUnplugged.xml");
const UNPLUGGED_URL: &str =
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.com/linuxunplugged";
const TIPOFF: &[u8] = include_bytes!("../tests/feeds/2018-01-20-TheTipOff.xml");
const TIPOFF_URL: &str =
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
// This feed has HUGE descripion and summary fields which can be very
// very expensive to parse.
const CODE: &[u8] = include_bytes!("../tests/feeds/2018-01-20-GreaterThanCode.xml");
const CODE_URL: &str =
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.com/feed/podcast";
// Relative small feed
const STARS: &[u8] = include_bytes!("../tests/feeds/2018-01-20-StealTheStars.xml");
const STARS_URL: &str =
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
static FEEDS: &[(&[u8], &str)] = &[
(INTERCEPTED, INTERCEPTED_URL),
(UNPLUGGED, UNPLUGGED_URL),
(TIPOFF, TIPOFF_URL),
(CODE, CODE_URL),
(STARS, STARS_URL),
];
fn bench_index_large_feed(c: &mut Criterion) {
truncate_db().unwrap();
let url = "https://www.greaterthancode.com/feed/podcast";
let mut core = Core::new().unwrap();
c.bench_function("index_large_feed", move |b| {
b.iter(|| {
let s = Source::from_url(url).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(CODE)).unwrap();
let feed = FeedBuilder::default()
.channel(chan)
.source_id(s.id())
.build()
.unwrap();
core.run(feed.index()).unwrap();
})
});
truncate_db().unwrap();
}
fn bench_index_small_feed(c: &mut Criterion) {
truncate_db().unwrap();
let url = "https://rss.art19.com/steal-the-stars";
let mut core = Core::new().unwrap();
c.bench_function("index_small_feed", move |b| {
b.iter(|| {
let s = Source::from_url(url).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(STARS)).unwrap();
let feed = FeedBuilder::default()
.channel(chan)
.source_id(s.id())
.build()
.unwrap();
core.run(feed.index()).unwrap();
})
});
truncate_db().unwrap();
}
criterion_group!(benches, bench_index_large_feed, bench_index_small_feed);
criterion_main!(benches);
-117
View File
@@ -1,117 +0,0 @@
#![recursion_limit = "1024"]
#![allow(unknown_lints)]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
#![cfg_attr(
feature = "clippy",
warn(
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut,
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else,
items_after_statements, used_underscore_binding
)
)]
#![warn(
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements,
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns,
private_no_mangle_statics, safe_extern_statics, unconditional_recursion,
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true,
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
unused_extern_crates, unused
)]
#![deny(warnings)]
//! FIXME: Docs
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[cfg(test)]
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate derive_builder;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
// #[macro_use]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate ammonia;
extern crate chrono;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate native_tls;
extern crate num_cpus;
extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate tokio_core;
extern crate url;
extern crate xdg;
extern crate xml;
pub mod database;
#[allow(missing_docs)]
pub mod dbqueries;
#[allow(missing_docs)]
pub mod errors;
mod feed;
pub(crate) mod models;
pub mod opml;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils;
pub use feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
// Set the user agent, See #53 for more
// Keep this in sync with Tor-browser releases
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)]
pub mod xdg_dirs {
use std::path::PathBuf;
use xdg;
lazy_static!{
pub(crate) static ref HAMMOND_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("hammond").unwrap()
};
/// XDG_DATA Directory `Pathbuf`.
pub static ref HAMMOND_DATA: PathBuf = {
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
};
/// XDG_CONFIG Directory `Pathbuf`.
pub static ref HAMMOND_CONFIG: PathBuf = {
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
};
/// XDG_CACHE Directory `Pathbuf`.
pub static ref HAMMOND_CACHE: PathBuf = {
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
};
/// Hammond Download Direcotry `PathBuf`.
pub static ref DL_DIR: PathBuf = {
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
};
}
}
-173
View File
@@ -1,173 +0,0 @@
use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Save, Source};
use schema::podcast;
use std::sync::Arc;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[belongs_to(Source, foreign_key = "source_id")]
#[changeset_options(treat_none_as_null = "true")]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
/// Diesel Model of the podcast table.
pub struct Podcast {
id: i32,
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
impl Save<Podcast> for Podcast {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Podcast, Self::Error> {
let db = connection();
let tempdb = db.get()?;
self.save_changes::<Podcast>(&*tempdb).map_err(From::from)
}
}
impl Podcast {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
}
/// Get the Feed `title`.
pub fn title(&self) -> &str {
&self.title
}
/// Get the Feed `link`.
///
/// Usually the website/homepage of the content creator.
pub fn link(&self) -> &str {
&self.link
}
/// Set the Podcast/Feed `link`.
pub fn set_link(&mut self, value: &str) {
self.link = value.to_string();
}
/// Get the `description`.
pub fn description(&self) -> &str {
&self.description
}
/// Set the `description`.
pub fn set_description(&mut self, value: &str) {
self.description = value.to_string();
}
/// Get the `image_uri`.
///
/// Represents the uri(url usually) that the Feed cover image is located at.
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
/// Set the `image_uri`.
pub fn set_image_uri(&mut self, value: Option<&str>) {
self.image_uri = value.map(|x| x.to_string());
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Podcast` Feed.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// Represents the download policy for the `Podcast` Feed.
///
/// Reserved for the use with a Download manager, yet to be implemented.
///
/// If true Podcast Episode should be downloaded automaticly/skipping
/// the selection queue.
pub fn always_download(&self) -> bool {
self.always_dl
}
/// Set the download policy.
pub fn set_always_download(&mut self, b: bool) {
self.always_dl = b
}
/// `Source` table foreign key.
pub fn source_id(&self) -> i32 {
self.source_id
}
}
#[derive(Queryable, Debug, Clone)]
/// Diesel Model of the podcast cover query.
/// Used for fetching information about a Podcast's cover.
pub struct PodcastCoverQuery {
id: i32,
title: String,
image_uri: Option<String>,
}
impl From<Podcast> for PodcastCoverQuery {
fn from(p: Podcast) -> PodcastCoverQuery {
PodcastCoverQuery {
id: p.id(),
title: p.title,
image_uri: p.image_uri,
}
}
}
impl From<Arc<Podcast>> for PodcastCoverQuery {
fn from(p: Arc<Podcast>) -> PodcastCoverQuery {
PodcastCoverQuery {
id: p.id(),
title: p.title.clone(),
image_uri: p.image_uri.clone(),
}
}
}
impl PodcastCoverQuery {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
}
/// Get the Feed `title`.
pub fn title(&self) -> &str {
&self.title
}
/// Get the `image_uri`.
///
/// Represents the uri(url usually) that the Feed cover image is located at.
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
}
-25
View File
@@ -1,25 +0,0 @@
#![recursion_limit = "1024"]
#![warn(unused_extern_crates, unused)]
#![allow(unknown_lints)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))]
#![deny(warnings)]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate glob;
extern crate hammond_data;
extern crate hyper;
extern crate mime_guess;
extern crate reqwest;
extern crate tempdir;
pub mod downloader;
pub mod errors;
-44
View File
@@ -1,44 +0,0 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
build = "build.rs"
name = "hammond-gtk"
version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.2"
crossbeam-channel = "0.1.2"
gdk = "0.8.0"
gdk-pixbuf = "0.4.0"
glib = "0.5.0"
humansize = "1.1.0"
lazy_static = "1.0.0"
log = "0.4.1"
loggerv = "0.7.1"
open = "1.2.1"
rayon = "1.0.1"
send-cell = "0.1.3"
url = "1.7.0"
failure = "0.1.1"
failure_derive = "0.1.1"
regex = "1.0.0"
reqwest = "0.8.5"
serde_json = "1.0.17"
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
[dependencies.gtk]
features = ["v3_22"]
version = "0.4.1"
[dependencies.gio]
features = ["v2_50"]
version = "0.4.0"
[dependencies.hammond-data]
path = "../hammond-data"
[dependencies.hammond-downloader]
path = "../hammond-downloader"
[dev-dependencies]
pretty_assertions = "0.5.1"
-9
View File
@@ -1,9 +0,0 @@
use std::process::Command;
fn main() {
Command::new("glib-compile-resources")
.args(&["--generate", "resources.xml"])
.current_dir("resources")
.status()
.unwrap();
}
-63
View File
@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="empty_show">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">128</property>
<property name="icon_name">application-rss+xml-symbolic</property>
<property name="use_fallback">True</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">This show does not have any episodes</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.4399999999999999"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">If you think this is an Error, Plese consider opening a bug report.</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>
-91
View File
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Hammond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="empty_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">128</property>
<property name="icon_name">application-rss+xml-symbolic</property>
<property name="use_fallback">True</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">No Feed Subscription Found</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.4399999999999999"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">You can subscribe to feeds using the "+" button</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>
-399
View File
@@ -1,399 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Hammond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="name">container</property>
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="frame_parent">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">32</property>
<property name="margin_right">32</property>
<property name="margin_top">32</property>
<property name="margin_bottom">32</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child>
<object class="GtkBox" id="today_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Today</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.5"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="today_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="yday_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Yesterday</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.5"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="yday_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="week_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">This Week</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.5"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="week_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="month_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">This Month</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.5"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="month_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="rest_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Older</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.5"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="rest_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</interface>
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">_Check for new episodes</attribute>
<attribute name="action">app.refresh</attribute>
<attribute name="accel">&lt;primary&gt;r</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Import Shows</attribute>
<attribute name="action">app.import</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Export Shows</attribute>
<attribute name="action">app.export</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Help</attribute>
<attribute name="action">app.help</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>
-300
View File
@@ -1,300 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Hammond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface domain="">
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can_default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">32</property>
<property name="margin_right">32</property>
<property name="margin_top">32</property>
<property name="margin_bottom">32</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">256</property>
<property name="icon_name">image-x-generic-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="min_content_height">80</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkLabel" id="description">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label">This is embarrasing!
Sorry, we could not find a description for this Show.</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max_width_chars">70</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkMenuButton" id="settings_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon_name">emblem-system-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="link_button">
<property name="label" translatable="yes">Website</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="unsub_button">
<property name="label" translatable="yes">Unsubscribe</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<style>
<class name="destructive-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="episodes">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<object class="GtkPopover" id="show_menu">
<property name="can_focus">False</property>
<property name="position">bottom</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton" id="mark_all_watched">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Mark all episodes as listened</property>
<property name="centered">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
-58
View File
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Hammond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="fb_child">
<property name="width_request">256</property>
<property name="height_request">256</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="pd_cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="pixel_size">256</property>
<property name="icon_name">image-x-generic-symbolic</property>
<property name="icon_size">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</interface>
-72
View File
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Hammond is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="fb_parent">
<property name="name">fb_parent</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkFlowBox" id="flowbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin_top">24</property>
<property name="margin_bottom">24</property>
<property name="homogeneous">True</property>
<property name="column_spacing">12</property>
<property name="row_spacing">12</property>
<property name="max_children_per_line">20</property>
<property name="selection_mode">none</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</interface>
-11
View File
@@ -1,11 +0,0 @@
row {
border-bottom: solid 1px rgba(0,0,0, 0.1);
}
row:last-child {
border-bottom: none;
}
list, border {
border-radius: 4px;
}
-6
View File
@@ -1,6 +0,0 @@
# subdir('icons')
install_data('org.gnome.Hammond.desktop', install_dir : datadir + '/applications')
install_data('org.gnome.Hammond.appdata.xml', install_dir : datadir + '/appdata')
install_data('org.gnome.Hammond.gschema.xml', install_dir: join_paths(datadir, 'glib-2.0', 'schemas'))
meson.add_install_script('../../scripts/compile-gschema.py')
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>org.gnome.Hammond</id>
<name>Hammond</name>
<project_license>GPL-3.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Jordan Petridis</developer_name>
<summary>Modern Podcast Client for the GNOME desktop</summary>
<description xml:lang="en">
<p>
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
desktop written in Rust.
</p>
</description>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<screenshots>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/episodes_view.png</image>
<caption>Page 1</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/shows_view.png</image>
<caption>Page 2</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/show_widget.png</image>
<caption>Page 3</caption>
</screenshot>
</screenshots>
<releases>
<release version="0.3.4" date="2018-05-20"/>
</releases>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<update_contact>jpetridis@gnome.org</update_contact>
<developer_name>Jordan Petridis and others</developer_name>
</component>
@@ -1,11 +0,0 @@
[Desktop Entry]
Name=Hammond
GenericName=Podcast Client
Comment=Play, Subscribe and Manage Podcast Feeds.
Icon=multimedia-player
Exec=hammond
Terminal=false
Type=Application
StartupNotify=true
Categories=AudioVideo;Audio;Video;
Keywords=Podcast
-299
View File
@@ -1,299 +0,0 @@
#![allow(new_without_default)]
use gio::{
ActionMapExt, ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt,
SimpleAction, SimpleActionExt,
};
use glib;
use gtk;
use gtk::prelude::*;
use gtk::SettingsExt as GtkSettingsExt;
use crossbeam_channel::{unbounded, Sender};
use hammond_data::{Podcast, dbqueries};
use headerbar::Header;
use settings::{self, WindowGeometry};
use stacks::{Content, PopulatedState};
use utils;
use widgets::appnotif::{InAppNotification, UndoState};
use widgets::{about_dialog, mark_all_notif, remove_show_notif};
use std::rc::Rc;
use std::sync::Arc;
/// Creates an action named $called in the action map $on with the handler $handle
macro_rules! action {
($on:expr, $called:expr, $handle:expr) => {{
// Create a stateless, parameterless action
let act = SimpleAction::new($called, None);
// Connect the handler
act.connect_activate($handle);
// Add it to the map
$on.add_action(&act);
// Return the action
act
}};
}
#[derive(Debug, Clone)]
pub enum Action {
RefreshAllViews,
RefreshEpisodesView,
RefreshEpisodesViewBGR,
RefreshShowsView,
ReplaceWidget(Arc<Podcast>),
RefreshWidgetIfSame(i32),
ShowWidgetAnimated,
ShowShowsAnimated,
HeaderBarShowTile(String),
HeaderBarNormal,
HeaderBarShowUpdateIndicator,
HeaderBarHideUpdateIndicator,
MarkAllPlayerNotification(Arc<Podcast>),
RemoveShow(Arc<Podcast>),
ErrorNotification(String),
AboutWidget(i32),
}
#[derive(Debug)]
pub struct App {
app_instance: gtk::Application,
settings: Settings,
}
impl App {
pub fn new() -> App {
let settings = Settings::new("org.gnome.Hammond");
let application = gtk::Application::new("org.gnome.Hammond", ApplicationFlags::empty())
.expect("Application Initialization failed...");
// Weird magic I copy-pasted that sets the Application Name in the Shell.
glib::set_application_name("Hammond");
glib::set_prgname(Some("Hammond"));
let cleanup_date = settings::get_cleanup_date(&settings);
utils::cleanup(cleanup_date);
application.connect_startup(clone!(settings => move |app| {
let (sender, receiver) = unbounded();
App::setup_gactions(&app, &sender);
app.connect_activate(clone!(sender, settings => move |app| {
// Get the current window (if any)
if let Some(window) = app.get_active_window() {
// Already open, just raise the window
window.present();
} else {
// Time to open one!
// Create the main window
let window = gtk::ApplicationWindow::new(&app);
window.set_title("Hammond");
window.connect_delete_event(clone!(app, settings => move |window, _| {
WindowGeometry::from_window(&window).write(&settings);
app.quit();
Inhibit(false)
}));
// Create a content instance
let content =
Rc::new(Content::new(sender.clone()).expect(
"Content Initialization failed."));
// Create the headerbar
let header = Rc::new(Header::new(&content, &window, &sender));
action!(window, "menu", clone!(header => move |_, _| {
header.open_menu();
}));
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
WindowGeometry::from_settings(&settings).apply(&window);
App::setup_timed_callbacks(&sender, &settings);
window.show_all();
window.activate();
gtk::timeout_add(50, clone!(sender, receiver => move || {
// Uses receiver, content, header, sender, overlay
match receiver.try_recv() {
Ok(Action::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(),
Ok(Action::RefreshWidgetIfSame(id)) =>
content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(),
Ok(Action::RefreshEpisodesViewBGR) =>
content.update_home_if_background(),
Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_|
error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
}
Ok(Action::ShowWidgetAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut().switch_visible(
PopulatedState::Widget,
gtk::StackTransitionType::SlideLeft,
);
}
Ok(Action::ShowShowsAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.switch_visible(PopulatedState::View,
gtk::StackTransitionType::SlideRight);
}
Ok(Action::HeaderBarShowTile(title)) =>
header.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => header.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) =>
header.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) =>
header.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let notif = mark_all_notif(pd, &sender);
notif.show(&overlay);
}
Ok(Action::RemoveShow(pd)) => {
let notif = remove_show_notif(pd, sender.clone());
notif.show(&overlay);
}
Ok(Action::ErrorNotification(err)) => {
error!("An error notification was triggered: {}", err);
let callback = || glib::Continue(false);
let notif = InAppNotification::new(&err, callback,
|| {}, UndoState::Hidden);
notif.show(&overlay);
}
Ok(Action::AboutWidget(rowid)) => {
if let Ok(d) = dbqueries::get_episode_description(rowid) {
if let Some(_desc) = d {
let _stack = content.get_visible_stack();
}
}
}
Err(_) => (),
}
Continue(true)
}));
}
}));
}));
App {
app_instance: application,
settings,
}
}
fn setup_timed_callbacks(sender: &Sender<Action>, settings: &Settings) {
App::setup_dark_theme(settings);
App::setup_refresh_on_startup(&sender, settings);
App::setup_auto_refresh(&sender, settings);
}
fn setup_dark_theme(settings: &Settings) {
let gtk_settings = gtk::Settings::get_default().unwrap();
let enabled = settings.get_boolean("dark-theme");
gtk_settings.set_property_gtk_application_prefer_dark_theme(enabled);
}
fn setup_refresh_on_startup(sender: &Sender<Action>, settings: &Settings) {
// Update the feeds right after the Application is initialized.
let sender = sender.clone();
if settings.get_boolean("refresh-on-startup") {
info!("Refresh on startup.");
// The ui loads async, after initialization
// so we need to delay this a bit so it won't block
// requests that will come from loading the gui on startup.
gtk::timeout_add(1500, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(false)
});
}
}
fn setup_auto_refresh(sender: &Sender<Action>, settings: &Settings) {
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
info!("Auto-refresh every {:?} seconds.", refresh_interval);
let sender = sender.clone();
gtk::timeout_add_seconds(refresh_interval, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(true)
});
}
/// Define the `GAction`s.
///
/// Used in menus and the keyboard shortcuts dialog.
fn setup_gactions(app: &gtk::Application, sender: &Sender<Action>) {
// Create the `refresh` action.
//
// This will trigger a refresh of all the shows in the database.
action!(
app,
"refresh",
clone!(sender => move |_, _| {
gtk::idle_add(clone!(sender => move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(false)
}));
})
);
app.set_accels_for_action("app.refresh", &["<primary>r"]);
// Create the `OPML` import action
action!(
app,
"import",
clone!(sender, app => move |_, _| {
let window = app.get_active_window().expect("Failed to get active window");
utils::on_import_clicked(&window, &sender);
})
);
// Create the action that shows a `gtk::AboutDialog`
action!(
app,
"about",
clone!(app => move |_, _| {
let window = app.get_active_window().expect("Failed to get active window");
about_dialog(&window);
})
);
// Create the quit action
action!(app, "quit", clone!(app => move |_, _| app.quit()));
app.set_accels_for_action("app.quit", &["<primary>q"]);
// Bind the hamburger menu button to `F10`
app.set_accels_for_action("win.menu", &["F10"]);
}
pub fn run(self) {
ApplicationExtManual::run(&self.app_instance, &[]);
}
}
-223
View File
@@ -1,223 +0,0 @@
use gio::MenuModel;
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use failure::ResultExt;
use url::Url;
use crossbeam_channel::Sender;
use hammond_data::{dbqueries, Source};
use app::Action;
use stacks::Content;
use utils::{itunes_to_rss, refresh};
#[derive(Debug, Clone)]
// TODO: split this into smaller
pub struct Header {
container: gtk::HeaderBar,
add_toggle: gtk::MenuButton,
switch: gtk::StackSwitcher,
back: gtk::Button,
show_title: gtk::Label,
update_box: gtk::Box,
update_label: gtk::Label,
update_spinner: gtk::Spinner,
menu_button: gtk::MenuButton,
app_menu: MenuModel,
}
impl Default for Header {
fn default() -> Header {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/headerbar.ui");
let menus = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/menus.ui");
let header = builder.get_object("headerbar").unwrap();
let add_toggle = builder.get_object("add_toggle").unwrap();
let switch = builder.get_object("switch").unwrap();
let back = builder.get_object("back").unwrap();
let show_title = builder.get_object("show_title").unwrap();
let update_box = builder.get_object("update_notification").unwrap();
let update_label = builder.get_object("update_label").unwrap();
let update_spinner = builder.get_object("update_spinner").unwrap();
let menu_button = builder.get_object("menu_button").unwrap();
let app_menu = menus.get_object("menu").unwrap();
Header {
container: header,
add_toggle,
switch,
back,
show_title,
update_box,
update_label,
update_spinner,
menu_button,
app_menu,
}
}
}
// TODO: Refactor components into smaller state machines
impl Header {
pub fn new(
content: &Content,
window: &gtk::ApplicationWindow,
sender: &Sender<Action>,
) -> Header {
let h = Header::default();
h.init(content, window, &sender);
h
}
pub fn init(
&self,
content: &Content,
window: &gtk::ApplicationWindow,
sender: &Sender<Action>,
) {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
let new_url: gtk::Entry = builder.get_object("new_url").unwrap();
let add_button: gtk::Button = builder.get_object("add_button").unwrap();
let result_label: gtk::Label = builder.get_object("result_label").unwrap();
self.switch.set_stack(&content.get_stack());
new_url.connect_changed(clone!(add_button => move |url| {
on_url_change(url, &result_label, &add_button)
.map_err(|err| error!("Error: {}", err))
.ok();
}));
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
on_add_bttn_clicked(&new_url, sender.clone())
.map_err(|err| error!("Error: {}", err))
.ok();
add_popover.hide();
}));
self.add_toggle.set_popover(&add_popover);
// Add the Headerbar to the window.
window.set_titlebar(&self.container);
let switch = &self.switch;
let add_toggle = &self.add_toggle;
let show_title = &self.show_title;
self.back.connect_clicked(
clone!(switch, add_toggle, show_title, sender => move |back| {
switch.show();
add_toggle.show();
back.hide();
show_title.hide();
sender.send(Action::ShowShowsAnimated)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}),
);
self.menu_button.set_menu_model(Some(&self.app_menu));
}
pub fn switch_to_back(&self, title: &str) {
self.switch.hide();
self.add_toggle.hide();
self.back.show();
self.set_show_title(title);
self.show_title.show();
}
pub fn switch_to_normal(&self) {
self.switch.show();
self.add_toggle.show();
self.back.hide();
self.show_title.hide();
}
pub fn set_show_title(&self, title: &str) {
self.show_title.set_text(title)
}
pub fn show_update_notification(&self) {
self.update_spinner.start();
self.update_box.show();
self.update_spinner.show();
self.update_label.show();
}
pub fn hide_update_notification(&self) {
self.update_spinner.stop();
self.update_box.hide();
self.update_spinner.hide();
self.update_label.hide();
}
pub fn open_menu(&self) {
self.menu_button.clicked();
}
}
// FIXME: THIS ALSO SUCKS!
fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(), Error> {
let url = entry.get_text().unwrap_or_default();
let url = if url.contains("itunes.com") || url.contains("apple.com") {
info!("Detected itunes url.");
let foo = itunes_to_rss(&url)?;
info!("Resolved to {}", foo);
foo
} else {
url.to_owned()
};
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text("");
gtk::idle_add(move || {
refresh(Some(vec![source.clone()]), sender.clone());
glib::Continue(false)
});
Ok(())
}
// FIXME: THIS SUCKS!
fn on_url_change(
entry: &gtk::Entry,
result: &gtk::Label,
add_button: &gtk::Button,
) -> Result<(), Error> {
let uri = entry
.get_text()
.ok_or_else(|| format_err!("GtkEntry blew up somehow."))?;
debug!("Url: {}", uri);
let url = Url::parse(&uri);
// TODO: refactor to avoid duplication
match url {
Ok(u) => {
if !dbqueries::source_exists(u.as_str())? {
add_button.set_sensitive(true);
result.hide();
result.set_label("");
} else {
add_button.set_sensitive(false);
result.set_label("Show already exists.");
result.show();
}
Ok(())
}
Err(err) => {
add_button.set_sensitive(false);
if !uri.is_empty() {
result.set_label("Invalid url.");
result.show();
error!("Error: {}", err);
} else {
result.hide();
}
Ok(())
}
}
}
-95
View File
@@ -1,95 +0,0 @@
#![cfg_attr(
feature = "cargo-clippy",
allow(clone_on_ref_ptr, blacklisted_name, match_same_arms, option_map_unit_fn)
)]
#![allow(unknown_lints)]
#![warn(unused_extern_crates, unused)]
#![deny(warnings)]
extern crate gdk;
extern crate gdk_pixbuf;
extern crate gio;
extern crate glib;
extern crate gtk;
#[macro_use]
extern crate failure;
// #[macro_use]
// extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate chrono;
extern crate crossbeam_channel;
extern crate hammond_data;
extern crate hammond_downloader;
extern crate html2pango;
extern crate humansize;
extern crate loggerv;
extern crate open;
extern crate rayon;
extern crate regex;
extern crate reqwest;
extern crate send_cell;
extern crate serde_json;
extern crate url;
use log::Level;
use gtk::prelude::*;
// http://gtk-rs.org/tuto/closures
#[macro_export]
macro_rules! clone {
(@param _) => ( _ );
(@param $x:ident) => ( $x );
($($n:ident),+ => move || $body:expr) => (
{
$( let $n = $n.clone(); )+
move || $body
}
);
($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
{
$( let $n = $n.clone(); )+
move |$(clone!(@param $p),)+| $body
}
);
}
mod stacks;
mod widgets;
mod app;
mod headerbar;
mod manager;
mod settings;
mod static_resource;
mod utils;
use app::App;
fn main() {
// TODO: make the the logger a cli -vv option
loggerv::init_with_level(Level::Info).expect("Error initializing loggerv.");
gtk::init().expect("Error initializing gtk.");
static_resource::init().expect("Something went wrong with the resource file initialization.");
// Add custom style
let provider = gtk::CssProvider::new();
gtk::CssProvider::load_from_resource(&provider, "/org/gnome/Hammond/gtk/style.css");
gtk::StyleContext::add_provider_for_screen(
&gdk::Screen::get_default().expect("Error initializing gtk css provider."),
&provider,
600,
);
App::new().run();
}
-117
View File
@@ -1,117 +0,0 @@
use gtk;
use gtk::prelude::*;
use gtk::StackTransitionType;
use crossbeam_channel::Sender;
use failure::Error;
use hammond_data::dbqueries::is_episodes_populated;
use hammond_data::errors::DataError;
use app::Action;
use widgets::{EmptyView, HomeView};
use std::rc::Rc;
#[derive(Debug, Clone, Copy)]
enum State {
Home,
Empty,
}
#[derive(Debug, Clone)]
pub struct HomeStack {
empty: EmptyView,
episodes: Rc<HomeView>,
stack: gtk::Stack,
state: State,
sender: Sender<Action>,
}
impl HomeStack {
pub fn new(sender: Sender<Action>) -> Result<HomeStack, Error> {
let episodes = HomeView::new(sender.clone())?;
let empty = EmptyView::new();
let stack = gtk::Stack::new();
let state = State::Empty;
stack.add_named(&episodes.container, "home");
stack.add_named(&empty.container, "empty");
let mut home = HomeStack {
empty,
episodes,
stack,
state,
sender,
};
home.determine_state()?;
Ok(home)
}
pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone()
}
pub fn update(&mut self) -> Result<(), Error> {
// Copy the vertical scrollbar adjustment from the old view.
self.episodes
.save_alignment()
.map_err(|err| error!("Failed to set episodes_view allignment: {}", err))
.ok();
self.replace_view()?;
// Determine the actuall state.
self.determine_state().map_err(From::from)
}
fn replace_view(&mut self) -> Result<(), Error> {
// Get the container of the view
let old = &self.episodes.container.clone();
let eps = HomeView::new(self.sender.clone())?;
// Remove the old widget and add the new one
// during this the previous view is removed,
// and the visibile child fallsback to empty view.
self.stack.remove(old);
self.stack.add_named(&eps.container, "home");
// Keep the previous state.
let s = self.state;
// Set the visible child back to the previous one to avoid
// the stack transition animation to show the empty view
self.switch_visible(s, StackTransitionType::None);
// replace view in the struct too
self.episodes = eps;
// This might not be needed
old.destroy();
Ok(())
}
fn switch_visible(&mut self, s: State, animation: StackTransitionType) {
use self::State::*;
match s {
Home => {
self.stack.set_visible_child_full("home", animation);
self.state = Home;
}
Empty => {
self.stack.set_visible_child_full("empty", animation);
self.state = Empty;
}
}
}
fn determine_state(&mut self) -> Result<(), DataError> {
if is_episodes_populated()? {
self.switch_visible(State::Home, StackTransitionType::Crossfade);
} else {
self.switch_visible(State::Empty, StackTransitionType::Crossfade);
};
Ok(())
}
}
-9
View File
@@ -1,9 +0,0 @@
mod content;
mod home;
mod populated;
mod show;
pub use self::content::Content;
pub use self::home::HomeStack;
pub use self::populated::{PopulatedStack, PopulatedState};
pub use self::show::{ShowStack, ShowState};
-94
View File
@@ -1,94 +0,0 @@
use glib;
use gtk;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone, Copy)]
pub enum UndoState {
Shown,
Hidden,
}
#[derive(Debug, Clone)]
pub struct InAppNotification {
revealer: gtk::Revealer,
text: gtk::Label,
undo: gtk::Button,
close: gtk::Button,
}
impl Default for InAppNotification {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/inapp_notif.ui");
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
let text: gtk::Label = builder.get_object("text").unwrap();
let undo: gtk::Button = builder.get_object("undo").unwrap();
let close: gtk::Button = builder.get_object("close").unwrap();
InAppNotification {
revealer,
text,
undo,
close,
}
}
}
impl InAppNotification {
pub fn new<F, U>(text: &str, mut callback: F, undo_callback: U, show_undo: UndoState) -> Self
where
F: FnMut() -> glib::Continue + 'static,
U: Fn() + 'static,
{
let notif = InAppNotification::default();
notif.text.set_text(&text);
let revealer = notif.revealer.clone();
let id = timeout_add_seconds(6, move || {
revealer.set_reveal_child(false);
callback()
});
let id = Rc::new(RefCell::new(Some(id)));
// Cancel the callback
let revealer = notif.revealer.clone();
notif.undo.connect_clicked(move |_| {
let foo = id.borrow_mut().take();
if let Some(id) = foo {
glib::source::source_remove(id);
}
undo_callback();
// Hide the notification
revealer.set_reveal_child(false);
});
// Hide the revealer when the close button is clicked
let revealer = notif.revealer.clone();
notif.close.connect_clicked(move |_| {
revealer.set_reveal_child(false);
});
match show_undo {
UndoState::Shown => (),
UndoState::Hidden => notif.undo.hide(),
}
notif
}
// This is a seperate method cause in order to get a nice animation
// the revealer should be attached to something that displays it.
// Previously we where doing it in the constructor, which had the result
// of the animation being skipped cause there was no parent widget to display it.
pub fn show(&self, overlay: &gtk::Overlay) {
overlay.add_overlay(&self.revealer);
// We need to display the notification after the widget is added to the overlay
// so there will be a nice animation.
self.revealer.set_reveal_child(true);
}
}
-21
View File
@@ -1,21 +0,0 @@
use gtk;
#[derive(Debug, Clone)]
pub struct EmptyView {
pub container: gtk::Box,
}
impl Default for EmptyView {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/empty_view.ui");
let view: gtk::Box = builder.get_object("empty_view").unwrap();
EmptyView { container: view }
}
}
impl EmptyView {
pub fn new() -> EmptyView {
EmptyView::default()
}
}
-15
View File
@@ -1,15 +0,0 @@
mod aboutdialog;
pub mod appnotif;
mod empty;
mod episode;
mod home_view;
mod show;
mod shows_view;
pub use self::aboutdialog::about_dialog;
pub use self::empty::EmptyView;
pub use self::episode::EpisodeWidget;
pub use self::home_view::HomeView;
pub use self::show::ShowWidget;
pub use self::show::{mark_all_notif, remove_show_notif};
pub use self::shows_view::ShowsView;
-347
View File
@@ -1,347 +0,0 @@
use glib;
use gtk;
use gtk::prelude::*;
use crossbeam_channel::{SendError, Sender};
use failure::Error;
use html2pango::markup_from_raw;
use open;
use rayon;
use send_cell::SendCell;
use hammond_data::dbqueries;
use hammond_data::utils::delete_show;
use hammond_data::Podcast;
use app::Action;
use utils::{self, lazy_load};
use widgets::appnotif::{InAppNotification, UndoState};
use widgets::EpisodeWidget;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
lazy_static! {
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
Mutex::new(None);
}
#[derive(Debug, Clone)]
pub struct ShowWidget {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
cover: gtk::Image,
description: gtk::Label,
link: gtk::Button,
settings: gtk::MenuButton,
unsub: gtk::Button,
episodes: gtk::ListBox,
podcast_id: Option<i32>,
}
impl Default for ShowWidget {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let episodes = builder.get_object("episodes").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
let description: gtk::Label = builder.get_object("description").unwrap();
let unsub: gtk::Button = builder.get_object("unsub_button").unwrap();
let link: gtk::Button = builder.get_object("link_button").unwrap();
let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap();
ShowWidget {
container,
scrolled_window,
cover,
description,
unsub,
link,
settings,
episodes,
podcast_id: None,
}
}
}
impl ShowWidget {
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> {
let mut pdw = ShowWidget::default();
pdw.init(&pd, &sender);
let pdw = Rc::new(pdw);
populate_listbox(&pdw, pd, sender)
.map_err(|err| error!("Failed to populate the listbox: {}", err))
.ok();
pdw
}
pub fn init(&mut self, pd: &Arc<Podcast>, sender: &Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui");
self.unsub
.connect_clicked(clone!(pd, sender => move |bttn| {
on_unsub_button_clicked(pd.clone(), bttn, &sender);
}));
self.set_description(pd.description());
self.podcast_id = Some(pd.id());
self.set_cover(&pd)
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
let link = pd.link().to_owned();
self.link.set_tooltip_text(Some(link.as_str()));
self.link.connect_clicked(move |_| {
info!("Opening link: {}", &link);
open::that(&link)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed open link: {}", &link))
.ok();
});
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap();
let episodes = self.episodes.clone();
mark_all.connect_clicked(clone!(pd, sender => move |_| {
on_played_button_clicked(
pd.clone(),
&episodes,
&sender
)
}));
self.settings.set_popover(&show_menu);
}
/// Set the show cover.
fn set_cover(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
utils::set_image_from_path(&self.cover, pd.id(), 256)
}
/// Set the descripton text.
fn set_description(&self, text: &str) {
self.description.set_markup(&markup_from_raw(text));
}
/// Save the scrollabar vajustment to the cache.
pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> {
if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() {
let adj = self
.scrolled_window
.get_vadjustment()
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
*guard = Some((oldid, SendCell::new(adj)));
debug!("Widget Alignment was saved with ID: {}.", oldid);
}
Ok(())
}
/// Set scrolled window vertical adjustment.
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
let guard = SHOW_WIDGET_VALIGNMENT
.lock()
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
if let Some((oldid, ref sendcell)) = *guard {
// Only copy the old scrollbar if both widget's represent the same podcast.
debug!("PID: {}", pd.id());
debug!("OLDID: {}", oldid);
if pd.id() != oldid {
debug!("Early return");
return Ok(());
};
// Copy the vertical scrollbar adjustment from the old view into the new one.
sendcell
.try_get()
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
}
Ok(())
}
pub fn podcast_id(&self) -> Option<i32> {
self.podcast_id
}
}
/// Populate the listbox with the shows episodes.
fn populate_listbox(
show: &Rc<ShowWidget>,
pd: Arc<Podcast>,
sender: Sender<Action>,
) -> Result<(), Error> {
use crossbeam_channel::bounded;
use crossbeam_channel::TryRecvError::*;
let count = dbqueries::get_pd_episodes_count(&pd)?;
let (sender_, receiver) = bounded(1);
rayon::spawn(clone!(pd => move || {
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
// The receiver can be dropped if there's an early return
// like on show without episodes for example.
sender_.send(episodes).ok();
}));
if count == 0 {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/empty_show.ui");
let container: gtk::Box = builder.get_object("empty_show").unwrap();
show.episodes.add(&container);
return Ok(());
}
let show_ = show.clone();
gtk::idle_add(move || {
let episodes = match receiver.try_recv() {
Ok(e) => e,
Err(Empty) => return glib::Continue(true),
Err(Disconnected) => return glib::Continue(false),
};
let list = show_.episodes.clone();
let constructor = clone!(sender => move |ep| {
EpisodeWidget::new(&ep, &sender).container.clone()
});
let callback = clone!(pd, show_ => move || {
show_.set_vadjustment(&pd)
.map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err))
.ok();
});
lazy_load(episodes, list.clone(), constructor, callback);
glib::Continue(false)
});
Ok(())
}
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender: &Sender<Action>) {
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.set_sensitive(false);
let wrap = || -> Result<(), SendError<_>> {
sender.send(Action::RemoveShow(pd))?;
sender.send(Action::HeaderBarNormal)?;
sender.send(Action::ShowShowsAnimated)?;
// Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
wrap().map_err(|err| error!("Action Sender: {}", err)).ok();
unsub_button.set_sensitive(true);
}
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
if dim_titles(episodes).is_none() {
error!("Something went horribly wrong when dimming the titles.");
warn!("RUN WHILE YOU STILL CAN!");
}
sender
.send(Action::MarkAllPlayerNotification(pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?;
// Not all widgets migth have been loaded when the mark_all is hit
// So we will need to refresh again after it's done.
sender.send(Action::RefreshWidgetIfSame(pd.id()))?;
sender.send(Action::RefreshEpisodesView).map_err(From::from)
}
pub fn mark_all_notif(pd: Arc<Podcast>, sender: &Sender<Action>) -> InAppNotification {
let id = pd.id();
let callback = clone!(sender => move || {
mark_all_watched(&pd, &sender)
.map_err(|err| error!("Notif Callback Error: {}", err))
.ok();
glib::Continue(false)
});
let undo_callback = clone!(sender => move || {
sender.send(Action::RefreshWidgetIfSame(id))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
});
let text = "Marked all episodes as listened";
InAppNotification::new(text, callback, undo_callback, UndoState::Shown)
}
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
let text = format!("Unsubscribed from {}", pd.title());
utils::ignore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not insert {} to the ignore list.", pd.title()))
.ok();
let callback = clone!(pd, sender => move || {
utils::uningore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not remove {} from the ignore list.", pd.title()))
.ok();
// Spawn a thread so it won't block the ui.
rayon::spawn(clone!(pd, sender => move || {
delete_show(&pd)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete {}", pd.title()))
.ok();
sender.send(Action::RefreshEpisodesView).ok();
}));
glib::Continue(false)
});
let undo_wrap = move || -> Result<(), Error> {
utils::uningore_show(pd.id())?;
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
let undo_callback = move || {
undo_wrap().map_err(|err| error!("{}", err)).ok();
};
InAppNotification::new(&text, callback, undo_callback, UndoState::Shown)
}
// Ideally if we had a custom widget this would have been as simple as:
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
// But now I can't think of a better way to do it than hardcoding the title
// position relative to the EpisodeWidget container gtk::Box.
fn dim_titles(episodes: &gtk::ListBox) -> Option<()> {
let children = episodes.get_children();
for row in children {
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let foo = container
.get_children()
.remove(0)
.downcast::<gtk::Box>()
.ok()?;
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let title = bar.get_children().remove(0).downcast::<gtk::Label>().ok()?;
title.get_style_context().map(|c| c.add_class("dim-label"));
}
Some(())
}
-168
View File
@@ -1,168 +0,0 @@
use gtk;
use gtk::prelude::*;
use crossbeam_channel::Sender;
use failure::Error;
use send_cell::SendCell;
use hammond_data::dbqueries;
use hammond_data::Podcast;
use app::Action;
use utils::{self, get_ignored_shows, lazy_load, set_image_from_path};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
lazy_static! {
static ref SHOWS_VIEW_VALIGNMENT: Mutex<Option<SendCell<gtk::Adjustment>>> = Mutex::new(None);
}
#[derive(Debug, Clone)]
pub struct ShowsView {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
flowbox: gtk::FlowBox,
}
impl Default for ShowsView {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/shows_view.ui");
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
ShowsView {
container,
scrolled_window,
flowbox,
}
}
}
impl ShowsView {
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> {
let pop = Rc::new(ShowsView::default());
pop.init(sender);
// Populate the flowbox with the Podcasts.
populate_flowbox(&pop)?;
Ok(pop)
}
pub fn init(&self, sender: Sender<Action>) {
self.flowbox.connect_child_activated(move |_, child| {
on_child_activate(child, &sender)
.map_err(|err| error!("Error along flowbox child activation: {}", err))
.ok();
});
}
/// Set scrolled window vertical adjustment.
#[allow(unused)]
fn set_vadjustment(&self) -> Result<(), Error> {
let guard = SHOWS_VIEW_VALIGNMENT
.lock()
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
if let Some(ref sendcell) = *guard {
// Copy the vertical scrollbar adjustment from the old view into the new one.
sendcell
.try_get()
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
}
Ok(())
}
/// Save the vertical scrollbar position.
pub fn save_alignment(&self) -> Result<(), Error> {
if let Ok(mut guard) = SHOWS_VIEW_VALIGNMENT.lock() {
let adj = self
.scrolled_window
.get_vadjustment()
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
*guard = Some(SendCell::new(adj));
info!("Saved episodes_view alignment.");
}
Ok(())
}
}
fn populate_flowbox(shows: &Rc<ShowsView>) -> Result<(), Error> {
let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
let constructor = move |parent| ShowsChild::new(&parent).child;
let callback = clone!(shows => move || {
shows.set_vadjustment()
.map_err(|err| error!("Failed to set ShowsView Alignment: {}", err))
.ok();
});
let flowbox = shows.flowbox.clone();
lazy_load(podcasts, flowbox, constructor, callback);
Ok(())
}
fn on_child_activate(child: &gtk::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> {
use gtk::WidgetExt;
// This is such an ugly hack...
let id = WidgetExt::get_name(child)
.ok_or_else(|| format_err!("Faild to get \"episodes\" child from the stack."))?
.parse::<i32>()?;
let pd = Arc::new(dbqueries::get_podcast_from_id(id)?);
sender.send(Action::HeaderBarShowTile(pd.title().into()))?;
sender.send(Action::ReplaceWidget(pd))?;
sender.send(Action::ShowWidgetAnimated)?;
Ok(())
}
#[derive(Debug)]
struct ShowsChild {
container: gtk::Box,
cover: gtk::Image,
child: gtk::FlowBoxChild,
}
impl Default for ShowsChild {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/shows_child.ui");
let container: gtk::Box = builder.get_object("fb_child").unwrap();
let cover: gtk::Image = builder.get_object("pd_cover").unwrap();
let child = gtk::FlowBoxChild::new();
child.add(&container);
ShowsChild {
container,
cover,
child,
}
}
}
impl ShowsChild {
pub fn new(pd: &Podcast) -> ShowsChild {
let child = ShowsChild::default();
child.init(pd);
child
}
fn init(&self, pd: &Podcast) {
self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string());
self.set_cover(pd.id())
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
}
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
set_image_from_path(&self.cover, podcast_id, 256)
}
}
+47 -18
View File
@@ -2,23 +2,46 @@
# https://gitlab.gnome.org/danigm/fractal/blob/6e2911f9d2353c99a18a6c19fab7f903c4bbb431/meson.build
project(
'hammond', 'rust',
version: '0.3.4',
'gnome-podcasts', 'rust',
version: '0.4.5',
license: 'GPLv3',
)
hammond_version = meson.project_version()
version_array = hammond_version.split('.')
hammond_major_version = version_array[0].to_int()
hammond_minor_version = version_array[1].to_int()
hammond_version_micro = version_array[2].to_int()
if get_option('profile') == 'development'
profile = 'Devel'
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip()
if vcs_tag == ''
version_suffix = '-devel'
else
version_suffix = '-@0@'.format (vcs_tag)
endif
else
profile = ''
version_suffix = ''
endif
hammond_prefix = get_option('prefix')
hammond_bindir = join_paths(hammond_prefix, get_option('bindir'))
application_id = 'org.gnome.Podcasts@0@'.format(profile)
i18n = import('i18n')
subdir('podcasts-gtk/po')
podir = join_paths (meson.source_root (), 'podcasts-gtk', 'po')
podcasts_version = meson.project_version()
version_array = podcasts_version.split('.')
podcasts_major_version = version_array[0].to_int()
podcasts_minor_version = version_array[1].to_int()
podcasts_version_micro = version_array[2].to_int()
podcasts_prefix = get_option('prefix')
podcasts_bindir = join_paths(podcasts_prefix, get_option('bindir'))
podcasts_localedir = join_paths(podcasts_prefix, get_option('localedir'))
podcasts_conf = configuration_data()
podcasts_conf.set('appid', application_id)
podcasts_conf.set('bindir', podcasts_bindir)
datadir = get_option('datadir')
icondir = join_paths(datadir, 'icons')
subdir('hammond-gtk/resources')
subdir('podcasts-gtk/resources')
cargo = find_program('cargo', required: false)
gresource = find_program('glib-compile-resources', required: false)
@@ -26,13 +49,19 @@ cargo_vendor = find_program('cargo-vendor', required: false)
cargo_script = find_program('scripts/cargo.sh')
cargo_release = custom_target('cargo-build',
build_by_default: true,
build_always: true,
output: ['hammond'],
build_always_stale: true,
output: ['gnome-podcasts'],
install: true,
install_dir: hammond_bindir,
command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
install_dir: podcasts_bindir,
command: [cargo_script,
'@CURRENT_SOURCE_DIR@',
'@OUTPUT@',
podcasts_localedir,
application_id,
podcasts_version + version_suffix,
profile
])
run_target('release', command: ['scripts/release.sh',
meson.project_name() + '-' + hammond_version
])
meson.project_name() + '-' + podcasts_version
])
+9
View File
@@ -0,0 +1,9 @@
option (
'profile',
type: 'combo',
choices: [
'default',
'development'
],
value: 'default'
)
@@ -1,12 +1,12 @@
{
"app-id" : "org.gnome.Hammond",
"app-id" : "org.gnome.PodcastsDevel",
"runtime" : "org.gnome.Platform",
"runtime-version" : "3.28",
"runtime-version" : "master",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command" : "hammond",
"command" : "gnome-podcasts",
"tags" : [
"nightly"
],
@@ -20,7 +20,8 @@
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--talk-name=org.freedesktop.Desktop"
"--socket=pulseaudio",
"--device=dri"
],
"build-options" : {
"append-path" : "/usr/lib/sdk/rust-stable/bin",
@@ -28,22 +29,32 @@
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/Hammond/cargo",
"RUST_BACKTRACE" : "1",
"DEBUG" : "true"
"CARGO_HOME" : "/run/build/Podcasts/cargo",
"RUST_BACKTRACE" : "1"
}
},
"modules" : [
{
"name" : "hammond",
"name" : "libhandy",
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/World/hammond.git",
"url" : "https://source.puri.sm/Librem5/libhandy.git"
}
]
},
{
"name" : "gnome-podcasts",
"buildsystem" : "meson",
"config-opts" : [ "-Dprofile=development" ],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/World/podcasts.git",
"branch" : "master"
}
]
}
]
}
}
@@ -1,46 +1,41 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-data"
name = "podcasts-data"
version = "0.1.0"
workspace = "../"
[dependencies]
ammonia = "1.1.0"
chrono = "0.4.2"
ammonia = "1.2.0"
chrono = "0.4.6"
derive_builder = "0.5.1"
lazy_static = "1.0.0"
log = "0.4.1"
rayon = "1.0.1"
lazy_static = "1.1.0"
log = "0.4.4"
rayon = "1.0.2"
rayon-futures = "0.1.0"
rfc822_sanitizer = "0.3.3"
rss = "1.5.0"
url = "1.7.0"
url = "1.7.1"
xdg = "2.1.0"
xml-rs = "0.8.0"
futures = "0.1.21"
futures = "0.1.23"
hyper = "0.11.27"
tokio-core = "0.1.17"
hyper-tls = "0.1.3"
native-tls = "0.1.5"
num_cpus = "1.8.0"
failure = "0.1.1"
failure_derive = "0.1.1"
failure = "0.1.2"
failure_derive = "0.1.2"
[dependencies.diesel]
features = ["sqlite", "r2d2"]
version = "= 1.2.2"
version = "1.3.2"
[dependencies.diesel_migrations]
features = ["sqlite"]
version = "= 1.2.0"
version = "1.3.0"
[dev-dependencies]
rand = "0.5.0"
rand = "0.5.5"
tempdir = "0.3.7"
criterion = "0.2.3"
pretty_assertions = "0.5.1"
maplit = "1.0.1"
[[bench]]
name = "bench"
harness = false
+6
View File
@@ -0,0 +1,6 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
patch_file = "src/schema.patch"
@@ -0,0 +1,53 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
favorite INTEGER DEFAULT 0,
archive INTEGER DEFAULT 0,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, favorite, archive)
SELECT title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, 0, 0
FROM old_table;
Drop table old_table;
ALTER TABLE podcast RENAME TO old_table;
CREATE TABLE `podcast` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`source_id` INTEGER NOT NULL UNIQUE,
`favorite` INTEGER NOT NULL DEFAULT 0,
`archive` INTEGER NOT NULL DEFAULT 0,
`always_dl` INTEGER NOT NULL DEFAULT 0
);
INSERT INTO podcast (
id,
title,
link,
description,
image_uri,
source_id
) SELECT id,
title,
link,
description,
image_uri,
source_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,66 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
podcast_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
podcast_id
FROM old_table;
Drop table old_table;
ALTER TABLE podcast RENAME TO old_table;
CREATE TABLE `podcast` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`source_id` INTEGER NOT NULL UNIQUE
);
INSERT INTO podcast (
id,
title,
link,
description,
image_uri,
source_id
) SELECT id,
title,
link,
description,
image_uri,
source_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,40 @@
ALTER TABLE episodes RENAME TO old_table;
ALTER TABLE shows RENAME TO podcast;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
podcast_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
show_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,40 @@
ALTER TABLE episode RENAME TO old_table;
ALTER TABLE podcast RENAME TO shows;
CREATE TABLE episodes (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
show_id INTEGER NOT NULL,
PRIMARY KEY (title, show_id)
);
INSERT INTO episodes (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
show_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
podcast_id
FROM old_table;
Drop table old_table;
@@ -24,7 +24,9 @@ lazy_static! {
#[cfg(not(test))]
lazy_static! {
static ref DB_PATH: PathBuf = xdg_dirs::HAMMOND_XDG.place_data_file("hammond.db").unwrap();
static ref DB_PATH: PathBuf = xdg_dirs::PODCASTS_XDG
.place_data_file("podcasts.db")
.unwrap();
}
#[cfg(test)]
@@ -32,8 +34,8 @@ extern crate tempdir;
#[cfg(test)]
lazy_static! {
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("podcasts_unit_test").unwrap() };
static ref DB_PATH: PathBuf = TEMPDIR.path().join("podcasts.db");
}
/// Get an r2d2 `SqliteConnection`.
@@ -64,12 +66,12 @@ fn run_migration_on(connection: &SqliteConnection) -> Result<(), DataError> {
/// Reset the database into a clean state.
// Test share a Temp file db.
#[allow(dead_code)]
#[cfg(test)]
pub fn truncate_db() -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
con.execute("DELETE FROM episode")?;
con.execute("DELETE FROM podcast")?;
con.execute("DELETE FROM episodes")?;
con.execute("DELETE FROM shows")?;
con.execute("DELETE FROM source")?;
Ok(())
}
@@ -23,169 +23,156 @@ pub fn get_sources() -> Result<Vec<Source>, DataError> {
.map_err(From::from)
}
pub fn get_podcasts() -> Result<Vec<Podcast>, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcasts() -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.order(title.asc())
.load::<Podcast>(&con)
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.order(title.asc())
.filter(id.ne_all(filter_ids))
.load::<Podcast>(&con)
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.order(epoch.desc())
.load::<Episode>(&con)
.map_err(From::from)
}
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
use schema::episode::dsl::*;
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((rowid, local_uri, played))
.filter(local_uri.is_not_null())
.load::<EpisodeCleanerQuery>(&con)
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
// pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
// use schema::episode::dsl::*;
// use schema::episodes::dsl::*;
// let db = connection();
// let con = db.get()?;
// episode
// episodes
// .filter(played.is_not_null())
// .load::<Episode>(&con)
// .map_err(From::from)
// }
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
use schema::episode::dsl::*;
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((rowid, local_uri, played))
.filter(played.is_not_null())
.load::<EpisodeCleanerQuery>(&con)
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(rowid.eq(ep_id))
.get_result::<Episode>(&con)
.map_err(From::from)
}
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetQuery, DataError> {
use schema::episode::dsl::*;
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetModel, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
))
.filter(rowid.eq(ep_id))
.get_result::<EpisodeWidgetQuery>(&con)
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
)).filter(rowid.eq(ep_id))
.get_result::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(rowid.eq(ep_id))
.select(local_uri)
.get_result::<Option<String>>(&con)
.map_err(From::from)
}
pub fn get_episode_description(rowid_: i32) -> Result<Option<String>, DataError> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
episode
.filter(rowid.eq(rowid_))
.select(description)
.get_result::<Option<String>>(&con)
.map_err(From::from)
}
pub fn get_episodes_widgets_filter_limit(
filter_ids: &[i32],
limit: u32,
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
) -> Result<Vec<EpisodeWidgetModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
);
episode
episodes
.select(columns)
.order(epoch.desc())
.filter(podcast_id.ne_all(filter_ids))
.filter(show_id.ne_all(filter_ids))
.limit(i64::from(limit))
.load::<EpisodeWidgetQuery>(&con)
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_from_id(pid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.filter(id.eq(pid))
.get_result::<Podcast>(&con)
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcast_cover_from_id(pid: i32) -> Result<PodcastCoverQuery, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.select((id, title, image_uri))
.filter(id.eq(pid))
.get_result::<PodcastCoverQuery>(&con)
.get_result::<ShowCoverModel>(&con)
.map_err(From::from)
}
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -195,7 +182,7 @@ pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
let db = connection();
let con = db.get()?;
@@ -205,24 +192,24 @@ pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_episodeswidgets(parent: &Show) -> Result<Vec<EpisodeWidgetModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
);
episode
episodes
.select(columns)
.filter(podcast_id.eq(parent.id()))
.filter(show_id.eq(parent.id()))
.order(epoch.desc())
.load::<EpisodeWidgetQuery>(&con)
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -233,8 +220,8 @@ pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataEr
.map_err(From::from)
}
// pub(crate) fn get_pd_episodes_limit(parent: &Podcast, limit: u32) ->
// Result<Vec<Episode>, DataError> { use schema::episode::dsl::*;
// pub(crate) fn get_pd_episodes_limit(parent: &Show, limit: u32) ->
// Result<Vec<Episode>, DataError> { use schema::episodes::dsl::*;
// let db = connection();
// let con = db.get()?;
@@ -268,25 +255,25 @@ pub fn get_source_from_id(id_: i32) -> Result<Source, DataError> {
.map_err(From::from)
}
pub fn get_podcast_from_source_id(sid: i32) -> Result<Podcast, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_from_source_id(sid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.filter(source_id.eq(sid))
.get_result::<Podcast>(&con)
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(title.eq(title_))
.filter(podcast_id.eq(pid))
.filter(show_id.eq(pid))
.get_result::<Episode>(&con)
.map_err(From::from)
}
@@ -295,19 +282,36 @@ pub(crate) fn get_episode_minimal_from_pk(
title_: &str,
pid: i32,
) -> Result<EpisodeMinimal, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
.select((rowid, title, uri, epoch, duration, guid, podcast_id))
episodes
.select((rowid, title, uri, epoch, length, duration, guid, show_id))
.filter(title.eq(title_))
.filter(podcast_id.eq(pid))
.filter(show_id.eq(pid))
.get_result::<EpisodeMinimal>(&con)
.map_err(From::from)
}
pub(crate) fn remove_feed(pd: &Podcast) -> Result<(), DataError> {
#[cfg(test)]
pub(crate) fn get_episode_cleaner_from_pk(
title_: &str,
pid: i32,
) -> Result<EpisodeCleanerModel, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.select((rowid, local_uri, played))
.filter(title.eq(title_))
.filter(show_id.eq(pid))
.get_result::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
pub(crate) fn remove_feed(pd: &Show) -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
@@ -326,16 +330,16 @@ fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
diesel::delete(source.filter(id.eq(source_id))).execute(con)
}
fn delete_podcast(con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
fn delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize> {
use schema::shows::dsl::*;
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(con)
diesel::delete(shows.filter(id.eq(show_id))).execute(con)
}
fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(con)
diesel::delete(episodes.filter(show_id.eq(parent_id))).execute(con)
}
pub fn source_exists(url: &str) -> Result<bool, DataError> {
@@ -350,81 +354,84 @@ pub fn source_exists(url: &str) -> Result<bool, DataError> {
}
pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(podcast.filter(source_id.eq(source_id_))))
select(exists(shows.filter(source_id.eq(source_id_))))
.get_result(&con)
.map_err(From::from)
}
#[cfg_attr(rustfmt, rustfmt_skip)]
pub(crate) fn episode_exists(title_: &str, podcast_id_: i32) -> Result<bool, DataError> {
use schema::episode::dsl::*;
pub(crate) fn episode_exists(title_: &str, show_id_: i32) -> Result<bool, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.filter(podcast_id.eq(podcast_id_)).filter(title.eq(title_))))
select(exists(episodes.filter(show_id.eq(show_id_)).filter(title.eq(title_))))
.get_result(&con)
.map_err(From::from)
}
pub fn episode_had_description(rowid_: i32) -> Result<bool, DataError> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.filter(rowid.eq(rowid_))))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `episode` table contains any rows
/// Check if the `episodes table contains any rows
///
/// Return true if `episode` table is populated.
/// Return true if `episodes` table is populated.
pub fn is_episodes_populated() -> Result<bool, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.as_query()))
select(exists(episodes.as_query()))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `podcast` table contains any rows
/// Check if the `shows` table contains any rows
///
/// Return true if `podcast table is populated.
/// Return true if `shows` table is populated.
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(podcast.filter(id.ne_all(filter_ids))))
select(exists(shows.filter(id.ne_all(filter_ids))))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `source` table contains any rows
///
/// Return true if `source` table is populated.
pub fn is_source_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(source.filter(id.ne_all(filter_ids))))
.get_result(&con)
.map_err(From::from)
}
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(episode)
diesel::insert_into(episodes)
.values(eps)
.execute(&*con)
.map_err(From::from)
.map(|_| ())
}
pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
use schema::episode::dsl::*;
pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -441,24 +448,26 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
mod tests {
use super::*;
use database::*;
use failure::Error;
use pipeline;
#[test]
fn test_update_none_to_played_now() {
truncate_db().unwrap();
fn test_update_none_to_played_now() -> Result<(), Error> {
truncate_db()?;
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let source = Source::from_url(url)?;
let id = source.id();
pipeline::run(vec![source], true).unwrap();
let pd = get_podcast_from_source_id(id).unwrap();
pipeline::run(vec![source])?;
let pd = get_podcast_from_source_id(id)?;
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
let eps_num = get_pd_unplayed_episodes(&pd)?.len();
assert_ne!(eps_num, 0);
update_none_to_played_now(&pd).unwrap();
let eps_num2 = get_pd_unplayed_episodes(&pd).unwrap().len();
update_none_to_played_now(&pd)?;
let eps_num2 = get_pd_unplayed_episodes(&pd)?.len();
assert_eq!(eps_num2, 0);
Ok(())
}
}
@@ -11,7 +11,12 @@ use std::io;
use models::Source;
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
#[fail(
display = "Request to {} returned {}. Context: {}",
url,
status_code,
context
)]
#[derive(Fail, Debug)]
pub struct HttpStatusError {
url: String,
@@ -42,7 +47,6 @@ pub enum DataError {
#[fail(display = "Hyper Error: {}", _0)]
HyperError(#[cause] hyper::Error),
#[fail(display = "Failed to parse a url: {}", _0)]
// TODO: print the url too
UrlError(#[cause] url::ParseError),
#[fail(display = "TLS Error: {}", _0)]
TLSError(#[cause] native_tls::Error),
@@ -58,7 +62,10 @@ pub enum DataError {
HttpStatusGeneral(HttpStatusError),
#[fail(display = "FIXME: This should be better")]
F301(Source),
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
#[fail(
display = "Error occured while Parsing an Episode. Reason: {}",
reason
)]
ParseEpisodeError { reason: String, parent_id: i32 },
#[fail(display = "Episode was not changed and thus skipped.")]
EpisodeNotChanged,
@@ -9,7 +9,7 @@ use rss;
use dbqueries;
use errors::DataError;
use models::{Index, IndexState, Update};
use models::{NewEpisode, NewEpisodeMinimal, NewPodcast, Podcast};
use models::{NewEpisode, NewEpisodeMinimal, NewShow, Show};
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
@@ -31,15 +31,15 @@ impl Feed {
.and_then(move |pd| self.index_channel_items(pd))
}
fn parse_podcast(&self) -> NewPodcast {
NewPodcast::new(&self.channel, self.source_id)
fn parse_podcast(&self) -> NewShow {
NewShow::new(&self.channel, self.source_id)
}
fn parse_podcast_async(&self) -> impl Future<Item = NewPodcast, Error = DataError> + Send {
fn parse_podcast_async(&self) -> impl Future<Item = NewShow, Error = DataError> + Send {
ok(self.parse_podcast())
}
fn index_channel_items(self, pd: Podcast) -> impl Future<Item = (), Error = DataError> + Send {
fn index_channel_items(self, pd: Show) -> impl Future<Item = (), Error = DataError> + Send {
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
// Parse the episodes
@@ -65,12 +65,12 @@ fn determine_ep_state(
item: &rss::Item,
) -> Result<IndexState<NewEpisode>, DataError> {
// Check if feed exists
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
let exists = dbqueries::episode_exists(ep.title(), ep.show_id())?;
if !exists {
Ok(IndexState::Index(ep.into_new_episode(item)))
} else {
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.show_id())?;
let rowid = old.rowid();
if ep != old {
@@ -87,21 +87,22 @@ fn filter_episodes<'a, S>(
where
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
{
stream.filter_map(|state| match state {
IndexState::NotChanged => None,
// Update individual rows, and filter them
IndexState::Update((ref ep, rowid)) => {
ep.update(rowid)
.map_err(|err| error!("{}", err))
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.ok();
stream
.filter_map(|state| match state {
IndexState::NotChanged => None,
// Update individual rows, and filter them
IndexState::Update((ref ep, rowid)) => {
ep.update(rowid)
.map_err(|err| error!("{}", err))
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.ok();
None
},
IndexState::Index(s) => Some(s),
})
// only Index is left, collect them for batch index
.collect()
None
}
IndexState::Index(s) => Some(s),
})
// only Index is left, collect them for batch index
.collect()
}
fn batch_insert_episodes(episodes: &[NewEpisode]) {
@@ -114,8 +115,7 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
.map_err(|err| {
error!("Failed batch indexng: {}", err);
info!("Fallign back to individual indexing.");
})
.unwrap_or_else(|_| {
}).unwrap_or_else(|_| {
episodes.iter().for_each(|ep| {
ep.index()
.map_err(|err| error!("Error: {}.", err))
@@ -127,6 +127,7 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
#[cfg(test)]
mod tests {
use failure::Error;
use rss::Channel;
use tokio_core::reactor::Core;
@@ -171,8 +172,8 @@ mod tests {
};
#[test]
fn test_complete_index() {
truncate_db().unwrap();
fn test_complete_index() -> Result<(), Error> {
truncate_db()?;
let feeds: Vec<_> = URLS
.iter()
@@ -180,44 +181,46 @@ mod tests {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
get_feed(path, s.id())
})
.collect();
}).collect();
let mut core = Core::new().unwrap();
// Index the channels
let mut core = Core::new()?;
// Index the channes
let list: Vec<_> = feeds.into_iter().map(|x| x.index()).collect();
let _foo = core.run(join_all(list));
// Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources().unwrap().len(), 5);
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
assert_eq!(dbqueries::get_sources()?.len(), 5);
assert_eq!(dbqueries::get_podcasts()?.len(), 5);
assert_eq!(dbqueries::get_episodes()?.len(), 354);
Ok(())
}
#[test]
fn test_feed_parse_podcast() {
truncate_db().unwrap();
fn test_feed_parse_podcast() -> Result<(), Error> {
truncate_db()?;
let path = "tests/feeds/2018-01-20-Intercepted.xml";
let feed = get_feed(path, 42);
let file = fs::File::open(path).unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let file = fs::File::open(path)?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(feed.parse_podcast(), pd);
Ok(())
}
#[test]
fn test_feed_index_channel_items() {
truncate_db().unwrap();
fn test_feed_index_channel_items() -> Result<(), Error> {
truncate_db()?;
let path = "tests/feeds/2018-01-20-Intercepted.xml";
let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap();
let pd = feed.parse_podcast().to_podcast()?;
feed.index_channel_items(pd).wait().unwrap();
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 1);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43);
feed.index_channel_items(pd).wait()?;
assert_eq!(dbqueries::get_podcasts()?.len(), 1);
assert_eq!(dbqueries::get_episodes()?.len(), 43);
Ok(())
}
}
+145
View File
@@ -0,0 +1,145 @@
#![recursion_limit = "1024"]
#![allow(unknown_lints)]
#![cfg_attr(
all(test, feature = "clippy"),
allow(option_unwrap_used, result_unwrap_used)
)]
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
#![cfg_attr(
feature = "clippy",
warn(
option_unwrap_used,
result_unwrap_used,
print_stdout,
wrong_pub_self_convention,
mut_mut,
non_ascii_literal,
similar_names,
unicode_not_nfc,
enum_glob_use,
if_not_else,
items_after_statements,
used_underscore_binding
)
)]
// Enable lint group collections
#![warn(nonstandard_style, bad_style, unused)]
#![allow(edition_2018, rust_2018_idioms)]
// standalone lints
#![warn(
const_err,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
plugin_as_library,
private_no_mangle_fns,
private_no_mangle_statics,
unconditional_recursion,
unions_with_drop_fields,
while_true,
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
elided_lifetime_in_paths,
missing_copy_implementations
)]
#![deny(warnings)]
//! FIXME: Docs
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[cfg(test)]
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate derive_builder;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
// #[macro_use]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate ammonia;
extern crate chrono;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate native_tls;
extern crate num_cpus;
extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate tokio_core;
extern crate url;
extern crate xdg;
extern crate xml;
pub mod database;
#[allow(missing_docs)]
pub mod dbqueries;
#[allow(missing_docs)]
pub mod errors;
mod feed;
pub(crate) mod models;
pub mod opml;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils;
pub use feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetModel, Show, ShowCoverModel, Source};
// Set the user agent, See #53 for more
// Keep this in sync with Tor-browser releases
/// The user-agent to be used for all the requests.
/// It originates from the Tor-browser UA.
pub const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)]
pub mod xdg_dirs {
use std::path::PathBuf;
use xdg;
lazy_static!{
pub(crate) static ref PODCASTS_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("gnome-podcasts").unwrap()
};
/// XDG_DATA Directory `Pathbuf`.
pub static ref PODCASTS_DATA: PathBuf = {
PODCASTS_XDG.create_data_directory(PODCASTS_XDG.get_data_home()).unwrap()
};
/// XDG_CONFIG Directory `Pathbuf`.
pub static ref PODCASTS_CONFIG: PathBuf = {
PODCASTS_XDG.create_config_directory(PODCASTS_XDG.get_config_home()).unwrap()
};
/// XDG_CACHE Directory `Pathbuf`.
pub static ref PODCASTS_CACHE: PathBuf = {
PODCASTS_XDG.create_cache_directory(PODCASTS_XDG.get_cache_home()).unwrap()
};
/// GNOME Podcasts Download Direcotry `PathBuf`.
pub static ref DL_DIR: PathBuf = {
PODCASTS_XDG.create_data_directory("Downloads").unwrap()
};
}
}
@@ -5,14 +5,14 @@ use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Podcast, Save};
use schema::episode;
use models::{Save, Show};
use schema::episodes;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[belongs_to(Podcast, foreign_key = "podcast_id")]
#[primary_key(title, show_id)]
#[belongs_to(Show, foreign_key = "show_id")]
#[derive(Debug, Clone)]
/// Diesel Model of the episode table.
pub struct Episode {
@@ -26,9 +26,7 @@ pub struct Episode {
duration: Option<i32>,
guid: Option<String>,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
show_id: i32,
}
impl Save<Episode> for Episode {
@@ -55,11 +53,6 @@ impl Episode {
&self.title
}
/// Set the `title`.
pub fn set_title(&mut self, value: &str) {
self.title = value.to_string();
}
/// Get the value of the `uri`.
///
/// Represents the url(usually) that the media file will be located at.
@@ -67,11 +60,6 @@ impl Episode {
self.uri.as_ref().map(|s| s.as_str())
}
/// Set the `uri`.
pub fn set_uri(&mut self, value: Option<&str>) {
self.uri = value.map(|x| x.to_string());
}
/// Get the value of the `local_uri`.
///
/// Represents the local uri,usually filesystem path,
@@ -80,31 +68,16 @@ impl Episode {
self.local_uri.as_ref().map(|s| s.as_str())
}
/// Set the `local_uri`.
pub fn set_local_uri(&mut self, value: Option<&str>) {
self.local_uri = value.map(|x| x.to_string());
}
/// Get the `description`.
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
/// Set the `description`.
pub fn set_description(&mut self, value: Option<&str>) {
self.description = value.map(|x| x.to_string());
}
/// Get the Episode's `guid`.
pub fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
/// Set the `guid`.
pub fn set_guid(&mut self, value: Option<&str>) {
self.guid = value.map(|x| x.to_string());
}
/// Get the `epoch` value.
///
/// Retrieved from the rss Item publish date.
@@ -113,11 +86,6 @@ impl Episode {
self.epoch
}
/// Set the `epoch`.
pub fn set_epoch(&mut self, value: i32) {
self.epoch = value;
}
/// Get the `length`.
///
/// The number represents the size of the file in bytes.
@@ -125,11 +93,6 @@ impl Episode {
self.length
}
/// Set the `length`.
pub fn set_length(&mut self, value: Option<i32>) {
self.length = value;
}
/// Get the `duration` value.
///
/// The number represents the duration of the item/episode in seconds.
@@ -137,11 +100,6 @@ impl Episode {
self.duration
}
/// Set the `duration`.
pub fn set_duration(&mut self, value: Option<i32>) {
self.duration = value;
}
/// Epoch representation of the last time the episode was played.
///
/// None/Null for unplayed.
@@ -149,54 +107,19 @@ impl Episode {
self.played
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
///
/// If true, the download cleanr will ignore the episode
/// and the corresponding media value will never be automaticly deleted.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Episode`.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
}
/// Sets the `played` value with the current `epoch` timestap and save it.
pub fn set_played_now(&mut self) -> Result<(), DataError> {
let epoch = Utc::now().timestamp() as i32;
self.set_played(Some(epoch));
self.save().map(|_| ())
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for constructing `EpisodeWidgets`.
pub struct EpisodeWidgetQuery {
pub struct EpisodeWidgetModel {
rowid: i32,
title: String,
uri: Option<String>,
@@ -205,14 +128,12 @@ pub struct EpisodeWidgetQuery {
length: Option<i32>,
duration: Option<i32>,
played: Option<i32>,
// favorite: bool,
// archive: bool,
podcast_id: i32,
show_id: i32,
}
impl From<Episode> for EpisodeWidgetQuery {
fn from(e: Episode) -> EpisodeWidgetQuery {
EpisodeWidgetQuery {
impl From<Episode> for EpisodeWidgetModel {
fn from(e: Episode) -> EpisodeWidgetModel {
EpisodeWidgetModel {
rowid: e.rowid,
title: e.title,
uri: e.uri,
@@ -221,30 +142,30 @@ impl From<Episode> for EpisodeWidgetQuery {
length: e.length,
duration: e.duration,
played: e.played,
podcast_id: e.podcast_id,
show_id: e.show_id,
}
}
}
impl Save<usize> for EpisodeWidgetQuery {
impl Save<usize> for EpisodeWidgetModel {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episode.filter(rowid.eq(self.rowid)))
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl EpisodeWidgetQuery {
impl EpisodeWidgetModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
@@ -302,11 +223,6 @@ impl EpisodeWidgetQuery {
self.duration
}
/// Set the `duration`.
pub fn set_duration(&mut self, value: Option<i32>) {
self.duration = value;
}
/// Epoch representation of the last time the episode was played.
///
/// None/Null for unplayed.
@@ -315,36 +231,13 @@ impl EpisodeWidgetQuery {
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
// /// Represents the archiving policy for the episode.
// pub fn archive(&self) -> bool {
// self.archive
// }
// /// Set the `archive` policy.
// ///
// /// If true, the download cleanr will ignore the episode
// /// and the corresponding media value will never be automaticly deleted.
// pub fn set_archive(&mut self, b: bool) {
// self.archive = b
// }
// /// Get the `favorite` status of the `Episode`.
// pub fn favorite(&self) -> bool {
// self.favorite
// }
// /// Set `favorite` status.
// pub fn set_favorite(&mut self, b: bool) {
// self.favorite = b
// }
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
/// Sets the `played` value with the current `epoch` timestap and save it.
@@ -356,38 +249,38 @@ impl EpisodeWidgetQuery {
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used internal with the `utils::checkup` function.
pub struct EpisodeCleanerQuery {
pub struct EpisodeCleanerModel {
rowid: i32,
local_uri: Option<String>,
played: Option<i32>,
}
impl Save<usize> for EpisodeCleanerQuery {
impl Save<usize> for EpisodeCleanerModel {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episode.filter(rowid.eq(self.rowid)))
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl From<Episode> for EpisodeCleanerQuery {
fn from(e: Episode) -> EpisodeCleanerQuery {
EpisodeCleanerQuery {
impl From<Episode> for EpisodeCleanerModel {
fn from(e: Episode) -> EpisodeCleanerModel {
EpisodeCleanerModel {
rowid: e.rowid(),
local_uri: e.local_uri,
played: e.played,
@@ -395,7 +288,7 @@ impl From<Episode> for EpisodeCleanerQuery {
}
}
impl EpisodeCleanerQuery {
impl EpisodeCleanerModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
@@ -428,9 +321,9 @@ impl EpisodeCleanerQuery {
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for FIXME.
pub struct EpisodeMinimal {
@@ -438,9 +331,10 @@ pub struct EpisodeMinimal {
title: String,
uri: Option<String>,
epoch: i32,
length: Option<i32>,
duration: Option<i32>,
guid: Option<String>,
podcast_id: i32,
show_id: i32,
}
impl From<Episode> for EpisodeMinimal {
@@ -449,10 +343,11 @@ impl From<Episode> for EpisodeMinimal {
rowid: e.rowid,
title: e.title,
uri: e.uri,
length: e.length,
guid: e.guid,
epoch: e.epoch,
duration: e.duration,
podcast_id: e.podcast_id,
show_id: e.show_id,
}
}
}
@@ -488,6 +383,13 @@ impl EpisodeMinimal {
self.epoch
}
/// Get the `length`.
///
/// The number represents the size of the file in bytes.
pub fn length(&self) -> Option<i32> {
self.length
}
/// Get the `duration` value.
///
/// The number represents the duration of the item/episode in seconds.
@@ -495,8 +397,8 @@ impl EpisodeMinimal {
self.duration
}
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
}
@@ -1,26 +1,26 @@
mod new_episode;
mod new_podcast;
mod new_show;
mod new_source;
mod episode;
mod podcast;
mod show;
mod source;
// use futures::prelude::*;
// use futures::future::*;
pub(crate) use self::episode::EpisodeCleanerQuery;
pub(crate) use self::episode::EpisodeCleanerModel;
pub(crate) use self::new_episode::{NewEpisode, NewEpisodeMinimal};
pub(crate) use self::new_podcast::NewPodcast;
pub(crate) use self::new_show::NewShow;
pub(crate) use self::new_source::NewSource;
#[cfg(test)]
pub(crate) use self::new_episode::NewEpisodeBuilder;
#[cfg(test)]
pub(crate) use self::new_podcast::NewPodcastBuilder;
pub(crate) use self::new_show::NewShowBuilder;
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetQuery};
pub use self::podcast::{Podcast, PodcastCoverQuery};
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetModel};
pub use self::show::{Show, ShowCoverModel};
pub use self::source::Source;
#[derive(Debug, Clone, PartialEq)]
@@ -9,11 +9,11 @@ use dbqueries;
use errors::DataError;
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
use parser;
use schema::episode;
use schema::episodes;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
@@ -26,7 +26,7 @@ pub(crate) struct NewEpisode {
duration: Option<i32>,
guid: Option<String>,
epoch: i32,
podcast_id: i32,
show_id: i32,
}
impl From<NewEpisodeMinimal> for NewEpisode {
@@ -36,7 +36,7 @@ impl From<NewEpisodeMinimal> for NewEpisode {
.uri(e.uri)
.duration(e.duration)
.epoch(e.epoch)
.podcast_id(e.podcast_id)
.show_id(e.show_id)
.guid(e.guid)
.build()
.unwrap()
@@ -47,12 +47,12 @@ impl Insert<()> for NewEpisode {
type Error = DataError;
fn insert(&self) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Inserting {:?}", self.title);
diesel::insert_into(episode)
diesel::insert_into(episodes)
.values(self)
.execute(&con)
.map_err(From::from)
@@ -64,12 +64,12 @@ impl Update<()> for NewEpisode {
type Error = DataError;
fn update(&self, episode_id: i32) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Updating {:?}", self.title);
diesel::update(episode.filter(rowid.eq(episode_id)))
diesel::update(episodes.filter(rowid.eq(episode_id)))
.set(self)
.execute(&con)
.map_err(From::from)
@@ -83,10 +83,10 @@ impl Index<()> for NewEpisode {
// Does not update the episode description if it's the only thing that has
// changed.
fn index(&self) -> Result<(), DataError> {
let exists = dbqueries::episode_exists(self.title(), self.podcast_id())?;
let exists = dbqueries::episode_exists(self.title(), self.show_id())?;
if exists {
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.podcast_id())?;
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.show_id())?;
if self != &other {
self.update(other.rowid())
@@ -106,7 +106,7 @@ impl PartialEq<EpisodeMinimal> for NewEpisode {
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.podcast_id() == other.podcast_id())
&& (self.show_id() == other.show_id())
}
}
@@ -117,7 +117,7 @@ impl PartialEq<Episode> for NewEpisode {
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.podcast_id() == other.podcast_id())
&& (self.show_id() == other.show_id())
&& (self.description() == other.description())
&& (self.length() == other.length())
}
@@ -126,14 +126,14 @@ impl PartialEq<Episode> for NewEpisode {
impl NewEpisode {
/// Parses an `rss::Item` into a `NewEpisode` Struct.
#[allow(dead_code)]
pub(crate) fn new(item: &rss::Item, podcast_id: i32) -> Result<Self, DataError> {
NewEpisodeMinimal::new(item, podcast_id).map(|ep| ep.into_new_episode(item))
pub(crate) fn new(item: &rss::Item, show_id: i32) -> Result<Self, DataError> {
NewEpisodeMinimal::new(item, show_id).map(|ep| ep.into_new_episode(item))
}
#[allow(dead_code)]
pub(crate) fn to_episode(&self) -> Result<Episode, DataError> {
self.index()?;
dbqueries::get_episode_from_pk(&self.title, self.podcast_id).map_err(From::from)
dbqueries::get_episode_from_pk(&self.title, self.show_id).map_err(From::from)
}
}
@@ -167,23 +167,24 @@ impl NewEpisode {
self.length
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[derive(Debug, Clone, Builder, PartialEq)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewEpisodeMinimal {
title: String,
uri: Option<String>,
length: Option<i32>,
duration: Option<i32>,
epoch: i32,
guid: Option<String>,
podcast_id: i32,
show_id: i32,
}
impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
@@ -193,7 +194,7 @@ impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.podcast_id() == other.podcast_id())
&& (self.show_id() == other.show_id())
}
}
@@ -211,12 +212,20 @@ impl NewEpisodeMinimal {
let title = item.title().unwrap().trim().to_owned();
let guid = item.guid().map(|s| s.value().trim().to_owned());
let uri = item.enclosure()
// Get the mime type, the `http` url and the length from the enclosure
// http://www.rssboard.org/rss-specification#ltenclosuregtSubelementOfLtitemgt
let enc = item.enclosure();
// Get the url
let uri = enc
.map(|s| url_cleaner(s.url().trim()))
// Fallback to Rss.Item.link if enclosure is None.
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
// If url is still None return an Error as this behaviour is
// Get the size of the content, it should be in bytes
let length = enc.and_then(|x| x.length().parse().ok());
// If url is still None return an Error as this behaviour is not
// compliant with the RSS Spec.
if uri.is_none() {
let err = DataError::ParseEpisodeError {
@@ -238,17 +247,18 @@ impl NewEpisodeMinimal {
NewEpisodeMinimalBuilder::default()
.title(title)
.uri(uri)
.length(length)
.duration(duration)
.epoch(epoch)
.guid(guid)
.podcast_id(parent_id)
.show_id(parent_id)
.build()
.map_err(From::from)
}
// TODO: TryInto is stabilizing in rustc v1.26!
// ^ Jokes on you past self!
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
let length = item.enclosure().and_then(|x| x.length().parse().ok());
let description = item.description().and_then(|s| {
let sanitized_html = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
@@ -263,9 +273,9 @@ impl NewEpisodeMinimal {
.uri(self.uri)
.duration(self.duration)
.epoch(self.epoch)
.podcast_id(self.podcast_id)
.show_id(self.show_id)
.guid(self.guid)
.length(length)
.length(self.length)
.description(description)
.build()
.unwrap()
@@ -294,14 +304,16 @@ impl NewEpisodeMinimal {
self.epoch
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[cfg(test)]
mod tests {
use database::truncate_db;
use dbqueries;
use failure::Error;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use models::*;
@@ -320,11 +332,11 @@ mod tests {
.title("The Super Bowl of Racism")
.uri(Some(String::from(
"http://traffic.megaphone.fm/PPY6458293736.mp3",
)))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
))).guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.epoch(1505296800)
.length(Some(66738886))
.duration(Some(4171))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -333,11 +345,11 @@ mod tests {
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
.uri(Some(String::from(
"http://traffic.megaphone.fm/FL5331443769.mp3",
)))
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
))).guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
.epoch(1502272800)
.length(Some(67527575))
.duration(Some(4415))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -352,13 +364,12 @@ mod tests {
.title("The Super Bowl of Racism")
.uri(Some(String::from(
"http://traffic.megaphone.fm/PPY6458293736.mp3",
)))
.description(Some(String::from(descr)))
))).description(Some(String::from(descr)))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(4171))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -376,13 +387,12 @@ mod tests {
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
.uri(Some(String::from(
"http://traffic.megaphone.fm/FL5331443769.mp3",
)))
.description(Some(String::from(descr)))
))).description(Some(String::from(descr)))
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
.length(Some(67527575))
.epoch(1502272800)
.duration(Some(4415))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -391,13 +401,12 @@ mod tests {
.title("The Super Bowl of Racism")
.uri(Some(String::from(
"http://traffic.megaphone.fm/PPY6458293736.mp3",
)))
.description(Some(String::from("New description")))
))).description(Some(String::from("New description")))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(424242))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -406,11 +415,11 @@ mod tests {
.title("Hacking Devices with Kali Linux | LUP 214")
.uri(Some(String::from(
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
)))
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
))).guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -419,11 +428,11 @@ mod tests {
.title("Gnome Does it Again | LUP 213")
.uri(Some(String::from(
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
)))
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
))).guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
.epoch(1504670247)
.length(Some(36544272))
.duration(Some(4491))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -437,13 +446,12 @@ mod tests {
.title("Hacking Devices with Kali Linux | LUP 214")
.uri(Some(String::from(
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
)))
.description(Some(String::from(descr)))
))).description(Some(String::from(descr)))
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -459,86 +467,93 @@ mod tests {
.title("Gnome Does it Again | LUP 213")
.uri(Some(String::from(
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
)))
.description(Some(String::from(descr)))
))).description(Some(String::from(descr)))
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
.length(Some(36544272))
.epoch(1504670247)
.duration(Some(4491))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
}
#[test]
fn test_new_episode_minimal_intercepted() {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_episode_minimal_intercepted() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let episode = channel.items().iter().nth(14).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_1);
let episode = channel.items().iter().nth(15).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_2);
Ok(())
}
#[test]
fn test_new_episode_intercepted() {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_episode_intercepted() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let episode = channel.items().iter().nth(14).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
let ep = NewEpisode::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
let episode = channel.items().iter().nth(15).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
let ep = NewEpisode::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
Ok(())
}
#[test]
fn test_new_episode_minimal_lup() {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_episode_minimal_lup() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let episode = channel.items().iter().nth(18).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_1);
let episode = channel.items().iter().nth(19).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_2);
Ok(())
}
#[test]
fn test_new_episode_lup() {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_episode_lup() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let episode = channel.items().iter().nth(18).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
let ep = NewEpisode::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_LUP_1);
let episode = channel.items().iter().nth(19).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
let ep = NewEpisode::new(&episode, 42)?;
assert_eq!(ep, *EXPECTED_LUP_2);
Ok(())
}
#[test]
fn test_minimal_into_new_episode() {
truncate_db().unwrap();
fn test_minimal_into_new_episode() -> Result<(), Error> {
truncate_db()?;
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let item = channel.items().iter().nth(14).unwrap();
let ep = EXPECTED_MINIMAL_INTERCEPTED_1
.clone()
.into_new_episode(&item);
println!(
"EPISODE: {:#?}\nEXPECTED: {:#?}",
ep, *EXPECTED_INTERCEPTED_1
);
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
let item = channel.items().iter().nth(15).unwrap();
@@ -546,61 +561,58 @@ mod tests {
.clone()
.into_new_episode(&item);
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
Ok(())
}
#[test]
fn test_new_episode_insert() {
truncate_db().unwrap();
fn test_new_episode_insert() -> Result<(), Error> {
truncate_db()?;
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let episode = channel.items().iter().nth(14).unwrap();
let new_ep = NewEpisode::new(&episode, 42).unwrap();
new_ep.insert().unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
let new_ep = NewEpisode::new(&episode, 42)?;
new_ep.insert()?;
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id())?;
assert_eq!(new_ep, ep);
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_1);
assert_eq!(&*EXPECTED_INTERCEPTED_1, &ep);
let episode = channel.items().iter().nth(15).unwrap();
let new_ep = NewEpisode::new(&episode, 42).unwrap();
new_ep.insert().unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
let new_ep = NewEpisode::new(&episode, 42)?;
new_ep.insert()?;
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id())?;
assert_eq!(new_ep, ep);
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_2);
assert_eq!(&*EXPECTED_INTERCEPTED_2, &ep);
Ok(())
}
#[test]
fn test_new_episode_update() {
truncate_db().unwrap();
let old = EXPECTED_INTERCEPTED_1.clone().to_episode().unwrap();
fn test_new_episode_update() -> Result<(), Error> {
truncate_db()?;
let old = EXPECTED_INTERCEPTED_1.clone().to_episode()?;
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
updated.update(old.rowid()).unwrap();
let mut new = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
updated.update(old.rowid())?;
let new = dbqueries::get_episode_from_pk(old.title(), old.show_id())?;
// Assert that updating does not change the rowid and podcast_id
// Assert that updating does not change the rowid and show_id
assert_ne!(old, new);
assert_eq!(old.rowid(), new.rowid());
assert_eq!(old.podcast_id(), new.podcast_id());
assert_eq!(old.show_id(), new.show_id());
assert_eq!(updated, &new);
assert_ne!(updated, &old);
new.set_archive(true);
new.save().unwrap();
let new2 = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
assert_eq!(true, new2.archive());
Ok(())
}
#[test]
fn test_new_episode_index() {
truncate_db().unwrap();
fn test_new_episode_index() -> Result<(), Error> {
truncate_db()?;
let expected = &*EXPECTED_INTERCEPTED_1;
// First insert
@@ -608,7 +620,7 @@ mod tests {
// Second identical, This should take the early return path
assert!(expected.index().is_ok());
// Get the episode
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
// Assert that NewPodcast is equal to the Indexed one
assert_eq!(*expected, old);
@@ -617,42 +629,33 @@ mod tests {
// Update the podcast
assert!(updated.index().is_ok());
// Get the new Podcast
let new = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let new = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
// Assert it's diff from the old one.
assert_ne!(new, old);
assert_eq!(*updated, new);
assert_eq!(new.rowid(), old.rowid());
assert_eq!(new.podcast_id(), old.podcast_id());
assert_eq!(new.show_id(), old.show_id());
Ok(())
}
#[test]
fn test_new_episode_to_episode() {
fn test_new_episode_to_episode() -> Result<(), Error> {
let expected = &*EXPECTED_INTERCEPTED_1;
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
// Assert insert() produces the same result that you would get with to_podcast()
truncate_db().unwrap();
expected.insert().unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let ep = expected.to_episode().unwrap();
truncate_db()?;
expected.insert()?;
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
let ep = expected.to_episode()?;
assert_eq!(old, ep);
// Same as above, diff order
truncate_db().unwrap();
let ep = expected.to_episode().unwrap();
truncate_db()?;
let ep = expected.to_episode()?;
// This should error as a unique constrain violation
assert!(expected.insert().is_err());
let mut old =
dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
assert_eq!(old, ep);
old.set_archive(true);
old.save().unwrap();
// Assert that it does not mess with user preferences
let ep = updated.to_episode().unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
assert_eq!(old, ep);
assert_eq!(old.archive(), true);
Ok(())
}
}
@@ -4,21 +4,21 @@ use diesel::prelude::*;
use rss;
use errors::DataError;
use models::Podcast;
use models::Show;
use models::{Index, Insert, Update};
use schema::podcast;
use schema::shows;
use database::connection;
use dbqueries;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "podcast"]
#[table_name = "shows"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewPodcast {
pub(crate) struct NewShow {
title: String,
link: String,
description: String,
@@ -26,15 +26,15 @@ pub(crate) struct NewPodcast {
source_id: i32,
}
impl Insert<()> for NewPodcast {
impl Insert<()> for NewShow {
type Error = DataError;
fn insert(&self) -> Result<(), Self::Error> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(podcast)
diesel::insert_into(shows)
.values(self)
.execute(&con)
.map(|_| ())
@@ -42,16 +42,16 @@ impl Insert<()> for NewPodcast {
}
}
impl Update<()> for NewPodcast {
impl Update<()> for NewShow {
type Error = DataError;
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
use schema::podcast::dsl::*;
fn update(&self, show_id: i32) -> Result<(), Self::Error> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
info!("Updating {}", self.title);
diesel::update(podcast.filter(id.eq(podcast_id)))
diesel::update(shows.filter(id.eq(show_id)))
.set(self)
.execute(&con)
.map(|_| ())
@@ -61,7 +61,7 @@ impl Update<()> for NewPodcast {
// TODO: Maybe return an Enum<Action(Resut)> Instead.
// It would make unti testing better too.
impl Index<()> for NewPodcast {
impl Index<()> for NewShow {
type Error = DataError;
fn index(&self) -> Result<(), DataError> {
@@ -81,8 +81,8 @@ impl Index<()> for NewPodcast {
}
}
impl PartialEq<Podcast> for NewPodcast {
fn eq(&self, other: &Podcast) -> bool {
impl PartialEq<Show> for NewShow {
fn eq(&self, other: &Show) -> bool {
(self.link() == other.link())
&& (self.title() == other.title())
&& (self.image_uri() == other.image_uri())
@@ -91,9 +91,9 @@ impl PartialEq<Podcast> for NewPodcast {
}
}
impl NewPodcast {
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
impl NewShow {
/// Parses a `rss::Channel` into a `NewShow` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewShow {
let title = chan.title().trim();
let link = url_cleaner(chan.link().trim());
@@ -111,7 +111,7 @@ impl NewPodcast {
// If itunes is None, try to get the channel.image from the rss spec
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().trim().to_owned()));
NewPodcastBuilder::default()
NewShowBuilder::default()
.title(title)
.description(description)
.link(link)
@@ -122,14 +122,14 @@ impl NewPodcast {
}
// Look out for when tryinto lands into stable.
pub(crate) fn to_podcast(&self) -> Result<Podcast, DataError> {
pub(crate) fn to_podcast(&self) -> Result<Show, DataError> {
self.index()?;
dbqueries::get_podcast_from_source_id(self.source_id).map_err(From::from)
}
}
// Ignore the following geters. They are used in unit tests mainly.
impl NewPodcast {
impl NewShow {
#[allow(dead_code)]
pub(crate) fn source_id(&self) -> i32 {
self.source_id
@@ -157,17 +157,18 @@ mod tests {
use super::*;
// use tokio_core::reactor::Core;
use failure::Error;
use rss::Channel;
use database::truncate_db;
use models::{NewPodcastBuilder, Save};
use models::NewShowBuilder;
use std::fs::File;
use std::io::BufReader;
// Pre-built expected NewPodcast structs.
// Pre-built expected NewShow structs.
lazy_static! {
static ref EXPECTED_INTERCEPTED: NewPodcast = {
static ref EXPECTED_INTERCEPTED: NewShow = {
let descr = "The people behind The Intercepts fearless reporting and incisive \
commentaryJeremy Scahill, Glenn Greenwald, Betsy Reed and \
othersdiscuss the crucial issues of our time: national security, civil \
@@ -175,7 +176,7 @@ mod tests {
artists, thinkers, and newsmakers who challenge our preconceptions about \
the world we live in.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description(descr)
@@ -188,23 +189,22 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_LUP: NewPodcast = {
static ref EXPECTED_LUP: NewShow = {
let descr = "An open show powered by community LINUX Unplugged takes the best \
attributes of open collaboration and focuses them into a weekly \
lifestyle show about Linux.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("LINUX Unplugged Podcast")
.link("http://www.jupiterbroadcasting.com/")
.description(descr)
.image_uri(Some(String::from(
"http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg",
)))
.source_id(42)
))).source_id(42)
.build()
.unwrap()
};
static ref EXPECTED_TIPOFF: NewPodcast = {
static ref EXPECTED_TIPOFF: NewShow = {
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
scenes of some of the best investigative journalism from recent years. \
Each episode well be digging into an investigative scoop- hearing from \
@@ -215,19 +215,18 @@ mod tests {
complicated detective work that goes into doing great investigative \
journalism- then this is the podcast for you.</p>";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("The Tip Off")
.link("http://www.acast.com/thetipoff")
.description(desc)
.image_uri(Some(String::from(
"https://imagecdn.acast.com/image?h=1500&w=1500&source=http%3A%2F%2Fi1.sndcdn.\
com%2Favatars-000317856075-a2coqz-original.jpg",
)))
.source_id(42)
))).source_id(42)
.build()
.unwrap()
};
static ref EXPECTED_STARS: NewPodcast = {
static ref EXPECTED_STARS: NewShow = {
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
love, a crashed UFO, an alien body, and an impossible heist unlike any \
@@ -237,7 +236,7 @@ mod tests {
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Steal the Stars")
.link("http://tor-labs.com/")
.description(descr)
@@ -246,24 +245,23 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_CODE: NewPodcast = {
static ref EXPECTED_CODE: NewShow = {
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
Livingston-Gray. Brought to you by @therubyrep.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Greater Than Code")
.link("https://www.greaterthancode.com/")
.description(descr)
.image_uri(Some(String::from(
"http://www.greaterthancode.com/wp-content/uploads/2016/10/code1400-4.jpg",
)))
.source_id(42)
))).source_id(42)
.build()
.unwrap()
};
static ref EXPECTED_ELLINOFRENEIA: NewPodcast = {
NewPodcastBuilder::default()
static ref EXPECTED_ELLINOFRENEIA: NewShow = {
NewShowBuilder::default()
.title("Ελληνοφρένεια")
.link("https://ellinofreneia.sealabs.net/feed.rss")
.description("Ανεπίσημο feed της Ελληνοφρένειας")
@@ -272,8 +270,8 @@ mod tests {
.build()
.unwrap()
};
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
NewPodcastBuilder::default()
static ref UPDATED_DESC_INTERCEPTED: NewShow = {
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description("New Description")
@@ -289,73 +287,80 @@ mod tests {
}
#[test]
fn test_new_podcast_intercepted() {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_intercepted() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_INTERCEPTED, pd);
Ok(())
}
#[test]
fn test_new_podcast_lup() {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_lup() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_LUP, pd);
Ok(())
}
#[test]
fn test_new_podcast_thetipoff() {
let file = File::open("tests/feeds/2018-01-20-TheTipOff.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_thetipoff() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-TheTipOff.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_TIPOFF, pd);
Ok(())
}
#[test]
fn test_new_podcast_steal_the_stars() {
let file = File::open("tests/feeds/2018-01-20-StealTheStars.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_steal_the_stars() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-StealTheStars.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_STARS, pd);
Ok(())
}
#[test]
fn test_new_podcast_greater_than_code() {
let file = File::open("tests/feeds/2018-01-20-GreaterThanCode.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_greater_than_code() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-01-20-GreaterThanCode.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_CODE, pd);
Ok(())
}
#[test]
fn test_new_podcast_ellinofreneia() {
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_ellinofreneia() -> Result<(), Error> {
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
Ok(())
}
#[test]
// This maybe could be a doc test on insert.
fn test_new_podcast_insert() {
truncate_db().unwrap();
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
fn test_new_podcast_insert() -> Result<(), Error> {
truncate_db()?;
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
let channel = Channel::read_from(BufReader::new(file))?;
let npd = NewPodcast::new(&channel, 42);
npd.insert().unwrap();
let pd = dbqueries::get_podcast_from_source_id(42).unwrap();
let npd = NewShow::new(&channel, 42);
npd.insert()?;
let pd = dbqueries::get_podcast_from_source_id(42)?;
assert_eq!(npd, pd);
assert_eq!(*EXPECTED_INTERCEPTED, npd);
assert_eq!(&*EXPECTED_INTERCEPTED, &pd);
Ok(())
}
#[test]
@@ -363,77 +368,64 @@ mod tests {
// Currently there's a test that only checks new description or title.
// If you have time and want to help, implement the test for the other fields
// too.
fn test_new_podcast_update() {
truncate_db().unwrap();
let old = EXPECTED_INTERCEPTED.to_podcast().unwrap();
fn test_new_podcast_update() -> Result<(), Error> {
truncate_db()?;
let old = EXPECTED_INTERCEPTED.to_podcast()?;
let updated = &*UPDATED_DESC_INTERCEPTED;
updated.update(old.id()).unwrap();
let mut new = dbqueries::get_podcast_from_source_id(42).unwrap();
updated.update(old.id())?;
let new = dbqueries::get_podcast_from_source_id(42)?;
assert_ne!(old, new);
assert_eq!(old.id(), new.id());
assert_eq!(old.source_id(), new.source_id());
assert_eq!(updated, &new);
assert_ne!(updated, &old);
// Chech that the update does not override user preferences.
new.set_archive(true);
new.save().unwrap();
let new2 = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(true, new2.archive());
Ok(())
}
#[test]
fn test_new_podcast_index() {
truncate_db().unwrap();
fn test_new_podcast_index() -> Result<(), Error> {
truncate_db()?;
// First insert
assert!(EXPECTED_INTERCEPTED.index().is_ok());
// Second identical, This should take the early return path
assert!(EXPECTED_INTERCEPTED.index().is_ok());
// Get the podcast
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
// Assert that NewPodcast is equal to the Indexed one
let old = dbqueries::get_podcast_from_source_id(42)?;
// Assert that NewShow is equal to the Indexed one
assert_eq!(&*EXPECTED_INTERCEPTED, &old);
let updated = &*UPDATED_DESC_INTERCEPTED;
// Update the podcast
assert!(updated.index().is_ok());
// Get the new Podcast
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
// Get the new Show
let new = dbqueries::get_podcast_from_source_id(42)?;
// Assert it's diff from the old one.
assert_ne!(new, old);
assert_eq!(new.id(), old.id());
assert_eq!(new.source_id(), old.source_id());
Ok(())
}
#[test]
fn test_to_podcast() {
fn test_to_podcast() -> Result<(), Error> {
// Assert insert() produces the same result that you would get with to_podcast()
truncate_db().unwrap();
EXPECTED_INTERCEPTED.insert().unwrap();
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
truncate_db()?;
EXPECTED_INTERCEPTED.insert()?;
let old = dbqueries::get_podcast_from_source_id(42)?;
let pd = EXPECTED_INTERCEPTED.to_podcast()?;
assert_eq!(old, pd);
// Same as above, diff order
truncate_db().unwrap();
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
truncate_db()?;
let pd = EXPECTED_INTERCEPTED.to_podcast()?;
// This should error as a unique constrain violation
assert!(EXPECTED_INTERCEPTED.insert().is_err());
let mut old = dbqueries::get_podcast_from_source_id(42).unwrap();
let old = dbqueries::get_podcast_from_source_id(42)?;
assert_eq!(old, pd);
old.set_archive(true);
old.save().unwrap();
// Assert that it does not mess with user preferences
let pd = UPDATED_DESC_INTERCEPTED.to_podcast().unwrap();
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(old, pd);
assert_eq!(old.archive(), true);
Ok(())
}
}
+91
View File
@@ -0,0 +1,91 @@
use models::Source;
use schema::shows;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[belongs_to(Source, foreign_key = "source_id")]
#[changeset_options(treat_none_as_null = "true")]
#[table_name = "shows"]
#[derive(Debug, Clone)]
/// Diesel Model of the shows table.
pub struct Show {
id: i32,
title: String,
link: String,
description: String,
image_uri: Option<String>,
source_id: i32,
}
impl Show {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
}
/// Get the Feed `title`.
pub fn title(&self) -> &str {
&self.title
}
/// Get the Feed `link`.
///
/// Usually the website/homepage of the content creator.
pub fn link(&self) -> &str {
&self.link
}
/// Get the `description`.
pub fn description(&self) -> &str {
&self.description
}
/// Get the `image_uri`.
///
/// Represents the uri(url usually) that the Feed cover image is located at.
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
/// `Source` table foreign key.
pub fn source_id(&self) -> i32 {
self.source_id
}
}
#[derive(Queryable, Debug, Clone)]
/// Diesel Model of the Show cover query.
/// Used for fetching information about a Show's cover.
pub struct ShowCoverModel {
id: i32,
title: String,
image_uri: Option<String>,
}
impl From<Show> for ShowCoverModel {
fn from(p: Show) -> ShowCoverModel {
ShowCoverModel {
id: p.id(),
title: p.title,
image_uri: p.image_uri,
}
}
}
impl ShowCoverModel {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
}
/// Get the Feed `title`.
pub fn title(&self) -> &str {
&self.title
}
/// Get the `image_uri`.
///
/// Represents the uri(url usually) that the Feed cover image is located at.
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
}
@@ -91,7 +91,7 @@ impl Source {
/// Extract Etag and LastModifier from res, and update self and the
/// corresponding db row.
fn update_etag(&mut self, res: &Response) -> Result<(), DataError> {
fn update_etag(mut self, res: &Response) -> Result<Self, DataError> {
let headers = res.headers();
let etag = headers.get::<ETag>().map(|x| x.tag());
@@ -100,10 +100,24 @@ impl Source {
if (self.http_etag() != etag) || (self.last_modified != lmod) {
self.set_http_etag(etag);
self.set_last_modified(lmod);
self.save()?;
self = self.save()?;
}
Ok(())
Ok(self)
}
/// Clear the `HTTP` `Etag` and `Last-modified` headers.
fn clear_etags(mut self) -> Result<Self, DataError> {
if self.http_etag().is_some() || self.last_modified().is_some() {
debug!("Source etags before clear: {:#?}", &self);
self.http_etag = None;
self.last_modified = None;
self = self.save()?;
debug!("Source etags after clear: {:#?}", &self);
info!("Etag fields cleared succesfully for Source: {}", self.uri);
}
Ok(self)
}
fn make_err(self, context: &str, code: StatusCode) -> DataError {
@@ -121,18 +135,25 @@ impl Source {
// 410: Feed deleted
// TODO: Rething this api,
fn match_status(mut self, res: Response) -> Result<Response, DataError> {
self.update_etag(&res)?;
let code = res.status();
match code {
// If request is succesful save the etag
StatusCode::NotModified | StatusCode::Ok => self = self.update_etag(&res)?,
// Clear the Etag/lmod else
_ => self = self.clear_etags()?,
};
match code {
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
StatusCode::MovedPermanently => {
StatusCode::MovedPermanently | StatusCode::Found | StatusCode::PermanentRedirect => {
error!("Feed was moved permanently.");
self.handle_301(&res)?;
self = self.update_url(&res)?;
return Err(DataError::F301(self));
}
StatusCode::TemporaryRedirect => {
debug!("307: Temporary Redirect.");
return Err(DataError::F301(self));
}
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
StatusCode::Unauthorized => return Err(self.make_err("401: Unauthorized.", code)),
StatusCode::Forbidden => return Err(self.make_err("403: Forbidden.", code)),
StatusCode::NotFound => return Err(self.make_err("404: Not found.", code)),
@@ -140,21 +161,27 @@ impl Source {
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
_ => info!("HTTP StatusCode: {}", code),
};
Ok(res)
}
fn handle_301(&mut self, res: &Response) -> Result<(), DataError> {
fn update_url(mut self, res: &Response) -> Result<Self, DataError> {
let code = res.status();
let headers = res.headers();
info!("HTTP StatusCode: {}", code);
debug!("Headers {:#?}", headers);
if let Some(url) = headers.get::<Location>() {
debug!("Previous Source: {:#?}", &self);
self.set_uri(url.to_string());
self.http_etag = None;
self.last_modified = None;
self.save()?;
info!("Feed url was updated succesfully.");
self = self.clear_etags()?;
debug!("Updated Source: {:#?}", &self);
info!("Feed url of Source {}, was updated succesfully.", self.id());
}
Ok(())
Ok(self)
}
/// Construct a new `Source` with the given `uri` and index it.
@@ -177,12 +204,11 @@ impl Source {
pub fn into_feed(
self,
client: Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> impl Future<Item = Feed, Error = DataError> {
let id = self.id();
let response = loop_fn(self, move |source| {
source
.request_constructor(&client.clone(), ignore_etags)
.request_constructor(&client.clone())
.then(|res| match res {
Ok(response) => Ok(Loop::Break(response)),
Err(err) => match err {
@@ -206,12 +232,9 @@ impl Source {
})
}
// TODO: make ignore_etags an Enum for better ergonomics.
// #bools_are_just_2variant_enmus
fn request_constructor(
self,
client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> impl Future<Item = Response, Error = DataError> {
// FIXME: remove unwrap somehow
let uri = Uri::from_str(self.uri()).unwrap();
@@ -220,16 +243,14 @@ impl Source {
// Set the UserAgent cause ppl still seem to check it for some reason...
req.headers_mut().set(UserAgent::new(USER_AGENT));
if !ignore_etags {
if let Some(etag) = self.http_etag() {
let tag = vec![EntityTag::new(true, etag.to_owned())];
req.headers_mut().set(IfNoneMatch::Items(tag));
}
if let Some(etag) = self.http_etag() {
let tag = vec![EntityTag::new(true, etag.to_owned())];
req.headers_mut().set(IfNoneMatch::Items(tag));
}
if let Some(lmod) = self.last_modified() {
if let Ok(date) = lmod.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(date));
}
if let Some(lmod) = self.last_modified() {
if let Ok(date) = lmod.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(date));
}
}
@@ -254,29 +275,31 @@ fn response_to_channel(res: Response) -> impl Future<Item = Channel, Error = Dat
#[cfg(test)]
mod tests {
use super::*;
use failure::Error;
use tokio_core::reactor::Core;
use database::truncate_db;
use utils::get_feed;
#[test]
fn test_into_feed() {
truncate_db().unwrap();
fn test_into_feed() -> Result<(), Error> {
truncate_db()?;
let mut core = Core::new().unwrap();
let mut core = Core::new()?;
let client = Client::configure()
.connector(HttpsConnector::new(4, &core.handle()).unwrap())
.connector(HttpsConnector::new(4, &core.handle())?)
.build(&core.handle());
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let source = Source::from_url(url)?;
let id = source.id();
let feed = source.into_feed(client, true);
let feed = core.run(feed).unwrap();
let feed = source.into_feed(client);
let feed = core.run(feed)?;
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
assert_eq!(expected, feed);
Ok(())
}
}
@@ -35,15 +35,14 @@ pub fn import_to_db<R: Read>(reader: R) -> Result<Vec<Source>, reader::Error> {
.filter_map(|s| {
if let Err(ref err) = s {
let txt = "If you think this might be a bug please consider filling a report over \
at https://gitlab.gnome.org/World/hammond/issues/new";
at https://gitlab.gnome.org/World/podcasts/issues/new";
error!("Failed to import a Show: {}", err);
error!("{}", txt);
}
s.ok()
})
.collect();
}).collect();
Ok(feeds)
}
@@ -92,8 +91,7 @@ pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Erro
}
Err(err) => Err(err),
_ => Ok(()),
})
.collect::<Result<Vec<_>, reader::Error>>()?;
}).collect::<Result<Vec<_>, reader::Error>>()?;
Ok(list)
}
@@ -102,9 +100,10 @@ pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Erro
mod tests {
use super::*;
use chrono::Local;
use failure::Error;
#[test]
fn test_extract() {
fn test_extract() -> Result<(), Error> {
let int_title = String::from("Intercepted with Jeremy Scahill");
let int_url = String::from("https://feeds.feedburner.com/InterceptedWithJeremyScahill");
let int_desc =
@@ -162,6 +161,7 @@ mod tests {
url: dec_url
},
];
assert_eq!(extract_sources(sample1.as_bytes()).unwrap(), map);
assert_eq!(extract_sources(sample1.as_bytes())?, map);
Ok(())
}
}
@@ -48,14 +48,13 @@ type HttpsClient = Client<HttpsConnector<HttpConnector>>;
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
pub fn pipeline<'a, S>(
sources: S,
ignore_etags: bool,
client: &HttpsClient,
) -> impl Future<Item = Vec<()>, Error = DataError> + 'a
where
S: Stream<Item = Source, Error = DataError> + 'a,
{
sources
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags)))
.and_then(clone!(client => move |s| s.into_feed(client.clone())))
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
// the stream will stop at the first error so
// we ensure that everything will succeded regardless.
@@ -66,7 +65,7 @@ where
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
/// runs the pipeline to completion. The `reactor::Core` is dropped afterwards.
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError>
pub fn run<S>(sources: S) -> Result<(), DataError>
where
S: IntoIterator<Item = Source>,
{
@@ -77,7 +76,7 @@ where
.build(&handle);
let stream = iter_ok::<_, DataError>(sources);
let p = pipeline(stream, ignore_etags, &client);
let p = pipeline(stream, &client);
core.run(p).map(|_| ())
}
@@ -86,6 +85,7 @@ mod tests {
use super::*;
use database::truncate_db;
use dbqueries;
use failure::Error;
use Source;
// (path, url) tuples.
@@ -101,28 +101,29 @@ mod tests {
#[test]
/// Insert feeds and update/index them.
fn test_pipeline() {
truncate_db().unwrap();
let bad_url = "https://gitlab.gnome.org/World/hammond.atom";
fn test_pipeline() -> Result<(), Error> {
truncate_db()?;
let bad_url = "https://gitlab.gnome.org/World/podcasts.atom";
// if a stream returns error/None it stops
// bad we want to parse all feeds regardless if one fails
Source::from_url(bad_url).unwrap();
Source::from_url(bad_url)?;
URLS.iter().for_each(|url| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
let sources = dbqueries::get_sources().unwrap();
run(sources, true).unwrap();
let sources = dbqueries::get_sources()?;
run(sources)?;
let sources = dbqueries::get_sources().unwrap();
let sources = dbqueries::get_sources()?;
// Run again to cover Unique constrains erros.
run(sources, true).unwrap();
run(sources)?;
// Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources().unwrap().len(), 6);
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
assert_eq!(dbqueries::get_sources()?.len(), 6);
assert_eq!(dbqueries::get_podcasts()?.len(), 5);
assert_eq!(dbqueries::get_episodes()?.len(), 354);
Ok(())
}
}
+29
View File
@@ -0,0 +1,29 @@
diff --git a/podcasts-data/src/schema.rs b/podcasts-data/src/schema.rs
index 03cbed0..88f1622 100644
--- a/podcasts-data/src/schema.rs
+++ b/podcasts-data/src/schema.rs
@@ -1,8 +1,11 @@
+#![allow(warnings)]
+
table! {
episodes (title, show_id) {
+ rowid -> Integer,
title -> Text,
uri -> Nullable<Text>,
local_uri -> Nullable<Text>,
description -> Nullable<Text>,
epoch -> Integer,
length -> Nullable<Integer>,
@@ -30,11 +33,7 @@ table! {
uri -> Text,
last_modified -> Nullable<Text>,
http_etag -> Nullable<Text>,
}
}
-allow_tables_to_appear_in_same_query!(
- episodes,
- shows,
- source,
-);
+allow_tables_to_appear_in_same_query!(episodes, shows, source);
@@ -1,5 +1,7 @@
#![allow(warnings)]
table! {
episode (title, podcast_id) {
episodes (title, show_id) {
rowid -> Integer,
title -> Text,
uri -> Nullable<Text>,
@@ -10,22 +12,17 @@ table! {
duration -> Nullable<Integer>,
guid -> Nullable<Text>,
played -> Nullable<Integer>,
favorite -> Bool,
archive -> Bool,
podcast_id -> Integer,
show_id -> Integer,
}
}
table! {
podcast (id) {
shows (id) {
id -> Integer,
title -> Text,
link -> Text,
description -> Text,
image_uri -> Nullable<Text>,
favorite -> Bool,
archive -> Bool,
always_dl -> Bool,
source_id -> Integer,
}
}
@@ -39,4 +36,4 @@ table! {
}
}
allow_tables_to_appear_in_same_query!(episode, podcast, source,);
allow_tables_to_appear_in_same_query!(episodes, shows, source);
@@ -7,7 +7,7 @@ use url::{Position, Url};
use dbqueries;
use errors::DataError;
use models::{EpisodeCleanerQuery, Podcast, Save};
use models::{EpisodeCleanerModel, Save, Show};
use xdg_dirs::DL_DIR;
use std::fs;
@@ -25,8 +25,7 @@ fn download_checker() -> Result<(), DataError> {
return Some(ep);
}
None
})
.for_each(|ep| {
}).for_each(|ep| {
ep.set_local_uri(None);
ep.save()
.map_err(|err| error!("{}", err))
@@ -59,7 +58,7 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
}
/// Check `ep.local_uri` field and delete the file it points to.
fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<(), DataError> {
fn delete_local_content(ep: &mut EpisodeCleanerModel) -> Result<(), DataError> {
if ep.local_uri().is_some() {
let uri = ep.local_uri().unwrap().to_owned();
if Path::new(&uri).exists() {
@@ -108,9 +107,9 @@ pub fn url_cleaner(s: &str) -> String {
}
}
/// Returns the URI of a Podcast Downloads given it's title.
/// Returns the URI of a Show Downloads given it's title.
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
// It might be better to make it a hash of the title or the podcast rowid
// It might be better to make it a hash of the title or the Show rowid
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
// Create the folder
@@ -123,7 +122,7 @@ pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
/// Removes all the entries associated with the given show from the database,
/// and deletes all of the downloaded content.
// TODO: Write Tests
pub fn delete_show(pd: &Podcast) -> Result<(), DataError> {
pub fn delete_show(pd: &Show) -> Result<(), DataError> {
dbqueries::remove_feed(pd)?;
info!("{} was removed succesfully.", pd.title());
@@ -163,6 +162,7 @@ mod tests {
use self::tempdir::TempDir;
use super::*;
use chrono::Duration;
use failure::Error;
use database::truncate_db;
use models::NewEpisodeBuilder;
@@ -170,49 +170,47 @@ mod tests {
use std::fs::File;
use std::io::Write;
fn helper_db() -> TempDir {
fn helper_db() -> Result<TempDir, Error> {
// Clean the db
truncate_db().unwrap();
truncate_db()?;
// Setup tmp file stuff
let tmp_dir = TempDir::new("hammond_test").unwrap();
let tmp_dir = TempDir::new("podcasts_test")?;
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
let bad_path = tmp_dir.path().join("invalid_thing.mp3");
let mut tmp_file = File::create(&valid_path).unwrap();
writeln!(tmp_file, "Foooo").unwrap();
let mut tmp_file = File::create(&valid_path)?;
writeln!(tmp_file, "Foooo")?;
// Setup episodes
let n1 = NewEpisodeBuilder::default()
.title("foo_bar".to_string())
.podcast_id(0)
.show_id(0)
.build()
.unwrap()
.to_episode()
.unwrap();
.to_episode()?;
let n2 = NewEpisodeBuilder::default()
.title("bar_baz".to_string())
.podcast_id(1)
.show_id(1)
.build()
.unwrap()
.to_episode()
.unwrap();
.to_episode()?;
let mut ep1 = dbqueries::get_episode_from_pk(n1.title(), n1.podcast_id()).unwrap();
let mut ep2 = dbqueries::get_episode_from_pk(n2.title(), n2.podcast_id()).unwrap();
let mut ep1 = dbqueries::get_episode_cleaner_from_pk(n1.title(), n1.show_id())?;
let mut ep2 = dbqueries::get_episode_cleaner_from_pk(n2.title(), n2.show_id())?;
ep1.set_local_uri(Some(valid_path.to_str().unwrap()));
ep2.set_local_uri(Some(bad_path.to_str().unwrap()));
ep1.save().unwrap();
ep2.save().unwrap();
ep1.save()?;
ep2.save()?;
tmp_dir
Ok(tmp_dir)
}
#[test]
fn test_download_checker() {
let tmp_dir = helper_db();
download_checker().unwrap();
let episodes = dbqueries::get_downloaded_episodes().unwrap();
fn test_download_checker() -> Result<(), Error> {
let tmp_dir = helper_db()?;
download_checker()?;
let episodes = dbqueries::get_downloaded_episodes()?;
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
assert_eq!(episodes.len(), 1);
@@ -221,69 +219,75 @@ mod tests {
episodes.first().unwrap().local_uri()
);
let _tmp_dir = helper_db();
download_checker().unwrap();
let episode = dbqueries::get_episode_from_pk("bar_baz", 1).unwrap();
let _tmp_dir = helper_db()?;
download_checker()?;
let episode = dbqueries::get_episode_cleaner_from_pk("bar_baz", 1)?;
assert!(episode.local_uri().is_none());
Ok(())
}
#[test]
fn test_download_cleaner() {
let _tmp_dir = helper_db();
let mut episode: EpisodeCleanerQuery =
dbqueries::get_episode_from_pk("foo_bar", 0).unwrap().into();
fn test_download_cleaner() -> Result<(), Error> {
let _tmp_dir = helper_db()?;
let mut episode: EpisodeCleanerModel =
dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?.into();
let valid_path = episode.local_uri().unwrap().to_owned();
delete_local_content(&mut episode).unwrap();
delete_local_content(&mut episode)?;
assert_eq!(Path::new(&valid_path).exists(), false);
Ok(())
}
#[test]
fn test_played_cleaner_expired() {
let _tmp_dir = helper_db();
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
fn test_played_cleaner_expired() -> Result<(), Error> {
let _tmp_dir = helper_db()?;
let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?;
let cleanup_date = Utc::now() - Duration::seconds(1000);
let epoch = cleanup_date.timestamp() as i32 - 1;
episode.set_played(Some(epoch));
episode.save().unwrap();
episode.save()?;
let valid_path = episode.local_uri().unwrap().to_owned();
// This should delete the file
played_cleaner(cleanup_date).unwrap();
played_cleaner(cleanup_date)?;
assert_eq!(Path::new(&valid_path).exists(), false);
Ok(())
}
#[test]
fn test_played_cleaner_none() {
let _tmp_dir = helper_db();
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
fn test_played_cleaner_none() -> Result<(), Error> {
let _tmp_dir = helper_db()?;
let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?;
let cleanup_date = Utc::now() - Duration::seconds(1000);
let epoch = cleanup_date.timestamp() as i32 + 1;
episode.set_played(Some(epoch));
episode.save().unwrap();
episode.save()?;
let valid_path = episode.local_uri().unwrap().to_owned();
// This should not delete the file
played_cleaner(cleanup_date).unwrap();
played_cleaner(cleanup_date)?;
assert_eq!(Path::new(&valid_path).exists(), true);
Ok(())
}
#[test]
fn test_url_cleaner() {
fn test_url_cleaner() -> Result<(), Error> {
let good_url = "http://traffic.megaphone.fm/FL8608731318.mp3";
let bad_url = "http://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184";
assert_eq!(url_cleaner(bad_url), good_url);
assert_eq!(url_cleaner(good_url), good_url);
assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url);
Ok(())
}
#[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
fn test_get_dl_folder() {
fn test_get_dl_folder() -> Result<(), Error> {
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
assert_eq!(get_download_folder("foo").unwrap(), foo_);
assert_eq!(get_download_folder("foo")?, foo_);
let _ = fs::remove_dir_all(foo_);
Ok(())
}
}
@@ -1,22 +1,22 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-downloader"
name = "podcasts-downloader"
version = "0.1.0"
workspace = "../"
[dependencies]
error-chain = "0.11.0"
error-chain = "0.12.0"
hyper = "0.11.27"
log = "0.4.1"
mime_guess = "1.8.4"
reqwest = "0.8.5"
log = "0.4.4"
mime_guess = "1.8.6"
reqwest = "0.8.8"
tempdir = "0.3.7"
glob = "0.2.11"
failure = "0.1.1"
failure_derive = "0.1.1"
failure = "0.1.2"
failure_derive = "0.1.2"
[dependencies.hammond-data]
path = "../hammond-data"
[dependencies.podcasts-data]
path = "../podcasts-data"
[dev-dependencies]
pretty_assertions = "0.5.1"
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
use podcasts_data::xdg_dirs::PODCASTS_CACHE;
use podcasts_data::{EpisodeWidgetModel, Save, ShowCoverModel};
// use failure::Error;
use errors::DownloadError;
@@ -38,7 +38,7 @@ fn download_into(
dir: &str,
file_title: &str,
url: &str,
progress: Option<Arc<Mutex<DownloadProgress>>>,
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
) -> Result<String, DownloadError> {
info!("GET request to: {}", url);
// Haven't included the loop check as
@@ -78,7 +78,7 @@ fn download_into(
// Construct a temp file to save desired content.
// It has to be a `new_in` instead of new cause rename can't move cross
// filesystems.
let tempdir = TempDir::new_in(HAMMOND_CACHE.to_str().unwrap(), "temp_download")?;
let tempdir = TempDir::new_in(PODCASTS_CACHE.to_str().unwrap(), "temp_download")?;
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
ct_len.map(|x| {
@@ -122,12 +122,12 @@ fn save_io(
file: &str,
resp: &mut reqwest::Response,
content_lenght: Option<u64>,
progress: Option<Arc<Mutex<DownloadProgress>>>,
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
) -> Result<(), DownloadError> {
info!("Downloading into: {}", file);
let chunk_size = match content_lenght {
Some(x) => x as usize / 99,
None => 1024 as usize, // default chunk size
None => 1024, // default chunk size
};
let mut writer = BufWriter::new(File::create(&file)?);
@@ -161,9 +161,9 @@ fn save_io(
// TODO: Refactor
pub fn get_episode(
ep: &mut EpisodeWidgetQuery,
ep: &mut EpisodeWidgetModel,
download_folder: &str,
progress: Option<Arc<Mutex<DownloadProgress>>>,
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
) -> Result<(), DownloadError> {
// Check if its alrdy downloaded
if ep.local_uri().is_some() {
@@ -196,7 +196,7 @@ pub fn get_episode(
Ok(())
}
pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
pub fn cache_image(pd: &ShowCoverModel) -> Result<String, DownloadError> {
let url = pd
.image_uri()
.ok_or_else(|| DownloadError::NoImageLocation)?
@@ -206,7 +206,7 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
return Err(DownloadError::NoImageLocation);
}
let cache_path = HAMMOND_CACHE
let cache_path = PODCASTS_CACHE
.to_str()
.ok_or_else(|| DownloadError::InvalidCacheLocation)?;
let cache_download_fold = format!("{}{}", cache_path, pd.title().to_owned());
@@ -236,35 +236,37 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::dbqueries;
use hammond_data::pipeline;
use hammond_data::Source;
use failure::Error;
use podcasts_data::dbqueries;
use podcasts_data::pipeline;
use podcasts_data::Source;
use std::fs;
#[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
// This test inserts an rss feed to your `XDG_DATA/podcasts/podcasts.db` so we make it explicit
// to run it.
#[ignore]
fn test_cache_image() {
fn test_cache_image() -> Result<(), Error> {
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
// Create and index a source
let source = Source::from_url(url).unwrap();
let source = Source::from_url(url)?;
// Copy it's id
let sid = source.id();
// Convert Source it into a future Feed and index it
pipeline::run(vec![source], true).unwrap();
pipeline::run(vec![source])?;
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap().into();
let pd = dbqueries::get_podcast_from_source_id(sid)?.into();
let img_path = cache_image(&pd);
let foo_ = format!(
"{}{}/cover.jpeg",
HAMMOND_CACHE.to_str().unwrap(),
PODCASTS_CACHE.to_str().unwrap(),
pd.title()
);
assert_eq!(img_path.unwrap(), foo_);
fs::remove_file(foo_).unwrap();
assert_eq!(img_path?, foo_);
fs::remove_file(foo_)?;
Ok(())
}
}
@@ -1,4 +1,6 @@
use hammond_data::errors::DataError;
#![allow(bare_trait_objects)]
use podcasts_data::errors::DataError;
use reqwest;
use std::io;
+54
View File
@@ -0,0 +1,54 @@
#![recursion_limit = "1024"]
#![allow(unknown_lints)]
#![cfg_attr(
feature = "cargo-clippy",
allow(blacklisted_name, option_map_unit_fn)
)]
// Enable lint group collections
#![warn(
nonstandard_style,
edition_2018,
rust_2018_idioms,
bad_style,
unused
)]
// standalone lints
#![warn(
const_err,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
plugin_as_library,
private_no_mangle_fns,
private_no_mangle_statics,
unconditional_recursion,
unions_with_drop_fields,
while_true,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
elided_lifetime_in_paths,
missing_copy_implementations
)]
#![deny(warnings)]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate glob;
extern crate hyper;
extern crate mime_guess;
extern crate podcasts_data;
extern crate reqwest;
extern crate tempdir;
pub mod downloader;
pub mod errors;
+51
View File
@@ -0,0 +1,51 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
build = "build.rs"
name = "podcasts-gtk"
version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.6"
crossbeam-channel = "0.2.4"
gdk = "0.8.0"
gdk-pixbuf = "0.4.0"
glib = "0.5.0"
gstreamer = "0.11.6"
gstreamer-player = "0.11.3"
humansize = "1.1.0"
lazy_static = "1.1.0"
log = "0.4.4"
loggerv = "0.7.1"
open = "1.2.2"
rayon = "1.0.2"
url = "1.7.1"
failure = "0.1.2"
failure_derive = "0.1.2"
fragile = "0.3.0"
regex = "1.0.4"
reqwest = "0.8.8"
serde_json = "1.0.26"
# html2text = "0.1.8"
html2text = { git = "https://github.com/alatiera/rust-html2text" }
gettext-rs = { git = "https://github.com/danigm/gettext-rs", branch = "no-gettext", features = ["gettext-system"] }
[dependencies.gtk]
features = ["v3_22"]
version = "0.4.1"
[dependencies.gio]
features = ["v2_50"]
version = "0.4.1"
[dependencies.libhandy]
git = "https://gitlab.gnome.org/jsparber/libhandy-rs"
[dependencies.podcasts-data]
path = "../podcasts-data"
[dependencies.podcasts-downloader]
path = "../podcasts-downloader"
[dev-dependencies]
pretty_assertions = "0.5.1"
+14
View File
@@ -0,0 +1,14 @@
use std::process::Command;
fn main() {
// Rerun the build script when files in the resources folder are changed.
println!("cargo:rerun-if-changed=resources");
println!("cargo:rerun-if-changed=resources/*");
let out = Command::new("glib-compile-resources")
.args(&["--generate", "resources.xml"])
.current_dir("resources")
.status()
.expect("failed to generate resources");
assert!(out.success());
}
+10
View File
@@ -0,0 +1,10 @@
# please keep this list sorted alphabetically
#
de
es
fi
gl
id
ko
pl
tr
+46
View File
@@ -0,0 +1,46 @@
# List of source files containing translatable strings.
# Please keep this file sorted alphabetically.
podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml
podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in
podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in
# ui files
podcasts-gtk/resources/gtk/empty_view.ui
podcasts-gtk/resources/gtk/episode_widget.ui
podcasts-gtk/resources/gtk/hamburger.ui
podcasts-gtk/resources/gtk/headerbar.ui
podcasts-gtk/resources/gtk/help-overlay.ui
podcasts-gtk/resources/gtk/home_episode.ui
podcasts-gtk/resources/gtk/home_view.ui
podcasts-gtk/resources/gtk/inapp_notif.ui
podcasts-gtk/resources/gtk/player_toolbar.ui
podcasts-gtk/resources/gtk/prefs.ui
podcasts-gtk/resources/gtk/secondary_menu.ui
podcasts-gtk/resources/gtk/show_menu.ui
podcasts-gtk/resources/gtk/show_widget.ui
# rust files
podcasts-gtk/src/app.rs
podcasts-gtk/src/headerbar.rs
podcasts-gtk/src/main.rs
podcasts-gtk/src/manager.rs
podcasts-gtk/src/prefs.rs
podcasts-gtk/src/settings.rs
podcasts-gtk/src/stacks/content.rs
podcasts-gtk/src/stacks/home.rs
podcasts-gtk/src/stacks/mod.rs
podcasts-gtk/src/stacks/populated.rs
podcasts-gtk/src/stacks/show.rs
podcasts-gtk/src/static_resource.rs
podcasts-gtk/src/utils.rs
podcasts-gtk/src/widgets/aboutdialog.rs
podcasts-gtk/src/widgets/appnotif.rs
podcasts-gtk/src/widgets/base_view.rs
podcasts-gtk/src/widgets/empty.rs
podcasts-gtk/src/widgets/episode.rs
podcasts-gtk/src/widgets/home_view.rs
podcasts-gtk/src/widgets/mod.rs
podcasts-gtk/src/widgets/player.rs
podcasts-gtk/src/widgets/show_menu.rs
podcasts-gtk/src/widgets/show.rs
podcasts-gtk/src/widgets/shows_view.rs
+411
View File
@@ -0,0 +1,411 @@
# German translation for podcasts.
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package.
# Mario Blättermann <mario.blaettermann@gmail.com>, 2018.
# Tim Sabsch <tim@sabsch.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-08-22 05:29+0000\n"
"PO-Revision-Date: 2018-08-25 23:26+0200\n"
"Last-Translator: Mario Blättermann <mario.blaettermann@gmail.com>\n"
"Language-Team: German <gnome-de@gnome.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.1.1\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Obere Position des zuletzt geöffneten Hauptfensters"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Linke Position des zuletzt geöffneten Hauptfensters"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Höhe des zuletzt geöffneten Hauptfensters"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Breite des zuletzt geöffneten Hauptfensters"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Maximierungsstatus des zuletzt geöffneten Hauptfensters"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Dunkles Thema aktivieren oder deaktivieren"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr ""
"Legt fest, ob Inhalte in regelmäßigen Abständen aktualisiert werden sollen"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "Zeitintervall zwischen automatischen Aktualisierungsvorgängen"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "Zeitintervall zwischen automatischen Aktualisierungsvorgängen"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Legt fest, ob Inhalte beim Programmstart aktualisiert werden sollen"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "Zeitintervall zwischen automatischen Löschvorgängen"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "Zeitintervall zwischen automatischen Löschvorgängen"
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcasts"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Hören Sie Ihre Lieblings-Podcasts direkt in Ihrer Arbeitsumgebung."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "Podcast-Anwendung für GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "Diese Show hat noch keine Folgen"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr ""
"Falls Sie meinen, dass es sich um einen Fehler handeln könnte, schreiben Sie "
"bitte einen Fehlerbericht."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Shows holen"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Neue Shows mittels Quellen-Adresse hinzufügen"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Shows von einem anderen Gerät importieren"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Größe der Folge wird ermittelt …"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Abbrechen"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Diese Folge herunterladen"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Diese Folge abspielen"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "Auf neue Folgen _prüfen"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "Shows _importieren"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Einstellungen"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "Tasten_kürzel"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "I_nfo"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Neue Quelle hinzufügen"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Hinzuzufügende Quellen-Adresse eingeben"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Hinzufügen"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Sie haben diese Quelle bereits abonniert!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Titel anzeigen"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Zurück"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Allgemein"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Auf neue Folgen prüfen"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Einstellungen"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Die Anwendung beenden"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Heute"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Gestern"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Diese Woche"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Diesen Monat"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Älter"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Eine anwendungsinterne Aktionsmeldung"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Rückgängig"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "10 Sekunden zurückspulen"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Wiedergeben"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Pause"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "10 Sekunden vorspulen"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Die Wiedergabegeschwindigkeit ändern"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1,00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1,50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "1,5-fache Geschwindigkeit"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1,25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "1,25-fache Geschwindigkeit"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Normale Geschwidigkeit"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Einstellungen"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Erscheinungsbild"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Dunkles Thema"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Angehörte Folgen löschen"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Nach"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "Alle Folgen als angehört _markieren"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "_Internetseite"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "Ab_bestellen"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Internetseite öffnen"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Alle als angehört markieren"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Abbestellen"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Neue Folgen werden abgerufen"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Sie haben diese Show nun abonniert"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "Ungültige Adresse"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Sekunden"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Minuten"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Stunden"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Tage"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Wochen"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Neu"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Shows"
#: podcasts-gtk/src/utils.rs:350
msgid "Select the file from which to you want to import shows."
msgstr "Wählen Sie die Datei aus, aus der Sie Shows importieren wollen."
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "_Importieren"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "OPML-Datei"
#: podcasts-gtk/src/utils.rs:379
msgid "Failed to parse the imported file"
msgstr "Die importierte Datei konnte nicht ausgewertet werden"
#: podcasts-gtk/src/utils.rs:384
msgid "Selected file could not be accessed."
msgstr "Auf die gewählte Datei konnte nicht zugegriffen werden."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Podcast-Client für die GNOME-Arbeitsumgebung."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr "Erfahren Sie mehr über GNOME Podcasts"
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr ""
"Mario Blättermann <mario.blaettermann@gmail.com>\n"
"Tim Sabsch <tim@sabsch.com>"
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "Die Medienwiedergabe konnte eine Aktion nicht ausführen."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Alle Folgen als angehört markieren"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Abonnement von {} gekündigt"
+447
View File
@@ -0,0 +1,447 @@
# Spanish translations for gnome-podcasts package.
# Copyright (C) 2018 THE gnome-podcasts'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gnome-podcasts package.
# Automatically generated, 2018.
# Daniel Garcia Moreno <danigm@wadobo.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: gnome-podcasts\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-22 06:50+0300\n"
"PO-Revision-Date: 2018-08-02 11:46+0200\n"
"Last-Translator: Daniel Garcia Moreno <danigm@wadobo.com>\n"
"Language-Team: Español; Castellano <info@wadobo.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Gtranslator 2.91.7\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Posición superior de la última ventana abierta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Posición izquierda de la última ventana abierta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Alto de la última ventana abierta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Ancho de la última ventana abierta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Estado de maximizado de la última ventana abierta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Activar o desactivar el tema oscuro"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "Si refrescar el contenido periódicamente"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "Cuántos periodos de tiempo a esperar entre refrescos automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "Qué periodo de tiempo hay que esperar entre refrescos automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Si refrescar el contenido al iniciar"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "Cuántos periodos de tiempo hay que esperar entre limpiados automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "Cuántos periodos de tiempo hay que esperar entre limpiados automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcasts"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Escucha tus podcasts favoritos, directamente desde tu escritorio."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "Aplicación de Podcast para GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
#, fuzzy
msgid "Jordan Petridis"
msgstr "Jordan Petridis y otros"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
#, fuzzy
msgid "This show does not have episodes yet"
msgstr "Este programa no tiene episodios todavía"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
#, fuzzy
msgid "If you think this is an error, please consider writing a bug report."
msgstr ""
"Si crees que esto es un error, por favor, considera abrir un bug report."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Consigue algún programa"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Añadir nuevo programa a través de canal URL"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Importar programas de otro dispositivo"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
#, fuzzy
msgid "Calculating episode size…"
msgstr "Calculando tamaño del episodio..."
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Cancelar"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Descargar este episodio"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Reproducir este episodio"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
#, fuzzy
msgid "_Check for New Episodes"
msgstr "_Comprobar si hay nuevos episodios"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "_Importar programas"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Preferencias"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "Atajos de _teclado"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "_Acerca de"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Añadir un nuevo canal"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Introduce la dirección del canal a añadir"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Añadir"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "¡Ya estás suscrito a este canal!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Título de Programa"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Atrás"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "General"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Comprobar si hay nuevos episodios"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Preferencias"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Salir de la aplicación"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Hoy"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Ayer"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Esta semana"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Este mes"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Antiguo"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Una notificación de acción in-app"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Deshacer"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "Atrás 10 segundos"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Reproducir"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Pausa"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
#, fuzzy
msgid "Fast forward 10 seconds"
msgstr "Adelante 10 segundos"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
#, fuzzy
msgid "Change the playback speed"
msgstr "Cambiar velocidad de reproducción"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
#, fuzzy
msgid "1.00×"
msgstr "1.00x"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
#, fuzzy
msgid "1.50×"
msgstr "1.50x"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "velocidad 1.5"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
#, fuzzy
msgid "1.25×"
msgstr "1.25x"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "velocidad 1.25"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Velocidad normal"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Preferencias"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Aspecto"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Tema oscuro"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Eliminar episodios reproducidos"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Después"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
#, fuzzy
msgid "_Mark All Episodes as Played"
msgstr "_Marcar todos los episodios como reproducidos"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "Página _Web"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Cancelar suscripción"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Abrir Web"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
#, fuzzy
msgid "Mark All as Played"
msgstr "Marcar como reproducido"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Cancelar suscripción"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Obteniendo nuevos episodios"
#: podcasts-gtk/src/headerbar.rs:98
#, fuzzy
msgid "You are already subscribed to this show"
msgstr "Ya estás suscrito a este programa"
#: podcasts-gtk/src/headerbar.rs:106
#, fuzzy
msgid "Invalid URL"
msgstr "Url no válida"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Segundos"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Minutos"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Horas"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Días"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Semanas"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Nuevo"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Programas"
#: podcasts-gtk/src/utils.rs:350
#, fuzzy
msgid "Select the file from which to you want to import shows."
msgstr "Selecciona el fichero desde el cual quieres importar programas."
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "_Importar"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "fichero OPML"
#: podcasts-gtk/src/utils.rs:379
#, fuzzy
msgid "Failed to parse the imported file"
msgstr "Fallo al leer el fichero importado"
#: podcasts-gtk/src/utils.rs:384
#, fuzzy
msgid "Selected file could not be accessed."
msgstr "El fichero seleccionado no es accesible."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Aplicación de Podcast para el escritorio GNOME."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr ""
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr "\"Daniel García Moreno <danigm@wadobo.com>\""
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "El reproductor no ha podido reproducir una acción."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Marcar todos los episodios como escuchados"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Suscripción cancelada para {}"
#~ msgid "Mark all episodes as listened"
#~ msgstr "Marcar todos los episodios como escuchados"
#~ msgid "Episode Title"
#~ msgstr "Título del episodio"
#~ msgid "3 Jan"
#~ msgstr "3 Jan"
#~ msgid "·"
#~ msgstr "·"
#~ msgid "42 min"
#~ msgstr "42 min"
#~ msgid "0 MB"
#~ msgstr "0 MB"
#~ msgid "/"
#~ msgstr "/"
#~ msgid "© 2017, 2018 Jordan Petridis"
#~ msgstr "© 2017, 2018 Jordan Petridis"
+410
View File
@@ -0,0 +1,410 @@
# Finnish translation for podcasts.
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package.
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-22 06:50+0300\n"
"PO-Revision-Date: 2018-08-11 17:09+0300\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos+l10n@iki.fi>\n"
"Language-Team: Finnish <lokalisointi-lista@googlegroups.com>\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.6\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Ota käyttöön tai poista käytöstä tumma teema"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Päivitetäänkö sisältö sovelluksen käynnistyttyä"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr ""
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcastit"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Kuuntele suosikkipodcastejasi suoraan työpöydältäsi."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "Podcast-sovellus Gnomelle"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
#, fuzzy
msgid "This show does not have episodes yet"
msgstr "Tässä ohjelmassa ei ole yhtäkään jaksoa"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
#, fuzzy
msgid "If you think this is an error, please consider writing a bug report."
msgstr "Jos luulet tämän olevan virhe, teethän bugiraportin."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Hanki ohjelmia"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Lisää uusia ohjelmia syötteen osoitteen avulla"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Tuo ohjelmia toiselta laitteelta"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Lasketaan jakson kokoa…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Peru"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Lataa tämä jakso"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Toista tämä jakso"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "Ta_rkista uudet jaksot"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "Tu_o jaksoja"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Asetukset"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "_Pikanäppäimet"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "_Tietoja"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Lisää uusi syöte"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Anna lisättävän syötteen osoite"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Lisää"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Olet jo tilannut kyseisen syötteen!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Ohjelman nimi"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Takaisin"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Yleiset"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Tarkista uudet jaksot"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Asetukset"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Lopeta sovellus"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Tänään"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Eilen"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Tällä viikolla"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Tässä kuussa"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Vanhemmat"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Sovelluksen sisäinen ilmoitus"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Kumoa"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "Siirry taakse 10 sekuntia"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Toista"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Keskeytä"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "Siirry eteen 10 sekuntia"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Vaihda toistonopeutta"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1.00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1.50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "1,5-kertainen nopeus"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1.25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "1,25-kertainen nopeus"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Tavallinen nopeus"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Asetukset"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Ulkoasu"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Tumma teema"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Poista toistetut jaksot"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Viive"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "_Merkitse kaikki jaksot toistetuiksi"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "_Verkkosivusto"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Lopeta tilaus"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Avaa verkkosivusto"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Merkitse kaikki toistetuiksi"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Lopeta tilaus"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Noudetaan uusia jaksoja"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Olet jo tilannut tämän ohjelman"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "Virheellinen osoite"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "sekuntia"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "minuuttia"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "tuntia"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "päivää"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "viikkoa"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Uudet"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Ohjelmat"
#: podcasts-gtk/src/utils.rs:350
msgid "Select the file from which to you want to import shows."
msgstr "Valitse tiedosto, josta haluat tuoda ohjelmia."
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "_Tuo"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "OPML-tiedosto"
#: podcasts-gtk/src/utils.rs:379
msgid "Failed to parse the imported file"
msgstr "Tuotua tiedostoa ei voitu jäsentää"
#: podcasts-gtk/src/utils.rs:384
msgid "Selected file could not be accessed."
msgstr "Valittua tiedostoa ei voitu käyttää."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Podcast-sovellus Gnome-työpöydlle."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr ""
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr "Jiri Grönroos"
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "Mediasoitin ei kyennyt suorittamaan toimintoa."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Merkitse kaikki jaksot toistetuiksi"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Lopeta syötteen {} tilaus"
#~ msgid "Episode Title"
#~ msgstr "Jakson nimi"
+407
View File
@@ -0,0 +1,407 @@
# Spanish translations for gnome-podcasts package.
# Copyright (C) 2018 THE gnome-podcasts'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gnome-podcasts package.
# Automatically generated, 2018.
# Fran Dieguez <frandieguez@gnome.org>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: gnome-podcasts\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-22 06:50+0300\n"
"PO-Revision-Date: 2018-08-30 10:39+0200\n"
"Last-Translator: Fran Dieguez <frandieguez@gnome.org>\n"
"Language-Team: Galician\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Virtaal 0.7.1\n"
"X-Project-Style: gnome\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Posición superior da última xanela aberta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Posición esquerda da última xanela aberta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Alto da última xanela aberta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Anchura da última xanela aberta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Estado de maximizado da última xanela aberta"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Activar o desactivar el tema oscuro"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "Se refrescar o contido periodicamente"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "Cantos periodos de tempo a esperar entre refrescos automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "Qué periodo de tempo hai que esperar entre refrescos automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Se refrescar o contido ao iniciar"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "Cantos periodos de tempo hai que esperar entre limpados automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "Cantos periodos de tiempo hai que esperar entre limpiados automáticos"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcasts"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr ""
"Escoita os teus podcasts favoritos, directamente desde o teu escritorio."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "Aplicativo de Podcast para GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "Este programa aínda non ten episodios"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr ""
"Se cres que isto é un erro, por favor, considera abrir un informe de erro."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Obter algúns programas"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Engadir novo programa a través do URL da canle"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Importar programas desde outro dispositivo"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Calculando tamaño do episodio…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Cancelar"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Descargar este episodio"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Reproducir este episodio"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "_Comprobar se hai novos episodios"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "_Importar programas"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Preferencias"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "Atallos de _teclado"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "_Sobre"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Engadir unha nova canle"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Escriba o enderezo da canle a engadir"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Engadir"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Xa estás suscrito a esta canle!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Título do Programa"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Atrás"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Xeral"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Comprobar se hai novos episodios"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Preferencias"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Saír do aplicativo"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Hoxe"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Onte"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Esta semana"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Este mes"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Antigo"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Unha notificación de acción en aplicativo"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Desfacer"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "Atrás 10 segundos"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Reproducir"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Pausa"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "Adiante 10 segundos"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Cambiar velocidade de reprodución"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1.00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1.50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "velocidade 1.5"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1.25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "velocidade 1.25"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Velocidade normal"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Preferencias"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Aspecto"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Tema escuro"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Eliminar episodios reproducidos"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Despois"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "_Marcar todos os episodios como reproducidos"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "Páxina _Web"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Cancelar subscrición"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Abrir sitio web"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Marcar todo como reproducido"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Cancelar subscrición"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Obtendo novos episodios"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Xa estás subscrito a este programa"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "URL non válida"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Segundos"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Minutos"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Horas"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Días"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Semanas"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Novo"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Programas"
#: podcasts-gtk/src/utils.rs:350
msgid "Select the file from which to you want to import shows."
msgstr "Seleccione o fichero desde o cal queres importar programas."
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "_Importar"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "Fichero OPML"
#: podcasts-gtk/src/utils.rs:379
msgid "Failed to parse the imported file"
msgstr "Produciuse un fallo ao leer o fichero importado"
#: podcasts-gtk/src/utils.rs:384
msgid "Selected file could not be accessed."
msgstr "O fichero seleccionado non é accesíbel."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Aplicativo de Podcast para o ambiente GNOME."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr "Saber máis sobre Podcasts de GNOME"
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr "Fran Dieguez <frandieguez@gnome.org>, 2018"
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "O reprodutor non puido reproducir unha acción."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Marcar todos os episodios como escoitados"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Subscrición cancelada para {}"
+404
View File
@@ -0,0 +1,404 @@
# Indonesian translation for podcasts.
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-08-30 08:45+0000\n"
"PO-Revision-Date: 2018-08-30 17:37+0700\n"
"Language-Team: Indonesian <gnome-l10n-id@googlegroups.com>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: Kukuh Syafaat <kukuhsyafaat@gnome.org>\n"
"X-Generator: Poedit 2.0.6\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Posisi atas dari jendela utama terbuka terakhir"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Posisi kiri dari jendela utama terbuka terakhir"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Tinggi dari jendela utama terbuka terakhir"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Lebar dari jendela utama terbuka terakhir"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Keadaan dimaksimalkan dari jendela utama terbuka terakhir"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Aktifkan atau nonaktifkan tema gelap"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "Apakah akan menyegarkan konten secara berkala"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "Berapa banyak periode waktu untuk menunggu antara penyegaran otomatis"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "Berapa lama waktu untuk menunggu antara penyegaran otomatis"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Apakah akan menyegarkan konten setelah awal mula"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "Berapa lama waktu menunggu antara pembersihan otomatis"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "Berapa lama waktu untuk menunggu di antara pembersihan otomatis"
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:4
#: podcasts-gtk/src/app.rs:91 podcasts-gtk/src/app.rs:414
#: podcasts-gtk/src/widgets/aboutdialog.rs:31
msgid "Podcasts"
msgstr "Podcast"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Dengarkan podcast favorit Anda, langsung dari destop Anda."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:6
msgid "@icon@"
msgstr "@icon@"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:8
msgid "Podcast app for GNOME"
msgstr "Aplikasi Podcast untuk GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "Acara ini belum memiliki episode"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr "Jika Anda pikir ini adalah kesalahan, silakan mempertimbangkan untuk menulis laporan kutu."
#: podcasts-gtk/resources/gtk/empty_view.ui:106
msgid "Get some shows"
msgstr "Dapatkan beberapa acara"
#: podcasts-gtk/resources/gtk/empty_view.ui:143
msgid "Add new shows via feed URL"
msgstr "Tambahkan acara baru melalui URL feed"
#: podcasts-gtk/resources/gtk/empty_view.ui:172
msgid "Import shows from another device"
msgstr "Impor acara dari perangkat lain"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Menghitung ukuran episode…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Batal"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Unduh episode ini"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Putar episode ini"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "Periksa Episode _Baru"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "_Impor Acara"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Preferensi"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "Pintasan Papan _Ketik"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "Tent_ang"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Tambahkan feed baru"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Masukkan alamat feed untuk ditambahkan"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Tambah"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Anda sudah berlangganan feed itu!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Tampilkan Judul"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Kembali"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Umum"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Periksa episode baru"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Preferensi"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Keluar dari aplikasi"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Hari ini"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Kemarin"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Minggu Ini"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Bulan ini"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Lebih lama"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Notifikasi aksi dalam aplikasi"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Tak Jadi"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "Putar mundur 10 detik"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Putar"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Jeda"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "Maju cepat 10 detik"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Ubah kecepatan pemutaran"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "Ubah kecepatan pemutaran"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1.50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "tingkat kecepatan 1.5"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1.25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "tingkat kecepatan 1.25"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Kecepatan normal"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Preferensi"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Penampilan"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Tema Gelap"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Hapus episode yang diputar"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Sesudah"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "Tandai Se_mua Episode sebagai Sudah Diputar"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "Situs _Web"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Berhenti Berlangganan"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Buka Situs Web"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Tandai Semua sebagai Sudah Diputar"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Berhenti Berlangganan"
#: podcasts-gtk/src/app.rs:332
msgid "Fetching new episodes"
msgstr "Mengambil episode baru"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Anda sudah berlangganan ke acara ini"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "URL tak valid"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Detik"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Menit"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Jam"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Hari"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Minggu"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Baru"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Acara"
#: podcasts-gtk/src/utils.rs:357
msgid "Select the file from which to you want to import shows."
msgstr "Pilih berkas dari mana Anda ingin mengimpor acara."
#: podcasts-gtk/src/utils.rs:360
msgid "_Import"
msgstr "_Impor"
#: podcasts-gtk/src/utils.rs:369
msgid "OPML file"
msgstr "Berkas OPML"
#: podcasts-gtk/src/utils.rs:386
msgid "Failed to parse the imported file"
msgstr "Gagal mengurai berkas yang diimpor"
#: podcasts-gtk/src/utils.rs:391
msgid "Selected file could not be accessed."
msgstr "Berkas yang dipilih tidak dapat diakses."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Klien Podcast untuk Destop GNOME."
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Learn more about GNOME Podcasts"
msgstr "Pelajari lebih lanjut tentang GNOME Podcast"
#: podcasts-gtk/src/widgets/aboutdialog.rs:38
msgid "translator-credits"
msgstr "Kukuh Syafaat <kukuhsyafaat@gnome.org>, 2018."
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} mnt"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "Pemutar media tidak dapat melakukan aksi."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Tandai semua episode sebagai sudah didengarkan"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Berhenti berlangganan dari {}"
+405
View File
@@ -0,0 +1,405 @@
# Korean translation for podcasts.
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package.
# Seong-ho Cho <shcho@gnome.org>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-08-30 10:43+0000\n"
"PO-Revision-Date: 2018-08-30 21:26+0900\n"
"Language-Team: Korean <gnome-kr@googlegroups.com>\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"Last-Translator: Seong-ho Cho <shcho@gnome.org>\n"
"X-Generator: Poedit 2.0.6\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "최근 연 메인 창 상단 위치"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "최근 연 메인 창 좌측 위치"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "최근 연 메인 창 높이"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "최근 연 메인 창 너비"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "최근 연 메인 창 최대화 상태"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "어두움 테마 사용 함/안함"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "주기적으로 내용 새로 고침 여부"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "자동 새로 고침 기다림 주기 시간 간격"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "자동 새로 고침 기다림 주기 시간"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "시작 후 내용 새로 고침 여부"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "자동 정리 기다림 주기 시간 간격"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "자동 정리 기다림 주기 시간"
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:4
#: podcasts-gtk/src/app.rs:91 podcasts-gtk/src/app.rs:414
#: podcasts-gtk/src/widgets/aboutdialog.rs:31
msgid "Podcasts"
msgstr "팟캐스트"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "데스크톱에서 바로 원하는 팟캐스트를 듣습니다."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:6
msgid "@icon@"
msgstr "@icon@"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:13
msgid "Podcast;RSS;"
msgstr "Podcast;팟캐스트;RSS;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:8
msgid "Podcast app for GNOME"
msgstr "그놈용 팟캐스트 앱"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "아직 보여줄 에피소드가 없습니다"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr "오류인 것 같다면 오류 보고서 작성을 고려하십시오."
#: podcasts-gtk/resources/gtk/empty_view.ui:106
msgid "Get some shows"
msgstr "일부 쇼 가져오기"
#: podcasts-gtk/resources/gtk/empty_view.ui:143
msgid "Add new shows via feed URL"
msgstr "피드 URL 주소로 새 쇼 추가"
#: podcasts-gtk/resources/gtk/empty_view.ui:172
msgid "Import shows from another device"
msgstr "다른 장치에서 쇼 가져오기"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "에피소드 용량 계산 중…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "취소"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "이 에피소드 다운로드"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "이 에피소드 재생"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "새 에피소드 확인(_C)"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "쇼 가져오기(_I)"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "기본 설정(_P)"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "키보드 바로 가기 키(_K)"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "정보(_A)"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "새 피드 추가"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "추가할 피드 주소를 입력하십시오"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "추가"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "해당 피드에 이미 가입했습니다!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "제목 표시"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "뒤로"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "일반"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "새 에피소드 확인"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "기본 설정"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "프로그램 끝내기"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "오늘"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "어제"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "금주"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "이번 달"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "이전"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "앱 동작 알림"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "실행 취소"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "10초 되감기"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "재생"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "일시 정지"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "10초 빨리감기"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "재생 속도 조절"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1.00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1.50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "1.5배 속도"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1.25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "1.25배 속도"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "일반 속도"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "기본 설정"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "모양새"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "어두움 테마"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "재생한 에피소드 삭제"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "다음"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "모든 에피소드를 재생 상태로 표시(_M)"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "웹사이트(_W)"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "가입 취소(_U)"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "웹사이트 열기"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "모두 재생한 항목으로 표시"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "가입 취소"
#: podcasts-gtk/src/app.rs:332
msgid "Fetching new episodes"
msgstr "새 에피소드 가져오는 중"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "이미 이 쇼에 가입했습니다"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "잘못된 URL 주소"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "초"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "분"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "시간"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "일"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "주"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "새 항목"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "쇼"
#: podcasts-gtk/src/utils.rs:357
msgid "Select the file from which to you want to import shows."
msgstr "가져올 쇼의 파일을 선택하십시오."
#: podcasts-gtk/src/utils.rs:360
msgid "_Import"
msgstr "가져오기(_I)"
#: podcasts-gtk/src/utils.rs:369
msgid "OPML file"
msgstr "OPML 파일"
#: podcasts-gtk/src/utils.rs:386
msgid "Failed to parse the imported file"
msgstr "가져온 파일 해석에 실패했습니다"
#: podcasts-gtk/src/utils.rs:391
msgid "Selected file could not be accessed."
msgstr "선택한 파일에 접근할 수 없습니다."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "그놈 데스크톱용 팟캐스트 클라이언트."
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Learn more about GNOME Podcasts"
msgstr "그놈 팟캐스트에 대해 더 알아보세요"
#: podcasts-gtk/src/widgets/aboutdialog.rs:38
msgid "translator-credits"
msgstr "조성호 <shcho@gnome.org>, 2018."
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{}분"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "미디어 재생기에서 이 동작을 수행할 수 없습니다."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "모든 에피소드를 감청 상태로 표시했습니다"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "{}에서 가입 취소했습니다"
+4
View File
@@ -0,0 +1,4 @@
i18n.gettext(meson.project_name(),
args: ['--keyword=i18n', '--keyword=i18n_f', '--keyword=i18n_k',
'--keyword=ni18n:1,2', '--keyword=ni18n_f:1,2', '--keyword=ni18n_k:1,2'],
preset: 'glib')
+408
View File
@@ -0,0 +1,408 @@
# Polish translation for podcasts.
# Copyright © 2018 the podcasts authors.
# This file is distributed under the same license as the podcasts package.
# Piotr Drąg <piotrdrag@gmail.com>, 2018.
# Aviary.pl <community-poland@mozilla.org>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-08-22 05:29+0000\n"
"PO-Revision-Date: 2018-08-26 18:30+0200\n"
"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
"Language-Team: Polish <community-poland@mozilla.org>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Górne położenie ostatnio otwartego głównego okna"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Lewe położenie ostatnio otwartego głównego okna"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Wysokość ostatnio otwartego głównego okna"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Szerokość ostatnio otwartego głównego okna"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Zmaksymalizowany stan ostatnio otwartego głównego okna"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Ciemny motyw"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "Czy okresowo odświeżać treści"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr "Ile okresów czekać między automatycznym odświeżeniem"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr "Jaki okres czekać między automatycznym odświeżeniem"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Czy odświeżać treści po uruchomieniu"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr "Ile okresów czekać między automatycznym czyszczeniem"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr "Jaki okres czekać między automatycznym czyszczeniem"
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcasty"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Słuchanie ulubionych podcastów prosto z komputera."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;Podkasty;RSS;Atom;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "Aplikacja do słuchania podcastów w GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "Ten program nie ma jeszcze odcinków"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr "Jeśli to błąd, to prosimy go zgłosić."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Pobierz programy"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Dodaj nowe programy za pomocą adresu kanału"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Zaimportuj programy z innego urządzenia"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Obliczanie rozmiaru odcinka…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "Anuluj"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Pobiera ten odcinek"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Odtwarza ten odcinek"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "_Wyszukaj nowe odcinki"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "Zai_mportuj programy"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Preferencje"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "_Skróty klawiszowe"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "_O programie"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Dodaje nowy kanał"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Adres kanału do dodania"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Dodaj"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Ten kanał jest już subskrybowany."
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Tytuł programu"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Wstecz"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Ogólne"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Wyszukanie nowych odcinków"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Preferencje"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Zakończenie działania programu"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Dzisiaj"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Wczoraj"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Ten tydzień"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Ten miesiąc"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Starsze"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Powiadomienie z działaniem w programie"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Cofnij"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "Przewija wstecz o 10 sekund"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Odtwarza"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Wstrzymuje"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "Przewija wprzód o 10 sekund"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Zmienia prędkość odtwarzania"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1,00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1,50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "Prędkość 1,5"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1,25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "Prędkość 1,25"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Zwykła prędkość"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Preferencje"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Wygląd"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Ciemny motyw"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Usuwanie odtworzonych odcinków"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Po"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "_Oznacz wszystkie odcinki jako odtworzone"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "Strona _WWW"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Zrezygnuj z subskrypcji"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Otwórz stronę WWW"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Oznacz wszystkie jako odtworzone"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Zrezygnuj z subskrypcji"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Pobieranie nowych odcinków"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Ten program jest już subskrybowany"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "Nieprawidłowy adres"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Sekundy"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Minuty"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Godziny"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Dni"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Tygodnie"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Nowe"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Programy"
#: podcasts-gtk/src/utils.rs:350
msgid "Select the file from which to you want to import shows."
msgstr "Proszę wybrać plik, z którego zaimportować programy."
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "Zai_mportuj"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "Plik OPML"
#: podcasts-gtk/src/utils.rs:379
msgid "Failed to parse the imported file"
msgstr "Przetworzenie zaimportowanego pliku się nie powiodło"
#: podcasts-gtk/src/utils.rs:384
msgid "Selected file could not be accessed."
msgstr "Nie można uzyskać dostępu do wybranego pliku."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "Klient podcastów dla środowiska GNOME."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr "Więcej informacji o Podcastach GNOME"
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr ""
"Piotr Drąg <piotrdrag@gmail.com>, 2018\n"
"Aviary.pl <community-poland@mozilla.org>, 2018"
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{}min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "Odtwarzacz multimediów nie może wykonać działania."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Oznaczono wszystkie odcinki jako odsłuchane"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "Zrezygnowano z subskrypcji „{}”"
+408
View File
@@ -0,0 +1,408 @@
# Turkish translation for podcasts.
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package.
# Emin Tufan Çetin <etcetin@gmail.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-08-22 05:29+0000\n"
"PO-Revision-Date: 2018-08-27 11:42+0300\n"
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
"Language-Team: Türkçe <gnome-turk@gnome.org>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Gtranslator 2.91.7\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window"
msgstr "Son açılan ana pencerenin üst konumu"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:19
msgid "Left position of the last open main window"
msgstr "Son açılan ana pencerenin sol konumu"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:23
msgid "Height of the last open main window"
msgstr "Son açılan ana pencerenin yüksekliği"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:27
msgid "Width of the last open main window"
msgstr "Son açılan ana pencerenin genişliği"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:31
msgid "Maximized state of the last open main window"
msgstr "Son açılan ana pencerenin büyütülme durumu"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:36
msgid "Enable or disable dark theme"
msgstr "Karanlık gövdeyi etkinleştir veya devre dışı bırak"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:41
msgid "Whether to periodically refresh content"
msgstr "Belirli aralıklarla içeriğin yenilenmesi"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:54
msgid "Whether to refresh content after startup"
msgstr "Başlangıçtan sonra içeriğin yenilenmesi"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:60
msgid "How many periods of time to wait between automatic cleanups"
msgstr ""
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:64
msgid "What period of time to wait between automatic cleanups"
msgstr ""
#. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:406
#: podcasts-gtk/src/widgets/aboutdialog.rs:33
msgid "Podcasts"
msgstr "Podcastler"
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:4
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:10
msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Gözde podcastlerinizi masaüstünüzden dinleyin."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:6
msgid "org.gnome.Podcasts"
msgstr "org.gnome.Podcasts"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop:13
msgid "Podcast;RSS;"
msgstr "Podcast;RSS;Pod Yayını;"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:8
msgid "Podcast app for GNOME"
msgstr "GNOME için Podcast uygulaması"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml:31
msgid "Jordan Petridis"
msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet"
msgstr "Bu gösterinin henüz herhangi bir bölümü yok"
#: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report."
msgstr "Bunun bir hata olduğunu düşünüyorsanız lütfen hata bildirimi yazın."
#: podcasts-gtk/resources/gtk/empty_view.ui:107
msgid "Get some shows"
msgstr "Birkaç gösteri edinin"
#: podcasts-gtk/resources/gtk/empty_view.ui:144
msgid "Add new shows via feed URL"
msgstr "Besleme URLʼsi ile yeni gösteriler ekle"
#: podcasts-gtk/resources/gtk/empty_view.ui:173
msgid "Import shows from another device"
msgstr "Gösterileri başka aygıttan içe aktar"
#: podcasts-gtk/resources/gtk/episode_widget.ui:177
msgid "Calculating episode size…"
msgstr "Bölüm boyutu hesaplanıyor…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:212
msgid "Cancel"
msgstr "İptal"
#: podcasts-gtk/resources/gtk/episode_widget.ui:230
msgid "Download this episode"
msgstr "Bu bölümü indir"
#: podcasts-gtk/resources/gtk/episode_widget.ui:254
msgid "Play this episode"
msgstr "Bu bölümü oynat"
#: podcasts-gtk/resources/gtk/hamburger.ui:7
msgid "_Check for New Episodes"
msgstr "Yeni Bölümleri _Denetle"
#: podcasts-gtk/resources/gtk/hamburger.ui:12
msgid "_Import Shows"
msgstr "_Gösterileri İçe Aktar"
#: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Preferences"
msgstr "_Tercihler"
#: podcasts-gtk/resources/gtk/hamburger.ui:29
msgid "_Keyboard Shortcuts"
msgstr "_Klavye Kısayolları"
#: podcasts-gtk/resources/gtk/hamburger.ui:37
msgid "_About"
msgstr "_Hakkında"
#: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189
msgid "Add a new feed"
msgstr "Yeni besleme ekle"
#: podcasts-gtk/resources/gtk/headerbar.ui:53
msgid "Enter feed address to add"
msgstr "Eklenecek besleme adresini gir"
#: podcasts-gtk/resources/gtk/headerbar.ui:89
msgid "Add"
msgstr "Ekle"
#: podcasts-gtk/resources/gtk/headerbar.ui:133
msgid "You are already subscribed to that feed!"
msgstr "Bu beslemeye zaten abonesiniz!"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title"
msgstr "Gösteri Başlığı"
#: podcasts-gtk/resources/gtk/headerbar.ui:210
msgid "Back"
msgstr "Geri"
#: podcasts-gtk/resources/gtk/help-overlay.ui:12
msgid "General"
msgstr "Genel"
#: podcasts-gtk/resources/gtk/help-overlay.ui:18
msgctxt "shortcut window"
msgid "Check for new episodes"
msgstr "Yeni bölümleri denetle"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window"
msgid "Preferences"
msgstr "Tercihler"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application"
msgstr "Uygulamadan çık"
#: podcasts-gtk/resources/gtk/home_view.ui:56
msgid "Today"
msgstr "Bugün"
#: podcasts-gtk/resources/gtk/home_view.ui:112
msgid "Yesterday"
msgstr "Dün"
#: podcasts-gtk/resources/gtk/home_view.ui:168
msgid "This Week"
msgstr "Bu Hafta"
#: podcasts-gtk/resources/gtk/home_view.ui:224
msgid "This Month"
msgstr "Bu Ay"
#: podcasts-gtk/resources/gtk/home_view.ui:281
msgid "Older"
msgstr "Daha Eski"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:101
msgid "An in-app action notification"
msgstr "Uygulama içi eylem bildirimi"
#: podcasts-gtk/resources/gtk/inapp_notif.ui:112
msgid "Undo"
msgstr "Geri al"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
msgid "Rewind 10 seconds"
msgstr "10 saniye geri sar"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
msgid "Play"
msgstr "Oynat"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Duraklat"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "10 saniye ileri sar"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed"
msgstr "Kayıttan yürütme hızını değiştir"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
msgid "1.00×"
msgstr "1.00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
msgid "1.50×"
msgstr "1.50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348
msgid "1.5 speed rate"
msgstr "1.5 hız oranı"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
msgid "1.25×"
msgstr "1.25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366
msgid "1.25 speed rate"
msgstr "1.25 hız oranı"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
msgid "Normal speed"
msgstr "Olağan hız"
#: podcasts-gtk/resources/gtk/prefs.ui:42
#: podcasts-gtk/resources/gtk/prefs.ui:295
msgid "Preferences"
msgstr "Tercihler"
#: podcasts-gtk/resources/gtk/prefs.ui:76
msgid "Appearance"
msgstr "Görünüm"
#: podcasts-gtk/resources/gtk/prefs.ui:120
msgid "Dark Theme"
msgstr "Karanlık Gövde"
#: podcasts-gtk/resources/gtk/prefs.ui:166
msgid "Delete played episodes"
msgstr "Oynatılan bölümleri sil"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Şundan Sonra"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played"
msgstr "Tüm Bölümleri Oynatıldı Olarak _İmle"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:11
msgid "_Website"
msgstr "_Web Sitesi"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:15
msgid "_Unsubscribe"
msgstr "_Abonelikten Çık"
#: podcasts-gtk/resources/gtk/show_menu.ui:51
msgid "Open Website"
msgstr "Web Sitesini Aç"
#: podcasts-gtk/resources/gtk/show_menu.ui:64
msgid "Mark All as Played"
msgstr "Tümünü Oynatıldı Olarak İmle"
#: podcasts-gtk/resources/gtk/show_menu.ui:89
msgid "Unsubscribe"
msgstr "Abonelikten Çık"
#: podcasts-gtk/src/app.rs:331
msgid "Fetching new episodes"
msgstr "Yeni bölümler alınıyor"
#: podcasts-gtk/src/headerbar.rs:98
msgid "You are already subscribed to this show"
msgstr "Bu gösteriye zaten abonesiniz"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "Geçersiz URL"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Saniye"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Dakika"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Saat"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Gün"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Hafta"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New"
msgstr "Yeni"
#: podcasts-gtk/src/stacks/content.rs:36
msgid "Shows"
msgstr "Gösteriler"
#: podcasts-gtk/src/utils.rs:350
msgid "Select the file from which to you want to import shows."
msgstr "Gösterileri içe aktarmak istediğiniz dosyayı seçin"
#: podcasts-gtk/src/utils.rs:353
msgid "_Import"
msgstr "_İçe Aktar"
#: podcasts-gtk/src/utils.rs:362
msgid "OPML file"
msgstr "OPML dosyası"
#: podcasts-gtk/src/utils.rs:379
msgid "Failed to parse the imported file"
msgstr "İçe aktarılan dosya ayrıştırılamadı"
#: podcasts-gtk/src/utils.rs:384
msgid "Selected file could not be accessed."
msgstr "Seçilen dosyaya erişilemedi."
#: podcasts-gtk/src/widgets/aboutdialog.rs:26
msgid "Podcast Client for the GNOME Desktop."
msgstr "GNOME Masaüstü için Podcast İstemcisi."
#: podcasts-gtk/src/widgets/aboutdialog.rs:35
msgid "Learn more about GNOME Podcasts"
msgstr "GNOME Podcastler hakkında daha çoğunu öğrenin"
#: podcasts-gtk/src/widgets/aboutdialog.rs:40
msgid "translator-credits"
msgstr "Emin Tufan Çetin <etcetin@gmail.com>"
#: podcasts-gtk/src/widgets/episode.rs:129
msgid "{} min"
msgstr "{} dak"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:301
msgid "The media player was unable to execute an action."
msgstr "Ortam oynatıcı eylemi yürütemiyor."
#: podcasts-gtk/src/widgets/show_menu.rs:150
msgid "Marked all episodes as listened"
msgstr "Tüm bölümler dinlendi olarak imlendi"
#: podcasts-gtk/src/widgets/show_menu.rs:155
msgid "Unsubscribed from {}"
msgstr "{} aboneliğinden çıkıldı"
#~ msgid "Episode Title"
#~ msgstr "Bölüm Başlığı"

Some files were not shown because too many files have changed in this diff Show More