933 Commits
Author SHA1 Message Date
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
Jordan Petridis dea517c17c Update the Changelog. 2018-06-05 14:58:29 +03:00
Jordan Petridis 6d93ceb910 EpisodeWidget: Minor style change. 2018-06-05 14:33:18 +03:00
Jordan Petridis 9b0ac5b83d EpisodeWidget: Do not lock the Proggress struck when running update callbacks.
Previously each time we wanted to inspect the `Progress` struct we
were blocking which was problematic since the downloader also wants
to block to update it.

Now we use try_lock() and if a lock can't be aquired we requeue another
callback. That way we can also be way more aggressive about the interval
in whihc it the callbacks will run.
2018-06-05 14:17:37 +03:00
Jordan Petridis acabb40171 CI and Flatpak: Use the 3.28 runtime till fdo 1.8 is fixed. 2018-06-04 14:25:38 +00:00
Jordan Petridis 04cd56ca16 CI: change the image of the flatpak job. 2018-06-02 20:55:09 +00:00
Jordan Petridis 4371512ba2 Merge branch 'episode-wiget-refactor' into 'master'
Episode wiget refactor

See merge request World/hammond!38
2018-06-02 20:42:46 +00:00
Jordan Petridis ced686e1cd EpisodeWidget: Remove explicit type declarations. 2018-06-02 22:33:00 +03:00
Jordan Petridis 272aab2397 EpisodeWidget: Document determine_state method. 2018-06-02 22:00:30 +03:00
Jordan Petridis a7f87f2ac8 ShowWidget: Fix markallplayed functionality.
cfe79a73d6 changed the structure
of the EpisodeWidget and broke this.
2018-06-02 21:34:19 +03:00
Jordan Petridis f9e85155a8 Remove unused dependancy. 2018-06-02 21:13:30 +03:00
Jordan Petridis d281c18951 EpisodeWidget: Pass EpisodeWidgetQuery by refference. 2018-06-02 21:03:07 +03:00
Jordan Petridis 04e7f4f8a7 EpisodeWidget: Wire the download_checker callback again.
If an episode is being downloaded we setup a callback that will
supervise the widget and update it's state once the download action is
completed and the episode rowid is removed from `manager::ACTIVEDOWNLOADS`.
2018-06-02 21:03:00 +03:00
Jordan Petridis a090c11f4a EpisodeWidget: Wire the progressbar again. 2018-06-02 19:42:19 +03:00
Jordan Petridis c303c697a9 EpisodeWidget: Wire the total_size label again.
The size might be provided by the rss feed but not alwasy. Additionally it might be
missleading so when a download starts we replace the label with the
HTTP ContentLength header.
2018-06-02 19:25:25 +03:00
Jordan Petridis 9466c5ea10 EpisodeWidget: Wire the cancel button. 2018-06-01 21:30:56 +03:00
Jordan Petridis 1268fcf1cc EpisodeWidget: Wire the download button. 2018-06-01 19:08:56 +03:00
Jordan Petridis 86d06fa879 EpisodeWidget: Wire the play button again. 2018-06-01 16:49:06 +03:00
Jordan Petridis cfe79a73d6 EpisodeWidget: Initial refactor.
State machines were a fun experiement but a nightmare to deal with
after the fact. This is the first steps for a refactor in a tradition
style with the goal to ultimatly making it easy to and port to relm.
2018-06-01 16:19:33 +03:00
Jordan Petridis d7a9d9ddc8 CI: FIx the clippy cache. 2018-05-31 14:46:51 +03:00
Jordan Petridis b3d45384e1 meson: Add debug build. 2018-05-31 14:45:54 +03:00
Jordan Petridis 64099e37e5 Merge branch 'dialog-enchance' into 'master'
Avoid un-closable AboutDialog

See merge request World/hammond!37
2018-05-31 09:42:52 +00:00
Zander Brown 2fe612d392 Avoid un-closable AboutDialog 2018-05-31 08:35:09 +00:00
Jordan Petridis 14d72b92cb h-gtk: Move appnotif.rs into the widgets module. 2018-05-30 16:45:46 +03:00
Jordan Petridis 8c0055723c cargo fmt 2018-05-30 16:25:15 +03:00
Jordan Petridis bb9e368b2d Merge branch 'ZanderBrown/hammond-gaction_macro' 2018-05-29 22:21:55 +03:00
Zander Brown 2c203acbd2 Use a macro when setting up simple SimpleActions 2018-05-29 17:16:05 +00:00
Jordan Petridis 7115eb573c Downgrade Diesel to 1.2.x
disel_migrations 1.3 triggeres some clippy lints atm.
2018-05-29 18:16:57 +03:00
Jordan Petridis e626c6f286 app.rs: Factor out the GAction definitions. 2018-05-29 14:04:09 +03:00
Jordan Petridis 24058f9534 h-gtk: Write doc comment for aboutdialog. 2018-05-29 13:36:22 +03:00
Jordan Petridis a8d47e9a72 app.rs: Remove unused variable. 2018-05-29 13:26:36 +03:00
Jordan Petridis 9a2f51b48d Update deps. 2018-05-28 22:06:07 +03:00
Jordan Petridis 667deef5f2 Use a mpmc channel instead of the mspc from the std. 2018-05-28 20:49:12 +03:00
Jordan Petridis aa349aa935 Flatpak: Remove access to XDG_HOME.
In 75c50392cb we switched to use the
FileChooserNative API which means that filesystem access is automatically
handled by the Documents(!)(Might be another one) xdg portal.
2018-05-28 19:50:25 +03:00
Jordan Petridis 666ab01d03 Merge branch 'appmenu' into 'master'
Appmenu

See merge request World/hammond!33
2018-05-28 16:41:25 +00:00
Zander Brown ffbab0136f Bind F10 to open the menu
Because we aren't using app-menu accels aren't automatically binded
2018-05-27 15:48:27 +01:00
Zander Brown b5f7399b2c RIP appmenu
F5 -> <primary>r for refresh
2018-05-27 14:34:58 +01:00
Zander Brown f1892eeba2 Always show hamburger menu 2018-05-22 10:46:50 +01:00
Zander Brown e7128a57db Resolve some comments 2018-05-22 10:28:13 +01:00
Zander Brown 793cafd294 Formatting updates 2018-05-22 09:55:00 +01:00
Zander Brown 9b1097effe Merge branch 'master' of https://gitlab.gnome.org/World/hammond into appmenu 2018-05-22 09:51:31 +01:00
Zander Brown 079ae0e1f3 Fallback to hamburger when the environment doesn't like app menus 2018-05-21 13:01:06 +01:00
Zander Brown e181a9837a Merge upstream master 2018-05-21 12:01:32 +01:00
Zander Brown ca5c7022ef Fixed some shortcut display issues
Also give FileChooserNative arguments in the right order & add F5 to refresh
2018-05-21 11:49:35 +01:00
Zander Brown 75c50392cb Everything works (ish)
Also use FileChooserNative for flatpak nicities
2018-05-21 10:06:10 +01:00
Jordan Petridis 74f8e744ac Version bump. 2018-05-20 17:47:33 +03:00
Jordan Petridis 1c657036da Update Changelog. 2018-05-20 17:40:27 +03:00
Jordan Petridis 2869bb3ef3 Flatpak: Allow the app to access the Home folder.
This is needed in order to import an OPML file. There
might be a better solution to this.
2018-05-20 17:37:48 +03:00
Jordan Petridis 784e117a8a cargo fmt 2018-05-20 16:39:36 +03:00
Zander Brown 8c2ea052de Keyboard shortcut overview!
(shame everything else is broken...)
2018-05-20 13:59:00 +01:00
Jordan Petridis 0b6bbf6733 CI: Fix rustfmt job. 2018-05-20 14:36:20 +03:00
Jordan Petridis 2312df3718 Fix rustfmt config. 2018-05-20 14:32:06 +03:00
Zander Brown 095dd73c52 Move refresh 2018-05-19 22:11:44 +01:00
Zander Brown ac6ac42860 Move import 2018-05-19 21:48:38 +01:00
Zander Brown c6ce888cc7 Define an app-menu with About & Quit actions
Rename some paths for auto resource magic
2018-05-19 20:38:36 +01:00
Jordan Petridis c4e6fcc451 Version bump. 2018-05-19 13:11:02 +03:00
Jordan Petridis 22ae5242b2 Update Changelog. 2018-05-19 12:44:14 +03:00
Jordan Petridis 4d77281249 Update deps. 2018-05-19 12:17:55 +03:00
Jordan Petridis b77a373efa Opml: remvoe uneccesary to_string invocation. 2018-05-17 15:08:38 +03:00
Jordan Petridis 463c3908d4 Revert "gitlabci: switch to test rust-nightly till the stable image is update."
This reverts commit 2711ebca68.

Rust stable image is updated again.
2018-05-16 21:00:35 +03:00
Jordan Petridis 79239a2e01 gitlabci: Cache cargo for the clippy job. 2018-05-16 20:59:54 +03:00
Jordan Petridis 41073615e9 Clippy: Derive Copy for appnotif::UndoState 2018-05-16 19:54:07 +03:00
Jordan Petridis bd12b09cbc ShowsView: Fix a bug where the last show would not be shown.
utils::lazy_load() now calls widget.show() for each widget it adds
to the parent container.
2018-05-16 19:44:58 +03:00
Jordan Petridis 7d598bb1d0 h-gtk: Smooth out some stack transitions. 2018-05-16 19:40:36 +03:00
Jordan Petridis ccd3e3ab2c h-gtk: Show error notifications when OPML import fails. 2018-05-16 17:54:32 +03:00
Jordan Petridis 118dac5a1a app.rs: Add an action for showing error notification. 2018-05-16 17:30:43 +03:00
Jordan Petridis 7035fe05c4 InAppNotification: Extend the widget to allow showing notifications without an undo button. 2018-05-16 16:58:03 +03:00
Jordan Petridis af7331c6c6 h-gtk: Rename EpisodeViewWidget to HomeEpisode. 2018-05-16 16:33:54 +03:00
Jordan Petridis 4d2b64e79d h-gtk: Remember the vertical allingment of the ShowsView. 2018-05-13 22:47:56 +03:00
Jordan Petridis d47bbd6131 Remove explicit and not needed inline calls.
This code is not performance critical and the compiler will already
inline whatever it thinks it might benefit it.
2018-05-13 22:08:25 +03:00
Jordan Petridis 54fafa07a2 h-gtk: Use clone! macro to replace some boilerplate. 2018-05-13 21:51:32 +03:00
Jordan Petridis 97eef9149c import_dialog: Do not show hidden files in the FileChooser. 2018-05-13 00:22:54 +03:00
Jordan Petridis b95e70a8c4 import_dialog: Only show xml files in the FileChooser. 2018-05-13 00:06:35 +03:00
Jordan Petridis a16d7de1ac Add EPIC.md issue template. 2018-05-12 23:15:53 +03:00
Jordan Petridis 6f590a64af gitlabci: Fix review-app. 2018-05-12 23:01:50 +03:00
Jordan Petridis 00e747eb5f h-gtk: Wire the import_shows button on the hamburger menu to the the opml import. 2018-05-12 22:55:35 +03:00
Jordan Petridis 2d8164cf0a Clippy: remove useless attribute.
Also merge some attributes together.
2018-05-12 15:02:33 +03:00
Jordan Petridis be1a8df3ef Headerbar: simplify the naming scheme a bit.
The type of the widgets is already a Button so there's no need in
repeating that in the struct field.

Also remove some type annotations since the compiler can infer them
from the type of the struct fields.
2018-05-12 14:45:00 +03:00
Jordan Petridis e8ca2faaa7 Headerbar: Add import and export items in the hamburger menu.
I strongly believe that these do not belong there and should be moved
elsewhere. But for the time being and prototyping they are "good enough".

People most of the time tend to import from an OPML file only on the first
use of the App. Then the functionality sits there and is never used again.
That's why I think import should be moved into a preferences dialogs and
have the empty view suggest the action.

Exporting OPML files is also not that common, I also believe this should be
moved into a preference dialog instead of the hamburger menu.
2018-05-12 14:17:40 +03:00
Jordan Petridis 83f9284b05 Cargo fmt 2018-05-12 13:45:19 +03:00
Jordan Petridis 6e5c70ab71 opml: Change the signature of import_opml function.
xml::reader::Error is the only error that can be returned so there
is no need to use the DataError type.
2018-05-12 13:19:14 +03:00
Jordan Petridis ab4f958b5f Readme: bump the minimum rust version. 2018-05-11 13:55:34 +03:00
Jordan Petridis 2711ebca68 gitlabci: switch to test rust-nightly till the stable image is update. 2018-05-11 13:54:25 +03:00
Jordan Petridis 041684b13a Use impl Trait syntax instead of Trait Objects.
Rust v1.26 introduced impl Trait which avoid the heap allocation with Box
and makes the code a bit more ergonomic.

For more see https://blog.rust-lang.org/2018/05/10/Rust-1.26.html
2018-05-11 12:06:31 +03:00
Jordan Petridis 777a2102f8 gitlabci: Add review apps.
This makes it able to show a link pointing to the bundle in Merge Requests.
2018-05-10 18:46:52 +03:00
Jordan Petridis 65949c5af5 gitlabci: Extend the lifespan of the flatpak bundle.
For more information see the following nautilus commit.
https://gitlab.gnome.org/GNOME/nautilus/commit/3a35b6035ae6f383744aa88b73e451918292736c

Closes #60.
2018-05-10 18:46:52 +03:00
Jordan Petridis e40f880b9e gitlabci: Use prebuilt clippy image
Since clippy won't fail to install now that it's bundled in the
container image also do not allow the to fail
2018-05-10 18:38:51 +03:00
Jordan Petridis f9b34bbd50 h-data: Initial implementation of an OPML parser and importer.
This is not really compiant with the OPML spec and there
does not seem to be an OPML crate sadly. There are edge-cases
that are not handled but will only be addressed if a problem is reported.
2018-05-10 18:17:19 +03:00
Jordan Petridis f06dbd0562 Version bump. 2018-05-07 19:48:21 +03:00
Jordan Petridis 4a6f5c6268 Update the Changelog. 2018-05-07 18:46:09 +03:00
Jordan Petridis b5dbfb1a86 PopulatedStack: Allow for more control over the stack transitions.
When you just update/replace the widget there is no need for an animation
to occur. Thus why animations where broken before. This commit is not ideal
as it makes it the responsibility of the caller to declare valid(UX wise)
transitions.
2018-05-04 11:32:50 +03:00
Jordan Petridis d86a17f76e ShowWidget: Set max_width_charters in the Description.
If the window is fullscreen or given a big width description
expands and becomes harder to read. This sets the character limit
of the description to 70charaters. The exact size might differ
from machine-to-machine based on user settings. (Hi-dpi, chosen
font, etc.)
2018-05-02 19:29:55 +03:00
Jordan Petridis 37c7f20256 gitlabci: Always run clippy as part of the lint state now. 2018-05-02 16:05:03 +03:00
Jordan Petridis f324407c9c Deny all the warnings when building. 2018-04-30 14:21:34 +03:00
Jordan Petridis c96b39d597 Fix all the clippy warnings! 2018-04-30 14:13:54 +03:00
Jordan Petridis d4d89a56e9 Revert "Update deps."
This reverts commit f19ad133c6.

There was dependancy conflitct that was not caught locally due to caching.
2018-04-29 20:15:44 +03:00
Jordan Petridis f19ad133c6 Update deps. 2018-04-29 19:57:32 +03:00
Jordan Petridis 00d9019f29 Do not pass some things by value when not needed. 2018-04-29 19:27:40 +03:00
Jordan Petridis 8951a6e237 h-gtk: Animate the adjustment of scrolled windows.
Many thanks to Julian Spaber for documenting this.
2018-04-29 19:07:12 +03:00
Jordan Petridis b0ac037964 Fix the broken test. 2018-04-28 14:53:43 +03:00
Jordan Petridis 2c8ff2d2f2 Cargo fmt 2018-04-28 14:19:55 +03:00
Jordan Petridis 03bd951848 EpisodeWidget: Handle updating states, withotu having to reload the views.
This code is ugly and terrible but it works™. Previsously when a download
would finish it would refresh all the views. Now the if the widget get's
into the Donwloading state, it will setup a callback that will check
periodicly if it's still downloading and update the widget state when
the episode stops downloading.
2018-04-28 14:09:26 +03:00
Jordan Petridis 63e2ea987e This was commited by accident. 2018-04-28 12:41:56 +03:00
Jordan Petridis 115df7f884 h-gtk: Re-work the minimum widget requests. 2018-04-27 12:08:07 +03:00
Jordan Petridis ed94d34589 h-gtk: Rename the stasckswitcher field in the Headerbar. 2018-04-27 11:41:39 +03:00
Jordan Petridis dc5ff9d809 h-gtk: Take into account the ignored_shows when detemening if podcast table is empty.
If you've had one show and pressed unsub, instead of going to
an empty view, it would stay to populated since it the db records
where still there.
2018-04-27 11:21:32 +03:00
Jordan Petridis 72a6832571 h-gtk: Rename HomeView and ShowView parent modules. 2018-04-25 20:57:05 +03:00
Jordan Petridis b0c692ab7d Readme: Add matrix link. 2018-04-25 20:15:43 +03:00
Jordan Petridis 06e8bc14f4 Readme: Remove badges and tweak the description.
Gitlab 10.7 natively supports badges
2018-04-25 20:00:41 +03:00
Jordan Petridis 3d160fc35c h-gtk: Restructure the stacks structure.
This commit reimplements support for the empty view in the ShowStack.
The current structure is the following:
* A Content stack which holds the HomeStack and the ShowStack.
  It's what is used in the headerbar StackSwitcher.

* The HomeStack is composed of the HomeView and an EmptyView.

* The ShowStack is composed of the PopulatedStack and an EmptyView.

* The PopulatedStack is composed of the ShowsView and the ShowWidget
  currently. An AboutEpisode widget is planned to be added here also.
2018-04-25 19:23:02 +03:00
Jordan Petridis 734f85a517 Fix logic typo. 2018-04-24 15:34:52 +03:00
Jordan Petridis a56a80db88 ShowWidget: Keep track of the podcast it was created from.
Since ShowStack now keeps a refference to ShowWidget we no
longer need to encode it in the widget name.
2018-04-24 15:25:34 +03:00
Jordan Petridis c4ed90dd5a ShowStack: Refactor to make stack restructure easier.
This removes the empty widget from the ShowStack. The plan is
to have a ShowsView which will be the parent of ShowStack and
an Empty Widget. Then make ShowStack have only valid populated
states of ShowsPopulate, ShowWidget and AboutEpisodeWidget later.
2018-04-24 13:12:27 +03:00
Jordan Petridis f173b326a4 Contnet: Minor renaming following 75af3c7f2b 2018-04-24 12:28:29 +03:00
Jordan Petridis 5e302290de HomeStack: Minor refactor to wrap gtk::Stack actions.
For now the methods are private and migth not be neccesarry,
but it will be much easier to manipulate the stack from outside
with this API if it's needed and the methods are made public.
2018-04-24 09:59:29 +03:00
Jordan Petridis 75af3c7f2b h-gtk: Rename EpisodeStack to HomeStack. 2018-04-24 09:31:56 +03:00
Jordan Petridis d7aec6fdfb h-gtk: Move vies inside the widgets module.
EpisodeView was renamed to HomeView. More renaming will follow.
2018-04-23 15:57:46 +03:00
Jordan Petridis 9a3586fd1c Readme: Fix the flatpak instructions. 2018-04-23 13:35:04 +03:00
Jordan Petridis 063bcbd0e5 Remove unused pub field. 2018-04-22 06:34:02 +03:00
Jordan Petridis a76e69e05d ShowWidget: Center the description label. 2018-04-21 09:25:21 +03:00
Jordan Petridis e560cce879 h-gtk: Further refactor of the ShowStack. 2018-04-21 08:01:34 +03:00
Jordan Petridis 6406c3af13 h-gtk: Refactor part of the ShowStack. 2018-04-21 07:40:42 +03:00
Jordan Petridis 173d2d3a3a h-gtk: Refactor EpisodesStack. 2018-04-20 17:23:07 +03:00
Jordan Petridis 9a5cc1595d ShowWidget: re-arrange the show's cover/desc/buttons widgets. 2018-04-20 10:20:09 +03:00
Jordan Petridis d1962ab745 Remove some boilerplate. 2018-04-20 10:15:27 +03:00
Jordan Petridis af5b27d0fc EpisodeWidget: Replace Arc<Mutex<Widget>> with Rc<RefCell<Widget>>.
Since gtk Widgets are not Send, and the callbacks all run in the gtk
main loop, it *should* not be possible that 2 things try to access the
same widget at the same time.
2018-04-20 07:26:56 +03:00
Jordan Petridis a9196d27d6 Merge branch 'master' into 'master'
Update links to new repo

See merge request World/hammond!32
2018-04-20 00:34:35 +00:00
Ivan Augusto 2f1ea12cfa Update links to new repo 2018-04-19 17:23:46 -03:00
Jordan Petridis 37a408e58a dbquerries: Fix the is_populated() querries.
Thanks a lot to the diesel gittrer channel!
2018-04-19 14:45:00 +03:00
Jordan Petridis 736a993284 h-gtk: Move forgotten test from a68987f257 2018-04-19 08:26:45 +03:00
Jordan Petridis 09359a8df3 Update deps and bump rss crate.
My PR for Channelinto_items() went through and a new rss
release in is already available!
2018-04-19 08:07:02 +03:00
Jordan Petridis a68987f257 h-gtk: Move some stuff from utils to settings module. 2018-04-19 08:04:40 +03:00
Jordan Petridis 3b5831f317 ShowsView: Do not block while loading ShowChilds. 2018-04-19 07:51:48 +03:00
Jordan Petridis 5336981154 h-gtk: Change the signature of utils::set_image_from_path to not require a Podcast.
It was only used to call the podcast.id() method. This allows EpisodeViewWidget
to be created whithout the need for a call to the database to get a Podcst
Object for each widget.
2018-04-19 07:15:12 +03:00
Jordan Petridis df417fa619 h-gtk: Use Rc instead of Arc wherever possible.
As logn we are not doing anything funny to bypass the borrow-checker,
we should not be able to touch gtk wigets from other threads anyway.
2018-04-19 06:34:02 +03:00
Jordan Petridis 509bbe25d2 EpisodeView: Retain the scrollbar valignment. 2018-04-19 06:12:08 +03:00
Jordan Petridis f49012ab51 EpisodeView: Reduce boilderplate. 2018-04-19 05:40:07 +03:00
Jordan Petridis e4fc7c336e EpisodeView: Fix empty state. 2018-04-19 05:32:27 +03:00
Jordan Petridis 0e4430bae4 EpisodeView: Initial refactor to make loading non-blocking. 2018-04-19 02:52:58 +03:00
Jordan Petridis f811a9c8f4 Feed: Split index_channel_items method. 2018-04-18 08:20:26 +03:00
Jordan Petridis 771999c603 h-data: Move some functions from pipeline to feed module. 2018-04-18 07:35:53 +03:00
Jordan Petridis 18820202d7 gitlabci: Add needed ENV vars.
abc8fb988f uses an feature of rayon
that's behind a compile time flag.
2018-04-18 07:06:17 +03:00
Jordan Petridis abc8fb988f Pipeline: Dispatch feed indexing to the rayon threadpool. 2018-04-18 05:06:02 +03:00
Jordan Petridis 885c525d7b Pipeline: change the signature of pipeline to accept future::Stream instead of IntoIterator. 2018-04-18 04:05:14 +03:00
Jordan Petridis 418a2f02b2 Pipeline: Add a bad feed test case. 2018-04-18 03:38:06 +03:00
Jordan Petridis 031078284c Feed: Print an error in stderr before discarding it. 2018-04-18 03:03:05 +03:00
Jordan Petridis 3c7ba8c9d9 Feed: Convert index_channel_items to a Future/Steam impl. 2018-04-18 02:49:21 +03:00
Jordan Petridis 835078a84c Pipeline: Convert the sources iterator into a Stream and return a Future
`futures::stream::iter_ok` is so conviniet, why had None told me
about it before?
2018-04-18 01:40:06 +03:00
Jordan Petridis 049418c2f5 Feed: clean up clunky impl of indexing episodes. 2018-04-17 12:05:10 +03:00
Jordan Petridis 7c03266d16 Inline a bunch of stuff. 2018-04-17 09:04:18 +03:00
Jordan Petridis 627f06ea9f Fix typos. 2018-04-17 08:50:03 +03:00
Jordan Petridis 9f84178182 h-gtk: Increase the sleep time between the action channel calls. 2018-04-17 07:52:04 +03:00
Jordan Petridis 54641fc3c5 ShowWidget: Try to retain scrollbar adjustment. 2018-04-17 06:00:06 +03:00
Jordan Petridis b8995d838a ShowWidget: Move listbox population to widgets/show.rs 2018-04-17 04:44:55 +03:00
Jordan Petridis a0154c5919 lazy_load: Add the ability to execute a callbakc on finish
When iteration of data is finished, None will be returned and
the or_else() block will be executed. Now a callback can be
passed that will be executd when the iteration/loading finishes.
2018-04-17 03:13:01 +03:00
Jordan Petridis 08365c412a h-gtk:utils Add a more flexible implementation of lazy_load.
lazy_load_full is meant for siturations that you don't need
the constraisn of passing a single container parent and adding
a sigle widget to it.

Reimplemnted lazy_load on top of lazy_load_full.
2018-04-17 02:33:32 +03:00
Jordan Petridis 2d291a08fc h-data: Refactor the Diesel helper traits to use Associated Types. 2018-04-17 01:33:50 +03:00
Jordan Petridis 7a17b3df4b ShowWidget: Restore sensitivite of the unsub button.
If you clicked unsub, then undo and then the same show widget you
would navigated to the previous ShowWidget and the unsub button
would still be insensitive.
2018-04-16 07:43:30 +03:00
Jordan Petridis 0589f2fe2a h-gtk: Move show notification creation into widgets/show.rs 2018-04-16 05:45:58 +03:00
Jordan Petridis a9abd75b51 h-gtk: Nuke Action::UpdateSource.
Use inline glib::idle_add since it can be called on the spot.
2018-04-16 04:34:17 +03:00
Jordan Petridis bc6eeec663 Replace if Let Err(_) with .map_err().ok() patterns.
I dislike the indentation and the noise if let adds.
2018-04-16 04:03:44 +03:00
Jordan Petridis 3132856efe h-gtk/utils: Remove expects and unwraps on senders 2018-04-16 01:27:59 +03:00
Jordan Petridis 4db7628eed h-gtk/utils: Make refresh_feed methods generic over Source. 2018-04-16 01:12:27 +03:00
Jordan Petridis 7b71f59d3e ShowWidget: Make unsub button insensitive instead of hidding it. 2018-04-15 23:53:28 +03:00
Jordan Petridis 76c177bc0f ShowWidget: Add a scrolled-window to the show description. 2018-04-15 04:07:43 +03:00
Jordan Petridis 50a508b596 Improve formatting 2018-04-15 02:50:06 +03:00
Jordan Petridis 59c634f626 Source: change the signature of the request constructor
We don't really need to return Self.
2018-04-15 02:39:24 +03:00
Jordan Petridis 08ebb9e7d6 pipeline: Make run function generic again.
also minor formatting changes.
2018-04-14 08:03:58 +03:00
Jordan Petridis f5f0a5b873 Remove dead code. 2018-04-14 07:52:55 +03:00
Jordan Petridis 1036176e51 pipeline: Make sure that the futures will be run.
Use .then() combinator to override the result and return
Ok(()) even if the task fails. That allows us to use join_all
instead of the custom written collect_futures function.
2018-04-14 07:41:50 +03:00
Jordan Petridis c6a24e839a h-data: Implement a tail-recursion loop to follow redirects.
Follow http 301 permanent redirects by using a future::loop_fn.
It's kinda funcky, match_status still returns status_codes as erros
and a new DataError Variant had to be added to distiguise when we
should Loop::Continue. This could be cleaned up a lot.
2018-04-14 05:30:29 +03:00
Jordan Petridis 87421ce74d Cargo fmt 2018-04-13 04:35:50 +03:00
Jordan Petridis f94ccb9947 InAppNotification: Remove the need to pass a sender. 2018-04-13 03:46:32 +03:00
Jordan Petridis 1f1d4af8ba Nuke the custom configure file.
There is nothing provided that meson can't do on it's own.
2018-04-13 01:54:35 +03:00
Jordan Petridis 633803e0ad h-gtk: Fix views not updating after a download completes.
While ideally we want episode widget to determine their states
themselves and avoid refreshing the whole view, currently there
is no infrastructure for that which resulted in views not being
updated their EpisodeWidgets stuck in the InProggress state.
2018-04-12 06:54:08 +03:00
Jordan Petridis 74fb8dc75c Update deps. 2018-04-12 05:06:33 +03:00
Jordan Petridis 2523a0cf90 Enable backtraces on the flatpak builds. 2018-04-12 02:49:51 +03:00
Jordan Petridis 47a58a9e65 Improve formatting 2018-04-12 02:49:27 +03:00
Jordan Petridis 27c4fd9b30 Remove .expect() call on channel that can be dropped. 2018-04-12 02:42:52 +03:00
Jordan Petridis f3904c599b Remove dead From implementations. 2018-04-12 02:17:23 +03:00
Jordan Petridis b86f288e86 EpidoseWidget: Recalculate widget's state when cancel is clicked.
Previously we would refresh all the views when download/cancel
button was clicked. This was done mainly to avoid zombie widget bugs
that would arise from shared state.

Now we still refresh all the background views but not the visible one.
Instead the widget has the reponsibility of recalculating it's state.
2018-04-12 02:00:23 +03:00
Jordan Petridis 67bdd3664a EpisodeWidget: Remove Widget::set/get name calls.
I don't even remember why this was there.
2018-04-12 00:13:43 +03:00
Jordan Petridis 8d4fdb8ece EpidoseWidget: Only refresh background views when download is clicked. 2018-04-11 23:59:08 +03:00
Jordan Petridis 0720222423 h-gtk/app: use idle_add instead of timeout_add for updating on startup. 2018-04-10 21:07:07 +03:00
Merge Bot, Bors Wannabe 7bca841a1a Merge branch '8-lazy-episodes' into 'master'
Resolve "Lazy evaluate Podcasts and Episodes instead of loading everything in one go."

Closes #8

See merge request World/hammond!31
2018-04-10 17:08:21 +00:00
Jordan Petridis 572ab86bc4 Document utils::lazy_load. 2018-04-10 16:57:08 +00:00
Jordan Petridis 29cf5940f5 Lazy_load: move to utils module and make it public. 2018-04-10 16:57:08 +00:00
Jordan Petridis 4b4f5c39d4 Lazy_load: improve the naming scheme. 2018-04-10 16:57:08 +00:00
Jordan Petridis 5069430a3a Lazy_load: remove unnecessary clone of an Rc pointer. 2018-04-10 16:57:08 +00:00
Jordan Petridis 28d7373779 Lazy_load: Use IntoIterator for T, instead of Iterator. 2018-04-10 16:57:08 +00:00
Jordan Petridis 9d5fa04d49 Lazy_load: accept an iterator instead a Vec<_> over T. 2018-04-10 16:57:08 +00:00
Jordan Petridis 43bf8b3f15 Lazy_load: Avoid manually indexing.
make the data: Vec<T> mutable, then reverse the vector
so it can be used as a stack, and then use the ::pop()
method to retrieve the item.

This also avoid the constrain for Clone on T.
2018-04-10 16:57:08 +00:00
Jordan Petridis ed80605755 Move the lazy_load logic to a Generic function. 2018-04-10 16:57:08 +00:00
Jordan Petridis cc84a4637d EpisodesListBox: Do not block while fetching episode backlog. 2018-04-10 16:57:08 +00:00
Jordan Petridis 701b759ba2 EpisodesListBox: Add each widget lazyly. 2018-04-10 16:57:08 +00:00
Jordan Petridis 9cb2782ef9 ShowWidget: Initial Lazier evaluation of the widgets. 2018-04-10 16:57:08 +00:00
Jordan Petridis 5cd0a3c451 Fix the things I broke in b74dbb74bb
Someone really needs to restrict my access to anything that involves
transistors when I am sleep deprived.
2018-04-10 07:01:55 +03:00
Jordan Petridis b74dbb74bb h-data: Remove rel attributes from <a> tags when sanitizing html.
They are invalid in `pango` markup so theres no reason they
should are not needed. Also add some paranoid .trim() calls.
It returnes a &str slice so it's cheap.
2018-04-10 06:31:51 +03:00
Jordan Petridis d332636dd4 Fix the fix that should have fixed the tests. 2018-04-06 19:36:52 +03:00
Jordan Petridis 32654f6cb2 Fix the tests. 2018-04-06 19:18:55 +03:00
Jordan Petridis 14a90e7138 Remove Futures_Cpupool.
The performance boost is not good enough to justify the
code complexity it add and the memory overhead of
yeat another threadpool.

We will start refactoring the whole pipeline implemantation
and might transition to either rayon-futures or tokio-runtime.
2018-04-06 18:18:03 +03:00
Jordan Petridis dd2366a15e Change the git url of the html2pango crate. 2018-04-05 20:44:28 +03:00
Jordan Petridis 370ba2d461 dlmanager: minor cleanup. 2018-04-04 21:36:23 +03:00
Jordan Petridis ef655ef5e0 EpisodeWidget: Keep the widget's heigth contant. Fixes #57 2018-04-04 18:44:23 +03:00
Jordan Petridis 5cc550c830 NewEpisode: refactor a closure to that returned Option<T> to use .and_then instead. 2018-04-04 16:32:03 +03:00
Jordan Petridis 52cbe67756 NewEpisode: refactor another if else statement and document it. 2018-04-04 16:31:59 +03:00
Jordan Petridis c910e0af40 NewPodcast: refactor an if else statement and document it. 2018-04-04 16:31:50 +03:00
Jordan Petridis 7a74534285 Merge branch '25-render-show-description' into 'master'
Resolve "Render link/bold/url attributes in ShowWidget description"

Closes #25

See merge request alatiera/hammond!30
2018-04-03 19:51:49 +00:00
Jordan Petridis 916775462d Update changelog. 2018-04-03 19:42:13 +00:00
Jordan Petridis 3d98600126 h-data: Sanitize html during Podcast/Episode parsing. 2018-04-03 19:42:13 +00:00
Jordan Petridis 7ba834ee8d Update deps now that we are at it. 2018-04-03 19:42:12 +00:00
Jordan Petridis 1c527cba03 Remove more commented out dead code. 2018-04-03 19:42:12 +00:00
Jordan Petridis 2d7ba7b246 h-data/source.rs: Reduce boilerplate. 2018-04-03 19:42:11 +00:00
Jordan Petridis 4ed70a8011 Rss::Error is now Send! 2018-04-03 19:42:11 +00:00
Jordan Petridis 7a3a148359 Remove more dead code. 2018-04-03 19:42:11 +00:00
Jordan Petridis e07e35110d Use pretty assertions! 2018-04-03 19:42:10 +00:00
Jordan Petridis a463753c84 NewEpidode: Use parse rss.description instead of itunes.summary.
We can deal with(sort of) html now, so we should start indexing
the proper rss description. Also cleanup commented out code.
2018-04-03 19:42:10 +00:00
Jordan Petridis a946ddfab1 html_to_pango: Switch to use the new library spawn from this.
Thanks to @danigm for spinning that part of fractal to a shared library.
2018-04-03 19:42:09 +00:00
Jordan Petridis af1cb43bd6 NewPodcast: Prefer the rss.description attribute.
Since we can handle rendering html stuff by converting it to pango
we no longer need the text-only itunes summary attribure.
2018-04-03 19:42:09 +00:00
Jordan Petridis 935d61324f ShowWidget: Convert html to pango markup and render it.
Instead of stipping all the html tags and just using the text
in the label we could *try* converting it to pango markup
which is a bit more flexible than plain text.

The code was copied from Fractal.
2018-04-03 19:42:08 +00:00
Jordan Petridis 10e016f2ea update appdata.xml 2018-04-03 21:41:53 +03:00
Jordan Petridis 154655d571 Readme: Fix the dependancies listed to include rustc and cargo. 2018-04-03 20:49:30 +03:00
Jordan Petridis 491cd8f01c cargo fmt 2018-03-30 17:24:38 +03:00
Jordan Petridis 2b711ff04c Update .doap file 2018-03-30 16:47:09 +03:00
Jordan Petridis 07eadd2364 h-gtk/utils: Improve itunes resolver and add extra test cases. 2018-03-30 16:38:59 +03:00
Jordan Petridis 7086afe73d h-gtk/utils: More refactor to improve formatting. 2018-03-30 15:33:19 +03:00
Jordan Petridis f21398357b h-gtk/utils: Refactor some mutex locks, improve formatting. 2018-03-30 14:39:30 +03:00
Jordan Petridis c338802329 Update deps. 2018-03-30 11:49:54 +03:00
Jordan Petridis c3076e748e cargo fmt 2018-03-30 10:22:34 +03:00
Jordan Petridis 1595256c86 Use rayon to manage all the threads. 2018-03-30 09:31:25 +03:00
Jordan Petridis 0623592f75 Merge branch '7-async-image-loading' into 'master'
Resolve "Make image loading not blocking the programs execution."

Closes #7

See merge request alatiera/Hammond!29
2018-03-29 14:57:44 +00:00
Jordan Petridis f2444f151c h-gtk/utils: Re-work format_err! calls and improve formatting 2018-03-29 15:26:44 +03:00
Jordan Petridis 710a3f2552 Use SendCell::try_get instead of SendCell::into_inner 2018-03-29 15:19:13 +03:00
Jordan Petridis 6071c664e7 Update changelog 2018-03-29 13:32:57 +03:00
Jordan Petridis e203815f4f hammond-gtk/utils.rs: Use a hashset to keep track of cover downloads.
Use a HashSet to check if a download of a cover is already active. If
it is, schedule a callback that will try to set the image from the
cached pixbuf later.
2018-03-29 13:07:46 +03:00
Jordan Petridis c3658080d3 Comment out a test. 2018-03-29 11:37:31 +03:00
Jordan Petridis 8703470010 h-gtk/utils: Use a threadpool to avoid spawning a million threads 2018-03-29 10:24:02 +03:00
Jordan Petridis 88cc7e6fec Fix set_image_from_path test 2018-03-29 09:21:49 +03:00
Jordan Petridis badcbc32c6 Implement async loading of the Show covers. 2018-03-28 22:41:45 +03:00
Jordan Petridis daa8f15ce9 hammond-gtk::utils: change the signature of get_pixbug_from_path and rename it
Requires a gtk::Image as argument now, it sets the pixbuf to the
img directly instead of returning it.
New name is set_image_from_path.

This is ground work so we can later keep the image reference, and
use it to set the image with a callback.
2018-03-28 21:47:10 +03:00
Jordan Petridis 89ee174ded Version bump. 2018-03-28 14:48:43 +03:00
Jordan Petridis cc03c2407b Merge branch '44-empty-show-widget' into 'master'
Resolve "Unpleasant ShowWidget behaviour if the show has no Episodes"

Closes #44

See merge request alatiera/Hammond!28
2018-03-28 11:25:35 +00:00
Jordan Petridis 89254025f3 Update changelog 2018-03-28 13:33:29 +03:00
Jordan Petridis f693c986ec Add an empty_show if Show has no episodes. 2018-03-28 13:24:26 +03:00
Jordan Petridis 3c7f3ecb56 NewPodcast: Fix Image parsing if Itunes ext is Some(None).
Instead of checking if the itunes img url was Some we were assuming
that if an itunes extension existed, it would have an image. That's
not always the case as it turns out there can be an Itunes Ext but
img still be None resulting to not falling back to the Rss image tag.
2018-03-28 12:08:41 +03:00
Jordan Petridis ef3809ed25 Update about dialog. 2018-03-27 16:53:12 +03:00
Jordan Petridis f5ddb7107e Update changelog 2018-03-27 16:43:06 +03:00
Jordan Petridis 1d32018c02 Merge branch 'feature/persist-window-geometry' into 'master'
Issue #50: Persist window geometry

See merge request alatiera/Hammond!25
2018-03-27 12:27:33 +00:00
Rowan Lewis c458b27573 Handle window geometry with a new struct. 2018-03-27 11:54:36 +00:00
Rowan Lewis 524e0bb0a8 Persist window geometry including maximized state for issue #50. 2018-03-27 11:54:36 +00:00
Jordan Petridis d525d1fe59 InAppNofitication: Make revealer field private, change show signature
Accept an overlay widget that the revealer will be attached to into
the show method. Thus revealer field no longer need to be public.
2018-03-27 12:01:53 +03:00
Jordan Petridis bdc6264701 app.rs: Minor formatting improvments. 2018-03-27 11:50:31 +03:00
Jordan Petridis 7e2640e2d0 ShowWidget: When unsub notification expires, remove show from the ignore list.
This should not make any difference regarding the behaviour since
the Show id is eq to the sqlite rowid which means that even
if the same show was removed and readded it would have diff id.
2018-03-27 11:09:53 +03:00
Jordan Petridis 822a72efde gitlabci: Enable the ubuntu/rust stable build, disable tests in flatpak
Due to meson not understanding cargo, it's actually faster to have
a separate build + test job than trying to compile the cargo
tests twice inside the flatpak enviroment
2018-03-27 10:04:02 +03:00
Jordan Petridis 192b13e393 Merge branch 'issue/52' into 'master'
Fix #52: Double border around main window

Closes #52

See merge request alatiera/Hammond!24
2018-03-26 19:21:04 +00:00
Rowan Lewis 2497cb31d0 Remove shadow_type from the show widget. 2018-03-26 18:53:48 +00:00
Rowan Lewis 446a0ede54 Fix #52 by removing the specified shadow_type from the episodes and shows scrolled windows. 2018-03-26 18:53:47 +00:00
Jordan Petridis 3d39638c99 Merge branch '36-add-undo-for-unsubscribing-from-shows' into 'master'
Resolve "Add "undo" for unsubscribing from shows"

Closes #36

See merge request alatiera/Hammond!27
2018-03-26 18:39:19 +00:00
Jordan Petridis 69e87d129a ShowWidget: Hide shows from the Views when unsub is hit. 2018-03-26 14:34:54 +03:00
Jordan Petridis f7a7510322 Implement the shared HashSet with the ignored Shows ids 2018-03-26 13:25:39 +03:00
Jordan Petridis e9f2ba47f2 dbquerries: Add get_episodes and get_podcasts querries that can filters. 2018-03-26 12:46:13 +03:00
Jordan Petridis b2c95e5a73 ShowWidget: display a notification before removing the show. 2018-03-26 10:57:44 +03:00
Jordan Petridis 482ed7c3c6 Update README 2018-03-26 10:11:43 +03:00
Jordan Petridis e497f73316 gitlabci: run glib-compile-resources from the flatpak enviroment 2018-03-21 14:24:56 +02:00
Jordan Petridis 28965dc2b1 gitlabci: Disable normal builds now that tests can run on the flatpak one. 2018-03-19 12:00:53 +02:00
Jordan Petridis 5425ca35b3 Cleanup .gitignore a bit 2018-03-19 11:52:02 +02:00
Jordan Petridis 9f191d0ab8 gitlabci: Run cargo test inside the flatpak. 2018-03-19 11:33:45 +02:00
Jordan Petridis c2a3ce5096 Why the hell you do not cache cargo. 2018-03-19 07:43:04 +02:00
Jordan Petridis 1af06f2e0d Neaw gtk-rs release, yay! 2018-03-19 06:34:28 +02:00
Jordan Petridis 2f2f11b7bc gitlabci: Fix flatpak issue building only master. 2018-03-18 17:03:16 +02:00
Jordan Petridis 260e6015a1 Merge branch 'issue/53' into 'master'
Fix #53 by setting the HTTP user agent string to the latest Firefox ESR.

Closes #53

See merge request alatiera/Hammond!26
2018-03-18 11:34:12 +00:00
Rowan Lewis 7696014545 Fix #53 by setting the HTTP user agent string to the latest Firefox ESR. 2018-03-18 11:57:41 +01:00
Jordan Petridis 2457e95f0e gitlabci: Improve caching 2018-03-18 05:59:35 +02:00
Jordan Petridis 327c907463 gitlabci: try to cache the flatpak build 2018-03-18 05:07:42 +02:00
Jordan Petridis 1b558d3b30 gitlabci: switch flatpak build to the custom image 2018-03-18 05:07:37 +02:00
Jordan Petridis a0d55417cd Merge branch 'feature/gsettings-integration' into 'master'
Integrate gsettings into application

See merge request alatiera/Hammond!23
2018-03-17 23:37:43 +00:00
Rowan Lewis f182d441d1 Allow the Copy trait to do the work for us. 2018-03-18 00:29:52 +01:00
Rowan Lewis e14f215793 Allow cleanup to be blocking on startup and remove cleanup from automatic content refreshes. 2018-03-17 23:50:59 +01:00
Rowan Lewis 34536f4e21 Set dark theme at application startup based on settings. 2018-03-17 23:46:37 +01:00
Jordan Petridis bc2da6e59e gitlabci: add flatpak build and reformat tabs. 2018-03-18 00:31:15 +02:00
Rowan Lewis afdedc7575 Use crono types instead of unsigned integers for time periods. 2018-03-17 01:10:10 +01:00
Rowan Lewis 69a7a9b180 Renamed the 'auto-refresh' settings to 'refresh-interval' so that they represent the internal behaviour better. 2018-03-16 23:31:12 +01:00
Rowan Lewis a7540583d6 Connect settings for auto refresh and cleanup. 2018-03-16 23:23:06 +01:00
Jordan Petridis a253d7ebf5 Merge branch 'feature/gsettings-schema' into 'master'
Added initial settings schema.

See merge request alatiera/Hammond!22
2018-03-16 20:05:19 +00:00
Rowan Lewis 3886402f8e Spelling correction. 2018-03-16 20:45:40 +01:00
Rowan Lewis 97e402b980 Added initial settings schema. 2018-03-16 20:38:37 +01:00
Jordan Petridis 93e15af209 Compress gresource ui files. 2018-03-16 21:29:54 +02:00
Jordan Petridis 9f3a5a13b3 EpisodeWidget: Some RefCell are not really necessary. 2018-03-16 20:11:17 +02:00
Jordan Petridis 3a0fb4bdec Upgrade dependancies. 2018-03-14 04:07:44 +02:00
Jordan Petridis 8a460930c6 Update changelog. 2018-03-14 01:10:02 +02:00
Jordan Petridis a11c4c9bd2 InAppNotification: Twek the margins around the text label. 2018-03-14 00:43:17 +02:00
Jordan Petridis d4b98b5cb2 I hate everything that has to do with centering stuff. 2018-03-14 00:40:29 +02:00
Jordan Petridis fdf3908494 This reverts cc052eb450
Turns out debian stable meson package is kinda old.
2018-03-13 07:23:43 +02:00
Jordan Petridis cc052eb450 gitlabci: use meson to test the build instead of cargo. 2018-03-13 07:03:31 +02:00
Jordan Petridis 4535c3005d Merge branch 'state-machines-experiements' into 'master'
EpisodeWidget as a state machine

See merge request alatiera/Hammond!18
2018-03-13 04:52:40 +00:00
Jordan Petridis 030fed6d12 EpisodeWidget: Just in case there was a deadlock. 2018-03-13 04:57:11 +02:00
Jordan Petridis fc9579cd51 EpisodeWidget: Replace some Mutexs with RefCells.
The state machines are not send and the code is sequnecial.
We only need `&mut machine` refference to pass to `take_mut::take`
to change the state of the machine. In 2/3 cases we can even use
`.get_mut()` method and even avoid the dynamic borrow checks at
runtime. For the `TitleMachine` The only thing that will hold
a refference to it after initialization will be the play_button
callback. So it's justifiable to use `RefCell` insetead of a `Mutex`.
2018-03-13 04:44:06 +02:00
Jordan Petridis 74712b5410 EpisodeWidget: Remove unnecessary Arcs.
`DateMachine` and `DurationMachine` are only mutated during initialization
and thus do not need shared ownership.
`TitleMachine` is only mutated during initialization and after that only
the callback will keep holding a referrence to it. The `EpisodeWidget`s
get dropped after initialization. So it's justifiable to use `Rc<Mutex<T>>`
instead of `Arc`.
2018-03-13 03:47:46 +02:00
Jordan Petridis 05628a2529 Update changelog. 2018-03-12 22:32:52 +02:00
Jordan Petridis 1bdd2f2f5b Merge branch 'master' into state-machines-experiements 2018-03-12 22:10:14 +02:00
Jordan Petridis 3af6e103aa Merge branch '49-itunes-to-rss-resolver' into 'master'
Resolve "Itunes to RSS resolver."

Closes #49

See merge request alatiera/Hammond!20
2018-03-12 19:52:51 +00:00
Jordan Petridis 3dcc20ae86 Update changelog. 2018-03-12 21:14:12 +02:00
Jordan Petridis 8a18630ae5 Initial integration of the itunes resolver with the Add button. 2018-03-12 20:49:02 +02:00
Jordan Petridis b87c331b12 Make the itunes_resolver functions inlined. 2018-03-12 07:28:09 +02:00
Jordan Petridis 9da2414e8b Initial implementation of an itunes_to_rss url resolver. #49 2018-03-12 06:48:05 +02:00
Jordan Petridis 285291b2ed Ignore tests that need access to the host system. 2018-03-12 03:35:07 +02:00
Jordan Petridis dbbb4e589e InAppNotification: Fix autohiding after the callback is executed. 2018-03-09 20:24:28 +02:00
Jordan Petridis 064879c4ce InAppNotification: Remove reduntant Overlay. 2018-03-09 19:46:46 +02:00
Jordan Petridis 8614922213 InAppNotification: Change box margins. 2018-03-09 19:36:43 +02:00
Jordan Petridis 745ea0ca10 Flatpak: Add dconf permissions.
Required also in order to run the gtk+ inspecor.
2018-03-09 17:14:49 +02:00
Jordan Petridis 99bc80c15b ShowWidget: Add a 6px margin to the settings popup. 2018-03-09 17:04:11 +02:00
Jordan Petridis 3423d854e1 ShowWidget: Change the mark_all notif wording. 2018-03-09 16:43:13 +02:00
Jordan Petridis 483fd090f1 InAppNotification: Add close button. 2018-03-09 15:25:53 +02:00
Jordan Petridis 82988b6011 Implement in-app notifications as App wide actions.
At first I tried creating custom InAppNotifications and send
them to the main loop to be added. That does not work sicne gtk
widgets are not thread safe. For now we can try having Action messages
that create them on demand. As oppose to create first then pass them.
2018-03-09 14:53:13 +02:00
Jordan Petridis d1821163c2 Factor out the In-app notification into something generic. 2018-03-09 01:21:54 +02:00
Jordan Petridis 7de118adeb Minor style changes. 2018-03-08 23:14:48 +02:00
Jordan Petridis abe7215bc0 Merge branch '47-mark-all-played' into 'master'
Resolve "Mark all episodes of a Show Feed as played."

Closes #47

See merge request alatiera/Hammond!19
2018-03-08 20:57:04 +00:00
Jordan Petridis 7b064e63b9 ShowWidget: Fix undo notif. 2018-03-08 16:21:42 +02:00
Jordan Petridis f6890c709f ShowWidget: Instantly dim episode titles when mark_all is clicked.
This is would have been way prettier, easier and safer if we could
have custom widgets. But till then I am not sure how to do it better.
2018-03-07 23:04:02 +02:00
Jordan Petridis e4814dbfbc ShowWidget: Initial prototype of an undo notification. 2018-03-07 16:37:10 +02:00
Jordan Petridis 8261b32c99 Update changelog. 2018-03-05 22:07:18 +02:00
Jordan Petridis 9a73520b25 dbquerries: Add a unit test for update_none_to_played_now func. 2018-03-05 21:40:11 +02:00
Jordan Petridis 10db4f7210 ShowWidget: Initial implementation of a menu popup.
Re implement mark_all_episodes_as_watched functionality too.
2018-03-05 20:14:06 +02:00
Jordan Petridis 94f6fdcbe7 Clippy. 2018-03-03 18:52:38 +02:00
Jordan Petridis bb467b7aba Rustfmt. 2018-03-03 16:45:37 +02:00
Jordan Petridis 3da503cdea Use prebuilt image for the rustfmt CI check. 2018-02-26 17:49:20 +02:00
Jordan Petridis b062f0a19f EpisodeWidget Machine: Remove unused From impls. 2018-02-22 13:16:33 +00:00
Jordan Petridis 2a6e0b0e07 Merge branch 'master' into state-machines-experiements 2018-02-22 12:14:55 +00:00
Jordan Petridis 1558ee2177 EpisodeWidget: Fix Date states. 2018-02-22 12:01:07 +00:00
Jordan Petridis 3d542e5554 Readme: add dependancy ci banner. 2018-02-21 09:33:03 +00:00
Jordan Petridis c61938ba62 Update dependancies. 2018-02-20 06:19:05 +00:00
Jordan Petridis c856b88008 EpisodeWidget: Add a Date state machine. 2018-02-19 18:14:34 +00:00
Jordan Petridis fce3684113 Readme: Improve flatpak instructions. 2018-02-19 16:52:58 +00:00
Jordan Petridis ae25dd65bf Cargo clippy and fmt. 2018-02-19 09:58:47 +00:00
Jordan Petridis a88a1c5f1f MediaMachine: Expose an interface to update the ProgressBar and local_size. 2018-02-17 19:49:43 +02:00
Jordan Petridis 0cd678cc1d MediaMachine: Expose an interface to update total_size label. 2018-02-16 17:18:02 +02:00
Jordan Petridis c9bf58af66 EpisodeWidget: Expose cancel button from the state machine. 2018-02-16 16:05:48 +02:00
Jordan Petridis ed87a00225 EpisodeWidget: Cleanup parts of the state machine. 2018-02-16 14:43:16 +02:00
Jordan Petridis bcc3608c04 EpisodeWidget: Split ButtonState enum from the MediaMachine.
Add a ButtonState Machine which represents the state of total_size
label, play button, and download button. Also implemented the
update/determine_state function for ButtonState.

Also implemented required generic functions for MediaMachine<X,Y,Z>
that convert it to the desired state.
2018-02-16 13:32:13 +02:00
Jordan Petridis 038d28779c ShowWidget: Limit description to 100 chars width. 2018-02-16 07:45:29 +02:00
Jordan Petridis 973d47ee05 EpisodeWidget: Expose the connect_clicked callbacks from the statemachine enum. 2018-02-15 18:07:21 +02:00
Jordan Petridis e803e11c81 Fix EpisodeWidget Vertical alignment. 2018-02-15 11:56:56 +02:00
Jordan Petridis f50c990d93 Yay, finally something that works. 2018-02-15 11:33:56 +02:00
Jordan Petridis 72eef6f104 Running in circles. 2018-02-15 11:08:21 +02:00
Jordan Petridis 5ccdb5d100 Minor cleanup. 2018-02-15 05:31:36 +02:00
Jordan Petridis 4b8fceaa7d Nothing makes sense. 2018-02-15 05:15:25 +02:00
Jordan Petridis a24c9b1350 EpisodesView: Fix EpisodeWidget spacing. 2018-02-14 12:44:30 +02:00
Jordan Petridis 8913b7aedb SHowWidet: Experiement with dynamic size. Relevant to #35. 2018-02-14 08:44:02 +02:00
Jordan Petridis 978edfc11f EpisodeWidget: Allow the title to ellipsize. Releavnt to #35. 2018-02-14 08:04:59 +02:00
Jordan Petridis f4b41d0fd3 ShowWidget: Restrict horizontal scrolling. Relevant to #35. 2018-02-14 07:56:27 +02:00
Jordan Petridis 20162a16a8 EpisodesView: Re-work box/frame layout
Restrict horizontal scrolling,
Allow the episode widget to expand along when more space becomes available.
2018-02-14 07:03:26 +02:00
Jordan Petridis 73f7bfa64b I dont even know what I am doing at this point. 2018-02-14 04:18:05 +02:00
Jordan Petridis 159b0d92dd EpisodeWidget: Merge Size and Progress machines, Split total_size to it's own machine. 2018-02-13 07:51:00 +02:00
Jordan Petridis 02de2059db EpisodeWidget: Shrink the Size state Machine. 2018-02-13 05:03:16 +02:00
Jordan Petridis bdf8901dd8 This compiles.
Instead of having a Wrapper of StateMachinesWrappers, use only the desired
possible states in A new struct with only 1 Wrapper that covers all 3 of
the embeded state machines.

I don't even know if the comment makes any sense. Sorry.
2018-02-13 02:23:32 +02:00
Jordan Petridis b6a71688c0 Version bump to 0.3 2018-02-11 22:57:37 +02:00
Jordan Petridis dcc4eae27e Use Gnome Nightly Sdk for the flatpak manifest. 2018-02-11 00:54:17 +02:00
Jordan Petridis 65a080ed1d Readme: Fix rustup link. 2018-02-10 22:42:13 +02:00
Jordan Petridis 48071c28a7 AboutDialog: Add a temporary icon, and update contributors list. 2018-02-10 09:26:08 +02:00
Jordan Petridis 2fbc833ebe EpisodeWidget: Add a state machine that will manager progress_bar and cancel bttn. 2018-02-10 09:11:31 +02:00
Jordan Petridis f7b5b35374 EpisodeWidget: change DownloadPlayMachine default constructor to a hidden state. 2018-02-10 08:13:07 +02:00
Jordan Petridis 46bd23cf66 EpisodeWidget: Add a StateMachine that manages Play and Download Buttons. 2018-02-10 08:00:12 +02:00
Jordan Petridis 138cfdb68c EpisodeWidget: use debug! instead of error! to avoid spamming stderr with *not actuall* errors. 2018-02-10 05:45:50 +02:00
Jordan Petridis 6d9dfe6fe1 EpisodeWidget: Add a StateMachine for the size labels. 2018-02-10 05:41:25 +02:00
Jordan Petridis fc48ce9c47 EpisodeWidget: Migrate Duration Machine to use take mut too, and revert the api to require just &mut self. 2018-02-10 03:33:39 +02:00
Jordan Petridis 3a9a2f4033 EpisdoeWidget: Use take_mut crate to allow for a better api.
Currently it's required that you take mut self in order to manipulate
the internal state machines. This would not allow passing an Arc/Rc to
a callback since A/Rc<T> only derefs to &T and not T.

The take_mut crate allows the retrieval of ownership if you have a &mut refference
and as long you return T again. So Arc<Mutex<Machine> could work with
callbacks and embed Nested state machies without copying.
2018-02-10 03:15:12 +02:00
Jordan Petridis f0ce0eb653 EpisodeWidget: Implement a state machine for duration label. 2018-02-09 10:12:37 +02:00
Jordan Petridis 23979b8f22 EpisodeWidget: Move state machine implementations into a separate module. 2018-02-09 09:13:41 +02:00
Jordan Petridis e22a78fac6 EpisodeWidget: Re-enable on_play_bttn_clicked callback.
Before we were avoiding reloading the widget in view by
directly dimming the title label. Now instead we reload
the whole widget since I can't figure out a way to have
multiple Owneded refferences of the same state machine.
2018-02-09 08:59:50 +02:00
Jordan Petridis 7690cb1356 Remove code duplication using generics. 2018-02-09 08:43:47 +02:00
Jordan Petridis a96f4c57c9 Probably the worst state machine implementation that was ever written. 2018-02-09 08:43:43 +02:00
Jordan Petridis 45522b86df Remove another unwrap. Though it would never be hit currently. 2018-02-08 21:34:11 +02:00
Jordan Petridis 1135d77147 EpisodeWidget: Remove unwrap on that could occur if an invalid path was passed. 2018-02-08 21:10:36 +02:00
Jordan Petridis 9dfb18a487 EpisodeWidget: Minor refactor of set_total_size method. 2018-02-08 21:02:16 +02:00
Jordan Petridis fbfa0de17e EpisodeWidget: Fix minutes label parsing.
Before if a feed had reported a number between 1 and 60, a label 0 min
would be set.

This fixes that, while also using chrono::Duration for parsing minutes.
2018-02-08 20:39:37 +02:00
Jordan Petridis ea29aae64a Fix benchmarks. 2018-02-08 17:33:40 +02:00
Jordan Petridis e77b0a84c7 Update dependancies. 2018-02-08 17:03:26 +02:00
Jordan Petridis c5e0bf34cb Headerbar: Use the Application Action instead of relying directly on Content. 2018-02-08 05:40:40 +02:00
Jordan Petridis 4ba82c1515 EpisodeWidget: Set title alignment from glade. 2018-02-08 05:29:47 +02:00
Jordan Petridis b172aa7aa3 Remove forgotten unwrap(). 2018-02-08 04:39:51 +02:00
Jordan Petridis c6f5a4200d EpisodeWidget: Fix progressbar alignment. 2018-02-07 22:08:52 +02:00
Jordan Petridis e2a1762af4 EpisodeWidget: Use refference counting intead of unnecessary clones.
Pass owenership of EpisodeWidgetQuery and use Atomic Ref counting
to pass to the callbacks. This should avoid extra allocations.
2018-02-07 18:40:22 +02:00
Jordan Petridis 07c1a9a0e9 Fix private field visibility of stacks. 2018-02-07 07:11:40 +02:00
Jordan Petridis 31bd75ae45 Minor Arc changes. 2018-02-07 07:02:24 +02:00
Jordan Petridis 67af85e347 Hammond-gtk: Use Atomic Refference counting to reduce cloning of Podcast. 2018-02-07 06:28:16 +02:00
Jordan Petridis df02054b29 Hammond-gtk: Change the privacy of the widgets module. 2018-02-07 05:47:58 +02:00
Jordan Petridis 0cdd48ac1e Hammond-gtk: Change the privacy of the views module. 2018-02-07 05:37:07 +02:00
Jordan Petridis bdda596806 Hammond-gtk: Inital split of content.rs into stacks module. 2018-02-07 05:33:29 +02:00
Jordan Petridis 8cbae4050e Merge branch '40-gtk-unwrap-cleanup' into 'master'
Resolve "Clean up the unwraps in the Gtk Client."

Closes #40

See merge request alatiera/Hammond!17
2018-02-07 02:12:11 +00:00
Jordan Petridis d3696fc5ec Minor Error messages wording changes. 2018-02-07 03:52:21 +02:00
Jordan Petridis 2d33606251 Hammond-gtk: Stack, Content constructors return Results now.
Constructors now proxy underlying errors that migth occur during initialazation.
I think that's about the last unwraps in the main thread.
2018-02-07 03:17:37 +02:00
Jordan Petridis 89564996df ShowStack: Convert rest methods to return Result<T, Error>. 2018-02-07 01:19:07 +02:00
Jordan Petridis e196a6c905 ShowStack: update_widget methods now return Result<T, Error>. 2018-02-07 00:55:43 +02:00
Jordan Petridis 7eb038b899 EpisodesStack: Refactor update method to return a Result<T, Error>. 2018-02-07 00:31:04 +02:00
Jordan Petridis 1c84f0d721 hammond-gtk::manager: Switch the add function to return a Result<(), Error>. 2018-02-06 22:03:17 +02:00
Jordan Petridis ab519a54d3 Headerbar: Use Result<T, failure::Error> wherever possible. 2018-02-06 21:32:21 +02:00
Jordan Petridis 7ed1cd8b26 hammond-gtk: Change utils::get_pixbuf_from_path function to return a Result. 2018-02-06 20:56:55 +02:00
Jordan Petridis c6e426cbac Downloader: Change cache_image function to return Result<T, DownlaoderError> instead of Option<T>. 2018-02-06 20:14:03 +02:00
Jordan Petridis d3f279374a ShowWidget: Refactor to return Result<T, Error> wherever possible. 2018-02-06 02:47:39 +02:00
Jordan Petridis 0dc16dab9a EpisodeWidget: Refactor to return Result<T, Error> wherever possible. 2018-02-06 02:31:53 +02:00
Jordan Petridis ed5ff16598 Downloader: clean a bit downloader::get_episode function. 2018-02-06 02:00:32 +02:00
Jordan Petridis c1ecdbda52 Merge branch '39-failure-libs-migration' into 'master'
Resolve "Migrate Error-Handling to Failure crate."

Closes #39

See merge request alatiera/Hammond!16
2018-02-05 19:16:11 +00:00
Jordan Petridis d3472b3205 hammond-data: Deny Unused. 2018-02-05 20:51:34 +02:00
Jordan Petridis 5cd3dff1d4 Downloader: Switch to returning Downloader::Error instead of failure::Error. 2018-02-05 20:41:45 +02:00
Jordan Petridis 4d1168803c Dbquerries: Remove Result type allias. 2018-02-05 19:43:26 +02:00
Jordan Petridis 7eec01a52e Minor rename of DataError::Bail. 2018-02-05 19:05:40 +02:00
Jordan Petridis 44ebe46f10 Replace last bail! invocations. 2018-02-05 19:00:31 +02:00
Jordan Petridis 008f57bec4 Create a ParseEpisodeError and replace bail! invocations. 2018-02-05 18:51:21 +02:00
Jordan Petridis d0ae5a4198 Typo fix. 2018-02-05 18:33:12 +02:00
Jordan Petridis 064c2b4be0 Start creating custom errors instead of using bail! macro. 2018-02-05 18:25:56 +02:00
Jordan Petridis 34d7391363 Make a counterfeit rss Error. 2018-02-05 16:37:50 +02:00
Jordan Petridis e84e7df3f1 Remove unwrap call. 2018-02-05 15:59:34 +02:00
Jordan Petridis 8ba9f928d6 Switch to using DataError instead of failure::Error. 2018-02-05 02:43:56 +02:00
Jordan Petridis 435ce05ac7 Add rest of the From impls for the Error Type. 2018-02-05 01:53:01 +02:00
Jordan Petridis ede4c21e30 Merge the Errors into a single Enum. 2018-02-05 01:39:50 +02:00
Jordan Petridis 0892fe26ba Manually implement From Trait for the DatabaseError. 2018-02-05 01:20:01 +02:00
Jordan Petridis 1085d468b9 cargo fmt 2018-02-04 18:45:19 +02:00
Jordan Petridis 8f0a63fdb7 Downloader: Make the Error type public. 2018-02-04 17:46:01 +02:00
Jordan Petridis de43cae015 Switch rest stuff of data/downloader to Failure Crate. 2018-02-04 17:36:27 +02:00
Jordan Petridis f9f015a211 Hammond-data: switch database module to use Failure. 2018-02-04 16:33:10 +02:00
Jordan Petridis 3874235074 Readme: Add a note about being able to built with Gnome-Builder. 2018-02-04 02:03:47 +02:00
Jordan Petridis 1888539a97 Rename assets folder to screenshots, and improve the build system. 2018-02-04 01:34:34 +02:00
Jordan Petridis d131c279b9 Add batch indexing fallback.
If an RSS feed triggered a unique constrains violation, none of it's
items would be indexed resulting in a Podcast without episodes.

This adds a fallback that indexes each item individually if the batch index fails.
2018-02-03 16:02:42 +02:00
Jordan Petridis 92d01ec51a Feed: enchance get_stuff unit test. 2018-02-03 15:55:26 +02:00
Jordan Petridis 97c65c8bd1 Add a download test for Steal the Stars podcast to spot #10 reggressions. 2018-02-03 15:43:00 +02:00
Jordan Petridis e8ae0b0251 Thats a hack, a terrible one, but without it the user
would have to manually click refresh if the user that was added
happen to redirect somewhere else.

The problem is in the hammond-data/src/models/source.rs
fn request_constructor. It has to do with futures and lifetimes
and I am not sure how to tackle it yet.
2018-02-03 00:06:02 +02:00
Jordan Petridis f1f248fa40 Merge branch 'master' into 'master'
Issue 10 - Infinite Redirect Fix

See merge request alatiera/Hammond!15
2018-02-02 16:12:46 +00:00
James Wykeham-Martin a007460029 Added Some to attempt.url() and removed unwrap 2018-02-02 15:41:00 +00:00
James Wykeham-Martin 7a0e7708be formatted 2018-02-02 10:43:20 +00:00
James Wykeham-Martin 5e7d8841af Added loopback detection using current last rather than contains 2018-02-02 09:58:09 +00:00
James Wykeham-Martin d0c5764471 Added custom redirect policy 2018-02-02 09:40:13 +00:00
James Wykeham-Martin 82f577c17d Added custom redirect policy 2018-02-02 09:38:06 +00:00
James Wykeham-Martin 32296d91b7 Merge remote-tracking branch 'upstream/master' 2018-02-02 09:09:43 +00:00
Jordan Petridis 80fd4e9fc5 Source: Set Etag and last modified to None upon redirect. 2018-02-01 19:35:35 +02:00
Jordan Petridis 7b62ef203d Source: Pass owenership around instead of &mut, cause futures. 2018-02-01 15:55:19 +02:00
Jordan Petridis 2f062afb07 Clippy suggestions 2018-01-29 19:09:53 +02:00
Jordan Petridis 817d5d9cf7 Fix meson config to build each time. 2018-01-29 18:17:02 +02:00
Jordan Petridis a2cf2357c6 Add about a basic about dialog. 2018-01-29 18:02:24 +02:00
Jordan Petridis 815639fe5f Add an order by tittle asc clasuse to the Podcast query. Address #34. 2018-01-29 12:30:13 +02:00
Jordan Petridis b1c3b4f9cc Pipeline: Fetch sources without etag support fist as they are more likely to need parsing. 2018-01-29 12:20:21 +02:00
Jordan Petridis 332a439b7a hammond-data: Factor out save method of diesel models into a Trait. 2018-01-28 22:09:06 +02:00
Jordan Petridis fb5264c479 NewSource: Rename into_source -> to_source and take reffence &self instead. 2018-01-27 18:34:03 +02:00
Jordan Petridis a3705d424b NewSource: Remove Insert implemantation and replace it with a insert_or_ignore method. 2018-01-27 18:09:40 +02:00
Jordan Petridis a4660a0700 Headerbar: Do not allow insertion of duplicate urls. 2018-01-27 14:40:51 +02:00
Jordan Petridis 77a52bdc8c hammond-data: Add a source_exists query. 2018-01-27 14:23:19 +02:00
Jordan Petridis 18e55e23ee Headerbar: Do not allow insertion of invalid urls. 2018-01-27 14:11:02 +02:00
Jordan Petridis d14973cf0d Use num_cpus instead of hardcoding the number of threads for dns look_up.
In the next hyper_tls relase it should be possible to use the existing
cpu_pool executor.
2018-01-27 11:08:57 +02:00
Jordan Petridis 120d494280 Update hyper and futures. 2018-01-27 09:45:25 +02:00
Jordan Petridis 8c0e3221cb refresh_feed: remove code duplication. 2018-01-26 19:29:45 +02:00
Jordan Petridis 5e1d1e557f GtkApplication: Add an action for showing the headerbar_update_indicator. 2018-01-26 19:15:07 +02:00
Jordan Petridis a7e3b1b99e GtkApplication: Headbar and Content constructors now return Self instead of Arc<Self>. 2018-01-26 19:05:19 +02:00
Jordan Petridis 1e722e0d58 Merge branch 'futuristic-pipeline' into 'master'
Futuristic pipeline

See merge request alatiera/Hammond!14
2018-01-26 16:33:46 +00:00
Jordan Petridis 39aa86157b Switch rest of the tests to point to the internet archive urls. 2018-01-26 17:25:35 +02:00
Jordan Petridis df85a4ea9e Pipeline: avoid panicking by checking the lenght and returning an error. 2018-01-26 14:17:43 +02:00
Jordan Petridis 2912bad110 Pipeline: Split the pipeline from the cpu-pool and the tokio-core. 2018-01-26 13:34:04 +02:00
Jordan Petridis a57fdef01d Merge branch 'patch-1' into 'master'
Added Flatpak building instructions

See merge request alatiera/Hammond!13
2018-01-25 19:42:45 +00:00
Gabriele Musco 7a0ae6ee14 fixed capital H in Hammond 2018-01-25 14:47:58 +00:00
Gabriele Musco f335bedc2a Added Flatpak building instructions 2018-01-25 14:46:11 +00:00
Jordan Petridis b370af35d4 Feed: Add a bunch of tests. 2018-01-24 18:42:33 +02:00
Jordan Petridis d6817aaa1f Parser: pass an rss::ItunesExtension to instead of an rss::Item. 2018-01-24 15:42:08 +02:00
Jordan Petridis 5da002fe6d Use map_err(From::from) instead of ? into Ok() wrapping pattern. 2018-01-24 15:27:36 +02:00
Jordan Petridis 0e16f0acb0 NewEpisode: rename into_episode -> to_episode, change its signature and add unit test. 2018-01-24 14:31:33 +02:00
Jordan Petridis d5a7fa9de8 NewEpisode: Add index method unit test. 2018-01-24 12:11:49 +02:00
Jordan Petridis 9274aba849 NewEpisode: Add update method unit test. 2018-01-24 11:22:03 +02:00
Jordan Petridis 457e43978f Pipeline: Fix a panic if the list of futures was 0. 2018-01-23 19:07:11 +02:00
Jordan Petridis f9096e5fac Pipeline: Print the error right away instead of waiting till all futures complete. 2018-01-23 18:18:56 +02:00
Jordan Petridis e74a2df27f Source: On Http 301 response, update Source.uri . 2018-01-23 16:43:02 +02:00
Jordan Petridis 1c4138c166 Benches: Remove dead stuff. 2018-01-23 13:24:08 +02:00
Jordan Petridis 050fe9c52b Pipeline: offload more stuff to the threadpool. 2018-01-23 11:43:37 +02:00
Jordan Petridis a8c49049dd Fix flatpak permission. 2018-01-23 10:40:14 +02:00
Jordan Petridis 8095919bbe This sort of works? 2018-01-23 10:12:32 +02:00
Jordan Petridis f7f6087d70 NewPodcast: On update unit test assert .id and .source_id fields stay the same. 2018-01-22 21:09:06 +02:00
Jordan Petridis ea75d50053 NewEpisode: Add insert unit test. 2018-01-22 17:25:23 +02:00
Jordan Petridis 44d7a4e222 NewEpisodeMinimal: Add unit test for into_new_episode method. 2018-01-22 15:49:58 +02:00
Jordan Petridis 2d6f34fa4d NewEpisode: Clean up tests a bit. 2018-01-22 15:34:57 +02:00
Jordan Petridis d231cc165f Fix batch indexing. 2018-01-22 14:51:31 +02:00
Jordan Petridis c2d5b5ded0 Flatpak: Remove the notification portal since its not going to be used any time soon. 2018-01-22 12:23:23 +02:00
Jordan Petridis 7bd361fbbb Upgrade dependancies. 2018-01-22 11:47:17 +02:00
Jordan Petridis 66e816fc5f Upgrade Diesel to 1.1 2018-01-22 11:22:01 +02:00
Jordan Petridis 20f36a67a0 Upgrade log and loggerv. 2018-01-21 15:22:13 +02:00
Jordan Petridis eb75e68b72 Fix benchmarks. 2018-01-21 14:52:21 +02:00
Jordan Petridis dc9a28c0c7 Remove xml files. 2018-01-21 14:49:54 +02:00
Jordan Petridis 843feabf25 NewEpisode: Move the parsing tests, delete old xml files. 2018-01-21 13:35:35 +02:00
Jordan Petridis 937bd61d5e NewPodcast: Add unit test for the to_podcast() method. 2018-01-21 12:17:47 +02:00
Jordan Petridis 4ee5888b7f NewPodcast: Rename into_podcast method -> to_podcast since it now does not take owenership. 2018-01-21 11:04:15 +02:00
Jordan Petridis 73eeb1b545 NewPodcast: Add NewPodcast.index unit test. 2018-01-21 10:04:53 +02:00
Jordan Petridis 16bbfb5478 NewPodcast: Add (lackign) NewPodcast.update unit test. 2018-01-21 09:35:48 +02:00
Jordan Petridis 410657c98b NewPodcast: Remove the boilerplate on the unit tests. 2018-01-21 08:57:50 +02:00
Jordan Petridis 1625f773c2 hammond-data: Fix PartialEq implementations, add NewPodcast.insert unit test. 2018-01-21 08:39:50 +02:00
Jordan Petridis da8c3a7827 Implement PartialEq accross Insertable and Queryable diesel models. 2018-01-20 19:09:25 +02:00
Jordan Petridis 6c9e0984a5 Migrate podcast tests to use the internet archive snapshots. 2018-01-20 18:02:07 +02:00
Jordan Petridis df17a10014 Source: refactor into_feed test. 2018-01-20 16:43:32 +02:00
Jordan Petridis ef52a026bc Feed: Add Builder pattern. 2018-01-20 08:46:05 +02:00
Jordan Petridis b3c4de320b Remove dead dependancies. 2018-01-20 07:41:09 +02:00
Jordan Petridis 7d9781052f hammond-data: Rework the modules privacy. 2018-01-20 07:28:30 +02:00
Jordan Petridis 855b1517a7 Feed: Removee unimplemented! hack to guard against my future stupidity. 2018-01-19 19:20:21 +02:00
Jordan Petridis 1606ceaadb Source: split response_to_channel into more futures. 2018-01-19 16:29:50 +02:00
Jordan Petridis 46b6c0d27e hammond-data: Remove leftover synchronous code. 2018-01-19 15:41:37 +02:00
Jordan Petridis b92ba7be76 Switch from libtest to criterion.rs for benchmarks. 2018-01-19 14:32:43 +02:00
Jordan Petridis 44dbb06dcc Add benchmarks for the async path. 2018-01-19 13:15:21 +02:00
Jordan Petridis e6b0cfccb5 Apply clippy suggestions. 2018-01-19 10:32:25 +02:00
Jordan Petridis 5c5faafc72 Source: Move around code and change some methods signature.
request_constructor is now a Source method.
update_etags now takes &mut self instead of mut self.
update_etags now uses source setters methods instead of raw fields.
changed the naming of into_feed to to_feed according to rust convention.
2018-01-19 07:51:55 +02:00
Jordan Petridis 9dc555cad7 Feed: Add an index_async implementation. 2018-01-18 18:57:44 +02:00
Jordan Petridis 1394366f91 hammond-data: Delete some dead code. 2018-01-18 17:04:57 +02:00
Jordan Petridis 6abf2535b0 hammond_data: Make index a trait that requires Insert + Update. 2018-01-18 16:33:07 +02:00
Jordan Petridis 1c96288178 hammond_data: Add more benchmark cases. 2018-01-18 15:50:48 +02:00
Jordan Petridis 098c5755b0 hammond-data: Do batch indexing of new_episodes. 2018-01-18 13:47:40 +02:00
Jordan Petridis 93372a30d0 1.21 Gigawatts. Remove non-future indexing loop.
Tried to have a seperate futures loop but it's too confusign having
too write a Trait2, functon2, etc version of everything and keep it together.

Futures are functional sort of, so the synchronous versioun can be removed.
It still needs a ton of work though to be ready, or even get near the perf
of of the sync+rayon version.
2018-01-18 11:38:24 +02:00
Jordan Petridis 5d88998180 hammond_data: Add an EpisodeMinimal Diesel model. 2018-01-17 10:21:16 +02:00
Jordan Petridis 574cfae5c6 rustfmt: enable reorder imports. 2018-01-17 08:57:02 +02:00
Jordan Petridis e707087e72 hammond_data: Split models::queriables into multiple modules. 2018-01-17 08:45:14 +02:00
Jordan Petridis 3a6f6d8931 hammond_data: Split models::insertables into multiple modules. 2018-01-17 08:27:39 +02:00
Jordan Petridis 3766db2b14 hammond_data: Move parse_podcast into a NewEpisode method. 2018-01-17 07:31:24 +02:00
Jordan Petridis 8174fe0bac hammond_data: Move parse_episode from parser into a NewEpisode method. 2018-01-17 07:16:59 +02:00
Jordan Petridis bd9844f012 Dbquerries: Use map_err() instead of ? into Ok() pattern. 2018-01-17 07:13:34 +02:00
Jordan Petridis f64779f70a Parser: Add NewEpisodeMinimal struct.
Parsing whole episodes can be expensive and we only need
a subset to determine if it should be indexed/updated or ignored.
2018-01-16 18:33:32 +02:00
Jordan Petridis 978e5a61f6 Dbquerries: add EXIST querries for podcast and episode. 2018-01-16 17:26:41 +02:00
Jordan Petridis fa03c20b00 Feed: add parse_channel_items_future method. 2018-01-16 14:52:20 +02:00
Jordan Petridis bf4f655ed2 Pipeline: remove submodule hack. 2018-01-16 14:37:51 +02:00
Jordan Petridis b3460b15a2 hammond_data::Feed: add parse_podcast_future method. 2018-01-16 13:44:33 +02:00
Jordan Petridis 7f78e87551 hammond_data::Feed: Remove unused parts. 2018-01-16 12:35:08 +02:00
Jordan Petridis 3358fcd0b3 hammond_data::Feed: general cleanup of no longer needed stuff. 2018-01-15 11:03:40 +02:00
Jordan Petridis 10345ffda7 Pipeline: add matching on status code. 2018-01-15 10:16:01 +02:00
Jordan Petridis 2f7a22355f Minor cleanup of the mess. 2018-01-14 08:27:50 +02:00
Jordan Petridis e63a366fdc I don't know why the benchmarks keep cycling. 2018-01-14 06:48:42 +02:00
Jordan Petridis 1031315cdd Getting somewhere? 2018-01-14 06:37:47 +02:00
Tobias Bernard d60dbfbd6b Fix some typos in README.md 2018-01-13 15:16:22 +00:00
Jordan Petridis a5fd79e220 I hate futures error handling. 2018-01-13 14:47:38 +02:00
Jordan Petridis e162f8fd3f Non-working yet benchmark. 2018-01-13 08:31:16 +02:00
Jordan Petridis e4d77a6ba4 Cleanup the id() method mess of the diesel models. 2018-01-13 07:46:56 +02:00
Jordan Petridis 6517956987 This works somehow... 2018-01-13 07:09:59 +02:00
Jordan Petridis ee9cede921 hammond_data: Remove Source dependancy from Feed struct. 2018-01-13 06:08:51 +02:00
Jordan Petridis 1dd25f91fd Minor cleanup 2018-01-13 05:47:23 +02:00
Jordan Petridis 771b7b3804 Something Something futures. 2018-01-13 01:28:02 +02:00
Jordan Petridis 8e367b7e86 Dont ask me how this is working. 2018-01-12 08:50:16 +02:00
Jordan Petridis e20b96e061 Update CHANGELOG.md 2018-01-12 03:36:09 +02:00
Jordan Petridis 58854c2ebd Update Screenshots and README.md 2018-01-12 03:28:02 +02:00
Jordan Petridis 01fe5c4730 Revert to using light theme. 2018-01-12 01:48:52 +02:00
Jordan Petridis e192cf6618 Headerbar: Use application action channel instead of GAction. 2018-01-12 01:41:36 +02:00
Jordan Petridis 88a4c2d9f0 gitlab-ci: Disable nightly builds and clippy. 2018-01-12 00:33:58 +02:00
Jordan Petridis 95ff3715a3 Migrate StackSwitch actions to use the AppAction channel. 2018-01-11 01:14:54 +02:00
Jordan Petridis 8a90de3c0e Implement download cancel action. #24 2018-01-10 09:43:38 +02:00
Jordan Petridis 77f005caab Update Contributing.md 2018-01-10 09:04:24 +02:00
Jordan Petridis a63a6e168c Added BrokenFeed Issue template. 2018-01-10 08:27:13 +02:00
Jordan Petridis 3899e8ed39 Add GPL notices to the glade ui files. 2018-01-10 02:59:47 +02:00
Jordan Petridis fe2f8617a2 Merge branch '33-downloader-re-work' into 'master'
Resolve "Downloader Re-work"

Closes #33

See merge request alatiera/Hammond!12
2018-01-10 00:56:18 +00:00
Jordan Petridis 7f8544373f EpisodesView: Show 50 episodes instead of 100. 2018-01-10 02:14:49 +02:00
Jordan Petridis 955845110b Merge branch 'master' into 33-downloader-re-work 2018-01-09 12:06:38 +02:00
Jordan Petridis d4e3bf696b EpisodeWidget: Fix updating. 2018-01-09 10:52:52 +02:00
Jordan Petridis 0ba5e14d7f EpisodeWidget: Only update if it's visible. 2018-01-09 10:10:54 +02:00
Jordan Petridis ea70addbc6 Removed some unwrap()s. 2018-01-09 09:21:07 +02:00
Jordan Petridis 87a259e1a4 Minor cleanup. 2018-01-09 07:21:38 +02:00
Jordan Petridis 68d7c621d3 EpisodeWidget: Update the local_size label. 2018-01-09 06:15:41 +02:00
Jordan Petridis c54f29e82a EpisodeWidget: Modulate callbacks. 2018-01-09 04:20:38 +02:00
Jordan Petridis 9dafb0ae9e EpisodeWidget: Add local_size label that shows the amount of bytes downloaded. 2018-01-09 03:58:13 +02:00
Jordan Petridis 193117f579 Downloader: Update the progress index of downloads. 2018-01-08 12:29:47 +02:00
Jordan Petridis eef83fc98c EpisodeWidget: Implement API to update the progress bar. 2018-01-08 05:11:37 +02:00
Jordan Petridis 345d4b3865 Check more http status codes when parsing feeds. 2018-01-08 02:41:37 +02:00
Jordan Petridis 81b1ec810c Apply clippy suggestions. 2018-01-07 08:36:02 +02:00
Jordan Petridis 074284d286 Move unsub show logic and get_download_folder func to hammond-data::utils. 2018-01-07 08:26:38 +02:00
Jordan Petridis e290ae223e Pixbuf cache: use rwlock where possible. 2018-01-07 07:49:16 +02:00
Jordan Petridis 5c333291e1 Move temporary downloads to XDG_CACHE/Downloads. 2018-01-07 07:46:11 +02:00
Jordan Petridis 6bd391d89e I hate global mutable state.. 2018-01-07 05:51:29 +02:00
Jordan Petridis 4a6a9517f1 ShowStack: Copy the scrollbar position only if both widget represent the same podcast. 2018-01-06 05:38:31 +02:00
Jordan Petridis e961d5f8b0 Use lazystatic to cache the current chrono date. This will backfire on every new year's eve. 2018-01-06 03:49:26 +02:00
Jordan Petridis 5fed283ff4 EpisodeWidget: Hide duration label if its equal to 0. 2018-01-06 03:18:28 +02:00
Jordan Petridis bb5c25d87f Write unit tests for the itunes_duration_extension parser. 2018-01-06 03:09:24 +02:00
Jordan Petridis da459707be Wire up the callbacks to the client. 2018-01-05 23:57:48 +02:00
Jordan Petridis 13ba2762ad Move the download manager to the gtk crate. 2018-01-05 22:02:06 +02:00
Jordan Petridis e9dd297bf3 Merge branch 'master' into 33-downloader-re-work 2018-01-05 21:43:41 +02:00
Jordan Petridis eafad35860 Upgrade diesel to 1.0.0 and update rest dependancies. 2018-01-05 19:57:11 +02:00
Jordan Petridis d8d4b08252 EpisodeWidget: Trim date label to avoid date/title miss alignment on single digit dates. 2018-01-05 18:46:22 +02:00
Jordan Petridis 65a2d666a3 ShowWidget: Upon unsubing spawn IO heavy tasks into a seperate thread. 2018-01-05 00:35:18 +02:00
Jordan Petridis 299d2e8db1 ShowsView: Retain vertical scrolling adjustment upon view refresh. 2018-01-05 00:09:09 +02:00
Jordan Petridis 8ad5bf6f09 ShowWidget: Retain vertical scrolling adjustment upon view refresh. 2018-01-04 23:43:31 +02:00
Jordan Petridis 112b61d39e EpisodesView: Retain vertical scrolling adjustment upon view refresh. 2018-01-04 23:19:15 +02:00
Jordan Petridis 9d82b0edda EpisodeWidget ui tweaks. 2018-01-04 17:27:55 +02:00
Jordan Petridis 29837ad39a Default to using Arc instead of Rc with composite structs of GtkWidgets. 2018-01-04 17:05:05 +02:00
Jordan Petridis 750abb519b GtkApplication: Change the action channel polling interval. 2018-01-04 16:42:17 +02:00
Jordan Petridis f873278a96 hammond_gtk: Move the gtk callback to an Application channel action. 2018-01-04 16:16:34 +02:00
Jordan Petridis f30c645596 hammond_gtk: Refactor refresh_feed func to use the Application channel. 2018-01-04 16:05:42 +02:00
Jordan Petridis 67bc3e5225 Minor cleanup. 2018-01-03 08:12:55 +02:00
Jordan Petridis c33b493dcd Migrate Headerbar transitions into Channel actions. 2018-01-03 08:02:06 +02:00
Jordan Petridis 2633161c67 GtkApplication: Remove some SimpleActions. 2018-01-03 06:57:15 +02:00
Jordan Petridis 33cd6e69ff EpisodeWidget: Migrate to use a Channel Action instead of simple Action. 2018-01-03 06:53:19 +02:00
Jordan Petridis 84da6aac8c ShowWidget: Replace the unsub simple action with a Channel Action. 2018-01-03 06:23:12 +02:00
Jordan Petridis c8537e9474 GtkApplication: Start to refactor to into a App-channel structure. 2018-01-03 05:58:50 +02:00
Jordan Petridis a0476fedec ShowWidget: Split init into smaller functions and add comments. 2018-01-03 03:33:33 +02:00
Jordan Petridis f54ae2f1d5 EpisodeWidget: Code cleanup and splitting. 2018-01-03 03:02:46 +02:00
Jordan Petridis 2d71a99121 Change the auto-updater and checkup startup scheduling. 2018-01-02 20:47:49 +02:00
Jordan Petridis c67c6e463e EpisodeWidget: Its still a mess, but a bit cleaner. 2018-01-02 08:19:24 +02:00
Jordan Petridis c61d322569 EpisodeWidget: Implement shared download state. 2018-01-02 08:00:38 +02:00
Jordan Petridis 37e9b6fbf0 'How hard could it be' 2018-01-02 07:23:53 +02:00
Jordan Petridis 15b3eb115c ShowWidget: Add tooltip to the website button. 2018-01-01 19:46:27 +02:00
Jordan Petridis b32f448957 When downloading an episode, set it's title to rowid instead of it's title. 2018-01-01 16:36:15 +02:00
Jordan Petridis e42353e03f cargo update 2018-01-01 00:46:31 +02:00
Jordan Petridis a3c204a02d Headerbar: Fix add popup. 2018-01-01 00:42:14 +02:00
Jordan Petridis a3cd76dbf8 hammond-downloader: Use glob Insead of hardcoded file extensions. 2017-12-31 03:59:48 +02:00
Jordan Petridis 785a5f80c6 hammond-data::utils: Small and minor refactor. 2017-12-31 01:42:28 +02:00
Jordan Petridis e727734443 DB Schema: Remove episode.published_date column. 2017-12-30 22:23:10 +02:00
Jordan Petridis 963ff212ad hammond-gtk: Add some tooltips. Closes #13. 2017-12-30 21:56:44 +02:00
Jordan Petridis 3c84d889fd Headerbar: Do more stuff through glade, cut some code. 2017-12-30 21:19:29 +02:00
Jordan Petridis 5730e71e2a Clear TODO and FIXME that are no longer releavant. 2017-12-30 20:54:23 +02:00
Jordan Petridis 4894683924 hammond-gtk: Set the default theme to the dark variant for now. 2017-12-30 18:12:53 +02:00
Jordan Petridis ce08d49107 Headerbar: Added update indication. 2017-12-29 22:58:20 +02:00
Jordan Petridis bcc089bd82 ShowWidget: Migrate unsub button to use the GAction instead. 2017-12-29 20:33:47 +02:00
Jordan Petridis 036292284d hammond-data: Do not clean source url's cause some feeds use queries as identifiers for some reason. 2017-12-29 20:21:36 +02:00
Jordan Petridis ac286888bd hammond-gtk: Add more GActions and wire the EpisodeWidget. 2017-12-29 20:06:04 +02:00
Jordan Petridis fdd63afdfe hammond-gtk: Create Application wide actions. 2017-12-29 16:05:10 +02:00
Jordan Petridis ca06a16bd9 Closes #2. Kudo to @jwykeham for the fix!. 2017-12-29 02:45:02 +02:00
Jordan Petridis 55b6fccefd Make it explicit to run some tests. 2017-12-28 23:46:09 +02:00
Jordan Petridis 1d78ab7c42 Cargo upgrade. 2017-12-28 23:19:36 +02:00
Jordan Petridis 8a7d6d9f1b Add an empty CHANGELOG that follows keepachangelog.com format. Closes #23. 2017-12-28 22:41:22 +02:00
Jordan Petridis 287620d6cd hammond-gtk: Added some getters and removed some public fields. 2017-12-28 19:16:51 +02:00
Jordan Petridis 5942e47f2a hammond-gtk: Split gtk::Application into its own module. 2017-12-28 16:59:05 +02:00
Jordan Petridis a9d1084e05 gitlab-ci: Remove ci cache until gitlab-runner 10.4 release 2017-12-27 18:14:50 +02:00
Jordan Petridis 7d7b09ff0f hammond-data::parser: Prefer itunes_summary over channel.description if it exists. Finishes #20. 2017-12-27 17:59:40 +02:00
Jordan Petridis 1f0a2b5c6c hammond-data::parser: Prefer itunes_summary over item.description if it exists. Finishes #20. 2017-12-27 17:40:31 +02:00
Jordan Petridis ada73a616f EpisodeWidget: Make cancel button insensitive. 2017-12-27 15:13:09 +02:00
Jordan Petridis 0a7825dfff EpisodeWidget: Remove delete button and hide cancel button till refactor. 2017-12-26 21:36:18 +02:00
Jordan Petridis bfb74c4dba hammond-data: Index_loop does not wait for GET request to finish now. 2017-12-26 18:18:48 +02:00
Jordan Petridis 41abeeb2e3 Merge branch 'hamburger-menu' into 'master'
Hamburger menu

See merge request alatiera/Hammond!11
2017-12-26 14:25:47 +00:00
Jordan Petridis e99ab58b83 Headerbar: Add margins to the hamburger menu. 2017-12-26 15:46:25 +02:00
Jordan Petridis 933fec55a5 hammond-data::parser: Refactor Episode tests. 2017-12-26 13:40:52 +02:00
Jordan Petridis fde4bedce6 hammond-data::parser: Refactor Podcast tests. 2017-12-25 23:29:42 +02:00
Jordan Petridis d5b9379732 Headerbar: Remove some option from the menu. 2017-12-25 22:26:38 +02:00
Jordan Petridis 0a47b91913 Headerbar: Wire menu refresh button. 2017-12-25 22:03:37 +02:00
Jordan Petridis 9f51f4826f Headerbar: Added menu popup. 2017-12-25 21:54:57 +02:00
Jordan Petridis eee491f17c appdata.xml update. 2017-12-25 20:22:53 +02:00
Jordan Petridis 8cccbe928d hammond-gtk: Change application title. Addresses #28. 2017-12-24 20:33:18 +02:00
Jordan Petridis ee03d92fdb Merge branch 'master' into 'master'
[ui] add round corners to listboxes

See merge request alatiera/Hammond!10
2017-12-24 13:02:37 +00:00
Julian Sparber f8b9bdb696 [ui] add round corners to listboxes 2017-12-24 13:16:37 +01:00
Jordan Petridis ef705f9a2a Merge branch '27-episodesview' into 'master'
Resolve "EpisodesView"

Closes #27, #21, #22, #19, and #18

See merge request alatiera/Hammond!8
2017-12-23 15:05:17 +00:00
Jordan Petridis e7185d2cd3 Visual tweaks, and other minor stuff.
Added margins bottom margins on EpisodesView and ShowWidget.
Added some comment's to the code.
Silenced needless_pass_by_value clippy warning.
Updated TODO.md.
2017-12-23 16:56:03 +02:00
Jordan Petridis 30970c8dfb gitlab-ci: Force regeneration of gresources. 2017-12-23 16:35:23 +02:00
Jordan Petridis 2d6f02c407 EpisodeWidget: Do not display size if it's 0 bytes. 2017-12-23 15:44:28 +02:00
Jordan Petridis 37dbfff766 gitlab-ci: Do not cache rustfmt and clippy stuff. 2017-12-23 14:22:02 +02:00
Jordan Petridis f9d17afad3 hammond-gtk: Remove app menu. 2017-12-23 10:39:20 +02:00
Jordan Petridis 5234ecb2a7 Merge branch '27-episodesview' into '27-episodesview'
[ui] add custom style and [fix] draw List separators with css

See merge request alatiera/Hammond!9
2017-12-22 21:41:18 +00:00
Julian Sparber 32c60e7ffa merge 2017-12-22 22:10:40 +01:00
Julian Sparber 95c290df50 [ui] add custom style and [fix] draw List separators with css 2017-12-22 21:54:28 +01:00
Jordan Petridis 832495bece EpisodesView: Remove Year category. 2017-12-22 22:14:08 +02:00
Jordan Petridis ae6a97d725 EpisodesViewWidget: Add margins from the cover. 2017-12-22 21:21:39 +02:00
Jordan Petridis fac048a24d Small Visual tweaks all over the client. 2017-12-22 20:47:37 +02:00
Jordan Petridis 7aebb4d50d gitlab-ci: Add caching to the ci config. 2017-12-22 18:49:24 +02:00
Jordan Petridis 31b19dd88e Just things apple force you to do. 2017-12-22 18:28:18 +02:00
Jordan Petridis 0129efb02e EpisodeWidget: Display episode's duration. Closes #21. 2017-12-22 17:49:15 +02:00
Jordan Petridis 4512790f2d hammond-data: Add duration column to the episode table. 2017-12-22 17:30:07 +02:00
Jordan Petridis 5541b18a6a hammond-data::parser: Add itunesext_duration parser. 2017-12-22 16:55:45 +02:00
Jordan Petridis b820ee4db7 hammond-data::parser : Do not clean image urls cause feeds put redirects for uri for some reason. 2017-12-22 14:51:57 +02:00
Jordan Petridis 8513ba3644 EpisodesView: Use chrono to categorize widgets intead of epoch calculations. 2017-12-22 14:29:41 +02:00
Jordan Petridis 2ad0539a82 Downloader: Overwrite episode.lenght column upon download finish with the actual file size. Closes #22. 2017-12-21 22:15:17 +02:00
Jordan Petridis b7e160f735 EpisodeWidget: Change date representation based on it's year. Closes #19. 2017-12-21 21:50:50 +02:00
Jordan Petridis 4a033e6d89 EpisodeWidget: Remove the decimal places in the size label. 2017-12-21 21:37:51 +02:00
Jordan Petridis 378b8609aa EpisodeWidget: Change the byte unit based on the size. Closes #18. 2017-12-21 21:31:25 +02:00
Jordan Petridis c8310b1eb9 EpisodesView: Bump the labels size a bit. 2017-12-21 20:01:24 +02:00
Jordan Petridis 13b2043b76 EpisodesStack: Fix view selection upon creation. 2017-12-21 19:10:37 +02:00
Jordan Petridis e416bca963 Implemented a pixbuf cache mechanism.
Since gdk_pixbuf::Pixbuf is refference counted and every episode,
use the cover of the Podcast Feed/Show, We can only create a Pixbuf
cover per show and pass around the Rc pointer.

GObjects do not implement Send trait, so SendCell is a way around that.
Also lazy_static requires Sync trait, so that's what the mutexes are.
2017-12-21 17:36:07 +02:00
Jordan Petridis 74a6e5814a ShowWidget: Update EpisodesView when unsub button is activated. 2017-12-21 15:14:29 +02:00
Jordan Petridis 994ea5af22 EpisodesView: Remove unused label. 2017-12-20 22:08:07 +02:00
Jordan Petridis 336846f6dd EpisodesStack: Implemnt the transition between empty and populated. 2017-12-20 21:25:00 +02:00
Jordan Petridis db59bed69d hammond-data::utils: Modified EpisodeCleaner Diesel model to also be used with played_cleaner. 2017-12-20 19:18:20 +02:00
Jordan Petridis 3c24b9f9d9 hammond-data::utils: Added new Diesel model for the download checker. 2017-12-20 19:00:14 +02:00
Jordan Petridis 2e06205eda Define a new Diesel Model for PodcastCover querries.
Define new Diesel Model and impl From<Podcast> trait,
Change the signature of downloader::cache_image function,
Change and merge hammond-gtk::utils::get_pixbuf_from_path functions.
2017-12-20 18:19:31 +02:00
Jordan Petridis c070fc3032 EpisodesView: Ugly and Hacky Prototype of episode splitting. 2017-12-20 15:12:55 +02:00
Jordan Petridis ace62f7ed6 EpisodesView: Initial draft of episodewidget's splitting into the frames. 2017-12-20 14:49:28 +02:00
Jordan Petridis ccfea527ac gitlab-ci: use --force for clippy and rustfmt isntallation. 2017-12-20 13:25:31 +02:00
Jordan Petridis bda09c032a EpisodeWidget: Update the progrress bar only when download clicked. 2017-12-20 13:18:29 +02:00
Jordan Petridis 632f011db5 EpisodeView: Implement initial view update. 2017-12-20 13:13:32 +02:00
Jordan Petridis 914cad72f5 EpisodesView: Custom Diesel model not really necessary. 2017-12-19 20:45:40 +02:00
Jordan Petridis 895591f628 EpisodesView: Use show cover image. 2017-12-19 13:19:38 +02:00
Jordan Petridis ad9a932143 Fix diesel query. 2017-12-19 12:56:16 +02:00
Jordan Petridis f602e8c36d EpisodesView: Create a DieselModel for EpisodeViewWidget. 2017-12-19 11:04:44 +02:00
Jordan Petridis 5220eaceee EpisodesView: Add an empty cover to the widget. 2017-12-18 21:35:51 +02:00
Jordan Petridis 61bd7893c7 EpisodeWidget: Remove Podcast depndancy from new() method. 2017-12-18 19:10:50 +02:00
Jordan Petridis 32cd24fc7b EpisodesView: Added glade files and initial yak shaving. 2017-12-18 18:20:40 +02:00
Jordan Petridis 945600b20d Merge branch 'master' into 'master'
Set dim-label for secondary labels of EpisodeWidget in Glade.

See merge request alatiera/Hammond!7
2017-12-17 14:30:10 +00:00
Jordan Petridis e04de5a576 ShowsView: Move title to a tooltip, fix gresource, center flowbox halign. 2017-12-17 15:57:06 +02:00
Jordan Petridis 0826514cbc ShowsView: Remove new_episodes indicator. 2017-12-17 15:34:34 +02:00
Jordan Petridis 3cbda5979d Merge branch 'show_widget_redesign' into 'master'
Show widget redesign

See merge request alatiera/Hammond!6
2017-12-17 13:05:25 +00:00
Constantin Nickel 681b91d368 Set dim-label for secondary labels of EpisodeWidget in Glade. 2017-12-17 13:17:04 +01:00
Jordan Petridis 2eb58eae00 Headerbar: Remove refresh button. 2017-12-17 13:57:08 +02:00
Jordan Petridis 4c8c1f0013 Add update option to the app menu. 2017-12-17 13:52:00 +02:00
Jordan Petridis 35009e2574 Added an auto-updater that runs each hour. 2017-12-17 13:44:00 +02:00
Jordan Petridis 75fe0f8ff5 Use Default trait for creating Widget's that don't need arguments. 2017-12-17 13:08:51 +02:00
Jordan Petridis 440badf1eb ShowWidget: Strip html tags from the description. 2017-12-16 20:45:15 +02:00
Jordan Petridis f7af05a650 EpisodeWidget: Dim/Grey out widget if episode is played. 2017-12-16 20:30:25 +02:00
Jordan Petridis aca35c0513 ShowWidget: Improve? description handling. 2017-12-16 19:20:40 +02:00
Jordan Petridis f8ae2dcffe Update rustfmt.toml and remove rustfmt warnings. 2017-12-16 18:22:35 +02:00
Jordan Petridis 0653460fb2 EpisodeWidget: prevent title label overflow.
Set label's maximu character width to prevent it and elipsize instead.
2017-12-16 14:58:16 +02:00
Jordan Petridis dbe08f7deb EpisodeListBox: Improve the separator workaround. 2017-12-16 14:17:27 +02:00
Jordan Petridis 5989f4f541 ShowWidget: Set widget width to 600. 2017-12-15 21:15:20 +02:00
Jordan Petridis 4375c7f4ce EpisodeWidget: Added a separator in each widget.
This is a work around since list_box_set_header_func is not yet
available in the gtk-rs bindings.
2017-12-15 21:06:57 +02:00
Jordan Petridis 3c90e98d43 ShowWidget: Make settings button a GtkMenuButton. 2017-12-15 21:06:43 +02:00
Jordan Petridis b1af25e4c0 ShowWidget: Wire the website button to open podcast homepage. 2017-12-15 21:06:16 +02:00
Jordan Petridis 6614818418 Use Gtk::Frame to limit ShowWidget's size. 2017-12-15 19:55:13 +02:00
Jordan Petridis c74153f56a Initial implementation of the new ShowWidget. 2017-12-15 18:42:49 +02:00
Jordan Petridis 4b0a6ea0f7 Add Show title in the Headerbar when switching to ShowWidget. 2017-12-15 13:45:08 +02:00
Jordan Petridis 6284c8828b Merge branch 'episode_widget_redisgn' into 'master'
Episode widget redisgn

See merge request alatiera/Hammond!5
2017-12-15 11:30:15 +00:00
Jordan Petridis 0c1e759a45 Dim out secondary label of EpisodeWidget. 2017-12-15 12:27:30 +02:00
Jordan Petridis 0ac78fcff1 Added date label into EpisodeWidget. 2017-12-14 18:03:37 +02:00
Jordan Petridis e3b540170a Add file size indication based on rss item length. 2017-12-14 15:32:21 +02:00
Jordan Petridis a7208b0c61 Set EpisodeWidget button valignment to center instead of fill. 2017-12-14 14:46:41 +02:00
Jordan Petridis 9d07ba24b8 HeaderBar Refactor.
Removed the Headerbar stack.
Removed the ShowTitle.
Fixed StackSwitcher centering.
Set the Headerbar button valignment to center instead of fill.
2017-12-14 14:27:51 +02:00
Jordan Petridis afdb79b712 Set the progress bar into activity mode. 2017-12-14 13:38:18 +02:00
Jordan Petridis 8fe6b526a5 Add a new Diesel Model for the EpisodeWidget. 2017-12-14 12:01:35 +02:00
Jordan Petridis ebbebf7735 Initial implementation of the new EpisodeWidget. 2017-12-14 10:57:29 +02:00
Jordan Petridis fe136c8dbe use user-trash-symbolic instead of edit-delete-symbolic icon. 2017-12-14 08:42:52 +02:00
Jordan Petridis da57a966d5 Merge branch 'stack_redesign' into 'master'
Stack redesign

See merge request alatiera/Hammond!4
2017-12-13 16:28:06 +00:00
Jordan Petridis f0de6bfb1b Use symbolic icons instead. 2017-12-13 17:58:55 +02:00
Jordan Petridis d74aa9c625 Wire Headerbar back button and change initialiazation order. 2017-12-13 16:36:40 +02:00
Jordan Petridis 0bb0035f3c Rework of the Headerbar.
Added a stack to switch between a normal and a BackButton view.
Also added a centered stack switcher and removed the Home button.
Backbutton is not wired yet.
2017-12-13 13:53:15 +02:00
Jordan Petridis 0a52f87f3a Rename glade files. 2017-12-13 12:21:34 +02:00
Jordan Petridis 82a59d80dd Update README.md and CONTRIBUTING.md 2017-12-13 11:53:56 +02:00
Jordan Petridis 5defb5867a Move and rename stuff. 2017-12-13 11:35:17 +02:00
Jordan Petridis d5d55d4ef3 Update dependancies. 2017-12-12 16:51:24 +02:00
Jordan Petridis 01310ee7fa Working non-state machine stack implementation.
Removed the stack state-machines. It was confusing trying to both
implement statemachines and re-design the stack architecture at the same time.
2017-12-12 16:01:19 +02:00
Jordan Petridis 211b36dfa3 Fix showmachine updating. 2017-12-11 16:14:43 +02:00
Jordan Petridis ddb195db3c Wired up refresh button. 2017-12-11 13:51:16 +02:00
Jordan Petridis fa33138d66 Start switching to the new stack architecture. 2017-12-11 11:53:29 +02:00
Jordan Petridis ad7f5013f3 Extend ShowsMachine functionality. 2017-12-11 11:15:54 +02:00
Jordan Petridis e9b6e26f08 yet another attempt at implementing a stack state machine. 2017-12-10 23:23:09 +02:00
Jordan Petridis 2461dca94d Remove home button. 2017-12-10 19:15:32 +02:00
James Wykeham-Martin 6f635ac9fa Updated client.get request headers 2017-12-10 07:58:41 +00:00
Jordan Petridis e127941d8a Initial stackswitcher addition into the headerbar. 2017-12-10 09:53:05 +02:00
Jordan Petridis 851be1fcfb Schema hotfix. 2017-12-10 06:47:10 +02:00
Jordan Petridis d8281c70a5 Better naming of a dbquery function. 2017-12-10 06:11:03 +02:00
Jordan Petridis bc57c33491 Change episode table schema. 2017-12-09 17:38:46 +02:00
Jordan Petridis 999a2a1fc1 Strip whitespace during parsing. 2017-12-09 10:22:09 +02:00
Jordan Petridis 8b4684679b Merge branch 'client-rework' into 'master'
Client rework

See merge request alatiera/Hammond!3
2017-12-09 04:25:39 +00:00
Jordan Petridis b528c48e3b Run rustfmt. 2017-12-09 05:57:16 +02:00
Jordan Petridis 5b19274e6a Ignore the stack state machine until TryInto trait is stabilized. 2017-12-08 10:00:21 +02:00
Jordan Petridis 48e61c6377 Added small a utility that eats extra whitespace. 2017-12-07 18:31:45 +02:00
Jordan Petridis 1266c6e971 Switch to a Generic Struct instead of an Enum. 2017-12-07 15:24:15 +02:00
Jordan Petridis 41b0a36b81 Implement Into ContentState types. 2017-12-07 11:14:12 +02:00
Jordan Petridis 6801d0b1d1 Move replace_*view functions inot Content methods. 2017-12-07 10:51:16 +02:00
Jordan Petridis 973212254c My code is horrible, don't look at it. 2017-12-07 09:43:47 +02:00
Jordan Petridis 2b2f44b10e Attempt n42 of implementing a StateMachine. 2017-12-07 08:50:20 +02:00
Jordan Petridis 9431fb827f Bug fix. 2017-12-07 08:42:36 +02:00
Jordan Petridis d7af108833 WIP markup to html parser func. 2017-12-07 06:41:10 +02:00
Jordan Petridis c07d240532 Sanitize html during feed parsing. 2017-12-07 06:38:31 +02:00
Jordan Petridis 05e056481f Take advantage of <Into> trait from the generated builders. 2017-12-05 09:28:13 +02:00
Jordan Petridis 8e6cc4d2f2 Remove custom builders in favor of derive_builder crate. 2017-12-05 09:10:02 +02:00
Jordan Petridis 35f7464540 Use a gitlab-ci template instead of script duplication. 2017-12-05 07:47:07 +02:00
Jordan Petridis c8c0699d9c gitlab-ci tweak. 2017-12-05 07:37:43 +02:00
Jordan Petridis 0835b5ed4f flatpak manifest update 2017-12-05 06:22:59 +02:00
Jordan Petridis ca2ad9b2da rustfmt fix?. 2017-12-04 14:32:38 +02:00
Jordan Petridis 276169e43d Minor cleanup of nested match/if let statements into using and_then(). 2017-12-04 14:32:16 +02:00
Jordan Petridis ad1d9a155e Readme typo fix. 2017-12-02 09:03:21 +02:00
Jordan Petridis 8bd48a09a6 Groupd stack manipulation functions into content module. 2017-12-02 08:58:28 +02:00
Jordan Petridis 7727bc5ec3 Remove unnecessary Arc. 2017-12-02 01:46:37 +02:00
Jordan Petridis 74a1aae168 Upgraded diesel to 0.99 and switched from the git master into crated.io.
Also did a cargo upgrade of all the dependancies.
Notable upgrades are r2d2, lazystatic 1.0, loggerv 0.6 and rss 1.2
2017-12-02 01:39:28 +02:00
Jordan Petridis 9312366f62 Enable more lints. 2017-12-02 00:06:42 +02:00
Jordan Petridis 8d63db047a Temporary removed unused Content.fields. 2017-12-01 07:30:28 +02:00
Jordan Petridis 0359e9dd35 Convert EpisodeWidget into a Composed struct. 2017-12-01 07:13:41 +02:00
Jordan Petridis 91ac2c6445 Minor cleanups. 2017-12-01 06:40:32 +02:00
Jordan Petridis 2d4acc3ba3 Tweaked stack transition effects. 2017-12-01 06:21:48 +02:00
Jordan Petridis fb6fc68d23 Fix empty view. 2017-12-01 06:00:06 +02:00
Jordan Petridis 3fca086d14 Renamed stack children. 2017-12-01 05:32:47 +02:00
Jordan Petridis dcc541d832 Added a PodcastChild struct and groupd some flowbox_child methods. 2017-12-01 04:22:26 +02:00
Jordan Petridis 300fc3467d Remove more stuff. 2017-12-01 03:50:23 +02:00
Jordan Petridis 326ee75cd8 Remove old implementation. 2017-12-01 02:57:55 +02:00
Jordan Petridis b4b2f24c77 Use content struct instead of plain stack. 2017-12-01 01:30:19 +02:00
Jordan Petridis 5414dbdb37 Wire and use the new Header struct instead. 2017-12-01 01:20:49 +02:00
Jordan Petridis dd0034327b Add Content Struct. 2017-12-01 01:12:25 +02:00
Jordan Petridis 774e5b38a0 Add EmptyView Struct. 2017-12-01 01:03:10 +02:00
Jordan Petridis f1f4d10fe3 Add PopulatedView struct. 2017-12-01 01:00:07 +02:00
Jordan Petridis 84c918bbd5 Fix bug where the played button will always be shown by setting default state to not visible. 2017-11-30 21:39:21 +02:00
Jordan Petridis 94879b4256 Move get_pixbuf_from_path into hammond_gtk/src/utils.rs module. 2017-11-30 21:38:21 +02:00
Jordan Petridis bf240a00f4 Make a PodcastWidget Struct type. 2017-11-30 21:19:03 +02:00
Jordan Petridis b32e35c574 Rename podcasts_view module. 2017-11-30 21:10:05 +02:00
Jordan Petridis 1fbcfa47ac Refactor Headerbar into a Wrapped struct type. 2017-11-30 21:02:26 +02:00
167 changed files with 32223 additions and 58119 deletions
+6 -9
View File
@@ -1,13 +1,10 @@
/target/
target/
**/*.rs.bk
Cargo.lock
.vscode
*.ui~
*.gresource
_build
resources.gresource
_build/
build/
vendor/
.flatpak-builder/
flatpak-build/
flatpak-repo/
repo/
Makefile
.criterion/
org.gnome.*.json~
+129 -61
View File
@@ -1,76 +1,144 @@
stages:
# meson uses cargo to do the build
# so it's ok to have the tests first.
- test
# - build
- lint
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential
- apt-get install -yqq --no-install-recommends libgtk-3-dev
# - apt-get install -yqq --no-install-recommends meson
# kcov
# - apt-get install -y libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
- review
variables:
# RUSTFLAGS: "-C link-dead-code"
RUST_BACKTRACE: "FULL"
BUNDLE: "gnome-podcasts-dev.flatpak"
# Currently doesnt work.
# # Build with meson
# build:stable:
# # Stable img
# # https://hub.docker.com/_/rust/
# image: "rust"
# script:
# - rustc --version && cargo --version
# - ./configure --prefix=/usr/local
# - make && sudo make install
.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/
# build:nightly:
# # Nightly
# # https://hub.docker.com/r/rustlang/rust/
# image: "rustlang/rust:nightly"
# script:
# - rustc --version && cargo --version
# - ./configure --prefix=/usr/local
# - make && sudo make install
.cargo_test_template: &cargo_test
stage: test
test:stable:
# Stable img
# https://hub.docker.com/_/rust/
image: "rust"
script:
- rustc --version && cargo --version
- cargo build
- cargo test --verbose -- --test-threads=1
variables:
RUSTFLAGS: "--cfg rayon_unstable"
RUST_BACKTRACE: "FULL"
test:nightly:
# Nightly
# https://hub.docker.com/r/rustlang/rust/
image: "rustlang/rust:nightly"
script:
- rustc --version && cargo --version
- cargo build
- cargo test --verbose -- --test-threads=1
# - cargo bench
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson libgstreamer1.0-dev
- 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 podcasts-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
flatpak:
image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.28
stage: test
script:
- flatpak-builder --stop-at=gnome-podcasts app org.gnome.Podcasts.json
# https://gitlab.gnome.org/World/podcasts/issues/55
# Force regeneration of gresources regardless of artifacts chage
- flatpak-builder --run app org.gnome.Podcasts.json glib-compile-resources --sourcedir=podcasts-gtk/resources/ podcasts-gtk/resources/resources.xml
# Build the flatpak repo
- flatpak-builder --run app org.gnome.Podcasts.json meson --prefix=/app --libdir=/app/lib _build
- flatpak-builder --run app org.gnome.Podcasts.json ninja -C _build install
- flatpak-builder --finish-only app org.gnome.Podcasts.json
- flatpak build-export repo app
# Create a flatpak bundle
- flatpak build-bundle repo ${BUNDLE} org.gnome.Podcasts
# Run the tests
# - flatpak-builder --run app org.gnome.Podcasts.json cargo test -- --test-threads=1
# - flatpak-builder --run app org.gnome.Podcasts.json cargo test -- --test-threads=1 --ignored
artifacts:
paths:
- $BUNDLE
expire_in: 30 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"
paths:
- .flatpak-builder/cache/
- target/
review:
stage: review
dependencies:
- flatpak
script:
- echo "Generating flatpak deployment"
artifacts:
paths:
- $BUNDLE
expire_in: 30 days
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
on_stop: stop_review
except:
- master@World/podcasts
stop_review:
stage: review
script:
- echo "Stopping flatpak deployment"
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
except:
- master@World/podcasts
# Configure and run rustfmt on nightly
# Exits and builds fails if on bad format
lint:rustfmt:
image: "rustlang/rust:nightly"
script:
- rustc --version && cargo --version
- cargo install rustfmt-nightly
- cargo fmt --all -- --write-mode=diff
rustfmt:
image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:nightly"
stage: lint
script:
- rustc -Vv && cargo -Vv
- cargo fmt --version
- cargo fmt --all -- --check
# Configure and run clippy on nightly
# Only fails on errors atm.
lint:clippy:
image: "rustlang/rust:nightly"
script:
- rustc --version && cargo --version
- cargo install clippy
- cargo clippy --all
.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 podcasts-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo clippy --all
<<: *cargo_cache
+24
View File
@@ -0,0 +1,24 @@
## Invalid RSS Feed Template.
Please provide the source of the xml rss feed.
**Feed URL**
example.com/podcast
**Detailed description of the issue**
Would be helpfull if error messages where included from stderr.
If you are not sure how to do it, feel free to ask and we will walk you through it!
Some common cases might be:
* Feed cannot be added
* Broken Feed Image
* Episode(s) do not download
Steps to reproduce:
1. Open GNOME Podcasts
2. Do an action
3. ...
+34 -12
View File
@@ -1,20 +1,42 @@
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 this link
https://gitlab.gnome.org/GNOME/gnome-apps-nightly/raw/master/nautilus.flatpakref and save
it as a ".flatpakref" file
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:
## Design Tasks
# Current behavior
<!-- Describe the current behavior. -->
* [ ] design tasks
## Development Tasks
# Expected behavior
<!-- Describe the expected behavior. -->
* [ ] development tasks
## QA Tasks
# 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
-->
* [ ] qa (quality assurance) tasks
<!-- Ignore the text under this line. -->
/label ~"Bug"
+41
View File
@@ -0,0 +1,41 @@
# 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
-->
# 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.
-->
# 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
-->
# 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"
+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"
+157
View File
@@ -0,0 +1,157 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added:
### Changed:
### Fixed:
### Removed:
## [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
not being able to access any file.
## [0.3.3] - 2018-05-19
### Added:
- Initial functionality for importing shows from an OPML file was implemented.
- ShowsView now rembmers the vertical alignment of the scrollbar between refreshes. 4d2b64e79d8518454b3677612664cd32044cf837
### Changed:
- Minimum `rustc` version requirment was bumped to `1.26`
- Some animations should be smoother now. 7d598bb1d08b05fd5ab532657acdad967c0afbc3
- InAppNotification now can be used to propagate some erros to the user. 7035fe05c4741b3e7ccce6827f72766226d5fc0a and 118dac5a1ab79c0b4ebe78e88256a4a38b138c04
### Fixed:
- Fixed a of by one bug in the `ShowsView` where the last show was never shown. bd12b09cbc8132fd39a266fd091e24bc6c3c040f
## [0.3.2] - 2018-05-07
### Added:
- Vies now have a new fancy scrolling animation when they are refereshed.
### Changed:
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
[#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/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
- `ShowWidget` layout was changed 9a5cc1595d982f3232ee7595b83b6512ac8f6c88
- `ShowWidget` Description is inside a scrolled window now
### Fixed:
- `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:
- Removed the custom configuration file and replaced instructions to just use meson. 1f1d4af8ba7db8f56435d13a1c191ecff3d4a85b
## [0.3.1] - 2018-03-28
### Added:
- Ability to mark all episodes of a Show as watched.
[#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/podcasts/issues/49)
- Hammond now remembers the window size and position. (Rowan Lewis)
[#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/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/podcasts/issues/44)
### Changed:
- EpisdeWidget has been reimplemented as a compile time state machine.
[!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/podcasts/issues/35)
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
[#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/podcasts/issues/52)
## [0.3.0] - 2018-02-11
- Tobias Bernard Redesigned the whole Gtk+ client.
- Complete re-write of hammond-data and hammond-gtk modules.
- Error handling for all crates was migrated from error-chain to Failure.
- Hammond-data now uses futures to parse feeds.
- Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets.
## [0.2.0] - 2017-11-28
- Database Schema Breaking Changes.
- Added url sanitization. #4.
- Reworked and refactored of the hammond-data API.
- Added some more unit tests
- Documented hammond-data public API.
## [0.1.1] - 2017-11-13
- Added appdata.xml file
## [0.1.0] - 2017-11-13
- Initial Release
+49 -20
View File
@@ -1,30 +1,57 @@
## Contributing
## Contributing to GNOME Podcasts
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.
Thank you for looking in this file!
Please note we have a code of conduct, please follow it in all your interactions with the project.
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 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
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.
Note that we don't do bug tracking in the Github mirror.
If you need to publish a branch, feel free to do it at any
publically-accessible Git hosting service, although gitlab.gnome.org
makes things easier for the maintainers.
## Style
We use rustfmt for code formatting and we enforce it on the gitlab-CI server.
We use [rustfmt](https://github.com/rust-lang-nursery/rustfmt) for code formatting and we enforce it on the gitlab-CI server.
Quick setup
```
cargo install rustfmt-nightly
cargo fmt --all
```
```
cargo install rustfmt-nightly
cargo fmt --all
```
It is recommended to add a pre-commit hook to run cargo test and cargo fmt
```
#!/bin/sh
cargo test -- --test-threads=1 && cargo fmt --all -- --write-mode=diff
```
It is recommended to add a pre-commit hook to run cargo test and `cargo fmt`.
Don't forget to `git add` again after `cargo fmt`.
```
#!/bin/sh
cargo test -- --test-threads=1 && cargo fmt --all -- --write-mode=diff
```
## Running the test suite
Running the tests requires an internet connection and it it will download some files from the [Internet Archive](archive.org)
The test suite sets a temporary sqlite database in the `/tmp` folder.
Due to that it's not possible to run them in parrallel.
In order to run the test suite use the following: `cargo test -- --test-threads=1`
# 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/alatiera/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:
@@ -47,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. ...
@@ -67,8 +94,10 @@ Steps to reproduce:
## Pull Request Process
1. Ensure your code compiles. Run `make` before creating the pull request.
2. If you're adding new API, it must be properly documented.
3. The commit message is formatted as follows:
2. Ensure the test suit passes. Run `cargo test -- --test-threads=1`.
3. Ensure your code is properly formated. Run `cargo fmt --all`.
4. If you're adding new API, it must be properly documented.
5. The commit message is formatted as follows:
```
component: <summary>
@@ -78,7 +107,7 @@ Steps to reproduce:
<link to the bug ticket>
```
4. You may merge the pull request in once you have the sign-off of the maintainers, or if you
6. You may merge the pull request in once you have the sign-off of the maintainers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
## Code of Conduct
Generated
+1819 -723
View File
File diff suppressed because it is too large Load Diff
+3 -8
View File
@@ -1,14 +1,9 @@
[workspace]
members = [
"hammond-data",
"hammond-downloader",
"hammond-gtk"
"podcasts-data",
"podcasts-downloader",
"podcasts-gtk"
]
[profile.release]
debug = false
[patch.crates-io]
diesel = { git = "https://github.com/diesel-rs/diesel.git" }
diesel_infer_schema = { git = "https://github.com/diesel-rs/diesel.git" }
diesel_codegen = { git = "https://github.com/diesel-rs/diesel.git" }
View File
+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.
+97 -71
View File
@@ -1,82 +1,106 @@
# Hammond
## Multithreaded and reliable Gtk+ Podcast client.
This is a prototype of a podcast client written in Rust.
# GNOME Podcasts
[![pipeline status](https://gitlab.gnome.org/alatiera/Hammond/badges/master/pipeline.svg)](https://gitlab.gnome.org/alatiera/Hammond/commits/master)
### A Podcast application for GNOME.
Listen to your favorite podcasts, right from your desktop.
![podcasts_view](./assets/podcasts_view.png)
![podcast_widget](./assets/podcast_widget.png)
![episdes_view](./screenshots/home_view.png)
![shows_view](./screenshots/shows_view.png)
![show_widget](./screenshots/show_widget.png)
## Quick start
GNOME Podcasts can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
## Broken Feeds
Found a feed that does not work in GNOME Podcasts?
Please [open an issue](https://gitlab.gnome.org/World/podcasts/issues/new) 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://irc.gnome.org/#hammond)
Sidenote:
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] or [matrix][matrix].
Note:
There isn't much documentation yet, so you will probably have question about parts of the Code.
## Quick start
The following steps assume you have a working installation of rustc and cargo.
If you dont take a look at [rustup.rs](rustup.rs)
```sh
git clone https://gitlab.gnome.org/alatiera/hammond.git
cd Hammond/
cargo run -p hammond-gtk --release
```
## Install from soure
```sh
git clone https://gitlab.gnome.org/alatiera/hammond.git
cd Hammond/
./configure --prefix=/usr/local
make && sudo make install
```
**Additionall:**
You can run `sudo make uninstall` for removal
And `make clean` to clean up the enviroment after instalation.
### Flatpak
Flatpak instructions... Soon™.
## Building
### Dependancies
### Flatpak
* Rust stable 1.22 or later.
Flatpak is the reccomended way of building and installing GNOME Podcasts.
#### Building a Flatpak
Download the `org.gnome.Podcasts.json` flatpak manifest from this repo.
```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 podcasts org.gnome.Podcasts.json --force-clean
```
To install the resulting flatpak you can do:
```bash
flatpak build-bundle repo gnome-podcasts.flatpak org.gnome.Podcasts
flatpak install --user --bundle gnome-podcasts.flatpak
```
### Building from soure
```sh
git clone https://gitlab.gnome.org/World/podcasts.git
cd gnome-podcasts/
meson --prefix=/usr build
ninja -C build
sudo ninja -C build install
```
#### Dependencies
* Rust stable 1.27 or later along with cargo.
* Gtk+ 3.22 or later
* Gstreamer 1.12 or later
* Meson
* A network connection
**Debian/Ubuntu**:
```sh
apt-get update -yqq
apt-get install -yqq --no-install-recommends build-essential
apt-get install -yqq --no-install-recommends libgtk-3-dev meson
```
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
**Fedora**:
```sh
dnf install -y 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 PR or an Issue to note it.
**Debian/Ubuntu**
```sh
git clone https://gitlab.gnome.org/alatiera/Hammond.git
cd Hammond/
cargo build --all
apt-get install -yqq cargo rustc libsqlite3-dev libssl-dev libgtk-3-dev meson \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav libgstrtspserver-1.0-dev ibgstreamer-plugins-bad1.0-dev
```
## Call for designers
**Fedora**
Currently there no design plans or mockups. They are highly needed in order to advance the Gtk Client.
```sh
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson \
gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-devel-docs \
gstreamer1-plugins-base-devel gstreamer1-plugins-base-devel-docs \
gstreamer1-plugins-good gstreamer1-plugins-good-extras \
gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-devel \
gstreamer1-plugins-bad-free-extras
There is the will for a complete client re-write if a someone contributes the mockups.
```
If you happen to be a designer and want to contribute please hope on [#hammond](https://docs.python.org/3/library/exceptions.html) and get in touch with us.
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
@@ -84,45 +108,47 @@ There alot of thins yet to be done.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
You can find start by taking a look at [Issues](https://gitlab.gnome.org/alatiera/Hammond/issues) or Opening a [New one](https://gitlab.gnome.org/alatiera/Hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/podcasts/issues) or by opening a [New issue](https://gitlab.gnome.org/World/podcasts/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
[contribution-guidelines]: https://gitlab.gnome.org/alatiera/Hammond/blob/master/CONTRIBUTING.md
[contribution-guidelines]: https://gitlab.gnome.org/World/podcasts/blob/master/CONTRIBUTING.md
## Overview
```sh
$ tree -d
├── assets # png's used in the README.md
├── hammond-data # Storate related stuff, Sqlite db, XDG setup.
│   ├── migrations # Diesel migrations.
├── screenshots # png's used in the README.md
├── 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
│   ├── views # Currently only contains the Podcasts_view.
│   └── widgets # Contains custom widgets such as Podcast and Episode.
│   ├── stacks # Contains the gtk Stacks that hold all the different views.
│   └── widgets # Contains custom widgets such as Show and Episode.
```
## 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](https://en.wikipedia.org/wiki/Evey_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 insired by [GNOME Music](https://wiki.gnome.org/Design/Apps/Music) and [Vocal](http://vocalproject.net/).
We also copied some elements from [Gnome-news](https://wiki.gnome.org/Design/Apps/Potential/News).
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
And almost the entirety of the build system is copied from the [Fractal](https://gitlab.gnome.org/danigm/fractal) project.
[vendor]: https://github.com/alexcrichton/cargo-vendor
[irc]: irc://irc.gnome.org/#hammond
[matrix]: https://matrix.to/#/#gnome-podcasts:matrix.org
+9 -34
View File
@@ -1,50 +1,25 @@
## TODOs:
**General:**
- [ ] Write docs
# TODOs
## Planned Features
## Priorities:
## Priorities
- [ ] Discuss and decide when to schedule the download cleaner. [#3](https://gitlab.gnome.org/alatiera/Hammond/issues/3)
- [ ] Unplayed Only and Downloaded only view.
- [ ] Auto-updater
- [ ] OPML import/export // Probably need to create a crate.
**Proper Desing Mockups for the Gtk Client:**
## Second
- [ ] Re-design EpisodeWidget.
- [ ] Re-design PodcastWidget.
- [ ] Polish the flowbox_child banner.
## Second:
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] Notifications
- [ ] Make use of file metadas?, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] Episode queue
- [ ] Embedded player
- [ ] MPRIS integration
- [ ] Search Implementation
## Third
## Third:
- [ ] Download Queue
- [ ] Download Queue
- [ ] Ability to Stream content on demand
- [ ] soundcloud and itunes feeds // [This](http://getrssfeed.com) seems intresting.
- [ ] Integrate with Itunes API for various crap
- [ ] YoutubeFeeds
- [ ] rss feeds from soundcloud urls? // [This](http://getrssfeed.com) seems intresting.
- [ ] Integrate with Itunes API for various crap?
- [ ] YoutubeFeeds?
## Rest Tasks
**Would be nice:**
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
- [ ] Lazy evaluate episode loading based on the podcast_widget's view scrolling.
- [ ] Headerbar back button and stack switching
**FIXME:**
- [ ] Fix Etag/Last-modified implementation. [#2](https://gitlab.gnome.org/alatiera/Hammond/issues/2)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

Vendored
-183
View File
@@ -1,183 +0,0 @@
#!/bin/bash
# Adapted from:
# https://gitlab.gnome.org/danigm/libgepub/blob/27f0d374e0c8f6fa972dbd111d4ce0c0f3096914/configure_meson
# configure script adapter for Meson
# Based on build-api: https://github.com/cgwalters/build-api
# Copyright 2010, 2011, 2013 Colin Walters <walters@verbum.org>
# Copyright 2016, 2017 Emmanuele Bassi
# Copyright 2017 Iñigo Martínez <inigomartinez@gmail.com>
# Licensed under the new-BSD license (http://www.opensource.org/licenses/bsd-license.php)
# Build API variables:
# Little helper function for reading args from the commandline.
# it automatically handles -a b and -a=b variants, and returns 1 if
# we need to shift $3.
read_arg() {
# $1 = arg name
# $2 = arg value
# $3 = arg parameter
local rematch='^[^=]*=(.*)$'
if [[ $2 =~ $rematch ]]; then
read "$1" <<< "${BASH_REMATCH[1]}"
else
read "$1" <<< "$3"
# There is no way to shift our callers args, so
# return 1 to indicate they should do it instead.
return 1
fi
}
sanitycheck() {
# $1 = arg name
# $1 = arg command
# $2 = arg alternates
local cmd=$( which $2 2>/dev/null )
if [ -x "$cmd" ]; then
read "$1" <<< "$cmd"
return 0
fi
test -z $3 || {
for alt in $3; do
cmd=$( which $alt 2>/dev/null )
if [ -x "$cmd" ]; then
read "$1" <<< "$cmd"
return 0
fi
done
}
echo -e "\e[1;31mERROR\e[0m: Command '$2' not found"
exit 1
}
checkoption() {
# $1 = arg
option="${1#*--}"
action="${option%%-*}"
name="${option#*-}"
if [ ${default_options[$name]+_} ]; then
case "$action" in
enable) meson_options[$name]=true;;
disable) meson_options[$name]=false;;
*) echo -e "\e[1;33mINFO\e[0m: Ignoring unknown action '$action'";;
esac
else
echo -e "\e[1;33mINFO\e[0m: Ignoring unknown option '$option'"
fi
}
echooption() {
# $1 = option
if [ ${meson_options[$1]+_} ]; then
echo ${meson_options[$1]}
elif [ ${default_options[$1]+_} ]; then
echo ${default_options[$1]}
fi
}
sanitycheck MESON 'meson'
sanitycheck MESONTEST 'mesontest'
sanitycheck NINJA 'ninja' 'ninja-build'
declare -A meson_options
while (($# > 0)); do
case "${1%%=*}" in
--prefix) read_arg prefix "$@" || shift;;
--bindir) read_arg bindir "$@" || shift;;
--sbindir) read_arg sbindir "$@" || shift;;
--libexecdir) read_arg libexecdir "$@" || shift;;
--datarootdir) read_arg datarootdir "$@" || shift;;
--datadir) read_arg datadir "$@" || shift;;
--sysconfdir) read_arg sysconfdir "$@" || shift;;
--libdir) read_arg libdir "$@" || shift;;
--mandir) read_arg mandir "$@" || shift;;
--includedir) read_arg includedir "$@" || shift;;
*) checkoption $1;;
esac
shift
done
# Defaults
test -z ${prefix} && prefix="/usr/local"
test -z ${bindir} && bindir=${prefix}/bin
test -z ${sbindir} && sbindir=${prefix}/sbin
test -z ${libexecdir} && libexecdir=${prefix}/bin
test -z ${datarootdir} && datarootdir=${prefix}/share
test -z ${datadir} && datadir=${datarootdir}
test -z ${sysconfdir} && sysconfdir=${prefix}/etc
test -z ${libdir} && libdir=${prefix}/lib
test -z ${mandir} && mandir=${prefix}/share/man
test -z ${includedir} && includedir=${prefix}/include
# The source directory is the location of this file
srcdir=$(dirname $0)
# The build directory is the current location
builddir=`pwd`
# If we're calling this file from the source directory then
# we automatically create a build directory and ensure that
# both Meson and Ninja invocations are relative to that
# location
if [[ -f "${builddir}/meson.build" ]]; then
mkdir -p _build
builddir="${builddir}/_build"
NINJA_OPT="-C ${builddir}"
fi
# Wrapper Makefile for Ninja
cat > Makefile <<END
# Generated by configure; do not edit
all:
${NINJA} ${NINJA_OPT}
install:
DESTDIR="\$(DESTDIR)" ${NINJA} ${NINJA_OPT} install
uninstall:
${NINJA} ${NINJA_OPT} uninstall
release:
${NINJA} ${NINJA_OPT} release
check:
${MESONTEST} ${NINJA_OPT}
END
echo "
hammond
=======
meson: ${MESON}
ninja: ${NINJA}
prefix: ${prefix}
Now type 'make' to build
"
cmd_options=""
for key in "${!meson_options[@]}"; do
cmd_options="$cmd_options -Denable-$key=${meson_options[$key]}"
done
exec ${MESON} \
--prefix=${prefix} \
--libdir=${libdir} \
--libexecdir=${libexecdir} \
--datadir=${datadir} \
--sysconfdir=${sysconfdir} \
--bindir=${bindir} \
--includedir=${includedir} \
--mandir=${mandir} \
${cmd_options} \
${builddir} \
${srcdir}
-32
View File
@@ -1,32 +0,0 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-data"
version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.0"
dotenv = "0.10.1"
error-chain = "0.11.0"
lazy_static = "0.2.11"
log = "0.3.8"
r2d2 = "0.7.4"
r2d2-diesel = "0.16.0"
rayon = "0.9.0"
reqwest = "0.8.1"
rfc822_sanitizer = "0.3.3"
rss = "1.1.0"
url = "1.6.0"
xdg = "2.1.0"
[dependencies.diesel]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.diesel_codegen]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dev-dependencies]
rand = "0.3.18"
tempdir = "0.3.5"
-66
View File
@@ -1,66 +0,0 @@
#![feature(test)]
extern crate diesel;
extern crate hammond_data;
extern crate rand;
extern crate rayon;
extern crate rss;
extern crate tempdir;
extern crate test;
use rayon::prelude::*;
use test::Bencher;
use hammond_data::Source;
use hammond_data::feed::{index, Feed};
use std::io::BufReader;
// Big rss feed
const PCPER: &[u8] = include_bytes!("feeds/pcpermp3.xml");
const UNPLUGGED: &[u8] = include_bytes!("feeds/linuxunplugged.xml");
const RADIO: &[u8] = include_bytes!("feeds/coderradiomp3.xml");
const SNAP: &[u8] = include_bytes!("feeds/techsnapmp3.xml");
const LAS: &[u8] = include_bytes!("feeds/TheLinuxActionShow.xml");
static URLS: &[(&[u8], &str)] = &[
(PCPER, "https://www.pcper.com/rss/podcasts-mp3.rss"),
(UNPLUGGED, "http://feeds.feedburner.com/linuxunplugged"),
(RADIO, "https://feeds.feedburner.com/coderradiomp3"),
(SNAP, "https://feeds.feedburner.com/techsnapmp3"),
(LAS, "https://feeds2.feedburner.com/TheLinuxActionShow"),
];
fn index_urls() {
let feeds: Vec<_> = URLS.par_iter()
.map(|&(buff, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(buff)).unwrap();
Feed::from_channel_source(chan, s)
})
.collect();
index(feeds);
}
#[bench]
fn bench_index_feeds(b: &mut Bencher) {
b.iter(|| {
index_urls();
});
}
#[bench]
fn bench_index_unchanged_feeds(b: &mut Bencher) {
// Index first so it will only bench the comparison test case.
index_urls();
b.iter(|| {
for _ in 0..10 {
index_urls();
}
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-77
View File
@@ -1,77 +0,0 @@
use r2d2_diesel::ConnectionManager;
use diesel::prelude::*;
use r2d2;
use std::path::PathBuf;
use std::sync::Arc;
use std::io;
use std::time::Duration;
use errors::*;
#[cfg(not(test))]
use xdg_dirs;
type Pool = Arc<r2d2::Pool<ConnectionManager<SqliteConnection>>>;
embed_migrations!("migrations/");
lazy_static!{
static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap());
}
#[cfg(not(test))]
lazy_static! {
static ref DB_PATH: PathBuf = xdg_dirs::HAMMOND_XDG.place_data_file("hammond.db").unwrap();
}
#[cfg(test)]
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");
}
pub(crate) fn connection() -> Pool {
Arc::clone(&POOL)
}
fn init_pool(db_path: &str) -> Pool {
let config = r2d2::Config::builder()
.pool_size(1)
.connection_timeout(Duration::from_secs(60))
.build();
let manager = ConnectionManager::<SqliteConnection>::new(db_path);
let pool = Arc::new(r2d2::Pool::new(config, manager).expect("Failed to create pool."));
{
let db = Arc::clone(&pool).get().expect("Failed to initialize pool.");
run_migration_on(&*db).expect("Failed to run migrations during init.");
}
info!("Database pool initialized.");
pool
}
fn run_migration_on(connection: &SqliteConnection) -> Result<()> {
info!("Running DB Migrations...");
// embedded_migrations::run(connection)?;
embedded_migrations::run_with_output(connection, &mut io::stdout())?;
Ok(())
}
/// Reset the database into a clean state.
// Test share a Temp file db.
#[allow(dead_code)]
pub fn truncate_db() -> Result<()> {
let db = connection();
let con = db.get()?;
con.execute("DELETE FROM episode")?;
con.execute("DELETE FROM podcast")?;
con.execute("DELETE FROM source")?;
Ok(())
}
-206
View File
@@ -1,206 +0,0 @@
//! Random CRUD helper functions.
use diesel::prelude::*;
use diesel;
use models::queryables::{Episode, Podcast, Source};
use chrono::prelude::*;
use errors::*;
use database::connection;
pub fn get_sources() -> Result<Vec<Source>> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
Ok(source.load::<Source>(&*con)?)
}
pub fn get_podcasts() -> Result<Vec<Podcast>> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
Ok(podcast.load::<Podcast>(&*con)?)
}
pub fn get_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode.order(epoch.desc()).load::<Episode>(&*con)?)
}
pub fn get_downloaded_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode
.filter(local_uri.is_not_null())
.load::<Episode>(&*con)?)
}
pub fn get_played_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode.filter(played.is_not_null()).load::<Episode>(&*con)?)
}
pub fn get_episode_from_id(ep_id: i32) -> Result<Episode> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode.filter(id.eq(ep_id)).get_result::<Episode>(&*con)?)
}
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode
.filter(id.eq(ep_id))
.select(local_uri)
.get_result::<Option<String>>(&*con)?)
}
pub fn get_episodes_with_limit(limit: u32) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(episode
.order(epoch.desc())
.limit(i64::from(limit))
.load::<Episode>(&*con)?)
}
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
Ok(podcast.filter(id.eq(pid)).get_result::<Podcast>(&*con)?)
}
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.order(epoch.desc())
.load::<Episode>(&*con)?)
}
pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.filter(played.is_null())
.order(epoch.desc())
.load::<Episode>(&*con)?)
}
pub fn get_pd_episodes_limit(parent: &Podcast, limit: u32) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.order(epoch.desc())
.limit(i64::from(limit))
.load::<Episode>(&*con)?)
}
pub fn get_source_from_uri(uri_: &str) -> Result<Source> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
Ok(source.filter(uri.eq(uri_)).get_result::<Source>(&*con)?)
}
// pub fn get_podcast_from_title(title_: &str) -> QueryResult<Podcast> {
// use schema::podcast::dsl::*;
// let db = connection();
// let con = db.get()?;
// podcast
// .filter(title.eq(title_))
// .get_result::<Podcast>(&*con)
// }
pub fn get_podcast_from_source_id(sid: i32) -> Result<Podcast> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
Ok(podcast
.filter(source_id.eq(sid))
.get_result::<Podcast>(&*con)?)
}
pub fn get_episode_from_uri(con: &SqliteConnection, uri_: &str) -> QueryResult<Episode> {
use schema::episode::dsl::*;
episode.filter(uri.eq(uri_)).get_result::<Episode>(&*con)
}
pub fn remove_feed(pd: &Podcast) -> Result<()> {
let db = connection();
let con = db.get()?;
con.transaction(|| -> Result<()> {
delete_source(&con, pd.source_id())?;
delete_podcast(&con, *pd.id())?;
delete_podcast_episodes(&con, *pd.id())?;
info!("Feed removed from the Database.");
Ok(())
})
}
pub fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
use schema::source::dsl::*;
diesel::delete(source.filter(id.eq(source_id))).execute(&*con)
}
pub fn delete_podcast(con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(&*con)
}
pub fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(&*con)
}
pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
let epoch_now = Utc::now().timestamp() as i32;
con.transaction(|| -> Result<usize> {
Ok(diesel::update(
Episode::belonging_to(parent).filter(played.is_null()),
).set(played.eq(Some(epoch_now)))
.execute(&*con)?)
})
}
-18
View File
@@ -1,18 +0,0 @@
use diesel::result;
use diesel::migrations::RunMigrationsError;
use rss;
use reqwest;
use r2d2;
use std::io;
error_chain! {
foreign_links {
R2D2TimeoutError(r2d2::GetTimeout);
DieselResultError(result::Error);
DieselMigrationError(RunMigrationsError);
RSSError(rss::Error);
ReqError(reqwest::Error);
IoError(io::Error);
}
}
-288
View File
@@ -1,288 +0,0 @@
//! Index and retrieve Feeds.
use rayon::prelude::*;
use diesel::prelude::*;
use rayon::iter::IntoParallelIterator;
use diesel::Identifiable;
use rss;
use dbqueries;
use parser;
use models::queryables::{Episode, Podcast, Source};
use models::insertables::{NewEpisode, NewPodcast};
use database::connection;
use errors::*;
#[derive(Debug)]
/// Wrapper struct that hold a `Source` and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
pub struct Feed {
channel: rss::Channel,
source: Source,
}
impl Feed {
/// Constructor that consumes a `Source` and returns the corresponding `Feed` struct.
pub fn from_source(s: Source) -> Result<Feed> {
s.into_feed()
}
/// Constructor that consumes a `Source` and a `rss::Channel` returns a `Feed` struct.
pub fn from_channel_source(chan: rss::Channel, s: Source) -> Feed {
Feed {
channel: chan,
source: s,
}
}
fn index(&self) -> Result<()> {
let pd = self.get_podcast()?;
self.index_channel_items(&pd)
}
// #[allow(dead_code)]
// fn index_channel(&self) -> Result<()> {
// self.parse_channel().index()?;
// Ok(())
// }
// TODO: Refactor transcactions and find a way to do it in parallel.
fn index_channel_items(&self, pd: &Podcast) -> Result<()> {
let episodes = self.parse_channel_items(pd);
let db = connection();
let con = db.get()?;
let _ = con.transaction::<(), Error, _>(|| {
episodes.into_iter().for_each(|x| {
let e = x.index(&con);
if let Err(err) = e {
error!("Failed to index episode: {:?}.", x.title());
error!("Error msg: {}", err);
};
});
Ok(())
});
Ok(())
}
fn parse_channel(&self) -> NewPodcast {
parser::new_podcast(&self.channel, *self.source.id())
}
fn parse_channel_items(&self, pd: &Podcast) -> Vec<NewEpisode> {
let items = self.channel.items();
let new_episodes: Vec<_> = items
.into_par_iter()
.filter_map(|item| parser::new_episode(item, *pd.id()).ok())
.collect();
new_episodes
}
fn get_podcast(&self) -> Result<Podcast> {
self.parse_channel().into_podcast()
}
#[allow(dead_code)]
fn get_episodes(&self) -> Result<Vec<Episode>> {
let pd = self.get_podcast()?;
let eps = self.parse_channel_items(&pd);
let db = connection();
let con = db.get()?;
// TODO: Make it parallel
// This returns only the episodes in the xml feed.
let episodes: Vec<_> = eps.into_iter()
.filter_map(|ep| ep.into_episode(&con).ok())
.collect();
Ok(episodes)
// This would return every episode of the feed from the db.
// self.index_channel_items(&pd)?;
// Ok(dbqueries::get_pd_episodes(&pd)?)
}
}
/// Use's `fetch_all` to retrieve a list of `Feed`s and use index them using `feed::index`.
pub fn index_all() -> Result<()> {
let feeds = fetch_all()?;
index(feeds);
Ok(())
}
/// Handle the indexing of a feed `F` into the Database.
///
/// Consume a `ParallelIterator<Feed>` and index it.
pub fn index<F: IntoParallelIterator<Item = Feed>>(feeds: F) {
feeds.into_par_iter().for_each(|f| {
let e = f.index();
if e.is_err() {
error!("Error While trying to update the database.");
error!("Error msg: {}", e.unwrap_err());
};
});
info!("Indexing done.");
}
/// Retrieve a list of all the `Source` in the database,
/// then use `feed::fetch` to convert them into `Feed`s
/// and return them.
pub fn fetch_all() -> Result<Vec<Feed>> {
let feeds = dbqueries::get_sources()?;
Ok(fetch(feeds))
}
/// Consume a `ParallelIterator<Source>` and return a list of `Feed`s.
pub fn fetch<F: IntoParallelIterator<Item = Source>>(feeds: F) -> Vec<Feed> {
let results: Vec<_> = feeds
.into_par_iter()
.filter_map(|x| {
let uri = x.uri().to_owned();
let feed = Feed::from_source(x).ok();
if feed.is_none() {
error!("Error While trying to fetch from source url: {}.", uri);
}
feed
})
.collect();
results
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::BufReader;
use database::truncate_db;
use super::*;
#[test]
/// Insert feeds and update/index them.
fn test_index_loop() {
truncate_db().unwrap();
let inpt = vec![
"https://request-for-explanation.github.io/podcast/rss.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
"http://feeds.propublica.org/propublica/podcast",
"http://feeds.feedburner.com/linuxunplugged",
];
inpt.iter().for_each(|url| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
index_all().unwrap();
// Run again to cover Unique constrains erros.
index_all().unwrap();
}
#[test]
/// Insert feeds and update/index them.
fn test_fetch_loop() {
truncate_db().unwrap();
let inpt = vec![
"https://request-for-explanation.github.io/podcast/rss.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
"http://feeds.propublica.org/propublica/podcast",
"http://feeds.feedburner.com/linuxunplugged",
];
inpt.iter().for_each(|url| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
fetch_all().unwrap();
}
#[test]
fn test_complete_index() {
// vec of (path, url) tuples.
let urls = vec![
(
"tests/feeds/Intercepted.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
),
(
"tests/feeds/LinuxUnplugged.xml",
"http://feeds.feedburner.com/linuxunplugged",
),
(
"tests/feeds/TheBreakthrough.xml",
"http://feeds.propublica.org/propublica/podcast",
),
(
"tests/feeds/R4Explanation.xml",
"https://request-for-explanation.github.io/podcast/rss.xml",
),
];
truncate_db().unwrap();
let feeds: Vec<_> = urls.iter()
.map(|&(path, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
// open the xml file
let feed = fs::File::open(path).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(feed)).unwrap();
Feed::from_channel_source(chan, s)
})
.collect();
// Index the channels
index(feeds);
// Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources().unwrap().len(), 4);
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 4);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 274);
}
#[test]
fn test_partial_index_podcast() {
truncate_db().unwrap();
let url = "https://feeds.feedburner.com/InterceptedWithJeremyScahill";
let s1 = Source::from_url(url).unwrap();
let s2 = Source::from_url(url).unwrap();
assert_eq!(s1, s2);
assert_eq!(s1.id(), s2.id());
let f1 = s1.into_feed().unwrap();
let f2 = s2.into_feed().unwrap();
let p1 = f1.get_podcast().unwrap();
let p2 = {
f2.index().unwrap();
f2.get_podcast().unwrap()
};
assert_eq!(p1, p2);
assert_eq!(p1.id(), p2.id());
assert_eq!(p1.source_id(), p2.source_id());
let eps1 = f1.get_episodes().unwrap();
let eps2 = {
f2.index().unwrap();
f2.get_episodes().unwrap()
};
eps1.into_par_iter()
.zip(eps2)
.into_par_iter()
.for_each(|(ep1, ep2): (Episode, Episode)| {
assert_eq!(ep1, ep2);
assert_eq!(ep1.id(), ep2.id());
assert_eq!(ep1.podcast_id(), ep2.podcast_id());
});
}
}
-81
View File
@@ -1,81 +0,0 @@
#![recursion_limit = "1024"]
#![deny(missing_docs)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
#![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))]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
//! A libraty for parsing, indexing and retrieving podcast Feeds,
//! into and from a Database.
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_codegen;
extern crate chrono;
extern crate r2d2;
extern crate r2d2_diesel;
extern crate rayon;
extern crate reqwest;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate url;
extern crate xdg;
#[allow(missing_docs)]
pub mod dbqueries;
pub mod utils;
pub mod feed;
#[allow(missing_docs)]
pub mod errors;
pub(crate) mod database;
pub(crate) mod models;
mod parser;
mod schema;
pub use models::queryables::{Episode, Podcast, Source};
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
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()
};
}
}
-424
View File
@@ -1,424 +0,0 @@
use diesel::prelude::*;
use schema::{episode, podcast, source};
use models::queryables::{Episode, Podcast, Source};
use utils::url_cleaner;
use errors::*;
use dbqueries;
use diesel;
use database::connection;
trait Insert {
fn insert(&self, &SqliteConnection) -> QueryResult<usize>;
}
trait Update {
fn update(&self, &SqliteConnection, i32) -> QueryResult<usize>;
}
#[derive(Insertable)]
#[table_name = "source"]
#[derive(Debug, Clone)]
pub(crate) struct NewSource {
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl Insert for NewSource {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::source::dsl::*;
diesel::insert_into(source).values(self).execute(&*con)
}
}
impl NewSource {
pub(crate) fn new_with_uri(uri: &str) -> NewSource {
let uri = url_cleaner(uri);
NewSource {
uri,
last_modified: None,
http_etag: None,
}
}
fn index(&self) -> Result<()> {
let db = connection();
let con = db.get()?;
// Throw away the result like `insert or ignore`
// Diesel deos not support `insert or ignore` yet.
let _ = self.insert(&con);
Ok(())
}
// Look out for when tryinto lands into stable.
pub(crate) fn into_source(self) -> Result<Source> {
self.index()?;
dbqueries::get_source_from_uri(&self.uri)
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
pub(crate) struct NewPodcast {
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
impl Insert for NewPodcast {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::insert_into(podcast).values(self).execute(&*con)
}
}
impl Update for NewPodcast {
fn update(&self, con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::update(podcast.filter(id.eq(podcast_id)))
.set(self)
.execute(&*con)
}
}
impl NewPodcast {
// Look out for when tryinto lands into stable.
pub(crate) fn into_podcast(self) -> Result<Podcast> {
self.index()?;
Ok(dbqueries::get_podcast_from_source_id(self.source_id)?)
}
pub(crate) fn index(&self) -> Result<()> {
let pd = dbqueries::get_podcast_from_source_id(self.source_id);
let db = connection();
let con = db.get()?;
match pd {
Ok(foo) => {
if foo.source_id() != self.source_id {
error!("NSPD sid: {}, SPD sid: {}", self.source_id, foo.source_id());
};
if (foo.link() != self.link) || (foo.title() != self.title)
|| (foo.image_uri() != self.image_uri.as_ref().map(|x| x.as_str()))
{
self.update(&con, *foo.id())?;
}
}
Err(_) => {
self.insert(&con)?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub(crate) struct NewPodcastBuilder {
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
#[allow(dead_code)]
impl NewPodcastBuilder {
pub(crate) fn new() -> NewPodcastBuilder {
NewPodcastBuilder::default()
}
pub(crate) fn title(mut self, s: String) -> NewPodcastBuilder {
self.title = s;
self
}
pub(crate) fn link(mut self, s: String) -> NewPodcastBuilder {
self.link = s;
self
}
pub(crate) fn description(mut self, s: String) -> NewPodcastBuilder {
self.description = s;
self
}
pub(crate) fn image_uri(mut self, s: Option<String>) -> NewPodcastBuilder {
self.image_uri = s;
self
}
pub(crate) fn source_id(mut self, s: i32) -> NewPodcastBuilder {
self.source_id = s;
self
}
pub(crate) fn favorite(mut self, s: bool) -> NewPodcastBuilder {
self.favorite = s;
self
}
pub(crate) fn archive(mut self, s: bool) -> NewPodcastBuilder {
self.archive = s;
self
}
pub(crate) fn always_dl(mut self, s: bool) -> NewPodcastBuilder {
self.always_dl = s;
self
}
pub(crate) fn build(self) -> NewPodcast {
NewPodcast {
title: self.title,
link: self.link,
description: self.description,
image_uri: self.image_uri,
favorite: self.favorite,
archive: self.archive,
always_dl: self.always_dl,
source_id: self.source_id,
}
}
}
#[allow(dead_code)]
// Ignore the following geters. They are used in unit tests mainly.
impl NewPodcast {
pub(crate) fn source_id(&self) -> i32 {
self.source_id
}
pub(crate) fn title(&self) -> &str {
&self.title
}
pub(crate) fn link(&self) -> &str {
&self.link
}
pub(crate) fn description(&self) -> &str {
&self.description
}
pub(crate) fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[derive(Debug, Clone, Default)]
pub(crate) struct NewEpisode {
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
length: Option<i32>,
guid: Option<String>,
epoch: i32,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
impl Insert for NewEpisode {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::insert_into(episode).values(self).execute(&*con)
}
}
impl Update for NewEpisode {
fn update(&self, con: &SqliteConnection, episode_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::update(episode.filter(id.eq(episode_id)))
.set(self)
.execute(&*con)
}
}
impl NewEpisode {
// TODO: Currently using diesel from master git.
// Watch out for v0.99.0 beta and change the toml.
// TODO: Refactor into batch indexes instead.
pub(crate) fn into_episode(self, con: &SqliteConnection) -> Result<Episode> {
self.index(con)?;
Ok(dbqueries::get_episode_from_uri(con, &self.uri)?)
}
pub(crate) fn index(&self, con: &SqliteConnection) -> QueryResult<()> {
let ep = dbqueries::get_episode_from_uri(con, &self.uri.clone());
match ep {
Ok(foo) => {
if foo.podcast_id() != self.podcast_id {
error!("NEP pid: {}, EP pid: {}", self.podcast_id, foo.podcast_id());
};
if foo.title() != self.title.as_ref().map(|x| x.as_str())
|| foo.published_date() != self.published_date.as_ref().map(|x| x.as_str())
{
self.update(con, *foo.id())?;
}
}
Err(_) => {
self.insert(con)?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub(crate) struct NewEpisodeBuilder {
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
length: Option<i32>,
guid: Option<String>,
epoch: i32,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
#[allow(dead_code)]
impl NewEpisodeBuilder {
pub(crate) fn new() -> NewEpisodeBuilder {
NewEpisodeBuilder::default()
}
pub(crate) fn title(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.title = s;
self
}
pub(crate) fn uri(mut self, s: String) -> NewEpisodeBuilder {
self.uri = s;
self
}
pub(crate) fn local_uri(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.local_uri = s;
self
}
pub(crate) fn description(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.description = s;
self
}
pub(crate) fn published_date(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.published_date = s;
self
}
pub(crate) fn length(mut self, s: Option<i32>) -> NewEpisodeBuilder {
self.length = s;
self
}
pub(crate) fn played(mut self, s: Option<i32>) -> NewEpisodeBuilder {
self.played = s;
self
}
pub(crate) fn guid(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.guid = s;
self
}
pub(crate) fn epoch(mut self, s: i32) -> NewEpisodeBuilder {
self.epoch = s;
self
}
pub(crate) fn podcast_id(mut self, s: i32) -> NewEpisodeBuilder {
self.podcast_id = s;
self
}
pub(crate) fn favorite(mut self, s: bool) -> NewEpisodeBuilder {
self.favorite = s;
self
}
pub(crate) fn archive(mut self, s: bool) -> NewEpisodeBuilder {
self.archive = s;
self
}
pub(crate) fn build(self) -> NewEpisode {
NewEpisode {
title: self.title,
uri: self.uri,
local_uri: self.local_uri,
description: self.description,
published_date: self.published_date,
length: self.length,
guid: self.guid,
epoch: self.epoch,
played: self.played,
favorite: self.favorite,
archive: self.archive,
podcast_id: self.podcast_id,
}
}
}
#[allow(dead_code)]
// Ignore the following geters. They are used in unit tests mainly.
impl NewEpisode {
pub(crate) fn title(&self) -> Option<&str> {
self.title.as_ref().map(|s| s.as_str())
}
pub(crate) fn uri(&self) -> &str {
self.uri.as_ref()
}
pub(crate) fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
pub(crate) fn published_date(&self) -> Option<&str> {
self.published_date.as_ref().map(|s| s.as_str())
}
pub(crate) fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
pub(crate) fn epoch(&self) -> i32 {
self.epoch
}
pub(crate) fn length(&self) -> Option<i32> {
self.length
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
}
}
-2
View File
@@ -1,2 +0,0 @@
pub(crate) mod insertables;
pub(crate) mod queryables;
-418
View File
@@ -1,418 +0,0 @@
use chrono::prelude::*;
use reqwest;
use diesel::SaveChangesDsl;
use reqwest::header::{ETag, LastModified};
use rss::Channel;
use schema::{episode, podcast, source};
use feed::Feed;
use errors::*;
use models::insertables::NewSource;
use database::connection;
use std::io::Read;
use std::str::FromStr;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[table_name = "episode"]
#[changeset_options(treat_none_as_null = "true")]
#[belongs_to(Podcast, foreign_key = "podcast_id")]
#[derive(Debug, Clone)]
/// Diesel Model of the episode table.
pub struct Episode {
id: i32,
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
epoch: i32,
length: Option<i32>,
guid: Option<String>,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
impl Episode {
/// Get the value of the `title` field.
pub fn title(&self) -> Option<&str> {
self.title.as_ref().map(|s| s.as_str())
}
/// Set the `title`.
pub fn set_title(&mut self, value: Option<&str>) {
self.title = value.map(|x| x.to_string());
}
/// Get the value of the `uri`.
///
/// Represents the url(usually) that the media file will be located at.
pub fn uri(&self) -> &str {
self.uri.as_ref()
}
/// Set the `uri`.
pub fn set_uri(&mut self, value: &str) {
self.uri = value.to_string();
}
/// Get the value of the `local_uri`.
///
/// Represents the local uri,usually filesystem path,
/// that the media file will be located at.
pub fn local_uri(&self) -> Option<&str> {
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 the `published_date`.
pub fn published_date(&self) -> Option<&str> {
self.published_date.as_ref().map(|s| s.as_str())
}
/// Set the `published_date`.
pub fn set_published_date(&mut self, value: Option<&str>) {
self.published_date = value.map(|x| x.to_string().to_owned());
}
/// Get the value of the `description`.
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.
/// Value is set to Utc whenever possible.
pub fn epoch(&self) -> i32 {
self.epoch
}
/// Set the `epoch`.
pub fn set_epoch(&mut self, value: i32) {
self.epoch = value;
}
/// Get the `length`.
pub fn length(&self) -> Option<i32> {
self.length
}
/// Set the `length`.
pub fn set_length(&mut self, value: Option<i32>) {
self.length = value;
}
/// Epoch representation of the last time the episode was played.
///
/// None/Null for unplayed.
pub fn played(&self) -> Option<i32> {
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<()> {
let epoch = Utc::now().timestamp() as i32;
self.set_played(Some(epoch));
self.save()?;
Ok(())
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Episode> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Episode>(&*tempdb)?)
}
}
#[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 Podcast {
/// 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
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Podcast> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Podcast>(&*tempdb)?)
}
}
#[derive(Queryable, Identifiable, AsChangeset, PartialEq)]
#[table_name = "source"]
#[changeset_options(treat_none_as_null = "true")]
#[derive(Debug, Clone)]
/// Diesel Model of the source table.
pub struct Source {
id: i32,
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl<'a> Source {
/// Represents the location(usually url) of the Feed xml file.
pub fn uri(&self) -> &str {
&self.uri
}
/// Represents the Http Last-Modified Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn last_modified(&self) -> Option<&str> {
self.last_modified.as_ref().map(|s| s.as_str())
}
/// Set `last_modified` value.
pub fn set_last_modified(&mut self, value: Option<&str>) {
self.last_modified = value.map(|x| x.to_string());
}
/// Represents the Http Etag Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn http_etag(&self) -> Option<&str> {
self.http_etag.as_ref().map(|s| s.as_str())
}
/// Set `http_etag` value.
pub fn set_http_etag(&mut self, value: Option<&str>) {
self.http_etag = value.map(|x| x.to_string());
}
/// Extract Etag and LastModifier from req, and update self and the
/// corresponding db row.
fn update_etag(&mut self, req: &reqwest::Response) -> Result<()> {
let headers = req.headers();
// let etag = headers.get_raw("ETag").unwrap();
let etag = headers.get::<ETag>();
let lmod = headers.get::<LastModified>();
// FIXME: This dsnt work most of the time apparently
if self.http_etag() != etag.map(|x| x.tag())
|| self.last_modified != lmod.map(|x| format!("{}", x))
{
self.http_etag = etag.map(|x| x.tag().to_string().to_owned());
self.last_modified = lmod.map(|x| format!("{}", x));
self.save()?;
}
Ok(())
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Source> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Source>(&*tempdb)?)
}
/// `Feed` constructor.
///
/// Fetches the latest xml Feed.
///
/// Updates the validator Http Headers.
///
/// Consumes `self` and Returns the corresponding `Feed` Object.
// TODO: Refactor into TryInto once it lands on stable.
pub fn into_feed(mut self) -> Result<Feed> {
use reqwest::header::{ETag, EntityTag, Headers, HttpDate, LastModified};
let mut headers = Headers::new();
if let Some(foo) = self.http_etag() {
headers.set(ETag(EntityTag::new(true, foo.to_owned())));
}
if let Some(foo) = self.last_modified() {
if let Ok(x) = foo.parse::<HttpDate>() {
headers.set(LastModified(x));
}
}
// FIXME: I have fucked up somewhere here.
// Getting back 200 codes even though I supposedly sent etags.
// info!("Headers: {:?}", headers);
let client = reqwest::Client::builder().referer(false).build()?;
let mut req = client.get(self.uri()).headers(headers).send()?;
info!("GET to {} , returned: {}", self.uri(), req.status());
// TODO match on more stuff
// 301: Permanent redirect of the url
// 302: Temporary redirect of the url
// 304: Up to date Feed, checked with the Etag
// 410: Feed deleted
// match req.status() {
// reqwest::StatusCode::NotModified => (),
// _ => (),
// };
self.update_etag(&req)?;
let mut buf = String::new();
req.read_to_string(&mut buf)?;
let chan = Channel::from_str(&buf)?;
Ok(Feed::from_channel_source(chan, self))
}
/// Construct a new `Source` with the given `uri` and index it.
pub fn from_url(uri: &str) -> Result<Source> {
NewSource::new_with_uri(uri).into_source()
}
}
-362
View File
@@ -1,362 +0,0 @@
use rss::{Channel, Item};
use rfc822_sanitizer::parse_from_rfc2822_with_fallback;
use models::insertables::{NewEpisode, NewEpisodeBuilder, NewPodcast, NewPodcastBuilder};
use utils::url_cleaner;
use errors::*;
// TODO: Extend the support for parsing itunes extensions
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub(crate) fn new_podcast(chan: &Channel, source_id: i32) -> NewPodcast {
let title = chan.title().trim().to_owned();
let description = chan.description().trim().to_owned();
let link = url_cleaner(chan.link()).to_owned();
let x = chan.itunes_ext().map(|s| s.image());
let image_uri = if let Some(img) = x {
img.map(|s| url_cleaner(s))
} else {
chan.image().map(|foo| url_cleaner(foo.url()))
};
NewPodcastBuilder::new()
.title(title)
.description(description)
.link(link)
.image_uri(image_uri)
.source_id(source_id)
.build()
}
/// Parses an `rss::Item` into a `NewEpisode` Struct.
pub(crate) fn new_episode(item: &Item, parent_id: i32) -> Result<NewEpisode> {
let title = item.title().map(|s| s.trim().to_owned());
let description = item.description().map(|s| s.trim().to_owned());
let guid = item.guid().map(|s| s.value().trim().to_owned());
// Its kinda weird this being an Option type.
// Rss 2.0 specified that it's optional.
// Though the db scema has a requirment of episode uri being Unique && Not Null.
// TODO: Restructure
let x = item.enclosure().map(|s| url_cleaner(s.url()));
let uri = if x.is_some() {
x.unwrap()
} else if item.link().is_some() {
item.link().map(|s| url_cleaner(s)).unwrap()
} else {
bail!("No url specified for the item.")
};
let date = parse_from_rfc2822_with_fallback(
// Default to rfc2822 represantation of epoch 0.
item.pub_date().unwrap_or("Thu, 1 Jan 1970 00:00:00 +0000"),
);
// Should treat information from the rss feeds as invalid by default.
// Case: Thu, 05 Aug 2016 06:00:00 -0400 <-- Actually that was friday.
let pub_date = date.map(|x| x.to_rfc2822()).ok();
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
let length = item.enclosure().map(|x| x.length().parse().unwrap_or(0));
Ok(
NewEpisodeBuilder::new()
.title(title)
.uri(uri)
.description(description)
.length(length)
.published_date(pub_date)
.epoch(epoch)
.guid(guid)
.podcast_id(parent_id)
.build(),
)
}
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::BufReader;
use rss::Channel;
use super::*;
#[test]
fn test_new_podcast_intercepted() {
let file = File::open("tests/feeds/Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let descr = "The people behind The Intercepts fearless reporting and incisive \
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss \
the crucial issues of our time: national security, civil liberties, foreign \
policy, and criminal justice. Plus interviews with artists, thinkers, and \
newsmakers who challenge our preconceptions about the world we live in.";
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title(), "Intercepted with Jeremy Scahill");
assert_eq!(pd.link(), "https://theintercept.com/podcasts");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri(),
Some(
"http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/\
uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_\
2FIntercepted_COVER%2B_281_29.png"
)
);
}
#[test]
fn test_new_podcast_breakthrough() {
let file = File::open("tests/feeds/TheBreakthrough.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let descr = "Latest Articles and Investigations from ProPublica, an independent, \
non-profit newsroom that produces investigative journalism in the public \
interest.";
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title(), "The Breakthrough");
assert_eq!(pd.link(), "http://www.propublica.org/podcast");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri(),
Some("http://www.propublica.org/images/podcast_logo_2.png")
);
}
#[test]
fn test_new_podcast_lup() {
let file = File::open("tests/feeds/LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
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.";
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title(), "LINUX Unplugged Podcast");
assert_eq!(pd.link(), "http://www.jupiterbroadcasting.com/");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri(),
Some("http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg")
);
}
#[test]
fn test_new_podcast_r4explanation() {
let file = File::open("tests/feeds/R4Explanation.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = new_podcast(&channel, 0);
let descr = "A weekly discussion of Rust RFCs";
assert_eq!(pd.title(), "Request For Explanation");
assert_eq!(
pd.link(),
"https://request-for-explanation.github.io/podcast/"
);
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri(),
Some("https://request-for-explanation.github.io/podcast/podcast.png")
);
}
#[test]
fn test_new_episode_intercepted() {
let file = File::open("tests/feeds/Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let firstitem = channel.items().first().unwrap();
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data breach \
and allegations of Russian interference in the US election. Commentator \
Shaun King explains his call for a boycott of the NFL and talks about his \
campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle \
performs.";
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title(), Some("The Super Bowl of Racism"));
assert_eq!(i.uri(), "http://traffic.megaphone.fm/PPY6458293736.mp3");
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(66738886));
assert_eq!(i.guid(), Some("7df4070a-9832-11e7-adac-cb37b05d5e24"));
assert_eq!(i.published_date(), Some("Wed, 13 Sep 2017 10:00:00 +0000"));
assert_eq!(i.epoch(), 1505296800);
let second = channel.items().iter().nth(1).unwrap();
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "This week on Intercepted: Jeremy gives an update on the aftermath of \
Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang \
lays out how a network of libertarian think tanks called the Atlas Network \
is insidiously shaping political infrastructure in Latin America. We speak \
with attorney and former Hugo Chavez adviser Eva Golinger about the \
Venezuela\'s political turmoil.And we hear Claudia Lizardo of the \
Caracas-based band, La Pequeña Revancha, talk about her music and hopes for \
Venezuela.";
assert_eq!(
i2.title(),
Some("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
);
assert_eq!(i2.uri(), "http://traffic.megaphone.fm/FL5331443769.mp3");
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(67527575));
assert_eq!(i2.guid(), Some("7c207a24-e33f-11e6-9438-eb45dcf36a1d"));
assert_eq!(i2.published_date(), Some("Wed, 9 Aug 2017 10:00:00 +0000"));
assert_eq!(i2.epoch(), 1502272800);
}
#[test]
fn test_new_episode_breakthrough() {
let file = File::open("tests/feeds/TheBreakthrough.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let firstitem = channel.items().first().unwrap();
let descr = "<p>A reporter finds that homes meant to replace New Yorks troubled \
psychiatric hospitals might be just as bad.</p>";
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(
i.title(),
Some("The Breakthrough: Hopelessness and Exploitation Inside Homes for Mentally Ill")
);
assert_eq!(
i.uri(),
"http://tracking.feedpress.it/link/10581/6726758/20170908-cliff-levy.mp3"
);
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(33396551));
assert_eq!(
i.guid(),
Some(
"https://www.propublica.org/podcast/\
the-breakthrough-hopelessness-exploitation-homes-for-mentally-ill#134472"
)
);
assert_eq!(i.published_date(), Some("Fri, 8 Sep 2017 12:00:00 +0000"));
assert_eq!(i.epoch(), 1504872000);
let second = channel.items().iter().nth(1).unwrap();
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "<p>Jonathan Allen and Amie Parnes didnt know their book would be called \
Shattered, or that their extraordinary access would let them chronicle \
the mounting signs of a doomed campaign.</p>";
assert_eq!(
i2.title(),
Some("The Breakthrough: Behind the Scenes of Hillary Clintons Failed Bid for \
President")
);
assert_eq!(
i2.uri(),
"http://tracking.feedpress.it/link/10581/6726759/16_JohnAllen-CRAFT.mp3".to_string()
);
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(17964071));
assert_eq!(
i2.guid(),
Some(
"https://www.propublica.\
org/podcast/the-breakthrough-hillary-clinton-failed-presidential-bid#133721"
)
);
assert_eq!(i2.published_date(), Some("Fri, 25 Aug 2017 12:00:00 +0000"));
assert_eq!(i2.epoch(), 1503662400);
}
#[test]
fn test_new_episode_lup() {
let file = File::open("tests/feeds/LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let firstitem = channel.items().first().unwrap();
let descr =
"Audit your network with a couple of easy commands on Kali Linux. Chris decides to \
blow off a little steam by attacking his IoT devices, Wes has the scope on Equifax \
blaming open source & the Beard just saved the show. Its a really packed episode!";
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title(), Some("Hacking Devices with Kali Linux | LUP 214"));
assert_eq!(
i.uri(),
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3"
);
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(46479789));
assert_eq!(i.guid(), Some("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D"));
assert_eq!(i.published_date(), Some("Tue, 12 Sep 2017 22:24:42 -0700"));
assert_eq!(i.epoch(), 1505280282);
let second = channel.items().iter().nth(1).unwrap();
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "<p>The Gnome project is about to solve one of our audience's biggest \
Waylands concerns. But as the project takes on a new level of relevance, \
decisions for the next version of Gnome have us worried about the \
future.</p>\n\n<p>Plus we chat with Wimpy about the Ubuntu Rally in NYC, \
Microsofts sneaky move to turn Windows 10 into the “ULTIMATE LINUX \
RUNTIME”, community news & more!</p>";
assert_eq!(i2.title(), Some("Gnome Does it Again | LUP 213"));
assert_eq!(
i2.uri(),
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3"
);
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(36544272));
assert_eq!(i2.guid(), Some("1CE57548-B36C-4F14-832A-5D5E0A24E35B"));
assert_eq!(i2.published_date(), Some("Tue, 5 Sep 2017 20:57:27 -0700"));
assert_eq!(i2.epoch(), 1504670247);
}
#[test]
fn test_new_episode_r4expanation() {
let file = File::open("tests/feeds/R4Explanation.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let firstitem = channel.items().iter().nth(9).unwrap();
let descr = "This week we look at <a \
href=\"https://github.com/rust-lang/rfcs/pull/2094\">RFC 2094</a> \
\"Non-lexical lifetimes\"";
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title(), Some("Episode #9 - A Once in a Lifetime RFC"));
assert_eq!(
i.uri(),
"http://request-for-explanation.github.\
io/podcast/ep9-a-once-in-a-lifetime-rfc/episode.mp3"
);
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(15077388));
assert_eq!(
i.guid(),
Some("https://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/")
);
assert_eq!(i.published_date(), Some("Mon, 28 Aug 2017 15:00:00 -0700"));
assert_eq!(i.epoch(), 1503957600);
let second = channel.items().iter().nth(8).unwrap();
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "This week we look at <a \
href=\"https://github.com/rust-lang/rfcs/pull/2071\">RFC 2071</a> \"Add \
impl Trait type alias and variable declarations\"";
assert_eq!(i2.title(), Some("Episode #8 - An Existential Crisis"));
assert_eq!(
i2.uri(),
"http://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/episode.\
mp3"
);
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(13713219));
assert_eq!(
i2.guid(),
Some("https://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/")
);
assert_eq!(i2.published_date(), Some("Tue, 15 Aug 2017 17:00:00 -0700"));
assert_eq!(i2.epoch(), 1502841600);
}
}
-235
View File
@@ -1,235 +0,0 @@
//! Helper utilities for accomplishing various tasks.
use rayon::prelude::*;
use chrono::prelude::*;
use url::{Position, Url};
use errors::*;
use dbqueries;
use models::queryables::Episode;
use std::path::Path;
use std::fs;
fn download_checker() -> Result<()> {
let episodes = dbqueries::get_downloaded_episodes()?;
episodes
.into_par_iter()
.for_each(|mut ep| checker_helper(&mut ep));
Ok(())
}
fn checker_helper(ep: &mut Episode) {
if !Path::new(ep.local_uri().unwrap()).exists() {
ep.set_local_uri(None);
let res = ep.save();
if let Err(err) = res {
error!("Error while trying to update episode: {:#?}", ep);
error!("Error: {}", err);
};
}
}
fn played_cleaner() -> Result<()> {
let episodes = dbqueries::get_played_episodes()?;
let now_utc = Utc::now().timestamp() as i32;
episodes.into_par_iter().for_each(|mut ep| {
if ep.local_uri().is_some() && ep.played().is_some() {
let played = ep.played().unwrap();
// TODO: expose a config and a user set option.
// Chnage the test too when exposed
let limit = played + 172_800; // add 2days in seconds
if now_utc > limit {
let e = delete_local_content(&mut ep);
if let Err(err) = e {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("Error: {}", err);
} else {
info!("Episode {:?} was deleted succesfully.", ep.title());
};
}
}
});
Ok(())
}
/// Check `ep.local_uri` field and delete the file it points to.
pub fn delete_local_content(ep: &mut Episode) -> Result<()> {
if ep.local_uri().is_some() {
let uri = ep.local_uri().unwrap().to_owned();
if Path::new(&uri).exists() {
let res = fs::remove_file(&uri);
if res.is_ok() {
ep.set_local_uri(None);
ep.save()?;
} else {
error!("Error while trying to delete file: {}", uri);
error!("Error: {}", res.unwrap_err());
};
}
} else {
error!(
"Something went wrong evaluating the following path: {:?}",
ep.local_uri(),
);
}
Ok(())
}
/// Database cleaning tasks.
///
/// Runs a download checker which looks for `Episode.local_uri` entries that
/// doesn't exist and sets them to None
///
/// Runs a cleaner for played Episode's that are pass the lifetime limit and
/// scheduled for removal.
pub fn checkup() -> Result<()> {
download_checker()?;
played_cleaner()?;
Ok(())
}
/// Remove fragment identifiers and query pairs from a URL
/// If url parsing fails, return's a trimmed version of the original input.
pub fn url_cleaner(s: &str) -> String {
// Copied from the cookbook.
// https://rust-lang-nursery.github.io/rust-cookbook/net.html
// #remove-fragment-identifiers-and-query-pairs-from-a-url
match Url::parse(s) {
Ok(parsed) => parsed[..Position::AfterPath].to_owned(),
_ => s.trim().to_owned(),
}
}
#[cfg(test)]
mod tests {
extern crate tempdir;
use super::*;
use database::{connection, truncate_db};
use models::insertables::NewEpisodeBuilder;
use self::tempdir::TempDir;
use std::fs::File;
use std::io::Write;
fn helper_db() -> TempDir {
// Clean the db
truncate_db().unwrap();
// Setup tmp file stuff
let tmp_dir = TempDir::new("hammond_test").unwrap();
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();
// Setup episodes
let db = connection();
let con = db.get().unwrap();
NewEpisodeBuilder::new()
.uri("foo_bar".to_string())
.local_uri(Some(valid_path.to_str().unwrap().to_owned()))
.build()
.into_episode(&con)
.unwrap();
NewEpisodeBuilder::new()
.uri("bar_baz".to_string())
.local_uri(Some(bad_path.to_str().unwrap().to_owned()))
.build()
.into_episode(&con)
.unwrap();
tmp_dir
}
#[test]
fn test_download_checker() {
let _tmp_dir = helper_db();
download_checker().unwrap();
let episodes = dbqueries::get_downloaded_episodes().unwrap();
assert_eq!(episodes.len(), 1);
assert_eq!("foo_bar", episodes.first().unwrap().uri());
}
#[test]
fn test_checker_helper() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "bar_baz").unwrap()
};
checker_helper(&mut episode);
assert!(episode.local_uri().is_none());
}
#[test]
fn test_download_cleaner() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let valid_path = episode.local_uri().unwrap().to_owned();
delete_local_content(&mut episode).unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_expired() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let now_utc = Utc::now().timestamp() as i32;
// let limit = now_utc - 172_800;
let epoch = now_utc - 200_000;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should delete the file
played_cleaner().unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_none() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let now_utc = Utc::now().timestamp() as i32;
// limit = 172_800;
let epoch = now_utc - 20_000;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should not delete the file
played_cleaner().unwrap();
assert_eq!(Path::new(&valid_path).exists(), true);
}
#[test]
fn test_url_cleaner() {
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);
}
}
File diff suppressed because it is too large Load Diff
-139
View File
@@ -1,139 +0,0 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>Request For Explanation</title>
<link>https://request-for-explanation.github.io/podcast/</link>
<description>A weekly discussion of Rust RFCs</description>
<image>
<url>https://request-for-explanation.github.io/podcast/podcast.png</url>
<title>Request For Explanation</title>
<link>https://request-for-explanation.github.io/podcast/</link>
</image>
<atom:link href="http://request-for-explanation.github.io/podcast/rss.xml" rel="self" type="application/rss+xml" />
<googleplay:author>The Request For Explanation Podcast</googleplay:author>
<itunes:author>The Request For Explanation Podcast</itunes:author>
<googleplay:email>manishearth@gmail.com</googleplay:email>
<itunes:owner>
<itunes:name>Manish Goregaokar</itunes:name>
<itunes:email>manishearth@gmail.com</itunes:email>
</itunes:owner>
<googleplay:image href="https://request-for-explanation.github.io/podcast/podcast.png" />
<itunes:image href="https://request-for-explanation.github.io/podcast/podcast.png" />
<language>en-us</language>
<googleplay:category text="Technology"/>
<itunes:category text="Technology" />
<itunes:explicit>no</itunes:explicit>
<item>
<title>Episode #0 - What the Hell</title>
<link>https://request-for-explanation.github.io/podcast/ep0-what-the-hell/</link>
<pubDate>Mon, 19 Jun 2017 19:45:01 EST</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep0-what-the-hell/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep0-what-the-hell/episode.mp3" length="7057920" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2005">RFC 2005</a> "Match Ergonomics Using Default Binding Modes"]]></description>
<itunes:order>1</itunes:order>
<itunes:duration>20:29</itunes:duration>
</item>
<item>
<title>Episode #1 - Constermash</title>
<link>https://request-for-explanation.github.io/podcast/ep1-constermash/</link>
<pubDate>Thu, 29 Jun 2017 17:30:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep1-constermash/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep1-constermash/episode.mp3" length="28588800" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2000">RFC 2000</a> "Const Generics"]]></description>
<itunes:order>2</itunes:order>
<itunes:duration>16:09</itunes:duration>
</item>
<item>
<title>Episode #2 - Stealing Chickens on the Internet</title>
<link>https://request-for-explanation.github.io/podcast/ep2-stealing-chickens-on-the-internet/</link>
<pubDate>Thu, 6 July 2017 15:30:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep2-stealing-chickens-on-the-internet</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep2-stealing-chickens-on-the-internet/episode.mp3" length="19608187" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2052">RFC 2052</a> "Evolving Rust through Epochs"]]></description>
<itunes:order>3</itunes:order>
<itunes:duration>43:25</itunes:duration>
</item>
<item>
<title>Episode #3 - Aaron's Favorite Topic</title>
<link>https://request-for-explanation.github.io/podcast/ep3-aarons-favorite-topic/</link>
<pubDate>Mon, 10 July 2017 16:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep3-aarons-favorite-topic</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep3-aarons-favorite-topic/episode.mp3" length="19070229" type="audio/mpeg" />
<description><![CDATA[This week we talk about the RFC process in general -- what it is, how it works, and how it came to be.]]></description>
<itunes:order>4</itunes:order>
<itunes:duration>54:01</itunes:duration>
</item>
<item>
<title>Episode #4 - Literally Haskell</title>
<link>https://request-for-explanation.github.io/podcast/ep4-literally-haskell/</link>
<pubDate>Mon, 17 July 2017 17:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep4-literally-haskell</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep4-literally-haskell/episode.mp3" length="12973478" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/1598">RFC 1598</a> "Generic Associated Types"]]></description>
<itunes:order>5</itunes:order>
<itunes:duration>36:41</itunes:duration>
</item>
<item>
<title>Episode #5 - Are you my main?</title>
<link>https://request-for-explanation.github.io/podcast/ep5-are-you-my-main/</link>
<pubDate>Mon, 24 July 2017 16:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep5-are-you-my-main</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep5-are-you-my-main/episode.mp3" length="8232966" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/1937">RFC 1937</a> "? in main", as well as discuss some news on other RFCs.]]></description>
<itunes:order>6</itunes:order>
<itunes:duration>22:33</itunes:duration>
</item>
<item>
<title>Episode #6 - Everything and the kitchen async</title>
<link>https://request-for-explanation.github.io/podcast/ep6-everything-and-the-kitchen-async/</link>
<pubDate>Mon, 31 July 2017 16:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep6-everything-and-the-kitchen-async/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep6-everything-and-the-kitchen-async/episode.mp3" length="9530774" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2033">eRFC 2033</a> "Experimentally add coroutines to Rust"]]></description>
<itunes:order>7</itunes:order>
<itunes:duration>25:52</itunes:duration>
</item>
<item>
<title>Episode #7 - Unwrapping a great RFC</title>
<link>https://request-for-explanation.github.io/podcast/ep7-unwrapping-a-great-rfc/</link>
<pubDate>Tue, 8 Aug 2017 16:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep7-unwrapping-a-great-rfc/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep7-unwrapping-a-great-rfc/episode.mp3" length="8715317" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2091">RFC 2091</a> "Implicit caller location"]]></description>
<itunes:order>8</itunes:order>
<itunes:duration>24:11</itunes:duration>
</item>
<item>
<title>Episode #8 - An Existential Crisis</title>
<link>https://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/</link>
<pubDate>Tue, 15 Aug 2017 17:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/episode.mp3" length="13713219" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2071">RFC 2071</a> "Add impl Trait type alias and variable declarations"]]></description>
<itunes:order>9</itunes:order>
<itunes:duration>38:33</itunes:duration>
</item>
<item>
<title>Episode #9 - A Once in a Lifetime RFC</title>
<link>https://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/</link>
<pubDate>Mon, 28 Aug 2017 15:00:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/episode.mp3" length="15077388" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2094">RFC 2094</a> "Non-lexical lifetimes"]]></description>
<itunes:order>10</itunes:order>
<itunes:duration>42:13</itunes:duration>
</item>
<item>
<title>Episode #10 - Two Paths Diverged in a Yellow Wood</title>
<link>https://request-for-explanation.github.io/podcast/ep10-two-paths-diverged-in-a-yellow-wood/</link>
<pubDate>Thu, 30 Aug 2017 1:30:00 PDT</pubDate>
<guid isPermaLink="false">https://request-for-explanation.github.io/podcast/ep10-two-paths-diverged-in-a-yellow-wood/</guid>
<enclosure url="http://request-for-explanation.github.io/podcast/ep10-two-paths-diverged-in-a-yellow-wood/episode.mp3" length="19994929" type="audio/mpeg" />
<description><![CDATA[This week we look at <a href="https://github.com/rust-lang/rfcs/pull/2126">RFC 2126</a> "Clarify and streamline paths and visibility" (aka "The modules RFC")]]></description>
<itunes:order>11</itunes:order>
<itunes:duration>56:40</itunes:duration>
</item>
</channel>
</rss>
@@ -1,457 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~files/feed-premium.xsl"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:admin="http://webns.net/mvcb/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:feedpress="https://feed.press/xmlns" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:media="http://www.rssboard.org/media-rss" version="2.0">
<channel>
<feedpress:locale>en</feedpress:locale>
<atom:link rel="via" href="http://feeds.propublica.org/propublica/podcast"/>
<atom:link rel="hub" href="http://feedpress.superfeedr.com/"/>
<itunes:category text="News &amp; Politics"/>
<media:category scheme="http://www.itunes.com/dtds/podcast-1.0.dtd">News &amp; Politics</media:category>
<itunes:category text="Society &amp; Culture">
<itunes:category text="History"/>
</itunes:category>
<media:category scheme="http://www.itunes.com/dtds/podcast-1.0.dtd">Society &amp; Culture/History</media:category>
<media:rating>nonadult</media:rating>
<media:description type="plain">The ProPublica Podcast</media:description>
<media:credit role="author">ProPublica</media:credit>
<media:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</media:keywords>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<googleplay:description>The podcast that takes you behind the scenes with journalists to hear how they nailed their biggest stories.</googleplay:description>
<googleplay:author>ProPublica</googleplay:author>
<googleplay:email>celeste.lecompte@propublica.org</googleplay:email>
<googleplay:explicit>no</googleplay:explicit>
<title>The Breakthrough</title>
<link>http://www.propublica.org/podcast</link>
<description>Latest Articles and Investigations from ProPublica, an independent, non-profit newsroom that produces investigative journalism in the public interest.</description>
<language>en</language>
<dc:creator>ProPublica</dc:creator>
<copyright>Copyright 2017 Pro Publica Inc.</copyright>
<pubDate>Fri, 22 Sep 2017 13:22:48 +0000</pubDate>
<dc:date>2017-09-22T13:22:48+00:00</dc:date>
<dc:language>en-us</dc:language>
<dc:rights>Copyright 2017 Pro Publica Inc.</dc:rights>
<atom:link href="http://feeds.propublica.org/propublica/podcast" rel="self" type="application/rss+xml"/>
<itunes:subtitle>The ProPublica Podcast</itunes:subtitle>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>The podcast that takes you behind the scenes with journalists to hear how they nailed their biggest stories.</itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:owner>
<itunes:email>celeste.lecompte@propublica.org</itunes:email>
<itunes:name>ProPublica</itunes:name>
</itunes:owner>
<itunes:image href="http://www.propublica.org/images/podcast_logo_2.png"/>
<item>
<title>The Breakthrough: Hopelessness and Exploitation Inside Homes for Mentally Ill</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>A reporter finds that homes meant to replace New Yorks troubled psychiatric hospitals might be just as bad.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726758/20170908-cliff-levy.mp3" length="33396551" type="audio/mpeg"/>
<itunes:duration>00:27:50</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-hopelessness-exploitation-homes-for-mentally-ill#134472</link>
<pubDate>Fri, 08 Sep 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-hopelessness-exploitation-homes-for-mentally-ill#134472</guid>
<description><![CDATA[
<p>A reporter finds that homes meant to replace New Yorks troubled psychiatric hospitals might be just as bad.</p>
]]></description>
<dc:subject/>
<dc:date>2017-09-08T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
A reporter finds that homes meant to replace New Yorks troubled psychiatric hospitals might be just as bad.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Behind the Scenes of Hillary Clintons Failed Bid for President</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>Jonathan Allen and Amie Parnes didnt know their book would be called Shattered, or that their extraordinary access would let them chronicle the mounting signs of a doomed campaign.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726759/16_JohnAllen-CRAFT.mp3" length="17964071" type="audio/mpeg"/>
<itunes:duration>00:18:45</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-hillary-clinton-failed-presidential-bid#133721</link>
<pubDate>Fri, 25 Aug 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-hillary-clinton-failed-presidential-bid#133721</guid>
<description><![CDATA[
<p>Jonathan Allen and Amie Parnes didnt know their book would be called Shattered, or that their extraordinary access would let them chronicle the mounting signs of a doomed campaign.</p>
]]></description>
<dc:subject/>
<dc:date>2017-08-25T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
Jonathan Allen and Amie Parnes didnt know their book would be called Shattered, or that their extraordinary access would let them chronicle the mounting signs of a doomed campaign.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: How a Small News Outlet Brought Down the State Hero</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>VTDiggers Anne Galloway was suspicious the moment she heard about a too-good-to-be-true development. She didnt know how right she was.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726760/15_VTDigger-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/the-breakthrough-how-a-small-news-outlet-brought-down-the-state-hero#133361</link>
<pubDate>Fri, 11 Aug 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-how-a-small-news-outlet-brought-down-the-state-hero#133361</guid>
<description><![CDATA[
<p>VTDiggers Anne Galloway was suspicious the moment she heard about a too-good-to-be-true development. She didnt know how right she was.</p>
]]></description>
<dc:subject/>
<dc:date>2017-08-11T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
VTDiggers Anne Galloway was suspicious the moment she heard about a too-good-to-be-true development. She didnt know how right she was.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Reporting on Life and Death in the Delivery Room</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>ProPublica reporter Nina Martin and her team used social media and old-fashioned shoe leather to show how the U.S. has the worst maternal death rate in the developed world.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726761/14_MaternalMortality-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/the-breakthrough-reporting-on-life-and-death-in-the-delivery-room#133354</link>
<pubDate>Fri, 28 Jul 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-reporting-on-life-and-death-in-the-delivery-room#133354</guid>
<description><![CDATA[
<p>ProPublica reporter Nina Martin and her team used social media and old-fashioned shoe leather to show how the U.S. has the worst maternal death rate in the developed world.</p>
]]></description>
<dc:subject>Health CareLost Mothers</dc:subject>
<dc:date>2017-07-28T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
ProPublica reporter Nina Martin and her team used social media and old-fashioned shoe leather to show how the U.S. has the worst maternal death rate in the developed world.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: How an ICIJ Reporter Dug Up the World Banks Best Kept Secret</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>Sasha Chavkin chased it down across three continents, and into places he was warned weren't safe.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726762/13_WorldBank-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/the-breakthrough-how-an-icij-reporter-dug-up-world-banks-best-kept-secret#133343</link>
<pubDate>Fri, 14 Jul 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-how-an-icij-reporter-dug-up-world-banks-best-kept-secret#133343</guid>
<description><![CDATA[
<p>Sasha Chavkin chased it down across three continents, and into places he was warned weren&#039;t safe.</p>
]]></description>
<dc:subject/>
<dc:date>2017-07-14T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
Sasha Chavkin chased it down across three continents, and into places he was warned weren't safe.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: A Reporter Crosses Borders to Uncover Labor Abuse</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>ProPublicas Michael Grabell travels from the heart of Ohio to the mountains of Guatemala to track down immigrant workers harmed in American poultry plants.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726763/12_CaseFarms_Grabell-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/the-breakthrough-case-farms-labor-abuse-guatemala-michael-grabell#133335</link>
<pubDate>Fri, 30 Jun 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-case-farms-labor-abuse-guatemala-michael-grabell#133335</guid>
<description><![CDATA[
<p>ProPublicas Michael Grabell travels from the heart of Ohio to the mountains of Guatemala to track down immigrant workers harmed in American poultry plants.</p>
]]></description>
<dc:subject>Labor</dc:subject>
<dc:date>2017-06-30T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
ProPublicas Michael Grabell travels from the heart of Ohio to the mountains of Guatemala to track down immigrant workers harmed in American poultry plants.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Uncovering NYC Cops Making Millions in Suspicious Deals</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>On our first episode of this seasons The Breakthrough, we talk with WNYCs Robert Lewis tells us how his reporting triggered an internal investigation of suspicious dealings made by active-duty New York police officers.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726764/11_RobertLewis-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/the-breakthrough-uncovering-nyc-cops-making-millions-in-suspicious-deals#133325</link>
<pubDate>Fri, 16 Jun 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-uncovering-nyc-cops-making-millions-in-suspicious-deals#133325</guid>
<description><![CDATA[
<p>On our first episode of this seasons The Breakthrough, we talk with WNYCs Robert Lewis tells us how his reporting triggered an internal investigation of suspicious dealings made by active-duty New York police officers.</p>
]]></description>
<dc:subject/>
<dc:date>2017-06-16T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
On our first episode of this seasons The Breakthrough, we talk with WNYCs Robert Lewis tells us how his reporting triggered an internal investigation of suspicious dealings made by active-duty New…</itunes:subtitle>
</item>
<item>
<title>Our Podcast, The Breakthrough, Is Back</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>In January, we launched The Breakthrough, which tells the stories behind investigative reporting. And were about to start a new season, on June 16.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/6726765/10_breakthrough_promo-CRAFT.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/our-podcast-the-breakthrough-is-back#133309</link>
<pubDate>Fri, 09 Jun 2017 12:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/our-podcast-the-breakthrough-is-back#133309</guid>
<description><![CDATA[
<p>In January, we launched The Breakthrough, which tells the stories behind investigative reporting. And were about to start a new season, on June 16.</p>
]]></description>
<dc:subject/>
<dc:date>2017-06-09T12:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
In January, we launched The Breakthrough, which tells the stories behind investigative reporting. And were about to start a new season, on June 16.
</itunes:subtitle>
</item>
<item>
<title>We Want Your Thoughts on Our Podcast</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary>We recently relaunched our podcast, in which journalists tell us how they nailed their biggest stories. Now we want to hear from you.</itunes:summary>
<enclosure url="http://tracking.feedpress.it/link/10581/5486888/9_Breakthrough_Survey.mp3" length="" type="audio/mpeg"/>
<itunes:duration/>
<link>https://www.propublica.org/podcast/we-want-your-thoughts-on-our-podcast#106727</link>
<pubDate>Sat, 11 Mar 2017 00:54:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/we-want-your-thoughts-on-our-podcast#106727</guid>
<description><![CDATA[
<p>We recently relaunched our podcast, in which journalists tell us how they nailed their biggest stories. Now we want to hear from you.</p>
]]></description>
<dc:subject/>
<dc:date>2017-03-11T00:54:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
We recently relaunched our podcast, in which journalists tell us how they nailed their biggest stories. Now we want to hear from you.
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: How Reporters Really Use Unnamed Sources</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<itunes:duration>00:16:27</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-how-reporters-really-use-unnamed-sources#66520</link>
<pubDate>Fri, 24 Feb 2017 14:00:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-how-reporters-really-use-unnamed-sources#66520</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-02-24T14:00:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Uncovering the FBIs Secret Rules</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<itunes:duration>00:20:28</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-uncovering-the-fbis-secret-rules#66523</link>
<pubDate>Fri, 17 Feb 2017 17:40:57 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-uncovering-the-fbis-secret-rules#66523</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-02-17T17:40:57+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Reporters Examine Murder Where Cops Struggle to Curb It</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<itunes:duration>00:20:01</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-reporters-examine-murder-where-cops-struggle-to-curb-it#66526</link>
<pubDate>Fri, 10 Feb 2017 17:25:36 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-reporters-examine-murder-where-cops-struggle-to-curb-it#66526</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-02-10T17:25:36+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: What American Journalists Can Learn From Reporting Under Putin</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<itunes:duration>00:31:54</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-what-american-journalists-can-learn-reporting-under-putin#66529</link>
<pubDate>Fri, 03 Feb 2017 14:00:40 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-what-american-journalists-can-learn-reporting-under-putin#66529</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-02-03T14:00:40+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Uncovering Danger at the Pharmacy Counter</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<itunes:duration>00:16:38</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-uncovering-danger-at-the-pharmacy-counter#66532</link>
<pubDate>Fri, 27 Jan 2017 17:03:24 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-uncovering-danger-at-the-pharmacy-counter#66532</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-01-27T17:03:24+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: How a Reporter Solved a Decades-Old Murder</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/6726766/4_Breakthrough_ MSKiller.mp3" length="56458604" type="audio/mpeg"/>
<itunes:duration>23:31</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-how-a-reporter-solved-a-decades-old-murder#66537</link>
<pubDate>Fri, 20 Jan 2017 14:00:14 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-how-a-reporter-solved-a-decades-old-murder#66537</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-01-20T14:00:14+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: Meet the Reporter Who Went Undercover in the Hermit Kingdom</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/5133080/3_Breakthrough_SukiKim.mp3" length="71396156" type="audio/mpeg"/>
<itunes:duration>00:29:44</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-meet-the-reporter-went-undercover-in-the-hermit-kingdom#66540</link>
<pubDate>Fri, 13 Jan 2017 14:00:24 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-meet-the-reporter-went-undercover-in-the-hermit-kingdom#66540</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-01-13T14:00:24+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>The Breakthrough: The $2 Drug Test</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/5094042/2_Breakthrough_DrugTest.mp3" length="63031678" type="audio/mpeg"/>
<itunes:duration>00:26:17</itunes:duration>
<link>https://www.propublica.org/podcast/the-breakthrough-the-2-dollar-drug-test#66543</link>
<pubDate>Fri, 06 Jan 2017 22:22:00 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/the-breakthrough-the-2-dollar-drug-test#66543</guid>
<description><![CDATA[
]]></description>
<dc:subject>Busted</dc:subject>
<dc:date>2017-01-06T22:22:00+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>Introducing Our New Podcast: The Breakthrough</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/5094043/1_TheBreakthrough_Promo.mp3" length="4910060" type="audio/mpeg"/>
<itunes:duration>00:02:03</itunes:duration>
<link>https://www.propublica.org/podcast/introducing-our-new-podcast-the-breakthrough#66546</link>
<pubDate>Fri, 06 Jan 2017 22:21:18 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/introducing-our-new-podcast-the-breakthrough#66546</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2017-01-06T22:21:18+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>Renewable Energy: An Exxon Investigation Given Second Life as Trump Taps Exec for Cabinet</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/5023664/52_NeelaBanerjee.mp3" length="34999184" type="audio/mpeg"/>
<itunes:duration>00:14:35</itunes:duration>
<link>https://www.propublica.org/podcast/renewable-energy-an-exxon-investigation-given-second-life-as-trump-taps-exe#66549</link>
<pubDate>Fri, 23 Dec 2016 21:57:02 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/renewable-energy-an-exxon-investigation-given-second-life-as-trump-taps-exe#66549</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2016-12-23T21:57:02+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
<item>
<title>How We Found a Pro-Trump Group Blew Past Campaign Finance Laws</title>
<itunes:author>ProPublica</itunes:author>
<itunes:summary/>
<enclosure url="http://tracking.feedpress.it/link/10581/4984992/51_Kate_Robert.mp3" length="34152423" type="audio/mpeg"/>
<itunes:duration>00:14:14</itunes:duration>
<link>https://www.propublica.org/podcast/how-we-found-a-pro-trump-group-blew-past-campaign-finance-laws#66552</link>
<pubDate>Fri, 16 Dec 2016 22:45:08 +0000</pubDate>
<guid isPermaLink="false">https://www.propublica.org/podcast/how-we-found-a-pro-trump-group-blew-past-campaign-finance-laws#66552</guid>
<description><![CDATA[
]]></description>
<dc:subject/>
<dc:date>2016-12-16T22:45:08+00:00</dc:date>
<dc:creator>ProPublica</dc:creator>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords>journalism, news, investigative journalism, interview, propublica, media, behind the scenes</itunes:keywords>
<itunes:subtitle>
</itunes:subtitle>
</item>
</channel>
</rss>
-20
View File
@@ -1,20 +0,0 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-downloader"
version = "0.1.0"
workspace = "../"
[dependencies]
error-chain = "0.11.0"
hyper = "0.11.7"
log = "0.3.8"
mime_guess = "1.8.2"
reqwest = "0.8.1"
tempdir = "0.3.5"
[dependencies.diesel]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.hammond-data]
path = "../hammond-data"
-223
View File
@@ -1,223 +0,0 @@
use reqwest;
use hyper::header::*;
use tempdir::TempDir;
use mime_guess;
use std::fs::{rename, DirBuilder, File};
use std::io::{BufWriter, Read, Write};
use std::path::Path;
use errors::*;
use hammond_data::{Episode, Podcast};
use hammond_data::xdg_dirs::{DL_DIR, HAMMOND_CACHE};
// TODO: Replace path that are of type &str with std::path.
// TODO: Have a convention/document absolute/relative paths, if they should end with / or not.
// Adapted from https://github.com/mattgathu/rget .
// I never wanted to write a custom downloader.
// Sorry to those who will have to work with that code.
// Would much rather use a crate,
// or bindings for a lib like youtube-dl(python),
// But cant seem to find one.
// TODO: Write unit-tests.
fn download_into(dir: &str, file_title: &str, url: &str) -> Result<String> {
info!("GET request to: {}", url);
let client = reqwest::Client::builder().referer(false).build()?;
let mut resp = client.get(url).send()?;
info!("Status Resp: {}", resp.status());
if !resp.status().is_success() {
bail!("Unexpected server response: {}", resp.status())
}
let headers = resp.headers().clone();
let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);
let ct_type = headers.get::<ContentType>();
ct_len.map(|x| info!("File Lenght: {}", x));
ct_type.map(|x| info!("Content Type: {}", x));
// This could be prettier.
// Determine the file extension from the http content-type header.
let ext = if let Some(t) = ct_type {
let mime = mime_guess::get_extensions(t.type_().as_ref(), t.subtype().as_ref());
if let Some(m) = mime {
if m.contains(&t.subtype().as_ref()) {
t.subtype().as_ref().to_string()
} else {
m.first().unwrap().to_string()
}
} else {
error!("Unkown mime type. {}", t);
"unkown".to_string()
}
} else {
error!("Unkown mime type.");
"unkown".to_string()
};
info!("Extension: {}", ext);
// Construct a temp file to save desired content.
let tempdir = TempDir::new_in(dir, "")?;
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
// Save requested content into the file.
save_io(&out_file, &mut resp, ct_len)?;
// Construct the desired path.
let target = format!("{}/{}.{}", dir, file_title, ext);
// Rename/move the tempfile into a permanent place upon success.
rename(out_file, &target)?;
info!("Downloading of {} completed succesfully.", &target);
Ok(target)
}
// TODO: Write unit-tests.
/// Handles the I/O of fetching a remote file and saving into a Buffer and A File.
fn save_io(file: &str, resp: &mut reqwest::Response, content_lenght: Option<u64>) -> Result<()> {
info!("Downloading into: {}", file);
let chunk_size = match content_lenght {
Some(x) => x as usize / 99,
None => 1024 as usize, // default chunk size
};
let mut writer = BufWriter::new(File::create(&file)?);
loop {
let mut buffer = vec![0; chunk_size];
let bcount = resp.read(&mut buffer[..])?;
buffer.truncate(bcount);
if !buffer.is_empty() {
writer.write_all(buffer.as_slice())?;
} else {
break;
}
}
Ok(())
}
pub fn get_download_folder(pd_title: &str) -> Result<String> {
// It might be better to make it a hash of the title
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
// Create the folder
DirBuilder::new().recursive(true).create(&download_fold)?;
Ok(download_fold)
}
// TODO: Refactor
pub fn get_episode(ep: &mut Episode, download_folder: &str) -> Result<()> {
// Check if its alrdy downloaded
if ep.local_uri().is_some() {
if Path::new(ep.local_uri().unwrap()).exists() {
return Ok(());
}
// If the path is not valid, then set it to None.
ep.set_local_uri(None);
ep.save()?;
};
let res = download_into(download_folder, ep.title().unwrap(), ep.uri());
if let Ok(path) = res {
// If download succedes set episode local_uri to dlpath.
ep.set_local_uri(Some(&path));
ep.save()?;
Ok(())
} else {
error!("Something whent wrong while downloading.");
Err(res.unwrap_err())
}
}
pub fn cache_image(pd: &Podcast) -> Option<String> {
let url = pd.image_uri()?.to_owned();
if url == "" {
return None;
}
let download_fold = format!(
"{}{}",
HAMMOND_CACHE.to_str().unwrap(),
pd.title().to_owned()
);
// Hacky way
// TODO: make it so it returns the first cover.* file encountered.
// Use glob instead
let png = format!("{}/cover.png", download_fold);
let jpg = format!("{}/cover.jpg", download_fold);
let jpe = format!("{}/cover.jpe", download_fold);
let jpeg = format!("{}/cover.jpeg", download_fold);
if Path::new(&png).exists() {
return Some(png);
} else if Path::new(&jpe).exists() {
return Some(jpe);
} else if Path::new(&jpg).exists() {
return Some(jpg);
} else if Path::new(&jpeg).exists() {
return Some(jpeg);
};
DirBuilder::new()
.recursive(true)
.create(&download_fold)
.unwrap();
let dlpath = download_into(&download_fold, "cover", &url);
if let Ok(path) = dlpath {
info!("Cached img into: {}", &path);
Some(path)
} else {
error!("Failed to get feed image.");
error!("Error: {}", dlpath.unwrap_err());
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::Source;
use hammond_data::feed::index;
use hammond_data::dbqueries;
use diesel::Identifiable;
use std::fs;
#[test]
fn test_get_dl_folder() {
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
assert_eq!(get_download_folder("foo").unwrap(), foo_);
let _ = fs::remove_dir_all(foo_);
}
#[test]
fn test_cache_image() {
let url = "http://www.newrustacean.com/feed.xml";
// Create and index a source
let source = Source::from_url(url).unwrap();
// Copy it's id
let sid = source.id().clone();
// Convert Source it into a Feed and index it
let feed = source.into_feed().unwrap();
index(vec![feed]);
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
let img_path = cache_image(&pd);
let foo_ = format!(
"{}{}/cover.png",
HAMMOND_CACHE.to_str().unwrap(),
pd.title()
);
assert_eq!(img_path, Some(foo_));
}
}
-13
View File
@@ -1,13 +0,0 @@
use diesel::result;
use reqwest;
use hammond_data;
use std::io;
error_chain! {
foreign_links {
ReqError(reqwest::Error);
IoError(io::Error);
DieselResultError(result::Error);
DataError(hammond_data::errors::Error);
}
}
-15
View File
@@ -1,15 +0,0 @@
#![recursion_limit = "1024"]
extern crate diesel;
#[macro_use]
extern crate error_chain;
extern crate hammond_data;
extern crate hyper;
#[macro_use]
extern crate log;
extern crate mime_guess;
extern crate reqwest;
extern crate tempdir;
pub mod downloader;
pub mod errors;
-35
View File
@@ -1,35 +0,0 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
build = "build.rs"
name = "hammond-gtk"
version = "0.1.0"
workspace = "../"
[dependencies]
dissolve = "0.2.2"
gdk = "0.7.0"
gdk-pixbuf = "0.3.0"
gio = "0.3.0"
glib = "0.4.0"
log = "0.3.8"
loggerv = "0.5.1"
open = "1.2.1"
rayon = "0.9.0"
[dependencies.diesel]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.diesel_codegen]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.gtk]
features = ["v3_22"]
version = "0.3.0"
[dependencies.hammond-data]
path = "../hammond-data"
[dependencies.hammond-downloader]
path = "../hammond-downloader"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 B

-202
View File
@@ -1,202 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="episode_box">
<property name="width_request">100</property>
<property name="height_request">25</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="play_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-play</property>
<property name="use_fallback">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="download_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-save</property>
<property name="use_fallback">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="delete_button">
<property name="name">delete_button</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-delete</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="mark_unplayed_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Mark episode as Unplayed.</property>
<property name="halign">end</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-undo</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="mark_played_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Mark episode as played.</property>
<property name="halign">end</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-apply</property>
</object>
</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>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
<property name="ellipsize">end</property>
<property name="lines">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expand_desc">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label_fill">True</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">100</property>
<property name="max_content_height">600</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkTextView" id="desc_text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_bottom">5</property>
<property name="editable">False</property>
<property name="wrap_mode">word-char</property>
<property name="cursor_visible">False</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Description:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</interface>
-204
View File
@@ -1,204 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface domain="gnome-music">
<requires lib="gtk+" version="3.12"/>
<object class="GtkBox" id="podcast_widget">
<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="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">32</property>
<property name="margin_right">32</property>
<property name="margin_start">32</property>
<property name="margin_end">32</property>
<property name="margin_top">64</property>
<property name="margin_bottom">32</property>
<child>
<object class="GtkBox">
<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>
<property name="spacing">15</property>
<child>
<object class="GtkImage" id="cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_start">1</property>
<property name="margin_end">1</property>
<property name="stock">gtk-missing-image</property>
<property name="use_fallback">True</property>
</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="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="width_request">50</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Foobar</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max_width_chars">28</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</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="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>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Unsubrscribe from this Podcast.
Warn: This will delete downloaded content associated with this Podcast.</property>
<property name="stock">gtk-delete</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="mark_all_played_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Mark all episodes as Played.</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-apply</property>
</object>
</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="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">200</property>
<property name="max_content_width">200</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkTextView" id="desc_text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word-char</property>
<property name="cursor_visible">False</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</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="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<child>
<object class="GtkViewport" id="view">
<property name="width_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>
@@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<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="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child type="overlay">
<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="stock">gtk-missing-image</property>
<property name="use_fallback">True</property>
<property name="icon_size">6</property>
</object>
</child>
<child type="overlay">
<object class="GtkImage" id="banner">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="stock">gtk-missing-image</property>
</object>
<packing>
<property name="index">1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkLabel" id="banner_label">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="margin_right">40</property>
<property name="margin_top">38</property>
<property name="label" translatable="yes">Num</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
<property name="track_visited_links">False</property>
<property name="xalign">1</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="index">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="pd_title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label" translatable="yes">label</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="max_width_chars">33</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="fb_parent">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</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">baseline</property>
<property name="valign">start</property>
<property name="homogeneous">True</property>
<property name="column_spacing">5</property>
<property name="row_spacing">2</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>
@@ -1,23 +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>Daniel García Moreno</developer_name>
<summary>Gtk+ Matrix.org client</summary>
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<description>
Hammond is a Fast, Safe and Reliable Gtk+ Podcast client written in Rust
</description>
<screenshots>
<screenshot>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/assets/podcasts_view.png</image>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/assets/podcast_widget.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.1.1" date="2017-11-13"/>
</releases>
<update_contact>jordanpetridis@protonmail.com</update_contact>
</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
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/hammond/">
<file>banner.png</file>
<file preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
<file preprocess="xml-stripblanks">gtk/podcast_widget.ui</file>
<file preprocess="xml-stripblanks">gtk/empty_view.ui</file>
<file preprocess="xml-stripblanks">gtk/podcasts_view.ui</file>
<file preprocess="xml-stripblanks">gtk/podcasts_child.ui</file>
<file preprocess="xml-stripblanks">gtk/headerbar.ui</file>
</gresource>
</gresources>
-67
View File
@@ -1,67 +0,0 @@
use gtk;
use gtk::prelude::*;
use hammond_data::Source;
use hammond_data::utils::url_cleaner;
use podcasts_view::update_podcasts_view;
use utils;
pub fn get_headerbar(stack: &gtk::Stack) -> gtk::HeaderBar {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let header: gtk::HeaderBar = builder.get_object("headerbar1").unwrap();
let home_button: gtk::Button = builder.get_object("homebutton").unwrap();
let refresh_button: gtk::Button = builder.get_object("refbutton").unwrap();
let add_toggle_button: gtk::MenuButton = builder.get_object("add-toggle-button").unwrap();
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();
// TODO: check if url exists in the db and lock the button
new_url.connect_changed(move |url| {
println!("{:?}", url.get_text());
});
add_button.connect_clicked(clone!(stack, add_popover => move |_| {
let url = new_url.get_text().unwrap_or_default();
let url = url_cleaner(&url);
on_add_bttn_clicked(&stack, &url);
// TODO: lock the button instead of hiding and add notification of feed added.
// TODO: map the spinner
add_popover.hide();
}));
add_popover.hide();
add_toggle_button.set_popover(&add_popover);
// TODO: make it a back arrow button, that will hide when appropriate,
// and add a StackSwitcher when more views are added.
home_button.connect_clicked(clone!(stack => move |_| {
let vis = stack.get_visible_child_name().unwrap();
stack.set_visible_child_name("fb_parent");
if vis != "pdw" {
update_podcasts_view(&stack);
}
}));
// FIXME: There appears to be a memmory leak here.
refresh_button.connect_clicked(clone!(stack => move |_| {
utils::refresh_feed(&stack, None, None);
}));
header
}
fn on_add_bttn_clicked(stack: &gtk::Stack, url: &str) {
let source = Source::from_url(url);
if let Ok(s) = source {
info!("{:?} feed added", url);
// update the db
utils::refresh_feed(stack, Some(vec![s]), None);
} else {
error!("Feed probably already exists.");
error!("Error: {:?}", source.unwrap_err());
}
}
-125
View File
@@ -1,125 +0,0 @@
extern crate gdk;
extern crate gdk_pixbuf;
extern crate gio;
extern crate glib;
extern crate gtk;
extern crate diesel;
extern crate dissolve;
extern crate hammond_data;
extern crate hammond_downloader;
#[macro_use]
extern crate log;
extern crate loggerv;
extern crate open;
// extern crate rayon;
// use rayon::prelude::*;
use log::LogLevel;
use hammond_data::utils::checkup;
use gtk::prelude::*;
use gio::{ActionMapExt, ApplicationExt, MenuExt, SimpleActionExt};
// 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 views;
mod widgets;
mod headerbar;
mod utils;
mod static_resource;
use views::podcasts_view;
/*
THIS IS STILL A PROTOTYPE.
*/
fn build_ui(app: &gtk::Application) {
let menu = gio::Menu::new();
menu.append("Quit", "app.quit");
menu.append("Checkup", "app.check");
app.set_app_menu(&menu);
// Get the main window
let window = gtk::ApplicationWindow::new(app);
window.set_default_size(1150, 650);
// Setup the Stack that will manage the switch between podcasts_view and podcast_widget.
let stack = podcasts_view::setup_stack();
window.add(&stack);
window.connect_delete_event(|w, _| {
w.destroy();
Inhibit(false)
});
// Setup quit in the app menu since default is overwritten.
let quit = gio::SimpleAction::new("quit", None);
let window2 = window.clone();
quit.connect_activate(move |_, _| {
window2.destroy();
});
app.add_action(&quit);
// Setup the checkup in the app menu.
let check = gio::SimpleAction::new("check", None);
check.connect_activate(move |_, _| {
let _ = checkup();
});
app.add_action(&check);
// queue a db update 1 minute after the startup.
gtk::idle_add(clone!(stack => move || {
utils::refresh_feed(&stack, None, Some(60));
glib::Continue(false)
}));
gtk::idle_add(move || {
let _ = checkup();
glib::Continue(false)
});
// Get the headerbar
let header = headerbar::get_headerbar(&stack);
window.set_titlebar(&header);
window.show_all();
window.activate();
app.connect_activate(move |_| ());
}
fn main() {
use gio::ApplicationExtManual;
// TODO: make the the logger a cli -vv option
loggerv::init_with_level(LogLevel::Info).unwrap();
static_resource::init().expect("Something went wrong with the resource file initialization.");
let application = gtk::Application::new("org.gnome.Hammond", gio::ApplicationFlags::empty())
.expect("Initialization failed...");
application.connect_startup(move |app| {
build_ui(app);
});
// application.run(&[]);
ApplicationExtManual::run(&application, &[]);
}
-63
View File
@@ -1,63 +0,0 @@
use glib;
use gtk;
use hammond_data::feed;
use hammond_data::Source;
use std::{thread, time};
use std::cell::RefCell;
use std::sync::mpsc::{channel, Receiver};
use views::podcasts_view;
type Foo = RefCell<Option<(gtk::Stack, Receiver<bool>)>>;
// Create a thread local storage that will store the arguments to be transfered.
thread_local!(static GLOBAL: Foo = RefCell::new(None));
/// Update the rss feed(s) originating from `Source`.
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
/// `delay` represents the desired time in seconds for the thread to sleep before executing.
/// When It's done,it queues up a `podcast_view` refresh.
pub fn refresh_feed(stack: &gtk::Stack, source: Option<Vec<Source>>, delay: Option<u64>) {
// Create a async channel.
let (sender, receiver) = channel();
// Pass the desired arguments into the Local Thread Storage.
GLOBAL.with(clone!(stack => move |global| {
*global.borrow_mut() = Some((stack, receiver));
}));
thread::spawn(move || {
if let Some(s) = delay {
let t = time::Duration::from_secs(s);
thread::sleep(t);
}
let feeds = {
if let Some(vec) = source {
Ok(feed::fetch(vec))
} else {
feed::fetch_all()
}
};
if let Ok(x) = feeds {
feed::index(x);
sender.send(true).expect("Couldn't send data to channel");;
glib::idle_add(refresh_podcasts_view);
};
});
}
fn refresh_podcasts_view() -> glib::Continue {
GLOBAL.with(|global| {
if let Some((ref stack, ref reciever)) = *global.borrow() {
if reciever.try_recv().is_ok() {
podcasts_view::update_podcasts_view(stack);
}
}
});
glib::Continue(false)
}
-1
View File
@@ -1 +0,0 @@
pub mod podcasts_view;
-152
View File
@@ -1,152 +0,0 @@
use gtk;
use gtk::prelude::*;
use gdk_pixbuf::Pixbuf;
use diesel::associations::Identifiable;
use hammond_data::dbqueries;
use hammond_data::Podcast;
use widgets::podcast::*;
fn setup_empty_view(stack: &gtk::Stack) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui");
let view: gtk::Box = builder.get_object("empty_view").unwrap();
stack.add_named(&view, "empty");
}
fn show_empty_view(stack: &gtk::Stack) {
stack.set_visible_child_name("empty");
info!("Empty view.");
}
fn populate_flowbox(flowbox: &gtk::FlowBox) {
let podcasts = dbqueries::get_podcasts();
if let Ok(pds) = podcasts {
pds.iter().for_each(|parent| {
let f = create_flowbox_child(parent);
flowbox.add(&f);
});
flowbox.show_all();
}
}
fn create_flowbox_child(pd: &Podcast) -> gtk::FlowBoxChild {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcasts_child.ui");
// Copy of gnome-music AlbumWidget
let box_: gtk::Box = builder.get_object("fb_child").unwrap();
let pd_title: gtk::Label = builder.get_object("pd_title").unwrap();
let pd_cover: gtk::Image = builder.get_object("pd_cover").unwrap();
let banner: gtk::Image = builder.get_object("banner").unwrap();
let banner_title: gtk::Label = builder.get_object("banner_label").unwrap();
pd_title.set_text(pd.title());
let cover = get_pixbuf_from_path(pd);
if let Some(img) = cover {
pd_cover.set_from_pixbuf(&img);
};
configure_banner(pd, &banner, &banner_title);
let fbc = gtk::FlowBoxChild::new();
// There's probably a better way to store the id somewhere.
// fbc.set_name(&pd.id().to_string());
WidgetExt::set_name(&fbc, &pd.id().to_string());
fbc.add(&box_);
fbc
}
fn configure_banner(pd: &Podcast, banner: &gtk::Image, banner_title: &gtk::Label) {
let bann = Pixbuf::new_from_resource_at_scale("/org/gnome/hammond/banner.png", 256, 256, true);
if let Ok(b) = bann {
banner.set_from_pixbuf(&b);
let new_episodes = dbqueries::get_pd_unplayed_episodes(pd);
if let Ok(n) = new_episodes {
if !n.is_empty() {
banner_title.set_text(&n.len().to_string());
banner.show();
banner_title.show();
}
}
}
}
fn on_flowbox_child_activate(stack: &gtk::Stack, parent: &Podcast) {
let old = stack.get_child_by_name("pdw").unwrap();
let pdw = podcast_widget(stack, parent);
stack.remove(&old);
stack.add_named(&pdw, "pdw");
stack.set_visible_child(&pdw);
// aggresive memory cleanup
// probably not needed
old.destroy();
}
fn setup_podcasts_flowbox(stack: &gtk::Stack) -> gtk::FlowBox {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcasts_view.ui");
let fb_parent: gtk::Box = builder.get_object("fb_parent").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
init_flowbox(stack, &flowbox);
stack.add_named(&fb_parent, "fb_parent");
if flowbox.get_children().is_empty() {
show_empty_view(stack);
} else {
stack.set_visible_child(&fb_parent);
};
flowbox
}
pub fn setup_stack() -> gtk::Stack {
let stack = gtk::Stack::new();
stack.set_transition_type(gtk::StackTransitionType::SlideLeftRight);
setup_empty_view(&stack);
setup_podcast_widget(&stack);
setup_podcasts_flowbox(&stack);
stack
}
pub fn update_podcasts_view(stack: &gtk::Stack) {
let vis = stack.get_visible_child_name().unwrap();
let old = stack.get_child_by_name("fb_parent").unwrap();
stack.remove(&old);
let flowbox = setup_podcasts_flowbox(stack);
if vis == "empty" && !flowbox.get_children().is_empty() {
stack.set_visible_child_name("fb_parent");
} else if vis == "fb_parent" && flowbox.get_children().is_empty() {
stack.set_visible_child_name("empty");
} else {
// preserve the visible widget
stack.set_visible_child_name(&vis);
};
// aggresive memory cleanup
// probably not needed
old.destroy();
}
fn init_flowbox(stack: &gtk::Stack, flowbox: &gtk::FlowBox) {
use gtk::WidgetExt;
// TODO: handle unwraps.
flowbox.connect_child_activated(clone!(stack => move |_, child| {
// This is such an ugly hack...
// let id = child.get_name().unwrap().parse::<i32>().unwrap();
let id = WidgetExt::get_name(child).unwrap().parse::<i32>().unwrap();
let parent = dbqueries::get_podcast_from_id(id).unwrap();
on_flowbox_child_activate(&stack, &parent);
}));
// Populate the flowbox with the Podcasts.
populate_flowbox(flowbox);
}
-204
View File
@@ -1,204 +0,0 @@
use open;
use hammond_data::dbqueries;
use hammond_data::{Episode, Podcast};
use hammond_downloader::downloader;
use hammond_data::utils::*;
use hammond_data::errors::*;
use dissolve::strip_html_tags;
use diesel::associations::Identifiable;
use std::thread;
use std::cell::RefCell;
use std::sync::mpsc::{channel, Receiver};
use std::path::Path;
use glib;
use gtk;
use gtk::prelude::*;
use gtk::{ContainerExt, TextBufferExt};
type Foo = RefCell<Option<(gtk::Button, gtk::Button, gtk::Button, Receiver<bool>)>>;
thread_local!(static GLOBAL: Foo = RefCell::new(None));
fn epidose_widget(episode: &mut Episode, pd_title: &str) -> gtk::Box {
// This is just a prototype and will be reworked probably.
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
let ep: gtk::Box = builder.get_object("episode_box").unwrap();
let download_button: gtk::Button = builder.get_object("download_button").unwrap();
let play_button: gtk::Button = builder.get_object("play_button").unwrap();
let delete_button: gtk::Button = builder.get_object("delete_button").unwrap();
let played_button: gtk::Button = builder.get_object("mark_played_button").unwrap();
let unplayed_button: gtk::Button = builder.get_object("mark_unplayed_button").unwrap();
let title_label: gtk::Label = builder.get_object("title_label").unwrap();
// let desc_label: gtk::Label = builder.get_object("desc_label").unwrap();
let expander: gtk::Expander = builder.get_object("expand_desc").unwrap();
let desc_text_view: gtk::TextView = builder.get_object("desc_text_view").unwrap();
title_label.set_xalign(0.0);
if let Some(t) = episode.title() {
title_label.set_text(t);
}
if episode.description().is_some() {
let d = episode.description().unwrap().to_owned();
expander.connect_activate(move |_| {
let plain_text = strip_html_tags(&d).join(" ");
// TODO: handle unwrap
let buff = desc_text_view.get_buffer().unwrap();
buff.set_text(plain_text.trim());
});
}
if episode.played().is_some() {
unplayed_button.show();
played_button.hide();
}
// Show or hide the play/delete/download buttons upon widget initialization.
let local_uri = episode.local_uri();
if local_uri.is_some() && Path::new(local_uri.unwrap()).exists() {
download_button.hide();
play_button.show();
delete_button.show();
}
play_button.connect_clicked(clone!(episode, played_button, unplayed_button => move |_| {
let mut episode = episode.clone();
on_play_bttn_clicked(*episode.id());
let _ = episode.set_played_now();
played_button.hide();
unplayed_button.show();
}));
delete_button.connect_clicked(clone!(episode, play_button, download_button => move |del| {
on_delete_bttn_clicked(*episode.id());
del.hide();
play_button.hide();
download_button.show();
}));
played_button.connect_clicked(clone!(episode, unplayed_button => move |played| {
let mut episode = episode.clone();
let _ = episode.set_played_now();
played.hide();
unplayed_button.show();
}));
unplayed_button.connect_clicked(clone!(episode, played_button => move |un| {
let mut episode = episode.clone();
episode.set_played(None);
let _ = episode.save();
un.hide();
played_button.show();
}));
let pd_title = pd_title.to_owned();
download_button.connect_clicked(clone!(play_button, delete_button, episode => move |dl| {
on_download_clicked(
&pd_title,
&mut episode.clone(),
dl,
&play_button,
&delete_button,
);
}));
ep
}
// TODO: show notification when dl is finished.
fn on_download_clicked(
pd_title: &str,
ep: &mut Episode,
download_bttn: &gtk::Button,
play_bttn: &gtk::Button,
del_bttn: &gtk::Button,
) {
// Create a async channel.
let (sender, receiver) = channel();
// Pass the desired arguments into the Local Thread Storage.
GLOBAL.with(clone!(download_bttn, play_bttn, del_bttn => move |global| {
*global.borrow_mut() = Some((download_bttn, play_bttn, del_bttn, receiver));
}));
let pd_title = pd_title.to_owned();
let mut ep = ep.clone();
thread::spawn(move || {
let download_fold = downloader::get_download_folder(&pd_title).unwrap();
let e = downloader::get_episode(&mut ep, download_fold.as_str());
if let Err(err) = e {
error!("Error while trying to download: {}", ep.uri());
error!("Error: {}", err);
};
sender.send(true).expect("Couldn't send data to channel");;
glib::idle_add(receive);
});
}
fn on_play_bttn_clicked(episode_id: i32) {
let local_uri = dbqueries::get_episode_local_uri_from_id(episode_id).unwrap();
if let Some(uri) = local_uri {
if Path::new(&uri).exists() {
info!("Opening {}", uri);
let e = open::that(&uri);
if let Err(err) = e {
error!("Error while trying to open file: {}", uri);
error!("Error: {}", err);
};
}
} else {
error!(
"Something went wrong evaluating the following path: {:?}",
local_uri
);
}
}
fn on_delete_bttn_clicked(episode_id: i32) {
let mut ep = dbqueries::get_episode_from_id(episode_id).unwrap();
let e = delete_local_content(&mut ep);
if let Err(err) = e {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("Error: {}", err);
};
}
fn receive() -> glib::Continue {
GLOBAL.with(|global| {
if let Some((ref download_bttn, ref play_bttn, ref del_bttn, ref reciever)) =
*global.borrow()
{
if reciever.try_recv().is_ok() {
download_bttn.hide();
play_bttn.show();
del_bttn.show();
}
}
});
glib::Continue(false)
}
pub fn episodes_listbox(pd: &Podcast) -> Result<gtk::ListBox> {
let episodes = dbqueries::get_pd_episodes(pd)?;
let list = gtk::ListBox::new();
episodes.into_iter().for_each(|mut ep| {
let w = epidose_widget(&mut ep, pd.title());
list.add(&w)
});
list.set_vexpand(false);
list.set_hexpand(false);
list.set_visible(true);
list.set_selection_mode(gtk::SelectionMode::None);
Ok(list)
}
-2
View File
@@ -1,2 +0,0 @@
pub mod podcast;
pub mod episode;
-144
View File
@@ -1,144 +0,0 @@
use gtk::prelude::*;
use gtk;
use gdk_pixbuf::Pixbuf;
use std::fs;
use hammond_data::dbqueries;
use hammond_data::Podcast;
use hammond_downloader::downloader;
use widgets::episode::episodes_listbox;
use podcasts_view::update_podcasts_view;
pub fn podcast_widget(stack: &gtk::Stack, pd: &Podcast) -> gtk::Box {
// Adapted from gnome-music AlbumWidget
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcast_widget.ui");
let pd_widget: gtk::Box = builder.get_object("podcast_widget").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
let title_label: gtk::Label = builder.get_object("title_label").unwrap();
let desc_text_view: gtk::TextView = builder.get_object("desc_text_view").unwrap();
let view: gtk::Viewport = builder.get_object("view").unwrap();
let unsub_button: gtk::Button = builder.get_object("unsub_button").unwrap();
let played_button: gtk::Button = builder.get_object("mark_all_played_button").unwrap();
// TODO: should spawn a thread to avoid locking the UI probably.
unsub_button.connect_clicked(clone!(stack, pd => move |bttn| {
on_unsub_button_clicked(&stack, &pd, bttn);
}));
title_label.set_text(pd.title());
let listbox = episodes_listbox(pd);
if let Ok(l) = listbox {
view.add(&l);
}
{
let buff = desc_text_view.get_buffer().unwrap();
buff.set_text(pd.description());
}
let img = get_pixbuf_from_path(pd);
if let Some(i) = img {
cover.set_from_pixbuf(&i);
}
played_button.connect_clicked(clone!(stack, pd => move |_| {
on_played_button_clicked(&stack, &pd);
}));
show_played_button(pd, &played_button);
pd_widget
}
fn on_unsub_button_clicked(stack: &gtk::Stack, pd: &Podcast, unsub_button: &gtk::Button) {
let res = dbqueries::remove_feed(pd);
if res.is_ok() {
info!("{} was removed succesfully.", pd.title());
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.hide();
let dl_fold = downloader::get_download_folder(pd.title());
if let Ok(fold) = dl_fold {
let res3 = fs::remove_dir_all(&fold);
if res3.is_ok() {
info!("All the content at, {} was removed succesfully", &fold);
}
};
}
stack.set_visible_child_name("fb_parent");
update_podcasts_view(stack);
}
fn on_played_button_clicked(stack: &gtk::Stack, pd: &Podcast) {
let _ = dbqueries::update_none_to_played_now(pd);
update_podcast_widget(stack, pd);
}
fn show_played_button(pd: &Podcast, played_button: &gtk::Button) {
let new_episodes = dbqueries::get_pd_unplayed_episodes(pd);
if let Ok(n) = new_episodes {
if !n.is_empty() {
played_button.show()
}
}
}
pub fn get_pixbuf_from_path(pd: &Podcast) -> Option<Pixbuf> {
let img_path = downloader::cache_image(pd);
if let Some(i) = img_path {
Pixbuf::new_from_file_at_scale(&i, 256, 256, true).ok()
} else {
None
}
}
pub fn setup_podcast_widget(stack: &gtk::Stack) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcast_widget.ui");
let pd_widget: gtk::Box = builder.get_object("podcast_widget").unwrap();
stack.add_named(&pd_widget, "pdw");
}
pub fn update_podcast_widget(stack: &gtk::Stack, pd: &Podcast) {
let old = stack.get_child_by_name("pdw").unwrap();
let pdw = podcast_widget(stack, pd);
let vis = stack.get_visible_child_name().unwrap();
stack.remove(&old);
stack.add_named(&pdw, "pdw");
stack.set_visible_child_name(&vis);
old.destroy();
}
#[cfg(test)]
mod tests {
use hammond_data::Source;
use hammond_data::feed::index;
use diesel::Identifiable;
use super::*;
#[test]
fn test_get_pixbuf_from_path() {
let url = "http://www.newrustacean.com/feed.xml";
// Create and index a source
let source = Source::from_url(url).unwrap();
// Copy it's id
let sid = source.id().clone();
// Convert Source it into a Feed and index it
let feed = source.into_feed().unwrap();
index(vec![feed]);
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
let pxbuf = get_pixbuf_from_path(&pd);
assert!(pxbuf.is_some());
}
}
+20 -16
View File
@@ -2,22 +2,26 @@
# https://gitlab.gnome.org/danigm/fractal/blob/6e2911f9d2353c99a18a6c19fab7f903c4bbb431/meson.build
project(
'hammond', 'rust',
version: '0.2.0',
'gnome-podcasts', 'rust',
version: '0.4.3',
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()
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()
hammond_prefix = get_option('prefix')
hammond_bindir = join_paths(hammond_prefix, get_option('bindir'))
podcasts_prefix = get_option('prefix')
podcasts_bindir = join_paths(podcasts_prefix, get_option('bindir'))
install_data('hammond-gtk/resources/org.gnome.Hammond.desktop', install_dir : get_option('datadir') + '/applications')
install_data('hammond-gtk/resources/org.gnome.Hammond.appdata.xml', install_dir : get_option('datadir') + '/appdata')
podcasts_conf = configuration_data()
podcasts_conf.set('BINDIR', podcasts_bindir)
datadir = get_option('datadir')
icondir = join_paths(datadir, 'icons')
subdir('podcasts-gtk/resources')
cargo = find_program('cargo', required: false)
gresource = find_program('glib-compile-resources', required: false)
@@ -26,12 +30,12 @@ cargo_script = find_program('scripts/cargo.sh')
cargo_release = custom_target('cargo-build',
build_by_default: true,
output: ['hammond'],
build_always: true,
output: ['gnome-podcasts'],
install: true,
install_dir: hammond_bindir,
install_dir: podcasts_bindir,
command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
run_target('release', command: ['scripts/release.sh',
meson.project_name() + '-' + hammond_version
],
depends: [cargo_release])
meson.project_name() + '-' + podcasts_version
])
-38
View File
@@ -1,38 +0,0 @@
{
"app-id": "org.gnome.Hammond",
"runtime": "org.gnome.Platform",
"runtime-version": "3.26",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "hammond",
"finish-args": [
"--share=network",
"--socket=x11",
"--socket=wayland",
"--talk-name=org.freedesktop.Notifications"
],
"build-options": {
"env": {
"CARGO_HOME": "/run/build/Hammond/cargo"
},
"build-args": [ "--share=network" ]
},
"modules": [
{
"name": "Hammond",
"buildsystem": "simple",
"build-commands": [
"source /usr/lib/sdk/rust-stable/enable.sh && ./configure --prefix=/app && make && make install"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/alatiera/Hammond.git",
"branch": "master"
}
]
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"app-id" : "org.gnome.Podcasts",
"runtime" : "org.gnome.Platform",
"runtime-version" : "3.28",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command" : "gnome-podcasts",
"tags" : [
"nightly"
],
"desktop-file-name-prefix" : "(Nightly) ",
"finish-args" : [
"--filesystem=xdg-run/dconf",
"--filesystem=~/.config/dconf:ro",
"--talk-name=ca.desrt.dconf",
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
"--share=network",
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Desktop"
],
"build-options" : {
"append-path" : "/usr/lib/sdk/rust-stable/bin",
"build-args" : [
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/Podcasts/cargo",
"RUST_BACKTRACE" : "1",
"DEBUG" : "true"
}
},
"modules" : [
{
"name" : "gnome-podcasts",
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/World/podcasts.git",
"branch" : "master"
}
]
}
]
}
+46
View File
@@ -0,0 +1,46 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "podcasts-data"
version = "0.1.0"
workspace = "../"
[dependencies]
ammonia = "1.2.0"
chrono = "0.4.4"
derive_builder = "0.5.1"
lazy_static = "1.0.2"
log = "0.4.3"
rayon = "1.0.2"
rayon-futures = "0.1.0"
rfc822_sanitizer = "0.3.3"
rss = "1.5.0"
url = "1.7.1"
xdg = "2.1.0"
xml-rs = "0.8.0"
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"
[dependencies.diesel]
features = ["sqlite", "r2d2"]
version = "1.3.2"
[dependencies.diesel_migrations]
features = ["sqlite"]
version = "1.3.0"
[dev-dependencies]
rand = "0.5.4"
tempdir = "0.3.7"
criterion = "0.2.4"
pretty_assertions = "0.5.1"
maplit = "1.0.1"
[[bench]]
name = "bench"
harness = false
+104
View File
@@ -0,0 +1,104 @@
#![allow(unused)]
#[macro_use]
extern crate criterion;
use criterion::Criterion;
// extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate podcasts_data;
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 podcasts_data::database::truncate_db;
use podcasts_data::pipeline;
use podcasts_data::FeedBuilder;
use podcasts_data::Source;
// use podcasts_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);
+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"
@@ -1,8 +1,3 @@
-- Till version 0.2 is released the plan is to edited directly and dont expect
-- any kind of non-braking changes.
-- After there is a stable prototype, Only diesel migrations will be used
-- in order to change the db schema.
CREATE TABLE `source` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`uri` TEXT NOT NULL UNIQUE,
@@ -0,0 +1,23 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
title TEXT,
uri TEXT NOT NULL UNIQUE,
local_uri TEXT,
description TEXT,
published_date TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
guid TEXT,
played INTEGER,
favorite INTEGER NOT NULL DEFAULT 0,
archive INTEGER NOT NULL DEFAULT 0,
podcast_id INTEGER NOT NULL
);
INSERT INTO episode (title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,22 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
published_date TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length 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, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,22 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
published_date TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length 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, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,23 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
published_date 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, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,24 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
published_date 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, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, epoch, length, duration, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,23 @@
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, favorite, archive, podcast_id)
SELECT title, uri, local_uri, description, epoch, length, duration, guid, played, favorite, archive, podcast_id
FROM old_table;
Drop table old_table;
@@ -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;
+77
View File
@@ -0,0 +1,77 @@
//! Database Setup. This is only public to help with some unit tests.
// Diesel embed_migrations! triggers the lint
#![allow(unused_imports)]
use diesel::prelude::*;
use diesel::r2d2;
use diesel::r2d2::ConnectionManager;
use std::io;
use std::path::PathBuf;
use errors::DataError;
#[cfg(not(test))]
use xdg_dirs;
type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
embed_migrations!("migrations/");
lazy_static! {
static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap());
}
#[cfg(not(test))]
lazy_static! {
static ref DB_PATH: PathBuf = xdg_dirs::PODCASTS_XDG
.place_data_file("podcasts.db")
.unwrap();
}
#[cfg(test)]
extern crate tempdir;
#[cfg(test)]
lazy_static! {
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`.
pub(crate) fn connection() -> Pool {
POOL.clone()
}
fn init_pool(db_path: &str) -> Pool {
let manager = ConnectionManager::<SqliteConnection>::new(db_path);
let pool = r2d2::Pool::builder()
.max_size(1)
.build(manager)
.expect("Failed to create pool.");
{
let db = pool.get().expect("Failed to initialize pool.");
run_migration_on(&*db).expect("Failed to run migrations during init.");
}
info!("Database pool initialized.");
pool
}
fn run_migration_on(connection: &SqliteConnection) -> Result<(), DataError> {
info!("Running DB Migrations...");
// embedded_migrations::run(connection)?;
embedded_migrations::run_with_output(connection, &mut io::stdout()).map_err(From::from)
}
/// Reset the database into a clean state.
// Test share a Temp file db.
#[allow(dead_code)]
pub fn truncate_db() -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
con.execute("DELETE FROM episodes")?;
con.execute("DELETE FROM shows")?;
con.execute("DELETE FROM source")?;
Ok(())
}
+441
View File
@@ -0,0 +1,441 @@
//! Random CRUD helper functions.
use chrono::prelude::*;
use diesel::prelude::*;
use diesel;
use diesel::dsl::exists;
use diesel::query_builder::AsQuery;
use diesel::select;
use database::connection;
use errors::DataError;
use models::*;
pub fn get_sources() -> Result<Vec<Source>, DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
source
.order((http_etag.asc(), last_modified.asc()))
.load::<Source>(&con)
.map_err(From::from)
}
pub fn get_podcasts() -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
shows
.order(title.asc())
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
shows
.order(title.asc())
.filter(id.ne_all(filter_ids))
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.order(epoch.desc())
.load::<Episode>(&con)
.map_err(From::from)
}
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.select((rowid, local_uri, played))
.filter(local_uri.is_not_null())
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
// pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
// use schema::episodes::dsl::*;
// let db = connection();
// let con = db.get()?;
// episodes
// .filter(played.is_not_null())
// .load::<Episode>(&con)
// .map_err(From::from)
// }
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.select((rowid, local_uri, played))
.filter(played.is_not_null())
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
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<EpisodeWidgetModel, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.select((
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::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.filter(rowid.eq(ep_id))
.select(local_uri)
.get_result::<Option<String>>(&con)
.map_err(From::from)
}
pub fn get_episodes_widgets_filter_limit(
filter_ids: &[i32],
limit: u32,
) -> 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, show_id,
);
episodes
.select(columns)
.order(epoch.desc())
.filter(show_id.ne_all(filter_ids))
.limit(i64::from(limit))
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_podcast_from_id(pid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
shows
.filter(id.eq(pid))
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
shows
.select((id, title, image_uri))
.filter(id.eq(pid))
.get_result::<ShowCoverModel>(&con)
.map_err(From::from)
}
pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
Episode::belonging_to(parent)
.order(epoch.desc())
.load::<Episode>(&con)
.map_err(From::from)
}
pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
let db = connection();
let con = db.get()?;
Episode::belonging_to(parent)
.count()
.get_result(&con)
.map_err(From::from)
}
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, show_id,
);
episodes
.select(columns)
.filter(show_id.eq(parent.id()))
.order(epoch.desc())
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
Episode::belonging_to(parent)
.filter(played.is_null())
.order(epoch.desc())
.load::<Episode>(&con)
.map_err(From::from)
}
// 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()?;
// Episode::belonging_to(parent)
// .order(epoch.desc())
// .limit(i64::from(limit))
// .load::<Episode>(&con)
// .map_err(From::from)
// }
pub fn get_source_from_uri(uri_: &str) -> Result<Source, DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
source
.filter(uri.eq(uri_))
.get_result::<Source>(&con)
.map_err(From::from)
}
pub fn get_source_from_id(id_: i32) -> Result<Source, DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
source
.filter(id.eq(id_))
.get_result::<Source>(&con)
.map_err(From::from)
}
pub fn get_podcast_from_source_id(sid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
shows
.filter(source_id.eq(sid))
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.filter(title.eq(title_))
.filter(show_id.eq(pid))
.get_result::<Episode>(&con)
.map_err(From::from)
}
pub(crate) fn get_episode_minimal_from_pk(
title_: &str,
pid: i32,
) -> Result<EpisodeMinimal, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episodes
.select((rowid, title, uri, epoch, length, duration, guid, show_id))
.filter(title.eq(title_))
.filter(show_id.eq(pid))
.get_result::<EpisodeMinimal>(&con)
.map_err(From::from)
}
pub(crate) fn remove_feed(pd: &Show) -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
con.transaction(|| {
delete_source(&con, pd.source_id())?;
delete_podcast(&con, pd.id())?;
delete_podcast_episodes(&con, pd.id())?;
info!("Feed removed from the Database.");
Ok(())
})
}
fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
use schema::source::dsl::*;
diesel::delete(source.filter(id.eq(source_id))).execute(con)
}
fn delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize> {
use schema::shows::dsl::*;
diesel::delete(shows.filter(id.eq(show_id))).execute(con)
}
fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
use schema::episodes::dsl::*;
diesel::delete(episodes.filter(show_id.eq(parent_id))).execute(con)
}
pub fn source_exists(url: &str) -> Result<bool, DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(source.filter(uri.eq(url))))
.get_result(&con)
.map_err(From::from)
}
pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
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, show_id_: i32) -> Result<bool, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episodes.filter(show_id.eq(show_id_)).filter(title.eq(title_))))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `episodes table contains any rows
///
/// Return true if `episodes` table is populated.
pub fn is_episodes_populated() -> Result<bool, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episodes.as_query()))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `shows` table contains any rows
///
/// Return true if `shows table is populated.
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(shows.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::episodes::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(episodes)
.values(eps)
.execute(&*con)
.map_err(From::from)
.map(|_| ())
}
pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
let epoch_now = Utc::now().timestamp() as i32;
con.transaction(|| {
diesel::update(Episode::belonging_to(parent).filter(played.is_null()))
.set(played.eq(Some(epoch_now)))
.execute(&con)
.map_err(From::from)
})
}
#[cfg(test)]
mod tests {
use super::*;
use database::*;
use pipeline;
#[test]
fn test_update_none_to_played_now() {
truncate_db().unwrap();
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let id = source.id();
pipeline::run(vec![source], true).unwrap();
let pd = get_podcast_from_source_id(id).unwrap();
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
assert_ne!(eps_num, 0);
update_none_to_played_now(&pd).unwrap();
let eps_num2 = get_pd_unplayed_episodes(&pd).unwrap().len();
assert_eq!(eps_num2, 0);
}
}
+139
View File
@@ -0,0 +1,139 @@
use diesel;
use diesel::r2d2;
use diesel_migrations::RunMigrationsError;
use hyper;
use native_tls;
use rss;
use url;
use xml;
use std::io;
use models::Source;
#[fail(
display = "Request to {} returned {}. Context: {}",
url,
status_code,
context
)]
#[derive(Fail, Debug)]
pub struct HttpStatusError {
url: String,
status_code: hyper::StatusCode,
context: String,
}
impl HttpStatusError {
pub fn new(url: String, code: hyper::StatusCode, context: String) -> Self {
HttpStatusError {
url,
status_code: code,
context,
}
}
}
#[derive(Fail, Debug)]
pub enum DataError {
#[fail(display = "SQL Query failed: {}", _0)]
DieselResultError(#[cause] diesel::result::Error),
#[fail(display = "Database Migration error: {}", _0)]
DieselMigrationError(#[cause] RunMigrationsError),
#[fail(display = "R2D2 error: {}", _0)]
R2D2Error(#[cause] r2d2::Error),
#[fail(display = "R2D2 Pool error: {}", _0)]
R2D2PoolError(#[cause] r2d2::PoolError),
#[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),
#[fail(display = "IO Error: {}", _0)]
IOError(#[cause] io::Error),
#[fail(display = "RSS Error: {}", _0)]
RssError(#[cause] rss::Error),
#[fail(display = "XML Reader Error: {}", _0)]
XmlReaderError(#[cause] xml::reader::Error),
#[fail(display = "Error: {}", _0)]
Bail(String),
#[fail(display = "{}", _0)]
HttpStatusGeneral(HttpStatusError),
#[fail(display = "FIXME: This should be better")]
F301(Source),
#[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,
}
impl From<RunMigrationsError> for DataError {
fn from(err: RunMigrationsError) -> Self {
DataError::DieselMigrationError(err)
}
}
impl From<diesel::result::Error> for DataError {
fn from(err: diesel::result::Error) -> Self {
DataError::DieselResultError(err)
}
}
impl From<r2d2::Error> for DataError {
fn from(err: r2d2::Error) -> Self {
DataError::R2D2Error(err)
}
}
impl From<r2d2::PoolError> for DataError {
fn from(err: r2d2::PoolError) -> Self {
DataError::R2D2PoolError(err)
}
}
impl From<hyper::Error> for DataError {
fn from(err: hyper::Error) -> Self {
DataError::HyperError(err)
}
}
impl From<url::ParseError> for DataError {
fn from(err: url::ParseError) -> Self {
DataError::UrlError(err)
}
}
impl From<native_tls::Error> for DataError {
fn from(err: native_tls::Error) -> Self {
DataError::TLSError(err)
}
}
impl From<io::Error> for DataError {
fn from(err: io::Error) -> Self {
DataError::IOError(err)
}
}
impl From<rss::Error> for DataError {
fn from(err: rss::Error) -> Self {
DataError::RssError(err)
}
}
impl From<xml::reader::Error> for DataError {
fn from(err: xml::reader::Error) -> Self {
DataError::XmlReaderError(err)
}
}
impl From<String> for DataError {
fn from(err: String) -> Self {
DataError::Bail(err)
}
}
+223
View File
@@ -0,0 +1,223 @@
#![cfg_attr(feature = "cargo-clippy", allow(unit_arg))]
//! Index Feeds.
use futures::future::*;
use futures::prelude::*;
use futures::stream;
use rss;
use dbqueries;
use errors::DataError;
use models::{Index, IndexState, Update};
use models::{NewEpisode, NewEpisodeMinimal, NewShow, Show};
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
#[derive(Debug, Clone, Builder, PartialEq)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub struct Feed {
/// The `rss::Channel` parsed from the `Source` uri.
channel: rss::Channel,
/// The `Source` id where the xml `rss::Channel` came from.
source_id: i32,
}
impl Feed {
/// Index the contents of the RSS `Feed` into the database.
pub fn index(self) -> impl Future<Item = (), Error = DataError> + Send {
self.parse_podcast_async()
.and_then(|pd| pd.to_podcast())
.and_then(move |pd| self.index_channel_items(pd))
}
fn parse_podcast(&self) -> NewShow {
NewShow::new(&self.channel, self.source_id)
}
fn parse_podcast_async(&self) -> impl Future<Item = NewShow, Error = DataError> + Send {
ok(self.parse_podcast())
}
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
let episodes = stream.filter_map(move |item| {
glue(&item, pd.id())
.map_err(|err| error!("Failed to parse an episode: {}", err))
.ok()
});
// Filter errors, Index updatable episodes, return insertables.
filter_episodes(episodes)
// Batch index insertable episodes.
.and_then(|eps| ok(batch_insert_episodes(&eps)))
}
}
fn glue(item: &rss::Item, id: i32) -> Result<IndexState<NewEpisode>, DataError> {
NewEpisodeMinimal::new(item, id).and_then(move |ep| determine_ep_state(ep, item))
}
fn determine_ep_state(
ep: NewEpisodeMinimal,
item: &rss::Item,
) -> Result<IndexState<NewEpisode>, DataError> {
// Check if feed exists
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.show_id())?;
let rowid = old.rowid();
if ep != old {
Ok(IndexState::Update((ep.into_new_episode(item), rowid)))
} else {
Ok(IndexState::NotChanged)
}
}
}
fn filter_episodes<'a, S>(
stream: S,
) -> impl Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a
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();
None
},
IndexState::Index(s) => Some(s),
})
// only Index is left, collect them for batch index
.collect()
}
fn batch_insert_episodes(episodes: &[NewEpisode]) {
if episodes.is_empty() {
return;
};
info!("Indexing {} episodes.", episodes.len());
dbqueries::index_new_episodes(episodes)
.map_err(|err| {
error!("Failed batch indexng: {}", err);
info!("Fallign back to individual indexing.");
})
.unwrap_or_else(|_| {
episodes.iter().for_each(|ep| {
ep.index()
.map_err(|err| error!("Error: {}.", err))
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.ok();
});
})
}
#[cfg(test)]
mod tests {
use rss::Channel;
use tokio_core::reactor::Core;
use database::truncate_db;
use dbqueries;
use utils::get_feed;
use Source;
use std::fs;
use std::io::BufReader;
use super::*;
// (path, url) tuples.
const URLS: &[(&str, &str)] = {
&[
(
"tests/feeds/2018-01-20-Intercepted.xml",
"https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill",
),
(
"tests/feeds/2018-01-20-LinuxUnplugged.xml",
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.\
com/linuxunplugged",
),
(
"tests/feeds/2018-01-20-TheTipOff.xml",
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff",
),
(
"tests/feeds/2018-01-20-StealTheStars.xml",
"https://web.archive.org/web/20180120104957if_/https://rss.art19.\
com/steal-the-stars",
),
(
"tests/feeds/2018-01-20-GreaterThanCode.xml",
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
com/feed/podcast",
),
]
};
#[test]
fn test_complete_index() {
truncate_db().unwrap();
let feeds: Vec<_> = URLS
.iter()
.map(|&(path, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
get_feed(path, s.id())
})
.collect();
let mut core = Core::new().unwrap();
// Index the channels
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);
}
#[test]
fn test_feed_parse_podcast() {
truncate_db().unwrap();
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 pd = NewShow::new(&channel, 42);
assert_eq!(feed.parse_podcast(), pd);
}
#[test]
fn test_feed_index_channel_items() {
truncate_db().unwrap();
let path = "tests/feeds/2018-01-20-Intercepted.xml";
let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap();
feed.index_channel_items(pd).wait().unwrap();
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 1);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43);
}
}
+152
View File
@@ -0,0 +1,152 @@
#![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,
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,
missing_docs,
trivial_casts,
trivial_numeric_casts,
elided_lifetime_in_paths,
missing_copy_implementations
)]
#![deny(warnings)]
// warn when code is not using dyn Trait syntax. req rustc 1.27
// #![deny(bare_trait_object)]
//! 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()
};
}
}
+466
View File
@@ -0,0 +1,466 @@
use chrono::prelude::*;
use diesel;
use diesel::prelude::*;
use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Save, Show};
use schema::episodes;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, show_id)]
#[belongs_to(Show, foreign_key = "show_id")]
#[derive(Debug, Clone)]
/// Diesel Model of the episode table.
pub struct Episode {
rowid: i32,
title: String,
uri: Option<String>,
local_uri: Option<String>,
description: Option<String>,
epoch: i32,
length: Option<i32>,
duration: Option<i32>,
guid: Option<String>,
played: Option<i32>,
show_id: i32,
}
impl Save<Episode> for Episode {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Episode, Self::Error> {
let db = connection();
let tempdb = db.get()?;
self.save_changes::<Episode>(&*tempdb).map_err(From::from)
}
}
impl Episode {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
}
/// Get the value of the `title` field.
pub fn title(&self) -> &str {
&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.
pub fn uri(&self) -> Option<&str> {
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,
/// that the media file will be located at.
pub fn local_uri(&self) -> Option<&str> {
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.
/// Value is set to Utc whenever possible.
pub fn epoch(&self) -> i32 {
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.
pub fn length(&self) -> Option<i32> {
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.
pub fn duration(&self) -> Option<i32> {
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.
pub fn played(&self) -> Option<i32> {
self.played
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
/// `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.
pub fn set_played_now(&mut self) -> Result<(), DataError> {
let epoch = Utc::now().timestamp() as i32;
self.set_played(Some(epoch));
self.save().map(|_| ())
}
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for constructing `EpisodeWidgets`.
pub struct EpisodeWidgetModel {
rowid: i32,
title: String,
uri: Option<String>,
local_uri: Option<String>,
epoch: i32,
length: Option<i32>,
duration: Option<i32>,
played: Option<i32>,
show_id: i32,
}
impl From<Episode> for EpisodeWidgetModel {
fn from(e: Episode) -> EpisodeWidgetModel {
EpisodeWidgetModel {
rowid: e.rowid,
title: e.title,
uri: e.uri,
local_uri: e.local_uri,
epoch: e.epoch,
length: e.length,
duration: e.duration,
played: e.played,
show_id: e.show_id,
}
}
}
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::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl EpisodeWidgetModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
}
/// Get the value of the `title` field.
pub fn title(&self) -> &str {
&self.title
}
/// Get the value of the `uri`.
///
/// Represents the url(usually) that the media file will be located at.
pub fn uri(&self) -> Option<&str> {
self.uri.as_ref().map(|s| s.as_str())
}
/// Get the value of the `local_uri`.
///
/// Represents the local uri,usually filesystem path,
/// that the media file will be located at.
pub fn local_uri(&self) -> Option<&str> {
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 `epoch` value.
///
/// Retrieved from the rss Item publish date.
/// Value is set to Utc whenever possible.
pub fn epoch(&self) -> i32 {
self.epoch
}
/// Get the `length`.
///
/// The number represents the size of the file in bytes.
pub fn length(&self) -> Option<i32> {
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.
pub fn duration(&self) -> Option<i32> {
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.
pub fn played(&self) -> Option<i32> {
self.played
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
/// `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.
pub fn set_played_now(&mut self) -> Result<(), DataError> {
let epoch = Utc::now().timestamp() as i32;
self.set_played(Some(epoch));
self.save().map(|_| ())
}
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used internal with the `utils::checkup` function.
pub struct EpisodeCleanerModel {
rowid: i32,
local_uri: Option<String>,
played: Option<i32>,
}
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::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl From<Episode> for EpisodeCleanerModel {
fn from(e: Episode) -> EpisodeCleanerModel {
EpisodeCleanerModel {
rowid: e.rowid(),
local_uri: e.local_uri,
played: e.played,
}
}
}
impl EpisodeCleanerModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
}
/// Get the value of the `local_uri`.
///
/// Represents the local uri,usually filesystem path,
/// that the media file will be located at.
pub fn local_uri(&self) -> Option<&str> {
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());
}
/// Epoch representation of the last time the episode was played.
///
/// None/Null for unplayed.
pub fn played(&self) -> Option<i32> {
self.played
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for FIXME.
pub struct EpisodeMinimal {
rowid: i32,
title: String,
uri: Option<String>,
epoch: i32,
length: Option<i32>,
duration: Option<i32>,
guid: Option<String>,
show_id: i32,
}
impl From<Episode> for EpisodeMinimal {
fn from(e: Episode) -> Self {
EpisodeMinimal {
rowid: e.rowid,
title: e.title,
uri: e.uri,
length: e.length,
guid: e.guid,
epoch: e.epoch,
duration: e.duration,
show_id: e.show_id,
}
}
}
impl EpisodeMinimal {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
}
/// Get the value of the `title` field.
pub fn title(&self) -> &str {
&self.title
}
/// Get the value of the `uri`.
///
/// Represents the url(usually) that the media file will be located at.
pub fn uri(&self) -> Option<&str> {
self.uri.as_ref().map(|s| s.as_str())
}
/// Get the Episode's `guid`.
pub fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
/// Get the `epoch` value.
///
/// Retrieved from the rss Item publish date.
/// Value is set to Utc whenever possible.
pub fn epoch(&self) -> i32 {
self.epoch
}
/// Get the `length`.
///
/// The number represents the size of the file in bytes.
pub fn length(&self) -> Option<i32> {
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.
pub fn duration(&self) -> Option<i32> {
self.duration
}
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
}
+59
View File
@@ -0,0 +1,59 @@
mod new_episode;
mod new_show;
mod new_source;
mod episode;
mod show;
mod source;
// use futures::prelude::*;
// use futures::future::*;
pub(crate) use self::episode::EpisodeCleanerModel;
pub(crate) use self::new_episode::{NewEpisode, NewEpisodeMinimal};
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_show::NewShowBuilder;
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetModel};
pub use self::show::{Show, ShowCoverModel};
pub use self::source::Source;
#[derive(Debug, Clone, PartialEq)]
pub enum IndexState<T> {
Index(T),
Update((T, i32)),
NotChanged,
}
pub trait Insert<T> {
type Error;
fn insert(&self) -> Result<T, Self::Error>;
}
pub trait Update<T> {
type Error;
fn update(&self, i32) -> Result<T, Self::Error>;
}
// This might need to change in the future
pub trait Index<T>: Insert<T> + Update<T> {
type Error;
fn index(&self) -> Result<T, <Self as Index<T>>::Error>;
}
/// FIXME: DOCS
pub trait Save<T> {
/// The Error type to be returned.
type Error;
/// Helper method to easily save/"sync" current state of a diesel model to
/// the Database.
fn save(&self) -> Result<T, Self::Error>;
}
+659
View File
@@ -0,0 +1,659 @@
use ammonia;
use diesel;
use diesel::prelude::*;
use rfc822_sanitizer::parse_from_rfc2822_with_fallback as parse_rfc822;
use rss;
use database::connection;
use dbqueries;
use errors::DataError;
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
use parser;
use schema::episodes;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "episodes"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewEpisode {
title: String,
uri: Option<String>,
description: Option<String>,
length: Option<i32>,
duration: Option<i32>,
guid: Option<String>,
epoch: i32,
show_id: i32,
}
impl From<NewEpisodeMinimal> for NewEpisode {
fn from(e: NewEpisodeMinimal) -> Self {
NewEpisodeBuilder::default()
.title(e.title)
.uri(e.uri)
.duration(e.duration)
.epoch(e.epoch)
.show_id(e.show_id)
.guid(e.guid)
.build()
.unwrap()
}
}
impl Insert<()> for NewEpisode {
type Error = DataError;
fn insert(&self) -> Result<(), DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Inserting {:?}", self.title);
diesel::insert_into(episodes)
.values(self)
.execute(&con)
.map_err(From::from)
.map(|_| ())
}
}
impl Update<()> for NewEpisode {
type Error = DataError;
fn update(&self, episode_id: i32) -> Result<(), DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Updating {:?}", self.title);
diesel::update(episodes.filter(rowid.eq(episode_id)))
.set(self)
.execute(&con)
.map_err(From::from)
.map(|_| ())
}
}
impl Index<()> for NewEpisode {
type Error = DataError;
// 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.show_id())?;
if exists {
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.show_id())?;
if self != &other {
self.update(other.rowid())
} else {
Ok(())
}
} else {
self.insert()
}
}
}
impl PartialEq<EpisodeMinimal> for NewEpisode {
fn eq(&self, other: &EpisodeMinimal) -> bool {
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
}
}
impl PartialEq<Episode> for NewEpisode {
fn eq(&self, other: &Episode) -> bool {
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
&& (self.description() == other.description())
&& (self.length() == other.length())
}
}
impl NewEpisode {
/// Parses an `rss::Item` into a `NewEpisode` Struct.
#[allow(dead_code)]
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.show_id).map_err(From::from)
}
}
// Ignore the following getters. They are used in unit tests mainly.
impl NewEpisode {
pub(crate) fn title(&self) -> &str {
self.title.as_ref()
}
pub(crate) fn uri(&self) -> Option<&str> {
self.uri.as_ref().map(|s| s.as_str())
}
pub(crate) fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
pub(crate) fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
pub(crate) fn epoch(&self) -> i32 {
self.epoch
}
pub(crate) fn duration(&self) -> Option<i32> {
self.duration
}
pub(crate) fn length(&self) -> Option<i32> {
self.length
}
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[derive(Insertable, AsChangeset)]
#[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>,
show_id: i32,
}
impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
fn eq(&self, other: &EpisodeMinimal) -> bool {
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
}
}
impl NewEpisodeMinimal {
pub(crate) fn new(item: &rss::Item, parent_id: i32) -> Result<Self, DataError> {
if item.title().is_none() {
let err = DataError::ParseEpisodeError {
reason: "No title specified for this Episode.".into(),
parent_id,
};
return Err(err);
}
let title = item.title().unwrap().trim().to_owned();
let guid = item.guid().map(|s| s.value().trim().to_owned());
// 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())));
// 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 {
reason: "No url specified for the item.".into(),
parent_id,
};
return Err(err);
};
// Default to rfc2822 represantation of epoch 0.
let date = parse_rfc822(item.pub_date().unwrap_or("Thu, 1 Jan 1970 00:00:00 +0000"));
// Should treat information from the rss feeds as invalid by default.
// Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday.
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
let duration = parser::parse_itunes_duration(item.itunes_ext());
NewEpisodeMinimalBuilder::default()
.title(title)
.uri(uri)
.length(length)
.duration(duration)
.epoch(epoch)
.guid(guid)
.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 description = item.description().and_then(|s| {
let sanitized_html = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
.link_rel(None)
.clean(s.trim())
.to_string();
Some(sanitized_html)
});
NewEpisodeBuilder::default()
.title(self.title)
.uri(self.uri)
.duration(self.duration)
.epoch(self.epoch)
.show_id(self.show_id)
.guid(self.guid)
.length(self.length)
.description(description)
.build()
.unwrap()
}
}
// Ignore the following getters. They are used in unit tests mainly.
impl NewEpisodeMinimal {
pub(crate) fn title(&self) -> &str {
self.title.as_ref()
}
pub(crate) fn uri(&self) -> Option<&str> {
self.uri.as_ref().map(|s| s.as_str())
}
pub(crate) fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
pub(crate) fn duration(&self) -> Option<i32> {
self.duration
}
pub(crate) fn epoch(&self) -> i32 {
self.epoch
}
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[cfg(test)]
mod tests {
use database::truncate_db;
use dbqueries;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use models::*;
use rss::Channel;
use std::fs::File;
use std::io::BufReader;
// TODO: Add tests for other feeds too.
// Especially if you find an *intresting* generated feed.
// Known prebuilt expected objects.
lazy_static! {
static ref EXPECTED_MINIMAL_INTERCEPTED_1: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default()
.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")))
.epoch(1505296800)
.length(Some(66738886))
.duration(Some(4171))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default()
.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")))
.epoch(1502272800)
.length(Some(67527575))
.duration(Some(4415))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_INTERCEPTED_1: NewEpisode = {
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data \
breach and allegations of Russian interference in the US election. \
Commentator Shaun King explains his call for a boycott of the NFL and \
talks about his campaign to bring violent neo-Nazis to justice. Rapper \
Open Mike Eagle performs.";
NewEpisodeBuilder::default()
.title("The Super Bowl of Racism")
.uri(Some(String::from(
"http://traffic.megaphone.fm/PPY6458293736.mp3",
)))
.description(Some(String::from(descr)))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(4171))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_INTERCEPTED_2: NewEpisode = {
let descr = "This week on Intercepted: Jeremy gives an update on the aftermath of \
Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee \
Fang lays out how a network of libertarian think tanks called the Atlas \
Network is insidiously shaping political infrastructure in Latin \
America. We speak with attorney and former Hugo Chavez adviser Eva \
Golinger about the Venezuela\'s political turmoil.And we hear Claudia \
Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her \
music and hopes for Venezuela.";
NewEpisodeBuilder::default()
.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)))
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
.length(Some(67527575))
.epoch(1502272800)
.duration(Some(4415))
.show_id(42)
.build()
.unwrap()
};
static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = {
NewEpisodeBuilder::default()
.title("The Super Bowl of Racism")
.uri(Some(String::from(
"http://traffic.megaphone.fm/PPY6458293736.mp3",
)))
.description(Some(String::from("New description")))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(424242))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default()
.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")))
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default()
.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")))
.epoch(1504670247)
.length(Some(36544272))
.duration(Some(4491))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_LUP_1: NewEpisode = {
let descr = "Audit your network with a couple of easy commands on Kali Linux. Chris \
decides to blow off a little steam by attacking his IoT devices, Wes has \
the scope on Equifax blaming open source &amp; the Beard just saved the \
show. Its a really packed episode!";
NewEpisodeBuilder::default()
.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)))
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.show_id(42)
.build()
.unwrap()
};
static ref EXPECTED_LUP_2: NewEpisode = {
let descr =
"<p>The Gnome project is about to solve one of our audience's biggest Waylands \
concerns. But as the project takes on a new level of relevance, decisions for \
the next version of Gnome have us worried about the future.</p>\n\n<p>Plus we \
chat with Wimpy about the Ubuntu Rally in NYC, Microsofts sneaky move to turn \
Windows 10 into the “ULTIMATE LINUX RUNTIME”, community news &amp; more!</p>";
NewEpisodeBuilder::default()
.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)))
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
.length(Some(36544272))
.epoch(1504670247)
.duration(Some(4491))
.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();
let episode = channel.items().iter().nth(14).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_1);
let episode = channel.items().iter().nth(15).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_2);
}
#[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();
let episode = channel.items().iter().nth(14).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
let episode = channel.items().iter().nth(15).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
}
#[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();
let episode = channel.items().iter().nth(18).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_1);
let episode = channel.items().iter().nth(19).unwrap();
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_2);
}
#[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();
let episode = channel.items().iter().nth(18).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_LUP_1);
let episode = channel.items().iter().nth(19).unwrap();
let ep = NewEpisode::new(&episode, 42).unwrap();
assert_eq!(ep, *EXPECTED_LUP_2);
}
#[test]
fn test_minimal_into_new_episode() {
truncate_db().unwrap();
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
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();
let ep = EXPECTED_MINIMAL_INTERCEPTED_2
.clone()
.into_new_episode(&item);
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
}
#[test]
fn test_new_episode_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();
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.show_id()).unwrap();
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.show_id()).unwrap();
assert_eq!(new_ep, ep);
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_2);
assert_eq!(&*EXPECTED_INTERCEPTED_2, &ep);
}
#[test]
fn test_new_episode_update() {
truncate_db().unwrap();
let old = EXPECTED_INTERCEPTED_1.clone().to_episode().unwrap();
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
updated.update(old.rowid()).unwrap();
let new = dbqueries::get_episode_from_pk(old.title(), old.show_id()).unwrap();
// Assert that updating does not change the rowid and show_id
assert_ne!(old, new);
assert_eq!(old.rowid(), new.rowid());
assert_eq!(old.show_id(), new.show_id());
assert_eq!(updated, &new);
assert_ne!(updated, &old);
}
#[test]
fn test_new_episode_index() {
truncate_db().unwrap();
let expected = &*EXPECTED_INTERCEPTED_1;
// First insert
assert!(expected.index().is_ok());
// 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.show_id()).unwrap();
// Assert that NewPodcast is equal to the Indexed one
assert_eq!(*expected, old);
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
// Update the podcast
assert!(updated.index().is_ok());
// Get the new Podcast
let new = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
// 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.show_id(), old.show_id());
}
#[test]
fn test_new_episode_to_episode() {
let expected = &*EXPECTED_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.show_id()).unwrap();
let ep = expected.to_episode().unwrap();
assert_eq!(old, ep);
// Same as above, diff order
truncate_db().unwrap();
let ep = expected.to_episode().unwrap();
// This should error as a unique constrain violation
assert!(expected.insert().is_err());
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
assert_eq!(old, ep);
}
}
+423
View File
@@ -0,0 +1,423 @@
use ammonia;
use diesel;
use diesel::prelude::*;
use rss;
use errors::DataError;
use models::Show;
use models::{Index, Insert, Update};
use schema::shows;
use database::connection;
use dbqueries;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "shows"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewShow {
title: String,
link: String,
description: String,
image_uri: Option<String>,
source_id: i32,
}
impl Insert<()> for NewShow {
type Error = DataError;
fn insert(&self) -> Result<(), Self::Error> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(shows)
.values(self)
.execute(&con)
.map(|_| ())
.map_err(From::from)
}
}
impl Update<()> for NewShow {
type Error = DataError;
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(shows.filter(id.eq(show_id)))
.set(self)
.execute(&con)
.map(|_| ())
.map_err(From::from)
}
}
// TODO: Maybe return an Enum<Action(Resut)> Instead.
// It would make unti testing better too.
impl Index<()> for NewShow {
type Error = DataError;
fn index(&self) -> Result<(), DataError> {
let exists = dbqueries::podcast_exists(self.source_id)?;
if exists {
let other = dbqueries::get_podcast_from_source_id(self.source_id)?;
if self != &other {
self.update(other.id())
} else {
Ok(())
}
} else {
self.insert()
}
}
}
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())
&& (self.description() == other.description())
&& (self.source_id() == other.source_id())
}
}
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());
let description = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
.link_rel(None)
.clean(chan.description().trim())
.to_string();
// Try to get the itunes img first
let itunes_img = chan
.itunes_ext()
.and_then(|s| s.image().map(|url| url.trim()))
.map(|s| s.to_owned());
// 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()));
NewShowBuilder::default()
.title(title)
.description(description)
.link(link)
.image_uri(image_uri)
.source_id(source_id)
.build()
.unwrap()
}
// Look out for when tryinto lands into stable.
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 NewShow {
#[allow(dead_code)]
pub(crate) fn source_id(&self) -> i32 {
self.source_id
}
pub(crate) fn title(&self) -> &str {
&self.title
}
pub(crate) fn link(&self) -> &str {
&self.link
}
pub(crate) fn description(&self) -> &str {
&self.description
}
pub(crate) fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
// use tokio_core::reactor::Core;
use rss::Channel;
use database::truncate_db;
use models::NewShowBuilder;
use std::fs::File;
use std::io::BufReader;
// Pre-built expected NewShow structs.
lazy_static! {
static ref EXPECTED_INTERCEPTED: NewShow = {
let descr = "The people behind The Intercepts fearless reporting and incisive \
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
others—discuss the crucial issues of our time: national security, civil \
liberties, foreign policy, and criminal justice. Plus interviews with \
artists, thinkers, and newsmakers who challenge our preconceptions about \
the world we live in.";
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description(descr)
.image_uri(Some(String::from(
"http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/\
uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_\
2FIntercepted_COVER%2B_281_29.png")
))
.source_id(42)
.build()
.unwrap()
};
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.";
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)
.build()
.unwrap()
};
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 \
the journalists behind the work as they tell us about the leads, the \
dead-ends and of course, the tip offs. Therell be car chases, slammed \
doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \
with despotic regimes and much more. So if youre curious about the fun, \
complicated detective work that goes into doing great investigative \
journalism- then this is the podcast for you.</p>";
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)
.build()
.unwrap()
};
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 \
ever attempted - scripted by Mac Rogers, the award-winning playwright \
and writer of the multi-million download The Message and LifeAfter.</p>";
let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
NewShowBuilder::default()
.title("Steal the Stars")
.link("http://tor-labs.com/")
.description(descr)
.image_uri(Some(String::from(img)))
.source_id(42)
.build()
.unwrap()
};
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.";
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)
.build()
.unwrap()
};
static ref EXPECTED_ELLINOFRENEIA: NewShow = {
NewShowBuilder::default()
.title("Ελληνοφρένεια")
.link("https://ellinofreneia.sealabs.net/feed.rss")
.description("Ανεπίσημο feed της Ελληνοφρένειας")
.image_uri(Some("https://ellinofreneia.sealabs.net/logo.png".into()))
.source_id(42)
.build()
.unwrap()
};
static ref UPDATED_DESC_INTERCEPTED: NewShow = {
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description("New Description")
.image_uri(Some(String::from(
"http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/\
uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_\
2FIntercepted_COVER%2B_281_29.png")
))
.source_id(42)
.build()
.unwrap()
};
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_INTERCEPTED, pd);
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_LUP, pd);
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_TIPOFF, pd);
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_STARS, pd);
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_CODE, pd);
}
#[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();
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
}
#[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();
let npd = NewShow::new(&channel, 42);
npd.insert().unwrap();
let pd = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(npd, pd);
assert_eq!(*EXPECTED_INTERCEPTED, npd);
assert_eq!(&*EXPECTED_INTERCEPTED, &pd);
}
#[test]
// TODO: Add more test/checks
// 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();
let updated = &*UPDATED_DESC_INTERCEPTED;
updated.update(old.id()).unwrap();
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
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);
}
#[test]
fn test_new_podcast_index() {
truncate_db().unwrap();
// 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 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 Show
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
// 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());
}
#[test]
fn test_to_podcast() {
// 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();
assert_eq!(old, pd);
// Same as above, diff order
truncate_db().unwrap();
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
// This should error as a unique constrain violation
assert!(EXPECTED_INTERCEPTED.insert().is_err());
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(old, pd);
}
}
+50
View File
@@ -0,0 +1,50 @@
use diesel;
use diesel::prelude::*;
use url::Url;
use database::connection;
use dbqueries;
// use models::{Insert, Update};
use errors::DataError;
use models::Source;
use schema::source;
#[derive(Insertable)]
#[table_name = "source"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewSource {
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl NewSource {
pub(crate) fn new(uri: &Url) -> NewSource {
NewSource {
uri: uri.to_string(),
last_modified: None,
http_etag: None,
}
}
pub(crate) fn insert_or_ignore(&self) -> Result<(), DataError> {
use schema::source::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_or_ignore_into(source)
.values(self)
.execute(&con)
.map(|_| ())
.map_err(From::from)
}
// Look out for when tryinto lands into stable.
pub(crate) fn to_source(&self) -> Result<Source, DataError> {
self.insert_or_ignore()?;
dbqueries::get_source_from_uri(&self.uri).map_err(From::from)
}
}
+123
View File
@@ -0,0 +1,123 @@
use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Save, 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 Save<Show> for Show {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Show, Self::Error> {
let db = connection();
let tempdb = db.get()?;
self.save_changes::<Show>(&*tempdb).map_err(From::from)
}
}
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
}
/// Set the Show/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());
}
/// `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())
}
}
+282
View File
@@ -0,0 +1,282 @@
use diesel::SaveChangesDsl;
// use failure::ResultExt;
use rss::Channel;
use url::Url;
use hyper::client::HttpConnector;
use hyper::header::{
ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified, Location, UserAgent,
};
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper_tls::HttpsConnector;
// use futures::future::ok;
use futures::future::{loop_fn, Future, Loop};
use futures::prelude::*;
use database::connection;
use errors::*;
use feed::{Feed, FeedBuilder};
use models::{NewSource, Save};
use schema::source;
use USER_AGENT;
use std::str::FromStr;
#[derive(Queryable, Identifiable, AsChangeset, PartialEq)]
#[table_name = "source"]
#[changeset_options(treat_none_as_null = "true")]
#[derive(Debug, Clone)]
/// Diesel Model of the source table.
pub struct Source {
id: i32,
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl Save<Source> for Source {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Source, Self::Error> {
let db = connection();
let con = db.get()?;
self.save_changes::<Source>(&con).map_err(From::from)
}
}
impl Source {
/// Get the source `id` column.
pub fn id(&self) -> i32 {
self.id
}
/// Represents the location(usually url) of the Feed xml file.
pub fn uri(&self) -> &str {
&self.uri
}
/// Set the `uri` field value.
pub fn set_uri(&mut self, uri: String) {
self.uri = uri;
}
/// Represents the Http Last-Modified Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn last_modified(&self) -> Option<&str> {
self.last_modified.as_ref().map(|s| s.as_str())
}
/// Set `last_modified` value.
pub fn set_last_modified(&mut self, value: Option<String>) {
// self.last_modified = value.map(|x| x.to_string());
self.last_modified = value;
}
/// Represents the Http Etag Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn http_etag(&self) -> Option<&str> {
self.http_etag.as_ref().map(|s| s.as_str())
}
/// Set `http_etag` value.
pub fn set_http_etag(&mut self, value: Option<&str>) {
self.http_etag = value.map(|x| x.to_string());
}
/// Extract Etag and LastModifier from res, and update self and the
/// corresponding db row.
fn update_etag(&mut self, res: &Response) -> Result<(), DataError> {
let headers = res.headers();
let etag = headers.get::<ETag>().map(|x| x.tag());
let lmod = headers.get::<LastModified>().map(|x| format!("{}", x));
if (self.http_etag() != etag) || (self.last_modified != lmod) {
self.set_http_etag(etag);
self.set_last_modified(lmod);
self.save()?;
}
Ok(())
}
fn make_err(self, context: &str, code: StatusCode) -> DataError {
DataError::HttpStatusGeneral(HttpStatusError::new(self.uri, code, context.into()))
}
// TODO match on more stuff
// 301: Moved Permanently
// 304: Up to date Feed, checked with the Etag
// 307: Temporary redirect of the url
// 308: Permanent redirect of the url
// 401: Unathorized
// 403: Forbidden
// 408: Timeout
// 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 {
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
StatusCode::MovedPermanently => {
error!("Feed was moved permanently.");
self.handle_301(&res)?;
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)),
StatusCode::RequestTimeout => return Err(self.make_err("408: Request Timeout.", code)),
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> {
let headers = res.headers();
if let Some(url) = headers.get::<Location>() {
self.set_uri(url.to_string());
self.http_etag = None;
self.last_modified = None;
self.save()?;
info!("Feed url was updated succesfully.");
}
Ok(())
}
/// Construct a new `Source` with the given `uri` and index it.
///
/// This only indexes the `Source` struct, not the Podcast Feed.
pub fn from_url(uri: &str) -> Result<Source, DataError> {
let url = Url::parse(uri)?;
NewSource::new(&url).to_source()
}
/// `Feed` constructor.
///
/// Fetches the latest xml Feed.
///
/// Updates the validator Http Headers.
///
/// Consumes `self` and Returns the corresponding `Feed` Object.
// Refactor into TryInto once it lands on stable.
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)
.then(|res| match res {
Ok(response) => Ok(Loop::Break(response)),
Err(err) => match err {
DataError::F301(s) => {
info!("Following redirect...");
Ok(Loop::Continue(s))
}
e => Err(e),
},
})
});
response
.and_then(response_to_channel)
.and_then(move |chan| {
FeedBuilder::default()
.channel(chan)
.source_id(id)
.build()
.map_err(From::from)
})
}
// 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();
let mut req = Request::new(Method::Get, uri);
// 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(lmod) = self.last_modified() {
if let Ok(date) = lmod.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(date));
}
}
}
client
.request(req)
.map_err(From::from)
.and_then(move |res| self.match_status(res))
}
}
#[allow(needless_pass_by_value)]
fn response_to_channel(res: Response) -> impl Future<Item = Channel, Error = DataError> + Send {
res.body()
.concat2()
.map(|x| x.into_iter())
.map_err(From::from)
.map(|iter| iter.collect::<Vec<u8>>())
.map(|utf_8_bytes| String::from_utf8_lossy(&utf_8_bytes).into_owned())
.and_then(|buf| Channel::from_str(&buf).map_err(From::from))
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_core::reactor::Core;
use database::truncate_db;
use utils::get_feed;
#[test]
fn test_into_feed() {
truncate_db().unwrap();
let mut core = Core::new().unwrap();
let client = Client::configure()
.connector(HttpsConnector::new(4, &core.handle()).unwrap())
.build(&core.handle());
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let id = source.id();
let feed = source.into_feed(client, true);
let feed = core.run(feed).unwrap();
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
assert_eq!(expected, feed);
}
}
+167
View File
@@ -0,0 +1,167 @@
//! FIXME: Docs
// #![allow(unused)]
use errors::DataError;
use models::Source;
use xml::reader;
use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::Path;
// use std::fs::{File, OpenOptions};
// use std::io::BufReader;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
// FIXME: Make it a Diesel model
/// Represents an `outline` xml element as per the `OPML` [specification][spec]
/// not `RSS` related sub-elements are ommited.
///
/// [spec]: http://dev.opml.org/spec2.html
pub struct Opml {
title: String,
description: String,
url: String,
}
/// Import feed url's from a `R` into the `Source` table.
// TODO: Write test
pub fn import_to_db<R: Read>(reader: R) -> Result<Vec<Source>, reader::Error> {
let feeds = extract_sources(reader)?
.iter()
.map(|opml| Source::from_url(&opml.url))
.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/podcasts/issues/new";
error!("Failed to import a Show: {}", err);
error!("{}", txt);
}
s.ok()
})
.collect();
Ok(feeds)
}
/// Open a File from `P`, try to parse the OPML then insert the Feeds in the database and
/// return the new `Source`s
// TODO: Write test
pub fn import_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<Source>, DataError> {
let content = fs::read(path)?;
import_to_db(content.as_slice()).map_err(From::from)
}
/// Extracts the `outline` elemnts from a reader `R` and returns a `HashSet` of `Opml` structs.
pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Error> {
let mut list = HashSet::new();
let parser = reader::EventReader::new(reader);
parser
.into_iter()
.map(|e| match e {
Ok(reader::XmlEvent::StartElement {
name, attributes, ..
}) => {
if name.local_name == "outline" {
let mut title = String::new();
let mut url = String::new();
let mut description = String::new();
attributes.into_iter().for_each(|attribute| {
match attribute.name.local_name.as_str() {
"title" => title = attribute.value,
"xmlUrl" => url = attribute.value,
"description" => description = attribute.value,
_ => {}
}
});
let feed = Opml {
title,
description,
url,
};
list.insert(feed);
}
Ok(())
}
Err(err) => Err(err),
_ => Ok(()),
})
.collect::<Result<Vec<_>, reader::Error>>()?;
Ok(list)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
#[test]
fn test_extract() {
let int_title = String::from("Intercepted with Jeremy Scahill");
let int_url = String::from("https://feeds.feedburner.com/InterceptedWithJeremyScahill");
let int_desc =
String::from(
"The people behind The Intercepts fearless reporting and incisive \
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss the \
crucial issues of our time: national security, civil liberties, foreign policy, \
and criminal justice. Plus interviews with artists, thinkers, and newsmakers \
who challenge our preconceptions about the world we live in.",
);
let dec_title = String::from("Deconstructed with Mehdi Hasan");
let dec_url = String::from("https://rss.prod.firstlook.media/deconstructed/podcast.rss");
let dec_desc = String::from(
"Journalist Mehdi Hasan is known around the world for his televised takedowns of \
presidents and prime ministers. In this new podcast from The Intercept, Mehdi \
unpacks a game-changing news event of the week while challenging the conventional \
wisdom. As a Brit, a Muslim and an immigrant based in Donald Trump's Washington \
D.C., Mehdi gives a refreshingly provocative perspective on the ups and downs of \
American—and global—politics.",
);
#[cfg_attr(rustfmt, rustfmt_skip)]
let sample1 = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?> \
<opml version=\"2.0\"> \
<head> \
<title>Test OPML File</title> \
<dateCreated>{}</dateCreated> \
<docs>http://www.opml.org/spec2</docs> \
</head> \
<body> \
<outline type=\"rss\" title=\"{}\" description=\"{}\" xmlUrl=\"{}\"/> \
<outline type=\"rss\" title=\"{}\" description=\"{}\" xmlUrl=\"{}\"/> \
</body> \
</opml>",
Local::now().format("%a, %d %b %Y %T %Z"),
int_title,
int_desc,
int_url,
dec_title,
dec_desc,
dec_url,
);
let map = hashset![
Opml {
title: int_title,
description: int_desc,
url: int_url
},
Opml {
title: dec_title,
description: dec_desc,
url: dec_url
},
];
assert_eq!(extract_sources(sample1.as_bytes()).unwrap(), map);
}
}
+81
View File
@@ -0,0 +1,81 @@
use rss::extension::itunes::ITunesItemExtension;
/// Parses an Item Itunes extension and returns it's duration value in seconds.
// FIXME: Rafactor
#[allow(non_snake_case)]
pub(crate) fn parse_itunes_duration(item: Option<&ITunesItemExtension>) -> Option<i32> {
let duration = item.map(|s| s.duration())??;
// FOR SOME FUCKING REASON, IN THE APPLE EXTENSION SPEC
// THE DURATION CAN BE EITHER AN INT OF SECONDS OR
// A STRING OF THE FOLLOWING FORMATS:
// HH:MM:SS, H:MM:SS, MM:SS, M:SS
// LIKE WHO THE FUCK THOUGH THAT WOULD BE A GOOD IDEA.
if let Ok(NO_FUCKING_LOGIC) = duration.parse::<i32>() {
return Some(NO_FUCKING_LOGIC);
};
let mut seconds = 0;
let fk_apple = duration.split(':').collect::<Vec<_>>();
if fk_apple.len() == 3 {
seconds += fk_apple[0].parse::<i32>().unwrap_or(0) * 3600;
seconds += fk_apple[1].parse::<i32>().unwrap_or(0) * 60;
seconds += fk_apple[2].parse::<i32>().unwrap_or(0);
} else if fk_apple.len() == 2 {
seconds += fk_apple[0].parse::<i32>().unwrap_or(0) * 60;
seconds += fk_apple[1].parse::<i32>().unwrap_or(0);
}
Some(seconds)
}
#[cfg(test)]
mod tests {
use rss::extension::itunes::ITunesItemExtensionBuilder;
use super::*;
#[test]
fn test_itunes_duration() {
// Input is a String<Int>
let extension = ITunesItemExtensionBuilder::default()
.duration(Some("3370".into()))
.build()
.unwrap();
let item = Some(&extension);
assert_eq!(parse_itunes_duration(item), Some(3370));
// Input is a String<M:SS>
let extension = ITunesItemExtensionBuilder::default()
.duration(Some("6:10".into()))
.build()
.unwrap();
let item = Some(&extension);
assert_eq!(parse_itunes_duration(item), Some(370));
// Input is a String<MM:SS>
let extension = ITunesItemExtensionBuilder::default()
.duration(Some("56:10".into()))
.build()
.unwrap();
let item = Some(&extension);
assert_eq!(parse_itunes_duration(item), Some(3370));
// Input is a String<H:MM:SS>
let extension = ITunesItemExtensionBuilder::default()
.duration(Some("1:56:10".into()))
.build()
.unwrap();
let item = Some(&extension);
assert_eq!(parse_itunes_duration(item), Some(6970));
// Input is a String<HH:MM:SS>
let extension = ITunesItemExtensionBuilder::default()
.duration(Some("01:56:10".into()))
.build()
.unwrap();
let item = Some(&extension);
assert_eq!(parse_itunes_duration(item), Some(6970));
}
}
+128
View File
@@ -0,0 +1,128 @@
// FIXME:
//! Docs.
use futures::future::*;
use futures::prelude::*;
use futures::stream::*;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_tls::HttpsConnector;
use tokio_core::reactor::Core;
use num_cpus;
use rayon;
use rayon_futures::ScopeFutureExt;
use errors::DataError;
use Source;
// use std::sync::{Arc, Mutex};
// 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
}
);
}
type HttpsClient = Client<HttpsConnector<HttpConnector>>;
/// The pipline to be run for indexing and updating a Podcast feed that originates from
/// `Source.uri`.
///
/// Messy temp diagram:
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` ->
/// 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(|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.
.map_err(|err| error!("Error: {}", err))
.then(|_| ok::<(), DataError>(()))
.collect()
}
/// 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>
where
S: IntoIterator<Item = Source>,
{
let mut core = Core::new()?;
let handle = core.handle();
let client = Client::configure()
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle);
let stream = iter_ok::<_, DataError>(sources);
let p = pipeline(stream, ignore_etags, &client);
core.run(p).map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
use database::truncate_db;
use dbqueries;
use Source;
// (path, url) tuples.
const URLS: &[&str] = &[
"https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill",
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.com/linuxunplugged",
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff",
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars",
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
com/feed/podcast",
];
#[test]
/// Insert feeds and update/index them.
fn test_pipeline() {
truncate_db().unwrap();
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();
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().unwrap();
// Run again to cover Unique constrains erros.
run(sources, true).unwrap();
// 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);
}
}
+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,31 +1,28 @@
#![allow(warnings)]
table! {
episode (id) {
id -> Integer,
title -> Nullable<Text>,
uri -> Text,
episodes (title, show_id) {
rowid -> Integer,
title -> Text,
uri -> Nullable<Text>,
local_uri -> Nullable<Text>,
description -> Nullable<Text>,
published_date -> Nullable<Text>,
epoch -> Integer,
length -> Nullable<Integer>,
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,
}
}
@@ -38,3 +35,5 @@ table! {
http_etag -> Nullable<Text>,
}
}
allow_tables_to_appear_in_same_query!(episodes, shows, source);
+289
View File
@@ -0,0 +1,289 @@
//! Helper utilities for accomplishing various tasks.
use chrono::prelude::*;
use rayon::prelude::*;
use url::{Position, Url};
use dbqueries;
use errors::DataError;
use models::{EpisodeCleanerModel, Save, Show};
use xdg_dirs::DL_DIR;
use std::fs;
use std::path::Path;
/// Scan downloaded `episode` entries that might have broken `local_uri`s and
/// set them to `None`.
fn download_checker() -> Result<(), DataError> {
let mut episodes = dbqueries::get_downloaded_episodes()?;
episodes
.par_iter_mut()
.filter_map(|ep| {
if !Path::new(ep.local_uri()?).exists() {
return Some(ep);
}
None
})
.for_each(|ep| {
ep.set_local_uri(None);
ep.save()
.map_err(|err| error!("{}", err))
.map_err(|_| error!("Error while trying to update episode: {:#?}", ep))
.ok();
});
Ok(())
}
/// Delete watched `episodes` that have exceded their liftime after played.
fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
let mut episodes = dbqueries::get_played_cleaner_episodes()?;
let now_utc = cleanup_date.timestamp() as i32;
episodes
.par_iter_mut()
.filter(|ep| ep.local_uri().is_some() && ep.played().is_some())
.for_each(|ep| {
let limit = ep.played().unwrap();
if now_utc > limit {
delete_local_content(ep)
.map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri()))
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri()))
.ok();
}
});
Ok(())
}
/// Check `ep.local_uri` field and delete the file it points to.
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() {
let res = fs::remove_file(&uri);
if res.is_ok() {
ep.set_local_uri(None);
ep.save()?;
} else {
error!("Error while trying to delete file: {}", uri);
error!("{}", res.unwrap_err());
};
}
} else {
error!(
"Something went wrong evaluating the following path: {:?}",
ep.local_uri(),
);
}
Ok(())
}
/// Database cleaning tasks.
///
/// Runs a download checker which looks for `Episode.local_uri` entries that
/// doesn't exist and sets them to None
///
/// Runs a cleaner for played Episode's that are pass the lifetime limit and
/// scheduled for removal.
pub fn checkup(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
info!("Running database checks.");
download_checker()?;
played_cleaner(cleanup_date)?;
info!("Checks completed.");
Ok(())
}
/// Remove fragment identifiers and query pairs from a URL
/// If url parsing fails, return's a trimmed version of the original input.
pub fn url_cleaner(s: &str) -> String {
// Copied from the cookbook.
// https://rust-lang-nursery.github.io/rust-cookbook/net.html
// #remove-fragment-identifiers-and-query-pairs-from-a-url
match Url::parse(s) {
Ok(parsed) => parsed[..Position::AfterPath].to_owned(),
_ => s.trim().to_owned(),
}
}
/// 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 Show rowid
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
// Create the folder
fs::DirBuilder::new()
.recursive(true)
.create(&download_fold)?;
Ok(download_fold)
}
/// 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: &Show) -> Result<(), DataError> {
dbqueries::remove_feed(pd)?;
info!("{} was removed succesfully.", pd.title());
let fold = get_download_folder(pd.title())?;
fs::remove_dir_all(&fold)?;
info!("All the content at, {} was removed succesfully", &fold);
Ok(())
}
#[cfg(test)]
use Feed;
#[cfg(test)]
/// Helper function that open a local file, parse the rss::Channel and gives back a Feed object.
/// Alternative Feed constructor to be used for tests.
pub fn get_feed(file_path: &str, id: i32) -> Feed {
use feed::FeedBuilder;
use rss::Channel;
use std::fs;
use std::io::BufReader;
// open the xml file
let feed = fs::File::open(file_path).unwrap();
// parse it into a channel
let chan = Channel::read_from(BufReader::new(feed)).unwrap();
FeedBuilder::default()
.channel(chan)
.source_id(id)
.build()
.unwrap()
}
#[cfg(test)]
mod tests {
extern crate tempdir;
use self::tempdir::TempDir;
use super::*;
use chrono::Duration;
use database::truncate_db;
use models::NewEpisodeBuilder;
use std::fs::File;
use std::io::Write;
fn helper_db() -> TempDir {
// Clean the db
truncate_db().unwrap();
// Setup tmp file stuff
let tmp_dir = TempDir::new("podcasts_test").unwrap();
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();
// Setup episodes
let n1 = NewEpisodeBuilder::default()
.title("foo_bar".to_string())
.show_id(0)
.build()
.unwrap()
.to_episode()
.unwrap();
let n2 = NewEpisodeBuilder::default()
.title("bar_baz".to_string())
.show_id(1)
.build()
.unwrap()
.to_episode()
.unwrap();
let mut ep1 = dbqueries::get_episode_from_pk(n1.title(), n1.show_id()).unwrap();
let mut ep2 = dbqueries::get_episode_from_pk(n2.title(), n2.show_id()).unwrap();
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();
tmp_dir
}
#[test]
fn test_download_checker() {
let tmp_dir = helper_db();
download_checker().unwrap();
let episodes = dbqueries::get_downloaded_episodes().unwrap();
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
assert_eq!(episodes.len(), 1);
assert_eq!(
Some(valid_path.to_str().unwrap()),
episodes.first().unwrap().local_uri()
);
let _tmp_dir = helper_db();
download_checker().unwrap();
let episode = dbqueries::get_episode_from_pk("bar_baz", 1).unwrap();
assert!(episode.local_uri().is_none());
}
#[test]
fn test_download_cleaner() {
let _tmp_dir = helper_db();
let mut episode: EpisodeCleanerModel =
dbqueries::get_episode_from_pk("foo_bar", 0).unwrap().into();
let valid_path = episode.local_uri().unwrap().to_owned();
delete_local_content(&mut episode).unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_expired() {
let _tmp_dir = helper_db();
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
let cleanup_date = Utc::now() - Duration::seconds(1000);
let epoch = cleanup_date.timestamp() as i32 - 1;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should delete the file
played_cleaner(cleanup_date).unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_none() {
let _tmp_dir = helper_db();
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
let cleanup_date = Utc::now() - Duration::seconds(1000);
let epoch = cleanup_date.timestamp() as i32 + 1;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should not delete the file
played_cleaner(cleanup_date).unwrap();
assert_eq!(Path::new(&valid_path).exists(), true);
}
#[test]
fn test_url_cleaner() {
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);
}
#[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
fn test_get_dl_folder() {
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
assert_eq!(get_download_folder("foo").unwrap(), foo_);
let _ = fs::remove_dir_all(foo_);
}
}
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,11 @@
<language>en</language>
<copyright>First Look Media Works, Inc.</copyright>
<description>The people behind The Intercepts fearless reporting and incisive commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss the crucial issues of our time: national security, civil liberties, foreign policy, and criminal justice. Plus interviews with artists, thinkers, and newsmakers who challenge our preconceptions about the world we live in.</description>
<image>
<url>http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_2FIntercepted_COVER%2B_281_29.png</url>
<title>Intercepted with Jeremy Scahill</title>
<link>https://theintercept.com/podcasts</link>
</image>
<itunes:explicit>no</itunes:explicit>
<itunes:type>episodic</itunes:type>
<itunes:subtitle>The people behind The Intercepts fearless reporting and incisive commentary discuss the crucial issues of our time.</itunes:subtitle>
@@ -20,6 +25,230 @@
<itunes:category text="News &amp; Politics">
</itunes:category>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/InterceptedWithJeremyScahill" /><feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" uri="interceptedwithjeremyscahill" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><item>
<title>White Mirror</title>
<description>Jeremy lays out the bloody US history in Haiti and El Salvador and blasts the bipartisan, selective amnesia and historical revisionism that “American exceptionalism” demands. Rep. Tulsi Gabbard discusses U.S. regime change, North Korea and why Bernie Sanders would have defeated Trump. As Robert Mueller hits Bannon with a Grand Jury subpoena, former CIA operative and&amp;nbsp; Cipher Brief columnist John Sipher and journalist Marcy Wheeler of Emptywheel analyze the Russia investigation and the Steele dossier. Leading Marxist scholar David Harvey talks about debt peonage in the age of Trump and the crimes of capitalism.</description>
<pubDate>Wed, 17 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>White Mirror</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump is a racist and the perfect man to represent Americas racist legacy in the countries he called shitholes. </itunes:subtitle>
<itunes:summary>
<![CDATA[Jeremy lays out the bloody US history in Haiti and El Salvador and blasts the bipartisan, selective amnesia and historical revisionism that “American exceptionalism” demands. Rep. Tulsi Gabbard discusses U.S. regime change, North Korea and why Bernie Sanders would have defeated Trump. As Robert Mueller hits Bannon with a Grand Jury subpoena, former CIA operative and&nbsp; Cipher Brief columnist John Sipher and journalist Marcy Wheeler of Emptywheel analyze the Russia investigation and the Steele dossier. Leading Marxist scholar David Harvey talks about debt peonage in the age of Trump and the crimes of capitalism.]]>
</itunes:summary>
<itunes:duration>6409</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[3660ad94-fb38-11e7-847d-436a066985fa]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY1407171456.mp3?updated=1516180736" length="102550465" type="audio/mpeg" />
</item>
<item>
<title>BONUS: All The News Unfit to Print</title>
<description>James Risen is a legend in the world of investigative and national security journalism. As a reporter for the New York Times, Risen broke some of the most important stories of the post 9/11 era, from the warrantless surveillance against Americans conducted under the Bush-Cheney administration, to black prison sites run by the CIA, to failed covert actions in Iran. Risen has won the Pulitzer and other major journalism awards. But perhaps what he is now most famous for is fighting a battle under both the Bush and Obama administrations as they demanded — under threat of imprisonment —the name of one of Risens alleged confidential sources. But it isnt just the government that Risen had to fight. He also battled his own editors and other powerful figures at the New York Times. Risen is now a senior national security correspondent at The Intercept where his incredible inside story has now been published. We talk with Risen about his career at the New York Times in a special edition of Intercepted.</description>
<pubDate>Wed, 03 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>BONUS: All The News Unfit to Print</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>James Risen on His Battles with Bush, Obama, and the New York Times</itunes:subtitle>
<itunes:summary>
<![CDATA[James Risen is a legend in the world of investigative and national security journalism. As a reporter for the New York Times, Risen broke some of the most important stories of the post 9/11 era, from the warrantless surveillance against Americans conducted under the Bush-Cheney administration, to black prison sites run by the CIA, to failed covert actions in Iran. Risen has won the Pulitzer and other major journalism awards. But perhaps what he is now most famous for is fighting a battle under both the Bush and Obama administrations as they demanded — under threat of imprisonment —the name of one of Risens alleged confidential sources. But it isnt just the government that Risen had to fight. He also battled his own editors and other powerful figures at the New York Times. Risen is now a senior national security correspondent at The Intercept where his incredible inside story has now been published. We talk with Risen about his career at the New York Times in a special edition of Intercepted.]]>
</itunes:summary>
<itunes:duration>3805</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[6bdd6660-f039-11e7-acba-33ffde0bb3cc]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY1217453507.mp3" length="60884950" type="audio/mpeg" />
</item>
<item>
<title>Full Metal Jackass</title>
<description>Former Nixon White House counsel John Dean talks about the Mueller investigation, how the CIA may benefit from Trumps presidency and how Trump stacks up to Nixon and Reagan. Pentagon Papers whistleblower Daniel Ellsberg talks about the classified secrets he has kept for decades. He has just published his story in a new book, The Doomsday Machine. Field of Vision takes us inside the very strange world of Steve Bannons films. Patterson Hood of the band Drive-By Truckers performs.</description>
<pubDate>Wed, 13 Dec 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Full Metal Jackass</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Former Nixon Lawyer John Dean and Daniel Ellsberg Analyze the Trump Moment</itunes:subtitle>
<itunes:summary>
<![CDATA[Former Nixon White House counsel John Dean talks about the Mueller investigation, how the CIA may benefit from Trumps presidency and how Trump stacks up to Nixon and Reagan. Pentagon Papers whistleblower Daniel Ellsberg talks about the classified secrets he has kept for decades. He has just published his story in a new book, The Doomsday Machine. Field of Vision takes us inside the very strange world of Steve Bannons films. Patterson Hood of the band Drive-By Truckers performs.]]>
</itunes:summary>
<itunes:duration>5670</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bb062002-9d9f-11e7-b8c4-9701f8d8d38e]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY9016904056.mp3" length="90720966" type="audio/mpeg" />
</item>
<item>
<title>Who's Afraid of the Alt-Deep State?</title>
<description>Matthew Cole joins Jeremy for a discussion about their explosive report in The Intercept that Blackwater founder Erik Prince has been pitching a private spy operation to the White House and CIA. Activist and comedian Randy Credico, who has been hit with a subpoena from the House Intelligence Committee investigating Trump and Russia, joins us.&amp;nbsp; Journalist Barrett Brown talks about the FBIs campaign against him and offers a critique of Wikileaks. Singer Amanda Palmer talks about her provocative new video for a cover she did of Pink Floyds “Mother."</description>
<pubDate>Wed, 06 Dec 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Who's Afraid of the Alt-Deep State?</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump wants to make 1980s Reagan-era covert wars great again.</itunes:subtitle>
<itunes:summary>
<![CDATA[Matthew Cole joins Jeremy for a discussion about their explosive report in The Intercept that Blackwater founder Erik Prince has been pitching a private spy operation to the White House and CIA. Activist and comedian Randy Credico, who has been hit with a subpoena from the House Intelligence Committee investigating Trump and Russia, joins us.&nbsp; Journalist Barrett Brown talks about the FBIs campaign against him and offers a critique of Wikileaks. Singer Amanda Palmer talks about her provocative new video for a cover she did of Pink Floyds “Mother."]]>
</itunes:summary>
<itunes:duration>6260</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bafb58fc-9d9f-11e7-b8c4-070c14a1debb]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY9210981870.mp3" length="100160574" type="audio/mpeg" />
</item>
<item>
<title>Very Bad Men</title>
<description>This week on Intercepted: Sen. Chris Murphy blasts the US government for its role in the destruction of Yemen. Jeremy tears apart Thomas Friedmans gross love letter to the Saudi Crown Prince and talks about the bi-partisan war against journalism from Bill Clinton to Donald Trump. The Intercepts Betsy Reed and Buzzfeeds Katie Baker analyze this unprecedented public fight against sexual assaulters. Analysis from Harare, Zimbabwe on the ouster of Robert Mugabe. Comedian Joe Para performs a dramatic reenactment of a secret Snowden document.</description>
<pubDate>Wed, 29 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Very Bad Men</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Trump, the Saudi Crown Prince, Sexual Assaulters, and Robert Mugabe</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Sen. Chris Murphy blasts the US government for its role in the destruction of Yemen. Jeremy tears apart Thomas Friedmans gross love letter to the Saudi Crown Prince and talks about the bi-partisan war against journalism from Bill Clinton to Donald Trump. The Intercepts Betsy Reed and Buzzfeeds Katie Baker analyze this unprecedented public fight against sexual assaulters. Analysis from Harare, Zimbabwe on the ouster of Robert Mugabe. Comedian Joe Para performs a dramatic reenactment of a secret Snowden document.]]>
</itunes:summary>
<itunes:duration>5565</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baf3e644-9d9f-11e7-b8c4-9f5a004c8b47]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY5979450332.mp3?updated=1511943682" length="89042024" type="audio/mpeg" />
</item>
<item>
<title>The Distraction in Chief</title>
<description>This week on Intercepted: Rami Khouri breaks down the Saudi agenda in the Middle East, its destruction of Yemen and the bizarre case of the exiled Lebanese prime minister. Aram Roston of Buzzfeed, Spencer Ackerman of the Daily Beast, and The Intercepts Matthew Cole join Jeremy for a discussion on the mysterious death of a Green Beret in Mali and why the CIA and US military are quite content with the Trump presidency. Wikileaks slid into Donald Trump Jr.s DMs. Intercept co-founder Glenn Greenwald analyzes what the messages say and how the media covered the story. And we talk to two newly elected Democrats in Virginia: Lee Carter and Elizabeth Guzman. Donald Trump stars in American Beauty.</description>
<pubDate>Wed, 15 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>The Distraction in Chief</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>While the media overwhelmingly focuses on Trump and Russia, Yemen is dying, covert ops are spreading and war is raging.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Rami Khouri breaks down the Saudi agenda in the Middle East, its destruction of Yemen and the bizarre case of the exiled Lebanese prime minister. Aram Roston of Buzzfeed, Spencer Ackerman of the Daily Beast, and The Intercepts Matthew Cole join Jeremy for a discussion on the mysterious death of a Green Beret in Mali and why the CIA and US military are quite content with the Trump presidency. Wikileaks slid into Donald Trump Jr.s DMs. Intercept co-founder Glenn Greenwald analyzes what the messages say and how the media covered the story. And we talk to two newly elected Democrats in Virginia: Lee Carter and Elizabeth Guzman. Donald Trump stars in American Beauty.]]>
</itunes:summary>
<itunes:duration>5716</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baec6cf2-9d9f-11e7-b8c4-832adc6b044a]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY5077597385.mp3" length="91468695" type="audio/mpeg" />
</item>
<item>
<title>Say Hello to My Little Hands</title>
<description>This week on Intercepted: Rep. Ro Khanna calls for a complete end to all U.S. military assistance to Saudi Arabia and the&amp;nbsp; catastrophe in Yemen. The former chief prosecutor at Guantanamo, Col. Morris Davis, blasts Trump over his interference in the case of Army Sergeant Bowe Bergdahl and the recent terror attack in New York. And as the Paradise Papers rock the world of the rich who use offshore banks and law firms, we get analysis from Nomi Prins.</description>
<pubDate>Wed, 08 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Say Hello to My Little Hands</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>True (War) Crimes of the Rich and Infamous</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Rep. Ro Khanna calls for a complete end to all U.S. military assistance to Saudi Arabia and the&nbsp; catastrophe in Yemen. The former chief prosecutor at Guantanamo, Col. Morris Davis, blasts Trump over his interference in the case of Army Sergeant Bowe Bergdahl and the recent terror attack in New York. And as the Paradise Papers rock the world of the rich who use offshore banks and law firms, we get analysis from Nomi Prins.]]>
</itunes:summary>
<itunes:duration>5476</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bae4af26-9d9f-11e7-b8c4-d7bd1cbbac44]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY3384065210.mp3" length="87615947" type="audio/mpeg" />
</item>
<item>
<title>Criminal Indictments at Home, Secret Wars Abroad</title>
<description>This week on Intercepted: New York Times reporter Charlie Savage and former federal prosecutor Ken White of Popehat break down the recent indictment and plea deal and what it may mean for Trump. Investigative journalist Nick Turse and Kenya scholar Samar Al-Bulushi take us into the world of US militarism in Africa: secret drone bases, US commandos and Washington-backed African forces operating under the guise of the “war on terror.” Musician Roberto Lange of Helado Negro performs.</description>
<pubDate>Wed, 01 Nov 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Criminal Indictments at Home, Secret Wars Abroad</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Robert Muellers investigation intensifies as Trump grants the CIA and military new kill powers.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: New York Times reporter Charlie Savage and former federal prosecutor Ken White of Popehat break down the recent indictment and plea deal and what it may mean for Trump. Investigative journalist Nick Turse and Kenya scholar Samar Al-Bulushi take us into the world of US militarism in Africa: secret drone bases, US commandos and Washington-backed African forces operating under the guise of the “war on terror.” Musician Roberto Lange of Helado Negro performs.]]>
</itunes:summary>
<itunes:duration>4504</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[badd1b62-9d9f-11e7-b8c4-bb0a3510ea9d]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY9002073032.mp3" length="72073299" type="audio/mpeg" />
</item>
<item>
<title>Mike Pence is The Koch Brothers' Manchurian Candidate</title>
<description>This week on Intercepted: Investigative journalist Jane Mayer exposes the Koch Brother puppet masters behind Vice President Mike Pences rise to power and the ruthless pursuit of corporate profits that put Pence a heartbeat from the presidency.We speak to Chinese dissident and renown artist Ai Weiwei about the humanitarian catastrophe of the 65 million globally displaced migrants and his new documentary, Human Flow. And we end with Deerhoof's Greg Saunier on the songs of “Mountain Moves.”</description>
<pubDate>Wed, 25 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Mike Pence is The Koch Brothers' Manchurian Candidate</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The ruthless pursuit of corporate profits is a heartbeat from the presidency.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Investigative journalist Jane Mayer exposes the Koch Brother puppet masters behind Vice President Mike Pences rise to power and the ruthless pursuit of corporate profits that put Pence a heartbeat from the presidency.We speak to Chinese dissident and renown artist Ai Weiwei about the humanitarian catastrophe of the 65 million globally displaced migrants and his new documentary, Human Flow. And we end with Deerhoof's Greg Saunier on the songs of “Mountain Moves.”]]>
</itunes:summary>
<itunes:duration>4458</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bad56944-9d9f-11e7-b8c4-036f3898314a]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY6525793662.mp3?updated=1508912939" length="71329332" type="audio/mpeg" />
</item>
<item>
<title>Canada is Racist Too</title>
<description>This week on Intercepted live from Toronto: A recent poll puts activist Desmond Cole in prime position to win the mayorship. We talk to him about Canadas stop and frisk and how Cole would change Toronto. Journalist Naomi Klein warns that the Trudeau and Trump brands may have more in common than expected. And returning Iraqi-Canadian hip-hop artist Narcy gives a powerful live performance.&lt;br&gt;&lt;br&gt;Become a sustaining member! Go to &lt;a href="https://theintercept.com/join"&gt;theintercept.com/join&lt;/a&gt; for more.</description>
<pubDate>Wed, 18 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Canada is Racist Too</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Could a young, radical black activist be the next mayor of Toronto?</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted live from Toronto: A recent poll puts activist Desmond Cole in prime position to win the mayorship. We talk to him about Canadas stop and frisk and how Cole would change Toronto. Journalist Naomi Klein warns that the Trudeau and Trump brands may have more in common than expected. And returning Iraqi-Canadian hip-hop artist Narcy gives a powerful live performance.<br><br>Become a sustaining member! Go to <a href="https://theintercept.com/join">theintercept.com/join</a> for more.]]>
</itunes:summary>
<itunes:duration>4613</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bacdc1bc-9d9f-11e7-b8c4-bf06486207e8]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY1212140419.mp3" length="73809084" type="audio/mpeg" />
</item>
<item>
<title>The White Stuff</title>
<description>Trump sent Mike Pence on a mission to protest black protesters at an NFL game. Acclaimed author and journalist Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more. Mehrsa Baradaran breaks down the roots of economic apartheid in the US, the ongoing impact of slavery on black communities and offers a provocative history of black banks. And the lead singer of Mashrou Leila, Hamed Sinno, talks about being queer and Arab in the Middle East and Trumps America.</description>
<pubDate>Wed, 11 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>The White Stuff</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more.</itunes:subtitle>
<itunes:summary>
<![CDATA[Trump sent Mike Pence on a mission to protest black protesters at an NFL game. Acclaimed author and journalist Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more. Mehrsa Baradaran breaks down the roots of economic apartheid in the US, the ongoing impact of slavery on black communities and offers a provocative history of black banks. And the lead singer of Mashrou Leila, Hamed Sinno, talks about being queer and Arab in the Middle East and Trumps America.]]>
</itunes:summary>
<itunes:duration>5664</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bac62452-9d9f-11e7-b8c4-17ec33943c11]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY7057322394.mp3?updated=1507702418" length="90635702" type="audio/mpeg" />
</item>
<item>
<title>Guns Before Country</title>
<description>This week, Jeremy talks about the Coalition of the Killing — gun lobbyists, politicians and weapons manufacturers — the only beneficiaries of the massacre in Las Vegas. Alynda Segarra of the band Hurray for the Riff Raff explores her Puerto Rican roots and performs new songs. Former US Army Ranger Rory Fanning talks about his slain comrade, NFL star-turned soldier Pat Tillman. Historian Jeanne Theoharis shreds the sanitizing of the legacies of Martin Luther King Jr. and Rosa Parks. And Donald Trump takes his love of guns into the Twilight Zone.&lt;br&gt;&lt;br&gt;Support our show — become a member!&amp;nbsp; &lt;a href="http://theintercept.com/join"&gt;theintercept.com/join&lt;/a&gt;&lt;br&gt;&lt;br&gt;Panoply's podcast listener survey: &lt;a href="http://survey.panoply.fm"&gt;survey.panoply.fm&lt;/a&gt;</description>
<pubDate>Wed, 04 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Guns Before Country</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Lobbyists, politicians and weapons manufacturers are the only beneficiaries of the massacre in Las Vegas. </itunes:subtitle>
<itunes:summary>
<![CDATA[This week, Jeremy talks about the Coalition of the Killing — gun lobbyists, politicians and weapons manufacturers — the only beneficiaries of the massacre in Las Vegas. Alynda Segarra of the band Hurray for the Riff Raff explores her Puerto Rican roots and performs new songs. Former US Army Ranger Rory Fanning talks about his slain comrade, NFL star-turned soldier Pat Tillman. Historian Jeanne Theoharis shreds the sanitizing of the legacies of Martin Luther King Jr. and Rosa Parks. And Donald Trump takes his love of guns into the Twilight Zone.<br><br>Support our show — become a member!&nbsp; <a href="http://theintercept.com/join">theintercept.com/join</a><br><br>Panoply's podcast listener survey: <a href="http://survey.panoply.fm">survey.panoply.fm</a>]]>
</itunes:summary>
<itunes:duration>4763</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[babd79c4-9d9f-11e7-b8c4-bfa2bf7b2870]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY1463981678.mp3" length="76216529" type="audio/mpeg" />
</item>
<item>
<title>For Whom the Trump Trolls</title>
<description>This week on Intercepted, physicist David Wright from the Union of Concerned Scientists explains how easy it would be for Trump to launch a nuclear strike. Professor James Fernandez of NYU talks about the Abraham Lincoln Brigade, the 3,000 Americans who tried to stop fascism before it spread in Europe. We speak with the directors of a haunting new film about a terror attack in an Israeli bus station that leads to the brutal mob killing of an innocent Eritrean immigrant. And Donald Trump gets a visit from the two Bobs in his Office Space.</description>
<pubDate>Wed, 27 Sep 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>For Whom the Trump Trolls</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>What the Abraham Lincoln Brigade can teach us about fighting fascism in the 21st century.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted, physicist David Wright from the Union of Concerned Scientists explains how easy it would be for Trump to launch a nuclear strike. Professor James Fernandez of NYU talks about the Abraham Lincoln Brigade, the 3,000 Americans who tried to stop fascism before it spread in Europe. We speak with the directors of a haunting new film about a terror attack in an Israeli bus station that leads to the brutal mob killing of an innocent Eritrean immigrant. And Donald Trump gets a visit from the two Bobs in his Office Space.]]>
</itunes:summary>
<itunes:duration>4754</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bab3035e-9d9f-11e7-b8c4-a723efdad05b]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY7212168126.mp3?updated=1506529903" length="76071497" type="audio/mpeg" />
</item>
<item>
<title>'Merican Psycho</title>
<description>Jeremy analyzes Trumps belligerent UN speech and the massive military budget the Democrats just gave him. Journalist Gary Rivlin takes us deep inside the world of the Goldman Sachs executives now working for Trump. Poet Aja Monet performs. The Intercepts Alice Speri investigates the militarization of police and how Israel is training American cops. Plus, Donald Trump stars in American Psycho.</description>
<pubDate>Wed, 20 Sep 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>'Merican Psycho</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump visits the UN and returns some videotapes.</itunes:subtitle>
<itunes:summary>
<![CDATA[Jeremy analyzes Trumps belligerent UN speech and the massive military budget the Democrats just gave him. Journalist Gary Rivlin takes us deep inside the world of the Goldman Sachs executives now working for Trump. Poet Aja Monet performs. The Intercepts Alice Speri investigates the militarization of police and how Israel is training American cops. Plus, Donald Trump stars in American Psycho.]]>
</itunes:summary>
<itunes:duration>4370</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baa7cb06-9d9f-11e7-b8c4-d7ae2711974f]]></guid>
<enclosure url="http://traffic.megaphone.fm/PPY8078356160.mp3" length="69924570" type="audio/mpeg" />
</item>
<item>
<title>The Super Bowl of Racism</title>
<description>NSA whistleblower Edward Snowden discusses the massive Equifax data breach and allegations of Russian interference in the US election. Commentator Shaun King explains his call for a boycott of the NFL and talks about his campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle performs.</description>
<pubDate>Wed, 13 Sep 2017 10:00:00 -0000</pubDate>
@@ -45,7 +274,7 @@
<itunes:summary>
<![CDATA[This week on Intercepted: Jeremy gives an update on the aftermath of Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang lays out how a network of libertarian think tanks called the Atlas Network is insidiously shaping political infrastructure in Latin America. We speak with attorney and former Hugo Chavez adviser Eva Golinger about the Venezuela's political turmoil.And we hear Claudia Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her music and hopes for Venezuela.]]>
</itunes:summary>
<itunes:duration>4220</itunes:duration>
<itunes:duration>4415</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[7c207a24-e33f-11e6-9438-eb45dcf36a1d]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5331443769.mp3" length="67527575" type="audio/mpeg" />
@@ -60,7 +289,7 @@
<itunes:summary>
<![CDATA[News from the White House this week has been like a twisted mash up of Here Comes Honey Boo Boo, Macbeth, Project Runway and a Mr. Bean movie. Dime-store Sopranos reject Anthony Scaramucci was fired after just 10 days as White House communications director. Reince Priebus is out as chief of staff, Gen. John Kelly is in. And with spiking tensions between the United States and North Korea, we reflect on the history of the region. Plus, The Intercepts Naomi Klein talks to U.K. Labour Party leader Jeremy Corbyn about the lessons the Democratic Party could learn from Corbyns unexpected electoral success.]]>
</itunes:summary>
<itunes:duration>3517</itunes:duration>
<itunes:duration>3712</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5850753c-dcf9-11e6-a5a2-a7df163d0693]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL4502761802.mp3" length="56280711" type="audio/mpeg" />
@@ -75,7 +304,7 @@
<itunes:summary>
<![CDATA[With all the constant hype about Russia, youd think we were living in a new Cold War. This week on Intercepted: Glenn Greenwald fills in for Jeremy Scahill, and we take a deep dive into the origins and evolution of the Trump-Russia story. Fox News' Tucker Carlson and Glenn find something they can actually agree on (the Democratic establishments Russia hysteria), but diverge on Tuckers coverage of immigration and crime. Russian-American writer Masha Gessen explains how conspiracy thinking is a mirror of the leaders we put in power.]]>
</itunes:summary>
<itunes:duration>3370</itunes:duration>
<itunes:duration>3565</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[584711b8-dcf9-11e6-a5a2-d7a378461c40]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL8633314507.mp3" length="53935124" type="audio/mpeg" />
@@ -90,7 +319,7 @@
<itunes:summary>
<![CDATA[Donald Trump enjoyed playing fireman and asking where the fire is. Hint: all around you, Mr. President. This week on Intercepted: the famed rebel academic, Alfred McCoy, whose book on narcotrafficking the CIA tried to stop from being published, lays out his meticulously argued theory that the U.S. empire will fall by the year 2030. The Washington Posts media columnist, Margaret Sullivan, talks about Trump ratcheting up the war on whistleblowers and the existence of a free press.]]>
</itunes:summary>
<itunes:duration>3951</itunes:duration>
<itunes:duration>4146</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[583dc8f6-dcf9-11e6-a5a2-97233491f3c8]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL4964577496.mp3" length="63216744" type="audio/mpeg" />
@@ -105,7 +334,7 @@
<itunes:summary>
<![CDATA[This week on Intercepted: Don Jr. is in the shit throne over a secret meeting he had with a Russian lawyer. Could this be, as many in the media are claiming, the smoking gun of Russia collusion? Intercept co-founder Glenn Greenwald weighs in and debunks a forged NSA document sent to Rachel Maddow. Intercept reporters Alice Speri and Alleen Brown talk about the shadowy mercenary company TigerSwan. We also hear music from Victoria Ruiz of the punk band Downtown Boys.]]>
</itunes:summary>
<itunes:duration>3864</itunes:duration>
<itunes:duration>4059</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5834b428-dcf9-11e6-a5a2-f7aca16eec6e]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5169968320.mp3" length="61824940" type="audio/mpeg" />
@@ -120,7 +349,7 @@
<itunes:summary>
<![CDATA[President Trump said when it comes to health insurance, he would cover everyone. He lied. Meanwhile the Crown Prince of America, Jared Kushner, and Mohammed Bin Salman, Crown Prince of Saudi Arabia, play house with foreign policy. This week: Al Jazeeras Mehdi Hasan fills in for Jeremy Scahill. Intercept reporter Murtaza Hussain and journalist Rula Jebreal discuss the global consequences of the House of Trumps meddling in the Middle East. Historian Tom Holland joins Mehdi for a debate on the role of Islam within the Islamic State. Plus, actor Bill Camp reprises his role as the “SIGINT Philosopher.”]]>
</itunes:summary>
<itunes:duration>3402</itunes:duration>
<itunes:duration>3597</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5825189c-dcf9-11e6-a5a2-3765693ebff5]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5926659703.mp3" length="54443781" type="audio/mpeg" />
@@ -135,7 +364,7 @@
<itunes:summary>
<![CDATA[While all eyes in Washington remain focused on the Russia investigation, a Republican firm forgot to secure its invasive personal data on 198 million American voters. This week on Intercepted: We speak to radical librarian Alison Macrina of the Library Freedom Project about the fight against digital surveillance. Sam Biddle gives an update on attacks on U.S. voting systems. And, we speak with one of the rising stars of the “dirtbag left,” Felix Biederman of Chapo Trap House.]]>
</itunes:summary>
<itunes:duration>3345</itunes:duration>
<itunes:duration>3540</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[581dd44c-dcf9-11e6-a5a2-03edebf2031b]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL7980248897.mp3" length="53535555" type="audio/mpeg" />
@@ -150,7 +379,7 @@
<itunes:summary>
<![CDATA[Donald Trump has a great affinity for strongmen and for unquestioned loyalty of those who work for him. This week on Intercepted: Trumps besties in Saudi Arabia convinced him that Qatar is the premiere Arab nation sponsoring terrorism. Amnesty Internationals Sherine Tadros and al Jazeeras Mehdi Hasan analyze the hypocrisy-laden, bizarre crisis. Jeremy discusses the prosecution of an alleged NSA leaker. MSNBCs Chris Hayes talks Russia, Trump, the media and his new book A Colony in a Nation. DJ Spooky imagines a Trump-inspired mash-up of Dantes Inferno and Disco Inferno.]]>
</itunes:summary>
<itunes:duration>4151</itunes:duration>
<itunes:duration>4346</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5815ed86-dcf9-11e6-a5a2-ab3d4ad4b944]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL2441335022.mp3?updated=1497422014" length="66430432" type="audio/mpeg" />
@@ -165,7 +394,7 @@
<itunes:summary>
<![CDATA[The Green Partys Jill Stein has been widely attacked by Democrats simply for running for president. Some blame her for Hillary Clintons loss. This week, Stein strikes back at her critics and reveals the story behind the infamous Moscow dinner where she was seated with Vladimir Putin and Gen. Michael Flynn. The Intercepts DC bureau chief Ryan Grim digs into the contents of a newly published top secret NSA document outlining alleged Russian cyberattacks against software companies that service U.S. elections. And singer-songwriter Damien Jurado performs.]]>
</itunes:summary>
<itunes:duration>3498</itunes:duration>
<itunes:duration>3693</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[580e21d2-dcf9-11e6-a5a2-53fae963a8d5]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5889277506.mp3?updated=1496817965" length="55977273" type="audio/mpeg" />
@@ -180,7 +409,7 @@
<itunes:summary>
<![CDATA[This week, the scandal spotlight shines on Trumps influential (and strangely quiet) son-in-law. We talk to national security correspondent Spencer Ackerman of The Daily Beast about Jared Kushners alleged meetings with Russian officials to establish back channel communications. Organizer and scholar Mariame Kaba offers a peoples history of prisons in the US and the politicians—both Democrats and Republicans—who have made them what they are today. And we hear an incredible rendition of “The Partisan” from composers and musicians Leo Heiblum of Mexico and Tenzin Choegyal of Tibet.&nbsp;]]>
</itunes:summary>
<itunes:duration>3567</itunes:duration>
<itunes:duration>3762</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5807254e-dcf9-11e6-a5a2-cb45327dca79]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3830941587.mp3" length="57086537" type="audio/mpeg" />
@@ -195,7 +424,7 @@
<itunes:summary>
<![CDATA[This week, Donald Trump stood in a sea of tyrants and joined in a bizarre group petting of a glowing white orb. Professor Asad AbuKhalil dissects Trumps summit in Saudi Arabia and the role Trumps friends in the Middle East play in fueling such horrors as the attack on Manchester. The Intercepts new DC bureau chief, Ryan Grim, and national security reporter Matthew Cole discuss Gen. Michael Flynn and whether anyone in the Trump administration realizes how insane their boss is. And Steve Earle performs live.]]>
</itunes:summary>
<itunes:duration>4003</itunes:duration>
<itunes:duration>4198</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57ffd3e8-dcf9-11e6-a5a2-17ed73ff2f09]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3575958410.mp3" length="64048065" type="audio/mpeg" />
@@ -210,7 +439,7 @@
<itunes:summary>
<![CDATA[Donald Trump is spectacularly bad at being president. This week on Intercepted, investigative journalist Marcy Wheeler and The Intercepts Glenn Greenwald analyze the latest insanity emanating from the White House. Pulitzer Prize-winning journalist Tim Weiner and Intercept writer Trevor Aaronson discuss the firing of James Comey and debate his FBI legacy. And Palestinian author and journalist Rula Jebreal explains why President Trump is going to Saudi Arabia and Israel on his first international trip.]]>
</itunes:summary>
<itunes:duration>3666</itunes:duration>
<itunes:duration>3861</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57f826de-dcf9-11e6-a5a2-ff41a0b1698e]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3660243774.mp3" length="58666422" type="audio/mpeg" />
@@ -225,7 +454,7 @@
<itunes:summary>
<![CDATA[Donald Trumps complicated relationship with FBI Director James Comey came to a shocking conclusion in Tuesday nights episode of American shitshow. Glenn Greenwald analyzes Comeys firing. Next week, Chelsea Manning will be freed from prison. We hear exclusive audio from her trial and talk to journalist Alexa OBrien. And French civil liberties activist Yasser Louati says despite her defeat in the presidential election, many of Marine Le Pens ideas are already embedded in mainstream French politics. And a premiere track from hip-hop artists MC Sole and DJ Pain 1.]]>
</itunes:summary>
<itunes:duration>4002</itunes:duration>
<itunes:duration>4197</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57ef98d4-dcf9-11e6-a5a2-374c19bb24e7]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5670631624.mp3" length="64045557" type="audio/mpeg" />
@@ -255,7 +484,7 @@
<itunes:summary>
<![CDATA[Wikileaks founder Julian Assange hits back at Trumps CIA director Mike Pompeo after Pompeo accused Wikileaks of being a “hostile non-state intelligence agency.” In a wide-ranging interview, Assange discusses the allegations Wikileaks was abetted by Russian intelligence in its publication of DNC emails, and the new-found admiration for him by FOX News and Donald Trump. Also, why Assange believes he and Hillary Clinton may get along if they ever met in person. And we premiere an unreleased song by Tom Morello of Rage Against the Machine.]]>
</itunes:summary>
<itunes:duration>3706</itunes:duration>
<itunes:duration>3901</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57e86f28-dcf9-11e6-a5a2-3f3b6bf611af]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5660744294.mp3" length="59298377" type="audio/mpeg" />
@@ -270,7 +499,7 @@
<itunes:summary>
<![CDATA[Nothing brings warmongers, hawks and elites from both parties closer than a cruise missile strike. This weeks Intercepted will piss off Assad supporters and the Democrats and Republicans fawning over Trumps newest war. Former Congressman Dennis Kucinich questions the official story on the chemical weapons attack. Murtaza Hussain on what Assad gains by using chemical weapons. And, Maher Arar is a Syrian-born Canadian engineer who was kidnapped at JFK airport by US operatives after 9/11 and rendered to Syria and tortured by Assads agents. Arar says he opposes Assad and US intervention. All that and a bucket of media stupidity to celebrate beautiful missiles.]]>
</itunes:summary>
<itunes:duration>3569</itunes:duration>
<itunes:duration>3764</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57e131d6-dcf9-11e6-a5a2-efc4038c6546]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL8700626063.mp3" length="57118720" type="audio/mpeg" />
@@ -285,7 +514,7 @@
<itunes:summary>
<![CDATA[Erik Prince—the most infamous mercenary in modern U.S. history—is Trumps secret emissary. This week, an exclusive interview with Rep. Jan Schakowsky, who has fought a decades-long battle against Prince. Tavis Smiley talks about the “Santa Claus-ification” of Dr. Martin Luther King Jr. on the 50th anniversary of Kings militant speech against the Vietnam War. Rep. Barbara Lee reflects on her own historic anti-war speech, delivered three days after 9/11. And Vice President Pence, who cant be alone in a room with a woman who is not his wife, goes Psycho.<br><br><em>Please take a moment to fill out Panoply's survey about the shows you listen to, love, and what else you'd like to hear: </em><a href="http://survey.panoply.fm"><em>survey.panoply.fm</em></a><em>.&nbsp; Many thanks!</em>]]>
</itunes:summary>
<itunes:duration>3428</itunes:duration>
<itunes:duration>3623</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57da1dce-dcf9-11e6-a5a2-2fa5756ae4a7]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL7737191155.mp3?updated=1491374970" length="54858396" type="audio/mpeg" />
@@ -300,7 +529,7 @@
<itunes:summary>
<![CDATA[Donald Trump officially rejects climate change and unofficially declares war on planet Earth. Naomi Klein takes us on a terrifying journey into Trumps real life version of The Purge. Boots Riley of The Coup discusses Trump and hip hop and performs. Murtaza Hussain talks about the US bombings in Iraq and Syria that have killed 1,000 civilians in one month. And, we talk to the developer of an app that tracks US drone strikes that Apple has censored 13 times.]]>
</itunes:summary>
<itunes:duration>3317</itunes:duration>
<itunes:duration>3512</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57d2f170-dcf9-11e6-a5a2-5fc4825a8119]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL9529758061.mp3" length="53079980" type="audio/mpeg" />
@@ -315,7 +544,7 @@
<itunes:summary>
<![CDATA[Donald Trump has not started any new wars… yet. But his administration is pouring gasoline on several initiated by his predecessors. This week on Intercepted: US forces are deploying in Syria, as drone strikes expand in Yemen. And Russia and Iran loom over everything. We talk to veteran war correspondents Anand Gopal and Iona Craig. Glenn Greenwald analyzes James Comeys testimony on Capitol Hill and exposes a major lie spread about Edward Snowden. Actor William Camp “stars” in the real life story of the spy who became “the Socrates of the NSA.”]]>
</itunes:summary>
<itunes:duration>3488</itunes:duration>
<itunes:duration>3683</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57cc3f10-dcf9-11e6-a5a2-4fe59e09c4e9]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL9134059577.mp3" length="55811761" type="audio/mpeg" />
@@ -330,7 +559,7 @@
<itunes:summary>
<![CDATA[This week, Intercepted is live from the SXSW Festival in Austin. Edward Snowden joins us via video feed from Moscow. He discusses Trumps allegations of Obamas wiretapping, analyzes some of the CIAs hacking capabilities, and blasts critics who accuse him of being a Russian agent. And we talk to Libyan-American hip hop artist Kayem, who was forced to keep a low profile the past several years after multiple detentions and visits from the FBI. He shares some verses with Intercepted.]]>
</itunes:summary>
<itunes:duration>3136</itunes:duration>
<itunes:duration>3331</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57c55fa6-dcf9-11e6-a5a2-1f24485c4305]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3645242256.mp3" length="50191046" type="audio/mpeg" />
@@ -345,7 +574,7 @@
<itunes:summary>
<![CDATA[The Notorious B.I.G. famously alleged that federal agents were mad because he was flagrant. Trump also believes he has beef with the Feds, accusing Obama of tapping his phones. The Intercepts Matthew Cole and journalist Marcy Wheeler dissect the accusations and the (curious) denials. Sam Biddle and Josh Begley explain what the CIA hacking docs published by Wikileaks say about our “smart” TVs and phones. Journalist Aura Bogado confronts Trumps assault on undocumented immigrants. Punk band Anti-Flag performs. Plus, Trump “stars” in a scene from Goodfellas. Can he get out of Mar-a-Lago alive?]]>
</itunes:summary>
<itunes:duration>3987</itunes:duration>
<itunes:duration>4182</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57be3b36-dcf9-11e6-a5a2-07d3f1f2cb5f]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3152884319.mp3" length="63798125" type="audio/mpeg" />
@@ -360,7 +589,7 @@
<itunes:summary>
<![CDATA[Ex-CIA analyst Nada Bakos and former FBI agent Clint Watts explain how Trumps administration could use “alternative intelligence” to justify dangerous military actions. Shane Bauer of Mother Jones breaks down the connections between immigration raids and soaring private prison profits. Plus the world premiere of a song by the Iraqi-Canadian hip hop artist Narcy. We bet you never thought youd hear Steve Bannons name rapped in autotune.]]>
</itunes:summary>
<itunes:duration>4001</itunes:duration>
<itunes:duration>4196</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57b74678-dcf9-11e6-a5a2-4fbc5ae0d0cf]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5707421213.mp3" length="64025913" type="audio/mpeg" />
@@ -375,7 +604,7 @@
<itunes:summary>
<![CDATA[New York Times investigative reporter James Risen breaks down Trumps declaration that journalists are the enemy and analyzes Trumps royal court. ACLU lawyer Chase Strangio and former New England Patriots star Donté Stallworth talk about the war on the transgender community and the rising resistance of pro athletes. Sam Biddle exposes the Trump-connected firm that helped the NSA spy on the world and actor Wallace Shawn stars as an NSA operative who is worried about adversaries spying on his luncheons. Plus music from Anohni.]]>
</itunes:summary>
<itunes:duration>4014</itunes:duration>
<itunes:duration>4209</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57b018d0-dcf9-11e6-a5a2-e736fa72fede]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL3910130795.mp3" length="64229877" type="audio/mpeg" />
@@ -390,7 +619,7 @@
<itunes:summary>
<![CDATA[The first contestant in Donald Trumps reality administration has left the West Wing. This week, Glenn Greenwald offers some provocative pushback on the Russia fear-mongering surrounding Gen. Michael Flynns resignation (or firing). Naomi Klein walks the dark aisles of the Trump family department store. Former Congresswoman Liz Holtzman, a key figure in the impeachment of Richard Nixon, explains how impeachment actually works. And Hina Shamsi of the ACLU recounts her interrogation at the border. Plus a performance from Jedi Mind Tricks.&nbsp;]]>
</itunes:summary>
<itunes:duration>3663</itunes:duration>
<itunes:duration>3858</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57a87fd0-dcf9-11e6-a5a2-af85a8453351]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL5616910839.mp3" length="58608326" type="audio/mpeg" />
@@ -405,7 +634,7 @@
<itunes:summary>
<![CDATA[This week, investigative reporter Allan Nairn breaks down Trump's relationship with the CIA and the killer assembly of neocons and right-wing conspiracists running the U.S. war machine. Princeton professor Keeanga Yamahtta-Taylor dismantles Obama's problematic legacy and offers strategic advice for resisting Trump. The Intercept's own distinguished alt-historian, Jon Schwarz, offers a lesson on the origins of presidential executive orders. And Kimya Dawson gives a raw performance of a new song about racism and the police state.]]>
</itunes:summary>
<itunes:duration>3557</itunes:duration>
<itunes:duration>3752</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57a133b0-dcf9-11e6-a5a2-9f64a29807d9]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL7005027452.mp3" length="56920189" type="audio/mpeg" />
@@ -420,7 +649,7 @@
<itunes:summary>
<![CDATA[Donald Trump is signing executive orders like autographed pictures. This week on Intercepted: Two former senior FBI agents blast the “Muslim ban” and Trumps campaign to make torture great again. Constitutional rights lawyers dissect the (il)legalities of Trumps orders. Rep. Barbara Lee confronts the president's terrifying approach to government.&nbsp; New secret documents reveal how Trump could resurrect J. Edgar Hoovers legacy. Brother Ali freestyles a verse, and Peter Sarsgaard stars in the bizarre true story of an NSA operative with vacation tips for deploying to Guantanamo.&nbsp;]]>
</itunes:summary>
<itunes:duration>3163</itunes:duration>
<itunes:duration>3358</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57996266-dcf9-11e6-a5a2-4ff22525cee4]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL4823102330.mp3?updated=1485937504" length="50611513" type="audio/mpeg" />
@@ -435,7 +664,7 @@
<itunes:summary>
<![CDATA[The clock struck thirteen on January 20, Donald Trump is the president of the United States and episode one of Intercepted is here. Intercept co-founder Glenn Greenwald and editor-in-chief Betsy Reed join Jeremy Scahill for a discussion on the crazy apocalyptic present. They break down Trumps attacks on the media, that insane speech he gave at the CIA and the state of the Democratic party. Naomi Klein sends in a dispatch from the Womens March on Washington. Jeremy goes deep into the secretive world of Seymour Hershs kitchen, and shoots the shit with the legendary Pulitzer Prize-winning journalist about why he calls Trump a “circuit breaker." And we hear a spoken word performance from hip-hop artist Immortal Technique.&nbsp;]]>
</itunes:summary>
<itunes:duration>3238</itunes:duration>
<itunes:duration>3433</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57913302-dcf9-11e6-a5a2-87c6a559fb64]]></guid>
<enclosure url="http://traffic.megaphone.fm/FL1844876464.mp3" length="51822341" type="audio/mpeg" />
@@ -7,8 +7,8 @@
<generator>Feeder 2.5.12(2294); Mac OS X Version 10.12.1 (Build 16B2657) http://reinventedsoftware.com/feeder/</generator>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<language>en</language>
<pubDate>Tue, 07 Nov 2017 18:47:35 -0800</pubDate>
<lastBuildDate>Tue, 07 Nov 2017 18:47:35 -0800</lastBuildDate>
<pubDate>Tue, 16 Jan 2018 20:28:23 -0800</pubDate>
<lastBuildDate>Tue, 16 Jan 2018 20:28:23 -0800</lastBuildDate>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:image href="http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg" />
@@ -17,6 +17,188 @@
<itunes:block>no</itunes:block>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/linuxunplugged" /><feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" uri="linuxunplugged" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><media:copyright>Copyright Jupiter Broadcasting</media:copyright><media:thumbnail url="http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg" /><media:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</media:keywords><media:category scheme="http://www.itunes.com/dtds/podcast-1.0.dtd">Technology/Tech News</media:category><itunes:owner><itunes:email>chris@jupiterbroadcasting.com</itunes:email><itunes:name>Jupiter Broadcasting</itunes:name></itunes:owner><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords><itunes:subtitle>Linux Action Show, with no rules.</itunes:subtitle><itunes:summary>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.</itunes:summary><itunes:category text="Technology"><itunes:category text="Tech News" /></itunes:category><image><link>http://www.jupiterbroadcasting.com/show/linuxun/</link><url>http://www.jupiterbroadcasting.com/images/lupbadge.jpg</url><title>LINUX Unplugged</title></image><item>
<title>The Secret to Future Linux Success | LUP 232</title>
<link>http://www.jupiterbroadcasting.com/121542/the-secret-to-future-linux-success-lup-232/</link>
<description><![CDATA[<p>A big week of community updates, events & news, including great news for Plasma Desktop users, MATE users & Wayland fans.</p>
<p>Then Barton George from Dell joins us to discuss the new XPS 13s shipping Ubuntu, where Linux could see its next big success & more!</p>]]></description>
<pubDate>Tue, 16 Jan 2018 20:28:02 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0232.mp3" length="50481539" type="audio/mpeg" />
<guid isPermaLink="false">903D4C53-092B-4131-BA52-08DBCDE55E3E</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>A big week of community updates, events &amp; news, including great news for Plasma Desktop users, MATE users &amp; Wayland fans. Then Barton George from Dell joins us to discuss the new XPS 13s shipping Ubuntu, where Linux could see its next big success &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[A big week of community updates, events & news, including great news for Plasma Desktop users, MATE users & Wayland fans.
Then Barton George from Dell joins us to discuss the new XPS 13s shipping Ubuntu, where Linux could see its next big success & more!]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:43:53</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2018/01/lup-0232-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0232.mp3" fileSize="50481539" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Most Expensive Linux Distro Ever | LUP 231</title>
<link>http://www.jupiterbroadcasting.com/121257/most-expensive-linux-distro-ever-lup-231/</link>
<description><![CDATA[<p>We slay the Gentoo challenge monster & give you our first take of the most expensive Linux distro weve ever tried. What does nearly $100 of Linux get you? We find out!</p>
<p>Plus tons of community news, the perfect Linux workstation coming soon & more!</p>]]></description>
<pubDate>Tue, 09 Jan 2018 20:33:30 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0231.mp3" length="47946199" type="audio/mpeg" />
<guid isPermaLink="false">2B587F89-B71C-4B5C-8DFF-EFF1106E8B81</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>We slay the Gentoo challenge monster &amp; give you our first take of the most expensive Linux distro weve ever tried. What does nearly $100 of Linux get you? We find out!
Plus tons of community news, the perfect Linux workstation coming soon &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[We slay the Gentoo challenge monster & give you our first take of the most expensive Linux distro weve ever tried. What does nearly $100 of Linux get you? We find out!
Plus tons of community news, the perfect Linux workstation coming soon & more!]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:38:36</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2018/01/lup-0231-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0231.mp3" fileSize="47946199" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Invest In Popcorn | LUP 230</title>
<link>http://www.jupiterbroadcasting.com/121092/invest-in-popcorn-lup-230/</link>
<description><![CDATA[Wes & the Beard kick Chris out to share their top tips for starting 2018 out right, plus a holiday surprise from Linux Journal, a new device for Googles Fuchsia & an unfortunate new flaw in a processor near you.]]></description>
<pubDate>Tue, 02 Jan 2018 23:18:25 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0230.mp3" length="27993036" type="audio/mpeg" />
<guid isPermaLink="false">AFC10D98-612B-4C12-BB5C-06A09B09681C</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>Wes &amp; the Beard kick Chris out to share their top tips for starting 2018 out right, plus a holiday surprise from Linux Journal, a new device for Googles Fuchsia &amp; an unfortunate new flaw in a processor near you.</itunes:subtitle>
<itunes:summary><![CDATA[Wes & the Beard kick Chris out to share their top tips for starting 2018 out right, plus a holiday surprise from Linux Journal, a new device for Googles Fuchsia & an unfortunate new flaw in a processor near you.]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>57:02</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2018/01/lup-0230-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0230.mp3" fileSize="27993036" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Taste of Linux 2017 | LUP 229</title>
<link>http://www.jupiterbroadcasting.com/120922/taste-of-linux-2017-lup-229/</link>
<description><![CDATA[We break from the unformat of the show for a special holiday chat about the top moments in the world of Linux this year that impacted us the most.]]></description>
<pubDate>Tue, 26 Dec 2017 18:29:58 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0229.mp3" length="41841905" type="audio/mpeg" />
<guid isPermaLink="false">FE1EC5F7-9DCF-4365-B5A6-096FAE104908</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>We break from the unformat of the show for a special holiday chat about the top moments in the world of Linux this year that impacted us the most.</itunes:subtitle>
<itunes:summary><![CDATA[We break from the unformat of the show for a special holiday chat about the top moments in the world of Linux this year that impacted us the most.]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:25:53</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/12/lup-0229-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0229.mp3" fileSize="41841905" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>rm -rf 2017 | LUP 228</title>
<link>http://www.jupiterbroadcasting.com/120772/rm-rf-2017-lup-228/</link>
<description><![CDATA[<p>We debate the best distros of 2017, get into some community news, and a bcachefs and Gentoo challenge update & also learn a bit about Canonicals new Multipass project.</p>
<p>Plus a few Linux commands that are guaranteed to destroy your install.</p>]]></description>
<pubDate>Tue, 19 Dec 2017 20:04:09 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0228.mp3" length="37338604" type="audio/mpeg" />
<guid isPermaLink="false">083E11EA-D405-4C53-AB09-6C4FC13DDD81</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>We debate the best distros of 2017, get into some community news, and a bcachefs and Gentoo challenge update &amp; also learn a bit about Canonicals new Multipass project.
Plus a few Linux commands that are guaranteed to destroy your install.</itunes:subtitle>
<itunes:summary><![CDATA[We debate the best distros of 2017, get into some community news, and a bcachefs and Gentoo challenge update & also learn a bit about Canonicals new Multipass project.
Plus a few Linux commands that are guaranteed to destroy your install.]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:16:30</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/12/lup-0228-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0228.mp3" fileSize="37338604" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Peer Pressure | LUP 227</title>
<link>http://www.jupiterbroadcasting.com/120622/peer-pressure-lup-227/</link>
<description><![CDATA[<p>Its time to replace Patreon, YouTube, Twitter/Facebook & all the other centralized platforms of the web. But can open source answer the call? This week we look at a few projects that could replace todays information silos if Linux users just step up.</p>
<p>Plus community news, some big updates & a lot more!</p>]]></description>
<pubDate>Thu, 14 Dec 2017 01:31:44 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0227.mp3" length="45057056" type="audio/mpeg" />
<guid isPermaLink="false">0533598D-EDA5-4757-ACF0-12F3142C9713</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>Its time to replace Patreon, YouTube, Twitter/Facebook &amp; other centralized platforms of the web. But can open source answer the call? This week we look at a few projects that could replace todays information silos if Linux users just step up &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[Its time to replace Patreon, YouTube, Twitter/Facebook & all the other centralized platforms of the web. But can open source answer the call? This week we look at a few projects that could replace todays information silos if Linux users just step up.
Plus community news, some big updates & a lot more!]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:32:35</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/12/lup-0227-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0227.mp3" fileSize="45057056" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Bitcoin for Linux Users | LUP 226</title>
<link>http://www.jupiterbroadcasting.com/120447/bitcoin-for-linux-users-lup-226/</link>
<description><![CDATA[Why Bitcoin is the next Linux, the Gentoo Challenge is in full swing, and we catch you up on the latest community news, a throwback app pick & more!]]></description>
<pubDate>Tue, 05 Dec 2017 20:58:00 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0226.mp3" length="40776318" type="audio/mpeg" />
<guid isPermaLink="false">E457EE68-10F2-4AE7-929B-8C6F93342988</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>Why Bitcoin is the next Linux, the Gentoo Challenge is in full swing, and we catch you up on the latest community news, a throwback app pick &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[Why Bitcoin is the next Linux, the Gentoo Challenge is in full swing, and we catch you up on the latest community news, a throwback app pick & more!]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:23:40</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/12/lup-0226-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0226.mp3" fileSize="40776318" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Hacking the Community | LUP 225</title>
<link>http://www.jupiterbroadcasting.com/120287/hacking-the-community-lup-225/</link>
<description><![CDATA[<p>Red Hat, Amazon, Facebook, Google, IBM, and others come together to push common sense GPL enforcement & a whole batch of community news.</p>
<p>Plus we call out the Register, DRMs dirty little secret & how Linux users can make a difference.</p>]]></description>
<pubDate>Wed, 29 Nov 2017 00:17:08 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0225.mp3" length="48188615" type="audio/mpeg" />
<guid isPermaLink="false">30BE2BE8-E0B0-4DFD-A1D2-EAE0F4DA4BBB</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>Red Hat, Amazon, Facebook, Google, IBM, and others come together to push common sense GPL enforcement &amp; a whole batch of community news.
Plus we call out the Register, DRMs dirty little secret &amp; how Linux users can make a difference.</itunes:subtitle>
<itunes:summary><![CDATA[Red Hat, Amazon, Facebook, Google, IBM, and others come together to push common sense GPL enforcement & a whole batch of community news.
Plus we call out the Register, DRMs dirty little secret & how Linux users can make a difference.]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:39:07</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/11/lup-0225-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0225.mp3" fileSize="48188615" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>No Escape from Google | LUP 224</title>
<link>http://www.jupiterbroadcasting.com/120086/no-escape-from-google-lup-224/</link>
<description><![CDATA[<p>Google gets caught red handed, we find lots of goodies in the new Linux kernel & we have three great new app picks this week.</p>
<p>But the meat of the show is Lynis a tool to audit your Linux box, create reports & teach you how to better secure your system. </p>
<p>Plus we officially lay the groundwork for the Gentoo Challenge.</p>]]></description>
<pubDate>Wed, 22 Nov 2017 04:14:57 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0224.mp3" length="40944756" type="audio/mpeg" />
<guid isPermaLink="false">C2E924CC-9F00-468B-9D33-AF163FE2D58F</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>Google gets caught red handed and goodies in the new Linux kernel. But the meat of the show is Lynis a tool to audit your Linux box, create reports &amp; teach you how to better secure your system.
Plus the groundwork for the Gentoo Challenge &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[Google gets caught red handed, we find lots of goodies in the new Linux kernel & we have three great new app picks this week.
But the meat of the show is Lynis a tool to audit your Linux box, create reports & teach you how to better secure your system.
Plus we officially lay the groundwork for the Gentoo Challenge.]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:24:01</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/11/lup-0224-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0224.mp3" fileSize="40944756" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>Fedoras New Trick | LUP 223</title>
<link>http://www.jupiterbroadcasting.com/119866/fedoras-new-trick-lup-223/</link>
<description><![CDATA[<p>A new version of Fedora hits the web and we share our thoughts & chat with a member of the project, Noah joins us to answer your live calls & were all excited about Firefoxs new quantum release.</p>
<p>Plus Gnome 4s ambitious goals, a new Linux Kernel that really matters, OpenShot woes & more!</p>]]></description>
<pubDate>Wed, 15 Nov 2017 02:37:23 -0800</pubDate>
<enclosure url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0223.mp3" length="53118235" type="audio/mpeg" />
<guid isPermaLink="false">205F5F07-AB94-4857-AE6D-6B3EB5C0E06C</guid>
<itunes:author>Jupiter Broadcasting</itunes:author>
<itunes:subtitle>A new version of Fedora hits the web and we share our thoughts &amp; chat with a member of the project, Noah joins us &amp; were all excited about Firefox quantum.
Plus Gnome 4s ambitious goals, a new Linux Kernel that really matters, OpenShot woes &amp; more!</itunes:subtitle>
<itunes:summary><![CDATA[A new version of Fedora hits the web and we share our thoughts & chat with a member of the project, Noah joins us to answer your live calls & were all excited about Firefoxs new quantum release.
Plus Gnome 4s ambitious goals, a new Linux Kernel that really matters, OpenShot woes & more!]]></itunes:summary>
<itunes:explicit>no</itunes:explicit>
<itunes:duration>1:49:23</itunes:duration>
<media:thumbnail url="http://www.jupiterbroadcasting.com/wp-content/uploads/2017/11/lup-0223-v.jpg" />
<author>chris@jupiterbroadcasting.com (Jupiter Broadcasting)</author><media:content url="http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0223.mp3" fileSize="53118235" type="audio/mpeg" /><itunes:keywords>Linux,KDE,Arch,Ubuntu,Unity,lifestyle,open,foss,libre,Chrome,Google,Android,Games,Steam,Gnome,GTK,Qt,Jupiter,Broadcasting,Linux,Action,Show</itunes:keywords></item>
<item>
<title>A Community Divided | LUP 222</title>
<link>http://www.jupiterbroadcasting.com/119691/a-community-divided-lup-222/</link>
<description><![CDATA[<p>Community news & app picks this week before we get into a bizarre story that could rip up parts of the open source community.</p>
@@ -0,0 +1,370 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:art19="https://art19.com/xmlns/rss-extensions/1.0">
<channel>
<title>Steal the Stars</title>
<description>
<![CDATA[<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 ever attempted - scripted by Mac Rogers, the award-winning playwright and writer of the multi-million download The Message and LifeAfter.</p>]]>
</description>
<managingEditor>podcasts@macmillan.com</managingEditor>
<copyright>© Gideon Media</copyright>
<atom:link href="https://rss.art19.com/steal-the-stars" rel="self" type="application/rss+xml"/>
<link>http://tor-labs.com/</link>
<itunes:owner>
<itunes:email>podcasts@macmillan.com</itunes:email>
</itunes:owner>
<itunes:author>Tor Labs / Gideon Media</itunes:author>
<itunes:summary>
<![CDATA[<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 ever attempted - scripted by Mac Rogers, the award-winning playwright and writer of the multi-million download The Message and LifeAfter.</p>]]>
</itunes:summary>
<language>en</language>
<itunes:explicit>yes</itunes:explicit>
<itunes:category text="Arts">
<itunes:category text="Performing Arts"/>
</itunes:category>
<itunes:type>episodic</itunes:type>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<image>
<url>https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg</url>
<link>http://tor-labs.com/</link>
<title>Steal the Stars</title>
</image>
<item>
<title>14: As Fierce, As Colossal, As All-Consuming</title>
<description>
<![CDATA[<p>In an epic final showdown in the Texas desert - as Sierra closes in from all sides - Dak and Matt finally learn the truth about Moss.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS).</p>]]>
</description>
<itunes:title>14: As Fierce, As Colossal, As All-Consuming</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>14</itunes:episode>
<itunes:summary>In an epic final showdown in the Texas desert - as Sierra closes in from all sides - Dak and Matt finally learn the truth about Moss. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS).</itunes:summary>
<content:encoded>
<![CDATA[<p>In an epic final showdown in the Texas desert - as Sierra closes in from all sides - Dak and Matt finally learn the truth about Moss.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/Ck8iDIER5Fpmc9Rx4ICwxJAvcYGaJWLRNYRf8IAZu2c</guid>
<pubDate>Wed, 01 Nov 2017 03:31:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:43:50</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/b9a3e534-1070-4e21-91f2-e97d35dc9bf3.mp3" type="audio/mpeg" length="40163369"/>
</item>
<item>
<title>13: Matt-25</title>
<description>
<![CDATA[<p>Dak and Matt hide out for the night with Matt's ex-girlfriend Teresa, leading Dak to an unexpected moment of connection... and another unexpected moment that threatens to ruin everything.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS) and Parcast.</p>]]>
</description>
<itunes:title>13: Matt-25</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>13</itunes:episode>
<itunes:summary>Dak and Matt hide out for the night with Matt's ex-girlfriend Teresa, leading Dak to an unexpected moment of connection... and another unexpected moment that threatens to ruin everything. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS) and Parcast.</itunes:summary>
<content:encoded>
<![CDATA[<p>Dak and Matt hide out for the night with Matt's ex-girlfriend Teresa, leading Dak to an unexpected moment of connection... and another unexpected moment that threatens to ruin everything.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace (Squarespace.com offer code STARS) and Parcast.</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/_r5K_8_5HFIXQ1xhv7RHvMvYLOo_0JMnYWHNTaABxuQ</guid>
<pubDate>Wed, 25 Oct 2017 03:35:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:36:14</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/a72503e8-a984-4911-a1dc-e8dcc1a10dfd.mp3" type="audio/mpeg" length="31192711"/>
</item>
<item>
<title>12: All That Sky</title>
<description>
<![CDATA[<p>Dak and Matt are finally on the road with their extraterrestrial contraband, but Sierra is hot on their heels. They're finally forced to take refuge in the last place Dak wants to go.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace and Audible.</p>]]>
</description>
<itunes:title>12: All That Sky</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>12</itunes:episode>
<itunes:summary>Dak and Matt are finally on the road with their extraterrestrial contraband, but Sierra is hot on their heels. They're finally forced to take refuge in the last place Dak wants to go. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Squarespace and Audible.</itunes:summary>
<content:encoded>
<![CDATA[<p>Dak and Matt are finally on the road with their extraterrestrial contraband, but Sierra is hot on their heels. They're finally forced to take refuge in the last place Dak wants to go.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Squarespace and Audible.</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/U1ByYJR2Eq124QgYFZ6FH8Pa1vjpo_XeXfr1meRL6hU</guid>
<pubDate>Wed, 18 Oct 2017 03:31:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:34:34</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/cea5f3e8-f82c-439d-89c8-6cbd402b9df9.mp3" type="audio/mpeg" length="27200365"/>
</item>
<item>
<title>11: Checkpoints</title>
<description>
<![CDATA[<p>Getting Moss's body out of Hangar 11 is one thing. Getting it out of Quill Marine is quite another. And there's a lot of checkpoints - and angry people - standing between Dak &amp; Mat and freedom.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Lore on Amazon.</p>]]>
</description>
<itunes:title>11: Checkpoints</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>11</itunes:episode>
<itunes:summary>Getting Moss's body out of Hangar 11 is one thing. Getting it out of Quill Marine is quite another. And there's a lot of checkpoints - and angry people - standing between Dak &amp;amp; Mat and freedom. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Lore on Amazon.</itunes:summary>
<content:encoded>
<![CDATA[<p>Getting Moss's body out of Hangar 11 is one thing. Getting it out of Quill Marine is quite another. And there's a lot of checkpoints - and angry people - standing between Dak &amp; Mat and freedom.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Lore on Amazon.</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/7-k-JxtB5xrmn6ZuXF7Xz0VTgO_Zm580QQVpQJ2OCq4</guid>
<pubDate>Wed, 11 Oct 2017 03:30:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:22:42</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/3a6e5f73-0b8f-435b-a7c3-b569dd9cedf6.mp3" type="audio/mpeg" length="19402083"/>
</item>
<item>
<title>10: Protocol</title>
<description>
<![CDATA[<p>By the end of this day, Dak and Matt will either be dead... or they'll have just pulled off the most incredible heist of all time.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p>]]>
</description>
<itunes:title>10: Protocol</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>10</itunes:episode>
<itunes:summary>By the end of this day, Dak and Matt will either be dead... or they'll have just pulled off the most incredible heist of all time. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</itunes:summary>
<content:encoded>
<![CDATA[<p>By the end of this day, Dak and Matt will either be dead... or they'll have just pulled off the most incredible heist of all time.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/zPXgIdxCbanfZNot2NoqCtuA8p6TvneH8uRWjipDeaU</guid>
<pubDate>Wed, 04 Oct 2017 03:30:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:25:24</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/19d8cc64-22cf-4d00-a67f-b55f05d0443b.mp3" type="audio/mpeg" length="21997609"/>
</item>
<item>
<title>9: The Real Stuff</title>
<description>
<![CDATA[<p>Dak has to take two vitally important meetings today, with two of the most powerful men in Washington, DC. And they have to go perfectly: her fate and Matt's hang in the balance.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</p>]]>
</description>
<itunes:title>9: The Real Stuff</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>9</itunes:episode>
<itunes:summary>Dak has to take two vitally important meetings today, with two of the most powerful men in Washington, DC. And they have to go perfectly: her fate and Matt's hang in the balance. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</itunes:summary>
<content:encoded>
<![CDATA[<p>Dak has to take two vitally important meetings today, with two of the most powerful men in Washington, DC. And they have to go perfectly: her fate and Matt's hang in the balance.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/UmSCXUzVugdSQDFnPvUeSuDeLH6cVhXvYXI-YOebnpg</guid>
<pubDate>Wed, 27 Sep 2017 03:30:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:28:02</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/24e08163-7f79-4144-9bd8-0b1978e082cd.mp3" type="audio/mpeg" length="23081795"/>
</item>
<item>
<title>8: The Walls of the Maze</title>
<description>
<![CDATA[<p>Dak has a whole new plan to be with Matt now, a far more dangerous one. One which will carry her across the country to start putting the pieces in place for a perfect getaway.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify and Audible (Audible.com/stealthestars).</p>]]>
</description>
<itunes:title>8: The Walls of the Maze</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>8</itunes:episode>
<itunes:summary>Dak has a whole new plan to be with Matt now, a far more dangerous one. One which will carry her across the country to start putting the pieces in place for a perfect getaway. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is sponsored by Spotify and Audible (Audible.com/stealthestars).</itunes:summary>
<content:encoded>
<![CDATA[<p>Dak has a whole new plan to be with Matt now, a far more dangerous one. One which will carry her across the country to start putting the pieces in place for a perfect getaway.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify and Audible (Audible.com/stealthestars).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/JZ3lAioyC2wxIx_HvrP_o7LgjercB6W5UsPbNsMn-ek</guid>
<pubDate>Wed, 20 Sep 2017 03:30:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:27:17</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/e8d9e9c0-f798-4283-ae7e-de5dc3cc7ef8.mp3" type="audio/mpeg" length="22116310"/>
</item>
<item>
<title>7: Altered Voices</title>
<description>
<![CDATA[<p>As Lloyd reveals startling new details about the origin of Object E, Dak and Matt's plan is hit with one brutal setback after another.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</p>]]>
</description>
<itunes:title>7: Altered Voices</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>7</itunes:episode>
<itunes:summary>As Lloyd reveals startling new details about the origin of Object E, Dak and Matt's plan is hit with one brutal setback after another. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</itunes:summary>
<content:encoded>
<![CDATA[<p>As Lloyd reveals startling new details about the origin of Object E, Dak and Matt's plan is hit with one brutal setback after another.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Spotify, Squarespace (Squarespace.com/stars) and Leesa (Leesa.com/stars)</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/i4i_I3dr9rxfV96N0rMCUcvDIdAdwV54lTJf29ZorTs</guid>
<pubDate>Wed, 13 Sep 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:31:06</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/4183c9bc-b272-4a08-8be3-4e49895c7134.mp3" type="audio/mpeg" length="26498194"/>
</item>
<item>
<title>6: 900 Microns</title>
<description>
<![CDATA[<p>As Dak and Matt put their dangerous plan into effect - which involves stealing highly classified footage and meeting in secret with a reporter - Quill Marine gets some devastating news from Sierra.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Spotify.&nbsp;</p>]]>
</description>
<itunes:title>6: 900 Microns</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>6</itunes:episode>
<itunes:summary>As Dak and Matt put their dangerous plan into effect - which involves stealing highly classified footage and meeting in secret with a reporter - Quill Marine gets some devastating news from Sierra. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Spotify. </itunes:summary>
<content:encoded>
<![CDATA[<p>As Dak and Matt put their dangerous plan into effect - which involves stealing highly classified footage and meeting in secret with a reporter - Quill Marine gets some devastating news from Sierra.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Spotify.&nbsp;</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/9BGMWV_KT25yeSPMNF25Z0OF4-TBcZ2NYzX4znBSzZY</guid>
<pubDate>Wed, 06 Sep 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:28:00</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/844ec351-9235-4c5b-9e80-012c61924b94.mp3" type="audio/mpeg" length="25448698"/>
</item>
<item>
<title>5: Lifers</title>
<description>
<![CDATA[<p>After Dak and Patty have to carry out the worst part of their job, Dak's romance with Matt reaches another level. With no legal way to be together, they decide on a desperate plan.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Audible (<a href="http://Audible.com/stealthestars" target="_blank">Audible.com/stealthestars</a>).</p>]]>
</description>
<itunes:title>5: Lifers</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>5</itunes:episode>
<itunes:summary>After Dak and Patty have to carry out the worst part of their job, Dak's romance with Matt reaches another level. With no legal way to be together, they decide on a desperate plan. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Audible (Audible.com/stealthestars).</itunes:summary>
<content:encoded>
<![CDATA[<p>After Dak and Patty have to carry out the worst part of their job, Dak's romance with Matt reaches another level. With no legal way to be together, they decide on a desperate plan.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Audible (<a href="http://Audible.com/stealthestars" target="_blank">Audible.com/stealthestars</a>).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/P1U26uhQ4m-p9ZvKAM0aaMpXPCxKzYhYoKF8JQl5fcw</guid>
<pubDate>Wed, 30 Aug 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:28:07</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/2b33e493-1751-4705-881e-7043ba5e1d56.mp3" type="audio/mpeg" length="24593554"/>
</item>
<item>
<title>4: Power Through</title>
<description>
<![CDATA[<p>Today, Trip Haydon - the head of Sierra and the man who holds all their fates in his hand - is visiting Quill Marine. It's the ultimate test of Dak's leadership. There's no margin for even one mistake.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Spotify, Plated (Plated.com/stars, terms apply) and Leesa (Leesa.com/stars).</p>]]>
</description>
<itunes:title>4: Power Through</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>4</itunes:episode>
<itunes:summary>Today, Trip Haydon - the head of Sierra and the man who holds all their fates in his hand - is visiting Quill Marine. It's the ultimate test of Dak's leadership. There's no margin for even one mistake. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Spotify, Plated (Plated.com/stars, terms apply) and Leesa (Leesa.com/stars).</itunes:summary>
<content:encoded>
<![CDATA[<p>Today, Trip Haydon - the head of Sierra and the man who holds all their fates in his hand - is visiting Quill Marine. It's the ultimate test of Dak's leadership. There's no margin for even one mistake.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Spotify, Plated (Plated.com/stars, terms apply) and Leesa (Leesa.com/stars).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/CfyNFMaQmlDZhNV4yqiUtKMiD-YdGrgDAHRZDbqlCkk</guid>
<pubDate>Wed, 23 Aug 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:38:48</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/ea58f0ff-693e-4d00-a133-e137a098b512.mp3" type="audio/mpeg" length="32449515"/>
</item>
<item>
<title>3: Turndown Service</title>
<description>
<![CDATA[<p>When they find out the man who runs Sierra is paying them a surprise visit, Dak and Matt have to carry out a hazardous test that will either bring them closer together or kill them.</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Plated&nbsp;(<a href="http://plated.com/stars" target="_blank">Plated.com/stars</a>.&nbsp;Terms&nbsp;and conditions&nbsp;apply) and Squarespace (<a href="http://Squarespace.com" target="_blank">Squarespace.com</a>, offer code: Stars).</p>]]>
</description>
<itunes:title>3: Turndown Service</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>3</itunes:episode>
<itunes:summary>When they find out the man who runs Sierra is paying them a surprise visit, Dak and Matt have to carry out a hazardous test that will either bring them closer together or kill them.
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is sponsored by Plated (Plated.com/stars. Terms and conditions apply) and Squarespace (Squarespace.com, offer code: Stars).</itunes:summary>
<content:encoded>
<![CDATA[<p>When they find out the man who runs Sierra is paying them a surprise visit, Dak and Matt have to carry out a hazardous test that will either bring them closer together or kill them.</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Plated&nbsp;(<a href="http://plated.com/stars" target="_blank">Plated.com/stars</a>.&nbsp;Terms&nbsp;and conditions&nbsp;apply) and Squarespace (<a href="http://Squarespace.com" target="_blank">Squarespace.com</a>, offer code: Stars).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/KSQYXYJF7bgP95gjvhiI3YRSckvkLtu8FTVgfhM9lrk</guid>
<pubDate>Wed, 16 Aug 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:33:08</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/0e800c7e-4d3c-4acf-aa3f-8d1e1f508d36.mp3" type="audio/mpeg" length="28936568"/>
</item>
<item>
<title>2: Three Dogs</title>
<description>
<![CDATA[<p>As Dak and Matt try to extinguish their forbidden relationship before it starts, we meet Lloyd, a brilliant xenobiologist who's about to risk his life in a dangerous encounter with the Harp.</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Plated&nbsp;(<a href="http://Plated.com/stars" target="_blank">Plated.com/stars</a>.&nbsp;Terms&nbsp;and conditions&nbsp;apply) and Leesa (<a href="http://Leesa.com/stars" target="_blank">Leesa.com/stars</a>).</p>]]>
</description>
<itunes:title>2: Three Dogs</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>2</itunes:episode>
<itunes:summary>As Dak and Matt try to extinguish their forbidden relationship before it starts, we meet Lloyd, a brilliant xenobiologist who's about to risk his life in a dangerous encounter with the Harp.
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is sponsored by Plated (Plated.com/stars. Terms and conditions apply) and Leesa (Leesa.com/stars).</itunes:summary>
<content:encoded>
<![CDATA[<p>As Dak and Matt try to extinguish their forbidden relationship before it starts, we meet Lloyd, a brilliant xenobiologist who's about to risk his life in a dangerous encounter with the Harp.</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is sponsored by Plated&nbsp;(<a href="http://Plated.com/stars" target="_blank">Plated.com/stars</a>.&nbsp;Terms&nbsp;and conditions&nbsp;apply) and Leesa (<a href="http://Leesa.com/stars" target="_blank">Leesa.com/stars</a>).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/YSwgVQfG9gshqMg7TEpn1q7tjJVYbtM2_Y6zEvjl0Ns</guid>
<pubDate>Wed, 09 Aug 2017 03:15:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:31:20</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/752f5cc4-66ac-4254-9cf1-b1d35991069c.mp3" type="audio/mpeg" length="27205381"/>
</item>
<item>
<title>1: Warm Bodies</title>
<description>
<![CDATA[<p>Dakota Prentiss runs security at the secretive Quill Marine compound, run by private defense conglomerate Sierra. Today she's breaking in a new security staffer, Matt Salem. Which means Matt has to pass a crucial test: how he reacts to the secret at the heart of Quill Marine.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Audible (<a href="http://Audible.com/stealthestars" target="_blank">Audible.com/stealthestars</a>) and Plated (<a href="http://Plated.com/stars" target="_blank">Plated.com/stars</a>. Terms apply).</p>]]>
</description>
<itunes:title>1: Warm Bodies</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:episode>1</itunes:episode>
<itunes:summary>Dakota Prentiss runs security at the secretive Quill Marine compound, run by private defense conglomerate Sierra. Today she's breaking in a new security staffer, Matt Salem. Which means Matt has to pass a crucial test: how he reacts to the secret at the heart of Quill Marine. 
Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel
This week's episode is brought to you by Audible (Audible.com/stealthestars) and Plated (Plated.com/stars. Terms apply).</itunes:summary>
<content:encoded>
<![CDATA[<p>Dakota Prentiss runs security at the secretive Quill Marine compound, run by private defense conglomerate Sierra. Today she's breaking in a new security staffer, Matt Salem. Which means Matt has to pass a crucial test: how he reacts to the secret at the heart of Quill Marine.&nbsp;</p><p>Learn more about Steal the Stars novelization here: http://bit.ly/STSNovel</p><p>This week's episode is brought to you by Audible (<a href="http://Audible.com/stealthestars" target="_blank">Audible.com/stealthestars</a>) and Plated (<a href="http://Plated.com/stars" target="_blank">Plated.com/stars</a>. Terms apply).</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/iKPHluojN_2HVUBDhz25sYOzeMO1xHLITg1JmTyE8nQ</guid>
<pubDate>Wed, 02 Aug 2017 03:00:00 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:27:06</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/f8c867ee-f954-46de-8bda-017305474e40.mp3" type="audio/mpeg" length="21696679"/>
</item>
<item>
<title>Introducing Steal the Stars</title>
<description>
<![CDATA[<p>Steal the Stars is the story of Dakota Prentiss and Matt Salem, two government employees guarding the biggest secret in the world: a crashed UFO. Episode 1 goes live on August 2, 2017.</p>]]>
</description>
<itunes:title>Introducing Steal the Stars</itunes:title>
<itunes:episodeType>trailer</itunes:episodeType>
<itunes:summary>Steal the Stars is the story of Dakota Prentiss and Matt Salem, two government employees guarding the biggest secret in the world: a crashed UFO. Episode 1 goes live on August 2, 2017.</itunes:summary>
<content:encoded>
<![CDATA[<p>Steal the Stars is the story of Dakota Prentiss and Matt Salem, two government employees guarding the biggest secret in the world: a crashed UFO. Episode 1 goes live on August 2, 2017.</p>]]>
</content:encoded>
<guid isPermaLink="false">gid://art19-episode-locator/V0/S6kmOE2cviFS0HD-IUYOPRO0fvjTPYmCsMDe5bjABnA</guid>
<pubDate>Tue, 11 Jul 2017 17:14:45 -0000</pubDate>
<itunes:explicit>yes</itunes:explicit>
<itunes:image href="https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"/>
<itunes:duration>00:01:22</itunes:duration>
<enclosure url="https://dts.podtrac.com/redirect.mp3/rss.art19.com/episodes/f13b703c-20d9-4ea5-83b6-dbcd2f02351a.mp3" type="audio/mpeg" length="1318661"/>
</item>
</channel>
</rss>
@@ -0,0 +1,578 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:media="http://search.yahoo.com/mrss/" xmlns:sm="https://schema.syndicated.media/core/1.0/">
<channel>
<title>The Tip Off</title>
<description><![CDATA[<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 the journalists behind the work as they tell us about the leads, the dead-ends and of course, the tip offs. Therell be car chases, slammed doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling with despotic regimes and much more. So if youre curious about the fun, complicated detective work that goes into doing great investigative journalism- then this is the podcast for you.</p>]]></description>
<link>http://www.acast.com/thetipoff</link>
<lastBuildDate>Mon, 15 Jan 2018 11:59:56 GMT</lastBuildDate>
<pubDate>Thu, 11 Jan 2018 04:00:00 GMT</pubDate>
<ttl>30</ttl>
<language>en</language>
<copyright><![CDATA[]]></copyright>
<docs>https://www.acast.com/thetipoff</docs>
<image>
<url>https://imagecdn.acast.com/image?h=1500&amp;w=1500&amp;source=http%3A%2F%2Fi1.sndcdn.com%2Favatars-000317856075-a2coqz-original.jpg</url>
<title>The Tip Off</title>
<link>http://www.acast.com/thetipoff</link>
</image>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;http%3A%2F%2Fi1.sndcdn.com%2Favatars-000317856075-a2coqz-original.jpg" />
<itunes:subtitle><![CDATA[Welcome to The Tip Off- the podcast where we take…]]></itunes:subtitle>
<itunes:type>episodic</itunes:type>
<itunes:author>The Tip Off</itunes:author>
<itunes:summary>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 the journalists behind the work as they tell us about the leads, the dead-ends and of course, the tip offs. Therell be car chases, slammed doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling with despotic regimes and much more. So if youre curious about the fun, complicated detective work that goes into doing great investigative journalism- then this is the podcast for you.</itunes:summary>
<atom:link rel="self" type="application/rss+xml" href="https://rss.acast.com/thetipoff" />
<itunes:owner>
<itunes:name><![CDATA[The Tip Off]]></itunes:name>
<itunes:email>tipoffpodcast@gmail.com</itunes:email>
</itunes:owner>
<itunes:explicit>no</itunes:explicit>
<itunes:keywords></itunes:keywords>
<itunes:category text="News &amp; Politics" />
<media:credit role="author">The Tip Off</media:credit>
<media:description type="html"><![CDATA[<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 the journalists behind the work as they tell us about the leads, the dead-ends and of course, the tip offs. Therell be car chases, slammed doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling with despotic regimes and much more. So if youre curious about the fun, complicated detective work that goes into doing great investigative journalism- then this is the podcast for you.</p>]]></media:description>
<item>
<title>Ep.13 Voices in the ether</title>
<itunes:subtitle>When you set out on an investigation you usually start with a hypothesis- an idea that you will test and challenge as you go. But not Paul Myles…
Working for On Our Radar, Paul set out to tell the story of dementia in the UK. Hours spent on trains, ...</itunes:subtitle>
<itunes:summary><![CDATA[When you set out on an investigation you usually start with a hypothesis- an idea that you will test and challenge as you go. But not Paul Myles…
Working for On Our Radar, Paul set out to tell the story of dementia in the UK. Hours spent on trains, workshops around the country and 3D printed phones brought him to Agnes, Melvyn and dozens of others. Together they give a never-before-seen insight into life with the condition.
Read all about it:
https://dementiadiaries.org/
https://www.buzzfeed.com/lukelewis/inspiring-tales-coping-with-dementia?utm_term=.raENZvdoA#.yoaMagBGd
https://www.theguardian.com/society/video/2017/jan/30/dementia-diaries-its-like-trying-to-go-through-a-brick-wall-video
http://www.telegraph.co.uk/science/2016/05/15/dealing-with-dementia-those-living-with-condition-outline-dos-an/
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Lee Rosevere]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[cd7778b1-e81d-4d45-8fe6-cc712190eabe]]></guid>
<pubDate>Thu, 11 Jan 2018 04:00:00 GMT</pubDate>
<itunes:duration>00:33:24</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:season>2</itunes:season>
<itunes:episode>13</itunes:episode>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fassets%2Fcd7778b1-e81d-4d45-8fe6-cc712190eabe%2Fcover-image-jc9kl26c-thetipoff_logo_1_.jpg" />
<description><![CDATA[<p>When you set out on an investigation you usually start with a hypothesis- an idea that you will test and challenge as you go. But not Paul Myles… </p><p><br></p><p>Working for On Our Radar, Paul set out to tell the story of dementia in the UK. Hours spent on trains, workshops around the country and 3D printed phones brought him to Agnes, Melvyn and dozens of others. Together they give a never-before-seen insight into life with the condition.</p><p><br></p><p><strong>Read all about it:</strong></p><p><a href="https://dementiadiaries.org/" target="_blank">https://dementiadiaries.org/</a></p><p><br></p><p><a href="https://www.buzzfeed.com/lukelewis/inspiring-tales-coping-with-dementia?utm_term=.raENZvdoA#.yoaMagBGd" target="_blank">https://www.buzzfeed.com/lukelewis/inspiring-tales-coping-with-dementia?utm_term=.raENZvdoA#.yoaMagBGd</a></p><p><br></p><p><a href="https://www.theguardian.com/society/video/2017/jan/30/dementia-diaries-its-like-trying-to-go-through-a-brick-wall-video" target="_blank">https://www.theguardian.com/society/video/2017/jan/30/dementia-diaries-its-like-trying-to-go-through-a-brick-wall-video</a> </p><p><br></p><p><a href="http://www.telegraph.co.uk/science/2016/05/15/dealing-with-dementia-those-living-with-condition-outline-dos-an/" target="_blank">http://www.telegraph.co.uk/science/2016/05/15/dealing-with-dementia-those-living-with-condition-outline-dos-an/</a></p><p><br></p><p>Hosted and produced: Maeve McClenaghan</p><p><br></p><p>Music: Dice Muse and <a href="http://freemusicarchive.org/music/Lee_Rosevere/Trappist-1/Lee_Rosevere_-_Trappist-1_-_05_Planet_F" target="_blank">Lee Rosevere</a> </p><p><br></p><p><br></p>]]></description>
<link>https://www.acast.com/thetipoff/ep.13voicesintheether</link>
<enclosure url="https://media.acast.com/thetipoff/ep.13voicesintheether/media.mp3" length="56188568" type="audio/mpeg"/>
</item>
<item>
<title>Ep. 12 The sound of news</title>
<itunes:subtitle>Leah Borromeo sees journalism differently to most. Or rather she hears it. Leah and colleagues are working on a project to tell investigative journalism through music.
So Maeve went along to the studio, to hear just exactly what that means.
Read a...</itunes:subtitle>
<itunes:summary><![CDATA[Leah Borromeo sees journalism differently to most. Or rather she hears it. Leah and colleagues are working on a project to tell investigative journalism through music.
So Maeve went along to the studio, to hear just exactly what that means.
Read all about it:
https://www.disobedientfilms.com/if-the-oceans-could-speak
https://www.disobedientfilms.com/climate-symphony
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Climate Symphony]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[7fd9f024-1fd5-48a1-a96b-a3419da9f754]]></guid>
<pubDate>Thu, 28 Dec 2017 03:00:00 GMT</pubDate>
<itunes:duration>00:36:36</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:season>2</itunes:season>
<itunes:episode>12</itunes:episode>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fassets%2F7fd9f024-1fd5-48a1-a96b-a3419da9f754%2Fcover-image-jbo1hf03-thetipoff_logo_1_.jpg" />
<description><![CDATA[<p>Leah Borromeo sees journalism differently to most. Or rather she hears it. Leah and colleagues are working on a project to tell investigative journalism through music.</p><p><br></p><p>So Maeve went along to the studio, to hear just exactly what that means. </p><p><br></p><p>Read all about it:</p><p><a href="https://www.disobedientfilms.com/if-the-oceans-could-speak" target="_blank">https://www.disobedientfilms.com/if-the-oceans-could-speak</a></p><p><br></p><p><a href="https://www.disobedientfilms.com/climate-symphony" target="_blank">https://www.disobedientfilms.com/climate-symphony</a> </p><p><br></p><p>Hosted and produced: Maeve McClenaghan</p><p><br></p><p>Music: Dice Muse and Climate Symphony</p><p><br></p><p><br></p>]]></description>
<link>https://www.acast.com/thetipoff/ep.12thesoundofnews</link>
<enclosure url="https://media.acast.com/thetipoff/ep.12thesoundofnews/media.mp3" length="63865436" type="audio/mpeg"/>
</item>
<item>
<title>Ep.11 Putting a price on health</title>
<itunes:subtitle>Ep. 11 Putting a price on health
Billy Kenber set out to look into one story and ended up finding another. Hidden within open datasets was proof of a practice that was costing the NHS hundreds of millions of pounds a year.
Battling to put the piece...</itunes:subtitle>
<itunes:summary><![CDATA[Billy Kenber set out to look into one story and ended up finding another. Hidden within open datasets was proof of a practice that was costing the NHS hundreds of millions of pounds a year.
Battling to put the pieces together in a quagmire of complex pricing structures and regulations understood by very few people, Billy blew the lid of a multi-million pound industry. And in the end one failed hypothesis led to an investigation that would change the law.
Read all about it:
https://www.thetimes.co.uk/article/extortionate-prices-add-260m-to-nhs-drug-bill-8mwtttwdk
https://www.thetimes.co.uk/article/victory-against-rip-off-drug-firms-after-times-investigation-qm6hlmqts
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Podington Bear]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[2af5efc1-5e6b-446e-9bb3-38704b9392d9]]></guid>
<pubDate>Thu, 14 Dec 2017 04:00:00 GMT</pubDate>
<itunes:duration>00:32:46</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:season>2</itunes:season>
<itunes:episode>11</itunes:episode>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fassets%2F2af5efc1-5e6b-446e-9bb3-38704b9392d9%2Fcover-image-jb5jim69-thetipoff_logo_1_.jpg" />
<description><![CDATA[<p><strong>Ep. 11 Putting a price on health</strong></p><p><br></p><p>Billy Kenber set out to look into one story and ended up finding another. Hidden within open datasets was proof of a practice that was costing the NHS hundreds of millions of pounds a year.</p><p><br></p><p>Battling to put the pieces together in a quagmire of complex pricing structures and regulations understood by very few people, Billy blew the lid of a multi-million pound industry. And in the end one failed hypothesis led to an investigation that would change the law.</p><p><br></p><p>Read all about it:</p><p><br></p><p><a href="https://www.thetimes.co.uk/article/extortionate-prices-add-260m-to-nhs-drug-bill-8mwtttwdk" target="_blank">https://www.thetimes.co.uk/article/extortionate-prices-add-260m-to-nhs-drug-bill-8mwtttwdk</a> </p><p><a href="https://www.thetimes.co.uk/article/victory-against-rip-off-drug-firms-after-times-investigation-qm6hlmqts" target="_blank">https://www.thetimes.co.uk/article/victory-against-rip-off-drug-firms-after-times-investigation-qm6hlmqts</a> </p><p><br></p><p>Hosted and produced: Maeve McClenaghan</p><p><br></p><p>Music: Dice Muse and <a href="http://freemusicarchive.org/music/Podington_Bear/" target="_blank">Podington Bear</a></p><p><br></p>]]></description>
<link>https://www.acast.com/thetipoff/ep.11puttingapriceonhealth</link>
<enclosure url="https://media.acast.com/thetipoff/ep.11puttingapriceonhealth/media.mp3" length="54656753" type="audio/mpeg"/>
</item>
<item>
<title>Ep. 10 Brown paper envelopes</title>
<itunes:subtitle>Jennifer Williams, of the Manchester Evening News, was hearing horrible stories coming from two hospitals in her patch. Rumour had it, there was a damning report out there... but getting hold of it would be easier said than done.
Sources, FOI battles...</itunes:subtitle>
<itunes:summary><![CDATA[Jennifer Williams, of the Manchester Evening News, was hearing horrible stories coming from two hospitals in her patch. Rumour had it, there was a damning report out there... but getting hold of it would be easier said than done.
Sources, FOI battles and an envelope full of surprises- this is the story of how one reporter revealed mothers and babies dying at a worrying rate.
Read all about it:
http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-secret-report-12218989
http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-report-revealed-12220033
Hosted and produced: Maeve McClenaghan
Music: Dice Muse, &nbsp;Komiku, John Spacek and Podington Bear]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[97e601de-45a4-4e6b-bb34-3409081c9202]]></guid>
<pubDate>Thu, 30 Nov 2017 08:45:00 GMT</pubDate>
<itunes:duration>00:34:57</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:season>2</itunes:season>
<itunes:episode>10</itunes:episode>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fassets%2F97e601de-45a4-4e6b-bb34-3409081c9202%2Fcover-image-jam7up44-thetipoff_logo_1_.jpg" />
<description><![CDATA[<p>Jennifer Williams, of the Manchester Evening News, was hearing horrible stories coming from two hospitals in her patch. Rumour had it, there was a damning report out there... but getting hold of it would be easier said than done.</p><p><br></p><p>Sources, FOI battles and an envelope full of surprises- this is the story of how one reporter revealed mothers and babies dying at a worrying rate.</p><p><br></p><p>Read all about it:</p><p><br></p><p><a href="http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-secret-report-12218989" target="_blank">http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-secret-report-12218989</a></p><p><br></p><p><a href="http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-report-revealed-12220033" target="_blank">http://www.manchestereveningnews.co.uk/news/greater-manchester-news/pennine-acute-maternity-report-revealed-12220033</a></p><p><br></p><p>Hosted and produced: Maeve McClenaghan</p><p><br></p><p>Music: Dice Muse,&nbsp;<a href="http://freemusicarchive.org/music/Komiku/Its_time_for_adventure__vol_3/Komiku_-_Its_time_for_adventure_vol_3_-_09_You_yourself_and_the_main_character" target="_blank">Komiku</a>, <a href="http://freemusicarchive.org/" target="_blank">John Spacek</a> and <a href="http://freemusicarchive.org/music/Podington_Bear/" target="_blank">Podington Bear</a></p>]]></description>
<link>https://www.acast.com/thetipoff/ep.10brownpaperenvelopes</link>
<enclosure url="https://media.acast.com/thetipoff/ep.10brownpaperenvelopes/media.mp3" length="59888557" type="audio/mpeg"/>
</item>
<item>
<title>Ep.9 When the roof came down</title>
<itunes:subtitle>Ep.9 When the roof came down In late July I read a Facebook post that would send me on a three month journey- from town council steps, to bland hotels to dirty council flats- I followed the stories of women fleeing domestic violence only to be let down...</itunes:subtitle>
<itunes:summary><![CDATA[Ep.9 When the roof came down In late July I read a Facebook post that would send me on a three month journey- from town council steps, to bland hotels to dirty council flats- I followed the stories of women fleeing domestic violence only to be let down by the system designed to support them. Meanwhile, a team of journalists all across the country, dug into FOI data, pulled in local council funding bids and surveyed experts on the ground. Together we uncovered a country-wide crisis, with huge cuts to refuge funding and hundreds of vulnerable women turned away. WARNING: This episodes contains descriptions of domestic violence and strong language. If you need to talk to someone about domestic violence contact Women's Aid/Refuge free helpline on 0808 2000 247. If you are in immediate danger, call 999. Read all about it: https://www.thebureauinvestigates.com/stories/2017-10-16/a-system-at-breaking-point https://www.thebureauinvestigates.com/stories/2017-10-16/new-entry https://www.thebureauinvestigates.com/blog/2017-10-19/refuges-at-breaking-point-stories-from-around-the-country Hosted and produced: Maeve McClenaghan Testimony voiced by: Emer O Connor and Stephanie Soh Music: Dice Muse and Komiku http://freemusicarchive.org/music/Komiku/Its_time_for_adventure__vol_3/Komiku_-_Its_time_for_adventure_vol_3_-_09_You_yourself_and_the_main_character]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/348656618]]></guid>
<pubDate>Thu, 26 Oct 2017 09:16:00 GMT</pubDate>
<itunes:duration>00:46:16</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>yes</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2Fd320b821-f2ba-4029-b95b-0cb840bfa8d8%2F44cc9c2e-879f-49c6-8b34-4d38d09e85d2%2Fartworks-000248917923-77ja4z-original.jpg" />
<description><![CDATA[<p>Ep.9 When the roof came down In late July I read a Facebook post that would send me on a three month journey- from town council steps, to bland hotels to dirty council flats- I followed the stories of women fleeing domestic violence only to be let down by the system designed to support them. Meanwhile, a team of journalists all across the country, dug into FOI data, pulled in local council funding bids and surveyed experts on the ground. Together we uncovered a country-wide crisis, with huge cuts to refuge funding and hundreds of vulnerable women turned away. WARNING: This episodes contains descriptions of domestic violence and strong language. If you need to talk to someone about domestic violence contact Women's Aid/Refuge free helpline on 0808 2000 247. If you are in immediate danger, call 999. Read all about it: https://www.thebureauinvestigates.com/stories/2017-10-16/a-system-at-breaking-point https://www.thebureauinvestigates.com/stories/2017-10-16/new-entry https://www.thebureauinvestigates.com/blog/2017-10-19/refuges-at-breaking-point-stories-from-around-the-country Hosted and produced: Maeve McClenaghan Testimony voiced by: Emer O Connor and Stephanie Soh Music: Dice Muse and Komiku http://freemusicarchive.org/music/Komiku/Its_time_for_adventure__vol_3/Komiku_-_Its_time_for_adventure_vol_3_-_09_You_yourself_and_the_main_character</p>]]></description>
<link>https://www.acast.com/thetipoff/ep9-when-the-roof-came-down</link>
<enclosure url="https://media.acast.com/thetipoff/ep9-when-the-roof-came-down/media.mp3" length="87063215" type="audio/mpeg"/>
</item>
<item>
<title>Ep.8 The stories untold</title>
<itunes:subtitle>Ep.8 The stories untold
Its one thing breaking a story- but how do you keep reporting a story that unfurls over years and not days?
Rebecca Omonira-Oyekanmi explains how she travelled to the vast refugee camps of Ethiopia before Rossalyn Warren takes...</itunes:subtitle>
<itunes:summary><![CDATA[Ep.8 The stories untold
Its one thing breaking a story- but how do you keep reporting a story that unfurls over years and not days?
Rebecca Omonira-Oyekanmi explains how she travelled to the vast refugee camps of Ethiopia before Rossalyn Warren takes up the baton, reporting from the perilous sea-crossings on the Mediterranean.
Working freelance, both women struggle to do the stories they care about while making a living.
WARNING: This episodes contains descriptions of domestic violence.
Read all about it:
http://www.newstatesman.com/world/africa/2017/03/i-want-try-live-or-die-how-refugees-decide-whether-make-dangerous-trip-europe
http://www.elleuk.com/life-and-culture/culture/longform/a36785/moroccan-teen-girl-fleeing-slavery/
Hosted and produced: Maeve McClenaghan
Music: Dice Muse, Clare Marks and Komiku
Audio of sea rescues from Medicins Sans Frontier
https://soundcloud.com/claremarks]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/335067535]]></guid>
<pubDate>Thu, 27 Jul 2017 12:53:18 GMT</pubDate>
<itunes:duration>00:41:20</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2Fd3c055ed-eb98-42e1-8ef1-e5e201d58a24%2Ffa8e7880-4001-4f1a-b480-e6e04f709f96%2Fartworks-000235413887-wac8v7-original.jpg" />
<description><![CDATA[Ep.8 The stories untold
Its one thing breaking a story- but how do you keep reporting a story that unfurls over years and not days?
Rebecca Omonira-Oyekanmi explains how she travelled to the vast refugee camps of Ethiopia before Rossalyn Warren takes up the baton, reporting from the perilous sea-crossings on the Mediterranean.
Working freelance, both women struggle to do the stories they care about while making a living.
WARNING: This episodes contains descriptions of domestic violence.
Read all about it:
http://www.newstatesman.com/world/africa/2017/03/i-want-try-live-or-die-how-refugees-decide-whether-make-dangerous-trip-europe
http://www.elleuk.com/life-and-culture/culture/longform/a36785/moroccan-teen-girl-fleeing-slavery/
Hosted and produced: Maeve McClenaghan
Music: Dice Muse, Clare Marks and Komiku
Audio of sea rescues from Medicins Sans Frontier
https://soundcloud.com/claremarks]]></description>
<link>https://www.acast.com/thetipoff/ep.8-the-stories-untold</link>
<enclosure url="https://media.acast.com/thetipoff/ep.8-the-stories-untold/media.mp3" length="75233921" type="audio/mpeg"/>
</item>
<item>
<title>Ep.7 Codename Prometheus</title>
<itunes:subtitle>Ep. 7 Codename: Prometheus
Everyones heard of the Panama Papers- the stories from the biggest data lead in history rocked the world.
But how did it happen? Where did the data appear from? How do you sift through 11.5m files? And how on earth do you ...</itunes:subtitle>
<itunes:summary><![CDATA[Ep. 7 Codename: Prometheus
Everyones heard of the Panama Papers- the stories from the biggest data lead in history rocked the world.
But how did it happen? Where did the data appear from? How do you sift through 11.5m files? And how on earth do you herd hundreds of journalists to the same finish line?
Bastian Obermayer (Süddeutsche Zeitung), Holly Watt (Guardian) and Will Fitzgibbon (ICIJ) talk us through the trials and tribulations of the worlds biggest cross-border collaboration.
Read all about it:
https://www.theguardian.com/news/2016/apr/07/david-cameron-admits-he-profited-fathers-offshore-fund-panama-papers
https://panamapapers.icij.org/about.html
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Josh Spacek]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/333989206]]></guid>
<pubDate>Thu, 20 Jul 2017 07:07:45 GMT</pubDate>
<itunes:duration>00:39:41</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2F66be48d0-9970-4254-a8ea-82a1066e2337%2Fe7762b1f-aada-433b-9edd-1a5ac348ab98%2Fartworks-000234362942-lmh754-original.jpg" />
<description><![CDATA[Ep. 7 Codename: Prometheus
Everyones heard of the Panama Papers- the stories from the biggest data lead in history rocked the world.
But how did it happen? Where did the data appear from? How do you sift through 11.5m files? And how on earth do you herd hundreds of journalists to the same finish line?
Bastian Obermayer (Süddeutsche Zeitung), Holly Watt (Guardian) and Will Fitzgibbon (ICIJ) talk us through the trials and tribulations of the worlds biggest cross-border collaboration.
Read all about it:
https://www.theguardian.com/news/2016/apr/07/david-cameron-admits-he-profited-fathers-offshore-fund-panama-papers
https://panamapapers.icij.org/about.html
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Josh Spacek]]></description>
<link>https://www.acast.com/thetipoff/ep.7-codename-prometheus</link>
<enclosure url="https://media.acast.com/thetipoff/ep.7-codename-prometheus/media.mp3" length="71273758" type="audio/mpeg"/>
</item>
<item>
<title>Ep.6 Caught offside?</title>
<itunes:subtitle>Ep. 6 Caught offside?
A two year investigation took reporters undercover as they met with top football managers and agents.
Claire Newell explains how the Daily Telegraphs investigation team exposed the murky world of British football, ending in the ...</itunes:subtitle>
<itunes:summary><![CDATA[Ep. 6 Caught offside?
A two year investigation took reporters undercover as they met with top football managers and agents.
Claire Newell explains how the Daily Telegraphs investigation team exposed the murky world of British football, ending in the England manager stepping down.
Read all about it:
http://www.telegraph.co.uk/news/2016/09/26/exclusive-investigation-england-manager-sam-allardyce-for-sale/
http://www.telegraph.co.uk/football-for-sale/
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Clare Marks
https://soundcloud.com/claremarks]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/332988171]]></guid>
<pubDate>Thu, 13 Jul 2017 07:44:20 GMT</pubDate>
<itunes:duration>00:44:11</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2Fdb22e275-5348-44f8-a975-e2ae773910cf%2F3419a5d8-7ad2-41fb-bc03-861b4b7dd0b4%2Fartworks-000233400257-9pzv50-original.jpg" />
<description><![CDATA[Ep. 6 Caught offside?
A two year investigation took reporters undercover as they met with top football managers and agents.
Claire Newell explains how the Daily Telegraphs investigation team exposed the murky world of British football, ending in the England manager stepping down.
Read all about it:
http://www.telegraph.co.uk/news/2016/09/26/exclusive-investigation-england-manager-sam-allardyce-for-sale/
http://www.telegraph.co.uk/football-for-sale/
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Clare Marks
https://soundcloud.com/claremarks]]></description>
<link>https://www.acast.com/thetipoff/ep.6-caught-offside</link>
<enclosure url="https://media.acast.com/thetipoff/ep.6-caught-offside/media.mp3" length="82073824" type="audio/mpeg"/>
</item>
<item>
<title>Ep.5 It started with a body</title>
<itunes:subtitle>Ep. 5 It started with a body
It started with a body and ended up in a months long investigation exploring the scale of homelessness in a London borough.
We follow Emma Youle as one simple story grows and morphs into an award-winning campaign.
Read a...</itunes:subtitle>
<itunes:summary><![CDATA[Ep. 5 It started with a body
It started with a body and ended up in a months long investigation exploring the scale of homelessness in a London borough.
We follow Emma Youle as one simple story grows and morphs into an award-winning campaign.
Read all about it: http://www.hackneygazette.co.uk/news/hackney-council-pays-35million-a-year-to-keep-the-homeless-homeless-1-4851576
http://www.hackneygazette.co.uk/news/revealed-shocking-modern-day-slum-conditions-at-hackney-hostel-for-homeless-people-1-4589067
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Podington Bear]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/331807588]]></guid>
<pubDate>Thu, 06 Jul 2017 06:43:02 GMT</pubDate>
<itunes:duration>00:38:17</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2F032b9688-585f-417d-903b-b651d8e3b20b%2F7e397853-7dad-4f38-b69f-8fa88b4567e8%2Fartworks-000232222022-dx0w5x-original.jpg" />
<description><![CDATA[Ep. 5 It started with a body
It started with a body and ended up in a months long investigation exploring the scale of homelessness in a London borough.
We follow Emma Youle as one simple story grows and morphs into an award-winning campaign.
Read all about it: http://www.hackneygazette.co.uk/news/hackney-council-pays-35million-a-year-to-keep-the-homeless-homeless-1-4851576
http://www.hackneygazette.co.uk/news/revealed-shocking-modern-day-slum-conditions-at-hackney-hostel-for-homeless-people-1-4589067
Hosted and produced: Maeve McClenaghan
Music: Dice Muse and Podington Bear]]></description>
<link>https://www.acast.com/thetipoff/ep.5-it-started-with-a-body</link>
<enclosure url="https://media.acast.com/thetipoff/ep.5-it-started-with-a-body/media.mp3" length="67903969" type="audio/mpeg"/>
</item>
<item>
<title>Ep.4 Knock Knock</title>
<itunes:subtitle>Ep.4 Knock knock
Jane Bradley is stood on doorstep in West London. She is about to knock and tell a mother that she suspects her son is one of the worlds most wanted terrorists.
We follow Janes progress as she tracks down not one but two of the Bea...</itunes:subtitle>
<itunes:summary><![CDATA[Ep.4 Knock knock
Jane Bradley is stood on doorstep in West London. She is about to knock and tell a mother that she suspects her son is one of the worlds most wanted terrorists.
We follow Janes progress as she tracks down not one but two of the Beatles terror cell.
Read all about it: https://www.buzzfeed.com/janebradley/unmasked-the-second-member-of-isiss-beatles-execution-cell?utm_term=.tbXjXVDRw#.vvG4w1PG7
https://www.buzzfeed.com/janebradley/my-son-the-isis-executioner?utm_term=.uxO3lDBw9#.uqWJrqAvR
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/330669491]]></guid>
<pubDate>Thu, 29 Jun 2017 07:53:28 GMT</pubDate>
<itunes:duration>00:35:05</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2F21aabb14-b47f-4bef-8cc0-b36eb65bf00a%2F4b5be8c8-9f36-440b-8422-19e220b39fb3%2Fartworks-000231099552-8kn14m-original.jpg" />
<description><![CDATA[Ep.4 Knock knock
Jane Bradley is stood on doorstep in West London. She is about to knock and tell a mother that she suspects her son is one of the worlds most wanted terrorists.
We follow Janes progress as she tracks down not one but two of the Beatles terror cell.
Read all about it: https://www.buzzfeed.com/janebradley/unmasked-the-second-member-of-isiss-beatles-execution-cell?utm_term=.tbXjXVDRw#.vvG4w1PG7
https://www.buzzfeed.com/janebradley/my-son-the-isis-executioner?utm_term=.uxO3lDBw9#.uqWJrqAvR
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></description>
<link>https://www.acast.com/thetipoff/ep.4-knock-knock</link>
<enclosure url="https://media.acast.com/thetipoff/ep.4-knock-knock/media.mp3" length="60211376" type="audio/mpeg"/>
</item>
<item>
<title>Ep.3 Back to the source</title>
<itunes:subtitle>Ep.3 Back to the source
The Bureau of Investigative Journalisms Abigail Fielding-Smith finds a vital source, who lets her into the world of the Pentagons top-secret propaganda machine.
With: Abigail Fielding-Smith
Read all about it: http://labs....</itunes:subtitle>
<itunes:summary><![CDATA[Ep.3 Back to the source
The Bureau of Investigative Journalisms Abigail Fielding-Smith finds a vital source, who lets her into the world of the Pentagons top-secret propaganda machine.
With: Abigail Fielding-Smith
Read all about it: http://labs.thebureauinvestigates.com/fake-news-and-false-flags/
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/329457734]]></guid>
<pubDate>Thu, 22 Jun 2017 07:18:17 GMT</pubDate>
<itunes:duration>00:40:45</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2Ff12efe36-3be7-4ab3-88c5-d74029f554cb%2F9fb70bc0-ec6a-40f6-9233-3f768356e6ce%2Fartworks-000229985808-w1scxb-original.jpg" />
<description><![CDATA[Ep.3 Back to the source
The Bureau of Investigative Journalisms Abigail Fielding-Smith finds a vital source, who lets her into the world of the Pentagons top-secret propaganda machine.
With: Abigail Fielding-Smith
Read all about it: http://labs.thebureauinvestigates.com/fake-news-and-false-flags/
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></description>
<link>https://www.acast.com/thetipoff/ep.3-back-to-the-source</link>
<enclosure url="https://media.acast.com/thetipoff/ep.3-back-to-the-source/media.mp3" length="73843165" type="audio/mpeg"/>
</item>
<item>
<title>Ep.2 The stuff of horror movies</title>
<itunes:subtitle>Ep. 2 The stuff of horror movies
Piece by piece the Washington Posts Louisa Loveluck puts together the picture of a horrifying Syrian torture facility, in a setting you wouldnt expect.
WARNING: This episode includes descriptions of violence and to...</itunes:subtitle>
<itunes:summary><![CDATA[Ep. 2 The stuff of horror movies
Piece by piece the Washington Posts Louisa Loveluck puts together the picture of a horrifying Syrian torture facility, in a setting you wouldnt expect.
WARNING: This episode includes descriptions of violence and torture and may not be suitable for all listeners.
With: Louisa Loveluck
Read all about it: https://www.washingtonpost.com/world/middle_east/the-hospitals-were-slaughterhouses-a-journey-intosyrias-secret-torture-wards/2017/04/02/90ccaa6e-0d61-11e7-b2bb-417e331877d9_story.html?utm_term=.78c0ffec5d51
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse and Podington Bear]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/327984833]]></guid>
<pubDate>Wed, 14 Jun 2017 07:39:16 GMT</pubDate>
<itunes:duration>00:31:58</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2F421a1d64-4f8d-4071-91d8-3181ef188f9b%2Ff21198e8-9018-4e3b-91eb-7d75b029b300%2Fartworks-000228291000-fdcndf-original.jpg" />
<description><![CDATA[Ep. 2 The stuff of horror movies
Piece by piece the Washington Posts Louisa Loveluck puts together the picture of a horrifying Syrian torture facility, in a setting you wouldnt expect.
WARNING: This episode includes descriptions of violence and torture and may not be suitable for all listeners.
With: Louisa Loveluck
Read all about it: https://www.washingtonpost.com/world/middle_east/the-hospitals-were-slaughterhouses-a-journey-intosyrias-secret-torture-wards/2017/04/02/90ccaa6e-0d61-11e7-b2bb-417e331877d9_story.html?utm_term=.78c0ffec5d51
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse and Podington Bear]]></description>
<link>https://www.acast.com/thetipoff/ep.2-the-stuff-of-horror-movies</link>
<enclosure url="https://media.acast.com/thetipoff/ep.2-the-stuff-of-horror-movies/media.mp3" length="52760373" type="audio/mpeg"/>
</item>
<item>
<title>Ep.1 Follow the money</title>
<itunes:subtitle>Ep.1 Follow the money
Wigs, car chases and rucksacks stuffed with cash. Buzzfeeds Heidi Blake explains how she exposed the questionable financial practices of one of the Conservative Partys largest donors.
With: Heidi Blake
Read all about it: ht...</itunes:subtitle>
<itunes:summary><![CDATA[Ep.1 Follow the money
Wigs, car chases and rucksacks stuffed with cash. Buzzfeeds Heidi Blake explains how she exposed the questionable financial practices of one of the Conservative Partys largest donors.
With: Heidi Blake
Read all about it: https://www.buzzfeed.com/heidiblake/this-tory-donor-was-secretly-filmed-dropping-cash-stuffed-ru?utm_term=.oxjmBQGKN#.qneDMy19Q
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/327983748]]></guid>
<pubDate>Wed, 14 Jun 2017 07:21:57 GMT</pubDate>
<itunes:duration>00:37:50</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2Ff37fae08-d65c-45cf-9957-55a045a87cd7%2F97e8631f-4e55-41a3-87c1-29c6114e9b83%2Fartworks-000228290187-pd4yfy-original.jpg" />
<description><![CDATA[Ep.1 Follow the money
Wigs, car chases and rucksacks stuffed with cash. Buzzfeeds Heidi Blake explains how she exposed the questionable financial practices of one of the Conservative Partys largest donors.
With: Heidi Blake
Read all about it: https://www.buzzfeed.com/heidiblake/this-tory-donor-was-secretly-filmed-dropping-cash-stuffed-ru?utm_term=.oxjmBQGKN#.qneDMy19Q
Hosted and produced: Maeve McClenaghan
Production advice: Lorna Stewart
Music: Dice Muse]]></description>
<link>https://www.acast.com/thetipoff/ep.1-follow-the-money</link>
<enclosure url="https://media.acast.com/thetipoff/ep.1-follow-the-money/media.mp3" length="66839221" type="audio/mpeg"/>
</item>
<item>
<title>Coming soon... The Tip Off</title>
<itunes:subtitle>Welcome to The Tip Off, a new weekly podcast that delves into the stories behind some of the biggest headlines in British journalism.</itunes:subtitle>
<itunes:summary><![CDATA[Welcome to The Tip Off, a new weekly podcast that delves into the stories behind some of the biggest headlines in British journalism.]]></itunes:summary>
<guid isPermaLink="false"><![CDATA[tag:soundcloud,2010:tracks/327539708]]></guid>
<pubDate>Sun, 11 Jun 2017 12:24:16 GMT</pubDate>
<itunes:duration>00:11:39</itunes:duration>
<itunes:keywords></itunes:keywords>
<itunes:explicit>no</itunes:explicit>
<itunes:episodeType>full</itunes:episodeType>
<itunes:image href="https://imagecdn.acast.com/image?h&#x3D;1500&amp;w&#x3D;1500&amp;source&#x3D;https%3A%2F%2Fmediacdn.acast.com%2Fsource%2F945dc34f-f4e6-4921-830a-df050e540722%2F9e2116e5-b87e-43a7-ae0c-41cbb324b4d8%2F7d456cf4-3671-4d98-ae5c-1c2e92b13527%2Fartworks-000227873559-gxybj6-original.jpg" />
<description><![CDATA[Welcome to The Tip Off, a new weekly podcast that delves into the stories behind some of the biggest headlines in British journalism.]]></description>
<link>https://www.acast.com/thetipoff/coming-soon...-the-tip-off</link>
<enclosure url="https://media.acast.com/thetipoff/coming-soon...-the-tip-off/media.mp3" length="3978132" type="audio/mpeg"/>
</item>
</channel>
</rss>
@@ -0,0 +1,752 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>Intercepted with Jeremy Scahill</title>
<link>https://theintercept.com/podcasts</link>
<language>en</language>
<copyright>First Look Media Works, Inc.</copyright>
<description>The people behind The Intercepts fearless reporting and incisive commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss the crucial issues of our time: national security, civil liberties, foreign policy, and criminal justice. Plus interviews with artists, thinkers, and newsmakers who challenge our preconceptions about the world we live in.</description>
<image>
<url>http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_2FIntercepted_COVER%2B_281_29.png</url>
<title>Intercepted with Jeremy Scahill</title>
<link>https://theintercept.com/podcasts</link>
</image>
<itunes:explicit>no</itunes:explicit>
<itunes:type>episodic</itunes:type>
<itunes:subtitle>The people behind The Intercepts fearless reporting and incisive commentary discuss the crucial issues of our time.</itunes:subtitle>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:summary>The people behind The Intercepts fearless reporting and incisive commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss the crucial issues of our time: national security, civil liberties, foreign policy, and criminal justice. Plus interviews with artists, thinkers, and newsmakers who challenge our preconceptions about the world we live in.</itunes:summary>
<itunes:owner>
<itunes:name>The Intercept / Panoply</itunes:name>
<itunes:email>podcasts@theintercept.com</itunes:email>
</itunes:owner>
<itunes:image href="http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_2FIntercepted_COVER%2B_281_29.png" />
<itunes:category text="News &amp; Politics">
</itunes:category>
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/InterceptedWithJeremyScahill" /><feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" uri="interceptedwithjeremyscahill" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><item>
<title>BONUS: The NFL's Violent Ballet</title>
<description>This year in the National Football League, there have been 281 recorded concussions that players have suffered — spanning from the pre-season right up to the last playoff games. This weekend is Super Bowl Sunday. That is a macabre sort of record — it represents the most concussions in a season since the NFL started keeping track six years ago. The hits that these players take over and over during their careers can lead to very serious brain damage and a&amp;nbsp; degenerative condition known as Chronic Traumatic Encephalopathy or CTE.&lt;br&gt;&lt;br&gt;We are doing this special episode of Intercepted to highlight a gut-wrenching new short film that The Intercepts Josh Begley has directed. It is called "Concussion Protocol."&lt;br&gt;&lt;br&gt;In this special bonus episode of Intercepted, Josh Begley, The Intercepts Shaun King and Donte Stallworth, a ten year veteran of the NFL, discuss brain injuries, the #TakeAKnee protests, and Trumps attacks on athletes.&lt;br&gt;&lt;br&gt;Josh Begleys video “Concussion Protocol” can be viewed at &lt;a href="https://theintercept.com/NFL"&gt;theintercept.com/NFL&lt;/a&gt;.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;</description>
<pubDate>Thu, 01 Feb 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>BONUS: The NFL's Violent Ballet</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle> There have been 281 recorded concussions in the National Football League this year.</itunes:subtitle>
<itunes:summary>
<![CDATA[This year in the National Football League, there have been 281 recorded concussions that players have suffered — spanning from the pre-season right up to the last playoff games. This weekend is Super Bowl Sunday. That is a macabre sort of record — it represents the most concussions in a season since the NFL started keeping track six years ago. The hits that these players take over and over during their careers can lead to very serious brain damage and a&nbsp; degenerative condition known as Chronic Traumatic Encephalopathy or CTE.<br><br>We are doing this special episode of Intercepted to highlight a gut-wrenching new short film that The Intercepts Josh Begley has directed. It is called "Concussion Protocol."<br><br>In this special bonus episode of Intercepted, Josh Begley, The Intercepts Shaun King and Donte Stallworth, a ten year veteran of the NFL, discuss brain injuries, the #TakeAKnee protests, and Trumps attacks on athletes.<br><br>Josh Begleys video “Concussion Protocol” can be viewed at <a href="https://theintercept.com/NFL">theintercept.com/NFL</a>.<br><br><br><br><br><br><br>]]>
</itunes:summary>
<itunes:duration>2456</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[53934cc4-0712-11e8-929c-1bcc1b677578]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY3860278286.mp3?updated=1517465594" length="39301120" type="audio/mpeg" />
</item>
<item>
<title>Hate of the Union</title>
<description>Naomi Klein and Jeremy analyze Trumps threats toward North Korea, his Executive Order on Guantanamo and the attack on immigrants, the poor, and the environment. Veteran journalist Juan González dissects the roots of fascism, the rise of authoritarian movements, and global migration trends. Marcy Wheeler gives a brief analysis of a theory floated by a former CIA officer that the “Steele dossier” contains Russian disinformation. Ali Abunimah of Electronic Intifada discusses Israeli collusion with the Trump campaign and Mike Pences trip to Israel. And Franklin James Fisher of the band Algiers talks about their music from "The Underside of Power."</description>
<pubDate>Wed, 31 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Hate of the Union</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trumps State of the Union speech was a hateful assault. </itunes:subtitle>
<itunes:summary>
<![CDATA[Naomi Klein and Jeremy analyze Trumps threats toward North Korea, his Executive Order on Guantanamo and the attack on immigrants, the poor, and the environment. Veteran journalist Juan González dissects the roots of fascism, the rise of authoritarian movements, and global migration trends. Marcy Wheeler gives a brief analysis of a theory floated by a former CIA officer that the “Steele dossier” contains Russian disinformation. Ali Abunimah of Electronic Intifada discusses Israeli collusion with the Trump campaign and Mike Pences trip to Israel. And Franklin James Fisher of the band Algiers talks about their music from "The Underside of Power."]]>
</itunes:summary>
<itunes:duration>5176</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[366e5f2a-fb38-11e7-847d-3709dcd25861]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY4088394307.mp3?updated=1517420286" length="82820702" type="audio/mpeg" />
</item>
<item>
<title>First They Came For the Immigrants</title>
<description>As Donald Trump forges ahead with his plans for mass deportations and Democrats flail in their response, Ninaj Roul and Yanira Arias describe the plight of hundreds of thousands of people in imminent danger of deportation. Journalist Nick Pinto reveals how ICE agents are staking out churches and homes of immigrant rights activists. Intercept Washington D.C. bureau chief Ryan Grim breaks down a clause slipped into the budget bill that gives the White House authority to fund CIA programs without oversight. We talk to revolutionary musical artist Seun Kuti, son of the legendary Afrobeat pioneer Fela, and hear music from his forthcoming album, Black Times.</description>
<pubDate>Wed, 24 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>First They Came For the Immigrants</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump promised to go to war against immigrants.</itunes:subtitle>
<itunes:summary>
<![CDATA[As Donald Trump forges ahead with his plans for mass deportations and Democrats flail in their response, Ninaj Roul and Yanira Arias describe the plight of hundreds of thousands of people in imminent danger of deportation. Journalist Nick Pinto reveals how ICE agents are staking out churches and homes of immigrant rights activists. Intercept Washington D.C. bureau chief Ryan Grim breaks down a clause slipped into the budget bill that gives the White House authority to fund CIA programs without oversight. We talk to revolutionary musical artist Seun Kuti, son of the legendary Afrobeat pioneer Fela, and hear music from his forthcoming album, Black Times.]]>
</itunes:summary>
<itunes:duration>5391</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[366970c8-fb38-11e7-847d-5376b55355c7]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY4168280488.mp3" length="86265939" type="audio/mpeg" />
</item>
<item>
<title>BONUS: Leading Marxist Scholar David Harvey on Trump, Wall Street and Debt Peonage</title>
<description>We live in a society that does not study its own history —&amp;nbsp; its unvarnished history — and often current events are analyzed in a vacuum that almost never includes the context or history necessary to understand what is new, what is old and how we got to where we are. As Trump celebrates his first year in office and demonstrations confront a year of his rule, leading Marxist scholar David Harvey sat down for an interview on Intercepted. Harvey is one of the leading Marxist thinkers in the world and a leading authority on Marxs "Das Kapital," which turned 150 years old late last year. Harvey is Distinguished Professor of Anthropology and Geography at the City University of New York.</description>
<pubDate>Sun, 21 Jan 2018 13:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>BONUS: Leading Marxist Scholar David Harvey on Trump, Wall Street and Debt Peonage</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Our full interview with Prof. David Harvey.</itunes:subtitle>
<itunes:summary>
<![CDATA[We live in a society that does not study its own history —&nbsp; its unvarnished history — and often current events are analyzed in a vacuum that almost never includes the context or history necessary to understand what is new, what is old and how we got to where we are. As Trump celebrates his first year in office and demonstrations confront a year of his rule, leading Marxist scholar David Harvey sat down for an interview on Intercepted. Harvey is one of the leading Marxist thinkers in the world and a leading authority on Marxs "Das Kapital," which turned 150 years old late last year. Harvey is Distinguished Professor of Anthropology and Geography at the City University of New York.]]>
</itunes:summary>
<itunes:duration>4868</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[51eff276-fe3b-11e7-bfff-f76c738c2592]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY3569313669.mp3" length="77897142" type="audio/mpeg" />
</item>
<item>
<title>White Mirror</title>
<description>Jeremy lays out the bloody US history in Haiti and El Salvador and blasts the bipartisan, selective amnesia and historical revisionism that “American exceptionalism” demands. Rep. Tulsi Gabbard discusses U.S. regime change, North Korea and why Bernie Sanders would have defeated Trump. As Robert Mueller hits Bannon with a Grand Jury subpoena, former CIA operative and&amp;nbsp; Cipher Brief columnist John Sipher and journalist Marcy Wheeler of Emptywheel analyze the Russia investigation and the Steele dossier. Leading Marxist scholar David Harvey talks about debt peonage in the age of Trump and the crimes of capitalism.</description>
<pubDate>Wed, 17 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>White Mirror</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump is a racist and the perfect man to represent Americas racist legacy in the countries he called shitholes. </itunes:subtitle>
<itunes:summary>
<![CDATA[Jeremy lays out the bloody US history in Haiti and El Salvador and blasts the bipartisan, selective amnesia and historical revisionism that “American exceptionalism” demands. Rep. Tulsi Gabbard discusses U.S. regime change, North Korea and why Bernie Sanders would have defeated Trump. As Robert Mueller hits Bannon with a Grand Jury subpoena, former CIA operative and&nbsp; Cipher Brief columnist John Sipher and journalist Marcy Wheeler of Emptywheel analyze the Russia investigation and the Steele dossier. Leading Marxist scholar David Harvey talks about debt peonage in the age of Trump and the crimes of capitalism.]]>
</itunes:summary>
<itunes:duration>6409</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[3660ad94-fb38-11e7-847d-436a066985fa]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY1407171456.mp3?updated=1516180736" length="102550465" type="audio/mpeg" />
</item>
<item>
<title>BONUS: All The News Unfit to Print</title>
<description>James Risen is a legend in the world of investigative and national security journalism. As a reporter for the New York Times, Risen broke some of the most important stories of the post 9/11 era, from the warrantless surveillance against Americans conducted under the Bush-Cheney administration, to black prison sites run by the CIA, to failed covert actions in Iran. Risen has won the Pulitzer and other major journalism awards. But perhaps what he is now most famous for is fighting a battle under both the Bush and Obama administrations as they demanded — under threat of imprisonment —the name of one of Risens alleged confidential sources. But it isnt just the government that Risen had to fight. He also battled his own editors and other powerful figures at the New York Times. Risen is now a senior national security correspondent at The Intercept where his incredible inside story has now been published. We talk with Risen about his career at the New York Times in a special edition of Intercepted.</description>
<pubDate>Wed, 03 Jan 2018 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>BONUS: All The News Unfit to Print</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>James Risen on His Battles with Bush, Obama, and the New York Times</itunes:subtitle>
<itunes:summary>
<![CDATA[James Risen is a legend in the world of investigative and national security journalism. As a reporter for the New York Times, Risen broke some of the most important stories of the post 9/11 era, from the warrantless surveillance against Americans conducted under the Bush-Cheney administration, to black prison sites run by the CIA, to failed covert actions in Iran. Risen has won the Pulitzer and other major journalism awards. But perhaps what he is now most famous for is fighting a battle under both the Bush and Obama administrations as they demanded — under threat of imprisonment —the name of one of Risens alleged confidential sources. But it isnt just the government that Risen had to fight. He also battled his own editors and other powerful figures at the New York Times. Risen is now a senior national security correspondent at The Intercept where his incredible inside story has now been published. We talk with Risen about his career at the New York Times in a special edition of Intercepted.]]>
</itunes:summary>
<itunes:duration>3805</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[6bdd6660-f039-11e7-acba-33ffde0bb3cc]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY1217453507.mp3" length="60884950" type="audio/mpeg" />
</item>
<item>
<title>Full Metal Jackass</title>
<description>Former Nixon White House counsel John Dean talks about the Mueller investigation, how the CIA may benefit from Trumps presidency and how Trump stacks up to Nixon and Reagan. Pentagon Papers whistleblower Daniel Ellsberg talks about the classified secrets he has kept for decades. He has just published his story in a new book, The Doomsday Machine. Field of Vision takes us inside the very strange world of Steve Bannons films. Patterson Hood of the band Drive-By Truckers performs.</description>
<pubDate>Wed, 13 Dec 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Full Metal Jackass</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Former Nixon Lawyer John Dean and Daniel Ellsberg Analyze the Trump Moment</itunes:subtitle>
<itunes:summary>
<![CDATA[Former Nixon White House counsel John Dean talks about the Mueller investigation, how the CIA may benefit from Trumps presidency and how Trump stacks up to Nixon and Reagan. Pentagon Papers whistleblower Daniel Ellsberg talks about the classified secrets he has kept for decades. He has just published his story in a new book, The Doomsday Machine. Field of Vision takes us inside the very strange world of Steve Bannons films. Patterson Hood of the band Drive-By Truckers performs.]]>
</itunes:summary>
<itunes:duration>5670</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bb062002-9d9f-11e7-b8c4-9701f8d8d38e]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY9016904056.mp3" length="90720966" type="audio/mpeg" />
</item>
<item>
<title>Who's Afraid of the Alt-Deep State?</title>
<description>Matthew Cole joins Jeremy for a discussion about their explosive report in The Intercept that Blackwater founder Erik Prince has been pitching a private spy operation to the White House and CIA. Activist and comedian Randy Credico, who has been hit with a subpoena from the House Intelligence Committee investigating Trump and Russia, joins us.&amp;nbsp; Journalist Barrett Brown talks about the FBIs campaign against him and offers a critique of Wikileaks. Singer Amanda Palmer talks about her provocative new video for a cover she did of Pink Floyds “Mother."</description>
<pubDate>Wed, 06 Dec 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Who's Afraid of the Alt-Deep State?</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump wants to make 1980s Reagan-era covert wars great again.</itunes:subtitle>
<itunes:summary>
<![CDATA[Matthew Cole joins Jeremy for a discussion about their explosive report in The Intercept that Blackwater founder Erik Prince has been pitching a private spy operation to the White House and CIA. Activist and comedian Randy Credico, who has been hit with a subpoena from the House Intelligence Committee investigating Trump and Russia, joins us.&nbsp; Journalist Barrett Brown talks about the FBIs campaign against him and offers a critique of Wikileaks. Singer Amanda Palmer talks about her provocative new video for a cover she did of Pink Floyds “Mother."]]>
</itunes:summary>
<itunes:duration>6260</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bafb58fc-9d9f-11e7-b8c4-070c14a1debb]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY9210981870.mp3" length="100160574" type="audio/mpeg" />
</item>
<item>
<title>Very Bad Men</title>
<description>This week on Intercepted: Sen. Chris Murphy blasts the US government for its role in the destruction of Yemen. Jeremy tears apart Thomas Friedmans gross love letter to the Saudi Crown Prince and talks about the bi-partisan war against journalism from Bill Clinton to Donald Trump. The Intercepts Betsy Reed and Buzzfeeds Katie Baker analyze this unprecedented public fight against sexual assaulters. Analysis from Harare, Zimbabwe on the ouster of Robert Mugabe. Comedian Joe Para performs a dramatic reenactment of a secret Snowden document.</description>
<pubDate>Wed, 29 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Very Bad Men</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Trump, the Saudi Crown Prince, Sexual Assaulters, and Robert Mugabe</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Sen. Chris Murphy blasts the US government for its role in the destruction of Yemen. Jeremy tears apart Thomas Friedmans gross love letter to the Saudi Crown Prince and talks about the bi-partisan war against journalism from Bill Clinton to Donald Trump. The Intercepts Betsy Reed and Buzzfeeds Katie Baker analyze this unprecedented public fight against sexual assaulters. Analysis from Harare, Zimbabwe on the ouster of Robert Mugabe. Comedian Joe Para performs a dramatic reenactment of a secret Snowden document.]]>
</itunes:summary>
<itunes:duration>5565</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baf3e644-9d9f-11e7-b8c4-9f5a004c8b47]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY5979450332.mp3?updated=1511943682" length="89042024" type="audio/mpeg" />
</item>
<item>
<title>The Distraction in Chief</title>
<description>This week on Intercepted: Rami Khouri breaks down the Saudi agenda in the Middle East, its destruction of Yemen and the bizarre case of the exiled Lebanese prime minister. Aram Roston of Buzzfeed, Spencer Ackerman of the Daily Beast, and The Intercepts Matthew Cole join Jeremy for a discussion on the mysterious death of a Green Beret in Mali and why the CIA and US military are quite content with the Trump presidency. Wikileaks slid into Donald Trump Jr.s DMs. Intercept co-founder Glenn Greenwald analyzes what the messages say and how the media covered the story. And we talk to two newly elected Democrats in Virginia: Lee Carter and Elizabeth Guzman. Donald Trump stars in American Beauty.</description>
<pubDate>Wed, 15 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>The Distraction in Chief</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>While the media overwhelmingly focuses on Trump and Russia, Yemen is dying, covert ops are spreading and war is raging.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Rami Khouri breaks down the Saudi agenda in the Middle East, its destruction of Yemen and the bizarre case of the exiled Lebanese prime minister. Aram Roston of Buzzfeed, Spencer Ackerman of the Daily Beast, and The Intercepts Matthew Cole join Jeremy for a discussion on the mysterious death of a Green Beret in Mali and why the CIA and US military are quite content with the Trump presidency. Wikileaks slid into Donald Trump Jr.s DMs. Intercept co-founder Glenn Greenwald analyzes what the messages say and how the media covered the story. And we talk to two newly elected Democrats in Virginia: Lee Carter and Elizabeth Guzman. Donald Trump stars in American Beauty.]]>
</itunes:summary>
<itunes:duration>5716</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baec6cf2-9d9f-11e7-b8c4-832adc6b044a]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY5077597385.mp3" length="91468695" type="audio/mpeg" />
</item>
<item>
<title>Say Hello to My Little Hands</title>
<description>This week on Intercepted: Rep. Ro Khanna calls for a complete end to all U.S. military assistance to Saudi Arabia and the&amp;nbsp; catastrophe in Yemen. The former chief prosecutor at Guantanamo, Col. Morris Davis, blasts Trump over his interference in the case of Army Sergeant Bowe Bergdahl and the recent terror attack in New York. And as the Paradise Papers rock the world of the rich who use offshore banks and law firms, we get analysis from Nomi Prins.</description>
<pubDate>Wed, 08 Nov 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Say Hello to My Little Hands</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>True (War) Crimes of the Rich and Infamous</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Rep. Ro Khanna calls for a complete end to all U.S. military assistance to Saudi Arabia and the&nbsp; catastrophe in Yemen. The former chief prosecutor at Guantanamo, Col. Morris Davis, blasts Trump over his interference in the case of Army Sergeant Bowe Bergdahl and the recent terror attack in New York. And as the Paradise Papers rock the world of the rich who use offshore banks and law firms, we get analysis from Nomi Prins.]]>
</itunes:summary>
<itunes:duration>5476</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bae4af26-9d9f-11e7-b8c4-d7bd1cbbac44]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY3384065210.mp3" length="87615947" type="audio/mpeg" />
</item>
<item>
<title>Criminal Indictments at Home, Secret Wars Abroad</title>
<description>This week on Intercepted: New York Times reporter Charlie Savage and former federal prosecutor Ken White of Popehat break down the recent indictment and plea deal and what it may mean for Trump. Investigative journalist Nick Turse and Kenya scholar Samar Al-Bulushi take us into the world of US militarism in Africa: secret drone bases, US commandos and Washington-backed African forces operating under the guise of the “war on terror.” Musician Roberto Lange of Helado Negro performs.</description>
<pubDate>Wed, 01 Nov 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Criminal Indictments at Home, Secret Wars Abroad</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Robert Muellers investigation intensifies as Trump grants the CIA and military new kill powers.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: New York Times reporter Charlie Savage and former federal prosecutor Ken White of Popehat break down the recent indictment and plea deal and what it may mean for Trump. Investigative journalist Nick Turse and Kenya scholar Samar Al-Bulushi take us into the world of US militarism in Africa: secret drone bases, US commandos and Washington-backed African forces operating under the guise of the “war on terror.” Musician Roberto Lange of Helado Negro performs.]]>
</itunes:summary>
<itunes:duration>4504</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[badd1b62-9d9f-11e7-b8c4-bb0a3510ea9d]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY9002073032.mp3" length="72073299" type="audio/mpeg" />
</item>
<item>
<title>Mike Pence is The Koch Brothers' Manchurian Candidate</title>
<description>This week on Intercepted: Investigative journalist Jane Mayer exposes the Koch Brother puppet masters behind Vice President Mike Pences rise to power and the ruthless pursuit of corporate profits that put Pence a heartbeat from the presidency.We speak to Chinese dissident and renown artist Ai Weiwei about the humanitarian catastrophe of the 65 million globally displaced migrants and his new documentary, Human Flow. And we end with Deerhoof's Greg Saunier on the songs of “Mountain Moves.”</description>
<pubDate>Wed, 25 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Mike Pence is The Koch Brothers' Manchurian Candidate</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The ruthless pursuit of corporate profits is a heartbeat from the presidency.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Investigative journalist Jane Mayer exposes the Koch Brother puppet masters behind Vice President Mike Pences rise to power and the ruthless pursuit of corporate profits that put Pence a heartbeat from the presidency.We speak to Chinese dissident and renown artist Ai Weiwei about the humanitarian catastrophe of the 65 million globally displaced migrants and his new documentary, Human Flow. And we end with Deerhoof's Greg Saunier on the songs of “Mountain Moves.”]]>
</itunes:summary>
<itunes:duration>4458</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bad56944-9d9f-11e7-b8c4-036f3898314a]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY6525793662.mp3?updated=1508912939" length="71329332" type="audio/mpeg" />
</item>
<item>
<title>Canada is Racist Too</title>
<description>This week on Intercepted live from Toronto: A recent poll puts activist Desmond Cole in prime position to win the mayorship. We talk to him about Canadas stop and frisk and how Cole would change Toronto. Journalist Naomi Klein warns that the Trudeau and Trump brands may have more in common than expected. And returning Iraqi-Canadian hip-hop artist Narcy gives a powerful live performance.&lt;br&gt;&lt;br&gt;Become a sustaining member! Go to &lt;a href="https://theintercept.com/join"&gt;theintercept.com/join&lt;/a&gt; for more.</description>
<pubDate>Wed, 18 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Canada is Racist Too</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Could a young, radical black activist be the next mayor of Toronto?</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted live from Toronto: A recent poll puts activist Desmond Cole in prime position to win the mayorship. We talk to him about Canadas stop and frisk and how Cole would change Toronto. Journalist Naomi Klein warns that the Trudeau and Trump brands may have more in common than expected. And returning Iraqi-Canadian hip-hop artist Narcy gives a powerful live performance.<br><br>Become a sustaining member! Go to <a href="https://theintercept.com/join">theintercept.com/join</a> for more.]]>
</itunes:summary>
<itunes:duration>4613</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bacdc1bc-9d9f-11e7-b8c4-bf06486207e8]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY1212140419.mp3" length="73809084" type="audio/mpeg" />
</item>
<item>
<title>The White Stuff</title>
<description>Trump sent Mike Pence on a mission to protest black protesters at an NFL game. Acclaimed author and journalist Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more. Mehrsa Baradaran breaks down the roots of economic apartheid in the US, the ongoing impact of slavery on black communities and offers a provocative history of black banks. And the lead singer of Mashrou Leila, Hamed Sinno, talks about being queer and Arab in the Middle East and Trumps America.</description>
<pubDate>Wed, 11 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>The White Stuff</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more.</itunes:subtitle>
<itunes:summary>
<![CDATA[Trump sent Mike Pence on a mission to protest black protesters at an NFL game. Acclaimed author and journalist Ta-Nehisi Coates talks about Trump, Obama, Bernie Sanders, Hillary Clinton, the NFL and much more. Mehrsa Baradaran breaks down the roots of economic apartheid in the US, the ongoing impact of slavery on black communities and offers a provocative history of black banks. And the lead singer of Mashrou Leila, Hamed Sinno, talks about being queer and Arab in the Middle East and Trumps America.]]>
</itunes:summary>
<itunes:duration>5664</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bac62452-9d9f-11e7-b8c4-17ec33943c11]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY7057322394.mp3?updated=1507702418" length="90635702" type="audio/mpeg" />
</item>
<item>
<title>Guns Before Country</title>
<description>This week, Jeremy talks about the Coalition of the Killing — gun lobbyists, politicians and weapons manufacturers — the only beneficiaries of the massacre in Las Vegas. Alynda Segarra of the band Hurray for the Riff Raff explores her Puerto Rican roots and performs new songs. Former US Army Ranger Rory Fanning talks about his slain comrade, NFL star-turned soldier Pat Tillman. Historian Jeanne Theoharis shreds the sanitizing of the legacies of Martin Luther King Jr. and Rosa Parks. And Donald Trump takes his love of guns into the Twilight Zone.&lt;br&gt;&lt;br&gt;Support our show — become a member!&amp;nbsp; &lt;a href="http://theintercept.com/join"&gt;theintercept.com/join&lt;/a&gt;&lt;br&gt;&lt;br&gt;Panoply's podcast listener survey: &lt;a href="http://survey.panoply.fm"&gt;survey.panoply.fm&lt;/a&gt;</description>
<pubDate>Wed, 04 Oct 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>Guns Before Country</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Lobbyists, politicians and weapons manufacturers are the only beneficiaries of the massacre in Las Vegas. </itunes:subtitle>
<itunes:summary>
<![CDATA[This week, Jeremy talks about the Coalition of the Killing — gun lobbyists, politicians and weapons manufacturers — the only beneficiaries of the massacre in Las Vegas. Alynda Segarra of the band Hurray for the Riff Raff explores her Puerto Rican roots and performs new songs. Former US Army Ranger Rory Fanning talks about his slain comrade, NFL star-turned soldier Pat Tillman. Historian Jeanne Theoharis shreds the sanitizing of the legacies of Martin Luther King Jr. and Rosa Parks. And Donald Trump takes his love of guns into the Twilight Zone.<br><br>Support our show — become a member!&nbsp; <a href="http://theintercept.com/join">theintercept.com/join</a><br><br>Panoply's podcast listener survey: <a href="http://survey.panoply.fm">survey.panoply.fm</a>]]>
</itunes:summary>
<itunes:duration>4763</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[babd79c4-9d9f-11e7-b8c4-bfa2bf7b2870]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY1463981678.mp3" length="76216529" type="audio/mpeg" />
</item>
<item>
<title>For Whom the Trump Trolls</title>
<description>This week on Intercepted, physicist David Wright from the Union of Concerned Scientists explains how easy it would be for Trump to launch a nuclear strike. Professor James Fernandez of NYU talks about the Abraham Lincoln Brigade, the 3,000 Americans who tried to stop fascism before it spread in Europe. We speak with the directors of a haunting new film about a terror attack in an Israeli bus station that leads to the brutal mob killing of an innocent Eritrean immigrant. And Donald Trump gets a visit from the two Bobs in his Office Space.</description>
<pubDate>Wed, 27 Sep 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>For Whom the Trump Trolls</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>What the Abraham Lincoln Brigade can teach us about fighting fascism in the 21st century.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted, physicist David Wright from the Union of Concerned Scientists explains how easy it would be for Trump to launch a nuclear strike. Professor James Fernandez of NYU talks about the Abraham Lincoln Brigade, the 3,000 Americans who tried to stop fascism before it spread in Europe. We speak with the directors of a haunting new film about a terror attack in an Israeli bus station that leads to the brutal mob killing of an innocent Eritrean immigrant. And Donald Trump gets a visit from the two Bobs in his Office Space.]]>
</itunes:summary>
<itunes:duration>4754</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[bab3035e-9d9f-11e7-b8c4-a723efdad05b]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY7212168126.mp3?updated=1506529903" length="76071497" type="audio/mpeg" />
</item>
<item>
<title>'Merican Psycho</title>
<description>Jeremy analyzes Trumps belligerent UN speech and the massive military budget the Democrats just gave him. Journalist Gary Rivlin takes us deep inside the world of the Goldman Sachs executives now working for Trump. Poet Aja Monet performs. The Intercepts Alice Speri investigates the militarization of police and how Israel is training American cops. Plus, Donald Trump stars in American Psycho.</description>
<pubDate>Wed, 20 Sep 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>'Merican Psycho</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump visits the UN and returns some videotapes.</itunes:subtitle>
<itunes:summary>
<![CDATA[Jeremy analyzes Trumps belligerent UN speech and the massive military budget the Democrats just gave him. Journalist Gary Rivlin takes us deep inside the world of the Goldman Sachs executives now working for Trump. Poet Aja Monet performs. The Intercepts Alice Speri investigates the militarization of police and how Israel is training American cops. Plus, Donald Trump stars in American Psycho.]]>
</itunes:summary>
<itunes:duration>4370</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[baa7cb06-9d9f-11e7-b8c4-d7ae2711974f]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY8078356160.mp3" length="69924570" type="audio/mpeg" />
</item>
<item>
<title>The Super Bowl of Racism</title>
<description>NSA whistleblower Edward Snowden discusses the massive Equifax data breach and allegations of Russian interference in the US election. Commentator Shaun King explains his call for a boycott of the NFL and talks about his campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle performs.</description>
<pubDate>Wed, 13 Sep 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:title>The Super Bowl of Racism</itunes:title>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump loves him some beauty pageants. But he probably wasnt so hot on this years Miss Texas who called him out on neo-Nazi violence.</itunes:subtitle>
<itunes:summary>
<![CDATA[NSA whistleblower Edward Snowden discusses the massive Equifax data breach and allegations of Russian interference in the US election. Commentator Shaun King explains his call for a boycott of the NFL and talks about his campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle performs.]]>
</itunes:summary>
<itunes:duration>4171</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[7df4070a-9832-11e7-adac-cb37b05d5e24]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY6458293736.mp3" length="66738886" type="audio/mpeg" />
</item>
<item>
<title>Atlas Golfed — U.S.-Backed Think Tanks Target Latin America</title>
<description>This week on Intercepted: Jeremy gives an update on the aftermath of Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang lays out how a network of libertarian think tanks called the Atlas Network is insidiously shaping political infrastructure in Latin America. We speak with attorney and former Hugo Chavez adviser Eva Golinger about the Venezuela's political turmoil.And we hear Claudia Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her music and hopes for Venezuela.</description>
<pubDate>Wed, 09 Aug 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump is on his version of a staycation, chilling at his golf course resort in New Jersey and watching FOX News or tweeting non-stop — when hes not golfing or threatening nuclear war.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Jeremy gives an update on the aftermath of Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang lays out how a network of libertarian think tanks called the Atlas Network is insidiously shaping political infrastructure in Latin America. We speak with attorney and former Hugo Chavez adviser Eva Golinger about the Venezuela's political turmoil.And we hear Claudia Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her music and hopes for Venezuela.]]>
</itunes:summary>
<itunes:duration>4415</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[7c207a24-e33f-11e6-9438-eb45dcf36a1d]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5331443769.mp3" length="67527575" type="audio/mpeg" />
</item>
<item>
<title>Pyongyang and the White House Gang</title>
<description>News from the White House this week has been like a twisted mash up of Here Comes Honey Boo Boo, Macbeth, Project Runway and a Mr. Bean movie. Dime-store Sopranos reject Anthony Scaramucci was fired after just 10 days as White House communications director. Reince Priebus is out as chief of staff, Gen. John Kelly is in. And with spiking tensions between the United States and North Korea, we reflect on the history of the region. Plus, The Intercepts Naomi Klein talks to U.K. Labour Party leader Jeremy Corbyn about the lessons the Democratic Party could learn from Corbyns unexpected electoral success.</description>
<pubDate>Wed, 02 Aug 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>With spiking tensions between the U.S. and North Korea, we reflect on the history of the region. And The Intercepts Naomi Klein talks to U.K. Labour Party leader Jeremy Corbyn.</itunes:subtitle>
<itunes:summary>
<![CDATA[News from the White House this week has been like a twisted mash up of Here Comes Honey Boo Boo, Macbeth, Project Runway and a Mr. Bean movie. Dime-store Sopranos reject Anthony Scaramucci was fired after just 10 days as White House communications director. Reince Priebus is out as chief of staff, Gen. John Kelly is in. And with spiking tensions between the United States and North Korea, we reflect on the history of the region. Plus, The Intercepts Naomi Klein talks to U.K. Labour Party leader Jeremy Corbyn about the lessons the Democratic Party could learn from Corbyns unexpected electoral success.]]>
</itunes:summary>
<itunes:duration>3712</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5850753c-dcf9-11e6-a5a2-a7df163d0693]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL4502761802.mp3" length="56280711" type="audio/mpeg" />
</item>
<item>
<title>Glenn Greenwald on the New Cold War</title>
<description>With all the constant hype about Russia, youd think we were living in a new Cold War. This week on Intercepted: Glenn Greenwald fills in for Jeremy Scahill, and we take a deep dive into the origins and evolution of the Trump-Russia story. Fox News' Tucker Carlson and Glenn find something they can actually agree on (the Democratic establishments Russia hysteria), but diverge on Tuckers coverage of immigration and crime. Russian-American writer Masha Gessen explains how conspiracy thinking is a mirror of the leaders we put in power.</description>
<pubDate>Wed, 26 Jul 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>With all the constant hype about Russia, youd think we were living in a new Cold War.</itunes:subtitle>
<itunes:summary>
<![CDATA[With all the constant hype about Russia, youd think we were living in a new Cold War. This week on Intercepted: Glenn Greenwald fills in for Jeremy Scahill, and we take a deep dive into the origins and evolution of the Trump-Russia story. Fox News' Tucker Carlson and Glenn find something they can actually agree on (the Democratic establishments Russia hysteria), but diverge on Tuckers coverage of immigration and crime. Russian-American writer Masha Gessen explains how conspiracy thinking is a mirror of the leaders we put in power.]]>
</itunes:summary>
<itunes:duration>3565</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[584711b8-dcf9-11e6-a5a2-d7a378461c40]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL8633314507.mp3" length="53935124" type="audio/mpeg" />
</item>
<item>
<title>Veni, Vidi, Tweeti</title>
<description>Donald Trump enjoyed playing fireman and asking where the fire is. Hint: all around you, Mr. President. This week on Intercepted: the famed rebel academic, Alfred McCoy, whose book on narcotrafficking the CIA tried to stop from being published, lays out his meticulously argued theory that the U.S. empire will fall by the year 2030. The Washington Posts media columnist, Margaret Sullivan, talks about Trump ratcheting up the war on whistleblowers and the existence of a free press.</description>
<pubDate>Wed, 19 Jul 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump enjoyed playing fireman and asking where the fire is. Hint: all around you, Mr. President.</itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump enjoyed playing fireman and asking where the fire is. Hint: all around you, Mr. President. This week on Intercepted: the famed rebel academic, Alfred McCoy, whose book on narcotrafficking the CIA tried to stop from being published, lays out his meticulously argued theory that the U.S. empire will fall by the year 2030. The Washington Posts media columnist, Margaret Sullivan, talks about Trump ratcheting up the war on whistleblowers and the existence of a free press.]]>
</itunes:summary>
<itunes:duration>4146</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[583dc8f6-dcf9-11e6-a5a2-97233491f3c8]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL4964577496.mp3" length="63216744" type="audio/mpeg" />
</item>
<item>
<title>Dumb, Dumber and Don Jr.</title>
<description>This week on Intercepted: Don Jr. is in the shit throne over a secret meeting he had with a Russian lawyer. Could this be, as many in the media are claiming, the smoking gun of Russia collusion? Intercept co-founder Glenn Greenwald weighs in and debunks a forged NSA document sent to Rachel Maddow. Intercept reporters Alice Speri and Alleen Brown talk about the shadowy mercenary company TigerSwan. We also hear music from Victoria Ruiz of the punk band Downtown Boys.</description>
<pubDate>Wed, 12 Jul 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The old adage that the cover-up is worse than the crime seems like it was tailored specifically for Donald Trump and his merry band of imbeciles, ideological zealots, and…family members.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week on Intercepted: Don Jr. is in the shit throne over a secret meeting he had with a Russian lawyer. Could this be, as many in the media are claiming, the smoking gun of Russia collusion? Intercept co-founder Glenn Greenwald weighs in and debunks a forged NSA document sent to Rachel Maddow. Intercept reporters Alice Speri and Alleen Brown talk about the shadowy mercenary company TigerSwan. We also hear music from Victoria Ruiz of the punk band Downtown Boys.]]>
</itunes:summary>
<itunes:duration>4059</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5834b428-dcf9-11e6-a5a2-f7aca16eec6e]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5169968320.mp3" length="61824940" type="audio/mpeg" />
</item>
<item>
<title>The House of Trump</title>
<description>President Trump said when it comes to health insurance, he would cover everyone. He lied. Meanwhile the Crown Prince of America, Jared Kushner, and Mohammed Bin Salman, Crown Prince of Saudi Arabia, play house with foreign policy. This week: Al Jazeeras Mehdi Hasan fills in for Jeremy Scahill. Intercept reporter Murtaza Hussain and journalist Rula Jebreal discuss the global consequences of the House of Trumps meddling in the Middle East. Historian Tom Holland joins Mehdi for a debate on the role of Islam within the Islamic State. Plus, actor Bill Camp reprises his role as the “SIGINT Philosopher.”</description>
<pubDate>Wed, 28 Jun 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The royal family of the United States takes some heat as the fate of American healthcare hangs on a few votes. </itunes:subtitle>
<itunes:summary>
<![CDATA[President Trump said when it comes to health insurance, he would cover everyone. He lied. Meanwhile the Crown Prince of America, Jared Kushner, and Mohammed Bin Salman, Crown Prince of Saudi Arabia, play house with foreign policy. This week: Al Jazeeras Mehdi Hasan fills in for Jeremy Scahill. Intercept reporter Murtaza Hussain and journalist Rula Jebreal discuss the global consequences of the House of Trumps meddling in the Middle East. Historian Tom Holland joins Mehdi for a debate on the role of Islam within the Islamic State. Plus, actor Bill Camp reprises his role as the “SIGINT Philosopher.”]]>
</itunes:summary>
<itunes:duration>3597</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5825189c-dcf9-11e6-a5a2-3765693ebff5]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5926659703.mp3" length="54443781" type="audio/mpeg" />
</item>
<item>
<title>Dispatch from the Dirtbag Left</title>
<description>While all eyes in Washington remain focused on the Russia investigation, a Republican firm forgot to secure its invasive personal data on 198 million American voters. This week on Intercepted: We speak to radical librarian Alison Macrina of the Library Freedom Project about the fight against digital surveillance. Sam Biddle gives an update on attacks on U.S. voting systems. And, we speak with one of the rising stars of the “dirtbag left,” Felix Biederman of Chapo Trap House.</description>
<pubDate>Wed, 21 Jun 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Why #resistance Twitter, establishment Democrats and neocon apologists are not leftists.</itunes:subtitle>
<itunes:summary>
<![CDATA[While all eyes in Washington remain focused on the Russia investigation, a Republican firm forgot to secure its invasive personal data on 198 million American voters. This week on Intercepted: We speak to radical librarian Alison Macrina of the Library Freedom Project about the fight against digital surveillance. Sam Biddle gives an update on attacks on U.S. voting systems. And, we speak with one of the rising stars of the “dirtbag left,” Felix Biederman of Chapo Trap House.]]>
</itunes:summary>
<itunes:duration>3540</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[581dd44c-dcf9-11e6-a5a2-03edebf2031b]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL7980248897.mp3" length="53535555" type="audio/mpeg" />
</item>
<item>
<title>The Trump Mixtape — Dantes Inferno meets Disco Inferno</title>
<description>Donald Trump has a great affinity for strongmen and for unquestioned loyalty of those who work for him. This week on Intercepted: Trumps besties in Saudi Arabia convinced him that Qatar is the premiere Arab nation sponsoring terrorism. Amnesty Internationals Sherine Tadros and al Jazeeras Mehdi Hasan analyze the hypocrisy-laden, bizarre crisis. Jeremy discusses the prosecution of an alleged NSA leaker. MSNBCs Chris Hayes talks Russia, Trump, the media and his new book A Colony in a Nation. DJ Spooky imagines a Trump-inspired mash-up of Dantes Inferno and Disco Inferno.</description>
<pubDate>Wed, 14 Jun 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump has made crystal clear that he has a great affinity for strongmen and for unquestioned loyalty of those who work for him. </itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump has a great affinity for strongmen and for unquestioned loyalty of those who work for him. This week on Intercepted: Trumps besties in Saudi Arabia convinced him that Qatar is the premiere Arab nation sponsoring terrorism. Amnesty Internationals Sherine Tadros and al Jazeeras Mehdi Hasan analyze the hypocrisy-laden, bizarre crisis. Jeremy discusses the prosecution of an alleged NSA leaker. MSNBCs Chris Hayes talks Russia, Trump, the media and his new book A Colony in a Nation. DJ Spooky imagines a Trump-inspired mash-up of Dantes Inferno and Disco Inferno.]]>
</itunes:summary>
<itunes:duration>4346</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5815ed86-dcf9-11e6-a5a2-ab3d4ad4b944]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL2441335022.mp3?updated=1497422014" length="66430432" type="audio/mpeg" />
</item>
<item>
<title>The Woman Democrats Love to Hate</title>
<description>The Green Partys Jill Stein has been widely attacked by Democrats simply for running for president. Some blame her for Hillary Clintons loss. This week, Stein strikes back at her critics and reveals the story behind the infamous Moscow dinner where she was seated with Vladimir Putin and Gen. Michael Flynn. The Intercepts DC bureau chief Ryan Grim digs into the contents of a newly published top secret NSA document outlining alleged Russian cyberattacks against software companies that service U.S. elections. And singer-songwriter Damien Jurado performs.</description>
<pubDate>Wed, 07 Jun 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Jill Stein has been widely attacked by Democrats simply for running for president. </itunes:subtitle>
<itunes:summary>
<![CDATA[The Green Partys Jill Stein has been widely attacked by Democrats simply for running for president. Some blame her for Hillary Clintons loss. This week, Stein strikes back at her critics and reveals the story behind the infamous Moscow dinner where she was seated with Vladimir Putin and Gen. Michael Flynn. The Intercepts DC bureau chief Ryan Grim digs into the contents of a newly published top secret NSA document outlining alleged Russian cyberattacks against software companies that service U.S. elections. And singer-songwriter Damien Jurado performs.]]>
</itunes:summary>
<itunes:duration>3693</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[580e21d2-dcf9-11e6-a5a2-53fae963a8d5]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5889277506.mp3?updated=1496817965" length="55977273" type="audio/mpeg" />
</item>
<item>
<title>There's Something About Jared</title>
<description>This week, the scandal spotlight shines on Trumps influential (and strangely quiet) son-in-law. We talk to national security correspondent Spencer Ackerman of The Daily Beast about Jared Kushners alleged meetings with Russian officials to establish back channel communications. Organizer and scholar Mariame Kaba offers a peoples history of prisons in the US and the politicians—both Democrats and Republicans—who have made them what they are today. And we hear an incredible rendition of “The Partisan” from composers and musicians Leo Heiblum of Mexico and Tenzin Choegyal of Tibet.&amp;nbsp;</description>
<pubDate>Wed, 31 May 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Jared Kushner is sort of like Donald Trumps less savvy version of Don Corleones consigliere. But did he make the Russians an offer they couldnt refuse?</itunes:subtitle>
<itunes:summary>
<![CDATA[This week, the scandal spotlight shines on Trumps influential (and strangely quiet) son-in-law. We talk to national security correspondent Spencer Ackerman of The Daily Beast about Jared Kushners alleged meetings with Russian officials to establish back channel communications. Organizer and scholar Mariame Kaba offers a peoples history of prisons in the US and the politicians—both Democrats and Republicans—who have made them what they are today. And we hear an incredible rendition of “The Partisan” from composers and musicians Leo Heiblum of Mexico and Tenzin Choegyal of Tibet.&nbsp;]]>
</itunes:summary>
<itunes:duration>3762</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[5807254e-dcf9-11e6-a5a2-cb45327dca79]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3830941587.mp3" length="57086537" type="audio/mpeg" />
</item>
<item>
<title>Donald Trump and his League of Extraordinary Despots</title>
<description>This week, Donald Trump stood in a sea of tyrants and joined in a bizarre group petting of a glowing white orb. Professor Asad AbuKhalil dissects Trumps summit in Saudi Arabia and the role Trumps friends in the Middle East play in fueling such horrors as the attack on Manchester. The Intercepts new DC bureau chief, Ryan Grim, and national security reporter Matthew Cole discuss Gen. Michael Flynn and whether anyone in the Trump administration realizes how insane their boss is. And Steve Earle performs live.</description>
<pubDate>Wed, 24 May 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>This week, Donald Trump stood in a sea of tyrants and joined in a bizarre group petting of a glowing white orb.</itunes:subtitle>
<itunes:summary>
<![CDATA[This week, Donald Trump stood in a sea of tyrants and joined in a bizarre group petting of a glowing white orb. Professor Asad AbuKhalil dissects Trumps summit in Saudi Arabia and the role Trumps friends in the Middle East play in fueling such horrors as the attack on Manchester. The Intercepts new DC bureau chief, Ryan Grim, and national security reporter Matthew Cole discuss Gen. Michael Flynn and whether anyone in the Trump administration realizes how insane their boss is. And Steve Earle performs live.]]>
</itunes:summary>
<itunes:duration>4198</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57ffd3e8-dcf9-11e6-a5a2-17ed73ff2f09]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3575958410.mp3" length="64048065" type="audio/mpeg" />
</item>
<item>
<title>Donald and the Terrible, Horrible, No Good, Very Bad Presidency</title>
<description>Donald Trump is spectacularly bad at being president. This week on Intercepted, investigative journalist Marcy Wheeler and The Intercepts Glenn Greenwald analyze the latest insanity emanating from the White House. Pulitzer Prize-winning journalist Tim Weiner and Intercept writer Trevor Aaronson discuss the firing of James Comey and debate his FBI legacy. And Palestinian author and journalist Rula Jebreal explains why President Trump is going to Saudi Arabia and Israel on his first international trip.</description>
<pubDate>Wed, 17 May 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump is spectacularly bad at being president. </itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump is spectacularly bad at being president. This week on Intercepted, investigative journalist Marcy Wheeler and The Intercepts Glenn Greenwald analyze the latest insanity emanating from the White House. Pulitzer Prize-winning journalist Tim Weiner and Intercept writer Trevor Aaronson discuss the firing of James Comey and debate his FBI legacy. And Palestinian author and journalist Rula Jebreal explains why President Trump is going to Saudi Arabia and Israel on his first international trip.]]>
</itunes:summary>
<itunes:duration>3861</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57f826de-dcf9-11e6-a5a2-ff41a0b1698e]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3660243774.mp3" length="58666422" type="audio/mpeg" />
</item>
<item>
<title>James Comey, Chelsea Manning and the secrets America keeps</title>
<description>Donald Trumps complicated relationship with FBI Director James Comey came to a shocking conclusion in Tuesday nights episode of American shitshow. Glenn Greenwald analyzes Comeys firing. Next week, Chelsea Manning will be freed from prison. We hear exclusive audio from her trial and talk to journalist Alexa OBrien. And French civil liberties activist Yasser Louati says despite her defeat in the presidential election, many of Marine Le Pens ideas are already embedded in mainstream French politics. And a premiere track from hip-hop artists MC Sole and DJ Pain 1.</description>
<pubDate>Wed, 10 May 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trumps complicated relationship with FBI Director James Comey came to a shocking conclusion in Tuesday nights episode of American shitshow.</itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trumps complicated relationship with FBI Director James Comey came to a shocking conclusion in Tuesday nights episode of American shitshow. Glenn Greenwald analyzes Comeys firing. Next week, Chelsea Manning will be freed from prison. We hear exclusive audio from her trial and talk to journalist Alexa OBrien. And French civil liberties activist Yasser Louati says despite her defeat in the presidential election, many of Marine Le Pens ideas are already embedded in mainstream French politics. And a premiere track from hip-hop artists MC Sole and DJ Pain 1.]]>
</itunes:summary>
<itunes:duration>4197</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57ef98d4-dcf9-11e6-a5a2-374c19bb24e7]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5670631624.mp3" length="64045557" type="audio/mpeg" />
</item>
<item>
<title>BONUS: Jeremy talks Milo on Politically Re-Active</title>
<description>We're still a week away from the beginning of season two, but here's a taster of Jeremy's interview on our sister podcast, Politically Re-Active. Jeremy clears the air on his cancelled appearance on "Real Time with Bill Maher" with hosts W. Kamau Bell and Hari Kondabolu, and much more. To hear the full interview, subscribe to Politically Re-Active or head to politicallyreactive.com.</description>
<pubDate>Wed, 03 May 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle />
<itunes:summary>
<![CDATA[We're still a week away from the beginning of season two, but here's a taster of Jeremy's interview on our sister podcast, Politically Re-Active. Jeremy clears the air on his cancelled appearance on "Real Time with Bill Maher" with hosts W. Kamau Bell and Hari Kondabolu, and much more. To hear the full interview, subscribe to Politically Re-Active or head to politicallyreactive.com.]]>
</itunes:summary>
<itunes:duration>692</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[a940d60e-2fb9-11e7-8fb4-4b8f8bf5bfe4]]></guid>
<enclosure url="https://traffic.megaphone.fm/PPY5034885459.mp3" length="11072574" type="audio/mpeg" />
</item>
<item>
<title>Wikileaks vs the CIA</title>
<description>Wikileaks founder Julian Assange hits back at Trumps CIA director Mike Pompeo after Pompeo accused Wikileaks of being a “hostile non-state intelligence agency.” In a wide-ranging interview, Assange discusses the allegations Wikileaks was abetted by Russian intelligence in its publication of DNC emails, and the new-found admiration for him by FOX News and Donald Trump. Also, why Assange believes he and Hillary Clinton may get along if they ever met in person. And we premiere an unreleased song by Tom Morello of Rage Against the Machine.</description>
<pubDate>Wed, 19 Apr 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Julian Assange hits back at Trumps CIA director Mike Pompeo after Pompeo accused Wikileaks of being a “hostile non-state intelligence agency.”</itunes:subtitle>
<itunes:summary>
<![CDATA[Wikileaks founder Julian Assange hits back at Trumps CIA director Mike Pompeo after Pompeo accused Wikileaks of being a “hostile non-state intelligence agency.” In a wide-ranging interview, Assange discusses the allegations Wikileaks was abetted by Russian intelligence in its publication of DNC emails, and the new-found admiration for him by FOX News and Donald Trump. Also, why Assange believes he and Hillary Clinton may get along if they ever met in person. And we premiere an unreleased song by Tom Morello of Rage Against the Machine.]]>
</itunes:summary>
<itunes:duration>3901</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57e86f28-dcf9-11e6-a5a2-3f3b6bf611af]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5660744294.mp3" length="59298377" type="audio/mpeg" />
</item>
<item>
<title>The Emperors New Cruise Missiles</title>
<description>Nothing brings warmongers, hawks and elites from both parties closer than a cruise missile strike. This weeks Intercepted will piss off Assad supporters and the Democrats and Republicans fawning over Trumps newest war. Former Congressman Dennis Kucinich questions the official story on the chemical weapons attack. Murtaza Hussain on what Assad gains by using chemical weapons. And, Maher Arar is a Syrian-born Canadian engineer who was kidnapped at JFK airport by US operatives after 9/11 and rendered to Syria and tortured by Assads agents. Arar says he opposes Assad and US intervention. All that and a bucket of media stupidity to celebrate beautiful missiles.</description>
<pubDate>Wed, 12 Apr 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Nothing brings warmongers, hawks and elites from both parties closer than a cruise missile strike.</itunes:subtitle>
<itunes:summary>
<![CDATA[Nothing brings warmongers, hawks and elites from both parties closer than a cruise missile strike. This weeks Intercepted will piss off Assad supporters and the Democrats and Republicans fawning over Trumps newest war. Former Congressman Dennis Kucinich questions the official story on the chemical weapons attack. Murtaza Hussain on what Assad gains by using chemical weapons. And, Maher Arar is a Syrian-born Canadian engineer who was kidnapped at JFK airport by US operatives after 9/11 and rendered to Syria and tortured by Assads agents. Arar says he opposes Assad and US intervention. All that and a bucket of media stupidity to celebrate beautiful missiles.]]>
</itunes:summary>
<itunes:duration>3764</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57e131d6-dcf9-11e6-a5a2-efc4038c6546]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL8700626063.mp3" length="57118720" type="audio/mpeg" />
</item>
<item>
<title>Trump's Secret Prince</title>
<description>Erik Prince—the most infamous mercenary in modern U.S. history—is Trumps secret emissary. This week, an exclusive interview with Rep. Jan Schakowsky, who has fought a decades-long battle against Prince. Tavis Smiley talks about the “Santa Claus-ification” of Dr. Martin Luther King Jr. on the 50th anniversary of Kings militant speech against the Vietnam War. Rep. Barbara Lee reflects on her own historic anti-war speech, delivered three days after 9/11. And Vice President Pence, who cant be alone in a room with a woman who is not his wife, goes Psycho.&lt;br&gt;&lt;br&gt;&lt;em&gt;Please take a moment to fill out Panoply's survey about the shows you listen to, love, and what else you'd like to hear: &lt;/em&gt;&lt;a href="http://survey.panoply.fm"&gt;&lt;em&gt;survey.panoply.fm&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&amp;nbsp; Many thanks!&lt;/em&gt;</description>
<pubDate>Wed, 05 Apr 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Erik Prince is the most infamous mercenary in modern U.S. history. Hes also Trumps shadow advisor and secret emissary.</itunes:subtitle>
<itunes:summary>
<![CDATA[Erik Prince—the most infamous mercenary in modern U.S. history—is Trumps secret emissary. This week, an exclusive interview with Rep. Jan Schakowsky, who has fought a decades-long battle against Prince. Tavis Smiley talks about the “Santa Claus-ification” of Dr. Martin Luther King Jr. on the 50th anniversary of Kings militant speech against the Vietnam War. Rep. Barbara Lee reflects on her own historic anti-war speech, delivered three days after 9/11. And Vice President Pence, who cant be alone in a room with a woman who is not his wife, goes Psycho.<br><br><em>Please take a moment to fill out Panoply's survey about the shows you listen to, love, and what else you'd like to hear: </em><a href="http://survey.panoply.fm"><em>survey.panoply.fm</em></a><em>.&nbsp; Many thanks!</em>]]>
</itunes:summary>
<itunes:duration>3623</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57da1dce-dcf9-11e6-a5a2-2fa5756ae4a7]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL7737191155.mp3?updated=1491374970" length="54858396" type="audio/mpeg" />
</item>
<item>
<title>Trump Declares War on the Planet</title>
<description>Donald Trump officially rejects climate change and unofficially declares war on planet Earth. Naomi Klein takes us on a terrifying journey into Trumps real life version of The Purge. Boots Riley of The Coup discusses Trump and hip hop and performs. Murtaza Hussain talks about the US bombings in Iraq and Syria that have killed 1,000 civilians in one month. And, we talk to the developer of an app that tracks US drone strikes that Apple has censored 13 times.</description>
<pubDate>Wed, 29 Mar 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump officially rejects climate change and unofficially declares war on planet Earth.</itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump officially rejects climate change and unofficially declares war on planet Earth. Naomi Klein takes us on a terrifying journey into Trumps real life version of The Purge. Boots Riley of The Coup discusses Trump and hip hop and performs. Murtaza Hussain talks about the US bombings in Iraq and Syria that have killed 1,000 civilians in one month. And, we talk to the developer of an app that tracks US drone strikes that Apple has censored 13 times.]]>
</itunes:summary>
<itunes:duration>3512</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57d2f170-dcf9-11e6-a5a2-5fc4825a8119]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL9529758061.mp3" length="53079980" type="audio/mpeg" />
</item>
<item>
<title>Could Trump Start World War III?</title>
<description>Donald Trump has not started any new wars… yet. But his administration is pouring gasoline on several initiated by his predecessors. This week on Intercepted: US forces are deploying in Syria, as drone strikes expand in Yemen. And Russia and Iran loom over everything. We talk to veteran war correspondents Anand Gopal and Iona Craig. Glenn Greenwald analyzes James Comeys testimony on Capitol Hill and exposes a major lie spread about Edward Snowden. Actor William Camp “stars” in the real life story of the spy who became “the Socrates of the NSA.”</description>
<pubDate>Wed, 22 Mar 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump has not started any new wars… yet. But his administration is pouring gasoline on several initiated by his predecessors. </itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump has not started any new wars… yet. But his administration is pouring gasoline on several initiated by his predecessors. This week on Intercepted: US forces are deploying in Syria, as drone strikes expand in Yemen. And Russia and Iran loom over everything. We talk to veteran war correspondents Anand Gopal and Iona Craig. Glenn Greenwald analyzes James Comeys testimony on Capitol Hill and exposes a major lie spread about Edward Snowden. Actor William Camp “stars” in the real life story of the spy who became “the Socrates of the NSA.”]]>
</itunes:summary>
<itunes:duration>3683</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57cc3f10-dcf9-11e6-a5a2-4fe59e09c4e9]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL9134059577.mp3" length="55811761" type="audio/mpeg" />
</item>
<item>
<title>Snowden vs. Trump</title>
<description>This week, Intercepted is live from the SXSW Festival in Austin. Edward Snowden joins us via video feed from Moscow. He discusses Trumps allegations of Obamas wiretapping, analyzes some of the CIAs hacking capabilities, and blasts critics who accuse him of being a Russian agent. And we talk to Libyan-American hip hop artist Kayem, who was forced to keep a low profile the past several years after multiple detentions and visits from the FBI. He shares some verses with Intercepted.</description>
<pubDate>Wed, 15 Mar 2017 10:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Intercepted is live from the SXSW Festival in Austin with Edward Snowden joining via video feed from Moscow. </itunes:subtitle>
<itunes:summary>
<![CDATA[This week, Intercepted is live from the SXSW Festival in Austin. Edward Snowden joins us via video feed from Moscow. He discusses Trumps allegations of Obamas wiretapping, analyzes some of the CIAs hacking capabilities, and blasts critics who accuse him of being a Russian agent. And we talk to Libyan-American hip hop artist Kayem, who was forced to keep a low profile the past several years after multiple detentions and visits from the FBI. He shares some verses with Intercepted.]]>
</itunes:summary>
<itunes:duration>3331</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57c55fa6-dcf9-11e6-a5a2-1f24485c4305]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3645242256.mp3" length="50191046" type="audio/mpeg" />
</item>
<item>
<title>Ready to Lie</title>
<description>The Notorious B.I.G. famously alleged that federal agents were mad because he was flagrant. Trump also believes he has beef with the Feds, accusing Obama of tapping his phones. The Intercepts Matthew Cole and journalist Marcy Wheeler dissect the accusations and the (curious) denials. Sam Biddle and Josh Begley explain what the CIA hacking docs published by Wikileaks say about our “smart” TVs and phones. Journalist Aura Bogado confronts Trumps assault on undocumented immigrants. Punk band Anti-Flag performs. Plus, Trump “stars” in a scene from Goodfellas. Can he get out of Mar-a-Lago alive?</description>
<pubDate>Wed, 08 Mar 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The Notorious B.I.G. said federal agents were mad because he was flagrant. President Donald Trump also believes he has beef with the Feds. </itunes:subtitle>
<itunes:summary>
<![CDATA[The Notorious B.I.G. famously alleged that federal agents were mad because he was flagrant. Trump also believes he has beef with the Feds, accusing Obama of tapping his phones. The Intercepts Matthew Cole and journalist Marcy Wheeler dissect the accusations and the (curious) denials. Sam Biddle and Josh Begley explain what the CIA hacking docs published by Wikileaks say about our “smart” TVs and phones. Journalist Aura Bogado confronts Trumps assault on undocumented immigrants. Punk band Anti-Flag performs. Plus, Trump “stars” in a scene from Goodfellas. Can he get out of Mar-a-Lago alive?]]>
</itunes:summary>
<itunes:duration>4182</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57be3b36-dcf9-11e6-a5a2-07d3f1f2cb5f]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3152884319.mp3" length="63798125" type="audio/mpeg" />
</item>
<item>
<title>Donald in Wonderland</title>
<description>Ex-CIA analyst Nada Bakos and former FBI agent Clint Watts explain how Trumps administration could use “alternative intelligence” to justify dangerous military actions. Shane Bauer of Mother Jones breaks down the connections between immigration raids and soaring private prison profits. Plus the world premiere of a song by the Iraqi-Canadian hip hop artist Narcy. We bet you never thought youd hear Steve Bannons name rapped in autotune.</description>
<pubDate>Wed, 01 Mar 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Pundits are heaping praise on his “presidential” speech to Congress. Dont believe the hype.</itunes:subtitle>
<itunes:summary>
<![CDATA[Ex-CIA analyst Nada Bakos and former FBI agent Clint Watts explain how Trumps administration could use “alternative intelligence” to justify dangerous military actions. Shane Bauer of Mother Jones breaks down the connections between immigration raids and soaring private prison profits. Plus the world premiere of a song by the Iraqi-Canadian hip hop artist Narcy. We bet you never thought youd hear Steve Bannons name rapped in autotune.]]>
</itunes:summary>
<itunes:duration>4196</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57b74678-dcf9-11e6-a5a2-4fbc5ae0d0cf]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5707421213.mp3" length="64025913" type="audio/mpeg" />
</item>
<item>
<title>The Undisciplined Authoritarian</title>
<description>New York Times investigative reporter James Risen breaks down Trumps declaration that journalists are the enemy and analyzes Trumps royal court. ACLU lawyer Chase Strangio and former New England Patriots star Donté Stallworth talk about the war on the transgender community and the rising resistance of pro athletes. Sam Biddle exposes the Trump-connected firm that helped the NSA spy on the world and actor Wallace Shawn stars as an NSA operative who is worried about adversaries spying on his luncheons. Plus music from Anohni.</description>
<pubDate>Wed, 22 Feb 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Journalist James Risen faced imprisonment under Obamas Justice Department and is preparing to do battle with Donald Trump.</itunes:subtitle>
<itunes:summary>
<![CDATA[New York Times investigative reporter James Risen breaks down Trumps declaration that journalists are the enemy and analyzes Trumps royal court. ACLU lawyer Chase Strangio and former New England Patriots star Donté Stallworth talk about the war on the transgender community and the rising resistance of pro athletes. Sam Biddle exposes the Trump-connected firm that helped the NSA spy on the world and actor Wallace Shawn stars as an NSA operative who is worried about adversaries spying on his luncheons. Plus music from Anohni.]]>
</itunes:summary>
<itunes:duration>4209</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57b018d0-dcf9-11e6-a5a2-e736fa72fede]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL3910130795.mp3" length="64229877" type="audio/mpeg" />
</item>
<item>
<title>We Are All in Trumps Hunger Games Now</title>
<description>The first contestant in Donald Trumps reality administration has left the West Wing. This week, Glenn Greenwald offers some provocative pushback on the Russia fear-mongering surrounding Gen. Michael Flynns resignation (or firing). Naomi Klein walks the dark aisles of the Trump family department store. Former Congresswoman Liz Holtzman, a key figure in the impeachment of Richard Nixon, explains how impeachment actually works. And Hina Shamsi of the ACLU recounts her interrogation at the border. Plus a performance from Jedi Mind Tricks.&amp;nbsp;</description>
<pubDate>Wed, 15 Feb 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>As General Flynn Falls, Glenn Greenwald Blasts the Bipartisan Hypocrisy and Naomi Klein Brands Trump</itunes:subtitle>
<itunes:summary>
<![CDATA[The first contestant in Donald Trumps reality administration has left the West Wing. This week, Glenn Greenwald offers some provocative pushback on the Russia fear-mongering surrounding Gen. Michael Flynns resignation (or firing). Naomi Klein walks the dark aisles of the Trump family department store. Former Congresswoman Liz Holtzman, a key figure in the impeachment of Richard Nixon, explains how impeachment actually works. And Hina Shamsi of the ACLU recounts her interrogation at the border. Plus a performance from Jedi Mind Tricks.&nbsp;]]>
</itunes:summary>
<itunes:duration>3858</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57a87fd0-dcf9-11e6-a5a2-af85a8453351]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL5616910839.mp3" length="58608326" type="audio/mpeg" />
</item>
<item>
<title>Trump's Cabinet of Killers and Why Orange is the New Anti-Black</title>
<description>This week, investigative reporter Allan Nairn breaks down Trump's relationship with the CIA and the killer assembly of neocons and right-wing conspiracists running the U.S. war machine. Princeton professor Keeanga Yamahtta-Taylor dismantles Obama's problematic legacy and offers strategic advice for resisting Trump. The Intercept's own distinguished alt-historian, Jon Schwarz, offers a lesson on the origins of presidential executive orders. And Kimya Dawson gives a raw performance of a new song about racism and the police state.</description>
<pubDate>Wed, 08 Feb 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Less than a month into the new administration, and not even a presidential bath robe can protect President Trump's orange from becoming the new anti-black. </itunes:subtitle>
<itunes:summary>
<![CDATA[This week, investigative reporter Allan Nairn breaks down Trump's relationship with the CIA and the killer assembly of neocons and right-wing conspiracists running the U.S. war machine. Princeton professor Keeanga Yamahtta-Taylor dismantles Obama's problematic legacy and offers strategic advice for resisting Trump. The Intercept's own distinguished alt-historian, Jon Schwarz, offers a lesson on the origins of presidential executive orders. And Kimya Dawson gives a raw performance of a new song about racism and the police state.]]>
</itunes:summary>
<itunes:duration>3752</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57a133b0-dcf9-11e6-a5a2-9f64a29807d9]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL7005027452.mp3" length="56920189" type="audio/mpeg" />
</item>
<item>
<title>Trump Week Two: The Rise of Chief Yookeroo</title>
<description>Donald Trump is signing executive orders like autographed pictures. This week on Intercepted: Two former senior FBI agents blast the “Muslim ban” and Trumps campaign to make torture great again. Constitutional rights lawyers dissect the (il)legalities of Trumps orders. Rep. Barbara Lee confronts the president's terrifying approach to government.&amp;nbsp; New secret documents reveal how Trump could resurrect J. Edgar Hoovers legacy. Brother Ali freestyles a verse, and Peter Sarsgaard stars in the bizarre true story of an NSA operative with vacation tips for deploying to Guantanamo.&amp;nbsp;</description>
<pubDate>Wed, 01 Feb 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>Donald Trump is signing executive orders like autographed pictures. But this isn't a reality show.</itunes:subtitle>
<itunes:summary>
<![CDATA[Donald Trump is signing executive orders like autographed pictures. This week on Intercepted: Two former senior FBI agents blast the “Muslim ban” and Trumps campaign to make torture great again. Constitutional rights lawyers dissect the (il)legalities of Trumps orders. Rep. Barbara Lee confronts the president's terrifying approach to government.&nbsp; New secret documents reveal how Trump could resurrect J. Edgar Hoovers legacy. Brother Ali freestyles a verse, and Peter Sarsgaard stars in the bizarre true story of an NSA operative with vacation tips for deploying to Guantanamo.&nbsp;]]>
</itunes:summary>
<itunes:duration>3358</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57996266-dcf9-11e6-a5a2-4ff22525cee4]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL4823102330.mp3?updated=1485937504" length="50611513" type="audio/mpeg" />
</item>
<item>
<title>The Clock Strikes Thirteen, Donald Trump is President</title>
<description>The clock struck thirteen on January 20, Donald Trump is the president of the United States and episode one of Intercepted is here. Intercept co-founder Glenn Greenwald and editor-in-chief Betsy Reed join Jeremy Scahill for a discussion on the crazy apocalyptic present. They break down Trumps attacks on the media, that insane speech he gave at the CIA and the state of the Democratic party. Naomi Klein sends in a dispatch from the Womens March on Washington. Jeremy goes deep into the secretive world of Seymour Hershs kitchen, and shoots the shit with the legendary Pulitzer Prize-winning journalist about why he calls Trump a “circuit breaker." And we hear a spoken word performance from hip-hop artist Immortal Technique.&amp;nbsp;</description>
<pubDate>Wed, 25 Jan 2017 11:00:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>The clock struck thirteen on January 20, Donald Trump is the president of the United States and Episode One of Intercepted is here.</itunes:subtitle>
<itunes:summary>
<![CDATA[The clock struck thirteen on January 20, Donald Trump is the president of the United States and episode one of Intercepted is here. Intercept co-founder Glenn Greenwald and editor-in-chief Betsy Reed join Jeremy Scahill for a discussion on the crazy apocalyptic present. They break down Trumps attacks on the media, that insane speech he gave at the CIA and the state of the Democratic party. Naomi Klein sends in a dispatch from the Womens March on Washington. Jeremy goes deep into the secretive world of Seymour Hershs kitchen, and shoots the shit with the legendary Pulitzer Prize-winning journalist about why he calls Trump a “circuit breaker." And we hear a spoken word performance from hip-hop artist Immortal Technique.&nbsp;]]>
</itunes:summary>
<itunes:duration>3433</itunes:duration>
<itunes:explicit>yes</itunes:explicit>
<guid isPermaLink="false"><![CDATA[57913302-dcf9-11e6-a5a2-87c6a559fb64]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL1844876464.mp3" length="51822341" type="audio/mpeg" />
</item>
<item>
<title>Introducing Intercepted with Jeremy Scahill</title>
<description>Hear a preview of Intercepted, a new podcast coming January 25 from the people behind the fearless, adversarial journalism of The Intercept. Every week, host Jeremy Scahill will discuss the crucial issues of our time with fellow reporters, and outspoken writers, artists and thinkers.</description>
<pubDate>Fri, 13 Jan 2017 18:38:00 -0000</pubDate>
<itunes:author>The Intercept / Panoply</itunes:author>
<itunes:episodeType>full</itunes:episodeType>
<itunes:subtitle>A preview of Intercepted, a new podcast coming January 25.</itunes:subtitle>
<itunes:summary>
<![CDATA[Hear a preview of Intercepted, a new podcast coming January 25 from the people behind the fearless, adversarial journalism of The Intercept. Every week, host Jeremy Scahill will discuss the crucial issues of our time with fellow reporters, and outspoken writers, artists and thinkers.]]>
</itunes:summary>
<itunes:duration>200</itunes:duration>
<itunes:explicit>no</itunes:explicit>
<guid isPermaLink="false"><![CDATA[e6dc75b4-d9b9-11e6-9bea-d73080315ad2]]></guid>
<enclosure url="https://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184" length="3202403" type="audio/mpeg" />
</item>
</channel>
</rss>
@@ -0,0 +1,73 @@
<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
<channel>
<title>Ελληνοφρένεια</title>
<link>https://ellinofreneia.sealabs.net/feed.rss</link>
<description>Ανεπίσημο feed της Ελληνοφρένειας</description>
<atom:link href="https://ellinofreneia.sealabs.net/feed.rss" rel="self"/>
<copyright>All rights reversed by http://ellinofreneianet.gr/</copyright>
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<image>
<url>https://ellinofreneia.sealabs.net/logo.png</url>
<title>Ελληνοφρένεια</title>
<link>https://ellinofreneia.sealabs.net/feed.rss</link>
</image>
<language>el</language>
<lastBuildDate>Tue, 27 Mar 2018 13:00:56 +0000</lastBuildDate>
<pubDate>Tue, 27 Mar 2018 13:00:56 +0000</pubDate>
<itunes:explicit>no</itunes:explicit>
<itunes:owner>
<itunes:name>Τζένη Μπώτση</itunes:name>
<itunes:email>tbotsi@example.com</itunes:email>
</itunes:owner>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 27ης Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="36087430" type="audio/mpeg"/>
<pubDate>Tue, 27 Mar 2018 11:11:02 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2257</itunes:duration>
</item>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 26ης Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="34974405" type="audio/mpeg"/>
<pubDate>Mon, 26 Mar 2018 11:16:17 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2188</itunes:duration>
</item>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 23ης Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="38491114" type="audio/mpeg"/>
<pubDate>Fri, 23 Mar 2018 12:11:08 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2408</itunes:duration>
</item>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 22ας Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="35607613" type="audio/mpeg"/>
<pubDate>Thu, 22 Mar 2018 12:31:20 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2227</itunes:duration>
</item>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 21ης Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="36459832" type="audio/mpeg"/>
<pubDate>Wed, 21 Mar 2018 12:10:59 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2281</itunes:duration>
</item>
<item>
<title>Η ρ_φ Ελληνοφρένεια της 20ης Μαρτίου 2018</title>
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="34507127" type="audio/mpeg"/>
<pubDate>Tue, 20 Mar 2018 12:11:06 +0000</pubDate>
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
<itunes:duration>2159</itunes:duration>
</item>
</channel>
</rss>

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