diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0f9f325..6d0f13a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,13 +39,10 @@ stable:test: # Configure and run rustfmt on nightly # Exits and builds fails if on bad format rustfmt: - image: "rustlang/rust:nightly" + image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:nightly" stage: lint - variables: - CFG_RELEASE_CHANNEL: "nightly" script: - rustc --version && cargo --version - - cargo install rustfmt-nightly --force - cargo fmt --all -- --write-mode=diff # Configure and run clippy on nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf5e9b..cb7dd84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +* Ability to mark all episodes of a Show as watched. [#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47) +* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.[#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49) + ## [0.3.0] - 2018-02-11 * Tobias Bernard Redesigned the whole Gtk+ client. diff --git a/Cargo.lock b/Cargo.lock index 16fdb1e..965723b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,7 +707,10 @@ dependencies = [ "loggerv 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "take_mut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/hammond-data/src/database.rs b/hammond-data/src/database.rs index d6e5be0..8db7e6a 100644 --- a/hammond-data/src/database.rs +++ b/hammond-data/src/database.rs @@ -16,7 +16,7 @@ type Pool = r2d2::Pool>; embed_migrations!("migrations/"); -lazy_static!{ +lazy_static! { static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap()); } @@ -30,10 +30,7 @@ extern crate tempdir; #[cfg(test)] lazy_static! { - static ref TEMPDIR: tempdir::TempDir = { - tempdir::TempDir::new("hammond_unit_test").unwrap() - }; - + static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() }; static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db"); } diff --git a/hammond-data/src/dbqueries.rs b/hammond-data/src/dbqueries.rs index 27c46f7..04bdcdc 100644 --- a/hammond-data/src/dbqueries.rs +++ b/hammond-data/src/dbqueries.rs @@ -348,3 +348,29 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result { .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(); + index_single_source(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); + } +} diff --git a/hammond-data/src/models/new_episode.rs b/hammond-data/src/models/new_episode.rs index b0ce47c..ad1a12d 100644 --- a/hammond-data/src/models/new_episode.rs +++ b/hammond-data/src/models/new_episode.rs @@ -311,7 +311,6 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = { NewEpisodeMinimalBuilder::default() .title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America") @@ -325,13 +324,12 @@ mod tests { .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."; + 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") @@ -347,16 +345,15 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_INTERCEPTED_2: NewEpisode = { let descr = "This week on Intercepted: Jeremy gives an update on the aftermath of \ - Blackwater’s 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."; + Blackwater’s 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") @@ -372,7 +369,6 @@ mod tests { .build() .unwrap() }; - static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = { NewEpisodeBuilder::default() .title("The Super Bowl of Racism") @@ -388,7 +384,6 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = { NewEpisodeMinimalBuilder::default() .title("Hacking Devices with Kali Linux | LUP 214") @@ -402,7 +397,6 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = { NewEpisodeMinimalBuilder::default() .title("Gnome Does it Again | LUP 213") @@ -416,12 +410,11 @@ mod tests { .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 & the Beard just saved the show. \ - It’s a really packed episode!"; + 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. It’s a really packed episode!"; NewEpisodeBuilder::default() .title("Hacking Devices with Kali Linux | LUP 214") @@ -437,13 +430,13 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_LUP_2: NewEpisode = { - let descr = "The Gnome project is about to solve one of our audience's biggest Wayland’s \ - 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.\nPlus we chat with Wimpy \ - about the Ubuntu Rally in NYC, Microsoft’s sneaky move to turn Windows 10 into the \ - “ULTIMATE LINUX RUNTIME”, community news & more!"; + let descr = "The Gnome project is about to solve one of our audience's biggest \ + Wayland’s 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.\nPlus we chat with Wimpy about the Ubuntu Rally in NYC, \ + Microsoft’s sneaky move to turn Windows 10 into the “ULTIMATE LINUX \ + RUNTIME”, community news & more!"; NewEpisodeBuilder::default() .title("Gnome Does it Again | LUP 213") diff --git a/hammond-data/src/models/new_podcast.rs b/hammond-data/src/models/new_podcast.rs index ce2d1c6..22f8d51 100644 --- a/hammond-data/src/models/new_podcast.rs +++ b/hammond-data/src/models/new_podcast.rs @@ -162,13 +162,14 @@ mod tests { use std::io::BufReader; // Pre-built expected NewPodcast structs. - lazy_static!{ + lazy_static! { static ref EXPECTED_INTERCEPTED: NewPodcast = { let descr = "The people behind The Intercept’s 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."; + 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."; NewPodcastBuilder::default() .title("Intercepted with Jeremy Scahill") @@ -183,11 +184,10 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_LUP: NewPodcast = { - 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 descr = "An open show powered by community LINUX Unplugged takes the best \ + attributes of open collaboration and focuses them into a weekly \ + lifestyle show about Linux."; NewPodcastBuilder::default() .title("LINUX Unplugged Podcast") @@ -200,17 +200,16 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_TIPOFF: NewPodcast = { - let desc = "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 \ - we’ll 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. There’ll be car chases, slammed doors, terrorist cells, \ - meetings in dimly lit bars and cafes, wrangling with despotic regimes and \ - much more. So if you’re curious about the fun, complicated detective work \ - that goes into doing great investigative journalism- then this is the podcast \ - for you."; + let desc = "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 we’ll 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. There’ll be car chases, slammed \ + doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \ + with despotic regimes and much more. So if you’re curious about the fun, \ + complicated detective work that goes into doing great investigative \ + journalism- then this is the podcast for you."; NewPodcastBuilder::default() .title("The Tip Off") @@ -223,15 +222,13 @@ mod tests { .source_id(42) .build() .unwrap() - }; - static ref EXPECTED_STARS: NewPodcast = { - let descr = "

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.

