51 Commits
Author SHA1 Message Date
Jordan Petridis 85983ec490 Load **every** episode for benchmark purposes. 2018-04-07 00:26:02 +03:00
Jordan Petridis 7abf6bcade Lazy_load: remove unnecessary clone of an Rc pointer. 2018-04-06 23:12:32 +03:00
Jordan Petridis 854581f0bf Lazy_load: Use IntoIterator for T, instead of Iterator. 2018-04-06 22:56:44 +03:00
Jordan Petridis c79a92f3b2 Lazy_load: accept an iterator instead a Vec<_> over T. 2018-04-06 22:37:40 +03:00
Jordan Petridis 4d6c3a67b1 Lazy_load: Avoid manually indexing.
make the data: Vec<T> mutable, then reverse the vector
so it can be used as a stack, and then use the ::pop()
method to retrieve the item.

This also avoid the constrain for Clone on T.
2018-04-06 22:37:36 +03:00
Jordan Petridis 83abb5a825 Move the lazy_load logic to a Generic function. 2018-04-06 22:37:33 +03:00
Jordan Petridis d618771125 EpisodesListBox: Do not block while fetching episode backlog. 2018-04-06 17:06:14 +00:00
Jordan Petridis 45c9fd308d EpisodesListBox: Add each widget lazyly. 2018-04-06 17:06:14 +00:00
Jordan Petridis 0c00ee1320 ShowWidget: Initial Lazier evaluation of the widgets. 2018-04-06 17:06:13 +00:00
Jordan Petridis d332636dd4 Fix the fix that should have fixed the tests. 2018-04-06 19:36:52 +03:00
Jordan Petridis 32654f6cb2 Fix the tests. 2018-04-06 19:18:55 +03:00
Jordan Petridis 14a90e7138 Remove Futures_Cpupool.
The performance boost is not good enough to justify the
code complexity it add and the memory overhead of
yeat another threadpool.

We will start refactoring the whole pipeline implemantation
and might transition to either rayon-futures or tokio-runtime.
2018-04-06 18:18:03 +03:00
Jordan Petridis dd2366a15e Change the git url of the html2pango crate. 2018-04-05 20:44:28 +03:00
Jordan Petridis 370ba2d461 dlmanager: minor cleanup. 2018-04-04 21:36:23 +03:00
Jordan Petridis ef655ef5e0 EpisodeWidget: Keep the widget's heigth contant. Fixes #57 2018-04-04 18:44:23 +03:00
Jordan Petridis 5cc550c830 NewEpisode: refactor a closure to that returned Option<T> to use .and_then instead. 2018-04-04 16:32:03 +03:00
Jordan Petridis 52cbe67756 NewEpisode: refactor another if else statement and document it. 2018-04-04 16:31:59 +03:00
Jordan Petridis c910e0af40 NewPodcast: refactor an if else statement and document it. 2018-04-04 16:31:50 +03:00
Jordan Petridis 7a74534285 Merge branch '25-render-show-description' into 'master'
Resolve "Render link/bold/url attributes in ShowWidget description"

Closes #25

See merge request alatiera/hammond!30
2018-04-03 19:51:49 +00:00
Jordan Petridis 916775462d Update changelog. 2018-04-03 19:42:13 +00:00
Jordan Petridis 3d98600126 h-data: Sanitize html during Podcast/Episode parsing. 2018-04-03 19:42:13 +00:00
Jordan Petridis 7ba834ee8d Update deps now that we are at it. 2018-04-03 19:42:12 +00:00
Jordan Petridis 1c527cba03 Remove more commented out dead code. 2018-04-03 19:42:12 +00:00
Jordan Petridis 2d7ba7b246 h-data/source.rs: Reduce boilerplate. 2018-04-03 19:42:11 +00:00
Jordan Petridis 4ed70a8011 Rss::Error is now Send! 2018-04-03 19:42:11 +00:00
Jordan Petridis 7a3a148359 Remove more dead code. 2018-04-03 19:42:11 +00:00
Jordan Petridis e07e35110d Use pretty assertions! 2018-04-03 19:42:10 +00:00
Jordan Petridis a463753c84 NewEpidode: Use parse rss.description instead of itunes.summary.
We can deal with(sort of) html now, so we should start indexing
the proper rss description. Also cleanup commented out code.
2018-04-03 19:42:10 +00:00
Jordan Petridis a946ddfab1 html_to_pango: Switch to use the new library spawn from this.
Thanks to @danigm for spinning that part of fractal to a shared library.
2018-04-03 19:42:09 +00:00
Jordan Petridis af1cb43bd6 NewPodcast: Prefer the rss.description attribute.
Since we can handle rendering html stuff by converting it to pango
we no longer need the text-only itunes summary attribure.
2018-04-03 19:42:09 +00:00
Jordan Petridis 935d61324f ShowWidget: Convert html to pango markup and render it.
Instead of stipping all the html tags and just using the text
in the label we could *try* converting it to pango markup
which is a bit more flexible than plain text.

The code was copied from Fractal.
2018-04-03 19:42:08 +00:00
Jordan Petridis 10e016f2ea update appdata.xml 2018-04-03 21:41:53 +03:00
Jordan Petridis 154655d571 Readme: Fix the dependancies listed to include rustc and cargo. 2018-04-03 20:49:30 +03:00
Jordan Petridis 491cd8f01c cargo fmt 2018-03-30 17:24:38 +03:00
Jordan Petridis 2b711ff04c Update .doap file 2018-03-30 16:47:09 +03:00
Jordan Petridis 07eadd2364 h-gtk/utils: Improve itunes resolver and add extra test cases. 2018-03-30 16:38:59 +03:00
Jordan Petridis 7086afe73d h-gtk/utils: More refactor to improve formatting. 2018-03-30 15:33:19 +03:00
Jordan Petridis f21398357b h-gtk/utils: Refactor some mutex locks, improve formatting. 2018-03-30 14:39:30 +03:00
Jordan Petridis c338802329 Update deps. 2018-03-30 11:49:54 +03:00
Jordan Petridis c3076e748e cargo fmt 2018-03-30 10:22:34 +03:00
Jordan Petridis 1595256c86 Use rayon to manage all the threads. 2018-03-30 09:31:25 +03:00
Jordan Petridis 0623592f75 Merge branch '7-async-image-loading' into 'master'
Resolve "Make image loading not blocking the programs execution."

Closes #7

