Merge branch '33-downloader-re-work' into 'master'

Resolve "Downloader Re-work"

Closes #33

See merge request alatiera/Hammond!12
This commit is contained in:
Jordan Petridis 2018-01-10 00:56:18 +00:00
commit fe2f8617a2
17 changed files with 527 additions and 171 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -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()
}

View File

@ -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;

View File

@ -215,6 +215,22 @@ pub struct EpisodeWidgetQuery {
podcast_id: i32,
}
impl From<Episode> 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::<ETag>();
let lmod = headers.get::<LastModified>();

View File

@ -137,21 +137,14 @@ pub fn get_download_folder(pd_title: &str) -> Result<String> {
/// 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() {
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() {
let fold = get_download_folder(pd.title())?;
fs::remove_dir_all(&fold)?;
info!("All the content at, {} was removed succesfully", &fold);
}
};
}
Ok(())
}
#[cfg(test)]

View File

@ -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"]

View File

@ -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<String> {
fn download_into(
dir: &str,
file_title: &str,
url: &str,
progress: Option<Arc<Mutex<DownloadProgress>>>,
) -> Result<String> {
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<String> {
}
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));
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<String> {
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<ContentType>) -> Option<String> {
let cont = content.clone()?;
content
@ -73,8 +91,14 @@ fn get_ext(content: Option<ContentType>) -> Option<String> {
}
// 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<u64>) -> Result<()> {
fn save_io(
file: &str,
resp: &mut reqwest::Response,
content_lenght: Option<u64>,
progress: Option<Arc<Mutex<DownloadProgress>>>,
) -> 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<u64>
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<u64>
}
// 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<Arc<Mutex<DownloadProgress>>>,
) -> 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,44 +177,35 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Option<String> {
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 {
match download_into(&cache_download_fold, "cover", &url, None) {
Ok(path) => {
info!("Cached img into: {}", &path);
Some(path)
} else {
}
Err(err) => {
error!("Failed to get feed image.");
error!("Error: {}", dlpath.unwrap_err());
error!("Error: {}", err);
None
}
}
}
#[cfg(test)]
mod tests {

View File

@ -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]

View File

@ -120,10 +120,10 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="size_label">
<object class="GtkLabel" id="local_size">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label" translatable="yes">42 MB</property>
<property name="label" translatable="yes">0 MB</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<style>
@ -137,10 +137,10 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="progress_label">
<object class="GtkLabel" id="prog_separator">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label" translatable="yes">12 MB / 42 MB</property>
<property name="label" translatable="yes">/</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<style>
@ -153,6 +153,23 @@
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="total_size">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label" translatable="yes">XX MB</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@ -257,6 +274,7 @@
<object class="GtkProgressBar" id="progress_bar">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="pulse_step">0</property>
</object>
<packing>
<property name="expand">False</property>

View File

@ -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<Source>),
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)

View File

@ -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::<i32>().unwrap());
let pd = dbqueries::get_podcast_from_id(id.unwrap().parse::<i32>().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);

View File

@ -53,6 +53,7 @@ mod content;
mod app;
mod utils;
mod manager;
mod static_resource;
use app::App;

161
hammond-gtk/src/manager.rs Normal file
View File

@ -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<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> = {
Arc::new(RwLock::new(HashMap::new()))
};
}
pub fn add(id: i32, directory: &str, sender: Sender<Action>) {
// 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());
}
}

View File

@ -28,7 +28,7 @@ pub fn refresh_feed(headerbar: Arc<Header>, source: Option<Vec<Source>>, sender:
};
sender.send(Action::HeaderBarHideUpdateIndicator).unwrap();
sender.send(Action::RefreshViews).unwrap();
sender.send(Action::RefreshAllViews).unwrap();
});
}

View File

@ -77,7 +77,7 @@ impl Default for EpisodesView {
impl EpisodesView {
pub fn new(sender: Sender<Action>) -> Arc<EpisodesView> {
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());

View File

@ -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<size_opts::FileSizeOpts> = {
// 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<Action>) {
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<i32>) {
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<i32>) {
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();
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::<i32>()
.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: &gtk::Button,
cancel_bttn: &gtk::Button,
progress_bar: gtk::ProgressBar,
sender: Sender<Action>,
) {
let progress = progress_bar.clone();
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) {
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<Mutex<manager::Progress>>,
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<Mutex<manager::Progress>>, 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()

View File

@ -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<Action>) {
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)]