9 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
58 changed files with 1882 additions and 2102 deletions
+5 -10
View File
@@ -5,9 +5,9 @@ stages:
.cargo_test_template: &cargo_test
stage: test
variables:
RUSTFLAGS: "--cfg rayon_unstable"
RUST_BACKTRACE: "FULL"
# variables:
# RUSTFLAGS: "-C link-dead-code"
# RUST_BACKTRACE: "FULL"
before_script:
- apt-get update -yqq
@@ -53,7 +53,7 @@ flatpak:
stage: test
script:
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
# https://gitlab.gnome.org/World/hammond/issues/55
# https://gitlab.gnome.org/alatiera/Hammond/issues/55
# Force regeneration of gresources regardless of artifacts chage
- flatpak-builder --run app org.gnome.Hammond.json glib-compile-resources --sourcedir=hammond-gtk/resources/ hammond-gtk/resources/resources.xml
@@ -98,15 +98,10 @@ rustfmt:
clippy:
image: "rustlang/rust:nightly"
stage: lint
variables:
RUSTFLAGS: "--cfg rayon_unstable"
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
script:
- rustc --version && cargo --version
- cargo install clippy --force
# Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo clippy --all
allow_failure: true
when: manual
+38 -56
View File
@@ -6,70 +6,52 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [0.3.2] - 2018-05-07
### Added:
- Vies now have a new fancy scrolling animation when they are refereshed.
### Changed:
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
[#7](https://gitlab.gnome.org/World/hammond/issues/7)
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
[#25](https://gitlab.gnome.org/World/hammond/issues/25)
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
- `EpisdeWidget`s are now loaded asynchronously accross views.
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
- `ShowWidget` layout was changed 9a5cc1595d982f3232ee7595b83b6512ac8f6c88
- `ShowWidget` Description is inside a scrolled window now
### Fixed:
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/hammond/issues/57)
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
### Removed:
- Removed the custom configuration file and replaced instructions to just use meson. 1f1d4af8ba7db8f56435d13a1c191ecff3d4a85b
* 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
### Added:
- Ability to mark all episodes of a Show as watched.
[#47](https://gitlab.gnome.org/World/hammond/issues/47)
- Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
[#49](https://gitlab.gnome.org/World/hammond/issues/49)
- Hammond now remembers the window size and position. (Rowan Lewis)
[#50](https://gitlab.gnome.org/World/hammond/issues/50)
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
[!22](https://gitlab.gnome.org/World/hammond/merge_requests/22) [!23](https://gitlab.gnome.org/World/hammond/merge_requests/23)
- Shows without episodes now display an empty message similar to EmptyView.
[#44](https://gitlab.gnome.org/World/hammond/issues/44)
### Changed:
- EpisdeWidget has been reimplemented as a compile time state machine.
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18)
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
[#35](https://gitlab.gnome.org/World/hammond/issues/35)
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
[#53](https://gitlab.gnome.org/World/hammond/issues/53)
### Fixed:
- Double border aroun the main window was fixed. (Rowan Lewis)
[#52](https://gitlab.gnome.org/World/hammond/issues/52)
* Ability to mark all episodes of a Show as watched.
[#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47)
* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
[#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49)
* EpisdeWidget has been reimplemented as a compile time state machine.
[!18](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/18)
* Content Views no longer scroll horizontally when shrunk bellow their minimum size.
[#35](https://gitlab.gnome.org/alatiera/Hammond/issues/35)
* Double border aroun the main window was fixed. (Rowan Lewis)
[#52](https://gitlab.gnome.org/alatiera/Hammond/issues/52)
* Some requests now use the Tor Browser's user agent. (Rowan Lewis)
[#53](https://gitlab.gnome.org/alatiera/Hammond/issues/53)
* Hammond now remembers the window size and position. (Rowan Lewis)
[#50](https://gitlab.gnome.org/alatiera/Hammond/issues/50)
* Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
[!22](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/22) [!23](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/23)
* Shows without episodes now display an empty message similar to EmptyView.
[#44](https://gitlab.gnome.org/alatiera/Hammond/issues/44)
## [0.3.0] - 2018-02-11
- Tobias Bernard Redesigned the whole Gtk+ client.
- Complete re-write of hammond-data and hammond-gtk modules.
- Error handling for all crates was migrated from error-chain to Failure.
- Hammond-data now uses futures to parse feeds.
- Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets.
* Tobias Bernard Redesigned the whole Gtk+ client.
* Complete re-write of hammond-data and hammond-gtk modules.
* Error handling for all crates was migrated from error-chain to Failure.
* Hammond-data now uses futures to parse feeds.
* Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets.
## [0.2.0] - 2017-11-28
- Database Schema Breaking Changes.
- Added url sanitization. #4.
- Reworked and refactored of the hammond-data API.
- Added some more unit tests
- Documented hammond-data public API.
* Database Schema Breaking Changes.
* Added url sanitization. #4.
* Reworked and refactored of the hammond-data API.
* Added some more unit tests
* Documented hammond-data public API.
## [0.1.1] - 2017-11-13
- Added appdata.xml file
* Added appdata.xml file
## [0.1.0] - 2017-11-13
- Initial Release
Initial Release
+2 -2
View File
@@ -12,7 +12,7 @@ Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOf
## Source repository
Hammond's main source repository is at gitlab.gnome.org. You can view
the web interface [here](https://gitlab.gnome.org/World/hammond)
the web interface [here](https://gitlab.gnome.org/alatiera/hammond)
Development happens in the master branch.
@@ -51,7 +51,7 @@ In order to run the test suite use the following: `cargo test -- --test-threads=
# Issues, issues and more issues!
There are many ways you can contribute to Hammond, and all of them involve creating issues
in [Hammond issue tracker](https://gitlab.gnome.org/World/hammond/issues). This is the entry point for your contribution.
in [Hammond issue tracker](https://gitlab.gnome.org/alatiera/Hammond/issues). This is the entry point for your contribution.
To create an effective and high quality ticket, try to put the following information on your
ticket:
Generated
+107 -167
View File
@@ -69,7 +69,7 @@ dependencies = [
[[package]]
name = "backtrace"
version = "0.3.6"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -84,7 +84,7 @@ name = "backtrace-sys"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
"cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "build_const"
version = "0.2.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -160,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cc"
version = "1.0.10"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -170,7 +170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "chrono"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -214,33 +214,33 @@ name = "crc"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "criterion"
version = "0.2.3"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion-plot 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion-stats 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion-stats 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"handlebars 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools-num 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "criterion-plot"
version = "0.2.3"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -250,7 +250,7 @@ dependencies = [
[[package]]
name = "criterion-stats"
version = "0.2.3"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -373,32 +373,31 @@ dependencies = [
[[package]]
name = "diesel"
version = "1.2.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_derives 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libsqlite3-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "diesel_derives"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "diesel_migrations"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"migrations_macros 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"migrations_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -429,7 +428,7 @@ name = "error-chain"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -437,7 +436,7 @@ name = "failure"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -672,17 +671,17 @@ name = "hammond-data"
version = "0.1.0"
dependencies = [
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_migrations 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_migrations 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -690,13 +689,10 @@ dependencies = [
"pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon-futures 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rss 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rss 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -710,7 +706,7 @@ dependencies = [
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"hammond-data 0.1.0",
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -722,7 +718,7 @@ dependencies = [
name = "hammond-gtk"
version = "0.1.0"
dependencies = [
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -743,8 +739,8 @@ dependencies = [
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
"send-cell 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -760,20 +756,18 @@ dependencies = [
"pest_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "html2pango"
version = "0.1.0"
source = "git+https://gitlab.gnome.org/World/html2pango#6bf3fe5e71459dc283148efe96a9c9f8c66d0791"
source = "git+https://gitlab.gnome.org/World/html2pango#63fb1ea644ef38bf7f268bbe8f40b19dd77d7995"
dependencies = [
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"marksman_escape 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -801,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hyper"
version = "0.11.25"
version = "0.11.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -816,7 +810,7 @@ dependencies = [
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -829,9 +823,9 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -932,15 +926,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "linkify"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -979,11 +965,6 @@ name = "maplit"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "marksman_escape"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "markup5ever"
version = "0.7.2"
@@ -991,9 +972,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1024,18 +1005,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "migrations_internals"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "migrations_macros"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1179,18 +1160,18 @@ dependencies = [
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.28 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl-sys"
version = "0.9.28"
version = "0.9.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
"cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1322,15 +1303,7 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "0.3.6"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1356,20 +1329,12 @@ name = "quote"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "quote"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1423,15 +1388,6 @@ dependencies = [
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon-futures"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.37"
@@ -1452,14 +1408,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.5.5"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1489,16 +1445,16 @@ dependencies = [
"bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_rs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1510,14 +1466,14 @@ name = "rfc822_sanitizer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rss"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1584,43 +1540,43 @@ dependencies = [
[[package]]
name = "send-cell"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.39"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_derive"
version = "1.0.39"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive_internals"
version = "0.23.1"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_json"
version = "1.0.15"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1630,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1639,7 +1595,7 @@ name = "simplelog"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1683,7 +1639,7 @@ dependencies = [
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
"precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1719,22 +1675,12 @@ dependencies = [
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1854,7 +1800,7 @@ dependencies = [
[[package]]
name = "tokio-core"
version = "0.1.17"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1863,11 +1809,11 @@ dependencies = [
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)",
"scoped-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-timer 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1900,7 +1846,7 @@ dependencies = [
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1968,7 +1914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2080,7 +2026,7 @@ dependencies = [
[[package]]
name = "vcpkg"
version = "0.2.3"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -2150,28 +2096,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
"checksum atk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8dc233521f7bffd3042c31082ea71bd08820abf44bac938fb36591e20f76f39"
"checksum atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "af80143d6f7608d746df1520709e5d141c96f240b0e62b0aa41bdfb53374d9d4"
"checksum backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbe525f66f42d207968308ee86bc2dd60aa5fab535b22e616323a173d097d8e"
"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2"
"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661"
"checksum base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "229d032f1a99302697f10b27167ae6d03d49d032e6a8e2550e8d3fc13356d2b4"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
"checksum build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e90dc84f5e62d2ebe7676b83c22d33b6db8bd27340fb6ffbff0a364efa0cb9c9"
"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87"
"checksum bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7db437d718977f6dc9b2e3fd6fc343c02ac6b899b73fdd2179163447bd9ce9"
"checksum c_vec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11f56b93adbca81a4ed1bb654070013b496a9b0b259a296281f4be24af84debd"
"checksum cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a110f269c2fd382df5fe8bd46dfa5f1b83608aa717fecb6e7a28c08c202f0e13"
"checksum cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0395175ecba60accac076a02c31d143b9dcd9d5eb5316d7163a3273803b765c7"
"checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427"
"checksum cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8b9d2900f78631a5876dc5d6c9033ede027253efcd33dd36b1309fc6cab97ee0"
"checksum cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2b4911e4bdcb4100c7680e7e854ff38e23f1b34d4d9e079efae3da2801341ffc"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6"
"checksum chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba5f60682a4c264e7f8d77b82e7788938a76befdf949d4a98026d19099c9d873"
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67"
"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d"
"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
"checksum criterion 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4f11151e2961d0483e5eb7a2ede5ed8071a460d04d2b7c89e8257aa5502b0e0b"
"checksum criterion-plot 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f7f7c88a8d341dd9fd9e31a72ca2ca24428db79afb491852873b2c784e037e6"
"checksum criterion-stats 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dd48feb0253b2968ff3085e7f3fba6738c9ff859f420a2fb81a48986eb66da36"
"checksum criterion 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4556caa5b5e69626a0b69c4892baa6ab518cf2e802384a5eeef15dd4416d4771"
"checksum criterion-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "029914bacd6bb0a521429ff8df0c6c79be1d99f5c4bb85475caabb78f5e06da6"
"checksum criterion-stats 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a57a868c589ef2208c0f441e816810e16bfd9cf6a6ea6548f53938b8a530d362"
"checksum crossbeam-channel 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9d7b07a3084d8718d95338443d5a46aab38ce16d5f991d4027a0906b369f70a3"
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
"checksum crossbeam-deque 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1bdc73742c36f7f35ebcda81dbb33a7e0d33757d03a06d9ddca762712ec5ea2"
@@ -2183,9 +2129,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3"
"checksum derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c998e6ab02a828dd9735c18f154e14100e674ed08cb4e1938f0e4177543f439"
"checksum derive_builder_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "735e24ee9e5fa8e16b86da5007856e97d592e11867e45d76e0c0d0a164a0b757"
"checksum diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "24815a0c2094f2c8dafe74ab3b9e975892f44acbb94b4d4b4898025a7615efa4"
"checksum diesel_derives 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6471a2b637b414d3ee1504cf230409a550381c79204282f8fe06c527e4ae56be"
"checksum diesel_migrations 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "992026fb46e684eaede3d9d05737de34bdcdcb7f97b389b3c0fa7999389d504e"
"checksum diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925325c57038f2f14c0413bdf6a92ca72acff644959d0a1a9ebf8d19be7e9c01"
"checksum diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28e2b2605ac6a3b9a586383f5f8b2b5f1108f07a421ade965b266289d2805e79"
"checksum diesel_migrations 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0928a7d6f27c849954185416bd59439837de55fbc89e2985b0e46e756ae4e3da"
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
@@ -2217,7 +2163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e579ac8647178ab915d400d7d22938bda5cd351c6c62e1c294d56884ccfc75fe"
"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37"
"checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
"checksum hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)" = "549dbb86397490ce69d908425b9beebc85bbaad25157d67479d4995bb56fdf9a"
"checksum hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)" = "df4dd5dae401458087396b6db7fabc4d6760aa456a5fa8e92bda549f39cae661"
"checksum hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a5aa51f6ae9842239b0fac14af5f22123b8432b4cc774a44ff059fcba0f675ca"
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
@@ -2233,20 +2179,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b"
"checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e"
"checksum libsqlite3-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e9eb7b8e152b6a01be6a4a2917248381875758250dc3df5d46caf9250341dda"
"checksum linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce9439c6f4a1092dc1861272bef01034891da39f13aa1cdcf40ca3e4081de5f"
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
"checksum loggerv 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba6b0664956d197c6e0223870c1cd1ec4117aea282b4c0bd5ab01119d31d708d"
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
"checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43"
"checksum marksman_escape 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e1a9925825c96f25fedf82d5a9290a54d1d95fe6d48d234120b27fd99affceb7"
"checksum markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfedc97d5a503e96816d10fedcd5b42f760b2e525ce2f7ec71f6a41780548475"
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
"checksum memoffset 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e163e5baece1a039e71e75b074de17a9b4114982aa109921fc20253bdf91a53c"
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
"checksum migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "54b8f3128f95b1e5b5f5d5d7ce2156b306b9d11dd50e23b594f25247e30ca83a"
"checksum migrations_macros 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a616c2538e1f09bf402efae6feb73c2711bb137c794b797f3fb8b3df84275d4b"
"checksum migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd916de6df9ac7e811e7e1ac28e0abfebe5205f3b29a7bda9ec8a41ee980a4eb"
"checksum migrations_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5a550cfd76f6cfdf15a7b541893d7c79b68277b0b309f12179211a373a56e617"
"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
"checksum mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e00e17be181010a91dbfefb01660b17311059dc8c7f48b9017677721e732bd"
"checksum mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b7e2b09d08313f84e0fb82d13a4d859109a17543fe9af3b6d941dc1431f7de79"
@@ -2262,7 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
"checksum open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c281318d992e4432cfa799969467003d05921582a7489a8325e37f8a450d5113"
"checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985"
"checksum openssl-sys 0.9.28 (registry+https://github.com/rust-lang/crates.io-index)" = "0bbd90640b148b46305c1691eed6039b5c8509bed16991e3562a01eeb76902a3"
"checksum openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdc5c4a02e69ce65046f1763a0181107038e02176233acb0b3351d7cc588f9"
"checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37"
"checksum pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "45374801e224373c3c0393cd48073c81093494c8735721e81d1dbaa4096b2767"
"checksum pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94039b3921a4af4058a3e4335e5d15099101f298a92f5afc40bab3a3027594a1"
@@ -2278,28 +2222,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
"checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
"checksum pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a029430f0d744bc3d15dd474d591bed2402b645d024583082b9f63bb936dac6"
"checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0"
"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118"
"checksum proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "388d7ea47318c5ccdeb9ba6312cee7d3f65dd2804be8580a170fce410d50b786"
"checksum quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eda5fe9b71976e62bc81b781206aaa076401769b2143379d3eb2118388babac4"
"checksum quick-xml 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b14c27e04216596a49f2b82398a24f67ed9f131a5c0e0235496ea446bdacfb12"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408"
"checksum quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0ff51282f28dc1b53fd154298feaa2e77c5ea0dba68e1fd8b03b72fbe13d2a"
"checksum r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f9078ca6a8a5568ed142083bb2f7dc9295b69d16f867ddcc9849e51b17d8db46"
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
"checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1"
"checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8"
"checksum rayon-futures 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea70dae42033c388536c7e9bdca084a14d75659c12e25fc105e7552464a87c4a"
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb"
"checksum regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b2550876c31dc914696a6c2e01cbce8afba79a93c8ae979d2fe051c0230b3756"
"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
"checksum remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24"
"checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb"
"checksum rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "680e8305c1e0cdf836dc4bec5424e045f278c975a3cac36d1ca01c4695f9d815"
"checksum rss 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d76fce76b29eeec1d003ec42d8819b0d07839c65d00219941ddf815fdb94812"
"checksum rss 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfaaee0d75c76ae197cc2671dc8c531314006e48790c725a9bf0b227d91d19e8"
"checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb"
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
"checksum schannel 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "fbaffce35eb61c5b00846e73128b0cd62717e7c0ec46abbec132370d013975b4"
@@ -2308,11 +2249,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
"checksum send-cell 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fecc1ff30726db72867bc644a57a8914e7cb023b396489873f78e11f9c96b280"
"checksum serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "53e6b6859395f46cf528414659ce43e70902b2277519707c3bd91797b3320330"
"checksum serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "16e97f8dc5b2dabc0183e0cde24b1a53835e5bb3d2c9e0fdb077f895bba7f2a9"
"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794"
"checksum serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7bf1cbb1387028a13739cb018ee0d9b3db534f22ca3c84a5904f7eadfde14e75"
"checksum send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c620dd7e056b468b9d374a9f51cfa6bb4bf17a8ca4ee62e5efa0d99aaff2c41"
"checksum serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "d3bcee660dcde8f52c3765dd9ca5ee36b4bf35470a738eb0bd5a8752b0389645"
"checksum serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "f1711ab8b208541fa8de00425f6a577d90f27bb60724d2bb5fd911314af9668f"
"checksum serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89b340a48245bc03ddba31d0ff1709c118df90edc6adabaca4aac77aea181cce"
"checksum serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5c508584d9913df116b91505eec55610a2f5b16e9ed793c46e4d0152872b3e74"
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
"checksum simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce595117de34b75e057b41e99079e43e9fcc4e5ec9c7ba5f2fea55321f0c624e"
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
@@ -2326,7 +2267,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5"
"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd"
@@ -2341,7 +2281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
"checksum tokio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "be15ef40f675c9fe66e354d74c73f3ed012ca1aa14d65846a33ee48f1ae8d922"
"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71"
"checksum tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "799492ccba3d8ed5e41f2520a7cfd504cb65bbfe5fbbbd0012e335ae5f188051"
"checksum tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cac2a7883ff3567e9d66bb09100d09b33d90311feca0206c7ca034bc0c55113"
"checksum tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6af9eb326f64b2d6b68438e1953341e00ab3cf54de7e35d92bfc73af8555313a"
"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
@@ -2366,7 +2306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1262dfab4c30d5cb7c07026be00ee343a6cf5027fdc0104a9160f354e5db75c"
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
"checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
"checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380"
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+1
View File
@@ -7,3 +7,4 @@ members = [
[profile.release]
debug = false
+21 -20
View File
@@ -1,6 +1,13 @@
# Hammond
### A Podcast Client for GNOME written in Rust.
## A Podcast Client for the GNOME Desktop written in Rust.
[![pipeline status](https://gitlab.gnome.org/alatiera/Hammond/badges/master/pipeline.svg)](https://gitlab.gnome.org/alatiera/Hammond/commits/master)
[![Dependency Status](https://dependencyci.com/github/alatiera/Hammond/badge)](https://dependencyci.com/github/alatiera/Hammond)
### Features
* TBA
![episdes_view](./screenshots/episodes_view.png)
![shows_view](./screenshots/shows_view.png)
@@ -15,13 +22,12 @@ Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
## Broken Feeds
Found a feed that does not work in Hammond?
Please [open an issue](https://gitlab.gnome.org/World/hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
Please [open an issue](https://gitlab.gnome.org/alatiera/Hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
## Getting in Touch
If you have any questions regarding the use or development of Hammond,
want to discuss design or simply hang out, please join us in `#hammond` on
[irc.gnome.org.][irc] or [matrix][matrix].
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
Note:
@@ -47,25 +53,22 @@ flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
# Install the required rust-stable extension from flathub
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
```
To install the resulting flatpak you can do:
```bash
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
flatpak install --user --bundle hammond.flatpak
flatpak build-bundle repo hammond org.gnome.Hammond
```
### Building from soure
```sh
git clone https://gitlab.gnome.org/World/hammond.git
git clone https://gitlab.gnome.org/alatiera/hammond.git
cd hammond/
meson --prefix=/usr build
ninja -C build
sudo ninja -C build install
./configure --prefix=/usr/local
make && sudo make install
```
**Additional:**
You can run `sudo make uninstall` for removal
#### Dependencies
* Rust stable 1.22 or later along with cargo.
@@ -98,11 +101,11 @@ There alot of thins yet to be done.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/World/hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
You can start by taking a look at [Issues](https://gitlab.gnome.org/alatiera/Hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/alatiera/Hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
[contribution-guidelines]: https://gitlab.gnome.org/World/hammond/blob/master/CONTRIBUTING.md
[contribution-guidelines]: https://gitlab.gnome.org/alatiera/Hammond/blob/master/CONTRIBUTING.md
## Overview
@@ -122,7 +125,7 @@ $ tree -d
│   ├── resources # GResources folder
│   │   └── gtk # Contains the glade.ui files.
│   └── src
│   ├── stacks # Contains the gtk Stacks that hold all the different views.
│   ├── views # Contains the Empty, Episodes and Shows view.
│   └── widgets # Contains custom widgets such as Show and Episode.
```
@@ -141,5 +144,3 @@ We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/App
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
[irc]: irc://irc.gnome.org/#hammond
[matrix]: https://matrix.to/#/#hammond:matrix.org
Vendored Executable
+186
View File
@@ -0,0 +1,186 @@
#!/bin/bash
# Adapted from:
# https://gitlab.gnome.org/danigm/libgepub/blob/27f0d374e0c8f6fa972dbd111d4ce0c0f3096914/configure_meson
# configure script adapter for Meson
# Based on build-api: https://github.com/cgwalters/build-api
# Copyright 2010, 2011, 2013 Colin Walters <walters@verbum.org>
# Copyright 2016, 2017 Emmanuele Bassi
# Copyright 2017 Iñigo Martínez <inigomartinez@gmail.com>
# Licensed under the new-BSD license (http://www.opensource.org/licenses/bsd-license.php)
# Build API variables:
# Little helper function for reading args from the commandline.
# it automatically handles -a b and -a=b variants, and returns 1 if
# we need to shift $3.
read_arg() {
# $1 = arg name
# $2 = arg value
# $3 = arg parameter
local rematch='^[^=]*=(.*)$'
if [[ $2 =~ $rematch ]]; then
read "$1" <<< "${BASH_REMATCH[1]}"
else
read "$1" <<< "$3"
# There is no way to shift our callers args, so
# return 1 to indicate they should do it instead.
return 1
fi
}
sanitycheck() {
# $1 = arg name
# $1 = arg command
# $2 = arg alternates
local cmd=$( which $2 2>/dev/null )
if [ -x "$cmd" ]; then
read "$1" <<< "$cmd"
return 0
fi
test -z $3 || {
for alt in $3; do
cmd=$( which $alt 2>/dev/null )
if [ -x "$cmd" ]; then
read "$1" <<< "$cmd"
return 0
fi
done
}
echo -e "\e[1;31mERROR\e[0m: Command '$2' not found"
exit 1
}
checkoption() {
# $1 = arg
option="${1#*--}"
action="${option%%-*}"
name="${option#*-}"
if [ ${default_options[$name]+_} ]; then
case "$action" in
enable) meson_options[$name]=true;;
disable) meson_options[$name]=false;;
*) echo -e "\e[1;33mINFO\e[0m: Ignoring unknown action '$action'";;
esac
else
echo -e "\e[1;33mINFO\e[0m: Ignoring unknown option '$option'"
fi
}
echooption() {
# $1 = option
if [ ${meson_options[$1]+_} ]; then
echo ${meson_options[$1]}
elif [ ${default_options[$1]+_} ]; then
echo ${default_options[$1]}
fi
}
sanitycheck MESON 'meson'
sanitycheck MESONTEST 'mesontest'
sanitycheck NINJA 'ninja' 'ninja-build'
declare -A meson_options
while (($# > 0)); do
case "${1%%=*}" in
--prefix) read_arg prefix "$@" || shift;;
--bindir) read_arg bindir "$@" || shift;;
--sbindir) read_arg sbindir "$@" || shift;;
--libexecdir) read_arg libexecdir "$@" || shift;;
--datarootdir) read_arg datarootdir "$@" || shift;;
--datadir) read_arg datadir "$@" || shift;;
--sysconfdir) read_arg sysconfdir "$@" || shift;;
--libdir) read_arg libdir "$@" || shift;;
--mandir) read_arg mandir "$@" || shift;;
--includedir) read_arg includedir "$@" || shift;;
*) checkoption $1;;
esac
shift
done
# Defaults
test -z ${prefix} && prefix="/usr/local"
test -z ${bindir} && bindir=${prefix}/bin
test -z ${sbindir} && sbindir=${prefix}/sbin
test -z ${libexecdir} && libexecdir=${prefix}/bin
test -z ${datarootdir} && datarootdir=${prefix}/share
test -z ${datadir} && datadir=${datarootdir}
test -z ${sysconfdir} && sysconfdir=${prefix}/etc
test -z ${libdir} && libdir=${prefix}/lib
test -z ${mandir} && mandir=${prefix}/share/man
test -z ${includedir} && includedir=${prefix}/include
# The source directory is the location of this file
srcdir=$(dirname $0)
# The build directory is the current location
builddir=`pwd`
# If we're calling this file from the source directory then
# we automatically create a build directory and ensure that
# both Meson and Ninja invocations are relative to that
# location
if [[ -f "${builddir}/meson.build" ]]; then
mkdir -p _build
builddir="${builddir}/_build"
NINJA_OPT="-C ${builddir}"
fi
# Wrapper Makefile for Ninja
cat > Makefile <<END
# Generated by configure; do not edit
all: rebuild
${NINJA} ${NINJA_OPT}
rebuild:
rm -f ${builddir}/hammond
install:
DESTDIR="\$(DESTDIR)" ${NINJA} ${NINJA_OPT} install
uninstall:
${NINJA} ${NINJA_OPT} uninstall
release:
${NINJA} ${NINJA_OPT} release
check:
${MESONTEST} ${NINJA_OPT}
END
echo "
hammond
=======
meson: ${MESON}
ninja: ${NINJA}
prefix: ${prefix}
Now type 'make' to build
"
cmd_options=""
for key in "${!meson_options[@]}"; do
cmd_options="$cmd_options -Denable-$key=${meson_options[$key]}"
done
exec ${MESON} \
--prefix=${prefix} \
--libdir=${libdir} \
--libexecdir=${libexecdir} \
--datadir=${datadir} \
--sysconfdir=${sysconfdir} \
--bindir=${bindir} \
--includedir=${includedir} \
--mandir=${mandir} \
${cmd_options} \
${builddir} \
${srcdir}
+8 -13
View File
@@ -6,42 +6,37 @@ workspace = "../"
[dependencies]
ammonia = "1.1.0"
chrono = "0.4.2"
chrono = "0.4.1"
derive_builder = "0.5.1"
itertools = "0.7.8"
lazy_static = "1.0.0"
log = "0.4.1"
rayon = "1.0.1"
rayon-futures = "0.1.0"
rfc822_sanitizer = "0.3.3"
rss = "1.5.0"
rss = "1.4.0"
url = "1.7.0"
xdg = "2.1.0"
futures = "0.1.21"
hyper = "0.11.25"
tokio-core = "0.1.17"
hyper = "0.11.24"
tokio-core = "0.1.16"
hyper-tls = "0.1.3"
native-tls = "0.1.5"
num_cpus = "1.8.0"
failure = "0.1.1"
failure_derive = "0.1.1"
# newer stuff do not compile inside flatpak
html5ever = "= 0.22.0"
string_cache = "= 0.7.1"
string_cache_codegen = "= 0.4.0"
[dependencies.diesel]
features = ["sqlite", "r2d2"]
version = "1.2.2"
version = "1.1.1"
[dependencies.diesel_migrations]
features = ["sqlite"]
version = "1.2.0"
version = "1.1.0"
[dev-dependencies]
rand = "0.4.2"
tempdir = "0.3.7"
criterion = "0.2.3"
criterion = "0.2.2"
pretty_assertions = "0.5.1"
[[bench]]
+24 -5
View File
@@ -1,5 +1,3 @@
#![allow(unused)]
#[macro_use]
extern crate criterion;
use criterion::Criterion;
@@ -18,10 +16,10 @@ extern crate rss;
// use futures::future::*;
use tokio_core::reactor::Core;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
use hammond_data::FeedBuilder;
use hammond_data::Source;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
// use hammond_data::errors::*;
use std::io::BufReader;
@@ -58,6 +56,22 @@ static FEEDS: &[(&[u8], &str)] = &[
(STARS, STARS_URL),
];
// This is broken and I don't know why.
fn bench_pipeline(c: &mut Criterion) {
truncate_db().unwrap();
FEEDS.iter().for_each(|&(_, url)| {
Source::from_url(url).unwrap();
});
c.bench_function("pipline", move |b| {
b.iter(|| {
let sources = hammond_data::dbqueries::get_sources().unwrap();
pipeline::run(sources, true).unwrap();
})
});
truncate_db().unwrap();
}
fn bench_index_large_feed(c: &mut Criterion) {
truncate_db().unwrap();
let url = "https://www.greaterthancode.com/feed/podcast";
@@ -100,5 +114,10 @@ fn bench_index_small_feed(c: &mut Criterion) {
truncate_db().unwrap();
}
criterion_group!(benches, bench_index_large_feed, bench_index_small_feed);
criterion_group!(
benches,
bench_pipeline,
bench_index_large_feed,
bench_index_small_feed
);
criterion_main!(benches);
+22 -61
View File
@@ -5,7 +5,6 @@ use diesel::prelude::*;
use diesel;
use diesel::dsl::exists;
use diesel::query_builder::AsQuery;
use diesel::select;
use database::connection;
@@ -41,7 +40,7 @@ pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError
podcast
.order(title.asc())
.filter(id.ne_all(filter_ids))
.filter(id.ne_any(filter_ids))
.load::<Podcast>(&con)
.map_err(From::from)
}
@@ -103,20 +102,6 @@ pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
.map_err(From::from)
}
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<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(rowid.eq(ep_id))
.get_result::<EpisodeWidgetQuery>(&con)
.map_err(From::from)
}
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
use schema::episode::dsl::*;
let db = connection();
@@ -133,17 +118,24 @@ pub fn get_episodes_widgets_filter_limit(
filter_ids: &[i32],
limit: u32,
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
use schema::episode;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
);
episode
.select(columns)
.order(epoch.desc())
.filter(podcast_id.ne_all(filter_ids))
episode::table
.select((
episode::rowid,
episode::title,
episode::uri,
episode::local_uri,
episode::epoch,
episode::length,
episode::duration,
episode::played,
episode::podcast_id,
))
.order(episode::epoch.desc())
.filter(episode::podcast_id.ne_any(filter_ids))
.limit(i64::from(limit))
.load::<EpisodeWidgetQuery>(&con)
.map_err(From::from)
@@ -193,17 +185,14 @@ pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
pub fn get_pd_episodeswidgets(_parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
);
episode
.select(columns)
.filter(podcast_id.eq(parent.id()))
episode.select((rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id))
// .filter(podcast_id.eq(parent.id()))
// .group_by(epoch)
.order(epoch.desc())
.load::<EpisodeWidgetQuery>(&con)
.map_err(From::from)
@@ -360,34 +349,6 @@ pub(crate) fn episode_exists(title_: &str, podcast_id_: i32) -> Result<bool, Dat
.map_err(From::from)
}
/// Check if the `episode table contains any rows
///
/// Return true if `episode` table is populated.
pub fn is_episodes_populated() -> Result<bool, DataError> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.as_query()))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `podcast` table contains any rows
///
/// Return true if `podcast table is populated.
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(podcast.filter(id.ne_all(filter_ids))))
.get_result(&con)
.map_err(From::from)
}
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
use schema::episode::dsl::*;
let db = connection();
@@ -418,7 +379,7 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
mod tests {
use super::*;
use database::*;
use pipeline;
use pipeline::*;
#[test]
fn test_update_none_to_played_now() {
@@ -428,7 +389,7 @@ mod tests {
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let id = source.id();
pipeline::run(vec![source], true).unwrap();
index_single_source(source, true).unwrap();
let pd = get_podcast_from_source_id(id).unwrap();
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
+8 -24
View File
@@ -8,26 +8,6 @@ use url;
use std::io;
use models::Source;
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
#[derive(Fail, Debug)]
pub struct HttpStatusError {
url: String,
status_code: hyper::StatusCode,
context: String,
}
impl HttpStatusError {
pub fn new(url: String, code: hyper::StatusCode, context: String) -> Self {
HttpStatusError {
url,
status_code: code,
context,
}
}
}
#[derive(Fail, Debug)]
pub enum DataError {
#[fail(display = "SQL Query failed: {}", _0)]
@@ -51,12 +31,16 @@ pub enum DataError {
RssError(#[cause] rss::Error),
#[fail(display = "Error: {}", _0)]
Bail(String),
#[fail(display = "{}", _0)]
HttpStatusGeneral(HttpStatusError),
#[fail(display = "FIXME: This should be better")]
F301(Source),
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
HttpStatusError {
url: String,
status_code: hyper::StatusCode,
context: String,
},
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
ParseEpisodeError { reason: String, parent_id: i32 },
#[fail(display = "No Futures where produced to be run.")]
EmptyFuturesList,
#[fail(display = "Episode was not changed and thus skipped.")]
EpisodeNotChanged,
}
+95 -91
View File
@@ -1,15 +1,16 @@
#![cfg_attr(feature = "cargo-clippy", allow(unit_arg))]
//! Index Feeds.
use futures::future::*;
use futures::prelude::*;
use futures::stream;
use itertools::{Either, Itertools};
use rss;
use dbqueries;
use errors::DataError;
use models::{Index, IndexState, Update};
use models::{NewEpisode, NewEpisodeMinimal, NewPodcast, Podcast};
use models::{NewEpisode, NewPodcast, Podcast};
use pipeline::*;
type InsertUpdate = (Vec<NewEpisode>, Vec<Option<(NewEpisode, i32)>>);
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
@@ -28,7 +29,7 @@ impl Feed {
pub fn index(self) -> Box<Future<Item = (), Error = DataError> + Send> {
let fut = self.parse_podcast_async()
.and_then(|pd| pd.to_podcast())
.and_then(move |pd| self.index_channel_items(pd));
.and_then(move |pd| self.index_channel_items(&pd));
Box::new(fut)
}
@@ -41,94 +42,74 @@ impl Feed {
Box::new(ok(self.parse_podcast()))
}
fn index_channel_items(self, pd: Podcast) -> Box<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| {
glue(&item, pd.id())
.map_err(|err| error!("Failed to parse an episode: {}", err))
.ok()
});
// Filter errors, Index updatable episodes, return insertables.
let insertables = filter_episodes(episodes);
// Batch index insertable episodes.
let idx = insertables.and_then(|eps| ok(batch_insert_episodes(&eps)));
Box::new(idx)
}
}
fn glue(item: &rss::Item, id: i32) -> Result<IndexState<NewEpisode>, DataError> {
NewEpisodeMinimal::new(item, id).and_then(move |ep| determine_ep_state(ep, item))
}
fn determine_ep_state(
ep: NewEpisodeMinimal,
item: &rss::Item,
) -> Result<IndexState<NewEpisode>, DataError> {
// Check if feed exists
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
if !exists {
Ok(IndexState::Index(ep.into_new_episode(item)))
} else {
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
let rowid = old.rowid();
if ep != old {
Ok(IndexState::Update((ep.into_new_episode(item), rowid)))
} else {
Ok(IndexState::NotChanged)
}
}
}
fn filter_episodes<'a, S>(
stream: S,
) -> Box<Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a>
where
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
{
let list = stream.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
.collect();
Box::new(list)
}
fn batch_insert_episodes(episodes: &[NewEpisode]) {
if episodes.is_empty() {
return;
fn index_channel_items(
&self,
pd: &Podcast,
) -> Box<Future<Item = (), Error = DataError> + Send> {
let fut = self.get_stuff(pd)
.and_then(|(insert, update)| {
if !insert.is_empty() {
info!("Indexing {} episodes.", insert.len());
if let Err(err) = dbqueries::index_new_episodes(insert.as_slice()) {
error!("Failed batch indexng, Fallign back to individual indexing.");
error!("{}", err);
insert.iter().for_each(|ep| {
if let Err(err) = ep.index() {
error!("Failed to index episode: {:?}.", ep.title());
error!("{}", err);
};
info!("Indexing {} episodes.", episodes.len());
dbqueries::index_new_episodes(episodes)
.map_err(|err| {
error!("Failed batch indexng: {}", err);
info!("Fallign back to individual indexing.");
})
.unwrap_or_else(|_| {
episodes.iter().for_each(|ep| {
ep.index()
.map_err(|err| error!("Error: {}.", err))
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
.ok();
}
}
Ok((insert, update))
})
.map(|(_, update)| {
if !update.is_empty() {
info!("Updating {} episodes.", update.len());
// see get_stuff for more
update
.into_iter()
.filter_map(|x| x)
.for_each(|(ref ep, rowid)| {
if let Err(err) = ep.update(rowid) {
error!("Failed to index episode: {:?}.", ep.title());
error!("{}", err);
};
})
}
});
Box::new(fut)
}
fn get_stuff(
&self,
pd: &Podcast,
) -> Box<Future<Item = InsertUpdate, Error = DataError> + Send> {
let (insert, update): (Vec<_>, Vec<_>) = self.channel
.items()
.into_iter()
.map(|item| glue_async(item, pd.id()))
// This is sort of ugly but I think it's cheaper than pushing None
// to updated and filtering it out later.
// Even though we already map_filter in index_channel_items.
// I am not sure what the optimizations are on match vs allocating None.
.map(|fut| {
fut.and_then(|x| match x {
IndexState::NotChanged => Err(DataError::EpisodeNotChanged),
_ => Ok(x),
})
})
.flat_map(|fut| fut.wait())
.partition_map(|state| match state {
IndexState::Index(e) => Either::Left(e),
IndexState::Update(e) => Either::Right(Some(e)),
// This should never occur
IndexState::NotChanged => Either::Right(None),
});
Box::new(ok((insert, update)))
}
}
#[cfg(test)]
@@ -136,10 +117,10 @@ mod tests {
use rss::Channel;
use tokio_core::reactor::Core;
use Source;
use database::truncate_db;
use dbqueries;
use utils::get_feed;
use Source;
use std::fs;
use std::io::BufReader;
@@ -221,8 +202,31 @@ mod tests {
let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap();
feed.index_channel_items(pd).wait().unwrap();
feed.index_channel_items(&pd).wait().unwrap();
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 1);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43);
}
#[test]
fn test_feed_get_stuff() {
truncate_db().unwrap();
let path = "tests/feeds/2018-01-20-Intercepted.xml";
let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap();
let (insert, update) = feed.get_stuff(&pd).wait().unwrap();
assert_eq!(43, insert.len());
assert_eq!(0, update.len());
feed.index().wait().unwrap();
let path = "tests/feeds/2018-02-03-Intercepted.xml";
let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap();
let (insert, update) = feed.get_stuff(&pd).wait().unwrap();
assert_eq!(4, insert.len());
assert_eq!(43, update.len());
}
}
+24 -33
View File
@@ -1,27 +1,22 @@
#![recursion_limit = "1024"]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
#![cfg_attr(
feature = "clippy",
warn(
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut,
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else,
items_after_statements, used_underscore_binding
)
)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
#![cfg_attr(feature = "clippy",
warn(option_unwrap_used, result_unwrap_used, print_stdout,
wrong_pub_self_convention, mut_mut, non_ascii_literal, similar_names,
unicode_not_nfc, enum_glob_use, if_not_else, items_after_statements,
used_underscore_binding))]
#![allow(unknown_lints)]
#![warn(
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements,
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns,
private_no_mangle_statics, safe_extern_statics, unconditional_recursion,
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true
)]
#![warn(
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
unused_extern_crates, unused
)]
#![deny(warnings)]
#![deny(bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals,
path_statements, patterns_in_fns_without_body, plugin_as_library, private_in_public,
private_no_mangle_fns, private_no_mangle_statics, safe_extern_statics,
unconditional_recursion, unions_with_drop_fields, unused_allocation, unused_comparisons,
unused_parens, while_true)]
#![deny(missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts)]
#![deny(unused_extern_crates, unused)]
// #![feature(conservative_impl_trait)]
//! FIXME: Docs
@@ -49,35 +44,31 @@ extern crate chrono;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate itertools;
extern crate native_tls;
extern crate num_cpus;
extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate tokio_core;
extern crate url;
extern crate xdg;
pub mod database;
#[allow(missing_docs)]
pub mod dbqueries;
#[allow(missing_docs)]
pub mod errors;
mod feed;
pub(crate) mod models;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils;
pub mod database;
pub mod pipeline;
pub(crate) mod models;
mod feed;
mod parser;
mod schema;
pub use feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
// Set the user agent, See #53 for more
// Keep this in sync with Tor-browser releases
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
pub use models::Save;
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)]
+7 -13
View File
@@ -1,7 +1,7 @@
use chrono::prelude::*;
use diesel;
use diesel::prelude::*;
use diesel::SaveChangesDsl;
use diesel::prelude::*;
use database::connection;
use errors::DataError;
@@ -31,12 +31,10 @@ pub struct Episode {
podcast_id: i32,
}
impl Save<Episode> for Episode {
type Error = DataError;
impl Save<Episode, DataError> for Episode {
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Episode, Self::Error> {
fn save(&self) -> Result<Episode, DataError> {
let db = connection();
let tempdb = db.get()?;
@@ -226,12 +224,10 @@ impl From<Episode> for EpisodeWidgetQuery {
}
}
impl Save<usize> for EpisodeWidgetQuery {
type Error = DataError;
impl Save<usize, DataError> for EpisodeWidgetQuery {
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
fn save(&self) -> Result<usize, DataError> {
use schema::episode::dsl::*;
let db = connection();
@@ -367,12 +363,10 @@ pub struct EpisodeCleanerQuery {
played: Option<i32>,
}
impl Save<usize> for EpisodeCleanerQuery {
type Error = DataError;
impl Save<usize, DataError> for EpisodeCleanerQuery {
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
fn save(&self) -> Result<usize, DataError> {
use schema::episode::dsl::*;
let db = connection();
+8 -16
View File
@@ -30,30 +30,22 @@ pub enum IndexState<T> {
NotChanged,
}
pub trait Insert<T> {
type Error;
fn insert(&self) -> Result<T, Self::Error>;
pub trait Insert<T, E> {
fn insert(&self) -> Result<T, E>;
}
pub trait Update<T> {
type Error;
fn update(&self, i32) -> Result<T, Self::Error>;
pub trait Update<T, E> {
fn update(&self, i32) -> Result<T, E>;
}
// This might need to change in the future
pub trait Index<T>: Insert<T> + Update<T> {
type Error;
fn index(&self) -> Result<T, <Self as Index<T>>::Error>;
pub trait Index<T, E>: Insert<T, E> + Update<T, E> {
fn index(&self) -> Result<T, E>;
}
/// FIXME: DOCS
pub trait Save<T> {
/// The Error type to be returned.
type Error;
pub trait Save<T, E> {
/// Helper method to easily save/"sync" current state of a diesel model to
/// the Database.
fn save(&self) -> Result<T, Self::Error>;
fn save(&self) -> Result<T, E>;
}
+7 -20
View File
@@ -43,9 +43,7 @@ impl From<NewEpisodeMinimal> for NewEpisode {
}
}
impl Insert<()> for NewEpisode {
type Error = DataError;
impl Insert<(), DataError> for NewEpisode {
fn insert(&self) -> Result<(), DataError> {
use schema::episode::dsl::*;
let db = connection();
@@ -60,9 +58,7 @@ impl Insert<()> for NewEpisode {
}
}
impl Update<()> for NewEpisode {
type Error = DataError;
impl Update<(), DataError> for NewEpisode {
fn update(&self, episode_id: i32) -> Result<(), DataError> {
use schema::episode::dsl::*;
let db = connection();
@@ -77,9 +73,7 @@ impl Update<()> for NewEpisode {
}
}
impl Index<()> for NewEpisode {
type Error = DataError;
impl Index<(), DataError> for NewEpisode {
// Does not update the episode description if it's the only thing that has
// changed.
fn index(&self) -> Result<(), DataError> {
@@ -203,9 +197,9 @@ impl NewEpisodeMinimal {
let guid = item.guid().map(|s| s.value().trim().to_owned());
let uri = item.enclosure()
.map(|s| url_cleaner(s.url().trim()))
.map(|s| url_cleaner(s.url()))
// Fallback to Rss.Item.link if enclosure is None.
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
.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.
@@ -240,14 +234,7 @@ impl NewEpisodeMinimal {
// TODO: TryInto is stabilizing in rustc v1.26!
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
let length = item.enclosure().and_then(|x| x.length().parse().ok());
let description = item.description().and_then(|s| {
let sanitized_html = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
.link_rel(None)
.clean(s.trim())
.to_string();
Some(sanitized_html)
});
let description = item.description().map(|s| ammonia::clean(s));
NewEpisodeBuilder::default()
.title(self.title)
@@ -293,8 +280,8 @@ impl NewEpisodeMinimal {
mod tests {
use database::truncate_db;
use dbqueries;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use models::*;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use rss::Channel;
+10 -20
View File
@@ -4,8 +4,8 @@ use diesel::prelude::*;
use rss;
use errors::DataError;
use models::Podcast;
use models::{Index, Insert, Update};
use models::Podcast;
use schema::podcast;
use database::connection;
@@ -26,10 +26,8 @@ pub(crate) struct NewPodcast {
source_id: i32,
}
impl Insert<()> for NewPodcast {
type Error = DataError;
fn insert(&self) -> Result<(), Self::Error> {
impl Insert<(), DataError> for NewPodcast {
fn insert(&self) -> Result<(), DataError> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
@@ -42,10 +40,8 @@ impl Insert<()> for NewPodcast {
}
}
impl Update<()> for NewPodcast {
type Error = DataError;
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
impl Update<(), DataError> for NewPodcast {
fn update(&self, podcast_id: i32) -> Result<(), DataError> {
use schema::podcast::dsl::*;
let db = connection();
let con = db.get()?;
@@ -61,9 +57,7 @@ impl Update<()> for NewPodcast {
// TODO: Maybe return an Enum<Action(Resut)> Instead.
// It would make unti testing better too.
impl Index<()> for NewPodcast {
type Error = DataError;
impl Index<(), DataError> for NewPodcast {
fn index(&self) -> Result<(), DataError> {
let exists = dbqueries::podcast_exists(self.source_id)?;
@@ -94,20 +88,16 @@ impl NewPodcast {
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
let title = chan.title().trim();
let link = url_cleaner(chan.link().trim());
let description = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
.link_rel(None)
.clean(chan.description().trim())
.to_string();
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(|url| url.trim()))
.and_then(|s| s.image())
.map(|s| s.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().trim().to_owned()));
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().to_owned()));
NewPodcastBuilder::default()
.title(title)
+2
View File
@@ -1,3 +1,5 @@
#![allow(unused_mut)]
use diesel;
use diesel::prelude::*;
use url::Url;
+2 -4
View File
@@ -25,12 +25,10 @@ pub struct Podcast {
source_id: i32,
}
impl Save<Podcast> for Podcast {
type Error = DataError;
impl Save<Podcast, DataError> for Podcast {
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Podcast, Self::Error> {
fn save(&self) -> Result<Podcast, DataError> {
let db = connection();
let tempdb = db.get()?;
+32 -40
View File
@@ -3,22 +3,20 @@ use diesel::SaveChangesDsl;
use rss::Channel;
use url::Url;
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper::client::HttpConnector;
use hyper::header::{ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified,
Location, UserAgent};
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper_tls::HttpsConnector;
// use futures::future::ok;
use futures::future::{loop_fn, Future, Loop};
use futures::prelude::*;
use database::connection;
use errors::*;
use errors::DataError;
use feed::{Feed, FeedBuilder};
use models::{NewSource, Save};
use schema::source;
use USER_AGENT;
use std::str::FromStr;
@@ -34,12 +32,10 @@ pub struct Source {
http_etag: Option<String>,
}
impl Save<Source> for Source {
type Error = DataError;
impl Save<Source, DataError> for Source {
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Source, Self::Error> {
fn save(&self) -> Result<Source, DataError> {
let db = connection();
let con = db.get()?;
@@ -106,7 +102,11 @@ impl Source {
}
fn make_err(self, context: &str, code: StatusCode) -> DataError {
DataError::HttpStatusGeneral(HttpStatusError::new(self.uri, code, context.into()))
DataError::HttpStatusError {
url: self.uri,
status_code: code,
context: context.into(),
}
}
// TODO match on more stuff
@@ -119,7 +119,7 @@ impl Source {
// 408: Timeout
// 410: Feed deleted
// TODO: Rething this api,
fn match_status(mut self, res: Response) -> Result<Response, DataError> {
fn match_status(mut self, res: Response) -> Result<(Self, Response), DataError> {
self.update_etag(&res)?;
let code = res.status();
@@ -128,7 +128,7 @@ impl Source {
StatusCode::MovedPermanently => {
error!("Feed was moved permanently.");
self.handle_301(&res)?;
return Err(DataError::F301(self));
return Err(self.make_err("301: Feed was moved permanently.", code));
}
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
@@ -139,7 +139,7 @@ impl Source {
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
_ => info!("HTTP StatusCode: {}", code),
};
Ok(res)
Ok((self, res))
}
fn handle_301(&mut self, res: &Response) -> Result<(), DataError> {
@@ -151,6 +151,8 @@ impl Source {
self.last_modified = None;
self.save()?;
info!("Feed url was updated succesfully.");
// TODO: Refresh in place instead of next time, Not a priority.
info!("New content will be fetched with the next refesh.");
}
Ok(())
@@ -175,27 +177,12 @@ impl Source {
// Refactor into TryInto once it lands on stable.
pub fn into_feed(
self,
client: Client<HttpsConnector<HttpConnector>>,
client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> Box<Future<Item = Feed, Error = DataError>> {
let id = self.id();
let response = loop_fn(self, move |source| {
source
.request_constructor(&client.clone(), ignore_etags)
.then(|res| match res {
Ok(response) => Ok(Loop::Break(response)),
Err(err) => match err {
DataError::F301(s) => {
info!("Following redirect...");
Ok(Loop::Continue(s))
}
e => Err(e),
},
})
});
let feed = response
.and_then(response_to_channel)
let feed = self.request_constructor(client, ignore_etags)
.and_then(move |(_, res)| response_to_channel(res))
.and_then(move |chan| {
FeedBuilder::default()
.channel(chan)
@@ -213,23 +200,27 @@ impl Source {
self,
client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> Box<Future<Item = Response, Error = DataError>> {
) -> Box<Future<Item = (Self, Response), Error = DataError>> {
// FIXME: remove unwrap somehow
let uri = Uri::from_str(self.uri()).unwrap();
let mut req = Request::new(Method::Get, uri);
// Set the UserAgent cause ppl still seem to check it for some reason...
req.headers_mut().set(UserAgent::new(USER_AGENT));
// Set the user agent as a fix for issue #53
// TODO: keep this in sync with tor-browser releases
req.headers_mut().set(UserAgent::new(
"Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
));
if !ignore_etags {
if let Some(etag) = self.http_etag() {
let tag = vec![EntityTag::new(true, etag.to_owned())];
req.headers_mut().set(IfNoneMatch::Items(tag));
if let Some(foo) = self.http_etag() {
req.headers_mut().set(IfNoneMatch::Items(vec![
EntityTag::new(true, foo.to_owned()),
]));
}
if let Some(lmod) = self.last_modified() {
if let Ok(date) = lmod.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(date));
if let Some(foo) = self.last_modified() {
if let Ok(x) = foo.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(x));
}
}
}
@@ -237,6 +228,7 @@ impl Source {
let work = client
.request(req)
.map_err(From::from)
// TODO: tail recursion loop that would follow redirects directly
.and_then(move |res| self.match_status(res));
Box::new(work)
}
@@ -277,7 +269,7 @@ mod tests {
let source = Source::from_url(url).unwrap();
let id = source.id();
let feed = source.into_feed(client, 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);
+131 -64
View File
@@ -2,119 +2,186 @@
//! Docs.
use futures::future::*;
use futures::prelude::*;
use futures::stream::*;
// use futures::prelude::*;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
use tokio_core::reactor::Core;
use num_cpus;
use rayon;
use rayon_futures::ScopeFutureExt;
use rss;
use errors::DataError;
use Source;
use dbqueries;
use errors::DataError;
use models::{IndexState, NewEpisode, NewEpisodeMinimal};
// use std::sync::{Arc, Mutex};
// http://gtk-rs.org/tuto/closures
#[macro_export]
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
}
);
}
type HttpsClient = Client<HttpsConnector<HttpConnector>>;
/// The pipline to be run for indexing and updating a Podcast feed that originates from
/// `Source.uri`.
///
/// Messy temp diagram:
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` ->
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
pub fn pipeline<'a, S>(
/// Source -> GET Request -> Update Etags -> Check Status -> Parse xml/Rss ->
/// Convert `rss::Channel` into Feed -> Index Podcast -> Index Episodes.
pub fn pipeline<S: IntoIterator<Item = Source>>(
sources: S,
ignore_etags: bool,
client: &HttpsClient,
) -> Box<Future<Item = Vec<()>, Error = DataError> + 'a>
where
S: Stream<Item = Source, Error = DataError> + 'a,
{
let pipeline = sources
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags)))
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
// the stream will stop at the first error so
// we ensure that everything will succeded regardless.
.map_err(|err| error!("Error: {}", err))
.then(|_| ok::<(), DataError>(()))
tokio_core: &mut Core,
client: Client<HttpsConnector<HttpConnector>>,
) -> Result<(), DataError> {
let list: Vec<_> = sources
.into_iter()
.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();
Box::new(pipeline)
if list.is_empty() {
return Err(DataError::EmptyFuturesList);
}
// Thats not really concurrent yet I think.
tokio_core.run(collect_futures(list))?;
Ok(())
}
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
/// runs the pipeline.
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError>
where
S: IntoIterator<Item = Source>,
{
pub fn run(sources: Vec<Source>, ignore_etags: bool) -> Result<(), DataError> {
if sources.is_empty() {
return Ok(());
}
let mut core = Core::new()?;
let handle = core.handle();
let client = Client::configure()
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle);
let stream = iter_ok::<_, DataError>(sources);
let p = pipeline(stream, ignore_etags, &client);
core.run(p).map(|_| ())
pipeline(sources, ignore_etags, &mut core, client)
}
/// Docs
pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataError> {
let mut core = Core::new()?;
let handle = core.handle();
let client = Client::configure()
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle);
let work = s.into_feed(&client, ignore_etags)
.and_then(move |feed| feed.index())
.map(|_| ());
core.run(work)
}
fn determine_ep_state(
ep: NewEpisodeMinimal,
item: &rss::Item,
) -> Result<IndexState<NewEpisode>, DataError> {
// Check if feed exists
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
if !exists {
Ok(IndexState::Index(ep.into_new_episode(item)))
} else {
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
let rowid = old.rowid();
if ep != old {
Ok(IndexState::Update((ep.into_new_episode(item), rowid)))
} else {
Ok(IndexState::NotChanged)
}
}
}
pub(crate) fn glue_async<'a>(
item: &'a rss::Item,
id: i32,
) -> Box<Future<Item = IndexState<NewEpisode>, Error = DataError> + 'a> {
Box::new(
result(NewEpisodeMinimal::new(item, id)).and_then(move |ep| determine_ep_state(ep, item)),
)
}
// Weird magic from #rust irc channel
// kudos to remexre
/// FIXME: Docs
#[cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
pub fn collect_futures<F>(
futures: Vec<F>,
) -> Box<Future<Item = Vec<Result<F::Item, F::Error>>, Error = DataError>>
where
F: 'static + Future,
<F as Future>::Item: 'static,
<F as Future>::Error: 'static,
{
Box::new(loop_fn((futures, vec![]), |(futures, mut done)| {
select_all(futures).then(|r| {
let (r, rest) = match r {
Ok((r, _, rest)) => (Ok(r), rest),
Err((r, _, rest)) => (Err(r), rest),
};
done.push(r);
if rest.is_empty() {
Ok(Loop::Break(done))
} else {
Ok(Loop::Continue((rest, done)))
}
})
}))
}
#[cfg(test)]
mod tests {
use super::*;
use database::truncate_db;
use dbqueries;
use Source;
use database::truncate_db;
// (path, url) tuples.
const URLS: &[&str] = &[
const URLS: &[(&str, &str)] = {
&[
(
"tests/feeds/2018-01-20-Intercepted.xml",
"https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill",
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.com/linuxunplugged",
),
(
"tests/feeds/2018-01-20-LinuxUnplugged.xml",
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.\
com/linuxunplugged",
),
(
"tests/feeds/2018-01-20-TheTipOff.xml",
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff",
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars",
),
(
"tests/feeds/2018-01-20-StealTheStars.xml",
"https://web.archive.org/web/20180120104957if_/https://rss.art19.\
com/steal-the-stars",
),
(
"tests/feeds/2018-01-20-GreaterThanCode.xml",
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
com/feed/podcast",
];
),
]
};
#[test]
/// Insert feeds and update/index them.
fn test_pipeline() {
truncate_db().unwrap();
let bad_url = "https://gitlab.gnome.org/World/hammond.atom";
// if a stream returns error/None it stops
// bad we want to parse all feeds regardless if one fails
Source::from_url(bad_url).unwrap();
URLS.iter().for_each(|url| {
URLS.iter().for_each(|&(_, url)| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
let sources = dbqueries::get_sources().unwrap();
run(sources, true).unwrap();
@@ -123,7 +190,7 @@ mod tests {
run(sources, true).unwrap();
// Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources().unwrap().len(), 6);
assert_eq!(dbqueries::get_sources().unwrap().len(), 5);
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
}
+10 -9
View File
@@ -28,10 +28,10 @@ fn download_checker() -> Result<(), DataError> {
})
.for_each(|ep| {
ep.set_local_uri(None);
ep.save()
.map_err(|err| error!("{}", err))
.map_err(|_| error!("Error while trying to update episode: {:#?}", ep))
.ok();
if let Err(err) = ep.save() {
error!("Error while trying to update episode: {:#?}", ep);
error!("{}", err);
};
});
Ok(())
@@ -48,11 +48,12 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
.for_each(|ep| {
let limit = ep.played().unwrap();
if now_utc > limit {
delete_local_content(ep)
.map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri()))
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri()))
.ok();
if let Err(err) = delete_local_content(ep) {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("{}", err);
} else {
info!("Episode {:?} was deleted succesfully.", ep.local_uri());
};
}
});
Ok(())
+1 -1
View File
@@ -6,7 +6,7 @@ workspace = "../"
[dependencies]
error-chain = "0.11.0"
hyper = "0.11.25"
hyper = "0.11.24"
log = "0.4.1"
mime_guess = "1.8.4"
reqwest = "0.8.5"
+2 -2
View File
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
// use failure::Error;
use errors::DownloadError;
@@ -234,9 +234,9 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::Source;
use hammond_data::dbqueries;
use hammond_data::pipeline;
use hammond_data::Source;
use std::fs;
+2 -3
View File
@@ -1,8 +1,7 @@
#![recursion_limit = "1024"]
#![warn(unused_extern_crates, unused)]
#![deny(unused_extern_crates, unused)]
#![allow(unknown_lints)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))]
#![deny(warnings)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
extern crate failure;
#[macro_use]
+3 -3
View File
@@ -6,7 +6,7 @@ version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.2"
chrono = "0.4.1"
crossbeam-channel = "0.1.2"
gdk = "0.8.0"
gdk-pixbuf = "0.4.0"
@@ -17,14 +17,14 @@ log = "0.4.1"
loggerv = "0.7.1"
open = "1.2.1"
rayon = "1.0.1"
send-cell = "0.1.3"
send-cell = "0.1.2"
url = "1.7.0"
failure = "0.1.1"
failure_derive = "0.1.1"
take_mut = "0.2.2"
regex = "0.2.10"
reqwest = "0.8.5"
serde_json = "1.0.15"
serde_json = "1.0.13"
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
[dependencies.gtk]
+1 -1
View File
@@ -56,11 +56,11 @@ Tobias Bernard
<property name="can_focus">False</property>
<property name="label" translatable="yes">Episode Title</property>
<property name="ellipsize">end</property>
<property name="width_chars">55</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<property name="lines">1</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="expand">False</property>
+9 -3
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
<!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018
@@ -32,14 +32,13 @@ Tobias Bernard
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="name">container</property>
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="height_request">400</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
@@ -69,6 +68,8 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox" id="frame_parent">
<property name="width_request">600</property>
<property name="height_request">-1</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">32</property>
@@ -80,6 +81,7 @@ Tobias Bernard
<property name="spacing">24</property>
<child>
<object class="GtkBox" id="today_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
@@ -137,6 +139,7 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox" id="yday_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
@@ -193,6 +196,7 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox" id="week_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
@@ -249,6 +253,7 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox" id="month_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
@@ -305,6 +310,7 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox" id="rest_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
+31 -36
View File
@@ -31,17 +31,16 @@ Tobias Bernard
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="width_request">700</property>
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<child>
<object class="GtkViewport">
@@ -71,26 +70,26 @@ Tobias Bernard
</child>
<child>
<object class="GtkBox">
<property name="width_request">700</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">32</property>
<property name="margin_right">32</property>
<property name="margin_top">32</property>
<property name="margin_bottom">32</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="valign">center</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage" id="cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">256</property>
<property name="pixel_size">128</property>
<property name="icon_name">image-x-generic-symbolic</property>
</object>
<packing>
@@ -100,32 +99,23 @@ Tobias Bernard
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="min_content_height">80</property>
<child>
<object class="GtkViewport">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<property name="valign">end</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child type="center">
<object class="GtkLabel" id="description">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label">This is embarrasing!
Sorry, we could not find a description for this Show.</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="label">foo</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max_width_chars">70</property>
</object>
</child>
</object>
</child>
<property name="wrap_mode">word-char</property>
<property name="max_width_chars">90</property>
</object>
<packing>
<property name="expand">False</property>
@@ -137,6 +127,7 @@ Sorry, we could not find a description for this Show.</property>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkMenuButton" id="settings_button">
@@ -198,8 +189,16 @@ Sorry, we could not find a description for this Show.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
@@ -210,17 +209,13 @@ Sorry, we could not find a description for this Show.</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<object class="GtkFrame" id="episodes">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox" id="episodes">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
</object>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
@@ -235,7 +230,7 @@ Sorry, we could not find a description for this Show.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
@@ -262,7 +257,7 @@ Sorry, we could not find a description for this Show.</property>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
+11 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
<!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018
@@ -39,6 +39,13 @@ Tobias Bernard
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child type="overlay">
<object class="GtkImage" id="pd_cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -48,8 +55,10 @@ Tobias Bernard
<property name="icon_name">image-x-generic-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
@@ -12,25 +12,25 @@
desktop written in Rust.
</p>
</description>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<screenshots>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/episodes_view.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/World/hammond/raw/master/screenshots/shows_view.png</image>
<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/World/hammond/raw/master/screenshots/show_widget.png</image>
<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.2" date="2018-05-07"/>
<release version="0.3.1" date="2018-03-28"/>
</releases>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<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>
+94 -50
View File
@@ -3,27 +3,35 @@
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
use glib;
use gtk;
use gtk::prelude::*;
use gtk::SettingsExt as GtkSettingsExt;
use gtk::prelude::*;
use hammond_data::Podcast;
use failure::Error;
use rayon;
use hammond_data::{Podcast, Source};
use hammond_data::utils::delete_show;
use appnotif::*;
use headerbar::Header;
use settings::{self, WindowGeometry};
use stacks::{Content, PopulatedState};
use settings::WindowGeometry;
use stacks::Content;
use utils;
use widgets::{mark_all_notif, remove_show_notif};
use widgets::mark_all_watched;
use std::rc::Rc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Duration;
#[derive(Clone, Debug)]
pub enum Action {
UpdateSources(Option<Source>),
RefreshAllViews,
RefreshEpisodesView,
RefreshEpisodesViewBGR,
RefreshShowsView,
RefreshWidget,
RefreshWidgetIfVis,
ReplaceWidget(Arc<Podcast>),
RefreshWidgetIfSame(i32),
ShowWidgetAnimated,
@@ -41,8 +49,8 @@ pub struct App {
app_instance: gtk::Application,
window: gtk::Window,
overlay: gtk::Overlay,
header: Rc<Header>,
content: Rc<Content>,
header: Arc<Header>,
content: Arc<Content>,
receiver: Receiver<Action>,
sender: Sender<Action>,
settings: Settings,
@@ -58,9 +66,6 @@ impl App {
glib::set_application_name("Hammond");
glib::set_prgname(Some("Hammond"));
let cleanup_date = settings::get_cleanup_date(&settings);
utils::cleanup(cleanup_date);
// Create the main window
let window = gtk::Window::new(gtk::WindowType::Toplevel);
@@ -79,10 +84,10 @@ impl App {
// Create a content instance
let content =
Rc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
Arc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
// Create the headerbar
let header = Rc::new(Header::new(&content, &window, &sender));
let header = Arc::new(Header::new(&content, &window, sender.clone()));
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
@@ -119,35 +124,34 @@ impl App {
fn setup_refresh_on_startup(&self) {
// Update the feeds right after the Application is initialized.
if self.settings.get_boolean("refresh-on-startup") {
let cleanup_date = utils::get_cleanup_date(&self.settings);
let sender = self.sender.clone();
info!("Refresh on startup.");
// The ui loads async, after initialization
// so we need to delay this a bit so it won't block
// requests that will come from loading the gui on startup.
gtk::timeout_add(1500, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
utils::cleanup(cleanup_date);
gtk::timeout_add_seconds(2, move || {
utils::refresh(None, sender.clone());
glib::Continue(false)
});
}
}
fn setup_auto_refresh(&self) {
let refresh_interval = settings::get_refresh_interval(&self.settings).num_seconds() as u32;
let refresh_interval = utils::get_refresh_interval(&self.settings).num_seconds() as u32;
let sender = self.sender.clone();
info!("Auto-refresh every {:?} seconds.", refresh_interval);
gtk::timeout_add_seconds(refresh_interval, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
utils::refresh(None, sender.clone());
glib::Continue(true)
});
}
#[inline]
pub fn run(self) {
WindowGeometry::from_settings(&self.settings).apply(&self.window);
@@ -163,46 +167,86 @@ impl App {
let sender = self.sender.clone();
let overlay = self.overlay.clone();
let receiver = self.receiver;
gtk::timeout_add(50, move || {
match receiver.try_recv() {
gtk::idle_add(move || {
match receiver.recv_timeout(Duration::from_millis(10)) {
Ok(Action::UpdateSources(source)) => {
if let Some(s) = source {
utils::refresh(Some(vec![s]), sender.clone());
} else {
utils::refresh(None, sender.clone());
}
}
Ok(Action::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(),
Ok(Action::RefreshWidget) => content.update_widget(),
Ok(Action::RefreshWidgetIfVis) => content.update_widget_if_visible(),
Ok(Action::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_home_if_background(),
Ok(Action::RefreshEpisodesView) => content.update_episode_view(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(),
Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_| error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
if let Err(err) = content.get_shows().replace_widget(pd) {
error!("Something went wrong while trying to update the ShowWidget.");
error!("Error: {}", err);
}
Ok(Action::ShowWidgetAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut().switch_visible(
PopulatedState::Widget,
gtk::StackTransitionType::SlideLeft,
);
}
Ok(Action::ShowShowsAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
}
Ok(Action::ShowWidgetAnimated) => content.get_shows().switch_widget_animated(),
Ok(Action::ShowShowsAnimated) => content.get_shows().switch_podcasts_animated(),
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let notif = mark_all_notif(pd, &sender);
let callback = clone!(sender => move || {
if let Err(err) = mark_all_watched(&pd, sender.clone()) {
error!("Something went horribly wrong with the notif callback: {}", err);
}
glib::Continue(false)
});
let text = "Marked all episodes as listened".into();
let notif = InAppNotification::new(text, callback, || {}, sender.clone());
notif.show(&overlay);
}
Ok(Action::RemoveShow(pd)) => {
let notif = remove_show_notif(pd, sender.clone());
let text = format!("Unsubscribed from {}", pd.title());
if let Err(err) = utils::ignore_show(pd.id()) {
error!("Could not insert {} to the ignore list.", pd.title());
error!("Error: {}", err);
}
let callback = clone!(pd => move || {
if let Err(err) = utils::uningore_show(pd.id()) {
error!("Could not remove {} from the ignore list.", pd.title());
error!("Error: {}", err);
}
// Spawn a thread so it won't block the ui.
rayon::spawn(clone!(pd => move || {
if let Err(err) = delete_show(&pd) {
error!("Something went wrong trying to remove {}", pd.title());
error!("Error: {}", err);
}
}));
glib::Continue(false)
});
let sender_ = sender.clone();
let undo_wrap = move || -> Result<(), Error> {
utils::uningore_show(pd.id())?;
sender_.send(Action::RefreshShowsView)?;
sender_.send(Action::RefreshEpisodesView)?;
Ok(())
};
let undo_callback = move || {
if let Err(err) = undo_wrap() {
error!("{}", err)
}
};
let sender_ = sender.clone();
let notif = InAppNotification::new(text, callback, undo_callback, sender_);
notif.show(&overlay);
}
Err(_) => (),
+13 -1
View File
@@ -2,8 +2,11 @@ use glib;
use gtk;
use gtk::prelude::*;
use app::Action;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct InAppNotification {
@@ -32,7 +35,12 @@ impl Default for InAppNotification {
}
impl InAppNotification {
pub fn new<F, U>(text: &str, mut callback: F, undo_callback: U) -> Self
pub fn new<F, U>(
text: String,
mut callback: F,
undo_callback: U,
sender: Sender<Action>,
) -> Self
where
F: FnMut() -> glib::Continue + 'static,
U: Fn() + 'static,
@@ -59,6 +67,10 @@ impl InAppNotification {
// Hide the notification
revealer.set_reveal_child(false);
// Refresh the widget if visible
if let Err(err) = sender.send(Action::RefreshWidgetIfVis) {
error!("Action channel blew up: {}", err)
}
});
// Hide the revealer when the close button is clicked
+21 -25
View File
@@ -1,4 +1,3 @@
use glib;
use gtk;
use gtk::prelude::*;
@@ -6,14 +5,14 @@ use failure::Error;
use failure::ResultExt;
use url::Url;
use hammond_data::dbqueries;
use hammond_data::Source;
use hammond_data::dbqueries;
use std::sync::mpsc::Sender;
use app::Action;
use stacks::Content;
use utils::{itunes_to_rss, refresh};
use utils::itunes_to_rss;
#[derive(Debug, Clone)]
pub struct Header {
@@ -61,13 +60,13 @@ impl Default for Header {
// TODO: Refactor components into smaller state machines
impl Header {
pub fn new(content: &Content, window: &gtk::Window, sender: &Sender<Action>) -> Header {
pub fn new(content: &Content, window: &gtk::Window, sender: Sender<Action>) -> Header {
let h = Header::default();
h.init(content, window, &sender);
h.init(content, window, sender);
h
}
pub fn init(&self, content: &Content, window: &gtk::Window, sender: &Sender<Action>) {
pub fn init(&self, content: &Content, window: &gtk::Window, sender: Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
@@ -77,15 +76,15 @@ impl Header {
self.switch.set_stack(&content.get_stack());
new_url.connect_changed(clone!(add_button => move |url| {
on_url_change(url, &result_label, &add_button)
.map_err(|err| error!("Error: {}", err))
.ok();
if let Err(err) = on_url_change(url, &result_label, &add_button) {
error!("Error: {}", err);
}
}));
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
on_add_bttn_clicked(&new_url, sender.clone())
.map_err(|err| error!("Error: {}", err))
.ok();
if let Err(err) = on_add_bttn_clicked(&new_url, sender.clone()) {
error!("Error: {}", err);
}
add_popover.hide();
}));
@@ -93,11 +92,9 @@ impl Header {
self.update_button
.connect_clicked(clone!(sender => move |_| {
gtk::idle_add(clone!(sender => move || {
let s: Option<Vec<_>> = None;
refresh(s, sender.clone());
glib::Continue(false)
}));
sender
.send(Action::UpdateSources(None))
.expect("Action channel blew up.");
}));
self.about_button
@@ -117,9 +114,9 @@ impl Header {
add_toggle.show();
back.hide();
show_title.hide();
sender.send(Action::ShowShowsAnimated)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
if let Err(err) = sender.send(Action::ShowShowsAnimated) {
error!("Action channel blew up: {}", err);
}
}),
);
}
@@ -173,10 +170,9 @@ fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(),
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text("");
gtk::idle_add(move || {
refresh(Some(vec![source.clone()]), sender.clone());
glib::Continue(false)
});
sender
.send(Action::UpdateSources(Some(source)))
.context("App channel blew up.")?;
Ok(())
}
@@ -243,7 +239,7 @@ fn about_dialog(window: &gtk::Window) {
dialog.set_modal(true);
// TODO: make it show it fetches the commit hash from which it was built
// and the version number is kept in sync automaticly
dialog.set_version("0.3.2");
dialog.set_version("0.3.1");
dialog.set_program_name("Hammond");
// TODO: Need a wiki page first.
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
+16 -15
View File
@@ -1,10 +1,8 @@
#![cfg_attr(
feature = "cargo-clippy",
allow(clone_on_ref_ptr, blacklisted_name, match_same_arms, option_map_unit_fn)
)]
#![cfg_attr(feature = "cargo-clippy",
allow(clone_on_ref_ptr, needless_pass_by_value, useless_format, blacklisted_name,
match_same_arms))]
#![allow(unknown_lints)]
#![warn(unused_extern_crates, unused)]
#![deny(warnings)]
#![deny(unused_extern_crates, unused)]
extern crate gdk;
extern crate gdk_pixbuf;
@@ -64,17 +62,20 @@ macro_rules! clone {
);
}
mod stacks;
mod widgets;
// They do not need to be public
// But it helps when looking at the generated docs.
pub mod views;
pub mod widgets;
pub mod stacks;
mod app;
mod headerbar;
pub mod headerbar;
pub mod app;
mod appnotif;
mod manager;
mod settings;
mod static_resource;
mod utils;
pub mod settings;
pub mod utils;
pub mod manager;
pub mod static_resource;
pub mod appnotif;
use app::App;
+23 -8
View File
@@ -5,8 +5,11 @@ use rayon;
use hammond_data::dbqueries;
use hammond_downloader::downloader::{get_episode, DownloadProgress};
use app::Action;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::mpsc::Sender;
// use std::sync::atomic::AtomicUsize;
// use std::path::PathBuf;
@@ -75,7 +78,7 @@ lazy_static! {
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
}
pub fn add(id: i32, directory: String) -> 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()));
@@ -85,12 +88,14 @@ pub fn add(id: i32, directory: String) -> Result<(), Error> {
};
DLPOOL.spawn(move || {
if let Ok(mut episode) = dbqueries::get_episode_widget_from_rowid(id) {
if let Ok(episode) = dbqueries::get_episode_from_rowid(id) {
let pid = episode.podcast_id();
let id = episode.rowid();
get_episode(&mut episode, directory.as_str(), Some(prog))
.map_err(|err| error!("Download Failed: {}", err))
.ok();
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() {
let foo = m.remove(&id);
@@ -100,6 +105,13 @@ pub fn add(id: i32, directory: String) -> Result<(), Error> {
// if let Ok(m) = ACTIVE_DOWNLOADS.read() {
// debug!("ACTIVE DOWNLOADS: {:#?}", m);
// }
sender
.send(Action::RefreshEpisodesView)
.expect("Action channel blew up.");
sender
.send(Action::RefreshWidgetIfSame(pid))
.expect("Action channel blew up.");
}
});
@@ -110,16 +122,17 @@ pub fn add(id: i32, directory: String) -> Result<(), Error> {
mod tests {
use super::*;
use hammond_data::{Episode, Source};
use hammond_data::dbqueries;
use hammond_data::pipeline;
use hammond_data::utils::get_download_folder;
use hammond_data::{Episode, Source};
use hammond_downloader::downloader::get_episode;
use std::{thread, time};
use std::fs;
use std::path::Path;
use std::{thread, time};
use std::sync::mpsc::channel;
#[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
@@ -141,9 +154,11 @@ mod tests {
// Get an episode
let episode: Episode = dbqueries::get_episode_from_pk(title, pd.id()).unwrap();
let (sender, _rx) = channel();
let download_fold = get_download_folder(&pd.title()).unwrap();
let fold2 = download_fold.clone();
add(episode.rowid(), download_fold).unwrap();
add(episode.rowid(), download_fold, sender).unwrap();
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
// Give it soem time to download the file
+1 -47
View File
@@ -1,11 +1,8 @@
use gio;
use gio::{Settings, SettingsExt};
use gio::SettingsExt;
use gtk;
use gtk::GtkWindowExt;
use chrono::prelude::*;
use chrono::Duration;
pub struct WindowGeometry {
left: i32,
top: i32,
@@ -70,49 +67,6 @@ impl WindowGeometry {
}
}
pub fn get_refresh_interval(settings: &Settings) -> Duration {
let time = i64::from(settings.get_int("refresh-interval-time"));
let period = settings.get_string("refresh-interval-period").unwrap();
time_period_to_duration(time, period.as_str())
}
pub fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
let time = i64::from(settings.get_int("cleanup-age-time"));
let period = settings.get_string("cleanup-age-period").unwrap();
let duration = time_period_to_duration(time, period.as_str());
Utc::now() - duration
}
pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
match period {
"weeks" => Duration::weeks(time),
"days" => Duration::days(time),
"hours" => Duration::hours(time),
"minutes" => Duration::minutes(time),
_ => Duration::seconds(time),
}
}
#[test]
fn test_time_period_to_duration() {
let time = 2;
let week = 604800 * time;
let day = 86400 * time;
let hour = 3600 * time;
let minute = 60 * time;
assert_eq!(week, time_period_to_duration(time, "weeks").num_seconds());
assert_eq!(day, time_period_to_duration(time, "days").num_seconds());
assert_eq!(hour, time_period_to_duration(time, "hours").num_seconds());
assert_eq!(
minute,
time_period_to_duration(time, "minutes").num_seconds()
);
assert_eq!(time, time_period_to_duration(time, "seconds").num_seconds());
}
// #[test]
// fn test_apply_window_geometry() {
// gtk::init().expect("Error initializing gtk.");
+46 -40
View File
@@ -4,85 +4,91 @@ use gtk::prelude::*;
use failure::Error;
use app::Action;
use stacks::{HomeStack, ShowStack};
use stacks::EpisodeStack;
use stacks::ShowStack;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct Content {
stack: gtk::Stack,
shows: Rc<RefCell<ShowStack>>,
home: Rc<RefCell<HomeStack>>,
shows: Arc<ShowStack>,
episodes: Arc<EpisodeStack>,
sender: Sender<Action>,
}
impl Content {
pub fn new(sender: Sender<Action>) -> Result<Content, Error> {
let stack = gtk::Stack::new();
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?));
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())?));
let episodes = Arc::new(EpisodeStack::new(sender.clone())?);
let shows = Arc::new(ShowStack::new(sender.clone())?);
stack.add_titled(&home.borrow().get_stack(), "home", "Recent");
stack.add_titled(&shows.borrow().get_stack(), "shows", "Shows");
stack.add_titled(&episodes.get_stack(), "episodes", "Episodes");
stack.add_titled(&shows.get_stack(), "shows", "Shows");
Ok(Content {
stack,
shows,
home,
episodes,
sender,
})
}
pub fn update(&self) {
self.update_home();
self.update_shows();
self.update_episode_view();
self.update_shows_view();
self.update_widget()
}
pub fn update_home(&self) {
self.home
.borrow_mut()
.update()
.map_err(|err| error!("Failed to update HomeView: {}", err))
.ok();
}
pub fn update_home_if_background(&self) {
if self.stack.get_visible_child_name() != Some("home".into()) {
self.update_home();
// TODO: Maybe propagate the error?
pub fn update_episode_view(&self) {
if let Err(err) = self.episodes.update() {
error!("Something went wrong while trying to update the episode view.");
error!("Error: {}", err);
}
}
fn update_shows(&self) {
self.shows
.borrow_mut()
.update()
.map_err(|err| error!("Failed to update ShowsView: {}", err))
.ok();
pub fn update_episode_view_if_baground(&self) {
if self.stack.get_visible_child_name() != Some("episodes".into()) {
self.update_episode_view();
}
}
pub fn update_shows_view(&self) {
self.shows
.borrow_mut()
.update()
.map_err(|err| error!("Failed to update ShowsView: {}", err))
.ok();
if let Err(err) = self.shows.update_podcasts() {
error!("Something went wrong while trying to update the ShowsView.");
error!("Error: {}", err);
}
}
pub fn update_widget(&self) {
if let Err(err) = self.shows.update_widget() {
error!("Something went wrong while trying to update the Show Widget.");
error!("Error: {}", err);
}
}
pub fn update_widget_if_same(&self, pid: i32) {
let pop = self.shows.borrow().populated();
pop.borrow_mut()
.update_widget_if_same(pid)
.map_err(|err| error!("Failed to update ShowsWidget: {}", err))
.ok();
if let Err(err) = self.shows.update_widget_if_same(pid) {
error!("Something went wrong while trying to update the Show Widget.");
error!("Error: {}", err);
}
}
pub fn update_widget_if_visible(&self) {
if self.stack.get_visible_child_name() == Some("shows".to_string())
&& self.shows.get_stack().get_visible_child_name() == Some("widget".to_string())
{
self.update_widget();
}
}
pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone()
}
pub fn get_shows(&self) -> Rc<RefCell<ShowStack>> {
pub fn get_shows(&self) -> Arc<ShowStack> {
self.shows.clone()
}
}
+77
View File
@@ -0,0 +1,77 @@
use gtk;
use gtk::Cast;
use gtk::prelude::*;
use failure::Error;
use views::{EmptyView, EpisodesView};
use app::Action;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct EpisodeStack {
stack: gtk::Stack,
sender: Sender<Action>,
}
impl EpisodeStack {
pub fn new(sender: Sender<Action>) -> Result<EpisodeStack, Error> {
let episodes = EpisodesView::new(sender.clone())?;
let empty = EmptyView::new();
let stack = gtk::Stack::new();
stack.add_named(&episodes.container, "episodes");
stack.add_named(&empty.container, "empty");
if episodes.is_empty() {
stack.set_visible_child_name("empty");
} else {
stack.set_visible_child_name("episodes");
}
Ok(EpisodeStack { stack, sender })
}
// Look into refactoring to a state-machine.
pub fn update(&self) -> Result<(), Error> {
let old = self.stack
.get_child_by_name("episodes")
.ok_or_else(|| format_err!("Faild to get \"episodes\" child from the stack."))?
.downcast::<gtk::Box>()
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
debug!("Name: {:?}", WidgetExt::get_name(&old));
let scrolled_window = old.get_children()
.first()
.ok_or_else(|| format_err!("Box container has no childs."))?
.clone()
.downcast::<gtk::ScrolledWindow>()
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
let eps = EpisodesView::new(self.sender.clone())?;
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| eps.set_vadjustment(&x));
self.stack.remove(&old);
self.stack.add_named(&eps.container, "episodes");
if eps.is_empty() {
self.stack.set_visible_child_name("empty");
} else {
self.stack.set_visible_child_name("episodes");
}
old.destroy();
Ok(())
}
pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone()
}
}
-110
View File
@@ -1,110 +0,0 @@
use gtk;
use gtk::prelude::*;
use failure::Error;
use hammond_data::dbqueries::is_episodes_populated;
use hammond_data::errors::DataError;
use app::Action;
use widgets::{EmptyView, HomeView};
use std::rc::Rc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone, Copy)]
enum State {
Home,
Empty,
}
#[derive(Debug, Clone)]
pub struct HomeStack {
empty: EmptyView,
episodes: Rc<HomeView>,
stack: gtk::Stack,
state: State,
sender: Sender<Action>,
}
impl HomeStack {
pub fn new(sender: Sender<Action>) -> Result<HomeStack, Error> {
let episodes = HomeView::new(sender.clone())?;
let empty = EmptyView::new();
let stack = gtk::Stack::new();
let state = State::Empty;
stack.add_named(&episodes.container, "home");
stack.add_named(&empty.container, "empty");
let mut home = HomeStack {
empty,
episodes,
stack,
state,
sender,
};
home.determine_state()?;
Ok(home)
}
pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone()
}
pub fn update(&mut self) -> Result<(), Error> {
// Copy the vertical scrollbar adjustment from the old view.
self.episodes
.save_alignment()
.map_err(|err| error!("Failed to set episodes_view allignment: {}", err))
.ok();
self.replace_view()?;
self.determine_state().map_err(From::from)
}
fn replace_view(&mut self) -> Result<(), Error> {
// Get the container of the view
let old = &self.episodes.container.clone();
let eps = HomeView::new(self.sender.clone())?;
// Remove the old widget and add the new one
self.stack.remove(old);
self.stack.add_named(&eps.container, "home");
// replace view in the struct too
self.episodes = eps;
// This might not be needed
old.destroy();
Ok(())
}
#[inline]
fn switch_visible(&mut self, s: State) {
use self::State::*;
match s {
Home => {
self.stack.set_visible_child_name("home");
self.state = Home;
}
Empty => {
self.stack.set_visible_child_name("empty");
self.state = Empty;
}
}
}
#[inline]
fn determine_state(&mut self) -> Result<(), DataError> {
if is_episodes_populated()? {
self.switch_visible(State::Home);
} else {
self.switch_visible(State::Empty);
};
Ok(())
}
}
+3 -5
View File
@@ -1,9 +1,7 @@
mod content;
mod home;
mod populated;
mod episode;
mod show;
pub use self::content::Content;
pub use self::home::HomeStack;
pub use self::populated::{PopulatedStack, PopulatedState};
pub use self::show::{ShowStack, ShowState};
pub use self::episode::EpisodeStack;
pub use self::show::ShowStack;
-152
View File
@@ -1,152 +0,0 @@
use gtk;
use gtk::prelude::*;
use failure::Error;
use hammond_data::dbqueries;
use hammond_data::Podcast;
use app::Action;
use widgets::{ShowWidget, ShowsView};
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
#[derive(Debug, Clone, Copy)]
pub enum PopulatedState {
View,
Widget,
}
#[derive(Debug, Clone)]
pub struct PopulatedStack {
container: gtk::Box,
populated: Rc<ShowsView>,
show: Rc<ShowWidget>,
stack: gtk::Stack,
state: PopulatedState,
sender: Sender<Action>,
}
impl PopulatedStack {
pub fn new(sender: Sender<Action>) -> Result<PopulatedStack, Error> {
let stack = gtk::Stack::new();
let state = PopulatedState::View;
let populated = ShowsView::new(sender.clone())?;
let show = Rc::new(ShowWidget::default());
let container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
stack.add_named(&populated.container, "shows");
stack.add_named(&show.container, "widget");
container.add(&stack);
container.show_all();
let show = PopulatedStack {
container,
stack,
populated,
show,
state,
sender,
};
Ok(show)
}
pub fn update(&mut self) {
self.update_widget().map_err(|err| format!("{}", err)).ok();
self.update_shows().map_err(|err| format!("{}", err)).ok();
}
pub fn update_shows(&mut self) -> Result<(), Error> {
let old = &self.populated.container.clone();
debug!("Name: {:?}", WidgetExt::get_name(old));
let pop = ShowsView::new(self.sender.clone())?;
self.populated = pop;
self.stack.remove(old);
self.stack.add_named(&self.populated.container, "shows");
// The current visible child might change depending on
// removal and insertion in the gtk::Stack, so we have
// to make sure it will stay the same.
let s = self.state;
self.switch_visible(s, gtk::StackTransitionType::None);
old.destroy();
Ok(())
}
pub fn replace_widget(&mut self, pd: Arc<Podcast>) -> Result<(), Error> {
let old = self.show.container.clone();
// save the ShowWidget vertical scrollabar alignment
self.show
.podcast_id()
.map(|id| self.show.save_vadjustment(id));
let new = ShowWidget::new(pd, self.sender.clone());
self.show = new;
self.stack.remove(&old);
self.stack.add_named(&self.show.container, "widget");
// The current visible child might change depending on
// removal and insertion in the gtk::Stack, so we have
// to make sure it will stay the same.
let s = self.state;
self.switch_visible(s, gtk::StackTransitionType::None);
Ok(())
}
pub fn update_widget(&mut self) -> Result<(), Error> {
let old = self.show.container.clone();
let id = self.show.podcast_id();
if id.is_none() {
return Ok(());
}
let pd = dbqueries::get_podcast_from_id(id.unwrap_or_default())?;
self.replace_widget(Arc::new(pd))?;
// The current visible child might change depending on
// removal and insertion in the gtk::Stack, so we have
// to make sure it will stay the same.
let s = self.state;
self.switch_visible(s, gtk::StackTransitionType::None);
old.destroy();
Ok(())
}
// Only update widget if its podcast_id is equal to pid.
pub fn update_widget_if_same(&mut self, pid: i32) -> Result<(), Error> {
if self.show.podcast_id() != Some(pid) {
debug!("Different widget. Early return");
return Ok(());
}
self.update_widget()
}
pub fn container(&self) -> gtk::Box {
self.container.clone()
}
#[inline]
pub fn switch_visible(&mut self, state: PopulatedState, animation: gtk::StackTransitionType) {
use self::PopulatedState::*;
match state {
View => {
self.stack.set_visible_child_full("shows", animation);
self.state = View;
}
Widget => {
self.stack.set_visible_child_full("widget", animation);
self.state = Widget;
}
}
}
}
+153 -68
View File
@@ -1,96 +1,181 @@
use gtk;
use gtk::Cast;
use gtk::prelude::*;
use failure::Error;
use hammond_data::dbqueries::is_podcasts_populated;
use hammond_data::Podcast;
use hammond_data::dbqueries;
use views::{EmptyView, ShowsPopulated};
use app::Action;
use stacks::PopulatedStack;
use utils::get_ignored_shows;
use widgets::EmptyView;
use widgets::ShowWidget;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone, Copy)]
pub enum ShowState {
Populated,
Empty,
}
#[derive(Debug, Clone)]
pub struct ShowStack {
empty: EmptyView,
populated: Rc<RefCell<PopulatedStack>>,
stack: gtk::Stack,
state: ShowState,
sender: Sender<Action>,
}
impl ShowStack {
pub fn new(sender: Sender<Action>) -> Result<Self, Error> {
let populated = Rc::new(RefCell::new(PopulatedStack::new(sender.clone())?));
let empty = EmptyView::new();
pub fn new(sender: Sender<Action>) -> Result<ShowStack, Error> {
let stack = gtk::Stack::new();
let state = ShowState::Empty;
stack.add_named(&populated.borrow().container(), "populated");
stack.add_named(&empty.container, "empty");
let mut show = ShowStack {
empty,
populated,
let show = ShowStack {
stack,
state,
sender,
sender: sender.clone(),
};
show.determine_state()?;
let pop = ShowsPopulated::new(sender.clone())?;
let widget = ShowWidget::default();
let empty = EmptyView::new();
show.stack.add_named(&pop.container, "podcasts");
show.stack.add_named(&widget.container, "widget");
show.stack.add_named(&empty.container, "empty");
if pop.is_empty() {
show.stack.set_visible_child_name("empty")
} else {
show.stack.set_visible_child_name("podcasts")
}
Ok(show)
}
// pub fn update(&self) {
// self.update_widget();
// self.update_podcasts();
// }
pub fn update_podcasts(&self) -> Result<(), Error> {
let vis = self.stack
.get_visible_child_name()
.ok_or_else(|| format_err!("Failed to get visible child name."))?;
let old = self.stack
.get_child_by_name("podcasts")
.ok_or_else(|| format_err!("Faild to get \"podcasts\" child from the stack."))?
.downcast::<gtk::Box>()
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
debug!("Name: {:?}", WidgetExt::get_name(&old));
let scrolled_window = old.get_children()
.first()
.ok_or_else(|| format_err!("Box container has no childs."))?
.clone()
.downcast::<gtk::ScrolledWindow>()
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
let pop = ShowsPopulated::new(self.sender.clone())?;
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| pop.set_vadjustment(&x));
self.stack.remove(&old);
self.stack.add_named(&pop.container, "podcasts");
if pop.is_empty() {
self.stack.set_visible_child_name("empty");
} else if vis != "empty" {
self.stack.set_visible_child_name(&vis);
} else {
self.stack.set_visible_child_name("podcasts");
}
old.destroy();
Ok(())
}
pub fn replace_widget(&self, pd: Arc<Podcast>) -> Result<(), Error> {
let old = self.stack
.get_child_by_name("widget")
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?
.downcast::<gtk::Box>()
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
debug!("Name: {:?}", WidgetExt::get_name(&old));
let new = ShowWidget::new(pd, self.sender.clone());
// Each composite ShowWidget is a gtkBox with the Podcast.id encoded in the
// gtk::Widget name. It's a hack since we can't yet subclass GObject
// easily.
let oldid = WidgetExt::get_name(&old);
let newid = WidgetExt::get_name(&new.container);
debug!("Old widget Name: {:?}\nNew widget Name: {:?}", oldid, newid);
// Only copy the old scrollbar if both widget's represent the same podcast.
if newid == oldid {
let scrolled_window = old.get_children()
.first()
.ok_or_else(|| format_err!("Box container has no childs."))?
.clone()
.downcast::<gtk::ScrolledWindow>()
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| new.set_vadjustment(&x));
}
self.stack.remove(&old);
self.stack.add_named(&new.container, "widget");
Ok(())
}
pub fn update_widget(&self) -> Result<(), Error> {
let vis = self.stack
.get_visible_child_name()
.ok_or_else(|| format_err!("Failed to get visible child name."))?;
let old = self.stack
.get_child_by_name("widget")
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?;
let id = WidgetExt::get_name(&old);
if id == Some("GtkBox".to_string()) || id.is_none() {
return Ok(());
}
let id = id.ok_or_else(|| format_err!("Failed to get widget's name."))?;
let pd = dbqueries::get_podcast_from_id(id.parse::<i32>()?)?;
self.replace_widget(Arc::new(pd))?;
self.stack.set_visible_child_name(&vis);
old.destroy();
Ok(())
}
// Only update widget if it's podcast_id is equal to pid.
pub fn update_widget_if_same(&self, pid: i32) -> Result<(), Error> {
let old = self.stack
.get_child_by_name("widget")
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?;
let id = WidgetExt::get_name(&old);
if id != Some(pid.to_string()) || id.is_none() {
debug!("Different widget. Early return");
return Ok(());
}
self.update_widget()
}
pub fn switch_podcasts_animated(&self) {
self.stack
.set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight);
}
pub fn switch_widget_animated(&self) {
self.stack
.set_visible_child_full("widget", gtk::StackTransitionType::SlideLeft)
}
pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone()
}
pub fn populated(&self) -> Rc<RefCell<PopulatedStack>> {
self.populated.clone()
}
pub fn update(&mut self) -> Result<(), Error> {
self.populated.borrow_mut().update();
self.determine_state()
}
#[inline]
fn switch_visible(&mut self, s: ShowState) {
use self::ShowState::*;
match s {
Populated => {
self.stack.set_visible_child_name("populated");
self.state = Populated;
}
Empty => {
self.stack.set_visible_child_name("empty");
self.state = Empty;
}
};
}
#[inline]
fn determine_state(&mut self) -> Result<(), Error> {
use self::ShowState::*;
let ign = get_ignored_shows()?;
debug!("IGNORED SHOWS {:?}", ign);
if is_podcasts_populated(&ign)? {
self.switch_visible(Populated);
} else {
self.switch_visible(Empty);
};
Ok(())
}
}
+107 -167
View File
@@ -1,13 +1,11 @@
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
use gdk::FrameClockExt;
use gdk_pixbuf::Pixbuf;
use gio::{Settings, SettingsExt};
use glib;
use gtk;
use gtk::prelude::*;
use gtk::{IsA, Widget};
use chrono::prelude::*;
use failure::Error;
use rayon;
use regex::Regex;
@@ -16,128 +14,21 @@ use send_cell::SendCell;
use serde_json::Value;
// use hammond_data::feed;
use hammond_data::{PodcastCoverQuery, Source};
use hammond_data::dbqueries;
use hammond_data::pipeline;
use hammond_data::utils::checkup;
use hammond_data::Source;
use hammond_downloader::downloader;
use std::collections::{HashMap, HashSet};
use std::sync::mpsc::*;
use std::sync::Arc;
use std::sync::{Mutex, RwLock};
use std::sync::Arc;
use std::sync::mpsc::*;
use app::Action;
/// Lazy evaluates and loads widgets to the parent `container` widget.
///
/// Accepts an `IntoIterator`, `data`, as the source from which each widget
/// will be constructed. An `FnMut` function that returns the desired
/// widget should be passed as the widget `constructor`. You can also specify
/// a `callback` that will be executed when the iteration finish.
///
/// ```no_run
/// # struct Message;
/// # struct MessageWidget(gtk::Label);
///
/// # impl MessageWidget {
/// # fn new(_: Message) -> Self {
/// # MessageWidget(gtk::Label::new("A message"))
/// # }
/// # }
///
/// let messages: Vec<Message> = Vec::new();
/// let list = gtk::ListBox::new();
/// let constructor = |m| { MessageWidget::new(m).0};
/// lazy_load(messages, list, constructor, || {});
/// ```
///
/// If you have already constructed the widgets and only want to
/// load them to the parent you can pass a closure that returns it's
/// own argument to the constructor.
///
/// ```no_run
/// # use std::collections::binary_heap::BinaryHeap;
/// let widgets: BinaryHeap<gtk::Button> = BinaryHeap::new();
/// let list = gtk::ListBox::new();
/// lazy_load(widgets, list, |w| w, || {});
/// ```
#[inline]
pub fn lazy_load<T, C, F, W, U>(data: T, container: C, mut contructor: F, callback: U)
where
T: IntoIterator + 'static,
T::Item: 'static,
C: ContainerExt + 'static,
F: FnMut(T::Item) -> W + 'static,
W: IsA<Widget>,
U: Fn() + 'static,
{
let func = move |x| container.add(&contructor(x));
lazy_load_full(data, func, callback);
}
/// Iterate over `data` and execute `func` using a `gtk::idle_add()`,
/// when the iteration finishes, it executes `finish_callback`.
///
/// This is a more flexible version of `lazy_load` with less constrains.
/// If you just want to lazy add `widgets` to a `container` check if
/// `lazy_load` fits your needs first.
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))]
pub fn lazy_load_full<T, F, U>(data: T, mut func: F, finish_callback: U)
where
T: IntoIterator + 'static,
T::Item: 'static,
F: FnMut(T::Item) + 'static,
U: Fn() + 'static,
{
let mut data = data.into_iter();
gtk::idle_add(move || {
data.next()
.map(|x| func(x))
.map(|_| glib::Continue(true))
.unwrap_or_else(|| {
finish_callback();
glib::Continue(false)
})
});
}
// Kudos to Julian Sparber
// https://blogs.gnome.org/jsparber/2018/04/29/animate-a-scrolledwindow/
#[cfg_attr(feature = "cargo-clippy", allow(float_cmp))]
pub fn smooth_scroll_to(view: &gtk::ScrolledWindow, target: &gtk::Adjustment) {
if let Some(adj) = view.get_vadjustment() {
if let Some(clock) = view.get_frame_clock() {
let duration = 200;
let start = adj.get_value();
let end = target.get_value();
let start_time = clock.get_frame_time();
let end_time = start_time + 1000 * duration;
view.add_tick_callback(move |_, clock| {
let now = clock.get_frame_time();
// FIXME: `adj.get_value != end` is a float comparison...
if now < end_time && adj.get_value().abs() != end.abs() {
let mut t = (now - start_time) as f64 / (end_time - start_time) as f64;
t = ease_out_cubic(t);
adj.set_value(start + t * (end - start));
glib::Continue(true)
} else {
adj.set_value(end);
glib::Continue(false)
}
});
}
}
}
// From clutter-easing.c, based on Robert Penner's
// infamous easing equations, MIT license.
fn ease_out_cubic(t: f64) -> f64 {
let p = t - 1f64;
p * p * p + 1f64
}
use chrono::Duration;
use chrono::prelude::*;
lazy_static! {
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
@@ -165,56 +56,78 @@ pub fn get_ignored_shows() -> Result<Vec<i32>, Error> {
}
pub fn cleanup(cleanup_date: DateTime<Utc>) {
checkup(cleanup_date)
.map_err(|err| error!("Check up failed: {}", err))
.ok();
if let Err(err) = checkup(cleanup_date) {
error!("Check up failed: {}", err);
}
}
pub fn refresh<S>(source: Option<S>, sender: Sender<Action>)
where
S: IntoIterator<Item = Source> + Send + 'static,
{
refresh_feed(source, sender)
.map_err(|err| error!("Failed to update feeds: {}", err))
.ok();
pub fn refresh(source: Option<Vec<Source>>, sender: Sender<Action>) {
if let Err(err) = refresh_feed(source, sender) {
error!("An error occured while trying to update the feeds.");
error!("Error: {}", err);
}
}
pub fn get_refresh_interval(settings: &Settings) -> Duration {
let time = settings.get_int("refresh-interval-time") as i64;
let period = settings.get_string("refresh-interval-period").unwrap();
time_period_to_duration(time, period.as_str())
}
pub fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
let time = settings.get_int("cleanup-age-time") as i64;
let period = settings.get_string("cleanup-age-period").unwrap();
let duration = time_period_to_duration(time, period.as_str());
Utc::now() - duration
}
/// Update the rss feed(s) originating from `source`.
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
/// When It's done,it queues up a `RefreshViews` action.
fn refresh_feed<S>(source: Option<S>, sender: Sender<Action>) -> Result<(), Error>
where
S: IntoIterator<Item = Source> + Send + 'static,
{
sender
.send(Action::HeaderBarShowUpdateIndicator)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(), Error> {
sender.send(Action::HeaderBarShowUpdateIndicator)?;
rayon::spawn(move || {
if let Some(s) = source {
// Refresh only specified feeds
pipeline::run(s, false)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Error While trying to update the database."))
.ok();
} else {
// Refresh all the feeds
dbqueries::get_sources()
.map(|s| s.into_iter())
.and_then(|s| pipeline::run(s, false))
.map_err(|err| error!("Error: {}", err))
.ok();
};
let mut sources = source.unwrap_or_else(|| {
dbqueries::get_sources().expect("Failed to retrieve Sources from the database.")
});
// Work around to improve the feed addition experience.
// Many times links to rss feeds are just redirects(usually to an https
// version). Sadly I haven't figured yet a nice way to follow up links
// redirects without getting to lifetime hell with futures and hyper.
// So the requested refresh is only of 1 feed, and the feed fails to be indexed,
// (as a 301 redict would update the source entry and exit), another refresh is
// run. For more see hammond_data/src/models/source.rs `fn
// request_constructor`. also ping me on irc if or open an issue if you
// want to tackle it.
if sources.len() == 1 {
let source = sources.remove(0);
let id = source.id();
if let Err(err) = pipeline::index_single_source(source, false) {
error!("Error While trying to update the database.");
error!("Error msg: {}", err);
if let Ok(source) = dbqueries::get_source_from_id(id) {
if let Err(err) = pipeline::index_single_source(source, false) {
error!("Error While trying to update the database.");
error!("Error msg: {}", err);
}
}
}
// This is what would normally run
} else if let Err(err) = pipeline::run(sources, false) {
error!("Error While trying to update the database.");
error!("Error msg: {}", err);
}
sender
.send(Action::HeaderBarHideUpdateIndicator)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
.expect("Action channel blew up.");
sender
.send(Action::RefreshAllViews)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
.expect("Action channel blew up.");
});
Ok(())
}
@@ -233,16 +146,19 @@ 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?
#[inline]
pub fn set_image_from_path(image: &gtk::Image, podcast_id: i32, size: u32) -> Result<(), Error> {
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(&podcast_id) {
let callback = clone!(image => move || {
let _ = set_image_from_path(&image, podcast_id, size);
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);
@@ -253,7 +169,7 @@ pub fn set_image_from_path(image: &gtk::Image, podcast_id: i32, size: u32) -> Re
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(&(podcast_id, size)) {
if let Some(guard) = hashmap.get(&(pd.id(), size)) {
guard
.lock()
.map_err(|err| format_err!("SendCell Mutex: {}", err))
@@ -269,20 +185,16 @@ pub fn set_image_from_path(image: &gtk::Image, podcast_id: i32, size: u32) -> Re
}
let (sender, receiver) = channel();
let pd_ = pd.clone();
THREADPOOL.spawn(move || {
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
guard.insert(podcast_id);
guard.insert(pd_.id());
}
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(podcast_id) {
sender
.send(downloader::cache_image(&pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}
let _ = sender.send(downloader::cache_image(&pd_));
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
guard.remove(&podcast_id);
guard.remove(&pd_.id());
}
});
@@ -293,7 +205,7 @@ pub fn set_image_from_path(image: &gtk::Image, podcast_id: i32, size: u32) -> Re
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((podcast_id, size), Mutex::new(SendCell::new(px.clone())));
hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone())));
image.set_from_pixbuf(&px);
}
}
@@ -335,17 +247,45 @@ fn lookup_id(id: u32) -> Result<String, Error> {
.ok_or_else(|| format_err!("Failed to get url from itunes response"))
}
pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
match period {
"weeks" => Duration::weeks(time),
"days" => Duration::days(time),
"hours" => Duration::hours(time),
"minutes" => Duration::minutes(time),
_ => Duration::seconds(time),
}
}
#[cfg(test)]
mod tests {
use super::*;
// use hammond_data::Source;
// use hammond_data::dbqueries;
#[test]
fn test_time_period_to_duration() {
let time = 2;
let week = 604800 * time;
let day = 86400 * time;
let hour = 3600 * time;
let minute = 60 * time;
assert_eq!(week, time_period_to_duration(time, "weeks").num_seconds());
assert_eq!(day, time_period_to_duration(time, "days").num_seconds());
assert_eq!(hour, time_period_to_duration(time, "hours").num_seconds());
assert_eq!(
minute,
time_period_to_duration(time, "minutes").num_seconds()
);
assert_eq!(time, time_period_to_duration(time, "seconds").num_seconds());
}
// #[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
// to run it.
// #[ignore]
// Disabled till https://gitlab.gnome.org/World/hammond/issues/56
// 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
@@ -6,7 +6,6 @@ pub struct EmptyView {
}
impl Default for EmptyView {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui");
let view: gtk::Box = builder.get_object("empty_view").unwrap();
@@ -16,7 +15,6 @@ impl Default for EmptyView {
}
impl EmptyView {
#[inline]
pub fn new() -> EmptyView {
EmptyView::default()
}
@@ -1,25 +1,17 @@
use chrono::prelude::*;
use failure::Error;
use gtk;
use gtk::prelude::*;
use hammond_data::dbqueries;
use hammond_data::EpisodeWidgetQuery;
use send_cell::SendCell;
use hammond_data::dbqueries;
use app::Action;
use utils::{self, lazy_load_full};
use utils::{get_ignored_shows, set_image_from_path};
use widgets::EpisodeWidget;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use std::sync::Mutex;
lazy_static! {
pub static ref EPISODES_VIEW_VALIGNMENT: Mutex<Option<SendCell<gtk::Adjustment>>> =
Mutex::new(None);
}
#[derive(Debug, Clone)]
enum ListSplit {
@@ -31,7 +23,7 @@ enum ListSplit {
}
#[derive(Debug, Clone)]
pub struct HomeView {
pub struct EpisodesView {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
frame_parent: gtk::Box,
@@ -47,8 +39,7 @@ pub struct HomeView {
rest_list: gtk::ListBox,
}
impl Default for HomeView {
#[inline]
impl Default for EpisodesView {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
@@ -65,7 +56,7 @@ impl Default for HomeView {
let month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
HomeView {
EpisodesView {
container,
scrolled_window,
frame_parent,
@@ -84,82 +75,91 @@ impl Default for HomeView {
}
// TODO: REFACTOR ME
impl HomeView {
#[inline]
pub fn new(sender: Sender<Action>) -> Result<Rc<HomeView>, Error> {
use self::ListSplit::*;
let view = Rc::new(HomeView::default());
let ignore = utils::get_ignored_shows()?;
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 100)?;
impl EpisodesView {
pub fn new(sender: Sender<Action>) -> Result<EpisodesView, Error> {
let view = EpisodesView::default();
let ignore = get_ignored_shows()?;
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 50)?;
let now_utc = Utc::now();
let view_ = view.clone();
let func = move |ep: EpisodeWidgetQuery| {
episodes.into_iter().for_each(|ep| {
let epoch = ep.epoch();
let widget = EpisodesViewWidget::new(ep, &sender);
let viewep = EpisodesViewWidget::new(ep, sender.clone());
match split(&now_utc, i64::from(epoch)) {
Today => add_to_box(&widget, &view_.today_list, &view_.today_box),
Yday => add_to_box(&widget, &view_.yday_list, &view_.yday_box),
Week => add_to_box(&widget, &view_.week_list, &view_.week_box),
Month => add_to_box(&widget, &view_.month_list, &view_.month_box),
Rest => add_to_box(&widget, &view_.rest_list, &view_.rest_box),
let t = split(&now_utc, i64::from(epoch));
match t {
ListSplit::Today => {
view.today_list.add(&viewep.container);
}
};
ListSplit::Yday => {
view.yday_list.add(&viewep.container);
}
ListSplit::Week => {
view.week_list.add(&viewep.container);
}
ListSplit::Month => {
view.month_list.add(&viewep.container);
}
ListSplit::Rest => {
view.rest_list.add(&viewep.container);
}
}
});
let view_ = view.clone();
let callback = move || {
view_
.set_vadjustment()
.map_err(|err| format!("{}", err))
.ok();
};
if view.today_list.get_children().is_empty() {
view.today_box.hide();
}
if view.yday_list.get_children().is_empty() {
view.yday_box.hide();
}
if view.week_list.get_children().is_empty() {
view.week_box.hide();
}
if view.month_list.get_children().is_empty() {
view.month_box.hide();
}
if view.rest_list.get_children().is_empty() {
view.rest_box.hide();
}
lazy_load_full(episodes, func, callback);
view.container.show_all();
Ok(view)
}
#[inline]
pub fn is_empty(&self) -> bool {
if !self.today_list.get_children().is_empty() {
return false;
}
if !self.yday_list.get_children().is_empty() {
return false;
}
if !self.week_list.get_children().is_empty() {
return false;
}
if !self.month_list.get_children().is_empty() {
return false;
}
if !self.rest_list.get_children().is_empty() {
return false;
}
true
}
/// Set scrolled window vertical adjustment.
fn set_vadjustment(&self) -> Result<(), Error> {
let guard = EPISODES_VIEW_VALIGNMENT
.lock()
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
if let Some(ref sendcell) = *guard {
// Copy the vertical scrollbar adjustment from the old view into the new one.
sendcell
.try_get()
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
}
Ok(())
}
#[inline]
/// Save the vertical scrollbar position.
pub fn save_alignment(&self) -> Result<(), Error> {
if let Ok(mut guard) = EPISODES_VIEW_VALIGNMENT.lock() {
let adj = self.scrolled_window
.get_vadjustment()
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
*guard = Some(SendCell::new(adj));
info!("Saved episodes_view alignment.");
}
Ok(())
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
#[inline]
fn add_to_box(widget: &EpisodesViewWidget, listbox: &gtk::ListBox, box_: &gtk::Box) {
listbox.add(&widget.container);
box_.show();
}
#[inline]
fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
let ep = Utc.timestamp(epoch, 0);
@@ -184,7 +184,6 @@ struct EpisodesViewWidget {
}
impl Default for EpisodesViewWidget {
#[inline]
fn default() -> Self {
let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
@@ -202,13 +201,13 @@ impl Default for EpisodesViewWidget {
}
impl EpisodesViewWidget {
fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodesViewWidget {
fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodesViewWidget {
let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let image: gtk::Image = builder.get_object("cover").unwrap();
let pid = episode.podcast_id();
let ep = EpisodeWidget::new(episode, sender);
let ep = EpisodeWidget::new(episode, sender.clone());
let view = EpisodesViewWidget {
container,
@@ -220,17 +219,16 @@ impl EpisodesViewWidget {
view
}
#[inline]
fn init(&self, podcast_id: i32) {
self.set_cover(podcast_id)
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
if let Err(err) = self.set_cover(podcast_id) {
error!("Failed to set a cover: {}", err)
}
self.container.pack_start(&self.episode, true, true, 6);
}
#[inline]
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
utils::set_image_from_path(&self.image, podcast_id, 64)
let pd = Arc::new(dbqueries::get_podcast_cover_from_id(podcast_id)?);
set_image_from_path(&self.image, pd, 64)
}
}
+7
View File
@@ -0,0 +1,7 @@
mod shows;
mod episodes;
mod empty;
pub use self::empty::EmptyView;
pub use self::episodes::EpisodesView;
pub use self::shows::ShowsPopulated;
@@ -2,32 +2,30 @@ use failure::Error;
use gtk;
use gtk::prelude::*;
use hammond_data::{Podcast, PodcastCoverQuery};
use hammond_data::dbqueries;
use hammond_data::Podcast;
use app::Action;
use utils::{get_ignored_shows, lazy_load, set_image_from_path};
use utils::{get_ignored_shows, set_image_from_path};
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct ShowsView {
pub struct ShowsPopulated {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
flowbox: gtk::FlowBox,
}
impl Default for ShowsView {
#[inline]
impl Default for ShowsPopulated {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_view.ui");
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
ShowsView {
ShowsPopulated {
container,
scrolled_window,
flowbox,
@@ -35,43 +33,49 @@ impl Default for ShowsView {
}
}
impl ShowsView {
#[inline]
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> {
let pop = Rc::new(ShowsView::default());
impl ShowsPopulated {
pub fn new(sender: Sender<Action>) -> Result<ShowsPopulated, Error> {
let pop = ShowsPopulated::default();
pop.init(sender)?;
Ok(pop)
}
#[inline]
pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> {
self.flowbox.connect_child_activated(move |_, child| {
on_child_activate(child, &sender)
.map_err(|err| error!("Error along flowbox child activation: {}", err))
.ok();
if let Err(err) = on_child_activate(child, sender.clone()) {
error!(
"Something went wrong during flowbox child activation: {}.",
err
)
};
});
// Populate the flowbox with the Podcasts.
self.populate_flowbox()
}
#[inline]
fn populate_flowbox(&self) -> Result<(), Error> {
let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
let flowbox = self.flowbox.clone();
let constructor = |parent| ShowsChild::new(&parent).child;
let callback = move || flowbox.show_all();
let flowbox = self.flowbox.clone();
lazy_load(podcasts, flowbox, constructor, callback);
podcasts.into_iter().for_each(|parent| {
let flowbox_child = ShowsChild::new(parent);
self.flowbox.add(&flowbox_child.child);
});
self.flowbox.show_all();
Ok(())
}
pub fn is_empty(&self) -> bool {
self.flowbox.get_children().is_empty()
}
#[inline]
fn on_child_activate(child: &gtk::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> {
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
fn on_child_activate(child: &gtk::FlowBoxChild, sender: Sender<Action>) -> Result<(), Error> {
use gtk::WidgetExt;
// This is such an ugly hack...
@@ -94,7 +98,6 @@ struct ShowsChild {
}
impl Default for ShowsChild {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui");
@@ -113,25 +116,23 @@ impl Default for ShowsChild {
}
impl ShowsChild {
#[inline]
pub fn new(pd: &Podcast) -> ShowsChild {
pub fn new(pd: Podcast) -> ShowsChild {
let child = ShowsChild::default();
child.init(pd);
child
}
#[inline]
fn init(&self, pd: &Podcast) {
fn init(&self, pd: Podcast) {
self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string());
self.set_cover(pd.id())
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
let pd = Arc::new(pd.into());
if let Err(err) = self.set_cover(pd) {
error!("Failed to set a cover: {}", err)
}
}
#[inline]
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
set_image_from_path(&self.cover, podcast_id, 256)
fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
set_image_from_path(&self.cover, pd, 256)
}
}
+124 -108
View File
@@ -5,11 +5,12 @@ use gtk::prelude::*;
use failure::Error;
use humansize::FileSize;
use open;
use rayon;
use take_mut;
use hammond_data::{EpisodeWidgetQuery, Podcast};
use hammond_data::dbqueries;
use hammond_data::utils::get_download_folder;
use hammond_data::EpisodeWidgetQuery;
use app::Action;
use manager;
@@ -19,8 +20,8 @@ use std::cell::RefCell;
use std::ops::DerefMut;
use std::path::Path;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::Sender;
#[derive(Debug)]
pub struct EpisodeWidget {
@@ -28,11 +29,10 @@ pub struct EpisodeWidget {
date: DateMachine,
duration: DurationMachine,
title: Rc<RefCell<TitleMachine>>,
media: Rc<RefCell<MediaMachine>>,
media: Arc<Mutex<MediaMachine>>,
}
impl Default for EpisodeWidget {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
@@ -66,7 +66,7 @@ impl Default for EpisodeWidget {
separator2,
prog_separator,
);
let media_machine = Rc::new(RefCell::new(media));
let media_machine = Arc::new(Mutex::new(media));
EpisodeWidget {
container,
@@ -79,15 +79,15 @@ impl Default for EpisodeWidget {
}
impl EpisodeWidget {
#[inline]
pub fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodeWidget {
pub fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodeWidget {
let mut widget = EpisodeWidget::default();
widget.init(episode, sender);
widget
}
#[inline]
fn init(&mut self, episode: EpisodeWidgetQuery, sender: &Sender<Action>) {
fn init(&mut self, episode: EpisodeWidgetQuery, sender: Sender<Action>) {
WidgetExt::set_name(&self.container, &episode.rowid().to_string());
// Set the date label.
self.set_date(episode.epoch());
@@ -98,49 +98,40 @@ impl EpisodeWidget {
self.set_duration(episode.duration());
// Determine what the state of the media widgets should be.
determine_media_state(&self.media, &episode)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
if let Err(err) = self.determine_media_state(&episode) {
error!("Something went wrong determining the Media State.");
error!("Error: {}", err);
}
let episode = Arc::new(Mutex::new(episode));
self.connect_buttons(&episode, sender);
self.connect_buttons(episode, sender);
}
#[inline]
fn connect_buttons(&self, episode: &Arc<Mutex<EpisodeWidgetQuery>>, sender: &Sender<Action>) {
fn connect_buttons(&self, episode: Arc<Mutex<EpisodeWidgetQuery>>, sender: Sender<Action>) {
let title = self.title.clone();
if let Ok(media) = self.media.try_borrow_mut() {
if let Ok(media) = self.media.lock() {
media.play_connect_clicked(clone!(episode, sender => move |_| {
if let Ok(mut ep) = episode.lock() {
on_play_bttn_clicked(&mut ep, &title, &sender)
.map_err(|err| error!("Error: {}", err))
.ok();
if let Err(err) = on_play_bttn_clicked(&mut ep, title.clone(), sender.clone()){
error!("Error: {}", err);
};
}
}));
let media_machine = self.media.clone();
media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| {
// Make the button insensitive so it won't be pressed twice
media.download_connect_clicked(clone!(episode, sender => move |dl| {
dl.set_sensitive(false);
if let Ok(ep) = episode.lock() {
on_download_clicked(&ep, &sender)
.and_then(|_| {
if let Err(err) = on_download_clicked(&ep, sender.clone()) {
error!("Download failed to start.");
error!("Error: {}", err);
} else {
info!("Donwload started succesfully.");
determine_media_state(&media_machine, &ep)
})
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
}
// Restore sensitivity after operations above complete
dl.set_sensitive(true);
}
}));
}
}
#[inline]
/// Determine the title state.
fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
let mut machine = self.title.borrow_mut();
@@ -150,27 +141,23 @@ impl EpisodeWidget {
});
}
#[inline]
/// Set the date label depending on the current time.
fn set_date(&mut self, epoch: i32) {
let machine = &mut self.date;
take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
}
#[inline]
/// Set the duration label.
fn set_duration(&mut self, seconds: Option<i32>) {
let machine = &mut self.duration;
take_mut::take(machine, |duration| duration.determine_state(seconds));
}
}
#[inline]
fn determine_media_state(
media_machine: &Rc<RefCell<MediaMachine>>,
episode: &EpisodeWidgetQuery,
) -> Result<(), Error> {
let id = episode.rowid();
fn determine_media_state(&self, episode: &EpisodeWidgetQuery) -> Result<(), Error> {
let id = WidgetExt::get_name(&self.container)
.ok_or_else(|| format_err!("Failed to get widget Name"))?
.parse::<i32>()?;
let active_dl = || -> Result<Option<_>, Error> {
let m = manager::ACTIVE_DOWNLOADS
.read()
@@ -179,7 +166,7 @@ fn determine_media_state(
Ok(m.get(&id).cloned())
}()?;
let mut lock = media_machine.try_borrow_mut()?;
let mut lock = self.media.lock().map_err(|err| format_err!("{}", err))?;
take_mut::take(lock.deref_mut(), |media| {
media.determine_state(
episode.length(),
@@ -190,67 +177,33 @@ fn determine_media_state(
// Show or hide the play/delete/download buttons upon widget initialization.
if let Some(prog) = active_dl {
// set a callback that will update the state when the download finishes
let id = episode.rowid();
let callback = clone!(media_machine => move || {
if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() {
if !guard.contains_key(&id) {
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
determine_media_state(&media_machine, &ep)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
return glib::Continue(false)
}
}
}
glib::Continue(true)
});
gtk::timeout_add(250, callback);
lock.cancel_connect_clicked(clone!(prog, media_machine => move |_| {
if let Ok(mut m) = prog.lock() {
m.cancel();
}
if let Ok(mut lock) = media_machine.try_borrow_mut() {
if let Ok(episode) = dbqueries::get_episode_widget_from_rowid(id) {
take_mut::take(lock.deref_mut(), |media| {
media.determine_state(
episode.length(),
false,
episode.local_uri().is_some(),
)
});
}
}
}));
lock.cancel_connect_clicked(prog.clone());
drop(lock);
// Setup a callback that will update the progress bar.
update_progressbar_callback(&prog, &media_machine, id);
update_progressbar_callback(prog.clone(), self.media.clone(), id);
// Setup a callback that will update the total_size label
// with the http ContentLength header number rather than
// relying to the RSS feed.
update_total_size_callback(&prog, &media_machine);
update_total_size_callback(prog.clone(), self.media.clone());
}
Ok(())
}
}
#[inline]
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Result<(), Error> {
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())?;
// Start a new download.
manager::add(ep.rowid(), download_fold)?;
manager::add(ep.rowid(), download_fold, sender.clone())?;
// Update Views
sender.send(Action::RefreshEpisodesViewBGR)?;
sender.send(Action::RefreshEpisodesView)?;
sender.send(Action::RefreshWidgetIfVis)?;
Ok(())
}
@@ -258,8 +211,8 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Resu
#[inline]
fn on_play_bttn_clicked(
episode: &mut EpisodeWidgetQuery,
title: &Rc<RefCell<TitleMachine>>,
sender: &Sender<Action>,
title: Rc<RefCell<TitleMachine>>,
sender: Sender<Action>,
) -> Result<(), Error> {
open_uri(episode.rowid())?;
episode.set_played_now()?;
@@ -273,7 +226,6 @@ fn on_play_bttn_clicked(
Ok(())
}
#[inline]
fn open_uri(rowid: i32) -> Result<(), Error> {
let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
.ok_or_else(|| format_err!("Expected Some found None."))?;
@@ -292,22 +244,24 @@ fn open_uri(rowid: i32) -> Result<(), Error> {
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
fn update_progressbar_callback(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
prog: Arc<Mutex<manager::Progress>>,
media: Arc<Mutex<MediaMachine>>,
episode_rowid: i32,
) {
let callback = clone!(prog, media => move || {
progress_bar_helper(&prog, &media, episode_rowid)
timeout_add(
400,
clone!(prog, media => move || {
progress_bar_helper(prog.clone(), media.clone(), episode_rowid)
.unwrap_or(glib::Continue(false))
});
timeout_add(300, callback);
}),
);
}
#[inline]
#[allow(if_same_then_else)]
fn progress_bar_helper(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
prog: Arc<Mutex<manager::Progress>>,
media: Arc<Mutex<MediaMachine>>,
episode_rowid: i32,
) -> Result<glib::Continue, Error> {
let (fraction, downloaded) = {
@@ -324,10 +278,9 @@ fn progress_bar_helper(
.file_size(SIZE_OPTS.clone())
.map_err(|err| format_err!("{}", err))?;
if let Ok(mut m) = media.try_borrow_mut() {
let mut m = media.lock().unwrap();
m.update_progress(&size, fraction);
}
}
// info!("Fraction: {}", progress_bar.get_fraction());
// info!("Fraction: {}", fraction);
@@ -354,19 +307,21 @@ fn progress_bar_helper(
// relying to the RSS feed.
#[inline]
fn update_total_size_callback(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
prog: Arc<Mutex<manager::Progress>>,
media: Arc<Mutex<MediaMachine>>,
) {
let callback = clone!(prog, media => move || {
total_size_helper(&prog, &media).unwrap_or(glib::Continue(true))
});
timeout_add(500, callback);
timeout_add(
500,
clone!(prog, media => move || {
total_size_helper(prog.clone(), media.clone()).unwrap_or(glib::Continue(true))
}),
);
}
#[inline]
fn total_size_helper(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
prog: Arc<Mutex<manager::Progress>>,
media: Arc<Mutex<MediaMachine>>,
) -> Result<glib::Continue, Error> {
// Get the total_bytes.
let total_bytes = {
@@ -378,7 +333,7 @@ fn total_size_helper(
debug!("Total Size: {}", total_bytes);
if total_bytes != 0 {
// Update the total_size label
if let Ok(mut m) = media.try_borrow_mut() {
if let Ok(mut m) = media.lock() {
take_mut::take(m.deref_mut(), |machine| {
machine.set_size(Some(total_bytes as i32))
});
@@ -395,3 +350,64 @@ fn total_size_helper(
// let mut ep = dbqueries::get_episode_from_rowid(episode_id)?.into();
// delete_local_content(&mut ep).map_err(From::from).map(|_| ())
// }
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 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);
}
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))
});
}
+96 -115
View File
@@ -13,7 +13,9 @@ use chrono::prelude::*;
use gtk::prelude::*;
use humansize::{file_size_opts as size_opts, FileSize};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use manager::Progress as OtherProgress;
lazy_static! {
pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
@@ -61,7 +63,6 @@ pub struct Title<S> {
impl<S> Title<S> {
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn set_title(&mut self, s: &str) {
@@ -70,7 +71,6 @@ impl<S> Title<S> {
}
impl Title<Normal> {
#[inline]
fn new(title: gtk::Label) -> Self {
Title {
title,
@@ -80,7 +80,6 @@ impl Title<Normal> {
}
impl From<Title<Normal>> for Title<GreyedOut> {
#[inline]
fn from(f: Title<Normal>) -> Self {
f.title
.get_style_context()
@@ -94,7 +93,6 @@ impl From<Title<Normal>> for Title<GreyedOut> {
}
impl From<Title<GreyedOut>> for Title<Normal> {
#[inline]
fn from(f: Title<GreyedOut>) -> Self {
f.title
.get_style_context()
@@ -114,13 +112,11 @@ pub enum TitleMachine {
}
impl TitleMachine {
#[inline]
pub fn new(label: gtk::Label, is_played: bool) -> Self {
let m = TitleMachine::Normal(Title::<Normal>::new(label));
m.determine_state(is_played)
}
#[inline]
pub fn determine_state(self, is_played: bool) -> Self {
use self::TitleMachine::*;
@@ -132,7 +128,6 @@ impl TitleMachine {
}
}
#[inline]
pub fn set_title(&mut self, s: &str) {
use self::TitleMachine::*;
@@ -156,7 +151,6 @@ pub struct Date<S> {
}
impl<S> Date<S> {
#[inline]
fn into_usual(self, epoch: i64) -> Date<Usual> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b").to_string().trim());
@@ -168,7 +162,6 @@ impl<S> Date<S> {
}
}
#[inline]
fn into_year_shown(self, epoch: i64) -> Date<YearShown> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b %Y").to_string().trim());
@@ -182,7 +175,6 @@ impl<S> Date<S> {
}
impl Date<UnInitialized> {
#[inline]
fn new(date: gtk::Label, epoch: i64) -> Self {
let ts = Utc.timestamp(epoch, 0);
date.set_text(ts.format("%e %b %Y").to_string().trim());
@@ -203,18 +195,16 @@ pub enum DateMachine {
}
impl DateMachine {
#[inline]
pub fn new(label: gtk::Label, epoch: i64) -> Self {
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
m.determine_state(epoch)
}
#[inline]
pub fn determine_state(self, epoch: i64) -> Self {
use self::DateMachine::*;
let ts = Utc.timestamp(epoch, 0);
let is_old = NOW.year() != ts.year();
let is_old = !(NOW.year() == ts.year());
match (self, is_old) {
// Into Usual
@@ -239,7 +229,6 @@ pub struct Duration<S: Visibility> {
}
impl<S: Visibility> Duration<S> {
#[inline]
// This needs a better name.
// TODO: make me mut
fn set_duration(&self, minutes: i64) {
@@ -248,7 +237,6 @@ impl<S: Visibility> Duration<S> {
}
impl Duration<Hidden> {
#[inline]
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
duration.hide();
separator.hide();
@@ -262,7 +250,6 @@ impl Duration<Hidden> {
}
impl From<Duration<Hidden>> for Duration<Shown> {
#[inline]
fn from(f: Duration<Hidden>) -> Self {
f.duration.show();
f.separator.show();
@@ -276,7 +263,6 @@ impl From<Duration<Hidden>> for Duration<Shown> {
}
impl From<Duration<Shown>> for Duration<Hidden> {
#[inline]
fn from(f: Duration<Shown>) -> Self {
f.duration.hide();
f.separator.hide();
@@ -296,13 +282,11 @@ pub enum DurationMachine {
}
impl DurationMachine {
#[inline]
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
m.determine_state(seconds)
}
#[inline]
pub fn determine_state(self, seconds: Option<i32>) -> Self {
match (self, seconds) {
(d @ DurationMachine::Hidden(_), None) => d,
@@ -337,7 +321,6 @@ pub struct Size<S> {
}
impl<S> Size<S> {
#[inline]
fn set_size(self, s: &str) -> Size<Shown> {
self.size.set_text(s);
self.size.show();
@@ -349,7 +332,6 @@ impl<S> Size<S> {
}
}
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
@@ -364,7 +346,6 @@ impl<S> Size<S> {
}
}
#[inline]
fn into_hidden(self) -> Size<Hidden> {
self.size.hide();
self.separator.hide();
@@ -378,7 +359,6 @@ impl<S> Size<S> {
}
impl Size<UnInitialized> {
#[inline]
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
size.hide();
separator.hide();
@@ -412,7 +392,6 @@ pub struct DownloadPlay<S> {
}
impl<S> DownloadPlay<S> {
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
@@ -427,7 +406,6 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn into_fetchable(self) -> DownloadPlay<Download> {
self.play.hide();
self.download.show();
@@ -439,7 +417,6 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn into_hidden(self) -> DownloadPlay<Hidden> {
self.play.hide();
self.download.hide();
@@ -451,7 +428,6 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@@ -459,14 +435,12 @@ impl<S> DownloadPlay<S> {
self.download.connect_clicked(f)
}
#[inline]
fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.play.connect_clicked(f)
}
}
impl DownloadPlay<UnInitialized> {
#[inline]
fn new(play: gtk::Button, download: gtk::Button) -> Self {
play.hide();
download.hide();
@@ -489,7 +463,6 @@ pub struct Progress<S> {
}
impl<S> Progress<S> {
#[inline]
fn into_shown(self) -> Progress<Shown> {
self.bar.show();
self.cancel.show();
@@ -505,7 +478,6 @@ impl<S> Progress<S> {
}
}
#[inline]
fn into_hidden(self) -> Progress<Hidden> {
self.bar.hide();
self.cancel.hide();
@@ -522,7 +494,6 @@ impl<S> Progress<S> {
}
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn update_progress(&mut self, local_size: &str, fraction: f64) {
@@ -530,14 +501,17 @@ impl<S> Progress<S> {
self.bar.set_fraction(fraction);
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.cancel.connect_clicked(f)
fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
self.cancel.connect_clicked(move |cancel| {
if let Ok(mut m) = prog.lock() {
m.cancel();
cancel.set_sensitive(false);
}
})
}
}
impl Progress<UnInitialized> {
#[inline]
fn new(
bar: gtk::ProgressBar,
cancel: gtk::Button,
@@ -569,9 +543,73 @@ pub struct Media<X, Y, Z> {
type New<Y> = Media<Download, Y, Hidden>;
type Playable<Y> = Media<Play, Y, Hidden>;
type InProgress = Media<Hidden, Shown, Shown>;
type MediaUnInitialized = Media<UnInitialized, UnInitialized, UnInitialized>;
impl From<New<Shown>> for InProgress {
fn from(f: New<Shown>) -> Self {
f.into_progress()
}
}
impl From<New<Hidden>> for InProgress {
fn from(f: New<Hidden>) -> Self {
f.into_progress()
}
}
impl From<Playable<Shown>> for InProgress {
fn from(f: Playable<Shown>) -> Self {
f.into_progress()
}
}
impl From<Playable<Hidden>> for InProgress {
fn from(f: Playable<Hidden>) -> Self {
f.into_progress()
}
}
impl<Y: Visibility> From<Playable<Y>> for New<Y> {
fn from(f: Playable<Y>) -> Self {
Media {
dl: f.dl.into_fetchable(),
size: f.size,
progress: f.progress,
}
}
}
impl<Y: Visibility> From<New<Y>> for Playable<Y> {
fn from(f: New<Y>) -> Self {
Media {
dl: f.dl.into_playable(),
size: f.size,
progress: f.progress,
}
}
}
impl From<MediaUnInitialized> for New<Hidden> {
fn from(f: MediaUnInitialized) -> Self {
Media {
dl: f.dl.into_fetchable(),
size: f.size.into_hidden(),
progress: f.progress.into_hidden(),
}
}
}
impl From<MediaUnInitialized> for Playable<Hidden> {
fn from(f: MediaUnInitialized) -> Self {
Media {
dl: f.dl.into_playable(),
size: f.size.into_hidden(),
progress: f.progress.into_hidden(),
}
}
}
impl<X, Y, Z> Media<X, Y, Z> {
#[inline]
fn set_size(self, s: &str) -> Media<X, Shown, Z> {
Media {
dl: self.dl,
@@ -580,7 +618,6 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn hide_size(self) -> Media<X, Hidden, Z> {
Media {
dl: self.dl,
@@ -589,7 +626,6 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_new(self, size: &str) -> New<Shown> {
Media {
dl: self.dl.into_fetchable(),
@@ -598,16 +634,6 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_new_without(self) -> New<Hidden> {
Media {
dl: self.dl.into_fetchable(),
size: self.size.into_hidden(),
progress: self.progress.into_hidden(),
}
}
#[inline]
fn into_playable(self, size: &str) -> Playable<Shown> {
Media {
dl: self.dl.into_playable(),
@@ -615,19 +641,9 @@ impl<X, Y, Z> Media<X, Y, Z> {
progress: self.progress.into_hidden(),
}
}
#[inline]
fn into_playable_without(self) -> Playable<Hidden> {
Media {
dl: self.dl.into_playable(),
size: self.size.into_hidden(),
progress: self.progress.into_hidden(),
}
}
}
impl<X, Z> Media<X, Shown, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
@@ -638,7 +654,6 @@ impl<X, Z> Media<X, Shown, Z> {
}
impl<X, Z> Media<X, Hidden, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
@@ -649,7 +664,6 @@ impl<X, Z> Media<X, Hidden, Z> {
}
impl<X, Z> Media<X, UnInitialized, Z> {
#[inline]
fn into_progress(self, size: Option<String>) -> InProgress {
if let Some(s) = size {
Media {
@@ -668,7 +682,6 @@ impl<X, Z> Media<X, UnInitialized, Z> {
}
impl InProgress {
#[inline]
#[allow(unused_must_use)]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
@@ -686,7 +699,6 @@ pub enum ButtonsState {
}
impl ButtonsState {
#[inline]
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
@@ -707,33 +719,32 @@ impl ButtonsState {
// From whatever to NewWithoutSize
(New(m), None, false) => NewWithoutSize(m.hide_size()),
(Playable(m), None, false) => NewWithoutSize(m.into_new_without()),
(Playable(m), None, false) => NewWithoutSize(Media::from(m).hide_size()),
(b @ NewWithoutSize(_), None, false) => b,
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into_new_without()),
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into()),
// From whatever to PlayableWithoutSize
(New(m), None, true) => PlayableWithoutSize(m.into_playable_without()),
(New(m), None, true) => PlayableWithoutSize(Media::from(m).hide_size()),
(Playable(m), None, true) => PlayableWithoutSize(m.hide_size()),
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into_playable_without()),
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into()),
(b @ PlayableWithoutSize(_), None, true) => b,
// _ => unimplemented!()
}
}
#[inline]
fn into_progress(self) -> InProgress {
use self::ButtonsState::*;
match self {
New(m) => m.into_progress(),
Playable(m) => m.into_progress(),
NewWithoutSize(m) => m.into_progress(),
PlayableWithoutSize(m) => m.into_progress(),
New(m) => m.into(),
Playable(m) => m.into(),
NewWithoutSize(m) => m.into(),
PlayableWithoutSize(m) => m.into(),
}
}
#[inline]
fn set_size(self, size: Option<String>) -> Self {
use self::ButtonsState::*;
@@ -749,7 +760,6 @@ impl ButtonsState {
}
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@@ -764,7 +774,6 @@ impl ButtonsState {
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@@ -779,15 +788,14 @@ impl ButtonsState {
}
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.progress.cancel_connect_clicked(f),
NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
Playable(ref val) => val.progress.cancel_connect_clicked(f),
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
New(ref val) => val.progress.cancel_connect_clicked(prog),
NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
Playable(ref val) => val.progress.cancel_connect_clicked(prog),
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
}
}
}
@@ -801,7 +809,6 @@ pub enum MediaMachine {
impl MediaMachine {
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
#[inline]
pub fn new(
play: gtk::Button,
download: gtk::Button,
@@ -819,7 +826,6 @@ impl MediaMachine {
MediaMachine::UnInitialized(Media { dl, progress, size })
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@@ -833,7 +839,6 @@ impl MediaMachine {
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@@ -847,21 +852,16 @@ impl MediaMachine {
}
}
#[inline]
pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
pub fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.progress.cancel_connect_clicked(f),
Initialized(ref val) => val.cancel_connect_clicked(f),
InProgress(ref val) => val.progress.cancel_connect_clicked(f),
UnInitialized(ref val) => val.progress.cancel_connect_clicked(prog),
Initialized(ref val) => val.cancel_connect_clicked(prog),
InProgress(ref val) => val.progress.cancel_connect_clicked(prog),
}
}
#[inline]
pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
use self::MediaMachine::*;
@@ -871,36 +871,18 @@ impl MediaMachine {
// Into New
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(UnInitialized(m), None, false, false) => {
Initialized(NewWithoutSize(m.into_new_without()))
}
(UnInitialized(m), None, false, false) => Initialized(NewWithoutSize(m.into())),
// Into Playable
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(UnInitialized(m), None, true, false) => {
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(UnInitialized(m), None, true, false) => Initialized(PlayableWithoutSize(m.into())),
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()),
// Into New
(InProgress(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(InProgress(m), None, false, false) => {
Initialized(NewWithoutSize(m.into_new_without()))
}
// Into Playable
(InProgress(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(InProgress(m), None, true, false) => {
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(i @ InProgress(_), _, _, _) => i,
}
}
#[inline]
pub fn set_size(self, bytes: Option<i32>) -> Self {
use self::MediaMachine::*;
let size = size_helper(bytes);
@@ -913,7 +895,6 @@ impl MediaMachine {
}
}
#[inline]
pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
use self::MediaMachine::*;
+2 -8
View File
@@ -1,13 +1,7 @@
mod empty;
mod show;
mod episode;
mod episode_states;
mod home_view;
mod show;
mod shows_view;
pub use self::empty::EmptyView;
pub use self::episode::EpisodeWidget;
pub use self::home_view::HomeView;
pub use self::show::ShowWidget;
pub use self::show::{mark_all_notif, remove_show_notif};
pub use self::shows_view::ShowsView;
pub use self::show::mark_all_watched;
+53 -223
View File
@@ -1,30 +1,19 @@
use glib;
use failure::Error;
// use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use html2pango::markup_from_raw;
use open;
use rayon;
use send_cell::SendCell;
use hammond_data::dbqueries;
use hammond_data::utils::delete_show;
use hammond_data::Podcast;
use hammond_data::dbqueries;
use app::Action;
use appnotif::InAppNotification;
use utils::{self, lazy_load};
use widgets::EpisodeWidget;
use utils::set_image_from_path;
use widgets::episode::episodes_listbox;
use std::rc::Rc;
use std::sync::mpsc::{SendError, Sender};
use std::sync::{Arc, Mutex};
lazy_static! {
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
Mutex::new(None);
}
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct ShowWidget {
@@ -35,17 +24,15 @@ pub struct ShowWidget {
link: gtk::Button,
settings: gtk::MenuButton,
unsub: gtk::Button,
episodes: gtk::ListBox,
podcast_id: Option<i32>,
episodes: gtk::Frame,
}
impl Default for ShowWidget {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let episodes = builder.get_object("episodes").unwrap();
let episodes: gtk::Frame = builder.get_object("episodes").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
let description: gtk::Label = builder.get_object("description").unwrap();
@@ -62,48 +49,45 @@ impl Default for ShowWidget {
link,
settings,
episodes,
podcast_id: None,
}
}
}
impl ShowWidget {
#[inline]
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> {
let mut pdw = ShowWidget::default();
pdw.init(&pd, &sender);
let pdw = Rc::new(pdw);
populate_listbox(&pdw, pd, sender)
.map_err(|err| error!("Failed to populate the listbox: {}", err))
.ok();
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> ShowWidget {
let pdw = ShowWidget::default();
pdw.init(pd, sender);
pdw
}
#[inline]
pub fn init(&mut self, pd: &Arc<Podcast>, sender: &Sender<Action>) {
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
// Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
WidgetExt::set_name(&self.container, &pd.id().to_string());
self.unsub
.connect_clicked(clone!(pd, sender => move |bttn| {
on_unsub_button_clicked(pd.clone(), bttn, &sender);
if let Err(err) = on_unsub_button_clicked(pd.clone(), bttn, sender.clone()) {
error!("Error: {}", err);
}
}));
self.setup_listbox(pd.clone(), sender.clone());
self.set_description(pd.description());
self.podcast_id = Some(pd.id());
self.set_cover(&pd)
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
if let Err(err) = self.set_cover(pd.clone()) {
error!("Failed to set a cover: {}", err)
}
let link = pd.link().to_owned();
self.link.set_tooltip_text(Some(link.as_str()));
self.link.connect_clicked(move |_| {
info!("Opening link: {}", &link);
open::that(&link)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed open link: {}", &link))
.ok();
if let Err(err) = open::that(&link) {
error!("Failed to open link: {}", &link);
error!("Error: {}", err);
}
});
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
@@ -114,130 +98,42 @@ impl ShowWidget {
on_played_button_clicked(
pd.clone(),
&episodes,
&sender
sender.clone()
)
}));
self.settings.set_popover(&show_menu);
}
#[inline]
/// Populate the listbox with the shows episodes.
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> {
utils::set_image_from_path(&self.cover, pd.id(), 256)
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
}
#[inline]
/// Set the descripton text.
fn set_description(&self, text: &str) {
self.description.set_markup(&markup_from_raw(text));
}
#[inline]
/// Save the scrollabar vajustment to the cache.
pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> {
if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() {
let adj = self.scrolled_window
.get_vadjustment()
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
*guard = Some((oldid, SendCell::new(adj)));
debug!("Widget Alignment was saved with ID: {}.", oldid);
}
Ok(())
}
#[inline]
/// Set scrolled window vertical adjustment.
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
let guard = SHOW_WIDGET_VALIGNMENT
.lock()
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
if let Some((oldid, ref sendcell)) = *guard {
// Only copy the old scrollbar if both widget's represent the same podcast.
debug!("PID: {}", pd.id());
debug!("OLDID: {}", oldid);
if pd.id() != oldid {
debug!("Early return");
return Ok(());
};
// Copy the vertical scrollbar adjustment from the old view into the new one.
sendcell
.try_get()
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
}
Ok(())
}
pub fn podcast_id(&self) -> Option<i32> {
self.podcast_id
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
#[inline]
/// Populate the listbox with the shows episodes.
fn populate_listbox(
show: &Rc<ShowWidget>,
fn on_unsub_button_clicked(
pd: Arc<Podcast>,
unsub_button: &gtk::Button,
sender: Sender<Action>,
) -> Result<(), Error> {
use crossbeam_channel::bounded;
use crossbeam_channel::TryRecvError::*;
let count = dbqueries::get_pd_episodes_count(&pd)?;
let (sender_, receiver) = bounded(1);
rayon::spawn(clone!(pd => move || {
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
// The receiver can be dropped if there's an early return
// like on show without episodes for example.
sender_.send(episodes).ok();
}));
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();
show.episodes.add(&container);
return Ok(());
}
let show_ = show.clone();
gtk::idle_add(move || {
let episodes = match receiver.try_recv() {
Ok(e) => e,
Err(Empty) => return glib::Continue(true),
Err(Disconnected) => return glib::Continue(false),
};
let list = show_.episodes.clone();
let constructor = clone!(sender => move |ep| {
EpisodeWidget::new(ep, &sender).container
});
let callback = clone!(pd, show_ => move || {
show_.set_vadjustment(&pd)
.map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err))
.ok();
});
lazy_load(episodes, list.clone(), constructor, callback);
glib::Continue(false)
});
Ok(())
}
#[inline]
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender: &Sender<Action>) {
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.set_sensitive(false);
let wrap = || -> Result<(), SendError<_>> {
unsub_button.hide();
sender.send(Action::RemoveShow(pd))?;
sender.send(Action::HeaderBarNormal)?;
@@ -245,103 +141,37 @@ fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender:
// Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
wrap().map_err(|err| error!("Action Sender: {}", err)).ok();
unsub_button.set_sensitive(true);
Ok(())
}
#[inline]
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::Frame, sender: Sender<Action>) {
if dim_titles(episodes).is_none() {
error!("Something went horribly wrong when dimming the titles.");
warn!("RUN WHILE YOU STILL CAN!");
}
sender
.send(Action::MarkAllPlayerNotification(pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
sender.send(Action::MarkAllPlayerNotification(pd)).unwrap();
}
#[inline]
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?;
// Not all widgets migth have been loaded when the mark_all is hit
// So we will need to refresh again after it's done.
sender.send(Action::RefreshWidgetIfSame(pd.id()))?;
sender.send(Action::RefreshEpisodesView).map_err(From::from)
}
#[inline]
pub fn mark_all_notif(pd: Arc<Podcast>, sender: &Sender<Action>) -> InAppNotification {
let id = pd.id();
let callback = clone!(sender => move || {
mark_all_watched(&pd, &sender)
.map_err(|err| error!("Notif Callback Error: {}", err))
.ok();
glib::Continue(false)
});
let undo_callback = clone!(sender => move || {
sender.send(Action::RefreshWidgetIfSame(id))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
});
let text = "Marked all episodes as listened";
InAppNotification::new(text, callback, undo_callback)
}
#[inline]
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
let text = format!("Unsubscribed from {}", pd.title());
utils::ignore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not insert {} to the ignore list.", pd.title()))
.ok();
let callback = clone!(pd, sender => move || {
utils::uningore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not remove {} from the ignore list.", pd.title()))
.ok();
// Spawn a thread so it won't block the ui.
rayon::spawn(clone!(pd, sender => move || {
delete_show(&pd)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete {}", pd.title()))
.ok();
sender.send(Action::RefreshEpisodesView).ok();
}));
glib::Continue(false)
});
let undo_wrap = move || -> Result<(), Error> {
utils::uningore_show(pd.id())?;
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshWidgetIfVis)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
let undo_callback = move || {
undo_wrap().map_err(|err| error!("{}", err)).ok();
};
InAppNotification::new(&text, callback, undo_callback)
}
#[inline]
// Ideally if we had a custom widget this would have been as simple as:
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
// But now I can't think of a better way to do it than hardcoding the title
// position relative to the EpisodeWidget container gtk::Box.
fn dim_titles(episodes: &gtk::ListBox) -> Option<()> {
let children = episodes.get_children();
fn dim_titles(episodes: &gtk::Frame) -> Option<()> {
let listbox = episodes
.get_children()
.remove(0)
.downcast::<gtk::ListBox>()
.ok()?;
let children = listbox.get_children();
for row in children {
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
+1 -1
View File
@@ -16,7 +16,7 @@
</p>
</description>
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> -->
<bug-database rdf:resource="https://gitlab.gnome.org/World/hammond/issues" />
<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>
+1 -1
View File
@@ -3,7 +3,7 @@
project(
'hammond', 'rust',
version: '0.3.2',
version: '0.3.1',
license: 'GPLv3',
)
+2 -4
View File
@@ -28,9 +28,7 @@
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/Hammond/cargo",
"RUST_BACKTRACE" : "1",
"RUSTFLAGS" : "--cfg rayon_unstable"
"CARGO_HOME" : "/run/build/Hammond/cargo"
}
},
"modules" : [
@@ -40,7 +38,7 @@
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/World/hammond.git",
"url" : "https://gitlab.gnome.org/alatiera/Hammond.git",
"branch" : "master"
}
]
+2
View File
@@ -11,3 +11,5 @@ condense_wildcard_suffixes = false
format_strings = true
normalize_comments = true
reorder_imports = true
reorder_imported_names = true
reorder_imports_in_group = true
-1
View File
@@ -1,6 +1,5 @@
#!/bin/sh
export CARGO_HOME=$1/target/cargo-home
export RUSTFLAGS="--cfg rayon_unstable"
cargo build --release -p hammond-gtk && cp $1/target/release/hammond-gtk $2
-1
View File
@@ -13,7 +13,6 @@ cp -rf hammond-data $DIST
cp -rf hammond-gtk $DIST
cp -rf hammond-downloader $DIST
cp Cargo.toml $DIST
cp Cargo.lock $DIST
cp configure $DIST
cp meson.build $DIST
cp Hammond.doap $DIST