See merge request alatiera/Hammond!29
2018-03-29 14:57:44 +00:00
Jordan Petridis f2444f151c h-gtk/utils: Re-work format_err! calls and improve formatting 2018-03-29 15:26:44 +03:00
Jordan Petridis 710a3f2552 Use SendCell::try_get instead of SendCell::into_inner 2018-03-29 15:19:13 +03:00
Jordan Petridis 6071c664e7 Update changelog 2018-03-29 13:32:57 +03:00
Jordan Petridis e203815f4f hammond-gtk/utils.rs: Use a hashset to keep track of cover downloads.
Use a HashSet to check if a download of a cover is already active. If
it is, schedule a callback that will try to set the image from the
cached pixbuf later.
2018-03-29 13:07:46 +03:00
Jordan Petridis c3658080d3 Comment out a test. 2018-03-29 11:37:31 +03:00
Jordan Petridis 8703470010 h-gtk/utils: Use a threadpool to avoid spawning a million threads 2018-03-29 10:24:02 +03:00
Jordan Petridis 88cc7e6fec Fix set_image_from_path test 2018-03-29 09:21:49 +03:00
Jordan Petridis badcbc32c6 Implement async loading of the Show covers. 2018-03-28 22:41:45 +03:00
Jordan Petridis daa8f15ce9 hammond-gtk::utils: change the signature of get_pixbug_from_path and rename it
Requires a gtk::Image as argument now, it sets the pixbuf to the
img directly instead of returning it.
New name is set_image_from_path.

