Merge branch 'master' of gitlab.gnome.org:World/podcasts

This commit is contained in:
Anton Palgunov 2020-06-21 21:15:03 +01:00
commit f2ac198831
No known key found for this signature in database
GPG Key ID: 2F082FC6B31FC9F1
31 changed files with 1949 additions and 2005 deletions

View File

@ -4,7 +4,7 @@ include:
# ref: '' # ref: ''
flatpak: flatpak:
image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:master' image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.36'
variables: variables:
MANIFEST_PATH: "org.gnome.Podcasts.Devel.json" MANIFEST_PATH: "org.gnome.Podcasts.Devel.json"
FLATPAK_MODULE: "gnome-podcasts" FLATPAK_MODULE: "gnome-podcasts"

2861
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,14 @@ else
version_suffix = '' version_suffix = ''
endif endif
podcast_toml = files(
'Cargo.toml',
'Cargo.lock',
'podcasts-data/Cargo.toml',
'podcasts-downloader/Cargo.toml',
'podcasts-gtk/Cargo.toml',
)
application_id = 'org.gnome.Podcasts@0@'.format(profile) application_id = 'org.gnome.Podcasts@0@'.format(profile)
i18n = import('i18n') i18n = import('i18n')
gnome = import('gnome') gnome = import('gnome')

View File

