diff --git a/hammond-data/src/database.rs b/hammond-data/src/database.rs index 00454a8..d3bb36d 100644 --- a/hammond-data/src/database.rs +++ b/hammond-data/src/database.rs @@ -1,3 +1,5 @@ +//! Database Setup. This is only public to help with some unit tests. + use r2d2_diesel::ConnectionManager; use diesel::prelude::*; use r2d2; @@ -35,7 +37,7 @@ lazy_static! { static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db"); } -// FIXME: this should not be public +/// Get an r2d2 SqliteConnection. pub fn connection() -> Pool { POOL.clone() } diff --git a/hammond-data/src/lib.rs b/hammond-data/src/lib.rs index f9ed95d..cde29e9 100644 --- a/hammond-data/src/lib.rs +++ b/hammond-data/src/lib.rs @@ -56,8 +56,6 @@ pub mod utils; pub mod feed; #[allow(missing_docs)] pub mod errors; -// FIXME: this should not be public -#[allow(missing_docs)] pub mod database; pub(crate) mod models; mod parser; diff --git a/hammond-data/src/models/queryables.rs b/hammond-data/src/models/queryables.rs index 53f2e53..6779abc 100644 --- a/hammond-data/src/models/queryables.rs +++ b/hammond-data/src/models/queryables.rs @@ -645,6 +645,7 @@ impl<'a> Source { // TODO: Refactor into TryInto once it lands on stable. pub fn into_feed(mut self, ignore_etags: bool) -> Result { use reqwest::header::{EntityTag, Headers, HttpDate, IfModifiedSince, IfNoneMatch}; + use reqwest::StatusCode; let mut headers = Headers::new(); @@ -670,12 +671,26 @@ impl<'a> Source { self.update_etag(&req)?; // TODO match on more stuff - // 301: Permanent redirect of the url - // 302: Temporary redirect of the url + // 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 match req.status() { - reqwest::StatusCode::NotModified => bail!("304, skipping.."), + StatusCode::NotModified => bail!("304: skipping.."), + StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."), + // TODO: Change the source uri to the new one + StatusCode::MovedPermanently | StatusCode::PermanentRedirect => { + warn!("Feed was moved permanently.") + } + StatusCode::Unauthorized => bail!("401: Unauthorized."), + StatusCode::Forbidden => bail!("403: Forbidden."), + StatusCode::NotFound => bail!("404: Not found."), + StatusCode::RequestTimeout => bail!("408: Request Timeout."), + StatusCode::Gone => bail!("410: Feed was deleted."), _ => (), }; diff --git a/hammond-data/src/parser.rs b/hammond-data/src/parser.rs index 0bfa5e9..9f9299b 100644 --- a/hammond-data/src/parser.rs +++ b/hammond-data/src/parser.rs @@ -123,10 +123,70 @@ fn parse_itunes_duration(item: &Item) -> Option { mod tests { use std::fs::File; use std::io::BufReader; - use rss::Channel; + use rss; use super::*; + #[test] + fn test_itunes_duration() { + use rss::extension::itunes::ITunesItemExtensionBuilder; + + // Input is a String + let extension = ITunesItemExtensionBuilder::default() + .duration(Some("3370".into())) + .build() + .unwrap(); + let item = rss::ItemBuilder::default() + .itunes_ext(Some(extension)) + .build() + .unwrap(); + assert_eq!(parse_itunes_duration(&item), Some(3370)); + + // Input is a String + let extension = ITunesItemExtensionBuilder::default() + .duration(Some("6:10".into())) + .build() + .unwrap(); + let item = rss::ItemBuilder::default() + .itunes_ext(Some(extension)) + .build() + .unwrap(); + assert_eq!(parse_itunes_duration(&item), Some(370)); + + // Input is a String + let extension = ITunesItemExtensionBuilder::default() + .duration(Some("56:10".into())) + .build() + .unwrap(); + let item = rss::ItemBuilder::default() + .itunes_ext(Some(extension)) + .build() + .unwrap(); + assert_eq!(parse_itunes_duration(&item), Some(3370)); + + // Input is a String + let extension = ITunesItemExtensionBuilder::default() + .duration(Some("1:56:10".into())) + .build() + .unwrap(); + let item = rss::ItemBuilder::default() + .itunes_ext(Some(extension)) + .build() + .unwrap(); + assert_eq!(parse_itunes_duration(&item), Some(6970)); + + // Input is a String + let extension = ITunesItemExtensionBuilder::default() + .duration(Some("01:56:10".into())) + .build() + .unwrap(); + let item = rss::ItemBuilder::default() + .itunes_ext(Some(extension)) + .build() + .unwrap(); + assert_eq!(parse_itunes_duration(&item), Some(6970)); + } + #[test] fn test_new_podcast_intercepted() { let file = File::open("tests/feeds/Intercepted.xml").unwrap(); diff --git a/hammond-data/src/utils.rs b/hammond-data/src/utils.rs index fd0224b..108a6ac 100644 --- a/hammond-data/src/utils.rs +++ b/hammond-data/src/utils.rs @@ -8,11 +8,13 @@ use itertools::Itertools; use errors::*; use dbqueries; -use models::queryables::EpisodeCleanerQuery; +use models::queryables::{EpisodeCleanerQuery, Podcast}; +use xdg_dirs::DL_DIR; use std::path::Path; use std::fs; +/// Scan downloaded `episode` entries that might have broken `local_uri`s and set them to `None`. fn download_checker() -> Result<()> { let episodes = dbqueries::get_downloaded_episodes()?; @@ -30,6 +32,7 @@ fn download_checker() -> Result<()> { Ok(()) } +/// Delete watched `episodes` that have exceded their liftime after played. fn played_cleaner() -> Result<()> { let mut episodes = dbqueries::get_played_cleaner_episodes()?; @@ -54,7 +57,7 @@ fn played_cleaner() -> Result<()> { } /// Check `ep.local_uri` field and delete the file it points to. -pub fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<()> { +fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<()> { if ep.local_uri().is_some() { let uri = ep.local_uri().unwrap().to_owned(); if Path::new(&uri).exists() { @@ -119,6 +122,31 @@ pub fn replace_extra_spaces(s: &str) -> String { .collect::() } +/// Returns the URI of a Podcast Downloads given it's title. +pub fn get_download_folder(pd_title: &str) -> Result { + // It might be better to make it a hash of the title or the podcast 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: &Podcast) -> Result<()> { + 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)] mod tests { extern crate tempdir; @@ -277,4 +305,11 @@ mod tests { assert_eq!(replace_extra_spaces(&bad_txt), valid_txt); } + + #[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_); + } } diff --git a/hammond-downloader/src/downloader.rs b/hammond-downloader/src/downloader.rs index b985a26..ad29411 100644 --- a/hammond-downloader/src/downloader.rs +++ b/hammond-downloader/src/downloader.rs @@ -12,7 +12,7 @@ use std::sync::{Arc, Mutex}; use errors::*; use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery}; -use hammond_data::xdg_dirs::{DL_DIR, HAMMOND_CACHE}; +use hammond_data::xdg_dirs::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. @@ -129,15 +129,6 @@ fn save_io( Ok(()) } -pub fn get_download_folder(pd_title: &str) -> Result { - // 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 EpisodeWidgetQuery, @@ -228,15 +219,6 @@ mod tests { use hammond_data::dbqueries; use diesel::associations::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] // This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit // to run it. diff --git a/hammond-gtk/src/content.rs b/hammond-gtk/src/content.rs index 5d03ed3..69754a4 100644 --- a/hammond-gtk/src/content.rs +++ b/hammond-gtk/src/content.rs @@ -174,21 +174,30 @@ impl ShowStack { .unwrap(); debug!("Name: {:?}", WidgetExt::get_name(&old)); - let scrolled_window = old.get_children() - .first() - // This is guaranted to exist based on the show_widget.ui file. - .unwrap() - .clone() - .downcast::() - // This is guaranted based on the show_widget.ui file. - .unwrap(); - debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window)); - let new = ShowWidget::new(Arc::new(self.clone()), pd, self.sender.clone()); - // Copy the vertical scrollbar adjustment from the old view into the new one. - scrolled_window - .get_vadjustment() - .map(|x| new.set_vadjustment(&x)); + // Each composite ShowWidget is a gtkBox with the Podcast.id encoded in the gtk::Widget + // name. It's a hack since we can't yet subclass GObject easily. + let oldid = WidgetExt::get_name(&old); + let newid = WidgetExt::get_name(&new.container); + debug!("Old widget Name: {:?}\nNew widget Name: {:?}", oldid, newid); + + // Only copy the old scrollbar if both widget's represent the same podcast. + if newid == oldid { + let scrolled_window = old.get_children() + .first() + // This is guaranted to exist based on the show_widget.ui file. + .unwrap() + .clone() + .downcast::() + // This is guaranted based on the show_widget.ui file. + .unwrap(); + debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window)); + + // Copy the vertical scrollbar adjustment from the old view into the new one. + scrolled_window + .get_vadjustment() + .map(|x| new.set_vadjustment(&x)); + } self.stack.remove(&old); self.stack.add_named(&new.container, "widget"); diff --git a/hammond-gtk/src/manager.rs b/hammond-gtk/src/manager.rs index 49fd2f9..d482fbe 100644 --- a/hammond-gtk/src/manager.rs +++ b/hammond-gtk/src/manager.rs @@ -106,11 +106,10 @@ pub fn add(id: i32, directory: &str, sender: Sender) { #[cfg(test)] mod tests { use super::*; - use hammond_downloader::downloader; - use diesel::Identifiable; use hammond_data::database; + use hammond_data::utils::get_download_folder; use hammond_data::feed::*; use hammond_data::{Episode, Source}; use hammond_data::dbqueries; @@ -148,7 +147,7 @@ mod tests { let (sender, _rx) = channel(); - let download_fold = downloader::get_download_folder(&pd.title()).unwrap(); + let download_fold = get_download_folder(&pd.title()).unwrap(); add(episode.rowid(), download_fold.as_str(), sender); // Give it soem time to download the file diff --git a/hammond-gtk/src/utils.rs b/hammond-gtk/src/utils.rs index 2f354ea..3c624a3 100644 --- a/hammond-gtk/src/utils.rs +++ b/hammond-gtk/src/utils.rs @@ -7,7 +7,7 @@ use hammond_downloader::downloader; use std::thread; use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::collections::HashMap; use headerbar::Header; @@ -22,11 +22,9 @@ pub fn refresh_feed(headerbar: Arc
, source: Option>, sender: thread::spawn(move || { if let Some(s) = source { feed::index_loop(s); - } else { - if let Err(err) = feed::index_all() { - error!("Error While trying to update the database."); - error!("Error msg: {}", err); - } + } else if let Err(err) = feed::index_all() { + error!("Error While trying to update the database."); + error!("Error msg: {}", err); }; sender.send(Action::HeaderBarHideUpdateIndicator).unwrap(); @@ -35,8 +33,8 @@ pub fn refresh_feed(headerbar: Arc
, source: Option>, sender: } lazy_static! { - static ref CACHED_PIXBUFS: Mutex>>> = { - Mutex::new(HashMap::new()) + static ref CACHED_PIXBUFS: RwLock>>> = { + RwLock::new(HashMap::new()) }; } @@ -48,8 +46,8 @@ lazy_static! { // Also lazy_static requires Sync trait, so that's what the mutexes are. // TODO: maybe use something that would just scale to requested size? pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Option { - let mut hashmap = CACHED_PIXBUFS.lock().unwrap(); { + let hashmap = CACHED_PIXBUFS.read().unwrap(); let res = hashmap.get(&(pd.id(), size)); if let Some(px) = res { let m = px.lock().unwrap(); @@ -60,6 +58,7 @@ pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Option let img_path = downloader::cache_image(pd)?; let px = Pixbuf::new_from_file_at_scale(&img_path, size as i32, size as i32, true).ok(); if let Some(px) = px { + let mut hashmap = CACHED_PIXBUFS.write().unwrap(); hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone()))); return Some(px); } diff --git a/hammond-gtk/src/widgets/episode.rs b/hammond-gtk/src/widgets/episode.rs index 56ea735..b17c9ce 100644 --- a/hammond-gtk/src/widgets/episode.rs +++ b/hammond-gtk/src/widgets/episode.rs @@ -9,9 +9,8 @@ use humansize::{file_size_opts as size_opts, FileSize}; use hammond_data::dbqueries; use hammond_data::{EpisodeWidgetQuery, Podcast}; -// use hammond_data::utils::*; +use hammond_data::utils::get_download_folder; use hammond_data::errors::*; -use hammond_downloader::downloader; use app::Action; use manager; @@ -94,6 +93,10 @@ impl Default for EpisodeWidget { } } +lazy_static! { + static ref NOW: DateTime = Utc::now(); +} + impl EpisodeWidget { pub fn new(episode: &mut EpisodeWidgetQuery, sender: Sender) -> EpisodeWidget { let widget = EpisodeWidget::default(); @@ -166,18 +169,21 @@ impl EpisodeWidget { /// Set the date label depending on the current time. fn set_date(&self, epoch: i32) { - let now = Utc::now(); let date = Utc.timestamp(i64::from(epoch), 0); - if now.year() == date.year() { - self.date.set_text(&date.format("%e %b").to_string().trim()); + if NOW.year() == date.year() { + self.date.set_text(date.format("%e %b").to_string().trim()); } else { self.date - .set_text(&date.format("%e %b %Y").to_string().trim()); + .set_text(date.format("%e %b %Y").to_string().trim()); }; } /// Set the duration label. fn set_duration(&self, seconds: Option) { + if (seconds == Some(0)) || seconds.is_none() { + return; + }; + if let Some(secs) = seconds { self.duration.set_text(&format!("{} min", secs / 60)); self.duration.show(); @@ -241,7 +247,7 @@ impl EpisodeWidget { fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender) { let download_fold = dbqueries::get_podcast_from_id(ep.podcast_id()) .ok() - .map(|pd| downloader::get_download_folder(&pd.title().to_owned()).ok()) + .map(|pd| get_download_folder(&pd.title().to_owned()).ok()) .and_then(|x| x); // Start a new download. diff --git a/hammond-gtk/src/widgets/show.rs b/hammond-gtk/src/widgets/show.rs index 74d69a3..ca2e850 100644 --- a/hammond-gtk/src/widgets/show.rs +++ b/hammond-gtk/src/widgets/show.rs @@ -6,8 +6,7 @@ use dissolve; use hammond_data::dbqueries; use hammond_data::Podcast; -use hammond_data::utils::replace_extra_spaces; -use hammond_downloader::downloader; +use hammond_data::utils::{delete_show, replace_extra_spaces}; use widgets::episode::episodes_listbox; use utils::get_pixbuf_from_path; @@ -17,7 +16,6 @@ use app::Action; use std::sync::mpsc::Sender; use std::sync::Arc; use std::thread; -use std::fs; #[derive(Debug, Clone)] pub struct ShowWidget { @@ -123,17 +121,10 @@ fn on_unsub_button_clicked( unsub_button.hide(); // Spawn a thread so it won't block the ui. thread::spawn(clone!(pd => move || { - dbqueries::remove_feed(&pd).ok().map(|_| { - info!("{} was removed succesfully.", pd.title()); - - downloader::get_download_folder(pd.title()).ok().map(|fold| { - let res3 = fs::remove_dir_all(&fold); - // TODO: Show errors? - if res3.is_ok() { - info!("All the content at, {} was removed succesfully", &fold); - } - }); - }); + if let Err(err) = delete_show(&pd) { + error!("Something went wrong trying to remove {}", pd.title()); + error!("Error: {}", err); + } })); shows.switch_podcasts_animated(); sender.send(Action::HeaderBarNormal).unwrap();