This is ground work so we can later keep the image reference, and
use it to set the image with a callback.
2018-03-28 21:47:10 +03:00
30 changed files with 753 additions and 629 deletions
+4
View File
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
* Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
[#7](https://gitlab.gnome.org/alatiera/Hammond/issues/7)
* Bold, italics links and some other `html` tags can now be rendered in the Show Description.
[#25](https://gitlab.gnome.org/alatiera/Hammond/issues/25)
## [0.3.1] - 2018-03-28
Generated
+352 -274
View File
File diff suppressed because it is too large Load Diff
View File
+10 -5
View File
@@ -60,7 +60,7 @@ flatpak build-bundle repo hammond org.gnome.Hammond
```sh
git clone https://gitlab.gnome.org/alatiera/hammond.git
cd Hammond/
cd hammond/
./configure --prefix=/usr/local
make && sudo make install
```
@@ -71,26 +71,29 @@ You can run `sudo make uninstall` for removal
#### Dependencies
* Rust stable 1.22 or later.
* Rust stable 1.22 or later along with cargo.
* Gtk+ 3.22 or later
* Meson
* A network connection
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
**Debian/Ubuntu**
```sh
apt-get update -yqq
apt-get install -yqq --no-install-recommends build-essential
apt-get install -yqq --no-install-recommends libgtk-3-dev meson
apt-get install -yqq --no-install-recommends rustc cargo libgtk-3-dev meson
```
**Fedora**
```sh
dnf install -y gtk3-devel glib2-devel openssl-devel sqlite-devel meson
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson
```
If you happen to build it on other distributions please let me know the names
of the corresponding libraries. Feel free to open a PR or an Issue to note it.
of the corresponding libraries. Feel free to open a MR or an Issue to note it.
## Contributing
@@ -139,3 +142,5 @@ Hammond's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Desi
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
And almost the entirety of the build system is copied from the [Fractal](https://gitlab.gnome.org/danigm/fractal) project.
[vendor]: https://github.com/alexcrichton/cargo-vendor
+8 -8
View File
@@ -6,9 +6,9 @@ workspace = "../"
[dependencies]
ammonia = "1.1.0"
chrono = "0.4.0"
chrono = "0.4.1"
derive_builder = "0.5.1"
itertools = "0.7.7"
itertools = "0.7.8"
lazy_static = "1.0.0"
log = "0.4.1"
rayon = "1.0.1"
@@ -16,12 +16,11 @@ rfc822_sanitizer = "0.3.3"
rss = "1.4.0"
url = "1.7.0"
xdg = "2.1.0"
futures = "0.1.18"
hyper = "0.11.22"
tokio-core = "0.1.15"
futures = "0.1.21"
hyper = "0.11.24"
tokio-core = "0.1.16"
hyper-tls = "0.1.3"
native-tls = "0.1.5"
futures-cpupool = "0.1.8"
num_cpus = "1.8.0"
failure = "0.1.1"
failure_derive = "0.1.1"
@@ -36,8 +35,9 @@ version = "1.1.0"
[dev-dependencies]
rand = "0.4.2"
tempdir = "0.3.6"
criterion = "0.2.1"
tempdir = "0.3.7"
criterion = "0.2.2"
pretty_assertions = "0.5.1"
[[bench]]
name = "bench"
-2
View File
@@ -3,7 +3,6 @@ extern crate criterion;
use criterion::Criterion;
// extern crate futures;
// extern crate futures_cpupool;
extern crate hammond_data;
extern crate hyper;
extern crate hyper_tls;
@@ -15,7 +14,6 @@ extern crate rss;
// use rayon::prelude::*;
// use futures::future::*;
// use futures_cpupool::CpuPool;
use tokio_core::reactor::Core;
use hammond_data::FeedBuilder;
+12 -2
View File
@@ -175,13 +175,23 @@ pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
let db = connection();
let con = db.get()?;
Episode::belonging_to(parent)
.count()
.get_result(&con)
.map_err(From::from)
}
pub fn get_pd_episodeswidgets(_parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
episode.select((rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id))
.filter(podcast_id.eq(parent.id()))
// .filter(podcast_id.eq(parent.id()))
// .group_by(epoch)
.order(epoch.desc())
.load::<EpisodeWidgetQuery>(&con)
+8 -9
View File
@@ -3,16 +3,10 @@ use diesel::r2d2;
use diesel_migrations::RunMigrationsError;
use hyper;
use native_tls;
// use rss;
use rss;
use url;
use std::io;
// use std::fmt;
// fadsadfs NOT SYNC
// #[derive(Fail, Debug)]
// #[fail(display = "RSS Error: {}", _0)]
// struct RSSError(rss::Error);
#[derive(Fail, Debug)]
pub enum DataError {
@@ -34,8 +28,7 @@ pub enum DataError {
#[fail(display = "IO Error: {}", _0)]
IOError(#[cause] io::Error),
#[fail(display = "RSS Error: {}", _0)]
// Rss::Error is not yet Sync
RssCrateError(String),
RssError(#[cause] rss::Error),
#[fail(display = "Error: {}", _0)]
Bail(String),
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
@@ -100,6 +93,12 @@ impl From<io::Error> for DataError {
}
}
impl From<rss::Error> for DataError {
fn from(err: rss::Error) -> Self {
DataError::RssError(err)
}
}
impl From<String> for DataError {
fn from(err: String) -> Self {
DataError::Bail(err)
+4 -1
View File
@@ -20,6 +20,10 @@
//! FIXME: Docs
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate derive_builder;
#[macro_use]
@@ -38,7 +42,6 @@ extern crate log;
extern crate ammonia;
extern crate chrono;
extern crate futures;
extern crate futures_cpupool;
extern crate hyper;
extern crate hyper_tls;
extern crate itertools;
+19 -24
View File
@@ -10,7 +10,7 @@ use errors::DataError;
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
use parser;
use schema::episode;
use utils::{replace_extra_spaces, url_cleaner};
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
@@ -196,11 +196,14 @@ impl NewEpisodeMinimal {
let title = item.title().unwrap().trim().to_owned();
let guid = item.guid().map(|s| s.value().trim().to_owned());
let uri = if let Some(url) = item.enclosure().map(|s| url_cleaner(s.url())) {
Some(url)
} else if item.link().is_some() {
item.link().map(|s| url_cleaner(s))
} else {
let uri = item.enclosure()
.map(|s| url_cleaner(s.url()))
// Fallback to Rss.Item.link if enclosure is None.
.or_else(|| item.link().map(|s| url_cleaner(s)));
// If url is still None return an Error as this behaviour is
// compliant with the RSS Spec.
if uri.is_none() {
let err = DataError::ParseEpisodeError {
reason: "No url specified for the item.".into(),
parent_id,
@@ -212,7 +215,7 @@ impl NewEpisodeMinimal {
// Default to rfc2822 represantation 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.
// Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday.
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
let duration = parser::parse_itunes_duration(item.itunes_ext());
@@ -228,18 +231,10 @@ impl NewEpisodeMinimal {
.map_err(From::from)
}
// TODO: TryInto is stabilizing in rustc v1.26!
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
let length = || -> Option<i32> { item.enclosure().map(|x| x.length().parse().ok())? }();
// Prefer itunes summary over rss.description since many feeds put html into
// rss.description.
let summary = item.itunes_ext().map(|s| s.summary()).and_then(|s| s);
let description = if summary.is_some() {
summary.map(|s| replace_extra_spaces(&ammonia::clean(s)))
} else {
item.description()
.map(|s| replace_extra_spaces(&ammonia::clean(s)))
};
let length = item.enclosure().and_then(|x| x.length().parse().ok());
let description = item.description().map(|s| ammonia::clean(s));
NewEpisodeBuilder::default()
.title(self.title)
@@ -431,12 +426,12 @@ mod tests {
.unwrap()
};
static ref EXPECTED_LUP_2: NewEpisode = {
let descr = "The Gnome project is about to solve one of our audience's biggest \
Wayland’s concerns. But as the project takes on a new level of \
relevance, decisions for the next version of Gnome have us worried about \
the future.\nPlus we chat with Wimpy about the Ubuntu Rally in NYC, \
Microsoft’s sneaky move to turn Windows 10 into the “ULTIMATE LINUX \
RUNTIME”, community news &amp; more!";
let descr =
"<p>The Gnome project is about to solve one of our audience's biggest Wayland’s \
concerns. But as the project takes on a new level of relevance, decisions for \
the next version of Gnome have us worried about the future.</p>\n\n<p>Plus we \
chat with Wimpy about the Ubuntu Rally in NYC, Microsoft’s sneaky move to turn \
Windows 10 into the “ULTIMATE LINUX RUNTIME”, community news &amp; more!</p>";
NewEpisodeBuilder::default()
.title("Gnome Does it Again | LUP 213")
+16 -26
View File
@@ -1,7 +1,6 @@
use ammonia;
use diesel;
use diesel::prelude::*;
use ammonia;
use rss;
use errors::DataError;
@@ -11,7 +10,7 @@ use schema::podcast;
use database::connection;
use dbqueries;
use utils::{replace_extra_spaces, url_cleaner};
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "podcast"]
@@ -90,24 +89,15 @@ impl NewPodcast {
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
let title = chan.title().trim();
// Prefer itunes summary over rss.description since many feeds put html into
// rss.description.
let summary = chan.itunes_ext().map(|s| s.summary()).and_then(|s| s);
let description = if let Some(sum) = summary {
replace_extra_spaces(&ammonia::clean(sum))
} else {
replace_extra_spaces(&ammonia::clean(chan.description()))
};
let description = ammonia::clean(chan.description().trim());
let link = url_cleaner(chan.link());
// Try to get the itunes img first
let itunes_img = chan.itunes_ext()
.and_then(|s| s.image())
.map(|s| s.to_owned());
let image_uri = if itunes_img.is_some() {
itunes_img
} else {
chan.image().map(|s| s.url().to_owned())
};
// If itunes is None, try to get the channel.image from the rss spec
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().to_owned()));
NewPodcastBuilder::default()
.title(title)
@@ -169,7 +159,7 @@ mod tests {
let descr = "The people behind The Intercept’s fearless reporting and incisive \
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
others—discuss the crucial issues of our time: national security, civil \
liberties, foreign policy, and criminal justice. Plus interviews with \
liberties, foreign policy, and criminal justice. Plus interviews with \
artists, thinkers, and newsmakers who challenge our preconceptions about \
the world we live in.";
@@ -203,15 +193,15 @@ mod tests {
.unwrap()
};
static ref EXPECTED_TIPOFF: NewPodcast = {
let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes \
of some of the best investigative journalism from recent years. Each \
episode we’ll be digging into an investigative scoop- hearing from the \
journalists behind the work as they tell us about the leads, the \
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
scenes of some of the best investigative journalism from recent years. \
Each episode we’ll be digging into an investigative scoop- hearing from \
the journalists behind the work as they tell us about the leads, the \
dead-ends and of course, the tip offs. There’ll be car chases, slammed \
doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \
with despotic regimes and much more. So if you’re curious about the fun, \
complicated detective work that goes into doing great investigative \
journalism- then this is the podcast for you.";
journalism- then this is the podcast for you.</p>";
NewPodcastBuilder::default()
.title("The Tip Off")
@@ -231,9 +221,9 @@ mod tests {
love, a crashed UFO, an alien body, and an impossible heist unlike any \
ever attempted - scripted by Mac Rogers, the award-winning playwright \
and writer of the multi-million download The Message and LifeAfter.</p>";
let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
NewPodcastBuilder::default()
.title("Steal the Stars")
+22 -76
View File
@@ -11,7 +11,6 @@ use hyper_tls::HttpsConnector;
// use futures::future::ok;
use futures::prelude::*;
use futures_cpupool::CpuPool;
use database::connection;
use errors::DataError;
@@ -102,6 +101,14 @@ impl Source {
Ok(())
}
fn make_err(self, context: &str, code: StatusCode) -> DataError {
DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: context.into(),
}
}
// TODO match on more stuff
// 301: Moved Permanently
// 304: Up to date Feed, checked with the Etag
@@ -115,75 +122,21 @@ impl Source {
fn match_status(mut self, res: Response) -> Result<(Self, Response), DataError> {
self.update_etag(&res)?;
let code = res.status();
match code {
StatusCode::NotModified => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "304: skipping..".into(),
};
return Err(err);
}
match code {
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
StatusCode::MovedPermanently => {
error!("Feed was moved permanently.");
self.handle_301(&res)?;
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "301: Feed was moved permanently.".into(),
};
return Err(err);
return Err(self.make_err("301: Feed was moved permanently.", code));
}
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
StatusCode::Unauthorized => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "401: Unauthorized.".into(),
};
return Err(err);
}
StatusCode::Forbidden => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "403: Forbidden.".into(),
};
return Err(err);
}
StatusCode::NotFound => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "404: Not found.".into(),
};
return Err(err);
}
StatusCode::RequestTimeout => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "408: Request Timeout.".into(),
};
return Err(err);
}
StatusCode::Gone => {
let err = DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: "410: Feed was deleted..".into(),
};
return Err(err);
}
StatusCode::Unauthorized => return Err(self.make_err("401: Unauthorized.", code)),
StatusCode::Forbidden => return Err(self.make_err("403: Forbidden.", code)),
StatusCode::NotFound => return Err(self.make_err("404: Not found.", code)),
StatusCode::RequestTimeout => return Err(self.make_err("408: Request Timeout.", code)),
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
_ => info!("HTTP StatusCode: {}", code),
};
Ok((self, res))
@@ -225,12 +178,11 @@ impl Source {
pub fn into_feed(
self,
client: &Client<HttpsConnector<HttpConnector>>,
pool: CpuPool,
ignore_etags: bool,
) -> Box<Future<Item = Feed, Error = DataError>> {
let id = self.id();
let feed = self.request_constructor(client, ignore_etags)
.and_then(move |(_, res)| response_to_channel(res, pool))
.and_then(move |(_, res)| response_to_channel(res))
.and_then(move |chan| {
FeedBuilder::default()
.channel(chan)
@@ -283,21 +235,16 @@ impl Source {
}
#[allow(needless_pass_by_value)]
fn response_to_channel(
res: Response,
pool: CpuPool,
) -> Box<Future<Item = Channel, Error = DataError> + Send> {
fn response_to_channel(res: Response) -> Box<Future<Item = Channel, Error = DataError> + Send> {
let chan = res.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).or_else(|err| Err(DataError::RssCrateError(format!("{}", err))))
});
let cpu_chan = pool.spawn(chan);
Box::new(cpu_chan)
.and_then(|buf| Channel::from_str(&buf).map_err(From::from));
Box::new(chan)
}
#[cfg(test)]
@@ -312,7 +259,6 @@ mod tests {
fn test_into_feed() {
truncate_db().unwrap();
let pool = CpuPool::new_num_cpus();
let mut core = Core::new().unwrap();
let client = Client::configure()
.connector(HttpsConnector::new(4, &core.handle()).unwrap())
@@ -323,7 +269,7 @@ mod tests {
let source = Source::from_url(url).unwrap();
let id = source.id();
let feed = source.into_feed(&client, pool.clone(), true);
let feed = source.into_feed(&client, true);
let feed = core.run(feed).unwrap();
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
+6 -27
View File
@@ -2,7 +2,6 @@
//! Docs.
use futures::future::*;
use futures_cpupool::CpuPool;
// use futures::prelude::*;
use hyper::Client;
@@ -20,23 +19,6 @@ use models::{IndexState, NewEpisode, NewEpisodeMinimal};
// use std::sync::{Arc, Mutex};
macro_rules! clone {
(@param _) => ( _ );
(@param $x:ident) => ( $x );
($($n:ident),+ => move || $body:expr) => (
{
$( let $n = $n.clone(); )+
move || $body
}
);
($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
{
$( let $n = $n.clone(); )+
move |$(clone!(@param $p),)+| $body
}
);
}
/// The pipline to be run for indexing and updating a Podcast feed that originates from
/// `Source.uri`.
///
@@ -47,13 +29,12 @@ pub fn pipeline<S: IntoIterator<Item = Source>>(
sources: S,
ignore_etags: bool,
tokio_core: &mut Core,
pool: &CpuPool,
client: Client<HttpsConnector<HttpConnector>>,
) -> Result<(), DataError> {
let list: Vec<_> = sources
.into_iter()
.map(clone!(pool => move |s| s.into_feed(&client, pool.clone(), ignore_etags)))
.map(|fut| fut.and_then(clone!(pool => move |feed| pool.clone().spawn(feed.index()))))
.map(move |s| s.into_feed(&client, ignore_etags))
.map(|fut| fut.and_then(|feed| feed.index()))
.map(|fut| fut.map(|_| ()).map_err(|err| error!("Error: {}", err)))
.collect();
@@ -67,26 +48,24 @@ pub fn pipeline<S: IntoIterator<Item = Source>>(
Ok(())
}
/// Creates a tokio `reactor::Core`, a `CpuPool`, and a `hyper::Client` and
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
/// runs the pipeline.
pub fn run(sources: Vec<Source>, ignore_etags: bool) -> Result<(), DataError> {
if sources.is_empty() {
return Ok(());
}
let pool = CpuPool::new_num_cpus();
let mut core = Core::new()?;
let handle = core.handle();
let client = Client::configure()
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle);
pipeline(sources, ignore_etags, &mut core, &pool, client)
pipeline(sources, ignore_etags, &mut core, client)
}
/// Docs
pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataError> {
let pool = CpuPool::new_num_cpus();
let mut core = Core::new()?;
let handle = core.handle();
@@ -94,8 +73,8 @@ pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataErro
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle);
let work = s.into_feed(&client, pool.clone(), ignore_etags)
.and_then(clone!(pool => move |feed| pool.clone().spawn(feed.index())))
let work = s.into_feed(&client, ignore_etags)
.and_then(move |feed| feed.index())
.map(|_| ());
core.run(work)
-35
View File
@@ -3,7 +3,6 @@
use chrono::prelude::*;
use rayon::prelude::*;
use itertools::Itertools;
use url::{Position, Url};
use dbqueries;
@@ -110,22 +109,6 @@ pub fn url_cleaner(s: &str) -> String {
}
}
/// Helper functions that strips extra spaces and newlines and ignores the tabs.
#[allow(match_same_arms)]
pub fn replace_extra_spaces(s: &str) -> String {
s.trim()
.chars()
.filter(|ch| *ch != '\t')
.coalesce(|current, next| match (current, next) {
('\n', '\n') => Ok('\n'),
('\n', ' ') => Ok('\n'),
(' ', '\n') => Ok('\n'),
(' ', ' ') => Ok(' '),
(_, _) => Err((current, next)),
})
.collect::<String>()
}
/// Returns the URI of a Podcast Downloads given it's title.
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
// It might be better to make it a hash of the title or the podcast rowid
@@ -296,24 +279,6 @@ mod tests {
assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url);
}
#[test]
fn test_whitespace() {
let bad_txt = "1 2 3 4 5";
let valid_txt = "1 2 3 4 5";
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
let bad_txt = "1 2 3 \n 4 5\n";
let valid_txt = "1 2 3\n4 5";
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
let bad_txt = "1 2 3 \n\n\n \n 4 5\n";
let valid_txt = "1 2 3\n4 5";
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
}
#[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
+4 -2
View File
@@ -6,11 +6,11 @@ workspace = "../"
[dependencies]
error-chain = "0.11.0"
hyper = "0.11.22"
hyper = "0.11.24"
log = "0.4.1"
mime_guess = "1.8.4"
reqwest = "0.8.5"
tempdir = "0.3.6"
tempdir = "0.3.7"
glob = "0.2.11"
failure = "0.1.1"
failure_derive = "0.1.1"
@@ -18,3 +18,5 @@ failure_derive = "0.1.1"
[dependencies.hammond-data]
path = "../hammond-data"
[dev-dependencies]
pretty_assertions = "0.5.1"
+4
View File
@@ -9,6 +9,10 @@ extern crate failure_derive;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate glob;
extern crate hammond_data;
extern crate hyper;
+8 -4
View File
@@ -6,8 +6,8 @@ version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.0"
dissolve = "0.2.2"
chrono = "0.4.1"
crossbeam-channel = "0.1.2"
gdk = "0.8.0"
gdk-pixbuf = "0.4.0"
glib = "0.5.0"
@@ -21,10 +21,11 @@ send-cell = "0.1.2"
url = "1.7.0"
failure = "0.1.1"
failure_derive = "0.1.1"
take_mut = "0.2.1"
take_mut = "0.2.2"
regex = "0.2.10"
reqwest = "0.8.5"
serde_json = "1.0.11"
serde_json = "1.0.13"
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
[dependencies.gtk]
features = ["v3_22"]
@@ -39,3 +40,6 @@ path = "../hammond-data"
[dependencies.hammond-downloader]
path = "../hammond-downloader"
[dev-dependencies]
pretty_assertions = "0.5.1"
+4 -5
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
@@ -31,6 +31,7 @@ Tobias Bernard
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="episode_container">
<property name="height_request">64</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
@@ -288,9 +289,8 @@ Tobias Bernard
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">False</property>
<property name="padding">6</property>
<property name="position">0</property>
</packing>
</child>
@@ -300,14 +300,13 @@ Tobias Bernard
<property name="no_show_all">True</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_bottom">6</property>
<property name="hexpand">True</property>
<property name="pulse_step">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="padding">6</property>
<property name="position">1</property>
</packing>
</child>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
@@ -33,14 +33,15 @@ Tobias Bernard
<object class="GtkBox" id="container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="pixel_size">64</property>
<property name="icon_name">image-x-generic-symbolic</property>
</object>
+1 -3
View File
@@ -112,12 +112,10 @@ Tobias Bernard
<property name="halign">start</property>
<property name="valign">end</property>
<property name="label">foo</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="max_width_chars">90</property>
<attributes>
<attribute name="weight" value="medium"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
@@ -5,19 +5,32 @@
<project_license>GPL-3.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Jordan Petridis</developer_name>
<summary>GNOME Podcast Client written in Rust</summary>
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<description>
Hammond is a Gtk+ Podcast client for the GNOME Desktop written in Rust
<summary>Modern Podcast Client for the GNOME desktop</summary>
<description xml:lang="en">
<p>
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
desktop written in Rust.
</p>
</description>
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<screenshots>
<screenshot>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/screenshots/podcasts_view.png</image>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/screenshots/podcast_widget.png</image>
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/episodes_view.png</image>
<caption>Page 1</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/shows_view.png</image>
<caption>Page 2</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/show_widget.png</image>
<caption>Page 3</caption>
</screenshot>
</screenshots>
<releases>
<release version="0.3.1" date="2018-03-28"/>
</releases>
<update_contact>jordanpetridis@protonmail.com</update_contact>
</component>
<url type="homepage">https://gitlab.gnome.org/alatiera/hammond</url>
<update_contact>jpetridis@gnome.org</update_contact>
<developer_name>Jordan Petridis and others</developer_name>
</component>
+2 -2
View File
@@ -7,6 +7,7 @@ use gtk::SettingsExt as GtkSettingsExt;
use gtk::prelude::*;
use failure::Error;
use rayon;
use hammond_data::{Podcast, Source};
use hammond_data::utils::delete_show;
@@ -20,7 +21,6 @@ use widgets::mark_all_watched;
use std::sync::Arc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time::Duration;
#[derive(Clone, Debug)]
@@ -222,7 +222,7 @@ impl App {
}
// Spawn a thread so it won't block the ui.
thread::spawn(clone!(pd => move || {
rayon::spawn(clone!(pd => move || {
if let Err(err) = delete_show(&pd) {
error!("Something went wrong trying to remove {}", pd.title());
error!("Error: {}", err);
+7 -3
View File
@@ -19,22 +19,26 @@ extern crate lazy_static;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate chrono;
extern crate dissolve;
extern crate crossbeam_channel;
extern crate hammond_data;
extern crate hammond_downloader;
extern crate html2pango;
extern crate humansize;
extern crate loggerv;
extern crate open;
extern crate rayon;
extern crate regex;
extern crate reqwest;
extern crate send_cell;
extern crate serde_json;
extern crate take_mut;
extern crate url;
// extern crate rayon;
// use rayon::prelude::*;
use log::Level;
use gtk::prelude::*;
+17 -21
View File
@@ -1,4 +1,5 @@
use failure::Error;
use rayon;
// use hammond_data::Episode;
use hammond_data::dbqueries;
@@ -11,7 +12,6 @@ use std::sync::{Arc, Mutex, RwLock};
use std::sync::mpsc::Sender;
// use std::sync::atomic::AtomicUsize;
// use std::path::PathBuf;
use std::thread;
// This is messy, undocumented and hacky af.
// I am terrible at writting downloaders and download managers.
@@ -75,40 +75,35 @@ impl DownloadProgress for Progress {
lazy_static! {
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
{ Arc::new(RwLock::new(HashMap::new())) };
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
}
pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> {
pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Error> {
// Create a new `Progress` struct to keep track of dl progress.
let prog = Arc::new(Mutex::new(Progress::default()));
{
let mut m = ACTIVE_DOWNLOADS
.write()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
m.insert(id, prog.clone());
}
match ACTIVE_DOWNLOADS.write() {
Ok(mut guard) => guard.insert(id, prog.clone()),
Err(err) => return Err(format_err!("ActiveDonwloads: {}.", err)),
};
let dir = directory.to_owned();
thread::spawn(move || {
DLPOOL.spawn(move || {
if let Ok(episode) = dbqueries::get_episode_from_rowid(id) {
let pid = episode.podcast_id();
let id = episode.rowid();
if let Err(err) = get_episode(&mut episode.into(), dir.as_str(), Some(prog)) {
if let Err(err) = get_episode(&mut episode.into(), directory.as_str(), Some(prog)) {
error!("Error while trying to download an episode");
error!("Error: {}", err);
}
{
if let Ok(mut m) = ACTIVE_DOWNLOADS.write() {
info!("Removed: {:?}", m.remove(&id));
}
if let Ok(mut m) = ACTIVE_DOWNLOADS.write() {
let foo = m.remove(&id);
debug!("Removed: {:?}", foo);
}
// {
// if let Ok(m) = ACTIVE_DOWNLOADS.read() {
// debug!("ACTIVE DOWNLOADS: {:#?}", m);
// }
// if let Ok(m) = ACTIVE_DOWNLOADS.read() {
// debug!("ACTIVE DOWNLOADS: {:#?}", m);
// }
sender
@@ -162,13 +157,14 @@ mod tests {
let (sender, _rx) = channel();
let download_fold = get_download_folder(&pd.title()).unwrap();
add(episode.rowid(), download_fold.as_str(), sender).unwrap();
let fold2 = download_fold.clone();
add(episode.rowid(), download_fold, sender).unwrap();
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
// Give it soem time to download the file
thread::sleep(time::Duration::from_secs(20));
let final_path = format!("{}/{}.mp3", &download_fold, episode.rowid());
let final_path = format!("{}/{}.mp3", &fold2, episode.rowid());
assert!(Path::new(&final_path).exists());
fs::remove_file(final_path).unwrap();
}
+119 -51
View File
@@ -1,8 +1,13 @@
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
use failure::Error;
use gdk_pixbuf::Pixbuf;
use gio::{Settings, SettingsExt};
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use rayon;
use regex::Regex;
use reqwest;
use send_cell::SendCell;
@@ -18,8 +23,7 @@ use hammond_downloader::downloader;
use std::collections::{HashMap, HashSet};
use std::sync::{Mutex, RwLock};
use std::sync::Arc;
use std::sync::mpsc::Sender;
use std::thread;
use std::sync::mpsc::*;
use app::Action;
@@ -30,22 +34,25 @@ lazy_static! {
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
}
pub fn ignore_show(id: i32) -> Result<(), Error> {
let mut guard = IGNORESHOWS.lock().map_err(|err| format_err!("{}", err))?;
guard.insert(id);
Ok(())
pub fn ignore_show(id: i32) -> Result<bool, Error> {
IGNORESHOWS
.lock()
.map(|mut guard| guard.insert(id))
.map_err(|err| format_err!("{}", err))
}
pub fn uningore_show(id: i32) -> Result<(), Error> {
let mut guard = IGNORESHOWS.lock().map_err(|err| format_err!("{}", err))?;
guard.remove(&id);
Ok(())
pub fn uningore_show(id: i32) -> Result<bool, Error> {
IGNORESHOWS
.lock()
.map(|mut guard| guard.remove(&id))
.map_err(|err| format_err!("{}", err))
}
pub fn get_ignored_shows() -> Result<Vec<i32>, Error> {
let guard = IGNORESHOWS.lock().map_err(|err| format_err!("{}", err))?;
let keys = guard.iter().cloned().collect::<Vec<_>>();
Ok(keys)
IGNORESHOWS
.lock()
.map(|guard| guard.iter().cloned().collect::<Vec<_>>())
.map_err(|err| format_err!("{}", err))
}
pub fn cleanup(cleanup_date: DateTime<Utc>) {
@@ -82,7 +89,7 @@ pub fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(), Error> {
sender.send(Action::HeaderBarShowUpdateIndicator)?;
thread::spawn(move || {
rayon::spawn(move || {
let mut sources = source.unwrap_or_else(|| {
dbqueries::get_sources().expect("Failed to retrieve Sources from the database.")
});
@@ -128,6 +135,8 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
lazy_static! {
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
{ RwLock::new(HashMap::new()) };
static ref COVER_DL_REGISTRY: RwLock<HashSet<i32>> = RwLock::new(HashSet::new());
static ref THREADPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
}
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
@@ -137,25 +146,76 @@ lazy_static! {
// GObjects do not implement Send trait, so SendCell is a way around that.
// Also lazy_static requires Sync trait, so that's what the mutexes are.
// TODO: maybe use something that would just scale to requested size?
pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Result<Pixbuf, Error> {
{
let hashmap = CACHED_PIXBUFS
.read()
.map_err(|_| format_err!("Failed to get a lock on the pixbuf cache mutex."))?;
if let Some(px) = hashmap.get(&(pd.id(), size)) {
let m = px.lock()
.map_err(|_| format_err!("Failed to lock pixbuf mutex."))?;
return Ok(m.clone().into_inner());
pub fn set_image_from_path(
image: &gtk::Image,
pd: Arc<PodcastCoverQuery>,
size: u32,
) -> Result<(), Error> {
// Check if there's an active download about this show cover.
// If there is, a callback will be set so this function will be called again.
// If the download succedes, there should be a quick return from the pixbuf cache_image
// If it fails another download will be scheduled.
if let Ok(guard) = COVER_DL_REGISTRY.read() {
if guard.contains(&pd.id()) {
let callback = clone!(image, pd => move || {
let _ = set_image_from_path(&image, pd.clone(), size);
glib::Continue(false)
});
gtk::timeout_add(250, callback);
return Ok(());
}
}
let img_path = downloader::cache_image(pd)?;
let px = Pixbuf::new_from_file_at_scale(&img_path, size as i32, size as i32, true)?;
let mut hashmap = CACHED_PIXBUFS
.write()
.map_err(|_| format_err!("Failed to lock pixbuf mutex."))?;
hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone())));
Ok(px)
if let Ok(hashmap) = CACHED_PIXBUFS.read() {
// Check if the requested (cover + size) is already in the chache
// and if so do an early return after that.
if let Some(guard) = hashmap.get(&(pd.id(), size)) {
guard
.lock()
.map_err(|err| format_err!("SendCell Mutex: {}", err))
.and_then(|sendcell| {
sendcell
.try_get()
.map(|px| image.set_from_pixbuf(px))
.ok_or_else(|| format_err!("Pixbuf was accessed from a different thread"))
})?;
return Ok(());
}
}
let (sender, receiver) = channel();
let pd_ = pd.clone();
THREADPOOL.spawn(move || {
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
guard.insert(pd_.id());
}
let _ = sender.send(downloader::cache_image(&pd_));
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
guard.remove(&pd_.id());
}
});
let image = image.clone();
let s = size as i32;
gtk::timeout_add(25, move || {
if let Ok(path) = receiver.try_recv() {
if let Ok(path) = path {
if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) {
if let Ok(mut hashmap) = CACHED_PIXBUFS.write() {
hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone())));
image.set_from_pixbuf(&px);
}
}
}
glib::Continue(false)
} else {
glib::Continue(true)
}
});
Ok(())
}
#[inline]
@@ -181,10 +241,10 @@ fn itunes_id_from_url(url: &str) -> Option<u32> {
fn lookup_id(id: u32) -> Result<String, Error> {
let url = format!("https://itunes.apple.com/lookup?id={}&entity=podcast", id);
let req: Value = reqwest::get(&url)?.json()?;
// FIXME: First time using serde, this could be done better and avoid using [] for indexing.
let feedurl = req["results"][0]["feedUrl"].as_str();
let feedurl = feedurl.ok_or_else(|| format_err!("Failed to get url from itunes response"))?;
Ok(feedurl.into())
let rssurl = || -> Option<&str> { req.get("results")?.get(0)?.get("feedUrl")?.as_str() };
rssurl()
.map(From::from)
.ok_or_else(|| format_err!("Failed to get url from itunes response"))
}
pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
@@ -200,8 +260,8 @@ pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::Source;
use hammond_data::dbqueries;
// use hammond_data::Source;
// use hammond_data::dbqueries;
#[test]
fn test_time_period_to_duration() {
@@ -221,29 +281,34 @@ mod tests {
assert_eq!(time, time_period_to_duration(time, "seconds").num_seconds());
}
#[test]
// #[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
// to run it.
#[ignore]
fn test_get_pixbuf_from_path() {
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
// Create and index a source
let source = Source::from_url(url).unwrap();
// Copy it's id
let sid = source.id();
pipeline::run(vec![source], true).unwrap();
// #[ignore]
// Disabled till https://gitlab.gnome.org/alatiera/Hammond/issues/56
// fn test_set_image_from_path() {
// let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
// Create and index a source
// let source = Source::from_url(url).unwrap();
// Copy it's id
// let sid = source.id();
// pipeline::run(vec![source], true).unwrap();
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
assert!(pxbuf.is_ok());
}
// Get the Podcast
// let img = gtk::Image::new();
// let pd = dbqueries::get_podcast_from_source_id(sid).unwrap().into();
// let pxbuf = set_image_from_path(&img, Arc::new(pd), 256);
// assert!(pxbuf.is_ok());
// }
#[test]
fn test_itunes_to_rss() {
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap());
let itunes_url = "https://itunes.apple.com/podcast/id000000000000000";
assert!(itunes_to_rss(itunes_url).is_err());
}
#[test]
@@ -258,5 +323,8 @@ mod tests {
let id = 1195206601;
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
assert_eq!(rss_url, lookup_id(id).unwrap());
let id = 000000000;
assert!(lookup_id(id).is_err());
}
}
+4 -5
View File
@@ -7,9 +7,10 @@ use hammond_data::EpisodeWidgetQuery;
use hammond_data::dbqueries;
use app::Action;
use utils::{get_ignored_shows, get_pixbuf_from_path};
use utils::{get_ignored_shows, set_image_from_path};
use widgets::EpisodeWidget;
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
@@ -227,9 +228,7 @@ impl EpisodesViewWidget {
}
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
let pd = dbqueries::get_podcast_cover_from_id(podcast_id)?;
let img = get_pixbuf_from_path(&pd, 64)?;
self.image.set_from_pixbuf(&img);
Ok(())
let pd = Arc::new(dbqueries::get_podcast_cover_from_id(podcast_id)?);
set_image_from_path(&self.image, pd, 64)
}
}
+10 -12
View File
@@ -2,11 +2,11 @@ use failure::Error;
use gtk;
use gtk::prelude::*;
use hammond_data::Podcast;
use hammond_data::{Podcast, PodcastCoverQuery};
use hammond_data::dbqueries;
use app::Action;
use utils::{get_ignored_shows, get_pixbuf_from_path};
use utils::{get_ignored_shows, set_image_from_path};
use std::sync::Arc;
use std::sync::mpsc::Sender;
@@ -57,7 +57,7 @@ impl ShowsPopulated {
let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
podcasts.into_iter().map(Arc::new).for_each(|parent| {
podcasts.into_iter().for_each(|parent| {
let flowbox_child = ShowsChild::new(parent);
self.flowbox.add(&flowbox_child.child);
});
@@ -116,25 +116,23 @@ impl Default for ShowsChild {
}
impl ShowsChild {
pub fn new(pd: Arc<Podcast>) -> ShowsChild {
pub fn new(pd: Podcast) -> ShowsChild {
let child = ShowsChild::default();
child.init(pd);
child
}
fn init(&self, pd: Arc<Podcast>) {
fn init(&self, pd: Podcast) {
self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string());
if let Err(err) = self.set_cover(pd.clone()) {
let pd = Arc::new(pd.into());
if let Err(err) = self.set_cover(pd) {
error!("Failed to set a cover: {}", err)
}
WidgetExt::set_name(&self.child, &pd.id().to_string());
}
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
let image = get_pixbuf_from_path(&pd.clone().into(), 256)?;
self.cover.set_from_pixbuf(&image);
Ok(())
fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
set_image_from_path(&self.cover, pd, 256)
}
}
+50 -9
View File
@@ -5,6 +5,7 @@ use gtk::prelude::*;
use failure::Error;
use humansize::FileSize;
use open;
use rayon;
use take_mut;
use hammond_data::{EpisodeWidgetQuery, Podcast};
@@ -195,10 +196,10 @@ impl EpisodeWidget {
#[inline]
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) -> Result<(), Error> {
let pd = dbqueries::get_podcast_from_id(ep.podcast_id())?;
let download_fold = get_download_folder(&pd.title().to_owned())?;
let download_fold = get_download_folder(&pd.title())?;
// Start a new download.
manager::add(ep.rowid(), &download_fold, sender.clone())?;
manager::add(ep.rowid(), download_fold, sender.clone())?;
// Update Views
sender.send(Action::RefreshEpisodesView)?;
@@ -350,23 +351,63 @@ fn total_size_helper(
// delete_local_content(&mut ep).map_err(From::from).map(|_| ())
// }
pub fn episodes_listbox(pd: &Podcast, sender: Sender<Action>) -> Result<gtk::ListBox, Error> {
let episodes = dbqueries::get_pd_episodeswidgets(pd)?;
pub fn episodes_listbox(pd: Arc<Podcast>, sender: Sender<Action>) -> Result<gtk::ListBox, Error> {
use crossbeam_channel::TryRecvError::*;
use crossbeam_channel::bounded;
let count = dbqueries::get_pd_episodes_count(&pd)?;
let (sender_, receiver) = bounded(1);
rayon::spawn(move || {
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
sender_
.send(episodes)
.expect("Something terrible happened to the channnel");
});
let list = gtk::ListBox::new();
list.set_visible(true);
list.set_selection_mode(gtk::SelectionMode::None);
if episodes.is_empty() {
if count == 0 {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_show.ui");
let container: gtk::Box = builder.get_object("empty_show").unwrap();
list.add(&container);
return Ok(list);
}
episodes.into_iter().for_each(|ep| {
let widget = EpisodeWidget::new(ep, sender.clone());
list.add(&widget.container);
});
gtk::idle_add(clone!(list => move || {
let episodes = match receiver.try_recv() {
Ok(e) => e,
Err(Empty) => return glib::Continue(true),
Err(Disconnected) => return glib::Continue(false),
};
lazy_load(episodes, list.clone(), clone!(sender => move |ep| {
EpisodeWidget::new(ep, sender.clone()).container
}));
glib::Continue(false)
}));
Ok(list)
}
use gtk::{IsA, Widget};
fn lazy_load<T, U, P, Z>(data: T, container: Z, mut predicate: P)
where
T: IntoIterator + 'static,
T::Item: 'static,
Z: ContainerExt + 'static,
P: FnMut(T::Item) -> U + 'static,
U: IsA<Widget>,
{
let mut data = data.into_iter();
gtk::idle_add(move || {
data.next()
.map(|x| container.add(&predicate(x)))
.map(|_| glib::Continue(true))
.unwrap_or(glib::Continue(false))
});
}
+6 -12
View File
@@ -1,16 +1,15 @@
use dissolve;
use failure::Error;
// use glib;
use gtk;
use gtk::prelude::*;
use html2pango::markup_from_raw;
use open;
use hammond_data::Podcast;
use hammond_data::dbqueries;
use hammond_data::utils::replace_extra_spaces;
use app::Action;
use utils::get_pixbuf_from_path;
use utils::set_image_from_path;
use widgets::episode::episodes_listbox;
use std::sync::Arc;
@@ -74,7 +73,7 @@ impl ShowWidget {
}
}));
self.setup_listbox(&pd, sender.clone());
self.setup_listbox(pd.clone(), sender.clone());
self.set_description(pd.description());
if let Err(err) = self.set_cover(pd.clone()) {
@@ -106,24 +105,19 @@ impl ShowWidget {
}
/// Populate the listbox with the shows episodes.
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
fn setup_listbox(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
let listbox = episodes_listbox(pd, sender.clone());
listbox.ok().map(|l| self.episodes.add(&l));
}
/// Set the show cover.
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
let image = get_pixbuf_from_path(&pd.into(), 128)?;
self.cover.set_from_pixbuf(&image);
Ok(())
set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
}
/// Set the descripton text.
fn set_description(&self, text: &str) {
// TODO: Temporary solution until we render html urls/bold/italic probably with
// markup.
let desc = dissolve::strip_html_tags(text).join(" ");
self.description.set_text(&replace_extra_spaces(&desc));
self.description.set_markup(&markup_from_raw(text));
}
/// Set scrolled window vertical adjustment.
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Hammond</name>
<shortdesc xml:lang="en">
Modern Podcast Client for the GNOME desktop
</shortdesc>
<description xml:lang="en">
<p>
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
desktop written in Rust.
</p>
</description>
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> -->
<bug-database rdf:resource="https://gitlab.gnome.org/alatiera/Hammond/issues" />
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
<programming-language>Rust</programming-language>
<maintainer>
<foaf:Person>
<foaf:name>Jordan petridis</foaf:name>
<foaf:mbox rdf:resource="mailto:jpetridis@gnome.org" />
<gnome:userid>jpetridis</gnome:userid>
</foaf:Person>
</maintainer>
</Project>