diff --git a/Cargo.lock b/Cargo.lock index 3995b46..ba57fd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,6 +508,11 @@ dependencies = [ "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "gobject-sys" version = "0.5.0" @@ -591,6 +596,7 @@ version = "0.1.0" dependencies = [ "diesel 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "hammond-data 0.1.0", "hyper 0.11.10 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1782,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a303bbf7a5e75ab3b627117ff10e495d1b9e97e1d68966285ac2b1f6270091bc" "checksum glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "450247060df7d52fdad31e1d66f30d967e925c9d1d26a0ae050cfe33dcd00d08" "checksum glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9693049613ff52b93013cc3d2590366d8e530366d288438724b73f6c7dc4be8" +"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60d507c87a71b1143c66ed21a969be9b99a76df234b342d733e787e6c9c7d7c2" "checksum gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0847c507e52c1feaede13ef56fb4847742438602655449d5f1f782e8633f146f" "checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f" diff --git a/hammond-data/src/database.rs b/hammond-data/src/database.rs index bb18bda..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,8 @@ lazy_static! { static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db"); } -pub(crate) fn connection() -> Pool { +/// 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 21b70ac..cde29e9 100644 --- a/hammond-data/src/lib.rs +++ b/hammond-data/src/lib.rs @@ -56,7 +56,7 @@ pub mod utils; pub mod feed; #[allow(missing_docs)] pub mod errors; -pub(crate) mod database; +pub mod database; pub(crate) mod models; mod parser; mod schema; diff --git a/hammond-data/src/models/queryables.rs b/hammond-data/src/models/queryables.rs index 35b342e..6779abc 100644 --- a/hammond-data/src/models/queryables.rs +++ b/hammond-data/src/models/queryables.rs @@ -215,6 +215,22 @@ pub struct EpisodeWidgetQuery { podcast_id: i32, } +impl From for EpisodeWidgetQuery { + fn from(e: Episode) -> EpisodeWidgetQuery { + EpisodeWidgetQuery { + 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, + podcast_id: e.podcast_id, + } + } +} + impl EpisodeWidgetQuery { /// Get the value of the sqlite's `ROW_ID` pub fn rowid(&self) -> i32 { @@ -597,7 +613,6 @@ impl<'a> Source { fn update_etag(&mut self, req: &reqwest::Response) -> Result<()> { let headers = req.headers(); - // let etag = headers.get_raw("ETag").unwrap(); let etag = headers.get::(); let lmod = headers.get::(); diff --git a/hammond-data/src/utils.rs b/hammond-data/src/utils.rs index 1e7f4c2..108a6ac 100644 --- a/hammond-data/src/utils.rs +++ b/hammond-data/src/utils.rs @@ -137,21 +137,14 @@ pub fn get_download_folder(pd_title: &str) -> Result { /// Removes all the entries associated with the given show from the database, /// and deletes all of the downloaded content. /// TODO: Write Tests -/// TODO: Return Result instead -pub fn delete_show(pd: &Podcast) { - let res = dbqueries::remove_feed(pd); - if res.is_ok() { - info!("{} was removed succesfully.", pd.title()); +pub fn delete_show(pd: &Podcast) -> Result<()> { + dbqueries::remove_feed(&pd)?; + info!("{} was removed succesfully.", pd.title()); - let dl_fold = get_download_folder(pd.title()); - if let Ok(fold) = dl_fold { - let res3 = fs::remove_dir_all(&fold); - // TODO: Show errors? - if res3.is_ok() { - info!("All the content at, {} was removed succesfully", &fold); - } - }; - } + let fold = get_download_folder(pd.title())?; + fs::remove_dir_all(&fold)?; + info!("All the content at, {} was removed succesfully", &fold); + Ok(()) } #[cfg(test)] diff --git a/hammond-downloader/Cargo.toml b/hammond-downloader/Cargo.toml index a12cf91..f541ba8 100644 --- a/hammond-downloader/Cargo.toml +++ b/hammond-downloader/Cargo.toml @@ -11,6 +11,7 @@ log = "0.3.8" mime_guess = "1.8.3" reqwest = "0.8.2" tempdir = "0.3.5" +glob = "0.2.11" [dependencies.diesel] features = ["sqlite"] diff --git a/hammond-downloader/src/downloader.rs b/hammond-downloader/src/downloader.rs index 83521e7..180e586 100644 --- a/hammond-downloader/src/downloader.rs +++ b/hammond-downloader/src/downloader.rs @@ -2,11 +2,13 @@ use reqwest; use hyper::header::*; use tempdir::TempDir; use mime_guess; +use glob::glob; use std::fs::{rename, DirBuilder, File}; use std::io::{BufWriter, Read, Write}; use std::path::Path; use std::fs; +use std::sync::{Arc, Mutex}; use errors::*; use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery}; @@ -15,6 +17,11 @@ 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. +pub trait DownloadProgress { + fn set_downloaded(&mut self, downloaded: u64); + fn set_size(&mut self, bytes: u64); +} + // 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. @@ -22,7 +29,12 @@ use hammond_data::xdg_dirs::HAMMOND_CACHE; // 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 { +fn download_into( + dir: &str, + file_title: &str, + url: &str, + progress: Option>>, +) -> Result { info!("GET request to: {}", url); let client = reqwest::Client::builder().referer(false).build()?; let mut resp = client.get(url).send()?; @@ -33,22 +45,28 @@ fn download_into(dir: &str, file_title: &str, url: &str) -> Result { } let headers = resp.headers().clone(); - let ct_len = headers.get::().map(|ct_len| **ct_len); let ct_type = headers.get::(); ct_len.map(|x| info!("File Lenght: {}", x)); ct_type.map(|x| info!("Content Type: {}", x)); - let ext = get_ext(ct_type.cloned()).unwrap_or(String::from("unkown")); + let ext = get_ext(ct_type.cloned()).unwrap_or(String::from("unknown")); info!("Extension: {}", ext); // Construct a temp file to save desired content. - let tempdir = TempDir::new_in(dir, "")?; - + // It has to be a `new_in` instead of new cause rename can't move cross filesystems. + let tempdir = TempDir::new_in(HAMMOND_CACHE.to_str().unwrap(), "temp_download")?; let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),); + ct_len.map(|x| { + if let Some(p) = progress.clone() { + let mut m = p.lock().unwrap(); + m.set_size(x); + } + }); + // Save requested content into the file. - save_io(&out_file, &mut resp, ct_len)?; + save_io(&out_file, &mut resp, ct_len, progress)?; // Construct the desired path. let target = format!("{}/{}.{}", dir, file_title, ext); @@ -58,7 +76,7 @@ fn download_into(dir: &str, file_title: &str, url: &str) -> Result { Ok(target) } -// Determine the file extension from the http content-type header. +/// Determine the file extension from the http content-type header. fn get_ext(content: Option) -> Option { let cont = content.clone()?; content @@ -73,8 +91,14 @@ fn get_ext(content: Option) -> Option { } // TODO: Write unit-tests. +// TODO: Refactor... Somehow. /// 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) -> Result<()> { +fn save_io( + file: &str, + resp: &mut reqwest::Response, + content_lenght: Option, + progress: Option>>, +) -> Result<()> { info!("Downloading into: {}", file); let chunk_size = match content_lenght { Some(x) => x as usize / 99, @@ -89,6 +113,14 @@ fn save_io(file: &str, resp: &mut reqwest::Response, content_lenght: Option buffer.truncate(bcount); if !buffer.is_empty() { writer.write_all(buffer.as_slice())?; + if let Some(prog) = progress.clone() { + // This sucks. + let len = writer.get_ref().metadata().map(|x| x.len()); + if let Ok(l) = len { + let mut m = prog.lock().unwrap(); + m.set_downloaded(l); + } + } } else { break; } @@ -98,7 +130,11 @@ fn save_io(file: &str, resp: &mut reqwest::Response, content_lenght: Option } // TODO: Refactor -pub fn get_episode(ep: &mut EpisodeWidgetQuery, download_folder: &str) -> Result<()> { +pub fn get_episode( + ep: &mut EpisodeWidgetQuery, + download_folder: &str, + progress: Option>>, +) -> Result<()> { // Check if its alrdy downloaded if ep.local_uri().is_some() { if Path::new(ep.local_uri().unwrap()).exists() { @@ -110,7 +146,12 @@ pub fn get_episode(ep: &mut EpisodeWidgetQuery, download_folder: &str) -> Result ep.save()?; }; - let res = download_into(download_folder, ep.title(), ep.uri().unwrap()); + let res = download_into( + download_folder, + &ep.rowid().to_string(), + ep.uri().unwrap(), + progress, + ); if let Ok(path) = res { // If download succedes set episode local_uri to dlpath. @@ -136,42 +177,33 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Option { return None; } - let download_fold = format!( - "{}{}", - HAMMOND_CACHE.to_str().unwrap(), - pd.title().to_owned() - ); + let cache_download_fold = format!("{}{}", HAMMOND_CACHE.to_str()?, 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); + // Weird glob magic. + if let Ok(mut foo) = glob(&format!("{}/cover.*", cache_download_fold)) { + // For some reason there is no .first() method so nth(0) is used + let path = foo.nth(0).and_then(|x| x.ok()); + if let Some(p) = path { + return Some(p.to_str()?.into()); + } }; + // Create the folders if they don't exist. DirBuilder::new() .recursive(true) - .create(&download_fold) - .unwrap(); + .create(&cache_download_fold) + .ok()?; - 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 + match download_into(&cache_download_fold, "cover", &url, None) { + Ok(path) => { + info!("Cached img into: {}", &path); + Some(path) + } + Err(err) => { + error!("Failed to get feed image."); + error!("Error: {}", err); + None + } } } diff --git a/hammond-downloader/src/lib.rs b/hammond-downloader/src/lib.rs index 98f6678..9e57db8 100644 --- a/hammond-downloader/src/lib.rs +++ b/hammond-downloader/src/lib.rs @@ -3,6 +3,7 @@ extern crate diesel; #[macro_use] extern crate error_chain; +extern crate glob; extern crate hammond_data; extern crate hyper; #[macro_use] diff --git a/hammond-gtk/resources/gtk/episode_widget.ui b/hammond-gtk/resources/gtk/episode_widget.ui index 7d06799..64a9ad1 100644 --- a/hammond-gtk/resources/gtk/episode_widget.ui +++ b/hammond-gtk/resources/gtk/episode_widget.ui @@ -120,10 +120,10 @@ - + False True - 42 MB + 0 MB True False + + + False + True + 6 + + False @@ -257,6 +274,7 @@ False True + 0 False diff --git a/hammond-gtk/src/app.rs b/hammond-gtk/src/app.rs index 8db8445..53ca4e2 100644 --- a/hammond-gtk/src/app.rs +++ b/hammond-gtk/src/app.rs @@ -11,14 +11,19 @@ use headerbar::Header; use content::Content; use utils; -use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; +use std::sync::mpsc::{channel, Receiver, Sender}; #[derive(Clone, Debug)] pub enum Action { UpdateSources(Option), - RefreshViews, + RefreshAllViews, + RefreshEpisodesView, RefreshEpisodesViewBGR, + RefreshShowsView, + RefreshWidget, + RefreshWidgetIfVis, + RefreshWidgetIfSame(i32), HeaderBarShowTile(String), HeaderBarNormal, HeaderBarHideUpdateIndicator, @@ -134,12 +139,17 @@ impl App { utils::refresh_feed(headerbar.clone(), Some(vec![s]), sender.clone()) } } - Ok(Action::RefreshViews) => content.update(), + Ok(Action::RefreshAllViews) => content.update(), + Ok(Action::RefreshShowsView) => content.update_shows_view(), + Ok(Action::RefreshWidget) => content.update_widget(), + Ok(Action::RefreshWidgetIfVis) => content.update_widget_if_visible(), + Ok(Action::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id), + Ok(Action::RefreshEpisodesView) => content.update_episode_view(), Ok(Action::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(), Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title), Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(), Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(), - _ => (), + Err(_) => (), } Continue(true) diff --git a/hammond-gtk/src/content.rs b/hammond-gtk/src/content.rs index 9a3861d..69754a4 100644 --- a/hammond-gtk/src/content.rs +++ b/hammond-gtk/src/content.rs @@ -41,8 +41,9 @@ impl Content { } pub fn update(&self) { - self.update_shows_view(); self.update_episode_view(); + self.update_shows_view(); + self.update_widget() } pub fn update_episode_view(&self) { @@ -56,7 +57,23 @@ impl Content { } pub fn update_shows_view(&self) { - self.shows.update(); + self.shows.update_podcasts(); + } + + pub fn update_widget(&self) { + self.shows.update_widget(); + } + + pub fn update_widget_if_same(&self, pid: i32) { + self.shows.update_widget_if_same(pid); + } + + pub fn update_widget_if_visible(&self) { + if self.stack.get_visible_child_name() == Some("shows".to_string()) + && self.shows.get_stack().get_visible_child_name() == Some("widget".to_string()) + { + self.shows.update_widget(); + } } pub fn get_stack(&self) -> gtk::Stack { @@ -100,15 +117,11 @@ impl ShowStack { show } - // fn is_empty(&self) -> bool { - // self.podcasts.is_empty() + // pub fn update(&self) { + // self.update_widget(); + // self.update_podcasts(); // } - pub fn update(&self) { - self.update_podcasts(); - self.update_widget(); - } - pub fn update_podcasts(&self) { let vis = self.stack.get_visible_child_name().unwrap(); @@ -194,12 +207,12 @@ impl ShowStack { let vis = self.stack.get_visible_child_name().unwrap(); let old = self.stack.get_child_by_name("widget").unwrap(); - let id = WidgetExt::get_name(&old).unwrap(); - if id == "GtkBox" { + let id = WidgetExt::get_name(&old); + if id == Some("GtkBox".to_string()) || id.is_none() { return; } - let pd = dbqueries::get_podcast_from_id(id.parse::().unwrap()); + let pd = dbqueries::get_podcast_from_id(id.unwrap().parse::().unwrap()); if let Ok(pd) = pd { self.replace_widget(&pd); self.stack.set_visible_child_name(&vis); @@ -207,6 +220,17 @@ impl ShowStack { } } + // Only update widget if it's podcast_id is equal to pid. + pub fn update_widget_if_same(&self, pid: i32) { + let old = self.stack.get_child_by_name("widget").unwrap(); + + let id = WidgetExt::get_name(&old); + if id != Some(pid.to_string()) || id.is_none() { + return; + } + self.update_widget(); + } + pub fn switch_podcasts_animated(&self) { self.stack .set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight); diff --git a/hammond-gtk/src/main.rs b/hammond-gtk/src/main.rs index 622cd10..9dead41 100644 --- a/hammond-gtk/src/main.rs +++ b/hammond-gtk/src/main.rs @@ -53,6 +53,7 @@ mod content; mod app; mod utils; +mod manager; mod static_resource; use app::App; diff --git a/hammond-gtk/src/manager.rs b/hammond-gtk/src/manager.rs new file mode 100644 index 0000000..b6830d6 --- /dev/null +++ b/hammond-gtk/src/manager.rs @@ -0,0 +1,161 @@ +// use hammond_data::Episode; +use hammond_data::dbqueries; +use hammond_downloader::downloader::get_episode; +use hammond_downloader::downloader::DownloadProgress; + +use app::Action; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::sync::mpsc::Sender; +// use std::sync::atomic::AtomicUsize; +// use std::path::PathBuf; +use std::thread; + +#[derive(Debug)] +pub struct Progress { + total_bytes: u64, + downloaded_bytes: u64, +} + +impl Progress { + pub fn get_fraction(&self) -> f64 { + let ratio = self.downloaded_bytes as f64 / self.total_bytes as f64; + debug!("{:?}", self); + debug!("Ratio completed: {}", ratio); + + if ratio >= 1.0 { + return 1.0; + }; + ratio + } + + pub fn get_total_size(&self) -> u64 { + self.total_bytes + } + + pub fn get_downloaded(&self) -> u64 { + self.downloaded_bytes + } +} + +impl Default for Progress { + fn default() -> Self { + Progress { + total_bytes: 0, + downloaded_bytes: 0, + } + } +} + +impl DownloadProgress for Progress { + fn set_downloaded(&mut self, downloaded: u64) { + self.downloaded_bytes = downloaded + } + + fn set_size(&mut self, bytes: u64) { + self.total_bytes = bytes; + } +} + +lazy_static! { + pub static ref ACTIVE_DOWNLOADS: Arc>>>> = { + Arc::new(RwLock::new(HashMap::new())) + }; +} + +pub fn add(id: i32, directory: &str, sender: Sender) { + // Create a new `Progress` struct to keep track of dl progress. + let prog = Arc::new(Mutex::new(Progress::default())); + + { + if let Ok(mut m) = ACTIVE_DOWNLOADS.write() { + m.insert(id, prog.clone()); + } + } + + let dir = directory.to_owned(); + thread::spawn(move || { + if let Ok(episode) = dbqueries::get_episode_from_rowid(id) { + let pid = episode.podcast_id(); + let id = episode.rowid(); + get_episode(&mut episode.into(), dir.as_str(), Some(prog)) + .err() + .map(|err| { + error!("Error while trying to download an episode"); + error!("Error: {}", err); + }); + + { + if let Ok(mut m) = ACTIVE_DOWNLOADS.write() { + info!("Removed: {:?}", m.remove(&id)); + } + } + + // { + // if let Ok(m) = ACTIVE_DOWNLOADS.read() { + // debug!("ACTIVE DOWNLOADS: {:#?}", m); + // } + // } + + sender.send(Action::RefreshEpisodesView).unwrap(); + sender.send(Action::RefreshWidgetIfSame(pid)).unwrap(); + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + 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; + + use std::path::Path; + use std::{thread, time}; + use std::sync::mpsc::channel; + + #[test] + // This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit + // to run it. + #[ignore] + // THIS IS NOT A RELIABLE TEST + // Just quick sanity check + fn test_start_dl() { + 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(true).unwrap(); + index(&feed); + + // Get the Podcast + let pd = dbqueries::get_podcast_from_source_id(sid).unwrap(); + // Get an episode + let episode: Episode = { + let con = database::connection(); + dbqueries::get_episode_from_pk(&*con.get().unwrap(), "e000: Hello, world!", *pd.id()) + .unwrap() + }; + + let (sender, _rx) = channel(); + + 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 + thread::sleep(time::Duration::from_secs(40)); + + let final_path = format!("{}/{}.unknown", &download_fold, episode.rowid()); + println!("{}", &final_path); + assert!(Path::new(&final_path).exists()); + } +} diff --git a/hammond-gtk/src/utils.rs b/hammond-gtk/src/utils.rs index 849eba6..3c624a3 100644 --- a/hammond-gtk/src/utils.rs +++ b/hammond-gtk/src/utils.rs @@ -28,7 +28,7 @@ pub fn refresh_feed(headerbar: Arc
, source: Option>, sender: }; sender.send(Action::HeaderBarHideUpdateIndicator).unwrap(); - sender.send(Action::RefreshViews).unwrap(); + sender.send(Action::RefreshAllViews).unwrap(); }); } diff --git a/hammond-gtk/src/views/episodes.rs b/hammond-gtk/src/views/episodes.rs index c0b04a8..dc36148 100644 --- a/hammond-gtk/src/views/episodes.rs +++ b/hammond-gtk/src/views/episodes.rs @@ -77,7 +77,7 @@ impl Default for EpisodesView { impl EpisodesView { pub fn new(sender: Sender) -> Arc { let view = EpisodesView::default(); - let episodes = dbqueries::get_episodes_widgets_with_limit(100).unwrap(); + let episodes = dbqueries::get_episodes_widgets_with_limit(50).unwrap(); let now_utc = Utc::now(); episodes.into_iter().for_each(|mut ep| { @@ -205,10 +205,7 @@ impl EpisodesViewWidget { let image: gtk::Image = builder.get_object("cover").unwrap(); if let Ok(pd) = dbqueries::get_podcast_cover_from_id(episode.podcast_id()) { - let img = get_pixbuf_from_path(&pd, 64); - if let Some(i) = img { - image.set_from_pixbuf(&i); - } + get_pixbuf_from_path(&pd, 64).map(|img| image.set_from_pixbuf(&img)); } let ep = EpisodeWidget::new(episode, sender.clone()); diff --git a/hammond-gtk/src/widgets/episode.rs b/hammond-gtk/src/widgets/episode.rs index c8f061a..b17c9ce 100644 --- a/hammond-gtk/src/widgets/episode.rs +++ b/hammond-gtk/src/widgets/episode.rs @@ -11,14 +11,32 @@ use hammond_data::dbqueries; use hammond_data::{EpisodeWidgetQuery, Podcast}; use hammond_data::utils::get_download_folder; use hammond_data::errors::*; -use hammond_downloader::downloader; use app::Action; +use manager; -use std::thread; use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; use std::path::Path; +lazy_static! { + static ref SIZE_OPTS: Arc = { + // Declare a custom humansize option struct + // See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html + Arc::new(size_opts::FileSizeOpts { + divider: size_opts::Kilo::Binary, + units: size_opts::Kilo::Decimal, + decimal_places: 0, + decimal_zeroes: 0, + fixed_at: size_opts::FixedAt::No, + long_units: false, + space: true, + suffix: "", + allow_negative: false, + }) + }; +} + #[derive(Debug, Clone)] pub struct EpisodeWidget { pub container: gtk::Box, @@ -28,11 +46,12 @@ pub struct EpisodeWidget { title: gtk::Label, date: gtk::Label, duration: gtk::Label, - size: gtk::Label, progress: gtk::ProgressBar, - progress_label: gtk::Label, + total_size: gtk::Label, + local_size: gtk::Label, separator1: gtk::Label, separator2: gtk::Label, + prog_separator: gtk::Label, } impl Default for EpisodeWidget { @@ -49,11 +68,12 @@ impl Default for EpisodeWidget { let title: gtk::Label = builder.get_object("title_label").unwrap(); let date: gtk::Label = builder.get_object("date_label").unwrap(); let duration: gtk::Label = builder.get_object("duration_label").unwrap(); - let size: gtk::Label = builder.get_object("size_label").unwrap(); - let progress_label: gtk::Label = builder.get_object("progress_label").unwrap(); + let local_size: gtk::Label = builder.get_object("local_size").unwrap(); + let total_size: gtk::Label = builder.get_object("total_size").unwrap(); let separator1: gtk::Label = builder.get_object("separator1").unwrap(); let separator2: gtk::Label = builder.get_object("separator2").unwrap(); + let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap(); EpisodeWidget { container, @@ -63,11 +83,12 @@ impl Default for EpisodeWidget { cancel, title, duration, - size, date, - progress_label, + total_size, + local_size, separator1, separator2, + prog_separator, } } } @@ -83,15 +104,15 @@ impl EpisodeWidget { widget } - // TODO: calculate lenght. - // TODO: wire the progress_bar to the downloader. // TODO: wire the cancel button. fn init(&self, episode: &mut EpisodeWidgetQuery, sender: Sender) { + WidgetExt::set_name(&self.container, &episode.rowid().to_string()); + // Set the title label state. self.set_title(episode); // Set the size label. - self.set_size(episode.length()); + self.set_total_size(episode.length()); // Set the duaration label. self.set_duration(episode.duration()); @@ -102,6 +123,9 @@ impl EpisodeWidget { // Show or hide the play/delete/download buttons upon widget initialization. self.show_buttons(episode.local_uri()); + // Determine what the state of the progress bar should be. + self.determine_progess_bar(); + let title = &self.title; self.play .connect_clicked(clone!(episode, title, sender => move |_| { @@ -115,17 +139,10 @@ impl EpisodeWidget { }; })); - let cancel = &self.cancel; - let progress = self.progress.clone(); self.download - .connect_clicked(clone!(episode, cancel, progress, sender => move |dl| { - on_download_clicked( - &mut episode.clone(), - dl, - &cancel, - progress.clone(), - sender.clone() - ); + .connect_clicked(clone!(episode, sender => move |dl| { + dl.set_sensitive(false); + on_download_clicked(&mut episode.clone(), sender.clone()); })); } @@ -175,80 +192,86 @@ impl EpisodeWidget { } /// Set the Episode label dependings on its size - fn set_size(&self, bytes: Option) { - if (bytes == Some(0)) || bytes.is_none() { - return; - }; - - // Declare a custom humansize option struct - // See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html - let custom_options = size_opts::FileSizeOpts { - divider: size_opts::Kilo::Binary, - units: size_opts::Kilo::Decimal, - decimal_places: 0, - decimal_zeroes: 0, - fixed_at: size_opts::FixedAt::No, - long_units: false, - space: true, - suffix: "", - allow_negative: false, - }; - + fn set_total_size(&self, bytes: Option) { if let Some(size) = bytes { - let s = size.file_size(custom_options); - if let Ok(s) = s { - self.size.set_text(&s); - self.size.show(); - self.separator2.show(); + if size != 0 { + size.file_size(SIZE_OPTS.clone()).ok().map(|s| { + self.total_size.set_text(&s); + self.total_size.show(); + self.separator2.show(); + }); } }; } + + // FIXME: REFACTOR ME + fn determine_progess_bar(&self) { + let id = WidgetExt::get_name(&self.container) + .unwrap() + .parse::() + .unwrap(); + + let prog_struct = || -> Option<_> { + if let Ok(m) = manager::ACTIVE_DOWNLOADS.read() { + if !m.contains_key(&id) { + return None; + }; + return m.get(&id).cloned(); + } + None + }(); + + let progress_bar = self.progress.clone(); + let total_size = self.total_size.clone(); + let local_size = self.local_size.clone(); + if let Some(prog) = prog_struct { + self.download.hide(); + self.progress.show(); + self.local_size.show(); + self.total_size.show(); + self.separator2.show(); + self.prog_separator.show(); + self.cancel.show(); + + // Setup a callback that will update the progress bar. + update_progressbar_callback(prog.clone(), id, progress_bar, local_size); + + // Setup a callback that will update the total_size label + // with the http ContentLength header number rather than + // relying to the RSS feed. + update_total_size_callback(prog.clone(), total_size); + } + } } -fn on_download_clicked( - ep: &mut EpisodeWidgetQuery, - download_bttn: >k::Button, - cancel_bttn: >k::Button, - progress_bar: gtk::ProgressBar, - sender: Sender, -) { - let progress = progress_bar.clone(); +fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender) { + let download_fold = dbqueries::get_podcast_from_id(ep.podcast_id()) + .ok() + .map(|pd| get_download_folder(&pd.title().to_owned()).ok()) + .and_then(|x| x); - // Start the proggress_bar pulse. - timeout_add(200, move || { - progress_bar.pulse(); - glib::Continue(true) - }); + // Start a new download. + if let Some(fold) = download_fold { + manager::add(ep.rowid(), &fold, sender.clone()); + } - let pd = dbqueries::get_podcast_from_id(ep.podcast_id()).unwrap(); - let pd_title = pd.title().to_owned(); - let mut ep = ep.clone(); - cancel_bttn.show(); - progress.show(); - download_bttn.hide(); - sender.send(Action::RefreshEpisodesViewBGR).unwrap(); - thread::spawn(move || { - let download_fold = 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(Action::RefreshViews).unwrap(); - }); + // Update Views + sender.send(Action::RefreshEpisodesView).unwrap(); + sender.send(Action::RefreshWidgetIfVis).unwrap(); } fn on_play_bttn_clicked(episode_id: i32) { - let local_uri = dbqueries::get_episode_local_uri_from_id(episode_id).unwrap(); + let local_uri = dbqueries::get_episode_local_uri_from_id(episode_id) + .ok() + .and_then(|x| x); if let Some(uri) = local_uri { if Path::new(&uri).exists() { info!("Opening {}", uri); - let e = open::that(&uri); - if let Err(err) = e { + open::that(&uri).err().map(|err| { error!("Error while trying to open file: {}", uri); error!("Error: {}", err); - }; + }); } } else { error!( @@ -258,6 +281,74 @@ fn on_play_bttn_clicked(episode_id: i32) { } } +// Setup a callback that will update the progress bar. +fn update_progressbar_callback( + prog: Arc>, + episode_rowid: i32, + progress_bar: gtk::ProgressBar, + local_size: gtk::Label, +) { + timeout_add( + 400, + clone!(prog, progress_bar => move || { + let (fraction, downloaded) = { + let m = prog.lock().unwrap(); + (m.get_fraction(), m.get_downloaded()) + }; + + // Update local_size label + downloaded.file_size(SIZE_OPTS.clone()).ok().map(|x| local_size.set_text(&x)); + + // I hate floating points. + // Update the progress_bar. + if (fraction >= 0.0) && (fraction <= 1.0) && (!fraction.is_nan()) { + progress_bar.set_fraction(fraction); + } + + // info!("Fraction: {}", progress_bar.get_fraction()); + // info!("Fraction: {}", fraction); + + // Check if the download is still active + let active = { + let m = manager::ACTIVE_DOWNLOADS.read().unwrap(); + m.contains_key(&episode_rowid) + }; + + if (fraction >= 1.0) && (!fraction.is_nan()){ + glib::Continue(false) + } else if !active { + glib::Continue(false) + } else { + glib::Continue(true) + } + }), + ); +} + +// Setup a callback that will update the total_size label +// with the http ContentLength header number rather than +// relying to the RSS feed. +fn update_total_size_callback(prog: Arc>, total_size: gtk::Label) { + timeout_add( + 500, + clone!(prog, total_size => move || { + let total_bytes = { + let m = prog.lock().unwrap(); + m.get_total_size() + }; + + debug!("Total Size: {}", total_bytes); + if total_bytes != 0 { + // Update the total_size label + total_bytes.file_size(SIZE_OPTS.clone()).ok().map(|x| total_size.set_text(&x)); + glib::Continue(false) + } else { + glib::Continue(true) + } + }), + ); +} + // fn on_delete_bttn_clicked(episode_id: i32) { // let mut ep = dbqueries::get_episode_from_rowid(episode_id) // .unwrap() diff --git a/hammond-gtk/src/widgets/show.rs b/hammond-gtk/src/widgets/show.rs index 5a0c5f8..ca2e850 100644 --- a/hammond-gtk/src/widgets/show.rs +++ b/hammond-gtk/src/widgets/show.rs @@ -69,7 +69,6 @@ impl ShowWidget { self.unsub .connect_clicked(clone!(shows, pd, sender => move |bttn| { on_unsub_button_clicked(shows.clone(), &pd, bttn, sender.clone()); - sender.send(Action::HeaderBarNormal).unwrap(); })); self.setup_listbox(pd, sender.clone()); @@ -80,24 +79,22 @@ impl ShowWidget { self.link.set_tooltip_text(Some(link.as_str())); self.link.connect_clicked(move |_| { info!("Opening link: {}", &link); - let _ = open::that(&link); + open::that(&link) + .err() + .map(|err| error!("Something went wrong: {}", err)); }); } /// Populate the listbox with the shows episodes. fn setup_listbox(&self, pd: &Podcast, sender: Sender) { let listbox = episodes_listbox(pd, sender.clone()); - if let Ok(l) = listbox { - self.episodes.add(&l); - } + listbox.ok().map(|l| self.episodes.add(&l)); } /// Set the show cover. fn set_cover(&self, pd: &Podcast) { let img = get_pixbuf_from_path(&pd.clone().into(), 128); - if let Some(i) = img { - self.cover.set_from_pixbuf(&i); - } + img.map(|i| self.cover.set_from_pixbuf(&i)); } /// Set the descripton text. @@ -124,11 +121,16 @@ fn on_unsub_button_clicked( unsub_button.hide(); // Spawn a thread so it won't block the ui. thread::spawn(clone!(pd => move || { - delete_show(&pd) + 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(); // Queue a refresh after the switch to avoid blocking the db. - sender.send(Action::RefreshViews).unwrap(); + sender.send(Action::RefreshShowsView).unwrap(); + sender.send(Action::RefreshEpisodesView).unwrap(); } #[allow(dead_code)]