"; + let descr = "

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.

"; let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\ b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\ 923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"; @@ -245,11 +242,10 @@ mod tests { .build() .unwrap() }; - static ref EXPECTED_CODE: NewPodcast = { - 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."; + let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \ + David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \ + Livingston-Gray. Brought to you by @therubyrep."; NewPodcastBuilder::default() .title("Greater Than Code") @@ -262,7 +258,6 @@ mod tests { .build() .unwrap() }; - static ref UPDATED_DESC_INTERCEPTED: NewPodcast = { NewPodcastBuilder::default() .title("Intercepted with Jeremy Scahill") diff --git a/hammond-data/src/utils.rs b/hammond-data/src/utils.rs index b19e76c..29138f1 100644 --- a/hammond-data/src/utils.rs +++ b/hammond-data/src/utils.rs @@ -318,6 +318,8 @@ mod tests { } #[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_); diff --git a/hammond-gtk/Cargo.toml b/hammond-gtk/Cargo.toml index 0c8632f..d208950 100644 --- a/hammond-gtk/Cargo.toml +++ b/hammond-gtk/Cargo.toml @@ -23,6 +23,9 @@ url = "1.6.0" failure = "0.1.1" failure_derive = "0.1.1" take_mut = "0.2.0" +regex = "0.2.6" +reqwest = "0.8.5" +serde_json = "1.0" [dependencies.gtk] features = ["v3_22"] diff --git a/hammond-gtk/resources/gtk/inapp_notif.ui b/hammond-gtk/resources/gtk/inapp_notif.ui new file mode 100644 index 0000000..14761b6 --- /dev/null +++ b/hammond-gtk/resources/gtk/inapp_notif.ui @@ -0,0 +1,101 @@ + + + + + + True + False + center + start + + + True + False + center + center + 0 + none + + + True + False + center + center + 6 + 6 + 6 + + + True + False + center + center + An in-app action notification + + + False + False + 0 + + + + + True + True + False + True + center + center + none + + + True + False + window-close-symbolic + + + + + + False + False + end + 1 + + + + + Undo + True + True + True + center + center + + + + False + False + end + 2 + + + + + + + + + + + + + diff --git a/hammond-gtk/resources/gtk/show_widget.ui b/hammond-gtk/resources/gtk/show_widget.ui index fca068c..586f85e 100644 --- a/hammond-gtk/resources/gtk/show_widget.ui +++ b/hammond-gtk/resources/gtk/show_widget.ui @@ -207,7 +207,7 @@ Tobias Bernard False False - 0 + 1 @@ -227,7 +227,7 @@ Tobias Bernard False False - 1 + 2 @@ -266,4 +266,33 @@ Tobias Bernard + + False + bottom + + + True + False + 6 + 6 + 6 + 6 + vertical + + + True + True + True + Mark all episodes as listened + True + + + False + True + 0 + + + + + diff --git a/hammond-gtk/resources/resources.xml b/hammond-gtk/resources/resources.xml index 366f71c..51b44cf 100644 --- a/hammond-gtk/resources/resources.xml +++ b/hammond-gtk/resources/resources.xml @@ -9,6 +9,7 @@ gtk/shows_view.ui gtk/shows_child.ui gtk/headerbar.ui + gtk/inapp_notif.ui gtk/style.css diff --git a/hammond-gtk/src/app.rs b/hammond-gtk/src/app.rs index 8027d4b..4fbdfd0 100644 --- a/hammond-gtk/src/app.rs +++ b/hammond-gtk/src/app.rs @@ -8,9 +8,11 @@ use gtk::prelude::*; use hammond_data::{Podcast, Source}; use hammond_data::utils::checkup; +use appnotif::*; use headerbar::Header; use stacks::Content; use utils; +use widgets::mark_all_watched; use std::sync::Arc; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -33,12 +35,14 @@ pub enum Action { HeaderBarNormal, HeaderBarShowUpdateIndicator, HeaderBarHideUpdateIndicator, + MarkAllPlayerNotification(Arc), } #[derive(Debug)] pub struct App { app_instance: gtk::Application, window: gtk::Window, + overlay: gtk::Overlay, header: Arc
, content: Arc, receiver: Receiver, @@ -73,12 +77,17 @@ impl App { // Create the headerbar let header = Arc::new(Header::new(&content, &window, sender.clone())); - // Add the content main stack to the window. - window.add(&content.get_stack()); + // Add the content main stack to the overlay. + let overlay = gtk::Overlay::new(); + overlay.add(&content.get_stack()); + + // Add the overlay to the main window + window.add(&overlay); App { app_instance: application, window, + overlay, header, content, receiver, @@ -86,7 +95,7 @@ impl App { } } - pub fn setup_timed_callbacks(&self) { + fn setup_timed_callbacks(&self) { let sender = self.sender.clone(); // Update the feeds right after the Application is initialized. gtk::timeout_add_seconds(2, move || { @@ -114,15 +123,15 @@ impl App { pub fn run(self) { let window = self.window.clone(); - let app = self.app_instance.clone(); - self.app_instance.connect_startup(move |_| { - build_ui(&window, &app); + self.app_instance.connect_startup(move |app| { + build_ui(&window, app); }); self.setup_timed_callbacks(); let content = self.content.clone(); let headerbar = self.header.clone(); let sender = self.sender.clone(); + let overlay = self.overlay.clone(); let receiver = self.receiver; gtk::idle_add(move || { match receiver.recv_timeout(Duration::from_millis(10)) { @@ -152,6 +161,21 @@ impl App { Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(), Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(), Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(), + Ok(Action::MarkAllPlayerNotification(pd)) => { + let callback = clone!(sender => move || { + if let Err(err) = mark_all_watched(&pd, sender.clone()) { + error!("Something went horribly wrong with the notif callback: {}", err); + } + glib::Continue(false) + }); + + let text = "Marked all episodes as listened"; + let notif = InAppNotification::new(text.into(), callback, sender.clone()); + overlay.add_overlay(¬if.revealer); + // We need to display the notification after the widget is added to the overlay + // so there will be a nice animation. + notif.show(); + } Err(_) => (), } diff --git a/hammond-gtk/src/appnotif.rs b/hammond-gtk/src/appnotif.rs new file mode 100644 index 0000000..07291a7 --- /dev/null +++ b/hammond-gtk/src/appnotif.rs @@ -0,0 +1,87 @@ +use glib; +use gtk; +use gtk::prelude::*; + +use app::Action; + +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::mpsc::Sender; + +#[derive(Debug, Clone)] +pub struct InAppNotification { + pub revealer: gtk::Revealer, + text: gtk::Label, + undo: gtk::Button, + close: gtk::Button, +} + +impl Default for InAppNotification { + fn default() -> Self { + let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/inapp_notif.ui"); + + let revealer: gtk::Revealer = builder.get_object("revealer").unwrap(); + let text: gtk::Label = builder.get_object("text").unwrap(); + let undo: gtk::Button = builder.get_object("undo").unwrap(); + let close: gtk::Button = builder.get_object("close").unwrap(); + + InAppNotification { + revealer, + text, + undo, + close, + } + } +} + +impl InAppNotification { + pub fn new(text: String, mut callback: F, sender: Sender) -> Self + where + F: FnMut() -> glib::Continue + 'static, + { + let notif = InAppNotification::default(); + notif.text.set_text(&text); + + let revealer = notif.revealer.clone(); + let id = timeout_add_seconds(6, move || { + revealer.set_reveal_child(false); + callback() + }); + let id = Rc::new(RefCell::new(Some(id))); + + // Cancel the callback + let revealer = notif.revealer.clone(); + notif.undo.connect_clicked(move |_| { + let foo = id.borrow_mut().take(); + if let Some(id) = foo { + glib::source::source_remove(id); + } + + // Hide the notification + revealer.set_reveal_child(false); + // Refresh the widget if visible + if let Err(err) = sender.send(Action::RefreshWidgetIfVis) { + error!( + "Something went horribly wrong with the Action channel: {}", + err + ) + } + }); + + // Hide the revealer when the close button is clicked + let revealer = notif.revealer.clone(); + notif.close.connect_clicked(move |_| { + revealer.set_reveal_child(false); + }); + + notif + } + + // This is a seperate method cause in order to get a nice animation + // the revealer should be attached to something that will display it. + // Previouslyi we where doing it in the constructor, which had the result + // of the animation being skipped cause there was no parent widget to display it. + pub fn show(&self) { + self.revealer.set_reveal_child(true); + } +} diff --git a/hammond-gtk/src/headerbar.rs b/hammond-gtk/src/headerbar.rs index 15c6b83..3b5bd50 100644 --- a/hammond-gtk/src/headerbar.rs +++ b/hammond-gtk/src/headerbar.rs @@ -12,6 +12,7 @@ use std::sync::mpsc::Sender; use app::Action; use stacks::Content; +use utils::itunes_to_rss; #[derive(Debug, Clone)] pub struct Header { @@ -154,8 +155,18 @@ impl Header { } } +// FIXME: THIS ALSO SUCKS! fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender) -> Result<(), Error> { let url = entry.get_text().unwrap_or_default(); + let url = if url.contains("itunes.com") || url.contains("apple.com") { + info!("Detected itunes url."); + let foo = itunes_to_rss(&url)?; + info!("Resolved to {}", foo); + foo + } else { + url.to_owned() + }; + let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?; entry.set_text(""); @@ -165,6 +176,7 @@ fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender) -> Result<(), Ok(()) } +// FIXME: THIS SUCKS! fn on_url_change( entry: >k::Entry, result: >k::Label, diff --git a/hammond-gtk/src/main.rs b/hammond-gtk/src/main.rs index 55d4bfb..79baa80 100644 --- a/hammond-gtk/src/main.rs +++ b/hammond-gtk/src/main.rs @@ -26,8 +26,11 @@ extern crate hammond_downloader; extern crate humansize; extern crate loggerv; extern crate open; +extern crate regex; +extern crate reqwest; extern crate send_cell; extern crate take_mut; +extern crate serde_json; extern crate url; // extern crate rayon; @@ -67,6 +70,7 @@ pub mod app; pub mod utils; pub mod manager; pub mod static_resource; +pub mod appnotif; use app::App; diff --git a/hammond-gtk/src/manager.rs b/hammond-gtk/src/manager.rs index a9d70fe..c4ead04 100644 --- a/hammond-gtk/src/manager.rs +++ b/hammond-gtk/src/manager.rs @@ -73,9 +73,8 @@ impl DownloadProgress for Progress { } lazy_static! { - pub static ref ACTIVE_DOWNLOADS: Arc>>>> = { - Arc::new(RwLock::new(HashMap::new())) - }; + pub static ref ACTIVE_DOWNLOADS: Arc>>>> = + { Arc::new(RwLock::new(HashMap::new())) }; } pub fn add(id: i32, directory: &str, sender: Sender) -> Result<(), Error> { @@ -175,6 +174,8 @@ mod tests { } #[test] + // This test needs access to local system so we ignore it by default. + #[ignore] fn test_dl_steal_the_stars() { let url = "https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars"; diff --git a/hammond-gtk/src/utils.rs b/hammond-gtk/src/utils.rs index f2b0876..5d94e7a 100644 --- a/hammond-gtk/src/utils.rs +++ b/hammond-gtk/src/utils.rs @@ -2,7 +2,10 @@ use failure::Error; use gdk_pixbuf::Pixbuf; +use regex::Regex; +use reqwest; use send_cell::SendCell; +use serde_json::Value; // use hammond_data::feed; use hammond_data::{PodcastCoverQuery, Source}; @@ -74,9 +77,8 @@ fn refresh_feed(source: Option>, sender: Sender) -> Result<( } lazy_static! { - static ref CACHED_PIXBUFS: RwLock>>> = { - RwLock::new(HashMap::new()) - }; + static ref CACHED_PIXBUFS: RwLock>>> = + { RwLock::new(HashMap::new()) }; } // Since gdk_pixbuf::Pixbuf is refference counted and every episode, @@ -107,6 +109,35 @@ pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Result Result` +pub fn itunes_to_rss(url: &str) -> Result { + let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an Itunes ID."))?; + lookup_id(id) +} + +#[inline] +fn itunes_id_from_url(url: &str) -> Option { + lazy_static! { + static ref RE: Regex = Regex::new(r"/id([0-9]+)").unwrap(); + } + + // Get the itunes id from the url + let foo = RE.captures_iter(url).nth(0)?.get(1)?.as_str(); + // Parse it to a u32, this *should* never fail + foo.parse::().ok() +} + +#[inline] +fn lookup_id(id: u32) -> Result { + let url = format!("https://itunes.apple.com/lookup?id={}&entity=podcast", id); + let req: Value = reqwest::get(&url)?.json()?; + // FIXME: First time using serde, this could be done better and avoid using [] for indexing. + let feedurl = req["results"][0]["feedUrl"].as_str(); + let feedurl = feedurl.ok_or_else(|| format_err!("Failed to get url from itunes response"))?; + Ok(feedurl.into()) +} + #[cfg(test)] mod tests { use super::*; @@ -130,4 +161,25 @@ mod tests { let pxbuf = get_pixbuf_from_path(&pd.into(), 256); assert!(pxbuf.is_ok()); } + + #[test] + fn test_itunes_to_rss() { + let itunes_url = "https://itunes.apple.com/podcast/id1195206601"; + let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill"); + assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap()); + } + + #[test] + fn test_itunes_id() { + let id = 1195206601; + let itunes_url = "https://itunes.apple.com/podcast/id1195206601"; + assert_eq!(id, itunes_id_from_url(itunes_url).unwrap()); + } + + #[test] + fn test_itunes_lookup_id() { + let id = 1195206601; + let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill"; + assert_eq!(rss_url, lookup_id(id).unwrap()); + } } diff --git a/hammond-gtk/src/widgets/mod.rs b/hammond-gtk/src/widgets/mod.rs index 476deef..46f759c 100644 --- a/hammond-gtk/src/widgets/mod.rs +++ b/hammond-gtk/src/widgets/mod.rs @@ -4,3 +4,4 @@ mod episode_states; pub use self::episode::EpisodeWidget; pub use self::show::ShowWidget; +pub use self::show::mark_all_watched; diff --git a/hammond-gtk/src/widgets/show.rs b/hammond-gtk/src/widgets/show.rs index 0a5808d..abe9a37 100644 --- a/hammond-gtk/src/widgets/show.rs +++ b/hammond-gtk/src/widgets/show.rs @@ -1,5 +1,6 @@ use dissolve; use failure::Error; +// use glib; use gtk; use gtk::prelude::*; use open; @@ -62,6 +63,8 @@ impl ShowWidget { } pub fn init(&self, pd: Arc, sender: Sender) { + let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui"); + // Hacky workaround so the pd.id() can be retrieved from the `ShowStack`. WidgetExt::set_name(&self.container, &pd.id().to_string()); @@ -88,6 +91,19 @@ impl ShowWidget { error!("Error: {}", err); } }); + + let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap(); + let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap(); + + let episodes = self.episodes.clone(); + mark_all.connect_clicked(clone!(pd, sender => move |_| { + on_played_button_clicked( + pd.clone(), + &episodes, + sender.clone() + ) + })); + self.settings.set_popover(&show_menu); } /// Populate the listbox with the shows episodes. @@ -142,9 +158,47 @@ fn on_unsub_button_clicked( Ok(()) } -#[allow(dead_code)] -fn on_played_button_clicked(pd: &Podcast, sender: Sender) -> Result<(), Error> { +fn on_played_button_clicked(pd: Arc, episodes: >k::Frame, sender: Sender) { + if dim_titles(episodes).is_none() { + error!("Something went horribly wrong when dimming the titles."); + warn!("RUN WHILE YOU STILL CAN!"); + } + + sender.send(Action::MarkAllPlayerNotification(pd)).unwrap(); +} + +pub fn mark_all_watched(pd: &Podcast, sender: Sender) -> Result<(), Error> { dbqueries::update_none_to_played_now(pd)?; - sender.send(Action::RefreshWidget)?; + sender.send(Action::RefreshWidgetIfVis)?; + sender.send(Action::RefreshEpisodesView)?; Ok(()) } + +// Ideally if we had a custom widget this would have been as simple as: +// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }` +// But now I can't think of a better way to do it than hardcoding the title +// position relative to the EpisodeWidget container gtk::Box. +fn dim_titles(episodes: >k::Frame) -> Option<()> { + let listbox = episodes + .get_children() + .remove(0) + .downcast::() + .ok()?; + let children = listbox.get_children(); + + for row in children { + let row = row.downcast::().ok()?; + let container = row.get_children().remove(0).downcast::().ok()?; + let foo = container + .get_children() + .remove(0) + .downcast::() + .ok()?; + let bar = foo.get_children().remove(0).downcast::().ok()?; + let baz = bar.get_children().remove(0).downcast::().ok()?; + let title = baz.get_children().remove(0).downcast::().ok()?; + + title.get_style_context().map(|c| c.add_class("dim-label")); + } + Some(()) +} diff --git a/org.gnome.Hammond.json b/org.gnome.Hammond.json index a27cc2c..86a21b9 100644 --- a/org.gnome.Hammond.json +++ b/org.gnome.Hammond.json @@ -12,6 +12,10 @@ ], "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",