Merge branch 'master' of gitlab.gnome.org:World/podcasts
This commit is contained in:
commit
f2ac198831
@ -4,7 +4,7 @@ include:
|
||||
# ref: ''
|
||||
|
||||
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:
|
||||
MANIFEST_PATH: "org.gnome.Podcasts.Devel.json"
|
||||
FLATPAK_MODULE: "gnome-podcasts"
|
||||
|
||||
2861
Cargo.lock
generated
2861
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,14 @@ else
|
||||
version_suffix = ''
|
||||
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)
|
||||
i18n = import('i18n')
|
||||
gnome = import('gnome')
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"app-id" : "org.gnome.Podcasts.Devel",
|
||||
"runtime" : "org.gnome.Platform",
|
||||
"runtime-version" : "master",
|
||||
"runtime-version" : "3.36",
|
||||
"sdk" : "org.gnome.Sdk",
|
||||
"sdk-extensions" : [
|
||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"app-id" : "org.gnome.Podcasts",
|
||||
"runtime" : "org.gnome.Platform",
|
||||
"runtime-version" : "master",
|
||||
"runtime-version" : "3.36",
|
||||
"sdk" : "org.gnome.Sdk",
|
||||
"sdk-extensions" : [
|
||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||
|
||||
@ -5,39 +5,36 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
ammonia = "3.0.0"
|
||||
chrono = "0.4.9"
|
||||
derive_builder = "0.8.0"
|
||||
ammonia = "3.1.0"
|
||||
chrono = "0.4.11"
|
||||
derive_builder = "0.9.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.8"
|
||||
rayon = "1.2.0"
|
||||
rayon = "1.3.1"
|
||||
rfc822_sanitizer = "0.3.3"
|
||||
rss = "1.9.0"
|
||||
url = "2.1.1"
|
||||
xdg = "2.2.0"
|
||||
xml-rs = "0.8.0"
|
||||
futures = "0.3.4"
|
||||
hyper = "0.13.2"
|
||||
http = "0.2.0"
|
||||
hyper-tls = "0.4.1"
|
||||
xml-rs = "0.8.3"
|
||||
futures = "0.1.29"
|
||||
hyper = "0.12.35"
|
||||
http = "0.1.19"
|
||||
tokio = "0.1.22"
|
||||
hyper-tls = "0.3.2"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.10.1"
|
||||
failure = "0.1.6"
|
||||
failure_derive = "0.1.6"
|
||||
base64 = "0.10.1"
|
||||
num_cpus = "1.13.0"
|
||||
failure = "0.1.8"
|
||||
failure_derive = "0.1.8"
|
||||
base64 = "0.12.2"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["sqlite", "r2d2"]
|
||||
version = "1.4.3"
|
||||
version = "1.4.5"
|
||||
|
||||
[dependencies.diesel_migrations]
|
||||
features = ["sqlite"]
|
||||
version = "1.4.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
features = ["rt-core", "rt-threaded", "macros"]
|
||||
version = "0.2.13"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.7.2"
|
||||
tempdir = "0.3.7"
|
||||
|
||||
@ -84,7 +84,10 @@ pub enum DataError {
|
||||
FeedRedirect(Source),
|
||||
#[fail(display = "Feed is up to date")]
|
||||
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 },
|
||||
#[fail(display = "Episode was not changed and thus skipped.")]
|
||||
EpisodeNotChanged,
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
#![allow(clippy::unit_arg)]
|
||||
//! Index Feeds.
|
||||
|
||||
use futures::future::*;
|
||||
use futures::prelude::*;
|
||||
use futures::stream;
|
||||
use rss;
|
||||
@ -44,31 +45,31 @@ pub struct Feed {
|
||||
|
||||
impl Feed {
|
||||
/// Index the contents of the RSS `Feed` into the database.
|
||||
pub async fn index(self) -> Result<(), DataError> {
|
||||
let show = self.parse_podcast().to_podcast()?;
|
||||
self.index_channel_items(show).await
|
||||
pub fn index(self) -> impl Future<Item = (), Error = DataError> + Send {
|
||||
ok(self.parse_podcast())
|
||||
.and_then(|pd| pd.to_podcast())
|
||||
.and_then(move |pd| self.index_channel_items(pd))
|
||||
}
|
||||
|
||||
fn parse_podcast(&self) -> NewShow {
|
||||
NewShow::new(&self.channel, self.source_id)
|
||||
}
|
||||
|
||||
async fn index_channel_items(self, pd: Show) -> Result<(), DataError> {
|
||||
let stream = stream::iter(self.channel.into_items());
|
||||
fn index_channel_items(self, pd: Show) -> impl Future<Item = (), Error = DataError> + Send {
|
||||
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
|
||||
|
||||
// Parse the episodes
|
||||
let episodes = stream.filter_map(move |item| {
|
||||
let ret = NewEpisodeMinimal::new(&item, pd.id())
|
||||
.and_then(move |ep| determine_ep_state(ep, &item));
|
||||
if ret.is_ok() {
|
||||
future::ready(Some(ret))
|
||||
} else {
|
||||
future::ready(None)
|
||||
}
|
||||
NewEpisodeMinimal::new(&item, pd.id())
|
||||
.and_then(move |ep| determine_ep_state(ep, &item))
|
||||
.map_err(|err| error!("Failed to parse an episode: {}", err))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// Filter errors, Index updatable episodes, return insertables.
|
||||
let insertable_episodes = filter_episodes(episodes).await?;
|
||||
batch_insert_episodes(&insertable_episodes);
|
||||
Ok(())
|
||||
filter_episodes(episodes)
|
||||
// Batch index insertable episodes.
|
||||
.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
|
||||
S: Stream<Item = Result<IndexState<NewEpisode>, DataError>>,
|
||||
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
|
||||
{
|
||||
stream
|
||||
.try_filter_map(|state| {
|
||||
async {
|
||||
let result = match state {
|
||||
IndexState::NotChanged => None,
|
||||
// Update individual rows, and filter them
|
||||
IndexState::Update((ref ep, rowid)) => {
|
||||
ep.update(rowid)
|
||||
.map_err(|err| error!("{}", err))
|
||||
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
||||
.ok();
|
||||
None
|
||||
}
|
||||
IndexState::Index(s) => Some(s),
|
||||
};
|
||||
Ok(result)
|
||||
.filter_map(|state| match state {
|
||||
IndexState::NotChanged => None,
|
||||
// Update individual rows, and filter them
|
||||
IndexState::Update((ref ep, rowid)) => {
|
||||
ep.update(rowid)
|
||||
.map_err(|err| error!("{}", err))
|
||||
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
||||
.ok();
|
||||
|
||||
None
|
||||
}
|
||||
IndexState::Index(s) => Some(s),
|
||||
})
|
||||
// only Index is left, collect them for batch index
|
||||
.try_collect()
|
||||
.await
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
||||
@ -144,9 +142,8 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use failure::Error;
|
||||
use futures::executor::block_on;
|
||||
use rss::Channel;
|
||||
use tokio;
|
||||
use tokio::{self, prelude::*};
|
||||
|
||||
use crate::database::truncate_db;
|
||||
use crate::dbqueries;
|
||||
@ -201,10 +198,9 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Index the channes
|
||||
let stream_ = stream::iter(feeds).for_each(|x| x.index().map(|x| x.unwrap()));
|
||||
let mut rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(stream_);
|
||||
// Index the channels
|
||||
let stream_ = stream::iter_ok(feeds).for_each(|x| x.index());
|
||||
tokio::run(stream_.map_err(|_| ()));
|
||||
|
||||
// Assert the index rows equal the controlled results
|
||||
assert_eq!(dbqueries::get_sources()?.len(), 5);
|
||||
@ -236,7 +232,7 @@ mod tests {
|
||||
let feed = get_feed(path, 42);
|
||||
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_episodes()?.len(), 43);
|
||||
Ok(())
|
||||
|
||||
@ -111,7 +111,7 @@ pub use crate::models::{Episode, EpisodeWidgetModel, Show, ShowCoverModel, Sourc
|
||||
/// 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";
|
||||
|
||||
/// [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)]
|
||||
pub mod xdg_dirs {
|
||||
use std::path::PathBuf;
|
||||
@ -137,7 +137,7 @@ pub mod xdg_dirs {
|
||||
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 = {
|
||||
PODCASTS_XDG.create_data_directory("Downloads").unwrap()
|
||||
};
|
||||
|
||||
@ -255,7 +255,7 @@ impl NewEpisodeMinimal {
|
||||
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"));
|
||||
// Should treat information from the rss feeds as invalid by default.
|
||||
// Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday.
|
||||
@ -342,7 +342,7 @@ mod tests {
|
||||
use std::io::BufReader;
|
||||
|
||||
// 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.
|
||||
lazy_static! {
|
||||
@ -578,6 +578,10 @@ mod tests {
|
||||
let ep = EXPECTED_MINIMAL_INTERCEPTED_1
|
||||
.clone()
|
||||
.into_new_episode(&item);
|
||||
println!(
|
||||
"EPISODE: {:#?}\nEXPECTED: {:#?}",
|
||||
ep, *EXPECTED_INTERCEPTED_1
|
||||
);
|
||||
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
|
||||
|
||||
let item = channel.items().iter().nth(15).unwrap();
|
||||
|
||||
@ -31,6 +31,9 @@ use http::header::{
|
||||
USER_AGENT as USER_AGENT_HEADER,
|
||||
};
|
||||
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};
|
||||
|
||||
@ -159,7 +162,7 @@ impl Source {
|
||||
let code = res.status();
|
||||
|
||||
if code.is_success() {
|
||||
// If request is successful save the etag
|
||||
// If request is succesful save the etag
|
||||
self = self.update_etag(&res)?
|
||||
} else {
|
||||
match code.as_u16() {
|
||||
@ -189,7 +192,7 @@ impl Source {
|
||||
return Err(DataError::FeedRedirect(self));
|
||||
}
|
||||
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)),
|
||||
408 => return Err(self.make_err("408: Request Timeout.", code)),
|
||||
410 => return Err(self.make_err("410: Feed was deleted..", code)),
|
||||
@ -213,7 +216,10 @@ impl Source {
|
||||
self = self.save()?;
|
||||
|
||||
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)
|
||||
@ -236,45 +242,41 @@ impl Source {
|
||||
///
|
||||
/// Consumes `self` and Returns the corresponding `Feed` Object.
|
||||
// Refactor into TryInto once it lands on stable.
|
||||
pub async fn into_feed(
|
||||
pub fn into_feed(
|
||||
self,
|
||||
client: Client<HttpsConnector<HttpConnector>>,
|
||||
) -> Result<Feed, DataError> {
|
||||
) -> impl Future<Item = Feed, Error = DataError> {
|
||||
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?;
|
||||
let chan = response_to_channel(resp).await?;
|
||||
|
||||
FeedBuilder::default()
|
||||
.channel(chan)
|
||||
.source_id(id)
|
||||
.build()
|
||||
.map_err(From::from)
|
||||
response
|
||||
.and_then(response_to_channel)
|
||||
.and_then(move |chan| {
|
||||
FeedBuilder::default()
|
||||
.channel(chan)
|
||||
.source_id(id)
|
||||
.build()
|
||||
.map_err(From::from)
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_response(
|
||||
fn request_constructor(
|
||||
self,
|
||||
client: &Client<HttpsConnector<HttpConnector>>,
|
||||
) -> Result<Response<Body>, 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> {
|
||||
) -> impl Future<Item = Response<Body>, Error = DataError> {
|
||||
// FIXME: remove unwrap somehow
|
||||
let uri = Uri::from_str(self.uri()).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());
|
||||
}
|
||||
|
||||
let res = client.request(req).await?;
|
||||
//.map_err(From::from)
|
||||
self.match_status(res)
|
||||
client
|
||||
.request(req)
|
||||
.map_err(From::from)
|
||||
.and_then(move |res| self.match_status(res))
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_to_channel(res: Response<Body>) -> Result<Channel, DataError> {
|
||||
let chunk = hyper::body::to_bytes(res.into_body()).await?;
|
||||
let buf = String::from_utf8_lossy(&chunk).into_owned();
|
||||
Channel::from_str(&buf).map_err(From::from)
|
||||
fn response_to_channel(
|
||||
res: Response<Body>,
|
||||
) -> impl Future<Item = Channel, Error = DataError> + Send {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use failure::Error;
|
||||
use num_cpus;
|
||||
use tokio;
|
||||
|
||||
use crate::database::truncate_db;
|
||||
@ -331,7 +341,7 @@ mod tests {
|
||||
truncate_db()?;
|
||||
|
||||
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 url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||
|
||||
@ -43,7 +43,7 @@ use failure::Error;
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
// FIXME: Make it a Diesel model
|
||||
/// 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
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn export_from_db<P: AsRef<Path>>(path: P, export_title: &str) -> Result<(), Error> {
|
||||
let file = File::create(path)?;
|
||||
@ -163,7 +163,7 @@ pub fn export_to_file<F: Write>(file: F, export_title: &str) -> Result<(), Error
|
||||
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> {
|
||||
let mut list = HashSet::new();
|
||||
let parser = reader::EventReader::new(reader);
|
||||
@ -210,7 +210,7 @@ mod tests {
|
||||
use super::*;
|
||||
use chrono::Local;
|
||||
use failure::Error;
|
||||
use futures::executor::block_on;
|
||||
use futures::Future;
|
||||
|
||||
use crate::database::{truncate_db, TEMPDIR};
|
||||
use crate::utils::get_feed;
|
||||
@ -318,7 +318,7 @@ mod tests {
|
||||
// Create and insert a Source into db
|
||||
let s = Source::from_url(url).unwrap();
|
||||
let feed = get_feed(path, s.id());
|
||||
block_on(feed.index()).unwrap();
|
||||
feed.index().wait().unwrap();
|
||||
});
|
||||
|
||||
let mut map: HashSet<Opml> = HashSet::new();
|
||||
|
||||
@ -20,13 +20,15 @@
|
||||
// FIXME:
|
||||
//! Docs.
|
||||
|
||||
use futures::{future::ok, prelude::*, stream::FuturesUnordered};
|
||||
use futures::{future::ok, lazy, prelude::*, stream::FuturesUnordered};
|
||||
use tokio;
|
||||
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::{Body, Client};
|
||||
use hyper_tls::HttpsConnector;
|
||||
|
||||
use num_cpus;
|
||||
|
||||
use crate::errors::DataError;
|
||||
use crate::Source;
|
||||
|
||||
@ -40,24 +42,29 @@ type HttpsClient = Client<HttpsConnector<HttpConnector>>;
|
||||
/// Messy temp diagram:
|
||||
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` ->
|
||||
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
|
||||
#[tokio::main]
|
||||
pub async fn pipeline<'a, S>(mut sources: S, client: HttpsClient)
|
||||
pub fn pipeline<'a, S>(sources: S, client: HttpsClient) -> impl Future<Item = (), Error = ()> + 'a
|
||||
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 {
|
||||
if let Ok(source) = source_result {
|
||||
match source.into_feed(client.clone()).await {
|
||||
Ok(feed) => {
|
||||
let fut = feed.index().map_err(|err| error!("Error: {}", err));
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
// Avoid spamming the stderr when it's not an actual error
|
||||
Err(DataError::FeedNotModified(_)) => (),
|
||||
Err(err) => error!("Error: {}", err),
|
||||
};
|
||||
}
|
||||
}
|
||||
sources
|
||||
.and_then(move |s| s.into_feed(client.clone()))
|
||||
.map_err(|err| {
|
||||
match err {
|
||||
// Avoid spamming the stderr when its not an eactual error
|
||||
DataError::FeedNotModified(_) => (),
|
||||
_ => 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
|
||||
@ -66,12 +73,13 @@ pub fn run<S>(sources: S) -> Result<(), DataError>
|
||||
where
|
||||
S: IntoIterator<Item = Source>,
|
||||
{
|
||||
let https = HttpsConnector::new();
|
||||
let https = HttpsConnector::new(num_cpus::get())?;
|
||||
let client = Client::builder().build::<_, Body>(https);
|
||||
|
||||
let foo = sources.into_iter().map(ok::<_, _>);
|
||||
let stream = FuturesUnordered::from_iter(foo);
|
||||
pipeline(stream, client);
|
||||
let p = pipeline(stream, client);
|
||||
tokio::run(p);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -113,7 +121,7 @@ mod tests {
|
||||
run(sources)?;
|
||||
|
||||
let sources = dbqueries::get_sources()?;
|
||||
// Run again to cover Unique constrains errors.
|
||||
// Run again to cover Unique constrains erros.
|
||||
run(sources)?;
|
||||
|
||||
// Assert the index rows equal the controlled results
|
||||
|
||||
@ -56,7 +56,7 @@ fn download_checker() -> Result<(), DataError> {
|
||||
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> {
|
||||
let mut episodes = dbqueries::get_played_cleaner_episodes()?;
|
||||
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();
|
||||
if now_utc > limit {
|
||||
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(|_| error!("Failed to delete file: {:?}", ep.local_uri()))
|
||||
.ok();
|
||||
@ -144,11 +144,11 @@ pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
|
||||
// TODO: Write Tests
|
||||
pub fn delete_show(pd: &Show) -> Result<(), DataError> {
|
||||
dbqueries::remove_feed(pd)?;
|
||||
info!("{} was removed succesfully.", pd.title());
|
||||
info!("{} was removed successfully.", pd.title());
|
||||
|
||||
let fold = get_download_folder(pd.title())?;
|
||||
fs::remove_dir_all(&fold)?;
|
||||
info!("All the content at, {} was removed succesfully", &fold);
|
||||
info!("All the content at, {} was removed successfully", &fold);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -5,14 +5,13 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
error-chain = "0.12.1"
|
||||
log = "0.4.8"
|
||||
mime_guess = "2.0.1"
|
||||
mime_guess = "2.0.3"
|
||||
reqwest = "0.9.22"
|
||||
tempdir = "0.3.7"
|
||||
glob = "0.3.0"
|
||||
failure = "0.1.6"
|
||||
failure_derive = "0.1.6"
|
||||
failure = "0.1.8"
|
||||
failure_derive = "0.1.8"
|
||||
|
||||
[dependencies.podcasts-data]
|
||||
path = "../podcasts-data"
|
||||
|
||||
@ -54,7 +54,7 @@ pub trait DownloadProgress {
|
||||
// Sorry to those who will have to work with that code.
|
||||
// Would much rather use a crate,
|
||||
// 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.
|
||||
fn download_into(
|
||||
dir: &str,
|
||||
@ -64,7 +64,7 @@ fn download_into(
|
||||
) -> Result<String, DownloadError> {
|
||||
info!("GET request to: {}", url);
|
||||
// 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
|
||||
let policy = RedirectPolicy::custom(|attempt| {
|
||||
info!("Redirect Attempt URL: {:?}", attempt.url());
|
||||
@ -104,7 +104,7 @@ fn download_into(
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.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));
|
||||
|
||||
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);
|
||||
// Rename/move the tempfile into a permanent place upon success.
|
||||
rename(out_file, &target)?;
|
||||
info!("Downloading of {} completed succesfully.", &target);
|
||||
info!("Downloading of {} completed successfully.", &target);
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
@ -219,10 +219,10 @@ pub fn get_episode(
|
||||
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));
|
||||
|
||||
// Over-write episode lenght
|
||||
// Over-write episode length
|
||||
let size = fs::metadata(path);
|
||||
if let Ok(s) = size {
|
||||
ep.set_length(Some(s.len() as i32))
|
||||
|
||||
@ -5,47 +5,44 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.9"
|
||||
chrono = "0.4.11"
|
||||
crossbeam-channel = "0.3.9"
|
||||
gdk = "0.12.0"
|
||||
gdk = "0.12.1"
|
||||
gdk-pixbuf = "0.8.0"
|
||||
gobject-sys = "0.9.1"
|
||||
glib-sys = "0.9.1"
|
||||
gst = { version = "0.15.2", package = "gstreamer" }
|
||||
gst-player = { version = "0.15.0", package = "gstreamer-player" }
|
||||
gst = { version = "0.15.7", package = "gstreamer" }
|
||||
gst-player = { version = "0.15.5", package = "gstreamer-player" }
|
||||
humansize = "1.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.8"
|
||||
loggerv = "0.7.2"
|
||||
open = "1.3.2"
|
||||
rayon = "1.2.0"
|
||||
open = "1.4.0"
|
||||
rayon = "1.3.1"
|
||||
url = "2.1.0"
|
||||
failure = "0.1.6"
|
||||
failure_derive = "0.1.6"
|
||||
fragile = "0.3.0"
|
||||
regex = "1.3.1"
|
||||
failure = "0.1.8"
|
||||
failure_derive = "0.1.8"
|
||||
fragile = "1.0.0"
|
||||
regex = "1.3.9"
|
||||
reqwest = "0.9.22"
|
||||
serde_json = "1.0.41"
|
||||
# html2text = "0.1.8"
|
||||
html2text = { git = "https://github.com/jugglerchris/rust-html2text" }
|
||||
serde_json = "1.0.55"
|
||||
html2text = "0.1.12"
|
||||
mpris-player = "0.5.0"
|
||||
pango = "0.8.0"
|
||||
glib = "0.9.3"
|
||||
|
||||
[dependencies.gettext-rs]
|
||||
git = "https://github.com/danigm/gettext-rs"
|
||||
branch = "no-gettext"
|
||||
features = ["gettext-system"]
|
||||
|
||||
[dependencies.glib]
|
||||
version = "0.9.1"
|
||||
|
||||
[dependencies.gio]
|
||||
features = ["v2_50"]
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
|
||||
[dependencies.gtk]
|
||||
features = ["v3_24"]
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
|
||||
[dependencies.libhandy]
|
||||
version = "0.5.0"
|
||||
|
||||
@ -2,22 +2,23 @@
|
||||
# Copyright (C) 2018 podcasts's COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the podcasts package.
|
||||
# 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 ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: podcasts master\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/podcasts/issues\n"
|
||||
"POT-Creation-Date: 2018-10-23 10:23+0000\n"
|
||||
"PO-Revision-Date: 2018-10-29 13:53+0100\n"
|
||||
"Last-Translator: Alexandre Franke <alexandre.franke@gmail.com>\n"
|
||||
"POT-Creation-Date: 2020-06-03 20:13+0000\n"
|
||||
"PO-Revision-Date: 2020-06-19 17:15+0200\n"
|
||||
"Last-Translator: Thibault Martin <mail@thibaultmart.in>\n"
|
||||
"Language-Team: French <gnomefr@traduc.org>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
||||
"X-Generator: Gtranslator 3.36.0\n"
|
||||
|
||||
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:15
|
||||
msgid "Top position of the last open main window"
|
||||
@ -49,8 +50,7 @@ msgstr "Indique s’il faut actualiser périodiquement le contenu"
|
||||
|
||||
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:46
|
||||
msgid "How many periods of time to wait between automatic refreshes"
|
||||
msgstr ""
|
||||
"Nombre de périodes de délais à attendre entre les actualisations automatiques"
|
||||
msgstr "Nombre de délais à attendre entre les actualisations automatiques"
|
||||
|
||||
#: podcasts-gtk/resources/org.gnome.Podcasts.gschema.xml:50
|
||||
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.
|
||||
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:3
|
||||
#: 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/src/widgets/aboutdialog.rs:37
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:158 podcasts-gtk/src/app.rs:353
|
||||
#: podcasts-gtk/src/widgets/aboutdialog.rs:56 podcasts-gtk/src/window.rs:82
|
||||
msgid "Podcasts"
|
||||
msgstr "Podcasts"
|
||||
|
||||
@ -81,11 +81,6 @@ msgstr "Podcasts"
|
||||
msgid "Listen to your favorite podcasts, right from your desktop."
|
||||
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!
|
||||
#: podcasts-gtk/resources/org.gnome.Podcasts.desktop.in.in:13
|
||||
msgid "Podcast;RSS;"
|
||||
@ -95,13 +90,13 @@ msgstr "Podcast;RSS;Baladodiffusion;Émissions;"
|
||||
msgid "Podcast app for 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"
|
||||
msgstr "Jordan Petridis"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/empty_view.ui:46
|
||||
msgid "This show does not have episodes yet"
|
||||
msgstr "Cette émission n’a pas encore d’épisodes"
|
||||
msgstr "Cette émission n’a pas encore d’épisode"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/empty_view.ui:62
|
||||
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"
|
||||
msgstr "Importer des émissions à partir d’un autre appareil"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/episode_widget.ui:180
|
||||
#: podcasts-gtk/resources/gtk/episode_widget.ui:79
|
||||
msgid "You’ve 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…"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr "Télécharger cet épisode"
|
||||
|
||||
@ -143,20 +142,20 @@ msgstr "_Chercher de nouveaux épisodes"
|
||||
msgid "_Import Shows"
|
||||
msgstr "_Importer les émissions"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:22
|
||||
msgid "_Preferences"
|
||||
msgstr "_Préférences"
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:16
|
||||
msgid "_Export Shows"
|
||||
msgstr "_Exporter les émissions"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:27
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:22
|
||||
msgid "_Keyboard Shortcuts"
|
||||
msgstr "_Raccourcis clavier"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:35
|
||||
#: podcasts-gtk/resources/gtk/hamburger.ui:30
|
||||
msgid "_About Podcasts"
|
||||
msgstr "À _propos de Podcasts"
|
||||
|
||||
#: 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"
|
||||
msgstr "Ajouter un nouveau flux"
|
||||
|
||||
@ -168,15 +167,11 @@ msgstr "Entrer l’adresse du flux à ajouter"
|
||||
msgid "Add"
|
||||
msgstr "Ajouter"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:133
|
||||
msgid "You are already subscribed to that feed!"
|
||||
msgstr "Vous êtes déjà abonné à ce flux !"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:169
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:171
|
||||
msgid "Show Title"
|
||||
msgstr "Titre de l’émission"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:210
|
||||
#: podcasts-gtk/resources/gtk/headerbar.ui:207
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
|
||||
@ -191,11 +186,6 @@ msgstr "Chercher de nouveaux épisodes"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/help-overlay.ui:25
|
||||
msgctxt "shortcut window"
|
||||
msgid "Preferences"
|
||||
msgstr "Préférences"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/help-overlay.ui:32
|
||||
msgctxt "shortcut window"
|
||||
msgid "Quit the application"
|
||||
msgstr "Quitter l’application"
|
||||
|
||||
@ -227,71 +217,54 @@ msgstr "Une notification d’action intégrée à l’application"
|
||||
msgid "Undo"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:72
|
||||
msgid "Rewind 10 seconds"
|
||||
msgstr "Reculer de 10 secondes"
|
||||
#: podcasts-gtk/resources/gtk/player_dialog.ui:14
|
||||
msgid "Now Playing"
|
||||
msgstr "Lecture en cours"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:87
|
||||
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
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:32
|
||||
msgid "Change the playback speed"
|
||||
msgstr "Changer la vitesse de lecture"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:300
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:380
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:47
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:122
|
||||
msgid "1.00×"
|
||||
msgstr "1,00×"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:344
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:86
|
||||
msgid "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"
|
||||
msgstr "Débit × 1,5"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:362
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:104
|
||||
msgid "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"
|
||||
msgstr "Débit × 1,25"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:384
|
||||
#: podcasts-gtk/resources/gtk/player_rate.ui:126
|
||||
msgid "Normal speed"
|
||||
msgstr "Vitesse normale"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:42
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:295
|
||||
msgid "Preferences"
|
||||
msgstr "Préférences"
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:97
|
||||
msgid "Rewind 10 seconds"
|
||||
msgstr "Reculer de 10 secondes"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:76
|
||||
msgid "Appearance"
|
||||
msgstr "Apparence"
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:112
|
||||
msgid "Play"
|
||||
msgstr "Lire"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:120
|
||||
msgid "Dark Theme"
|
||||
msgstr "Thème sombre"
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:128
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:166
|
||||
msgid "Delete played episodes"
|
||||
msgstr "Supprimer les épisodes lus"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/prefs.ui:211
|
||||
msgid "After"
|
||||
msgstr "Après"
|
||||
#: podcasts-gtk/resources/gtk/player_toolbar.ui:144
|
||||
msgid "Fast forward 10 seconds"
|
||||
msgstr "Avancer de 10 secondes"
|
||||
|
||||
#: podcasts-gtk/resources/gtk/secondary_menu.ui:7
|
||||
msgid "_Mark All Episodes as Played"
|
||||
@ -317,91 +290,135 @@ msgstr "Marquer tout comme lu"
|
||||
msgid "Unsubscribe"
|
||||
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"
|
||||
msgstr "Récupérer les nouveaux épisodes"
|
||||
|
||||
#: podcasts-gtk/src/headerbar.rs:98
|
||||
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
|
||||
#: podcasts-gtk/src/stacks/content.rs:54
|
||||
msgid "New"
|
||||
msgstr "Nouveau"
|
||||
|
||||
#: podcasts-gtk/src/stacks/content.rs:36
|
||||
#: podcasts-gtk/src/stacks/content.rs:55
|
||||
msgid "Shows"
|
||||
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."
|
||||
msgstr "Sélectionnez le fichier à partir duquel importer les émissions."
|
||||
|
||||
#: podcasts-gtk/src/utils.rs:360
|
||||
#: podcasts-gtk/src/utils.rs:401
|
||||
msgid "_Import"
|
||||
msgstr "_Importer"
|
||||
|
||||
#: podcasts-gtk/src/utils.rs:369
|
||||
#: podcasts-gtk/src/utils.rs:410 podcasts-gtk/src/utils.rs:457
|
||||
msgid "OPML file"
|
||||
msgstr "Fichier OPML"
|
||||
|
||||
#: podcasts-gtk/src/utils.rs:386
|
||||
#: podcasts-gtk/src/utils.rs:427
|
||||
msgid "Failed to parse the imported file"
|
||||
msgstr "Échec de l’analyse 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."
|
||||
msgstr "Le fichier sélectionné n’est 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 l’exportation des podcasts"
|
||||
|
||||
#: podcasts-gtk/src/widgets/aboutdialog.rs:51
|
||||
msgid "Podcast Client for the GNOME Desktop."
|
||||
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"
|
||||
msgstr "En apprendre plus sur GNOME Podcasts"
|
||||
|
||||
#: podcasts-gtk/src/widgets/aboutdialog.rs:44
|
||||
#: podcasts-gtk/src/widgets/aboutdialog.rs:63
|
||||
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"
|
||||
msgstr "{} min"
|
||||
|
||||
#. 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."
|
||||
msgstr "Le lecteur n’a pas pu réaliser l’action."
|
||||
|
||||
#: podcasts-gtk/src/widgets/show_menu.rs:150
|
||||
#: podcasts-gtk/src/widgets/show_menu.rs:174
|
||||
msgid "Marked all episodes as listened"
|
||||
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 {}"
|
||||
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"
|
||||
|
||||
@ -110,6 +110,9 @@ Tobias Bernard
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -143,6 +146,9 @@ Tobias Bernard
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -177,6 +183,9 @@ Tobias Bernard
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -212,6 +221,9 @@ Tobias Bernard
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@ -132,6 +132,9 @@
|
||||
<property name="can_focus">False</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="xalign">1</property>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="small-label"/>
|
||||
@ -154,6 +157,9 @@
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="width_chars">5</property>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="small-label"/>
|
||||
|
||||
@ -45,6 +45,9 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">1.00×</property>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -75,23 +78,15 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_start">6</property>
|
||||
<property name="margin_end">6</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="margin">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">3</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_3_00">
|
||||
<property name="label" translatable="yes">3.00×</property>
|
||||
<object class="GtkModelButton" id="rate_4_00">
|
||||
<property name="text"><span font-features="tnum=1">4.00×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">4.0 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -100,16 +95,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_2_75">
|
||||
<property name="label" translatable="yes">2.75×</property>
|
||||
<object class="GtkModelButton" id="rate_3_75">
|
||||
<property name="text"><span font-features="tnum=1">3.75×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">3.75 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -118,16 +109,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_2_50">
|
||||
<property name="label" translatable="yes">2.50×</property>
|
||||
<object class="GtkModelButton" id="rate_3_50">
|
||||
<property name="text"><span font-features="tnum=1">3.50×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">3.5 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -136,16 +123,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_2_25">
|
||||
<property name="label" translatable="yes">2.25×</property>
|
||||
<object class="GtkModelButton" id="rate_3_25">
|
||||
<property name="text"><span font-features="tnum=1">3.25×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">3.25 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -154,16 +137,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_2_00">
|
||||
<property name="label" translatable="yes">2.00×</property>
|
||||
<object class="GtkModelButton" id="rate_3_00">
|
||||
<property name="text"><span font-features="tnum=1">3.00×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">3.0 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -172,16 +151,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_1_75">
|
||||
<property name="label" translatable="yes">1.75×</property>
|
||||
<object class="GtkModelButton" id="rate_2_75">
|
||||
<property name="text"><span font-features="tnum=1">2.75×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">2.75 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -190,16 +165,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_1_50">
|
||||
<property name="label" translatable="yes">1.50×</property>
|
||||
<object class="GtkModelButton" id="rate_2_50">
|
||||
<property name="text"><span font-features="tnum=1">2.50×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">2.5 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -208,16 +179,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="rate_1_25">
|
||||
<property name="label" translatable="yes">1.25×</property>
|
||||
<object class="GtkModelButton" id="rate_2_25">
|
||||
<property name="text"><span font-features="tnum=1">2.25×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">2.25 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -226,16 +193,12 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="normal_rate">
|
||||
<property name="label" translatable="yes">1.00×</property>
|
||||
<object class="GtkModelButton" id="rate_2_00">
|
||||
<property name="text"><span font-features="tnum=1">2.00×</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</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>
|
||||
<property name="tooltip_text" translatable="yes">2.0 speed rate</property>
|
||||
<property name="role">radio</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@ -243,6 +206,63 @@ Tobias Bernard
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="rate_1_75">
|
||||
<property name="text"><span font-features="tnum=1">1.75×</span></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"><span font-features="tnum=1">1.50×</span></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"><span font-features="tnum=1">1.25×</span></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"><span font-features="tnum=1">1.00×</span></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>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@ -263,6 +263,9 @@ Tobias Bernard
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label">0:00</property>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -291,6 +294,9 @@ Tobias Bernard
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label">0:00</property>
|
||||
<attributes>
|
||||
<attribute name="font-features" value="tnum=1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@ -51,13 +51,13 @@
|
||||
<release version="0.4.5" date="2018-08-31">
|
||||
<description>
|
||||
<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>
|
||||
<ul>
|
||||
<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>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>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
@ -54,12 +54,14 @@ podcasts_sources = files(
|
||||
'main.rs',
|
||||
'manager.rs',
|
||||
'settings.rs',
|
||||
'utils.rs'
|
||||
'utils.rs',
|
||||
'window.rs',
|
||||
)
|
||||
|
||||
cargo_release = custom_target('cargo-build',
|
||||
build_by_default: true,
|
||||
input: [
|
||||
podcast_toml,
|
||||
data_sources,
|
||||
downloader_sources,
|
||||
podcasts_sources,
|
||||
|
||||
@ -88,7 +88,7 @@ use crate::i18n::i18n;
|
||||
pub(crate) fn lazy_load<T, C, F, W, U>(
|
||||
data: T,
|
||||
container: WeakRef<C>,
|
||||
mut contructor: F,
|
||||
mut constructor: F,
|
||||
callback: U,
|
||||
) where
|
||||
T: IntoIterator + 'static,
|
||||
@ -104,7 +104,7 @@ pub(crate) fn lazy_load<T, C, F, W, U>(
|
||||
None => return,
|
||||
};
|
||||
|
||||
let widget = contructor(x);
|
||||
let widget = constructor(x);
|
||||
container.add(&widget);
|
||||
widget.show();
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use glib;
|
||||
use glib::clone;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
@ -82,21 +83,17 @@ impl InAppNotification {
|
||||
let notif = InAppNotification::default();
|
||||
notif.text.set_text(&text);
|
||||
|
||||
let revealer_weak = notif.revealer.downgrade();
|
||||
let mut time = 0;
|
||||
let id = timeout_add(250, move || {
|
||||
if time < timer {
|
||||
time += 250;
|
||||
return glib::Continue(true);
|
||||
};
|
||||
|
||||
let revealer = match revealer_weak.upgrade() {
|
||||
Some(r) => r,
|
||||
None => return glib::Continue(false),
|
||||
};
|
||||
|
||||
callback(revealer)
|
||||
});
|
||||
let id = timeout_add(
|
||||
250,
|
||||
clone!(@weak notif.revealer as revealer => @default-return glib::Continue(false), move || {
|
||||
if time < timer {
|
||||
time += 250;
|
||||
return glib::Continue(true);
|
||||
};
|
||||
callback(revealer)
|
||||
}),
|
||||
);
|
||||
let id = Rc::new(RefCell::new(Some(id)));
|
||||
|
||||
if undo_callback.is_some() {
|
||||
|
||||
@ -22,6 +22,8 @@ use failure::Error;
|
||||
|
||||
use gtk::{self, prelude::*, Adjustment};
|
||||
|
||||
use glib::clone;
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use libhandy::{Column, ColumnExt};
|
||||
use podcasts_data::dbqueries;
|
||||
@ -135,17 +137,11 @@ impl HomeView {
|
||||
}
|
||||
};
|
||||
|
||||
let home_weak = Rc::downgrade(&home);
|
||||
let callback = move || {
|
||||
let home = match home_weak.upgrade() {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let callback = clone!(@weak home => move || {
|
||||
if let Some(ref v) = vadj {
|
||||
home.view.set_adjustments(None, Some(v))
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
lazy_load_full(episodes, func, callback);
|
||||
home.view.container().show_all();
|
||||
|
||||
@ -208,15 +208,19 @@ fn format_duration(seconds: u32) -> String {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PlayerRate {
|
||||
radio300: gtk::RadioButton,
|
||||
radio275: gtk::RadioButton,
|
||||
radio250: gtk::RadioButton,
|
||||
radio225: gtk::RadioButton,
|
||||
radio200: gtk::RadioButton,
|
||||
radio175: gtk::RadioButton,
|
||||
radio150: gtk::RadioButton,
|
||||
radio125: gtk::RadioButton,
|
||||
radio_normal: gtk::RadioButton,
|
||||
radio400: gtk::ModelButton,
|
||||
radio375: gtk::ModelButton,
|
||||
radio350: gtk::ModelButton,
|
||||
radio325: gtk::ModelButton,
|
||||
radio300: gtk::ModelButton,
|
||||
radio275: gtk::ModelButton,
|
||||
radio250: gtk::ModelButton,
|
||||
radio225: gtk::ModelButton,
|
||||
radio200: gtk::ModelButton,
|
||||
radio175: gtk::ModelButton,
|
||||
radio150: gtk::ModelButton,
|
||||
radio125: gtk::ModelButton,
|
||||
radio_normal: gtk::ModelButton,
|
||||
popover: gtk::Popover,
|
||||
btn: gtk::MenuButton,
|
||||
label: gtk::Label,
|
||||
@ -226,20 +230,28 @@ impl PlayerRate {
|
||||
fn new() -> Self {
|
||||
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 radio275: gtk::RadioButton = builder.get_object("rate_2_75").unwrap();
|
||||
let radio250: gtk::RadioButton = builder.get_object("rate_2_50").unwrap();
|
||||
let radio225: gtk::RadioButton = builder.get_object("rate_2_25").unwrap();
|
||||
let radio200: gtk::RadioButton = builder.get_object("rate_2_00").unwrap();
|
||||
let radio175: gtk::RadioButton = builder.get_object("rate_1_75").unwrap();
|
||||
let radio150: gtk::RadioButton = builder.get_object("rate_1_50").unwrap();
|
||||
let radio125: gtk::RadioButton = builder.get_object("rate_1_25").unwrap();
|
||||
let radio_normal: gtk::RadioButton = builder.get_object("normal_rate").unwrap();
|
||||
let radio400: gtk::ModelButton = builder.get_object("rate_4_00").unwrap();
|
||||
let radio375: gtk::ModelButton = builder.get_object("rate_3_75").unwrap();
|
||||
let radio350: gtk::ModelButton = builder.get_object("rate_3_50").unwrap();
|
||||
let radio325: gtk::ModelButton = builder.get_object("rate_3_25").unwrap();
|
||||
let radio300: gtk::ModelButton = builder.get_object("rate_3_00").unwrap();
|
||||
let radio275: gtk::ModelButton = builder.get_object("rate_2_75").unwrap();
|
||||
let radio250: gtk::ModelButton = builder.get_object("rate_2_50").unwrap();
|
||||
let radio225: gtk::ModelButton = builder.get_object("rate_2_25").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 btn = builder.get_object("rate_button").unwrap();
|
||||
let label = builder.get_object("rate_label").unwrap();
|
||||
|
||||
PlayerRate {
|
||||
radio400,
|
||||
radio375,
|
||||
radio350,
|
||||
radio325,
|
||||
radio300,
|
||||
radio275,
|
||||
radio250,
|
||||
@ -257,45 +269,74 @@ impl PlayerRate {
|
||||
|
||||
fn set_rate(&self, rate: f64) {
|
||||
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>) {
|
||||
self.radio_normal
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(1.00);
|
||||
}));
|
||||
self.radio125
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(1.25);
|
||||
}));
|
||||
self.radio150
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(1.50);
|
||||
}));
|
||||
self.radio175
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(1.75);
|
||||
}));
|
||||
self.radio200
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(2.00);
|
||||
}));
|
||||
self.radio225
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(2.25);
|
||||
}));
|
||||
self.radio250
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(2.50);
|
||||
}));
|
||||
self.radio275
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
widget.on_rate_changed(2.75);
|
||||
}));
|
||||
self.radio300
|
||||
.connect_toggled(clone!(@weak widget => move |_| {
|
||||
.connect_clicked(clone!(@weak widget => move |_| {
|
||||
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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use glib::clone;
|
||||
use gtk::{self, prelude::*, Adjustment, Align, SelectionMode};
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
@ -83,16 +84,14 @@ impl ShowsView {
|
||||
fn populate_flowbox(shows: &Rc<ShowsView>, vadj: Option<Adjustment>) -> Result<(), Error> {
|
||||
let ignore = get_ignored_shows()?;
|
||||
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
|
||||
let show_weak = Rc::downgrade(&shows);
|
||||
let flowbox_weak = shows.flowbox.downgrade();
|
||||
|
||||
let constructor = move |parent| ShowsChild::new(&parent).child;
|
||||
let callback = move || {
|
||||
match (show_weak.upgrade(), &vadj) {
|
||||
(Some(ref shows), Some(ref v)) => shows.view.set_adjustments(None, Some(v)),
|
||||
_ => (),
|
||||
};
|
||||
};
|
||||
let callback = clone!(@weak shows => move || {
|
||||
if vadj.is_some() {
|
||||
shows.view.set_adjustments(None, vadj.as_ref())
|
||||
}
|
||||
});
|
||||
|
||||
lazy_load(podcasts, flowbox_weak, constructor, callback);
|
||||
Ok(())
|
||||
|
||||
@ -48,7 +48,7 @@ use crate::i18n::i18n;
|
||||
fn action<T, F>(thing: &T, name: &str, action: F)
|
||||
where
|
||||
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
|
||||
let act = gio::SimpleAction::new(name, None);
|
||||
@ -84,27 +84,17 @@ impl MainWindow {
|
||||
window.get_style_context().add_class("devel");
|
||||
}
|
||||
|
||||
let weak_s = settings.downgrade();
|
||||
let weak_app = app.downgrade();
|
||||
window.connect_delete_event(move |window, _| {
|
||||
let app = match weak_app.upgrade() {
|
||||
Some(a) => a,
|
||||
None => return Inhibit(false),
|
||||
};
|
||||
window.connect_delete_event(
|
||||
clone!(@strong settings, @weak app => @default-return Inhibit(false), move |window, _| {
|
||||
info!("Saving window position");
|
||||
WindowGeometry::from_window(&window).write(&settings);
|
||||
|
||||
let settings = match weak_s.upgrade() {
|
||||
Some(s) => s,
|
||||
None => return 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)
|
||||
});
|
||||
info!("Application is exiting");
|
||||
let app = app.upcast::<gio::Application>();
|
||||
app.quit();
|
||||
Inhibit(false)
|
||||
}),
|
||||
);
|
||||
|
||||
// Create a content instance
|
||||
let content = Content::new(&sender).expect("Content initialization failed.");
|
||||
@ -128,8 +118,6 @@ impl MainWindow {
|
||||
|
||||
wrap.add(&header.bottom_switcher);
|
||||
|
||||
let updater = RefCell::new(None);
|
||||
|
||||
window.add(&wrap);
|
||||
|
||||
// Retrieve the previous window position and size.
|
||||
@ -138,20 +126,19 @@ impl MainWindow {
|
||||
// Update the feeds right after the Window is initialized.
|
||||
if settings.get_boolean("refresh-on-startup") {
|
||||
info!("Refresh on startup.");
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::schedule_refresh(s, sender.clone());
|
||||
utils::schedule_refresh(None, sender.clone());
|
||||
}
|
||||
|
||||
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
|
||||
info!("Auto-refresh every {:?} seconds.", refresh_interval);
|
||||
|
||||
let r_sender = sender.clone();
|
||||
gtk::timeout_add_seconds(refresh_interval, move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::schedule_refresh(s, r_sender.clone());
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
gtk::timeout_add_seconds(
|
||||
refresh_interval,
|
||||
clone!(@strong sender => move || {
|
||||
utils::schedule_refresh(None, sender.clone());
|
||||
glib::Continue(true)
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
app: app.clone(),
|
||||
@ -161,7 +148,7 @@ impl MainWindow {
|
||||
content,
|
||||
player,
|
||||
updating: Cell::new(false),
|
||||
updater,
|
||||
updater: RefCell::new(None),
|
||||
sender,
|
||||
receiver,
|
||||
}
|
||||
@ -176,41 +163,45 @@ impl MainWindow {
|
||||
// Create the `refresh` action.
|
||||
//
|
||||
// This will trigger a refresh of all the shows in the database.
|
||||
action(&self.window, "refresh", clone!(@strong sender => move |_, _| {
|
||||
gtk::idle_add(clone!(@strong sender => move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::schedule_refresh(s, sender.clone());
|
||||
glib::Continue(false)
|
||||
action(&self.window, "refresh",
|
||||
clone!(@strong sender => move |_, _| {
|
||||
gtk::idle_add(
|
||||
clone!(@strong sender => move || {
|
||||
utils::schedule_refresh(None, sender.clone());
|
||||
glib::Continue(false)
|
||||
}));
|
||||
}));
|
||||
self.app.set_accels_for_action("win.refresh", &["<primary>r"]);
|
||||
|
||||
// Create the `OPML` import action
|
||||
action(&self.window, "import", clone!(@strong sender, @weak self.window as win => move |_, _| {
|
||||
utils::on_import_clicked(&win, &sender);
|
||||
action(&self.window, "import",
|
||||
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 |_, _| {
|
||||
utils::on_export_clicked(&win, &sender);
|
||||
action(&self.window, "export",
|
||||
clone!(@strong sender, @weak self.window as window => move |_, _| {
|
||||
utils::on_export_clicked(&window, &sender);
|
||||
}));
|
||||
|
||||
// Create the action that shows a `gtk::AboutDialog`
|
||||
action(&self.window, "about", clone!(@weak self.window as win => move |_, _| {
|
||||
about_dialog(&win);
|
||||
action(&self.window, "about",
|
||||
clone!(@weak self.window as win => move |_, _| {
|
||||
about_dialog(&win);
|
||||
}));
|
||||
|
||||
// Create the quit action
|
||||
let weak_instance = self.app.downgrade();
|
||||
action(&self.window, "quit", move |_, _| {
|
||||
weak_instance.upgrade().map(|app| app.quit());
|
||||
});
|
||||
action(&self.window, "quit",
|
||||
clone!(@weak self.app as app => move |_, _| {
|
||||
app.quit();
|
||||
}));
|
||||
self.app.set_accels_for_action("win.quit", &["<primary>q"]);
|
||||
|
||||
// Create the menu action
|
||||
let header = Rc::downgrade(&self.headerbar);
|
||||
action(&self.window, "menu", move |_, _| {
|
||||
header.upgrade().map(|h| h.open_menu());
|
||||
});
|
||||
// Create the menu actions
|
||||
action(&self.window, "menu",
|
||||
clone!(@weak self.headerbar as headerbar => move |_, _| {
|
||||
headerbar.open_menu();
|
||||
}));
|
||||
// Bind the hamburger menu button to `F10`
|
||||
self.app.set_accels_for_action("win.menu", &["F10"]);
|
||||
}
|
||||
|
||||
@ -6,16 +6,14 @@ set -x
|
||||
|
||||
# $1 Passed by meson and should be the builddir
|
||||
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
|
||||
# sdk-extension binaries to the path
|
||||
if [ -f "/.flatpak-info" ]
|
||||
then
|
||||
export PATH="$PATH:/usr/lib/sdk/rust-stable/bin"
|
||||
# This assumes its run inside a Builder terminal
|
||||
export CARGO_TARGET_DIR="$BUILDDIR/target/"
|
||||
fi
|
||||
|
||||
export CARGO_HOME="$CARGO_TARGET_DIR/cargo-home"
|
||||
|
||||
cargo test -j 1 -- --test-threads=1 --nocapture
|
||||
cargo fetch --locked
|
||||
cargo test --all-features --offline -- --test-threads=1 --nocapture
|
||||
|
||||
Loading…
Reference in New Issue
Block a user