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: ''
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

File diff suppressed because it is too large Load Diff

View File

@ -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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.\

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 sil 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 na pas encore dépisodes"
msgstr "Cette émission na 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 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…"
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 ladresse 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 lapplication"
@ -227,71 +217,54 @@ msgstr "Une notification daction intégrée à lapplication"
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 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."
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."
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 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"
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"

View File

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

View File

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

View File

@ -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">&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="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">&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="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">&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="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">&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="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">&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="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">&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="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">&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="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">&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="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">&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="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">&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>
</child>
</object>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]);
}

View File

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