@ -1,7 +1,7 @@
{ {
"app-id" : "org.gnome.Podcasts.Devel", "app-id" : "org.gnome.Podcasts.Devel",
"runtime" : "org.gnome.Platform", "runtime" : "org.gnome.Platform",
"runtime-version" : "master", "runtime-version" : "3.36",
"sdk" : "org.gnome.Sdk", "sdk" : "org.gnome.Sdk",
"sdk-extensions" : [ "sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable" "org.freedesktop.Sdk.Extension.rust-stable"

View File

@ -1,7 +1,7 @@
{ {
"app-id" : "org.gnome.Podcasts", "app-id" : "org.gnome.Podcasts",
"runtime" : "org.gnome.Platform", "runtime" : "org.gnome.Platform",
"runtime-version" : "master", "runtime-version" : "3.36",
"sdk" : "org.gnome.Sdk", "sdk" : "org.gnome.Sdk",
"sdk-extensions" : [ "sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable" "org.freedesktop.Sdk.Extension.rust-stable"

View File

@ -5,39 +5,36 @@ version = "0.1.0"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
ammonia = "3.0.0" ammonia = "3.1.0"
chrono = "0.4.9" chrono = "0.4.11"
derive_builder = "0.8.0" derive_builder = "0.9.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.8" log = "0.4.8"
rayon = "1.2.0" rayon = "1.3.1"
rfc822_sanitizer = "0.3.3" rfc822_sanitizer = "0.3.3"
rss = "1.9.0" rss = "1.9.0"
url = "2.1.1" url = "2.1.1"
xdg = "2.2.0" xdg = "2.2.0"
xml-rs = "0.8.0" xml-rs = "0.8.3"
futures = "0.3.4" futures = "0.1.29"
hyper = "0.13.2" hyper = "0.12.35"
http = "0.2.0" http = "0.1.19"
hyper-tls = "0.4.1" tokio = "0.1.22"
hyper-tls = "0.3.2"
native-tls = "0.2.3" native-tls = "0.2.3"
num_cpus = "1.10.1" num_cpus = "1.13.0"
failure = "0.1.6" failure = "0.1.8"
failure_derive = "0.1.6" failure_derive = "0.1.8"
base64 = "0.10.1" base64 = "0.12.2"
[dependencies.diesel] [dependencies.diesel]
features = ["sqlite", "r2d2"] features = ["sqlite", "r2d2"]
version = "1.4.3" version = "1.4.5"
[dependencies.diesel_migrations] [dependencies.diesel_migrations]
features = ["sqlite"] features = ["sqlite"]
version = "1.4.0" version = "1.4.0"
[dependencies.tokio]
features = ["rt-core", "rt-threaded", "macros"]
version = "0.2.13"
[dev-dependencies] [dev-dependencies]
rand = "0.7.2" rand = "0.7.2"
tempdir = "0.3.7" tempdir = "0.3.7"

View File

@ -84,7 +84,10 @@ pub enum DataError {
FeedRedirect(Source), FeedRedirect(Source),
#[fail(display = "Feed is up to date")] #[fail(display = "Feed is up to date")]
FeedNotModified(Source), FeedNotModified(Source),
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)] #[fail(
display = "Error occurred while Parsing an Episode. Reason: {}",
reason
)]
ParseEpisodeError { reason: String, parent_id: i32 }, ParseEpisodeError { reason: String, parent_id: i32 },
#[fail(display = "Episode was not changed and thus skipped.")] #[fail(display = "Episode was not changed and thus skipped.")]
EpisodeNotChanged, EpisodeNotChanged,

View File

@ -21,6 +21,7 @@
#![allow(clippy::unit_arg)] #![allow(clippy::unit_arg)]
//! Index Feeds. //! Index Feeds.
use futures::future::*;
use futures::prelude::*; use futures::prelude::*;
use futures::stream; use futures::stream;
use rss; use rss;
@ -44,31 +45,31 @@ pub struct Feed {
impl Feed { impl Feed {
/// Index the contents of the RSS `Feed` into the database. /// Index the contents of the RSS `Feed` into the database.
pub async fn index(self) -> Result<(), DataError> { pub fn index(self) -> impl Future<Item = (), Error = DataError> + Send {
let show = self.parse_podcast().to_podcast()?; ok(self.parse_podcast())
self.index_channel_items(show).await .and_then(|pd| pd.to_podcast())
.and_then(move |pd| self.index_channel_items(pd))
} }
fn parse_podcast(&self) -> NewShow { fn parse_podcast(&self) -> NewShow {
NewShow::new(&self.channel, self.source_id) NewShow::new(&self.channel, self.source_id)
} }
async fn index_channel_items(self, pd: Show) -> Result<(), DataError> { fn index_channel_items(self, pd: Show) -> impl Future<Item = (), Error = DataError> + Send {
let stream = stream::iter(self.channel.into_items()); let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
// Parse the episodes // Parse the episodes
let episodes = stream.filter_map(move |item| { let episodes = stream.filter_map(move |item| {
let ret = NewEpisodeMinimal::new(&item, pd.id()) NewEpisodeMinimal::new(&item, pd.id())
.and_then(move |ep| determine_ep_state(ep, &item)); .and_then(move |ep| determine_ep_state(ep, &item))
if ret.is_ok() { .map_err(|err| error!("Failed to parse an episode: {}", err))
future::ready(Some(ret)) .ok()
} else {
future::ready(None)
}
}); });
// Filter errors, Index updatable episodes, return insertables. // Filter errors, Index updatable episodes, return insertables.
let insertable_episodes = filter_episodes(episodes).await?; filter_episodes(episodes)
batch_insert_episodes(&insertable_episodes); // Batch index insertable episodes.
Ok(()) .and_then(|eps| ok(batch_insert_episodes(&eps)))
} }
} }
@ -93,31 +94,28 @@ fn determine_ep_state(
} }
} }
async fn filter_episodes<'a, S>(stream: S) -> Result<Vec<NewEpisode>, DataError> fn filter_episodes<'a, S>(
stream: S,
) -> impl Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a
where where
S: Stream<Item = Result<IndexState<NewEpisode>, DataError>>, S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
{ {
stream stream
.try_filter_map(|state| { .filter_map(|state| match state {
async { IndexState::NotChanged => None,
let result = match state { // Update individual rows, and filter them
IndexState::NotChanged => None, IndexState::Update((ref ep, rowid)) => {
// Update individual rows, and filter them ep.update(rowid)
IndexState::Update((ref ep, rowid)) => { .map_err(|err| error!("{}", err))
ep.update(rowid) .map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.map_err(|err| error!("{}", err)) .ok();
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.ok(); None
None
}
IndexState::Index(s) => Some(s),
};
Ok(result)
} }
IndexState::Index(s) => Some(s),
}) })
// only Index is left, collect them for batch index // only Index is left, collect them for batch index
.try_collect() .collect()
.await
} }
fn batch_insert_episodes(episodes: &[NewEpisode]) { fn batch_insert_episodes(episodes: &[NewEpisode]) {
@ -144,9 +142,8 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use failure::Error; use failure::Error;
use futures::executor::block_on;
use rss::Channel; use rss::Channel;
use tokio; use tokio::{self, prelude::*};
use crate::database::truncate_db; use crate::database::truncate_db;
use crate::dbqueries; use crate::dbqueries;
@ -201,10 +198,9 @@ mod tests {
}) })
.collect(); .collect();
// Index the channes // Index the channels
let stream_ = stream::iter(feeds).for_each(|x| x.index().map(|x| x.unwrap())); let stream_ = stream::iter_ok(feeds).for_each(|x| x.index());
let mut rt = tokio::runtime::Runtime::new()?; tokio::run(stream_.map_err(|_| ()));
rt.block_on(stream_);
// Assert the index rows equal the controlled results // Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources()?.len(), 5); assert_eq!(dbqueries::get_sources()?.len(), 5);
@ -236,7 +232,7 @@ mod tests {
let feed = get_feed(path, 42); let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast()?; let pd = feed.parse_podcast().to_podcast()?;
block_on(feed.index_channel_items(pd))?; feed.index_channel_items(pd).wait()?;
assert_eq!(dbqueries::get_podcasts()?.len(), 1); assert_eq!(dbqueries::get_podcasts()?.len(), 1);
assert_eq!(dbqueries::get_episodes()?.len(), 43); assert_eq!(dbqueries::get_episodes()?.len(), 43);
Ok(()) Ok(())

View File

@ -111,7 +111,7 @@ pub use crate::models::{Episode, EpisodeWidgetModel, Show, ShowCoverModel, Sourc
/// It originates from the Tor-browser UA. /// It originates from the Tor-browser UA.
pub const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0"; pub const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths. /// [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub mod xdg_dirs { pub mod xdg_dirs {
use std::path::PathBuf; use std::path::PathBuf;
@ -137,7 +137,7 @@ pub mod xdg_dirs {
PODCASTS_XDG.create_cache_directory(PODCASTS_XDG.get_cache_home()).unwrap() PODCASTS_XDG.create_cache_directory(PODCASTS_XDG.get_cache_home()).unwrap()
}; };
/// GNOME Podcasts Download Direcotry `PathBuf`. /// GNOME Podcasts Download Directory `PathBuf`.
pub static ref DL_DIR: PathBuf = { pub static ref DL_DIR: PathBuf = {
PODCASTS_XDG.create_data_directory("Downloads").unwrap() PODCASTS_XDG.create_data_directory("Downloads").unwrap()
}; };

View File

@ -255,7 +255,7 @@ impl NewEpisodeMinimal {
return Err(err); return Err(err);
}; };
// Default to rfc2822 represantation of epoch 0. // Default to rfc2822 representation of epoch 0.
let date = parse_rfc822(item.pub_date().unwrap_or("Thu, 1 Jan 1970 00:00:00 +0000")); let date = parse_rfc822(item.pub_date().unwrap_or("Thu, 1 Jan 1970 00:00:00 +0000"));
// Should treat information from the rss feeds as invalid by default. // Should treat information from the rss feeds as invalid by default.
// Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday. // Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday.
@ -342,7 +342,7 @@ mod tests {
use std::io::BufReader; use std::io::BufReader;
// TODO: Add tests for other feeds too. // TODO: Add tests for other feeds too.
// Especially if you find an *intresting* generated feed. // Especially if you find an *interesting* generated feed.
// Known prebuilt expected objects. // Known prebuilt expected objects.
lazy_static! { lazy_static! {
@ -578,6 +578,10 @@ mod tests {
let ep = EXPECTED_MINIMAL_INTERCEPTED_1 let ep = EXPECTED_MINIMAL_INTERCEPTED_1
.clone() .clone()
.into_new_episode(&item); .into_new_episode(&item);
println!(
"EPISODE: {:#?}\nEXPECTED: {:#?}",
ep, *EXPECTED_INTERCEPTED_1
);
assert_eq!(ep, *EXPECTED_INTERCEPTED_1); assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
let item = channel.items().iter().nth(15).unwrap(); let item = channel.items().iter().nth(15).unwrap();

View File

@ -31,6 +31,9 @@ use http::header::{
USER_AGENT as USER_AGENT_HEADER, USER_AGENT as USER_AGENT_HEADER,
}; };
use http::{Request, Response, StatusCode, Uri}; use http::{Request, Response, StatusCode, Uri};
// use futures::future::ok;
use futures::future::{loop_fn, Future, Loop};
use futures::prelude::*;
use base64::{encode_config, URL_SAFE}; use base64::{encode_config, URL_SAFE};
@ -159,7 +162,7 @@ impl Source {
let code = res.status(); let code = res.status();
if code.is_success() { if code.is_success() {
// If request is successful save the etag // If request is succesful save the etag
self = self.update_etag(&res)? self = self.update_etag(&res)?
} else { } else {
match code.as_u16() { match code.as_u16() {
@ -189,7 +192,7 @@ impl Source {
return Err(DataError::FeedRedirect(self)); return Err(DataError::FeedRedirect(self));
} }
401 => return Err(self.make_err("401: Unauthorized.", code)), 401 => return Err(self.make_err("401: Unauthorized.", code)),
403 => return Err(self.make_err("403: Forbidden.", code)), 403 => return Err(self.make_err("403: Forbidden.", code)),
404 => return Err(self.make_err("404: Not found.", code)), 404 => return Err(self.make_err("404: Not found.", code)),
408 => return Err(self.make_err("408: Request Timeout.", code)), 408 => return Err(self.make_err("408: Request Timeout.", code)),
410 => return Err(self.make_err("410: Feed was deleted..", code)), 410 => return Err(self.make_err("410: Feed was deleted..", code)),
@ -213,7 +216,10 @@ impl Source {
self = self.save()?; self = self.save()?;
debug!("Updated Source: {:#?}", &self); debug!("Updated Source: {:#?}", &self);
info!("Feed url of Source {}, was updated succesfully.", self.id()); info!(
"Feed url of Source {}, was updated successfully.",
self.id()
);
} }
Ok(self) Ok(self)
@ -236,45 +242,41 @@ impl Source {
/// ///
/// Consumes `self` and Returns the corresponding `Feed` Object. /// Consumes `self` and Returns the corresponding `Feed` Object.
// Refactor into TryInto once it lands on stable. // Refactor into TryInto once it lands on stable.
pub async fn into_feed( pub fn into_feed(
self, self,
client: Client<HttpsConnector<HttpConnector>>, client: Client<HttpsConnector<HttpConnector>>,
) -> Result<Feed, DataError> { ) -> impl Future<Item = Feed, Error = DataError> {
let id = self.id(); let id = self.id();
let response = loop_fn(self, move |source| {
source
.request_constructor(&client.clone())
.then(|res| match res {
Ok(response) => Ok(Loop::Break(response)),
Err(err) => match err {
DataError::FeedRedirect(s) => {
info!("Following redirect...");
Ok(Loop::Continue(s))
}
e => Err(e),
},
})
});
let resp = self.get_response(&client).await?; response
let chan = response_to_channel(resp).await?; .and_then(response_to_channel)
.and_then(move |chan| {
FeedBuilder::default() FeedBuilder::default()
.channel(chan) .channel(chan)
.source_id(id) .source_id(id)
.build() .build()
.map_err(From::from) .map_err(From::from)
})
} }
async fn get_response( fn request_constructor(
self, self,
client: &Client<HttpsConnector<HttpConnector>>, client: &Client<HttpsConnector<HttpConnector>>,
) -> Result<Response<Body>, DataError> { ) -> impl Future<Item = Response<Body>, Error = DataError> {
let mut source = self;
loop {
match source.request_constructor(&client.clone()).await {
Ok(response) => return Ok(response),
Err(err) => match err {
DataError::FeedRedirect(s) => {
info!("Following redirect...");
source = s;
}
e => return Err(e),
},
}
}
}
async fn request_constructor(
self,
client: &Client<HttpsConnector<HttpConnector>>,
) -> Result<Response<Body>, DataError> {
// FIXME: remove unwrap somehow // FIXME: remove unwrap somehow
let uri = Uri::from_str(self.uri()).unwrap(); let uri = Uri::from_str(self.uri()).unwrap();
let mut req = Request::get(uri).body(Body::empty()).unwrap(); let mut req = Request::get(uri).body(Body::empty()).unwrap();
@ -305,22 +307,30 @@ impl Source {
.insert(IF_MODIFIED_SINCE, HeaderValue::from_str(lmod).unwrap()); .insert(IF_MODIFIED_SINCE, HeaderValue::from_str(lmod).unwrap());
} }
let res = client.request(req).await?; client
//.map_err(From::from) .request(req)
self.match_status(res) .map_err(From::from)
.and_then(move |res| self.match_status(res))
} }
} }
async fn response_to_channel(res: Response<Body>) -> Result<Channel, DataError> { fn response_to_channel(
let chunk = hyper::body::to_bytes(res.into_body()).await?; res: Response<Body>,
let buf = String::from_utf8_lossy(&chunk).into_owned(); ) -> impl Future<Item = Channel, Error = DataError> + Send {
Channel::from_str(&buf).map_err(From::from) res.into_body()
.concat2()
.map(|x| x.into_iter())
.map_err(From::from)
.map(|iter| iter.collect::<Vec<u8>>())
.map(|utf_8_bytes| String::from_utf8_lossy(&utf_8_bytes).into_owned())
.and_then(|buf| Channel::from_str(&buf).map_err(From::from))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use failure::Error; use failure::Error;
use num_cpus;
use tokio; use tokio;
use crate::database::truncate_db; use crate::database::truncate_db;
@ -331,7 +341,7 @@ mod tests {
truncate_db()?; truncate_db()?;
let mut rt = tokio::runtime::Runtime::new()?; let mut rt = tokio::runtime::Runtime::new()?;
let https = HttpsConnector::new(); let https = HttpsConnector::new(num_cpus::get())?;
let client = Client::builder().build::<_, Body>(https); let client = Client::builder().build::<_, Body>(https);
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\ let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\

View File

@ -43,7 +43,7 @@ use failure::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
// FIXME: Make it a Diesel model // FIXME: Make it a Diesel model
/// Represents an `outline` xml element as per the `OPML` [specification][spec] /// Represents an `outline` xml element as per the `OPML` [specification][spec]
/// not `RSS` related sub-elements are ommited. /// not `RSS` related sub-elements are omitted.
/// ///
/// [spec]: http://dev.opml.org/spec2.html /// [spec]: http://dev.opml.org/spec2.html
pub struct Opml { pub struct Opml {
@ -82,7 +82,7 @@ pub fn import_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<Source>, DataErro
import_to_db(content.as_slice()).map_err(From::from) import_to_db(content.as_slice()).map_err(From::from)
} }
/// Export a file to `P`, taking the feeds from the database and outputing /// Export a file to `P`, taking the feeds from the database and outputting
/// them in opml format. /// them in opml format.
pub fn export_from_db<P: AsRef<Path>>(path: P, export_title: &str) -> Result<(), Error> { pub fn export_from_db<P: AsRef<Path>>(path: P, export_title: &str) -> Result<(), Error> {
let file = File::create(path)?; let file = File::create(path)?;
@ -163,7 +163,7 @@ pub fn export_to_file<F: Write>(file: F, export_title: &str) -> Result<(), Error
Ok(()) Ok(())
} }
/// Extracts the `outline` elemnts from a reader `R` and returns a `HashSet` of `Opml` structs. /// Extracts the `outline` elements from a reader `R` and returns a `HashSet` of `Opml` structs.
pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Error> { pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Error> {
let mut list = HashSet::new(); let mut list = HashSet::new();
let parser = reader::EventReader::new(reader); let parser = reader::EventReader::new(reader);
@ -210,7 +210,7 @@ mod tests {
use super::*; use super::*;
use chrono::Local; use chrono::Local;
use failure::Error; use failure::Error;
use futures::executor::block_on; use futures::Future;
use crate::database::{truncate_db, TEMPDIR}; use crate::database::{truncate_db, TEMPDIR};
use crate::utils::get_feed; use crate::utils::get_feed;
@ -318,7 +318,7 @@ mod tests {
// Create and insert a Source into db // Create and insert a Source into db
let s = Source::from_url(url).unwrap(); let s = Source::from_url(url).unwrap();
let feed = get_feed(path, s.id()); let feed = get_feed(path, s.id());
block_on(feed.index()).unwrap(); feed.index().wait().unwrap();
}); });
let mut map: HashSet<Opml> = HashSet::new(); let mut map: HashSet<Opml> = HashSet::new();

View File

@ -20,13 +20,15 @@
// FIXME: // FIXME:
//! Docs. //! Docs.
use futures::{future::ok, prelude::*, stream::FuturesUnordered}; use futures::{future::ok, lazy, prelude::*, stream::FuturesUnordered};
use tokio; use tokio;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::{Body, Client}; use hyper::{Body, Client};
use hyper_tls::HttpsConnector; use hyper_tls::HttpsConnector;
use num_cpus;
use crate::errors::DataError; use crate::errors::DataError;
use crate::Source; use crate::Source;
@ -40,24 +42,29 @@ type HttpsClient = Client<HttpsConnector<HttpConnector>>;
/// Messy temp diagram: /// Messy temp diagram:
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` -> /// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` ->
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes. /// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
#[tokio::main] pub fn pipeline<'a, S>(sources: S, client: HttpsClient) -> impl Future<Item = (), Error = ()> + 'a
pub async fn pipeline<'a, S>(mut sources: S, client: HttpsClient)
where where
S: Stream<Item = Result<Source, DataError>> + Send + 'a + std::marker::Unpin, S: Stream<Item = Source, Error = DataError> + Send + 'a,
{ {
while let Some(source_result) = sources.next().await { sources
if let Ok(source) = source_result { .and_then(move |s| s.into_feed(client.clone()))
match source.into_feed(client.clone()).await { .map_err(|err| {
Ok(feed) => { match err {
let fut = feed.index().map_err(|err| error!("Error: {}", err)); // Avoid spamming the stderr when its not an eactual error
tokio::spawn(fut); DataError::FeedNotModified(_) => (),
} _ => error!("Error: {}", err),
// Avoid spamming the stderr when it's not an actual error }
Err(DataError::FeedNotModified(_)) => (), })
Err(err) => error!("Error: {}", err), .and_then(move |feed| {
}; let fut = lazy(|| feed.index().map_err(|err| error!("Error: {}", err)));
} tokio::spawn(fut);
} Ok(())
})
// For each terminates the stream at the first error so we make sure
// we pass good values regardless
.then(move |_| ok(()))
// Convert the stream into a Future to later execute as a tokio task
.for_each(move |_| ok(()))
} }
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and /// Creates a tokio `reactor::Core`, and a `hyper::Client` and
@ -66,12 +73,13 @@ pub fn run<S>(sources: S) -> Result<(), DataError>
where where
S: IntoIterator<Item = Source>, S: IntoIterator<Item = Source>,
{ {
let https = HttpsConnector::new(); let https = HttpsConnector::new(num_cpus::get())?;
let client = Client::builder().build::<_, Body>(https); let client = Client::builder().build::<_, Body>(https);
let foo = sources.into_iter().map(ok::<_, _>); let foo = sources.into_iter().map(ok::<_, _>);
let stream = FuturesUnordered::from_iter(foo); let stream = FuturesUnordered::from_iter(foo);
pipeline(stream, client); let p = pipeline(stream, client);
tokio::run(p);
Ok(()) Ok(())
} }
@ -113,7 +121,7 @@ mod tests {
run(sources)?; run(sources)?;
let sources = dbqueries::get_sources()?; let sources = dbqueries::get_sources()?;
// Run again to cover Unique constrains errors. // Run again to cover Unique constrains erros.
run(sources)?; run(sources)?;
// Assert the index rows equal the controlled results // Assert the index rows equal the controlled results

View File

@ -56,7 +56,7 @@ fn download_checker() -> Result<(), DataError> {
Ok(()) Ok(())
} }
/// Delete watched `episodes` that have exceded their liftime after played. /// Delete watched `episodes` that have exceeded their lifetime after played.
fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> { fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
let mut episodes = dbqueries::get_played_cleaner_episodes()?; let mut episodes = dbqueries::get_played_cleaner_episodes()?;
let now_utc = cleanup_date.timestamp() as i32; let now_utc = cleanup_date.timestamp() as i32;
@ -68,7 +68,7 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
let limit = ep.played().unwrap(); let limit = ep.played().unwrap();
if now_utc > limit { if now_utc > limit {
delete_local_content(ep) delete_local_content(ep)
.map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri())) .map(|_| info!("Episode {:?} was deleted successfully.", ep.local_uri()))
.map_err(|err| error!("Error: {}", err)) .map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri())) .map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri()))
.ok(); .ok();
@ -144,11 +144,11 @@ pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
// TODO: Write Tests // TODO: Write Tests
pub fn delete_show(pd: &Show) -> Result<(), DataError> { pub fn delete_show(pd: &Show) -> Result<(), DataError> {
dbqueries::remove_feed(pd)?; dbqueries::remove_feed(pd)?;
info!("{} was removed succesfully.", pd.title()); info!("{} was removed successfully.", pd.title());
let fold = get_download_folder(pd.title())?; let fold = get_download_folder(pd.title())?;
fs::remove_dir_all(&fold)?; fs::remove_dir_all(&fold)?;
info!("All the content at, {} was removed succesfully", &fold); info!("All the content at, {} was removed successfully", &fold);
Ok(()) Ok(())
} }

View File

@ -5,14 +5,13 @@ version = "0.1.0"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
error-chain = "0.12.1"
log = "0.4.8" log = "0.4.8"
mime_guess = "2.0.1" mime_guess = "2.0.3"
reqwest = "0.9.22" reqwest = "0.9.22"
tempdir = "0.3.7" tempdir = "0.3.7"
glob = "0.3.0" glob = "0.3.0"
failure = "0.1.6" failure = "0.1.8"
failure_derive = "0.1.6" failure_derive = "0.1.8"
[dependencies.podcasts-data] [dependencies.podcasts-data]
path = "../podcasts-data" path = "../podcasts-data"

View File

@ -54,7 +54,7 @@ pub trait DownloadProgress {
// Sorry to those who will have to work with that code. // Sorry to those who will have to work with that code.
// Would much rather use a crate, // Would much rather use a crate,
// 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 can't seem to find one.
// TODO: Write unit-tests. // TODO: Write unit-tests.
fn download_into( fn download_into(
dir: &str, dir: &str,
@ -64,7 +64,7 @@ fn download_into(
) -> Result<String, DownloadError> { ) -> Result<String, DownloadError> {
info!("GET request to: {}", url); info!("GET request to: {}", url);
// Haven't included the loop check as // Haven't included the loop check as
// Steal the Stars would tigger it as // Steal the Stars would trigger it as
// it has a loop back before giving correct url // it has a loop back before giving correct url
let policy = RedirectPolicy::custom(|attempt| { let policy = RedirectPolicy::custom(|attempt| {
info!("Redirect Attempt URL: {:?}", attempt.url()); info!("Redirect Attempt URL: {:?}", attempt.url());
@ -104,7 +104,7 @@ fn download_into(
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.map(From::from); .map(From::from);
ct_len.map(|x| info!("File Lenght: {}", x)); ct_len.map(|x| info!("File Length: {}", x));
ct_type.map(|x| info!("Content Type: {}", x)); ct_type.map(|x| info!("Content Type: {}", x));
let ext = get_ext(ct_type).unwrap_or_else(|| String::from("unknown")); let ext = get_ext(ct_type).unwrap_or_else(|| String::from("unknown"));
@ -131,7 +131,7 @@ fn download_into(
let target = format!("{}/{}.{}", dir, file_title, ext); let target = format!("{}/{}.{}", dir, file_title, ext);
// Rename/move the tempfile into a permanent place upon success. // Rename/move the tempfile into a permanent place upon success.
rename(out_file, &target)?; rename(out_file, &target)?;
info!("Downloading of {} completed succesfully.", &target); info!("Downloading of {} completed successfully.", &target);
Ok(target) Ok(target)
} }
@ -219,10 +219,10 @@ pub fn get_episode(
progress, progress,
)?; )?;
// If download succedes set episode local_uri to dlpath. // If download succeeds set episode local_uri to dlpath.
ep.set_local_uri(Some(&path)); ep.set_local_uri(Some(&path));
// Over-write episode lenght // Over-write episode length
let size = fs::metadata(path); let size = fs::metadata(path);
if let Ok(s) = size { if let Ok(s) = size {
ep.set_length(Some(s.len() as i32)) ep.set_length(Some(s.len() as i32))

View File

@ -5,47 +5,44 @@ version = "0.1.0"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
chrono = "0.4.9" chrono = "0.4.11"
crossbeam-channel = "0.3.9" crossbeam-channel = "0.3.9"
gdk = "0.12.0" gdk = "0.12.1"
gdk-pixbuf = "0.8.0" gdk-pixbuf = "0.8.0"
gobject-sys = "0.9.1" gobject-sys = "0.9.1"
glib-sys = "0.9.1" glib-sys = "0.9.1"
gst = { version = "0.15.2", package = "gstreamer" } gst = { version = "0.15.7", package = "gstreamer" }
gst-player = { version = "0.15.0", package = "gstreamer-player" } gst-player = { version = "0.15.5", package = "gstreamer-player" }
humansize = "1.1.0" humansize = "1.1.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.8" log = "0.4.8"
loggerv = "0.7.2" loggerv = "0.7.2"
open = "1.3.2" open = "1.4.0"
rayon = "1.2.0" rayon = "1.3.1"
url = "2.1.0" url = "2.1.0"
failure = "0.1.6" failure = "0.1.8"
failure_derive = "0.1.6" failure_derive = "0.1.8"
fragile = "0.3.0" fragile = "1.0.0"
regex = "1.3.1" regex = "1.3.9"
reqwest = "0.9.22" reqwest = "0.9.22"
serde_json = "1.0.41" serde_json = "1.0.55"
# html2text = "0.1.8" html2text = "0.1.12"
html2text = { git = "https://github.com/jugglerchris/rust-html2text" }
mpris-player = "0.5.0" mpris-player = "0.5.0"
pango = "0.8.0" pango = "0.8.0"
glib = "0.9.3"
[dependencies.gettext-rs] [dependencies.gettext-rs]
git = "https://github.com/danigm/gettext-rs" git = "https://github.com/danigm/gettext-rs"
branch = "no-gettext" branch = "no-gettext"
features = ["gettext-system"] features = ["gettext-system"]
[dependencies.glib]
version = "0.9.1"
[dependencies.gio] [dependencies.gio]
features = ["v2_50"] features = ["v2_50"]
version = "0.8.0" version = "0.8.1"
[dependencies.gtk] [dependencies.gtk]
features = ["v3_24"] features = ["v3_24"]
version = "0.8.0" version = "0.8.1"
[dependencies.libhandy] [dependencies.libhandy]
version = "0.5.0" version = "0.5.0"

View File

@ -2,22 +2,23 @@
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER # Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
# This file is distributed under the same license as the podcasts package. # This file is distributed under the same license as the podcasts package.
# arverne73 <arverne@wanadoo.fr>, 2018. # arverne73 <arverne@wanadoo.fr>, 2018.
# Alexandre Franke <alexandre.franke@gmail.com>, 2018 # Alexandre Franke <alexandre.franke@gmail.com>, 2018, 2020
# Thibault Martin <mail@thibaultmart.in>, 2020.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: podcasts master\n" "Project-Id-Version: podcasts master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n" "Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
"POT-Creation-Date: 2018-10-23 10:23+0000\n" "POT-Creation-Date: 2020-06-03 20:13+0000\n"
"PO-Revision-Date: 2018-10-29 13:53+0100\n" "PO-Revision-Date: 2020-06-19 17:15+0200\n"
"Last-Translator: Alexandre Franke <alexandre.franke@gmail.com>\n" "Last-Translator: Thibault Martin <mail@thibaultmart.in>\n"
"Language-Team: French <gnomefr@traduc.org>\n" "Language-Team: French <gnomefr@traduc.org>\n"
"Language: fr\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n"
"X-Generator: Poedit 2.0.6\n" "X-Generator: Gtranslator 3.36.0\n"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15 #: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
msgid "Top position of the last open main window" msgid "Top position of the last open main window"
@ -49,8 +50,7 @@ msgstr "Indique sil faut actualiser périodiquement le contenu"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46 #: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
msgid "How many periods of time to wait between automatic refreshes" msgid "How many periods of time to wait between automatic refreshes"
msgstr "" msgstr "Nombre de délais à attendre entre les actualisations automatiques"
"Nombre de périodes de délais à attendre entre les actualisations automatiques"
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50 #: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
msgid "What period of time to wait between automatic refreshes" msgid "What period of time to wait between automatic refreshes"
@ -71,8 +71,8 @@ msgstr "Délai entre les nettoyages automatiques"
#. Weird magic I copy-pasted that sets the Application Name in the Shell. #. Weird magic I copy-pasted that sets the Application Name in the Shell.
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:3 #: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:3
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:4 #: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:4
#: podcasts-gtk/src/app.rs:92 podcasts-gtk/src/app.rs:416 #: podcasts-gtk/resources/gtk/headerbar.ui:158 podcasts-gtk/src/app.rs:353
#: podcasts-gtk/src/widgets/aboutdialog.rs:37 #: podcasts-gtk/src/widgets/aboutdialog.rs:56 podcasts-gtk/src/window.rs:82
msgid "Podcasts" msgid "Podcasts"
msgstr "Podcasts" msgstr "Podcasts"
@ -81,11 +81,6 @@ msgstr "Podcasts"
msgid "Listen to your favorite podcasts, right from your desktop." msgid "Listen to your favorite podcasts, right from your desktop."
msgstr "Écouter vos podcasts favoris directement sur votre bureau." msgstr "Écouter vos podcasts favoris directement sur votre bureau."
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:6
msgid "@icon@"
msgstr "@icon@"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:13 #: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:13
msgid "Podcast;RSS;" msgid "Podcast;RSS;"
@ -95,13 +90,13 @@ msgstr "Podcast;RSS;Baladodiffusion;Émissions;"
msgid "Podcast app for GNOME" msgid "Podcast app for GNOME"
msgstr "Application de podcast pour GNOME" msgstr "Application de podcast pour GNOME"
#: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:68 #: podcasts-gtk/resources/org.gnome.Podcasts.appdata.xml.in.in:84
msgid "Jordan Petridis" msgid "Jordan Petridis"
msgstr "Jordan Petridis" msgstr "Jordan Petridis"
#: podcasts-gtk/resources/gtk/empty_view.ui:46 #: podcasts-gtk/resources/gtk/empty_view.ui:46
msgid "This show does not have episodes yet" msgid "This show does not have episodes yet"
msgstr "Cette émission na pas encore dépisodes" msgstr "Cette émission na pas encore dépisode"
#: podcasts-gtk/resources/gtk/empty_view.ui:62 #: podcasts-gtk/resources/gtk/empty_view.ui:62
msgid "If you think this is an error, please consider writing a bug report." msgid "If you think this is an error, please consider writing a bug report."
@ -119,19 +114,23 @@ msgstr "Ajouter de nouvelles émissions via une URL de flux"
msgid "Import shows from another device" msgid "Import shows from another device"
msgstr "Importer des émissions à partir dun autre appareil" msgstr "Importer des émissions à partir dun autre appareil"
#: podcasts-gtk/resources/gtk/episode_widget.ui:180 #: podcasts-gtk/resources/gtk/episode_widget.ui:79
msgid "Youve already listened to this episode."
msgstr "Vous avez déjà écouté cet épisode."
#: podcasts-gtk/resources/gtk/episode_widget.ui:208
msgid "Calculating episode size…" msgid "Calculating episode size…"
msgstr "Calcul de la taille de lépisode…" msgstr "Calcul de la taille de lépisode…"
#: podcasts-gtk/resources/gtk/episode_widget.ui:220 #: podcasts-gtk/resources/gtk/episode_widget.ui:248
msgid "Play this episode" msgid "Play this episode"
msgstr "Lire cet épisode" msgstr "Lire cet épisode"
#: podcasts-gtk/resources/gtk/episode_widget.ui:241 #: podcasts-gtk/resources/gtk/episode_widget.ui:269
msgid "Cancel the download process" msgid "Cancel the download process"
msgstr "Annuler le téléchargement" msgstr "Annuler le téléchargement"
#: podcasts-gtk/resources/gtk/episode_widget.ui:264 #: podcasts-gtk/resources/gtk/episode_widget.ui:292
msgid "Download this episode" msgid "Download this episode"
msgstr "Télécharger cet épisode" msgstr "Télécharger cet épisode"
@ -143,20 +142,20 @@ msgstr "_Chercher de nouveaux épisodes"
msgid "_Import Shows" msgid "_Import Shows"
msgstr "_Importer les émissions" msgstr "_Importer les émissions"
#: podcasts-gtk/resources/gtk/hamburger.ui:22 #: podcasts-gtk/resources/gtk/hamburger.ui:16
msgid "_Preferences" msgid "_Export Shows"
msgstr "_Préférences" msgstr "_Exporter les émissions"
#: podcasts-gtk/resources/gtk/hamburger.ui:27 #: podcasts-gtk/resources/gtk/hamburger.ui:22
msgid "_Keyboard Shortcuts" msgid "_Keyboard Shortcuts"
msgstr "_Raccourcis clavier" msgstr "_Raccourcis clavier"
#: podcasts-gtk/resources/gtk/hamburger.ui:35 #: podcasts-gtk/resources/gtk/hamburger.ui:30
msgid "_About Podcasts" msgid "_About Podcasts"
msgstr "À _propos de Podcasts" msgstr "À _propos de Podcasts"
#: podcasts-gtk/resources/gtk/headerbar.ui:35 #: podcasts-gtk/resources/gtk/headerbar.ui:35
#: podcasts-gtk/resources/gtk/headerbar.ui:189 #: podcasts-gtk/resources/gtk/headerbar.ui:186
msgid "Add a new feed" msgid "Add a new feed"
msgstr "Ajouter un nouveau flux" msgstr "Ajouter un nouveau flux"
@ -168,15 +167,11 @@ msgstr "Entrer ladresse du flux à ajouter"
msgid "Add" msgid "Add"
msgstr "Ajouter" msgstr "Ajouter"
#: podcasts-gtk/resources/gtk/headerbar.ui:133 #: podcasts-gtk/resources/gtk/headerbar.ui:171
msgid "You are already subscribed to that feed!"
msgstr "Vous êtes déjà abonné à ce flux !"
#: podcasts-gtk/resources/gtk/headerbar.ui:169
msgid "Show Title" msgid "Show Title"
msgstr "Titre de lémission" msgstr "Titre de lémission"
#: podcasts-gtk/resources/gtk/headerbar.ui:210 #: podcasts-gtk/resources/gtk/headerbar.ui:207
msgid "Back" msgid "Back"
msgstr "Retour" msgstr "Retour"
@ -191,11 +186,6 @@ msgstr "Chercher de nouveaux épisodes"
#: podcasts-gtk/resources/gtk/help-overlay.ui:25 #: podcasts-gtk/resources/gtk/help-overlay.ui:25
msgctxt "shortcut window" msgctxt "shortcut window"
msgid "Preferences"
msgstr "Préférences"
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
msgctxt "shortcut window"
msgid "Quit the application" msgid "Quit the application"
msgstr "Quitter lapplication" msgstr "Quitter lapplication"
@ -227,71 +217,54 @@ msgstr "Une notification daction intégrée à lapplication"
msgid "Undo" msgid "Undo"
msgstr "Annuler" msgstr "Annuler"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72 #: podcasts-gtk/resources/gtk/player_dialog.ui:14
msgid "Rewind 10 seconds" msgid "Now Playing"
msgstr "Reculer de 10 secondes" msgstr "Lecture en cours"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87 #: podcasts-gtk/resources/gtk/player_rate.ui:32
msgid "Play"
msgstr "Lire"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:103
msgid "Pause"
msgstr "Pause"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:119
msgid "Fast forward 10 seconds"
msgstr "Avancer de 10 secondes"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:285
msgid "Change the playback speed" msgid "Change the playback speed"
msgstr "Changer la vitesse de lecture" msgstr "Changer la vitesse de lecture"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300 #: podcasts-gtk/resources/gtk/player_rate.ui:47
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380 #: podcasts-gtk/resources/gtk/player_rate.ui:122
msgid "1.00×" msgid "1.00×"
msgstr "1,00×" msgstr "1,00×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344 #: podcasts-gtk/resources/gtk/player_rate.ui:86
msgid "1.50×" msgid "1.50×"
msgstr "1,50×" msgstr "1,50×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:348 #: podcasts-gtk/resources/gtk/player_rate.ui:90
msgid "1.5 speed rate" msgid "1.5 speed rate"
msgstr "Débit × 1,5" msgstr "Débit × 1,5"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362 #: podcasts-gtk/resources/gtk/player_rate.ui:104
msgid "1.25×" msgid "1.25×"
msgstr "1,25×" msgstr "1,25×"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:366 #: podcasts-gtk/resources/gtk/player_rate.ui:108
msgid "1.25 speed rate" msgid "1.25 speed rate"
msgstr "Débit × 1,25" msgstr "Débit × 1,25"
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384 #: podcasts-gtk/resources/gtk/player_rate.ui:126
msgid "Normal speed" msgid "Normal speed"
msgstr "Vitesse normale" msgstr "Vitesse normale"
#: podcasts-gtk/resources/gtk/prefs.ui:42 #: podcasts-gtk/resources/gtk/player_toolbar.ui:97
#: podcasts-gtk/resources/gtk/prefs.ui:295 msgid "Rewind 10 seconds"
msgid "Preferences" msgstr "Reculer de 10 secondes"
msgstr "Préférences"
#: podcasts-gtk/resources/gtk/prefs.ui:76 #: podcasts-gtk/resources/gtk/player_toolbar.ui:112
msgid "Appearance" msgid "Play"
msgstr "Apparence" msgstr "Lire"
#: podcasts-gtk/resources/gtk/prefs.ui:120 #: podcasts-gtk/resources/gtk/player_toolbar.ui:128
msgid "Dark Theme" msgid "Pause"
msgstr "Thème sombre" msgstr "Pause"
#: podcasts-gtk/resources/gtk/prefs.ui:166 #: podcasts-gtk/resources/gtk/player_toolbar.ui:144
msgid "Delete played episodes" msgid "Fast forward 10 seconds"
msgstr "Supprimer les épisodes lus" msgstr "Avancer de 10 secondes"
#: podcasts-gtk/resources/gtk/prefs.ui:211
msgid "After"
msgstr "Après"
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7 #: podcasts-gtk/resources/gtk/secondary_menu.ui:7
msgid "_Mark All Episodes as Played" msgid "_Mark All Episodes as Played"
@ -317,91 +290,135 @@ msgstr "Marquer tout comme lu"
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "Se désabonner" msgstr "Se désabonner"
#: podcasts-gtk/src/app.rs:333 #: podcasts-gtk/resources/gtk/show_widget.ui:99
msgid "Read More"
msgstr "En savoir plus"
#: podcasts-gtk/src/app.rs:287
msgid "Fetching new episodes" msgid "Fetching new episodes"
msgstr "Récupérer les nouveaux épisodes" msgstr "Récupérer les nouveaux épisodes"
#: podcasts-gtk/src/headerbar.rs:98 #: podcasts-gtk/src/stacks/content.rs:54
msgid "You are already subscribed to this show"
msgstr "Vous êtes déjà abonné à cette émission"
#: podcasts-gtk/src/headerbar.rs:106
msgid "Invalid URL"
msgstr "URL non valide"
#: podcasts-gtk/src/prefs.rs:59
msgid "Seconds"
msgstr "Secondes"
#: podcasts-gtk/src/prefs.rs:60
msgid "Minutes"
msgstr "Minutes"
#: podcasts-gtk/src/prefs.rs:61
msgid "Hours"
msgstr "Heures"
#: podcasts-gtk/src/prefs.rs:62
msgid "Days"
msgstr "Jours"
#: podcasts-gtk/src/prefs.rs:63
msgid "Weeks"
msgstr "Semaines"
#: podcasts-gtk/src/stacks/content.rs:35
msgid "New" msgid "New"
msgstr "Nouveau" msgstr "Nouveau"
#: podcasts-gtk/src/stacks/content.rs:36 #: podcasts-gtk/src/stacks/content.rs:55
msgid "Shows" msgid "Shows"
msgstr "Émissions" msgstr "Émissions"
#: podcasts-gtk/src/utils.rs:357 #: podcasts-gtk/src/utils.rs:398
msgid "Select the file from which to you want to import shows." msgid "Select the file from which to you want to import shows."
msgstr "Sélectionnez le fichier à partir duquel importer les émissions." msgstr "Sélectionnez le fichier à partir duquel importer les émissions."
#: podcasts-gtk/src/utils.rs:360 #: podcasts-gtk/src/utils.rs:401
msgid "_Import" msgid "_Import"
msgstr "_Importer" msgstr "_Importer"
#: podcasts-gtk/src/utils.rs:369 #: podcasts-gtk/src/utils.rs:410 podcasts-gtk/src/utils.rs:457
msgid "OPML file" msgid "OPML file"
msgstr "Fichier OPML" msgstr "Fichier OPML"
#: podcasts-gtk/src/utils.rs:386 #: podcasts-gtk/src/utils.rs:427
msgid "Failed to parse the imported file" msgid "Failed to parse the imported file"
msgstr "Échec de lanalyse du fichier importé" msgstr "Échec de lanalyse du fichier importé"
#: podcasts-gtk/src/utils.rs:391 #: podcasts-gtk/src/utils.rs:432 podcasts-gtk/src/utils.rs:475
msgid "Selected file could not be accessed." msgid "Selected file could not be accessed."
msgstr "Le fichier sélectionné nest pas accessible." msgstr "Le fichier sélectionné nest pas accessible."
#: podcasts-gtk/src/widgets/aboutdialog.rs:32 #: podcasts-gtk/src/utils.rs:445
msgid "Export shows to…"
msgstr "Exporter les émissions vers…"
#: podcasts-gtk/src/utils.rs:448
msgid "_Export"
msgstr "_Exporter"
#: podcasts-gtk/src/utils.rs:449
msgid "_Cancel"
msgstr "A_nnuler"
#: podcasts-gtk/src/utils.rs:469
msgid "GNOME Podcasts Subscriptions"
msgstr "Abonnements GNOME Podcasts"
#: podcasts-gtk/src/utils.rs:470
msgid "Failed to export podcasts"
msgstr "Échec de lexportation des podcasts"
#: podcasts-gtk/src/widgets/aboutdialog.rs:51
msgid "Podcast Client for the GNOME Desktop." msgid "Podcast Client for the GNOME Desktop."
msgstr "Client de podcast pour le bureau GNOME." msgstr "Client de podcast pour le bureau GNOME."
#: podcasts-gtk/src/widgets/aboutdialog.rs:39 #: podcasts-gtk/src/widgets/aboutdialog.rs:58
msgid "Learn more about GNOME Podcasts" msgid "Learn more about GNOME Podcasts"
msgstr "En apprendre plus sur GNOME Podcasts" msgstr "En apprendre plus sur GNOME Podcasts"
#: podcasts-gtk/src/widgets/aboutdialog.rs:44 #: podcasts-gtk/src/widgets/aboutdialog.rs:63
msgid "translator-credits" msgid "translator-credits"
msgstr "Alexandre Franke" msgstr ""
"Alexandre Franke\n"
"Thibault Martin"
#: podcasts-gtk/src/widgets/episode.rs:130 #: podcasts-gtk/src/widgets/episode.rs:149
msgid "{} min" msgid "{} min"
msgstr "{} min" msgstr "{} min"
#. sender.send(Action::ErrorNotification(format!("Player Error: {}", error))); #. sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
#: podcasts-gtk/src/widgets/player.rs:365 #: podcasts-gtk/src/widgets/player.rs:828
msgid "The media player was unable to execute an action." msgid "The media player was unable to execute an action."
msgstr "Le lecteur na pas pu réaliser laction." msgstr "Le lecteur na pas pu réaliser laction."
#: podcasts-gtk/src/widgets/show_menu.rs:150 #: podcasts-gtk/src/widgets/show_menu.rs:174
msgid "Marked all episodes as listened" msgid "Marked all episodes as listened"
msgstr "Marquer tous les épisodes comme écoutés" msgstr "Marquer tous les épisodes comme écoutés"
#: podcasts-gtk/src/widgets/show_menu.rs:155 #: podcasts-gtk/src/widgets/show_menu.rs:179
msgid "Unsubscribed from {}" msgid "Unsubscribed from {}"
msgstr "Se désabonner de {}" msgstr "Se désabonner de {}"
#~ msgid "@icon@"
#~ msgstr "@icon@"
#~ msgid "_Preferences"
#~ msgstr "_Préférences"
#~ msgid "You are already subscribed to that feed!"
#~ msgstr "Vous êtes déjà abonné à ce flux !"
#~ msgctxt "shortcut window"
#~ msgid "Preferences"
#~ msgstr "Préférences"
#~ msgid "Preferences"
#~ msgstr "Préférences"
#~ msgid "Appearance"
#~ msgstr "Apparence"
#~ msgid "Dark Theme"
#~ msgstr "Thème sombre"
#~ msgid "Delete played episodes"
#~ msgstr "Supprimer les épisodes lus"
#~ msgid "After"
#~ msgstr "Après"
#~ msgid "Invalid URL"
#~ msgstr "URL non valide"
#~ msgid "Seconds"
#~ msgstr "Secondes"
#~ msgid "Minutes"
#~ msgstr "Minutes"
#~ msgid "Hours"
#~ msgstr "Heures"
#~ msgid "Days"
#~ msgstr "Jours"
#~ msgid "Weeks"
#~ msgstr "Semaines"

View File

@ -110,6 +110,9 @@ Tobias Bernard
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
</style> </style>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -143,6 +146,9 @@ Tobias Bernard
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
</style> </style>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -177,6 +183,9 @@ Tobias Bernard
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
</style> </style>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -212,6 +221,9 @@ Tobias Bernard
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
</style> </style>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -132,6 +132,9 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="width_chars">5</property> <property name="width_chars">5</property>
<property name="xalign">1</property> <property name="xalign">1</property>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
<class name="small-label"/> <class name="small-label"/>
@ -154,6 +157,9 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="width_chars">5</property> <property name="width_chars">5</property>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
<style> <style>
<class name="dim-label"/> <class name="dim-label"/>
<class name="small-label"/> <class name="small-label"/>

View File

@ -45,6 +45,9 @@ Tobias Bernard
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">1.00×</property> <property name="label" translatable="yes">1.00×</property>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -75,23 +78,15 @@ Tobias Bernard
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_start">6</property> <property name="margin">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">3</property>
<child> <child>
<object class="GtkRadioButton" id="rate_3_00"> <object class="GtkModelButton" id="rate_4_00">
<property name="label" translatable="yes">3.00×</property> <property name="text">&lt;span font-features="tnum=1"&gt;4.00×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">4.0 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">3.0 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -100,16 +95,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_2_75"> <object class="GtkModelButton" id="rate_3_75">
<property name="label" translatable="yes">2.75×</property> <property name="text">&lt;span font-features="tnum=1"&gt;3.75×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">3.75 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">2.75 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -118,16 +109,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_2_50"> <object class="GtkModelButton" id="rate_3_50">
<property name="label" translatable="yes">2.50×</property> <property name="text">&lt;span font-features="tnum=1"&gt;3.50×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">3.5 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">2.5 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -136,16 +123,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_2_25"> <object class="GtkModelButton" id="rate_3_25">
<property name="label" translatable="yes">2.25×</property> <property name="text">&lt;span font-features="tnum=1"&gt;3.25×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">3.25 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">2.25 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -154,16 +137,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_2_00"> <object class="GtkModelButton" id="rate_3_00">
<property name="label" translatable="yes">2.00×</property> <property name="text">&lt;span font-features="tnum=1"&gt;3.00×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">3.0 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">2.0 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -172,16 +151,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_1_75"> <object class="GtkModelButton" id="rate_2_75">
<property name="label" translatable="yes">1.75×</property> <property name="text">&lt;span font-features="tnum=1"&gt;2.75×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">2.75 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">1.75 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -190,16 +165,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_1_50"> <object class="GtkModelButton" id="rate_2_50">
<property name="label" translatable="yes">1.50×</property> <property name="text">&lt;span font-features="tnum=1"&gt;2.50×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">2.5 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">1.5 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -208,16 +179,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="rate_1_25"> <object class="GtkModelButton" id="rate_2_25">
<property name="label" translatable="yes">1.25×</property> <property name="text">&lt;span font-features="tnum=1"&gt;2.25×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">2.25 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">1.25 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -226,16 +193,12 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="normal_rate"> <object class="GtkModelButton" id="rate_2_00">
<property name="label" translatable="yes">1.00×</property> <property name="text">&lt;span font-features="tnum=1"&gt;2.00×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">2.0 speed rate</property>
<property name="receives_default">False</property> <property name="role">radio</property>
<property name="tooltip_text" translatable="yes">Normal speed</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -243,6 +206,63 @@ Tobias Bernard
<property name="position">8</property> <property name="position">8</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkModelButton" id="rate_1_75">
<property name="text">&lt;span font-features="tnum=1"&gt;1.75×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property>
<property name="tooltip_text" translatable="yes">1.75 speed rate</property>
<property name="role">radio</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="rate_1_50">
<property name="text">&lt;span font-features="tnum=1"&gt;1.50×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property>
<property name="tooltip_text" translatable="yes">1.5 speed rate</property>
<property name="role">radio</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">10</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="rate_1_25">
<property name="text">&lt;span font-features="tnum=1"&gt;1.25×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property>
<property name="tooltip_text" translatable="yes">1.25 speed rate</property>
<property name="role">radio</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="normal_rate">
<property name="text">&lt;span font-features="tnum=1"&gt;1.00×&lt;/span&gt;</property>
<property name="use-markup">True</property>
<property name="visible">True</property>
<property name="tooltip_text" translatable="yes">Normal speed</property>
<property name="active">True</property>
<property name="role">radio</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
</object> </object>
</child> </child>
</object> </object>

View File

@ -263,6 +263,9 @@ Tobias Bernard
<property name="halign">start</property> <property name="halign">start</property>
<property name="valign">center</property> <property name="valign">center</property>
<property name="label">0:00</property> <property name="label">0:00</property>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -291,6 +294,9 @@ Tobias Bernard
<property name="halign">start</property> <property name="halign">start</property>
<property name="valign">center</property> <property name="valign">center</property>
<property name="label">0:00</property> <property name="label">0:00</property>
<attributes>
<attribute name="font-features" value="tnum=1"/>
</attributes>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -51,13 +51,13 @@
<release version="0.4.5" date="2018-08-31"> <release version="0.4.5" date="2018-08-31">
<description> <description>
<p> <p>
Podcasts 0.4.5 brings a month of bug fixes, performance improvements and initial tranlations support. Podcasts 0.4.5 brings a month of bug fixes, performance improvements and initial translations support.
</p> </p>
<ul> <ul>
<li>Finish, Polish, Turkish, Spanish, German, Galician, Indonesian and Korean Translations were added.</li> <li>Finish, Polish, Turkish, Spanish, German, Galician, Indonesian and Korean Translations were added.</li>
<li>Views now adapt better to different window sizes, thanks to libhandy HdyColumn</li> <li>Views now adapt better to different window sizes, thanks to libhandy HdyColumn</li>
<li>The update indacator was moved to an In-App notification</li> <li>The update indacator was moved to an In-App notification</li>
<li>Performance improvments when loading Show Cover images.</li> <li>Performance improvements when loading Show Cover images.</li>
<li>Improved handling of HTTP Redirects</li> <li>Improved handling of HTTP Redirects</li>
</ul> </ul>
</description> </description>

View File

@ -54,12 +54,14 @@ podcasts_sources = files(
'main.rs', 'main.rs',
'manager.rs', 'manager.rs',
'settings.rs', 'settings.rs',
'utils.rs' 'utils.rs',
'window.rs',
) )
cargo_release = custom_target('cargo-build', cargo_release = custom_target('cargo-build',
build_by_default: true, build_by_default: true,
input: [ input: [
podcast_toml,
data_sources, data_sources,
downloader_sources, downloader_sources,
podcasts_sources, podcasts_sources,

View File

@ -88,7 +88,7 @@ use crate::i18n::i18n;
pub(crate) fn lazy_load<T, C, F, W, U>( pub(crate) fn lazy_load<T, C, F, W, U>(
data: T, data: T,
container: WeakRef<C>, container: WeakRef<C>,
mut contructor: F, mut constructor: F,
callback: U, callback: U,
) where ) where
T: IntoIterator + 'static, T: IntoIterator + 'static,
@ -104,7 +104,7 @@ pub(crate) fn lazy_load<T, C, F, W, U>(
None => return, None => return,
}; };
let widget = contructor(x); let widget = constructor(x);
container.add(&widget); container.add(&widget);
widget.show(); widget.show();
}; };

View File

@ -18,6 +18,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use glib; use glib;
use glib::clone;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
@ -82,21 +83,17 @@ impl InAppNotification {
let notif = InAppNotification::default(); let notif = InAppNotification::default();
notif.text.set_text(&text); notif.text.set_text(&text);
let revealer_weak = notif.revealer.downgrade();
let mut time = 0; let mut time = 0;
let id = timeout_add(250, move || { let id = timeout_add(
if time < timer { 250,
time += 250; clone!(@weak notif.revealer as revealer => @default-return glib::Continue(false), move || {
return glib::Continue(true); if time < timer {
}; time += 250;
return glib::Continue(true);
let revealer = match revealer_weak.upgrade() { };
Some(r) => r, callback(revealer)
None => return glib::Continue(false), }),
}; );
callback(revealer)
});
let id = Rc::new(RefCell::new(Some(id))); let id = Rc::new(RefCell::new(Some(id)));
if undo_callback.is_some() { if undo_callback.is_some() {

View File

@ -22,6 +22,8 @@ use failure::Error;
use gtk::{self, prelude::*, Adjustment}; use gtk::{self, prelude::*, Adjustment};
use glib::clone;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use libhandy::{Column, ColumnExt}; use libhandy::{Column, ColumnExt};
use podcasts_data::dbqueries; use podcasts_data::dbqueries;
@ -135,17 +137,11 @@ impl HomeView {
} }
}; };
let home_weak = Rc::downgrade(&home); let callback = clone!(@weak home => move || {
let callback = move || {
let home = match home_weak.upgrade() {
Some(h) => h,
None => return,
};
if let Some(ref v) = vadj { if let Some(ref v) = vadj {
home.view.set_adjustments(None, Some(v)) home.view.set_adjustments(None, Some(v))
}; };
}; });
lazy_load_full(episodes, func, callback); lazy_load_full(episodes, func, callback);
home.view.container().show_all(); home.view.container().show_all();

View File

@ -208,15 +208,19 @@ fn format_duration(seconds: u32) -> String {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct PlayerRate { struct PlayerRate {
radio300: gtk::RadioButton, radio400: gtk::ModelButton,
radio275: gtk::RadioButton, radio375: gtk::ModelButton,
radio250: gtk::RadioButton, radio350: gtk::ModelButton,
radio225: gtk::RadioButton, radio325: gtk::ModelButton,
radio200: gtk::RadioButton, radio300: gtk::ModelButton,
radio175: gtk::RadioButton, radio275: gtk::ModelButton,
radio150: gtk::RadioButton, radio250: gtk::ModelButton,
radio125: gtk::RadioButton, radio225: gtk::ModelButton,
radio_normal: gtk::RadioButton, radio200: gtk::ModelButton,
radio175: gtk::ModelButton,
radio150: gtk::ModelButton,
radio125: gtk::ModelButton,
radio_normal: gtk::ModelButton,
popover: gtk::Popover, popover: gtk::Popover,
btn: gtk::MenuButton, btn: gtk::MenuButton,
label: gtk::Label, label: gtk::Label,
@ -226,20 +230,28 @@ impl PlayerRate {
fn new() -> Self { fn new() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/player_rate.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/player_rate.ui");
let radio300: gtk::RadioButton = builder.get_object("rate_3_00").unwrap(); let radio400: gtk::ModelButton = builder.get_object("rate_4_00").unwrap();
let radio275: gtk::RadioButton = builder.get_object("rate_2_75").unwrap(); let radio375: gtk::ModelButton = builder.get_object("rate_3_75").unwrap();
let radio250: gtk::RadioButton = builder.get_object("rate_2_50").unwrap(); let radio350: gtk::ModelButton = builder.get_object("rate_3_50").unwrap();
let radio225: gtk::RadioButton = builder.get_object("rate_2_25").unwrap(); let radio325: gtk::ModelButton = builder.get_object("rate_3_25").unwrap();
let radio200: gtk::RadioButton = builder.get_object("rate_2_00").unwrap(); let radio300: gtk::ModelButton = builder.get_object("rate_3_00").unwrap();
let radio175: gtk::RadioButton = builder.get_object("rate_1_75").unwrap(); let radio275: gtk::ModelButton = builder.get_object("rate_2_75").unwrap();
let radio150: gtk::RadioButton = builder.get_object("rate_1_50").unwrap(); let radio250: gtk::ModelButton = builder.get_object("rate_2_50").unwrap();
let radio125: gtk::RadioButton = builder.get_object("rate_1_25").unwrap(); let radio225: gtk::ModelButton = builder.get_object("rate_2_25").unwrap();
let radio_normal: gtk::RadioButton = builder.get_object("normal_rate").unwrap(); let radio200: gtk::ModelButton = builder.get_object("rate_2_00").unwrap();
let radio175: gtk::ModelButton = builder.get_object("rate_1_75").unwrap();
let radio150: gtk::ModelButton = builder.get_object("rate_1_50").unwrap();
let radio125: gtk::ModelButton = builder.get_object("rate_1_25").unwrap();
let radio_normal: gtk::ModelButton = builder.get_object("normal_rate").unwrap();
let popover = builder.get_object("rate_popover").unwrap(); let popover = builder.get_object("rate_popover").unwrap();
let btn = builder.get_object("rate_button").unwrap(); let btn = builder.get_object("rate_button").unwrap();
let label = builder.get_object("rate_label").unwrap(); let label = builder.get_object("rate_label").unwrap();
PlayerRate { PlayerRate {
radio400,
radio375,
radio350,
radio325,
radio300, radio300,
radio275, radio275,
radio250, radio250,
@ -257,45 +269,74 @@ impl PlayerRate {
fn set_rate(&self, rate: f64) { fn set_rate(&self, rate: f64) {
self.label.set_text(&format!("{:.2}×", rate)); self.label.set_text(&format!("{:.2}×", rate));
self.radio_normal.set_property_active(rate == 1.0);
self.radio125.set_property_active(rate == 1.25);
self.radio150.set_property_active(rate == 1.5);
self.radio175.set_property_active(rate == 1.75);
self.radio200.set_property_active(rate == 2.0);
self.radio225.set_property_active(rate == 2.25);
self.radio250.set_property_active(rate == 2.5);
self.radio275.set_property_active(rate == 2.75);
self.radio300.set_property_active(rate == 3.0);
self.radio325.set_property_active(rate == 3.25);
self.radio350.set_property_active(rate == 3.5);
self.radio375.set_property_active(rate == 3.75);
self.radio400.set_property_active(rate == 4.0);
} }
fn connect_signals(&self, widget: &Rc<PlayerWidget>) { fn connect_signals(&self, widget: &Rc<PlayerWidget>) {
self.radio_normal self.radio_normal
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.00); widget.on_rate_changed(1.00);
})); }));
self.radio125 self.radio125
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.25); widget.on_rate_changed(1.25);
})); }));
self.radio150 self.radio150
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.50); widget.on_rate_changed(1.50);
})); }));
self.radio175 self.radio175
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.75); widget.on_rate_changed(1.75);
})); }));
self.radio200 self.radio200
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.00); widget.on_rate_changed(2.00);
})); }));
self.radio225 self.radio225
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.25); widget.on_rate_changed(2.25);
})); }));
self.radio250 self.radio250
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.50); widget.on_rate_changed(2.50);
})); }));
self.radio275 self.radio275
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.75); widget.on_rate_changed(2.75);
})); }));
self.radio300 self.radio300
.connect_toggled(clone!(@weak widget => move |_| { .connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(3.00); widget.on_rate_changed(3.00);
})); }));
self.radio325
.connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(3.25);
}));
self.radio350
.connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(3.50);
}));
self.radio375
.connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(3.75);
}));
self.radio400
.connect_clicked(clone!(@weak widget => move |_| {
widget.on_rate_changed(4.00);
}));
} }
} }

