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:
commit
fe2f8617a2
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -508,6 +508,11 @@ dependencies = [
|
|||||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -591,6 +596,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"diesel 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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",
|
"hammond-data 0.1.0",
|
||||||
"hyper 0.11.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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 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 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 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 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 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"
|
"checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//! Database Setup. This is only public to help with some unit tests.
|
||||||
|
|
||||||
use r2d2_diesel::ConnectionManager;
|
use r2d2_diesel::ConnectionManager;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use r2d2;
|
use r2d2;
|
||||||
@ -35,7 +37,8 @@ lazy_static! {
|
|||||||
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
|
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()
|
POOL.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ pub mod utils;
|
|||||||
pub mod feed;
|
pub mod feed;
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub(crate) mod database;
|
pub mod database;
|
||||||
pub(crate) mod models;
|
pub(crate) mod models;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|||||||
@ -215,6 +215,22 @@ pub struct EpisodeWidgetQuery {
|
|||||||
podcast_id: i32,
|
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 {
|
impl EpisodeWidgetQuery {
|
||||||
/// Get the value of the sqlite's `ROW_ID`
|
/// Get the value of the sqlite's `ROW_ID`
|
||||||
pub fn rowid(&self) -> i32 {
|
pub fn rowid(&self) -> i32 {
|
||||||
@ -597,7 +613,6 @@ impl<'a> Source {
|
|||||||
fn update_etag(&mut self, req: &reqwest::Response) -> Result<()> {
|
fn update_etag(&mut self, req: &reqwest::Response) -> Result<()> {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
|
|
||||||
// let etag = headers.get_raw("ETag").unwrap();
|
|
||||||
let etag = headers.get::<ETag>();
|
let etag = headers.get::<ETag>();
|
||||||
let lmod = headers.get::<LastModified>();
|
let lmod = headers.get::<LastModified>();
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
/// Removes all the entries associated with the given show from the database,
|
||||||
/// and deletes all of the downloaded content.
|
/// and deletes all of the downloaded content.
|
||||||
/// TODO: Write Tests
|
/// TODO: Write Tests
|
||||||
/// TODO: Return Result instead
|
pub fn delete_show(pd: &Podcast) -> Result<()> {
|
||||||
pub fn delete_show(pd: &Podcast) {
|
dbqueries::remove_feed(&pd)?;
|
||||||
let res = dbqueries::remove_feed(pd);
|
info!("{} was removed succesfully.", pd.title());
|
||||||
if res.is_ok() {
|
|
||||||
info!("{} was removed succesfully.", pd.title());
|
|
||||||
|
|
||||||
let dl_fold = get_download_folder(pd.title());
|
let fold = get_download_folder(pd.title())?;
|
||||||
if let Ok(fold) = dl_fold {
|
fs::remove_dir_all(&fold)?;
|
||||||
let res3 = fs::remove_dir_all(&fold);
|
info!("All the content at, {} was removed succesfully", &fold);
|
||||||
// TODO: Show errors?
|
Ok(())
|
||||||
if res3.is_ok() {
|
|
||||||
info!("All the content at, {} was removed succesfully", &fold);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -11,6 +11,7 @@ log = "0.3.8"
|
|||||||
mime_guess = "1.8.3"
|
mime_guess = "1.8.3"
|
||||||
reqwest = "0.8.2"
|
reqwest = "0.8.2"
|
||||||
tempdir = "0.3.5"
|
tempdir = "0.3.5"
|
||||||
|
glob = "0.2.11"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["sqlite"]
|
features = ["sqlite"]
|
||||||
|
|||||||
@ -2,11 +2,13 @@ use reqwest;
|
|||||||
use hyper::header::*;
|
use hyper::header::*;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use mime_guess;
|
use mime_guess;
|
||||||
|
use glob::glob;
|
||||||
|
|
||||||
use std::fs::{rename, DirBuilder, File};
|
use std::fs::{rename, DirBuilder, File};
|
||||||
use std::io::{BufWriter, Read, Write};
|
use std::io::{BufWriter, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use errors::*;
|
use errors::*;
|
||||||
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery};
|
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: 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.
|
// 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 .
|
// Adapted from https://github.com/mattgathu/rget .
|
||||||
// I never wanted to write a custom downloader.
|
// I never wanted to write a custom downloader.
|
||||||
// Sorry to those who will have to work with that code.
|
// 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),
|
// or bindings for a lib like youtube-dl(python),
|
||||||
// But cant seem to find one.
|
// But cant seem to find one.
|
||||||
// TODO: Write unit-tests.
|
// 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);
|
info!("GET request to: {}", url);
|
||||||
let client = reqwest::Client::builder().referer(false).build()?;
|
let client = reqwest::Client::builder().referer(false).build()?;
|
||||||
let mut resp = client.get(url).send()?;
|
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 headers = resp.headers().clone();
|
||||||
|
|
||||||
let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);
|
let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);
|
||||||
let ct_type = headers.get::<ContentType>();
|
let ct_type = headers.get::<ContentType>();
|
||||||
ct_len.map(|x| info!("File Lenght: {}", x));
|
ct_len.map(|x| info!("File Lenght: {}", x));
|
||||||
ct_type.map(|x| info!("Content Type: {}", 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);
|
info!("Extension: {}", ext);
|
||||||
|
|
||||||
// Construct a temp file to save desired content.
|
// 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(),);
|
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 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.
|
// Construct the desired path.
|
||||||
let target = format!("{}/{}.{}", dir, file_title, ext);
|
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)
|
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> {
|
fn get_ext(content: Option<ContentType>) -> Option<String> {
|
||||||
let cont = content.clone()?;
|
let cont = content.clone()?;
|
||||||
content
|
content
|
||||||
@ -73,8 +91,14 @@ fn get_ext(content: Option<ContentType>) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Write unit-tests.
|
// TODO: Write unit-tests.
|
||||||
|
// TODO: Refactor... Somehow.
|
||||||
/// Handles the I/O of fetching a remote file and saving into a Buffer and A File.
|
/// 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);
|
info!("Downloading into: {}", file);
|
||||||
let chunk_size = match content_lenght {
|
let chunk_size = match content_lenght {
|
||||||
Some(x) => x as usize / 99,
|
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);
|
buffer.truncate(bcount);
|
||||||
if !buffer.is_empty() {
|
if !buffer.is_empty() {
|
||||||
writer.write_all(buffer.as_slice())?;
|
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 {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -98,7 +130,11 @@ fn save_io(file: &str, resp: &mut reqwest::Response, content_lenght: Option<u64>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor
|
// 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
|
// Check if its alrdy downloaded
|
||||||
if ep.local_uri().is_some() {
|
if ep.local_uri().is_some() {
|
||||||
if Path::new(ep.local_uri().unwrap()).exists() {
|
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()?;
|
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 let Ok(path) = res {
|
||||||
// If download succedes set episode local_uri to dlpath.
|
// If download succedes set episode local_uri to dlpath.
|
||||||
@ -136,42 +177,33 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Option<String> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let download_fold = format!(
|
let cache_download_fold = format!("{}{}", HAMMOND_CACHE.to_str()?, pd.title().to_owned());
|
||||||
"{}{}",
|
|
||||||
HAMMOND_CACHE.to_str().unwrap(),
|
|
||||||
pd.title().to_owned()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hacky way
|
// Weird glob magic.
|
||||||
// TODO: make it so it returns the first cover.* file encountered.
|
if let Ok(mut foo) = glob(&format!("{}/cover.*", cache_download_fold)) {
|
||||||
// Use glob instead
|
// For some reason there is no .first() method so nth(0) is used
|
||||||
let png = format!("{}/cover.png", download_fold);
|
let path = foo.nth(0).and_then(|x| x.ok());
|
||||||
let jpg = format!("{}/cover.jpg", download_fold);
|
if let Some(p) = path {
|
||||||
let jpe = format!("{}/cover.jpe", download_fold);
|
return Some(p.to_str()?.into());
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create the folders if they don't exist.
|
||||||
DirBuilder::new()
|
DirBuilder::new()
|
||||||
.recursive(true)
|
.recursive(true)
|
||||||
.create(&download_fold)
|
.create(&cache_download_fold)
|
||||||
.unwrap();
|
.ok()?;
|
||||||
|
|
||||||
let dlpath = download_into(&download_fold, "cover", &url);
|
match download_into(&cache_download_fold, "cover", &url, None) {
|
||||||
if let Ok(path) = dlpath {
|
Ok(path) => {
|
||||||
info!("Cached img into: {}", &path);
|
info!("Cached img into: {}", &path);
|
||||||
Some(path)
|
Some(path)
|
||||||
} else {
|
}
|
||||||
error!("Failed to get feed image.");
|
Err(err) => {
|
||||||
error!("Error: {}", dlpath.unwrap_err());
|
error!("Failed to get feed image.");
|
||||||
None
|
error!("Error: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate error_chain;
|
extern crate error_chain;
|
||||||
|
extern crate glob;
|
||||||
extern crate hammond_data;
|
extern crate hammond_data;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
@ -120,10 +120,10 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="size_label">
|
<object class="GtkLabel" id="local_size">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</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="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@ -137,10 +137,10 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="progress_label">
|
<object class="GtkLabel" id="prog_separator">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</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="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@ -153,6 +153,23 @@
|
|||||||
<property name="position">5</property>
|
<property name="position">5</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</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>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
@ -257,6 +274,7 @@
|
|||||||
<object class="GtkProgressBar" id="progress_bar">
|
<object class="GtkProgressBar" id="progress_bar">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="pulse_step">0</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
|||||||
@ -11,14 +11,19 @@ use headerbar::Header;
|
|||||||
use content::Content;
|
use content::Content;
|
||||||
use utils;
|
use utils;
|
||||||
|
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
UpdateSources(Option<Source>),
|
UpdateSources(Option<Source>),
|
||||||
RefreshViews,
|
RefreshAllViews,
|
||||||
|
RefreshEpisodesView,
|
||||||
RefreshEpisodesViewBGR,
|
RefreshEpisodesViewBGR,
|
||||||
|
RefreshShowsView,
|
||||||
|
RefreshWidget,
|
||||||
|
RefreshWidgetIfVis,
|
||||||
|
RefreshWidgetIfSame(i32),
|
||||||
HeaderBarShowTile(String),
|
HeaderBarShowTile(String),
|
||||||
HeaderBarNormal,
|
HeaderBarNormal,
|
||||||
HeaderBarHideUpdateIndicator,
|
HeaderBarHideUpdateIndicator,
|
||||||
@ -134,12 +139,17 @@ impl App {
|
|||||||
utils::refresh_feed(headerbar.clone(), Some(vec![s]), sender.clone())
|
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::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(),
|
||||||
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
|
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
|
||||||
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
|
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
|
||||||
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
|
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
|
||||||
_ => (),
|
Err(_) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
|
|||||||
@ -41,8 +41,9 @@ impl Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self) {
|
pub fn update(&self) {
|
||||||
self.update_shows_view();
|
|
||||||
self.update_episode_view();
|
self.update_episode_view();
|
||||||
|
self.update_shows_view();
|
||||||
|
self.update_widget()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_episode_view(&self) {
|
pub fn update_episode_view(&self) {
|
||||||
@ -56,7 +57,23 @@ impl Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_shows_view(&self) {
|
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 {
|
pub fn get_stack(&self) -> gtk::Stack {
|
||||||
@ -100,15 +117,11 @@ impl ShowStack {
|
|||||||
show
|
show
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn is_empty(&self) -> bool {
|
// pub fn update(&self) {
|
||||||
// self.podcasts.is_empty()
|
// self.update_widget();
|
||||||
|
// self.update_podcasts();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn update(&self) {
|
|
||||||
self.update_podcasts();
|
|
||||||
self.update_widget();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_podcasts(&self) {
|
pub fn update_podcasts(&self) {
|
||||||
let vis = self.stack.get_visible_child_name().unwrap();
|
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 vis = self.stack.get_visible_child_name().unwrap();
|
||||||
let old = self.stack.get_child_by_name("widget").unwrap();
|
let old = self.stack.get_child_by_name("widget").unwrap();
|
||||||
|
|
||||||
let id = WidgetExt::get_name(&old).unwrap();
|
let id = WidgetExt::get_name(&old);
|
||||||
if id == "GtkBox" {
|
if id == Some("GtkBox".to_string()) || id.is_none() {
|
||||||
return;
|
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 {
|
if let Ok(pd) = pd {
|
||||||
self.replace_widget(&pd);
|
self.replace_widget(&pd);
|
||||||
self.stack.set_visible_child_name(&vis);
|
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) {
|
pub fn switch_podcasts_animated(&self) {
|
||||||
self.stack
|
self.stack
|
||||||
.set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight);
|
.set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight);
|
||||||
|
|||||||
@ -53,6 +53,7 @@ mod content;
|
|||||||
mod app;
|
mod app;
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod manager;
|
||||||
mod static_resource;
|
mod static_resource;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
|
|||||||
161
hammond-gtk/src/manager.rs
Normal file
161
hammond-gtk/src/manager.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ pub fn refresh_feed(headerbar: Arc<Header>, source: Option<Vec<Source>>, sender:
|
|||||||
};
|
};
|
||||||
|
|
||||||
sender.send(Action::HeaderBarHideUpdateIndicator).unwrap();
|
sender.send(Action::HeaderBarHideUpdateIndicator).unwrap();
|
||||||
sender.send(Action::RefreshViews).unwrap();
|
sender.send(Action::RefreshAllViews).unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,7 @@ impl Default for EpisodesView {
|
|||||||
impl EpisodesView {
|
impl EpisodesView {
|
||||||
pub fn new(sender: Sender<Action>) -> Arc<EpisodesView> {
|
pub fn new(sender: Sender<Action>) -> Arc<EpisodesView> {
|
||||||
let view = EpisodesView::default();
|
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();
|
let now_utc = Utc::now();
|
||||||
|
|
||||||
episodes.into_iter().for_each(|mut ep| {
|
episodes.into_iter().for_each(|mut ep| {
|
||||||
@ -205,10 +205,7 @@ impl EpisodesViewWidget {
|
|||||||
let image: gtk::Image = builder.get_object("cover").unwrap();
|
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
|
|
||||||
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(episode.podcast_id()) {
|
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(episode.podcast_id()) {
|
||||||
let img = get_pixbuf_from_path(&pd, 64);
|
get_pixbuf_from_path(&pd, 64).map(|img| image.set_from_pixbuf(&img));
|
||||||
if let Some(i) = img {
|
|
||||||
image.set_from_pixbuf(&i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ep = EpisodeWidget::new(episode, sender.clone());
|
let ep = EpisodeWidget::new(episode, sender.clone());
|
||||||
|
|||||||
@ -11,14 +11,32 @@ use hammond_data::dbqueries;
|
|||||||
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
||||||
use hammond_data::utils::get_download_folder;
|
use hammond_data::utils::get_download_folder;
|
||||||
use hammond_data::errors::*;
|
use hammond_data::errors::*;
|
||||||
use hammond_downloader::downloader;
|
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
|
use manager;
|
||||||
|
|
||||||
use std::thread;
|
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use std::path::Path;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EpisodeWidget {
|
pub struct EpisodeWidget {
|
||||||
pub container: gtk::Box,
|
pub container: gtk::Box,
|
||||||
@ -28,11 +46,12 @@ pub struct EpisodeWidget {
|
|||||||
title: gtk::Label,
|
title: gtk::Label,
|
||||||
date: gtk::Label,
|
date: gtk::Label,
|
||||||
duration: gtk::Label,
|
duration: gtk::Label,
|
||||||
size: gtk::Label,
|
|
||||||
progress: gtk::ProgressBar,
|
progress: gtk::ProgressBar,
|
||||||
progress_label: gtk::Label,
|
total_size: gtk::Label,
|
||||||
|
local_size: gtk::Label,
|
||||||
separator1: gtk::Label,
|
separator1: gtk::Label,
|
||||||
separator2: gtk::Label,
|
separator2: gtk::Label,
|
||||||
|
prog_separator: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EpisodeWidget {
|
impl Default for EpisodeWidget {
|
||||||
@ -49,11 +68,12 @@ impl Default for EpisodeWidget {
|
|||||||
let title: gtk::Label = builder.get_object("title_label").unwrap();
|
let title: gtk::Label = builder.get_object("title_label").unwrap();
|
||||||
let date: gtk::Label = builder.get_object("date_label").unwrap();
|
let date: gtk::Label = builder.get_object("date_label").unwrap();
|
||||||
let duration: gtk::Label = builder.get_object("duration_label").unwrap();
|
let duration: gtk::Label = builder.get_object("duration_label").unwrap();
|
||||||
let size: gtk::Label = builder.get_object("size_label").unwrap();
|
let local_size: gtk::Label = builder.get_object("local_size").unwrap();
|
||||||
let progress_label: gtk::Label = builder.get_object("progress_label").unwrap();
|
let total_size: gtk::Label = builder.get_object("total_size").unwrap();
|
||||||
|
|
||||||
let separator1: gtk::Label = builder.get_object("separator1").unwrap();
|
let separator1: gtk::Label = builder.get_object("separator1").unwrap();
|
||||||
let separator2: gtk::Label = builder.get_object("separator2").unwrap();
|
let separator2: gtk::Label = builder.get_object("separator2").unwrap();
|
||||||
|
let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap();
|
||||||
|
|
||||||
EpisodeWidget {
|
EpisodeWidget {
|
||||||
container,
|
container,
|
||||||
@ -63,11 +83,12 @@ impl Default for EpisodeWidget {
|
|||||||
cancel,
|
cancel,
|
||||||
title,
|
title,
|
||||||
duration,
|
duration,
|
||||||
size,
|
|
||||||
date,
|
date,
|
||||||
progress_label,
|
total_size,
|
||||||
|
local_size,
|
||||||
separator1,
|
separator1,
|
||||||
separator2,
|
separator2,
|
||||||
|
prog_separator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,15 +104,15 @@ impl EpisodeWidget {
|
|||||||
widget
|
widget
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: calculate lenght.
|
|
||||||
// TODO: wire the progress_bar to the downloader.
|
|
||||||
// TODO: wire the cancel button.
|
// TODO: wire the cancel button.
|
||||||
fn init(&self, episode: &mut EpisodeWidgetQuery, sender: Sender<Action>) {
|
fn init(&self, episode: &mut EpisodeWidgetQuery, sender: Sender<Action>) {
|
||||||
|
WidgetExt::set_name(&self.container, &episode.rowid().to_string());
|
||||||
|
|
||||||
// Set the title label state.
|
// Set the title label state.
|
||||||
self.set_title(episode);
|
self.set_title(episode);
|
||||||
|
|
||||||
// Set the size label.
|
// Set the size label.
|
||||||
self.set_size(episode.length());
|
self.set_total_size(episode.length());
|
||||||
|
|
||||||
// Set the duaration label.
|
// Set the duaration label.
|
||||||
self.set_duration(episode.duration());
|
self.set_duration(episode.duration());
|
||||||
@ -102,6 +123,9 @@ impl EpisodeWidget {
|
|||||||
// Show or hide the play/delete/download buttons upon widget initialization.
|
// Show or hide the play/delete/download buttons upon widget initialization.
|
||||||
self.show_buttons(episode.local_uri());
|
self.show_buttons(episode.local_uri());
|
||||||
|
|
||||||
|
// Determine what the state of the progress bar should be.
|
||||||
|
self.determine_progess_bar();
|
||||||
|
|
||||||
let title = &self.title;
|
let title = &self.title;
|
||||||
self.play
|
self.play
|
||||||
.connect_clicked(clone!(episode, title, sender => move |_| {
|
.connect_clicked(clone!(episode, title, sender => move |_| {
|
||||||
@ -115,17 +139,10 @@ impl EpisodeWidget {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let cancel = &self.cancel;
|
|
||||||
let progress = self.progress.clone();
|
|
||||||
self.download
|
self.download
|
||||||
.connect_clicked(clone!(episode, cancel, progress, sender => move |dl| {
|
.connect_clicked(clone!(episode, sender => move |dl| {
|
||||||
on_download_clicked(
|
dl.set_sensitive(false);
|
||||||
&mut episode.clone(),
|
on_download_clicked(&mut episode.clone(), sender.clone());
|
||||||
dl,
|
|
||||||
&cancel,
|
|
||||||
progress.clone(),
|
|
||||||
sender.clone()
|
|
||||||
);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,80 +192,86 @@ impl EpisodeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the Episode label dependings on its size
|
/// Set the Episode label dependings on its size
|
||||||
fn set_size(&self, bytes: Option<i32>) {
|
fn set_total_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,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(size) = bytes {
|
if let Some(size) = bytes {
|
||||||
let s = size.file_size(custom_options);
|
if size != 0 {
|
||||||
if let Ok(s) = s {
|
size.file_size(SIZE_OPTS.clone()).ok().map(|s| {
|
||||||
self.size.set_text(&s);
|
self.total_size.set_text(&s);
|
||||||
self.size.show();
|
self.total_size.show();
|
||||||
self.separator2.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(
|
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) {
|
||||||
ep: &mut EpisodeWidgetQuery,
|
let download_fold = dbqueries::get_podcast_from_id(ep.podcast_id())
|
||||||
download_bttn: >k::Button,
|
.ok()
|
||||||
cancel_bttn: >k::Button,
|
.map(|pd| get_download_folder(&pd.title().to_owned()).ok())
|
||||||
progress_bar: gtk::ProgressBar,
|
.and_then(|x| x);
|
||||||
sender: Sender<Action>,
|
|
||||||
) {
|
|
||||||
let progress = progress_bar.clone();
|
|
||||||
|
|
||||||
// Start the proggress_bar pulse.
|
// Start a new download.
|
||||||
timeout_add(200, move || {
|
if let Some(fold) = download_fold {
|
||||||
progress_bar.pulse();
|
manager::add(ep.rowid(), &fold, sender.clone());
|
||||||
glib::Continue(true)
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let pd = dbqueries::get_podcast_from_id(ep.podcast_id()).unwrap();
|
// Update Views
|
||||||
let pd_title = pd.title().to_owned();
|
sender.send(Action::RefreshEpisodesView).unwrap();
|
||||||
let mut ep = ep.clone();
|
sender.send(Action::RefreshWidgetIfVis).unwrap();
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_play_bttn_clicked(episode_id: i32) {
|
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 let Some(uri) = local_uri {
|
||||||
if Path::new(&uri).exists() {
|
if Path::new(&uri).exists() {
|
||||||
info!("Opening {}", uri);
|
info!("Opening {}", uri);
|
||||||
let e = open::that(&uri);
|
open::that(&uri).err().map(|err| {
|
||||||
if let Err(err) = e {
|
|
||||||
error!("Error while trying to open file: {}", uri);
|
error!("Error while trying to open file: {}", uri);
|
||||||
error!("Error: {}", err);
|
error!("Error: {}", err);
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
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) {
|
// fn on_delete_bttn_clicked(episode_id: i32) {
|
||||||
// let mut ep = dbqueries::get_episode_from_rowid(episode_id)
|
// let mut ep = dbqueries::get_episode_from_rowid(episode_id)
|
||||||
// .unwrap()
|
// .unwrap()
|
||||||
|
|||||||
@ -69,7 +69,6 @@ impl ShowWidget {
|
|||||||
self.unsub
|
self.unsub
|
||||||
.connect_clicked(clone!(shows, pd, sender => move |bttn| {
|
.connect_clicked(clone!(shows, pd, sender => move |bttn| {
|
||||||
on_unsub_button_clicked(shows.clone(), &pd, bttn, sender.clone());
|
on_unsub_button_clicked(shows.clone(), &pd, bttn, sender.clone());
|
||||||
sender.send(Action::HeaderBarNormal).unwrap();
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.setup_listbox(pd, sender.clone());
|
self.setup_listbox(pd, sender.clone());
|
||||||
@ -80,24 +79,22 @@ impl ShowWidget {
|
|||||||
self.link.set_tooltip_text(Some(link.as_str()));
|
self.link.set_tooltip_text(Some(link.as_str()));
|
||||||
self.link.connect_clicked(move |_| {
|
self.link.connect_clicked(move |_| {
|
||||||
info!("Opening link: {}", &link);
|
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.
|
/// Populate the listbox with the shows episodes.
|
||||||
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
|
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
|
||||||
let listbox = episodes_listbox(pd, sender.clone());
|
let listbox = episodes_listbox(pd, sender.clone());
|
||||||
if let Ok(l) = listbox {
|
listbox.ok().map(|l| self.episodes.add(&l));
|
||||||
self.episodes.add(&l);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the show cover.
|
/// Set the show cover.
|
||||||
fn set_cover(&self, pd: &Podcast) {
|
fn set_cover(&self, pd: &Podcast) {
|
||||||
let img = get_pixbuf_from_path(&pd.clone().into(), 128);
|
let img = get_pixbuf_from_path(&pd.clone().into(), 128);
|
||||||
if let Some(i) = img {
|
img.map(|i| self.cover.set_from_pixbuf(&i));
|
||||||
self.cover.set_from_pixbuf(&i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the descripton text.
|
/// Set the descripton text.
|
||||||
@ -124,11 +121,16 @@ fn on_unsub_button_clicked(
|
|||||||
unsub_button.hide();
|
unsub_button.hide();
|
||||||
// Spawn a thread so it won't block the ui.
|
// Spawn a thread so it won't block the ui.
|
||||||
thread::spawn(clone!(pd => move || {
|
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();
|
shows.switch_podcasts_animated();
|
||||||
|
sender.send(Action::HeaderBarNormal).unwrap();
|
||||||
// Queue a refresh after the switch to avoid blocking the db.
|
// 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)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user