View File

@ -17,6 +17,7 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use glib::clone;
use gtk::{self, prelude::*, Adjustment, Align, SelectionMode}; use gtk::{self, prelude::*, Adjustment, Align, SelectionMode};
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
@ -83,16 +84,14 @@ impl ShowsView {
fn populate_flowbox(shows: &Rc<ShowsView>, vadj: Option<Adjustment>) -> Result<(), Error> { fn populate_flowbox(shows: &Rc<ShowsView>, vadj: Option<Adjustment>) -> Result<(), Error> {
let ignore = get_ignored_shows()?; let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?; let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
let show_weak = Rc::downgrade(&shows);
let flowbox_weak = shows.flowbox.downgrade(); let flowbox_weak = shows.flowbox.downgrade();
let constructor = move |parent| ShowsChild::new(&parent).child; let constructor = move |parent| ShowsChild::new(&parent).child;
let callback = move || { let callback = clone!(@weak shows => move || {
match (show_weak.upgrade(), &vadj) { if vadj.is_some() {
(Some(ref shows), Some(ref v)) => shows.view.set_adjustments(None, Some(v)), shows.view.set_adjustments(None, vadj.as_ref())
_ => (), }
}; });
};
lazy_load(podcasts, flowbox_weak, constructor, callback); lazy_load(podcasts, flowbox_weak, constructor, callback);
Ok(()) Ok(())

View File

@ -48,7 +48,7 @@ use crate::i18n::i18n;
fn action<T, F>(thing: &T, name: &str, action: F) fn action<T, F>(thing: &T, name: &str, action: F)
where where
T: ActionMapExt, T: ActionMapExt,
for<'r, 's> F: Fn(&'r gio::SimpleAction, Option<&Variant>) + 'static, F: Fn(&gio::SimpleAction, Option<&Variant>) + 'static,
{ {
// Create a stateless, parameterless action // Create a stateless, parameterless action
let act = gio::SimpleAction::new(name, None); let act = gio::SimpleAction::new(name, None);
@ -84,27 +84,17 @@ impl MainWindow {
window.get_style_context().add_class("devel"); window.get_style_context().add_class("devel");
} }
let weak_s = settings.downgrade(); window.connect_delete_event(
let weak_app = app.downgrade(); clone!(@strong settings, @weak app => @default-return Inhibit(false), move |window, _| {
window.connect_delete_event(move |window, _| { info!("Saving window position");
let app = match weak_app.upgrade() { WindowGeometry::from_window(&window).write(&settings);
Some(a) => a,
None => return Inhibit(false),
};
let settings = match weak_s.upgrade() { info!("Application is exiting");
Some(s) => s, let app = app.upcast::<gio::Application>();
None => return Inhibit(false), app.quit();
}; Inhibit(false)
}),
info!("Saving window position"); );
WindowGeometry::from_window(&window).write(&settings);
info!("Application is exiting");
let app = app.clone().upcast::<gio::Application>();
app.quit();
Inhibit(false)
});
// Create a content instance // Create a content instance
let content = Content::new(&sender).expect("Content initialization failed."); let content = Content::new(&sender).expect("Content initialization failed.");
@ -128,8 +118,6 @@ impl MainWindow {
wrap.add(&header.bottom_switcher); wrap.add(&header.bottom_switcher);
let updater = RefCell::new(None);
window.add(&wrap); window.add(&wrap);
// Retrieve the previous window position and size. // Retrieve the previous window position and size.
@ -138,20 +126,19 @@ impl MainWindow {
// Update the feeds right after the Window is initialized. // Update the feeds right after the Window is initialized.
if settings.get_boolean("refresh-on-startup") { if settings.get_boolean("refresh-on-startup") {
info!("Refresh on startup."); info!("Refresh on startup.");
let s: Option<Vec<_>> = None; utils::schedule_refresh(None, sender.clone());
utils::schedule_refresh(s, sender.clone());
} }
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32; let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
info!("Auto-refresh every {:?} seconds.", refresh_interval); info!("Auto-refresh every {:?} seconds.", refresh_interval);
let r_sender = sender.clone(); gtk::timeout_add_seconds(
gtk::timeout_add_seconds(refresh_interval, move || { refresh_interval,
let s: Option<Vec<_>> = None; clone!(@strong sender => move || {
utils::schedule_refresh(s, r_sender.clone()); utils::schedule_refresh(None, sender.clone());
glib::Continue(true)
glib::Continue(true) }),
}); );
Self { Self {
app: app.clone(), app: app.clone(),
@ -161,7 +148,7 @@ impl MainWindow {
content, content,
player, player,
updating: Cell::new(false), updating: Cell::new(false),
updater, updater: RefCell::new(None),
sender, sender,
receiver, receiver,
} }
@ -176,41 +163,45 @@ impl MainWindow {
// Create the `refresh` action. // Create the `refresh` action.
// //
// This will trigger a refresh of all the shows in the database. // This will trigger a refresh of all the shows in the database.
action(&self.window, "refresh", clone!(@strong sender => move |_, _| { action(&self.window, "refresh",
gtk::idle_add(clone!(@strong sender => move || { clone!(@strong sender => move |_, _| {
let s: Option<Vec<_>> = None; gtk::idle_add(
utils::schedule_refresh(s, sender.clone()); clone!(@strong sender => move || {
glib::Continue(false) utils::schedule_refresh(None, sender.clone());
glib::Continue(false)
})); }));
})); }));
self.app.set_accels_for_action("win.refresh", &["<primary>r"]); self.app.set_accels_for_action("win.refresh", &["<primary>r"]);
// Create the `OPML` import action // Create the `OPML` import action
action(&self.window, "import", clone!(@strong sender, @weak self.window as win => move |_, _| { action(&self.window, "import",
utils::on_import_clicked(&win, &sender); clone!(@strong sender, @weak self.window as window => move |_, _| {
utils::on_import_clicked(&window, &sender);
})); }));
action(&self.window, "export", clone!(@strong sender, @weak self.window as win => move |_, _| { action(&self.window, "export",
utils::on_export_clicked(&win, &sender); clone!(@strong sender, @weak self.window as window => move |_, _| {
utils::on_export_clicked(&window, &sender);
})); }));
// Create the action that shows a `gtk::AboutDialog` // Create the action that shows a `gtk::AboutDialog`
action(&self.window, "about", clone!(@weak self.window as win => move |_, _| { action(&self.window, "about",
about_dialog(&win); clone!(@weak self.window as win => move |_, _| {
about_dialog(&win);
})); }));
// Create the quit action // Create the quit action
let weak_instance = self.app.downgrade(); action(&self.window, "quit",
action(&self.window, "quit", move |_, _| { clone!(@weak self.app as app => move |_, _| {
weak_instance.upgrade().map(|app| app.quit()); app.quit();
}); }));
self.app.set_accels_for_action("win.quit", &["<primary>q"]); self.app.set_accels_for_action("win.quit", &["<primary>q"]);
// Create the menu action // Create the menu actions
let header = Rc::downgrade(&self.headerbar); action(&self.window, "menu",
action(&self.window, "menu", move |_, _| { clone!(@weak self.headerbar as headerbar => move |_, _| {
header.upgrade().map(|h| h.open_menu()); headerbar.open_menu();
}); }));
// Bind the hamburger menu button to `F10` // Bind the hamburger menu button to `F10`
self.app.set_accels_for_action("win.menu", &["F10"]); self.app.set_accels_for_action("win.menu", &["F10"]);
} }

View File

@ -6,16 +6,14 @@ set -x
# $1 Passed by meson and should be the builddir # $1 Passed by meson and should be the builddir
export CARGO_TARGET_DIR="$1/target/" export CARGO_TARGET_DIR="$1/target/"
export CARGO_HOME="$CARGO_TARGET_DIR/cargo-home"
# If this is run inside a flatpak envrironment, append the export the rustc # If this is run inside a flatpak envrironment, append the export the rustc
# sdk-extension binaries to the path # sdk-extension binaries to the path
if [ -f "/.flatpak-info" ] if [ -f "/.flatpak-info" ]
then then
export PATH="$PATH:/usr/lib/sdk/rust-stable/bin" export PATH="$PATH:/usr/lib/sdk/rust-stable/bin"
# This assumes its run inside a Builder terminal
export CARGO_TARGET_DIR="$BUILDDIR/target/"
fi fi
export CARGO_HOME="$CARGO_TARGET_DIR/cargo-home" cargo fetch --locked
cargo test --all-features --offline -- --test-threads=1 --nocapture
cargo test -j 1 -- --test-threads=1 --nocapture