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 .cargo_test_template: &cargo_test
stage: test stage: test
variables: # variables:
RUSTFLAGS: "--cfg rayon_unstable" # RUSTFLAGS: "-C link-dead-code"
RUST_BACKTRACE: "FULL" # RUST_BACKTRACE: "FULL"
before_script: before_script:
- apt-get update -yqq - apt-get update -yqq
@@ -53,7 +53,7 @@ flatpak:
stage: test stage: test
script: script:
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json - 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 # 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 - 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: clippy:
image: "rustlang/rust:nightly" image: "rustlang/rust:nightly"
stage: lint 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: script:
- rustc --version && cargo --version - rustc --version && cargo --version
- cargo install clippy --force - cargo install clippy --force
# Force regeneration of gresources regardless of artifacts chage # Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../ - cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo clippy --all - 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] ## [Unreleased]
* Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
## [0.3.2] - 2018-05-07 [#7](https://gitlab.gnome.org/alatiera/Hammond/issues/7)
### Added: * Bold, italics links and some other `html` tags can now be rendered in the Show Description.
- Vies now have a new fancy scrolling animation when they are refereshed. [#25](https://gitlab.gnome.org/alatiera/Hammond/issues/25)
### 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
## [0.3.1] - 2018-03-28 ## [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: * Ability to mark all episodes of a Show as watched.
- EpisdeWidget has been reimplemented as a compile time state machine. [#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47)
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18) * Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
- Content Views no longer scroll horizontally when shrunk bellow their minimum size. [#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49)
[#35](https://gitlab.gnome.org/World/hammond/issues/35) * EpisdeWidget has been reimplemented as a compile time state machine.
- Some requests now use the Tor Browser's user agent. (Rowan Lewis) [!18](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/18)
[#53](https://gitlab.gnome.org/World/hammond/issues/53) * Content Views no longer scroll horizontally when shrunk bellow their minimum size.
[#35](https://gitlab.gnome.org/alatiera/Hammond/issues/35)
### Fixed: * Double border aroun the main window was fixed. (Rowan Lewis)
- Double border aroun the main window was fixed. (Rowan Lewis) [#52](https://gitlab.gnome.org/alatiera/Hammond/issues/52)
[#52](https://gitlab.gnome.org/World/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 ## [0.3.0] - 2018-02-11
- Tobias Bernard Redesigned the whole Gtk+ client.
- Complete re-write of hammond-data and hammond-gtk modules. * Tobias Bernard Redesigned the whole Gtk+ client.
- Error handling for all crates was migrated from error-chain to Failure. * Complete re-write of hammond-data and hammond-gtk modules.
- Hammond-data now uses futures to parse feeds. * Error handling for all crates was migrated from error-chain to Failure.
- Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets. * 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 ## [0.2.0] - 2017-11-28
- Database Schema Breaking Changes.
- Added url sanitization. #4. * Database Schema Breaking Changes.
- Reworked and refactored of the hammond-data API. * Added url sanitization. #4.
- Added some more unit tests * Reworked and refactored of the hammond-data API.
- Documented hammond-data public API. * Added some more unit tests
* Documented hammond-data public API.
## [0.1.1] - 2017-11-13 ## [0.1.1] - 2017-11-13
- Added appdata.xml file
* Added appdata.xml file
## [0.1.0] - 2017-11-13 ## [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 ## Source repository
Hammond's main source repository is at gitlab.gnome.org. You can view 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. 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! # Issues, issues and more issues!
There are many ways you can contribute to Hammond, and all of them involve creating 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 To create an effective and high quality ticket, try to put the following information on your
ticket: ticket:
Generated
+107 -167
View File
@@ -69,7 +69,7 @@ dependencies = [
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.6" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "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" version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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]] [[package]]
name = "build_const" name = "build_const"
version = "0.2.1" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@@ -160,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.10" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@@ -170,7 +170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.2" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -214,33 +214,33 @@ name = "crc"
version = "1.7.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "criterion" name = "criterion"
version = "0.2.3" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion-stats 0.2.3 (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 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)", "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)", "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 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)", "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)", "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 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.39 (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.15 (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)", "simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "criterion-plot" name = "criterion-plot"
version = "0.2.3" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -250,7 +250,7 @@ dependencies = [
[[package]] [[package]]
name = "criterion-stats" name = "criterion-stats"
version = "0.2.3" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -373,32 +373,31 @@ dependencies = [
[[package]] [[package]]
name = "diesel" name = "diesel"
version = "1.2.2" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "1.2.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "diesel_migrations" name = "diesel_migrations"
version = "1.2.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)",
"migrations_macros 1.2.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]] [[package]]
@@ -429,7 +428,7 @@ name = "error-chain"
version = "0.11.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
@@ -437,7 +436,7 @@ name = "failure"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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" version = "0.1.0"
dependencies = [ dependencies = [
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.2.3 (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)", "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 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_migrations 1.2.0 (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 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)", "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)", "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.24 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.3 (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)", "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)", "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)", "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)", "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)", "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 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)", "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)", "rss 1.4.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)",
"tempdir 0.3.7 (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)", "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)", "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)", "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)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"hammond-data 0.1.0", "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)", "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)", "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)", "pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -722,7 +718,7 @@ dependencies = [
name = "hammond-gtk" name = "hammond-gtk"
version = "0.1.0" version = "0.1.0"
dependencies = [ 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)", "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 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)", "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)", "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)", "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)", "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)", "send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (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)", "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)", "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)", "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)", "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)", "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 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "html2pango" name = "html2pango"
version = "0.1.0" version = "0.1.0"
source = "git+https://gitlab.gnome.org/World/html2pango#6bf3fe5e71459dc283148efe96a9c9f8c66d0791" source = "git+https://gitlab.gnome.org/World/html2pango#63fb1ea644ef38bf7f268bbe8f40b19dd77d7995"
dependencies = [ dependencies = [
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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]] [[package]]
name = "hyper" name = "hyper"
version = "0.11.25" version = "0.11.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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-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-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)", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"futures 0.1.21 (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)",
"native-tls 0.1.5 (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-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-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)", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"pkg-config 0.3.9 (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]]
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)",
] ]
[[package]] [[package]]
@@ -979,11 +965,6 @@ name = "maplit"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.7.2" version = "0.7.2"
@@ -991,9 +972,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.39 (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.15 (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 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)", "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)", "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]] [[package]]
name = "migrations_internals" name = "migrations_internals"
version = "1.2.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "migrations_macros" name = "migrations_macros"
version = "1.2.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.28" version = "0.9.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "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]] [[package]]
@@ -1322,15 +1303,7 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "0.2.3" 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)",
]
[[package]]
name = "proc-macro2"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1356,20 +1329,12 @@ name = "quote"
version = "0.3.15" version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "quote" name = "quote"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
@@ -1423,15 +1388,6 @@ dependencies = [
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.37" version = "0.1.37"
@@ -1452,14 +1408,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.5.5" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "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)", "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)", "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)", "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 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.15 (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)", "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-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)", "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)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1510,14 +1466,14 @@ name = "rfc822_sanitizer"
version = "0.3.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "rss" name = "rss"
version = "1.5.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1584,43 +1540,43 @@ dependencies = [
[[package]] [[package]]
name = "send-cell" name = "send-cell"
version = "0.1.3" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.39" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.39" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "serde_derive_internals" name = "serde_derive_internals"
version = "0.23.1" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.15" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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]] [[package]]
@@ -1630,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@@ -1639,7 +1595,7 @@ name = "simplelog"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "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)", "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)", "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)", "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_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)", "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)", "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]] [[package]]
name = "syn" name = "syn"
version = "0.13.1" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@@ -1854,7 +1800,7 @@ dependencies = [
[[package]] [[package]]
name = "tokio-core" name = "tokio-core"
version = "0.1.17" version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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 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-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-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-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]] [[package]]
@@ -1900,7 +1846,7 @@ dependencies = [
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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-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-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 = [ dependencies = [
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@@ -2080,7 +2026,7 @@ dependencies = [
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.3" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[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 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 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 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 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 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 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 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 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 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 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-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 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 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 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 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 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 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 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 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4556caa5b5e69626a0b69c4892baa6ab518cf2e802384a5eeef15dd4416d4771"
"checksum criterion-plot 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f7f7c88a8d341dd9fd9e31a72ca2ca24428db79afb491852873b2c784e037e6" "checksum criterion-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "029914bacd6bb0a521429ff8df0c6c79be1d99f5c4bb85475caabb78f5e06da6"
"checksum criterion-stats 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dd48feb0253b2968ff3085e7f3fba6738c9ff859f420a2fb81a48986eb66da36" "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-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.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" "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 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 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 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 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925325c57038f2f14c0413bdf6a92ca72acff644959d0a1a9ebf8d19be7e9c01"
"checksum diesel_derives 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6471a2b637b414d3ee1504cf230409a550381c79204282f8fe06c527e4ae56be" "checksum diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28e2b2605ac6a3b9a586383f5f8b2b5f1108f07a421ade965b266289d2805e79"
"checksum diesel_migrations 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "992026fb46e684eaede3d9d05737de34bdcdcb7f97b389b3c0fa7999389d504e" "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 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 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" "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 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 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 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 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 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" "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 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 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 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.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 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 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 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 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 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 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 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.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 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_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd916de6df9ac7e811e7e1ac28e0abfebe5205f3b29a7bda9ec8a41ee980a4eb"
"checksum migrations_macros 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a616c2538e1f09bf402efae6feb73c2711bb137c794b797f3fb8b3df84275d4b" "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.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 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" "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 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 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 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 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 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" "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 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 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 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.1 (registry+https://github.com/rust-lang/crates.io-index)" = "388d7ea47318c5ccdeb9ba6312cee7d3f65dd2804be8580a170fce410d50b786"
"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118"
"checksum quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eda5fe9b71976e62bc81b781206aaa076401769b2143379d3eb2118388babac4" "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 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.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 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 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.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 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 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-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_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 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 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 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 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 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 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 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 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" "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 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 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 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 send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c620dd7e056b468b9d374a9f51cfa6bb4bf17a8ca4ee62e5efa0d99aaff2c41"
"checksum serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "53e6b6859395f46cf528414659ce43e70902b2277519707c3bd91797b3320330" "checksum serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "d3bcee660dcde8f52c3765dd9ca5ee36b4bf35470a738eb0bd5a8752b0389645"
"checksum serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "16e97f8dc5b2dabc0183e0cde24b1a53835e5bb3d2c9e0fdb077f895bba7f2a9" "checksum serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "f1711ab8b208541fa8de00425f6a577d90f27bb60724d2bb5fd911314af9668f"
"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794" "checksum serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89b340a48245bc03ddba31d0ff1709c118df90edc6adabaca4aac77aea181cce"
"checksum serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7bf1cbb1387028a13739cb018ee0d9b3db534f22ca3c84a5904f7eadfde14e75" "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 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 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" "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 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 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.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 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 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" "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 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 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 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-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-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" "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 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 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 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 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 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" "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] [profile.release]
debug = false debug = false
+21 -20
View File
@@ -1,6 +1,13 @@
# Hammond # 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) ![episdes_view](./screenshots/episodes_view.png)
![shows_view](./screenshots/shows_view.png) ![shows_view](./screenshots/shows_view.png)
@@ -15,13 +22,12 @@ Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
## Broken Feeds ## Broken Feeds
Found a feed that does not work in Hammond? 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 ## Getting in Touch
If you have any questions regarding the use or development of Hammond, 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 want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
[irc.gnome.org.][irc] or [matrix][matrix].
Note: Note:
@@ -47,25 +53,22 @@ flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
# Install the required rust-stable extension from flathub # Install the required rust-stable extension from flathub
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
``` flatpak build-bundle repo hammond org.gnome.Hammond
To install the resulting flatpak you can do:
```bash
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
flatpak install --user --bundle hammond.flatpak
``` ```
### Building from soure ### Building from soure
```sh ```sh
git clone https://gitlab.gnome.org/World/hammond.git git clone https://gitlab.gnome.org/alatiera/hammond.git
cd hammond/ cd hammond/
meson --prefix=/usr build ./configure --prefix=/usr/local
ninja -C build make && sudo make install
sudo ninja -C build install
``` ```
**Additional:**
You can run `sudo make uninstall` for removal
#### Dependencies #### Dependencies
* Rust stable 1.22 or later along with cargo. * 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]. 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. 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 ## Overview
@@ -122,7 +125,7 @@ $ tree -d
│   ├── resources # GResources folder │   ├── resources # GResources folder
│   │   └── gtk # Contains the glade.ui files. │   │   └── gtk # Contains the glade.ui files.
│   └── src │   └── 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. │   └── 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. 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 [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] [dependencies]
ammonia = "1.1.0" ammonia = "1.1.0"
chrono = "0.4.2" chrono = "0.4.1"
derive_builder = "0.5.1" derive_builder = "0.5.1"
itertools = "0.7.8"
lazy_static = "1.0.0" lazy_static = "1.0.0"
log = "0.4.1" log = "0.4.1"
rayon = "1.0.1" rayon = "1.0.1"
rayon-futures = "0.1.0"
rfc822_sanitizer = "0.3.3" rfc822_sanitizer = "0.3.3"
rss = "1.5.0" rss = "1.4.0"
url = "1.7.0" url = "1.7.0"
xdg = "2.1.0" xdg = "2.1.0"
futures = "0.1.21" futures = "0.1.21"
hyper = "0.11.25" hyper = "0.11.24"
tokio-core = "0.1.17" tokio-core = "0.1.16"
hyper-tls = "0.1.3" hyper-tls = "0.1.3"
native-tls = "0.1.5" native-tls = "0.1.5"
num_cpus = "1.8.0" num_cpus = "1.8.0"
failure = "0.1.1" failure = "0.1.1"
failure_derive = "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] [dependencies.diesel]
features = ["sqlite", "r2d2"] features = ["sqlite", "r2d2"]
version = "1.2.2" version = "1.1.1"
[dependencies.diesel_migrations] [dependencies.diesel_migrations]
features = ["sqlite"] features = ["sqlite"]
version = "1.2.0" version = "1.1.0"
[dev-dependencies] [dev-dependencies]
rand = "0.4.2" rand = "0.4.2"
tempdir = "0.3.7" tempdir = "0.3.7"
criterion = "0.2.3" criterion = "0.2.2"
pretty_assertions = "0.5.1" pretty_assertions = "0.5.1"
[[bench]] [[bench]]
+24 -5
View File
@@ -1,5 +1,3 @@
#![allow(unused)]
#[macro_use] #[macro_use]
extern crate criterion; extern crate criterion;
use criterion::Criterion; use criterion::Criterion;
@@ -18,10 +16,10 @@ extern crate rss;
// use futures::future::*; // use futures::future::*;
use tokio_core::reactor::Core; use tokio_core::reactor::Core;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
use hammond_data::FeedBuilder; use hammond_data::FeedBuilder;
use hammond_data::Source; use hammond_data::Source;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
// use hammond_data::errors::*; // use hammond_data::errors::*;
use std::io::BufReader; use std::io::BufReader;
@@ -58,6 +56,22 @@ static FEEDS: &[(&[u8], &str)] = &[
(STARS, STARS_URL), (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) { fn bench_index_large_feed(c: &mut Criterion) {
truncate_db().unwrap(); truncate_db().unwrap();
let url = "https://www.greaterthancode.com/feed/podcast"; let url = "https://www.greaterthancode.com/feed/podcast";
@@ -100,5 +114,10 @@ fn bench_index_small_feed(c: &mut Criterion) {
truncate_db().unwrap(); 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); criterion_main!(benches);
+22 -61
View File
@@ -5,7 +5,6 @@ use diesel::prelude::*;
use diesel; use diesel;
use diesel::dsl::exists; use diesel::dsl::exists;
use diesel::query_builder::AsQuery;
use diesel::select; use diesel::select;
use database::connection; use database::connection;
@@ -41,7 +40,7 @@ pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError
podcast podcast
.order(title.asc()) .order(title.asc())
.filter(id.ne_all(filter_ids)) .filter(id.ne_any(filter_ids))
.load::<Podcast>(&con) .load::<Podcast>(&con)
.map_err(From::from) .map_err(From::from)
} }
@@ -103,20 +102,6 @@ pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
.map_err(From::from) .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> { pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
@@ -133,17 +118,24 @@ pub fn get_episodes_widgets_filter_limit(
filter_ids: &[i32], filter_ids: &[i32],
limit: u32, limit: u32,
) -> Result<Vec<EpisodeWidgetQuery>, DataError> { ) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*; use schema::episode;
let db = connection(); let db = connection();
let con = db.get()?; let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
);
episode episode::table
.select(columns) .select((
.order(epoch.desc()) episode::rowid,
.filter(podcast_id.ne_all(filter_ids)) 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)) .limit(i64::from(limit))
.load::<EpisodeWidgetQuery>(&con) .load::<EpisodeWidgetQuery>(&con)
.map_err(From::from) .map_err(From::from)
@@ -193,17 +185,14 @@ pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
.map_err(From::from) .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::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
let con = db.get()?; let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
);
episode episode.select((rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id))
.select(columns) // .filter(podcast_id.eq(parent.id()))
.filter(podcast_id.eq(parent.id())) // .group_by(epoch)
.order(epoch.desc()) .order(epoch.desc())
.load::<EpisodeWidgetQuery>(&con) .load::<EpisodeWidgetQuery>(&con)
.map_err(From::from) .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) .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> { pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
@@ -418,7 +379,7 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
mod tests { mod tests {
use super::*; use super::*;
use database::*; use database::*;
use pipeline; use pipeline::*;
#[test] #[test]
fn test_update_none_to_played_now() { fn test_update_none_to_played_now() {
@@ -428,7 +389,7 @@ mod tests {
com/InterceptedWithJeremyScahill"; com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap(); let source = Source::from_url(url).unwrap();
let id = source.id(); 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 pd = get_podcast_from_source_id(id).unwrap();
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len(); let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
+8 -24
View File
@@ -8,26 +8,6 @@ use url;
use std::io; 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)] #[derive(Fail, Debug)]
pub enum DataError { pub enum DataError {
#[fail(display = "SQL Query failed: {}", _0)] #[fail(display = "SQL Query failed: {}", _0)]
@@ -51,12 +31,16 @@ pub enum DataError {
RssError(#[cause] rss::Error), RssError(#[cause] rss::Error),
#[fail(display = "Error: {}", _0)] #[fail(display = "Error: {}", _0)]
Bail(String), Bail(String),
#[fail(display = "{}", _0)] #[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
HttpStatusGeneral(HttpStatusError), HttpStatusError {
#[fail(display = "FIXME: This should be better")] url: String,
F301(Source), status_code: hyper::StatusCode,
context: String,
},
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)] #[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
ParseEpisodeError { reason: String, parent_id: i32 }, 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.")] #[fail(display = "Episode was not changed and thus skipped.")]
EpisodeNotChanged, EpisodeNotChanged,
} }
+95 -91
View File
@@ -1,15 +1,16 @@
#![cfg_attr(feature = "cargo-clippy", allow(unit_arg))]
//! Index Feeds. //! Index Feeds.
use futures::future::*; use futures::future::*;
use futures::prelude::*; use itertools::{Either, Itertools};
use futures::stream;
use rss; use rss;
use dbqueries; use dbqueries;
use errors::DataError; use errors::DataError;
use models::{Index, IndexState, Update}; 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` /// Wrapper struct that hold a `Source` id and the `rss::Channel`
/// that corresponds to the `Source.uri` field. /// that corresponds to the `Source.uri` field.
@@ -28,7 +29,7 @@ impl Feed {
pub fn index(self) -> Box<Future<Item = (), Error = DataError> + Send> { pub fn index(self) -> Box<Future<Item = (), Error = DataError> + Send> {
let fut = self.parse_podcast_async() let fut = self.parse_podcast_async()
.and_then(|pd| pd.to_podcast()) .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) Box::new(fut)
} }
@@ -41,94 +42,74 @@ impl Feed {
Box::new(ok(self.parse_podcast())) Box::new(ok(self.parse_podcast()))
} }
fn index_channel_items(self, pd: Podcast) -> Box<Future<Item = (), Error = DataError> + Send> { fn index_channel_items(
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items()); &self,
pd: &Podcast,
// Parse the episodes ) -> Box<Future<Item = (), Error = DataError> + Send> {
let episodes = stream.filter_map(move |item| { let fut = self.get_stuff(pd)
glue(&item, pd.id()) .and_then(|(insert, update)| {
.map_err(|err| error!("Failed to parse an episode: {}", err)) if !insert.is_empty() {
.ok() 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.");
// Filter errors, Index updatable episodes, return insertables. error!("{}", err);
let insertables = filter_episodes(episodes); insert.iter().for_each(|ep| {
// Batch index insertable episodes. if let Err(err) = ep.index() {
let idx = insertables.and_then(|eps| ok(batch_insert_episodes(&eps))); error!("Failed to index episode: {:?}.", ep.title());
error!("{}", err);
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;
}; };
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() Ok((insert, update))
.map_err(|err| error!("Error: {}.", err)) })
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title())) .map(|(_, update)| {
.ok(); 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)] #[cfg(test)]
@@ -136,10 +117,10 @@ mod tests {
use rss::Channel; use rss::Channel;
use tokio_core::reactor::Core; use tokio_core::reactor::Core;
use Source;
use database::truncate_db; use database::truncate_db;
use dbqueries; use dbqueries;
use utils::get_feed; use utils::get_feed;
use Source;
use std::fs; use std::fs;
use std::io::BufReader; use std::io::BufReader;
@@ -221,8 +202,31 @@ mod tests {
let feed = get_feed(path, 42); let feed = get_feed(path, 42);
let pd = feed.parse_podcast().to_podcast().unwrap(); 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_podcasts().unwrap().len(), 1);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43); 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"] #![recursion_limit = "1024"]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))] #![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 = "cargo-clippy", allow(blacklisted_name))]
#![cfg_attr( #![cfg_attr(feature = "clippy",
feature = "clippy", warn(option_unwrap_used, result_unwrap_used, print_stdout,
warn( wrong_pub_self_convention, mut_mut, non_ascii_literal, similar_names,
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut, unicode_not_nfc, enum_glob_use, if_not_else, items_after_statements,
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else, used_underscore_binding))]
items_after_statements, used_underscore_binding
)
)]
#![allow(unknown_lints)] #![allow(unknown_lints)]
#![warn( #![deny(bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership, non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals,
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements, path_statements, patterns_in_fns_without_body, plugin_as_library, private_in_public,
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns, private_no_mangle_fns, private_no_mangle_statics, safe_extern_statics,
private_no_mangle_statics, safe_extern_statics, unconditional_recursion, unconditional_recursion, unions_with_drop_fields, unused_allocation, unused_comparisons,
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true unused_parens, while_true)]
)] #![deny(missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts)]
#![warn( #![deny(unused_extern_crates, unused)]
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
unused_extern_crates, unused // #![feature(conservative_impl_trait)]
)]
#![deny(warnings)]
//! FIXME: Docs //! FIXME: Docs
@@ -49,35 +44,31 @@ extern crate chrono;
extern crate futures; extern crate futures;
extern crate hyper; extern crate hyper;
extern crate hyper_tls; extern crate hyper_tls;
extern crate itertools;
extern crate native_tls; extern crate native_tls;
extern crate num_cpus; extern crate num_cpus;
extern crate rayon; extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer; extern crate rfc822_sanitizer;
extern crate rss; extern crate rss;
extern crate tokio_core; extern crate tokio_core;
extern crate url; extern crate url;
extern crate xdg; extern crate xdg;
pub mod database;
#[allow(missing_docs)] #[allow(missing_docs)]
pub mod dbqueries; pub mod dbqueries;
#[allow(missing_docs)] #[allow(missing_docs)]
pub mod errors; pub mod errors;
mod feed;
pub(crate) mod models;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils; 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 feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source}; pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
pub use models::Save;
// 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";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths. /// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
+7 -13
View File
@@ -1,7 +1,7 @@
use chrono::prelude::*; use chrono::prelude::*;
use diesel; use diesel;
use diesel::prelude::*;
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use diesel::prelude::*;
use database::connection; use database::connection;
use errors::DataError; use errors::DataError;
@@ -31,12 +31,10 @@ pub struct Episode {
podcast_id: i32, podcast_id: i32,
} }
impl Save<Episode> for Episode { impl Save<Episode, DataError> for Episode {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the /// Helper method to easily save/"sync" current state of self to the
/// Database. /// Database.
fn save(&self) -> Result<Episode, Self::Error> { fn save(&self) -> Result<Episode, DataError> {
let db = connection(); let db = connection();
let tempdb = db.get()?; let tempdb = db.get()?;
@@ -226,12 +224,10 @@ impl From<Episode> for EpisodeWidgetQuery {
} }
} }
impl Save<usize> for EpisodeWidgetQuery { impl Save<usize, DataError> for EpisodeWidgetQuery {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the /// Helper method to easily save/"sync" current state of self to the
/// Database. /// Database.
fn save(&self) -> Result<usize, Self::Error> { fn save(&self) -> Result<usize, DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
@@ -367,12 +363,10 @@ pub struct EpisodeCleanerQuery {
played: Option<i32>, played: Option<i32>,
} }
impl Save<usize> for EpisodeCleanerQuery { impl Save<usize, DataError> for EpisodeCleanerQuery {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the /// Helper method to easily save/"sync" current state of self to the
/// Database. /// Database.
fn save(&self) -> Result<usize, Self::Error> { fn save(&self) -> Result<usize, DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
+8 -16
View File
@@ -30,30 +30,22 @@ pub enum IndexState<T> {
NotChanged, NotChanged,
} }
pub trait Insert<T> { pub trait Insert<T, E> {
type Error; fn insert(&self) -> Result<T, E>;
fn insert(&self) -> Result<T, Self::Error>;
} }
pub trait Update<T> { pub trait Update<T, E> {
type Error; fn update(&self, i32) -> Result<T, E>;
fn update(&self, i32) -> Result<T, Self::Error>;
} }
// This might need to change in the future // This might need to change in the future
pub trait Index<T>: Insert<T> + Update<T> { pub trait Index<T, E>: Insert<T, E> + Update<T, E> {
type Error; fn index(&self) -> Result<T, E>;
fn index(&self) -> Result<T, <Self as Index<T>>::Error>;
} }
/// FIXME: DOCS /// FIXME: DOCS
pub trait Save<T> { pub trait Save<T, E> {
/// The Error type to be returned.
type Error;
/// Helper method to easily save/"sync" current state of a diesel model to /// Helper method to easily save/"sync" current state of a diesel model to
/// the Database. /// 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 { impl Insert<(), DataError> for NewEpisode {
type Error = DataError;
fn insert(&self) -> Result<(), DataError> { fn insert(&self) -> Result<(), DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
@@ -60,9 +58,7 @@ impl Insert<()> for NewEpisode {
} }
} }
impl Update<()> for NewEpisode { impl Update<(), DataError> for NewEpisode {
type Error = DataError;
fn update(&self, episode_id: i32) -> Result<(), DataError> { fn update(&self, episode_id: i32) -> Result<(), DataError> {
use schema::episode::dsl::*; use schema::episode::dsl::*;
let db = connection(); let db = connection();
@@ -77,9 +73,7 @@ impl Update<()> for NewEpisode {
} }
} }
impl Index<()> for NewEpisode { impl Index<(), DataError> for NewEpisode {
type Error = DataError;
// Does not update the episode description if it's the only thing that has // Does not update the episode description if it's the only thing that has
// changed. // changed.
fn index(&self) -> Result<(), DataError> { fn index(&self) -> Result<(), DataError> {
@@ -203,9 +197,9 @@ impl NewEpisodeMinimal {
let guid = item.guid().map(|s| s.value().trim().to_owned()); let guid = item.guid().map(|s| s.value().trim().to_owned());
let uri = item.enclosure() 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. // 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 // If url is still None return an Error as this behaviour is
// compliant with the RSS Spec. // compliant with the RSS Spec.
@@ -240,14 +234,7 @@ impl NewEpisodeMinimal {
// TODO: TryInto is stabilizing in rustc v1.26! // TODO: TryInto is stabilizing in rustc v1.26!
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode { pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
let length = item.enclosure().and_then(|x| x.length().parse().ok()); let length = item.enclosure().and_then(|x| x.length().parse().ok());
let description = item.description().and_then(|s| { let description = item.description().map(|s| ammonia::clean(s));
let sanitized_html = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
.link_rel(None)
.clean(s.trim())
.to_string();
Some(sanitized_html)
});
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title(self.title) .title(self.title)
@@ -293,8 +280,8 @@ impl NewEpisodeMinimal {
mod tests { mod tests {
use database::truncate_db; use database::truncate_db;
use dbqueries; use dbqueries;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use models::*; use models::*;
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
use rss::Channel; use rss::Channel;
+10 -20
View File
@@ -4,8 +4,8 @@ use diesel::prelude::*;
use rss; use rss;
use errors::DataError; use errors::DataError;
use models::Podcast;
use models::{Index, Insert, Update}; use models::{Index, Insert, Update};
use models::Podcast;
use schema::podcast; use schema::podcast;
use database::connection; use database::connection;
@@ -26,10 +26,8 @@ pub(crate) struct NewPodcast {
source_id: i32, source_id: i32,
} }
impl Insert<()> for NewPodcast { impl Insert<(), DataError> for NewPodcast {
type Error = DataError; fn insert(&self) -> Result<(), DataError> {
fn insert(&self) -> Result<(), Self::Error> {
use schema::podcast::dsl::*; use schema::podcast::dsl::*;
let db = connection(); let db = connection();
let con = db.get()?; let con = db.get()?;
@@ -42,10 +40,8 @@ impl Insert<()> for NewPodcast {
} }
} }
impl Update<()> for NewPodcast { impl Update<(), DataError> for NewPodcast {
type Error = DataError; fn update(&self, podcast_id: i32) -> Result<(), DataError> {
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
use schema::podcast::dsl::*; use schema::podcast::dsl::*;
let db = connection(); let db = connection();
let con = db.get()?; let con = db.get()?;
@@ -61,9 +57,7 @@ impl Update<()> for NewPodcast {
// TODO: Maybe return an Enum<Action(Resut)> Instead. // TODO: Maybe return an Enum<Action(Resut)> Instead.
// It would make unti testing better too. // It would make unti testing better too.
impl Index<()> for NewPodcast { impl Index<(), DataError> for NewPodcast {
type Error = DataError;
fn index(&self) -> Result<(), DataError> { fn index(&self) -> Result<(), DataError> {
let exists = dbqueries::podcast_exists(self.source_id)?; let exists = dbqueries::podcast_exists(self.source_id)?;
@@ -94,20 +88,16 @@ impl NewPodcast {
/// Parses a `rss::Channel` into a `NewPodcast` Struct. /// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast { pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
let title = chan.title().trim(); let title = chan.title().trim();
let link = url_cleaner(chan.link().trim());
let description = ammonia::Builder::new() let description = ammonia::clean(chan.description().trim());
// Remove `rel` attributes from `<a>` tags let link = url_cleaner(chan.link());
.link_rel(None)
.clean(chan.description().trim())
.to_string();
// Try to get the itunes img first // Try to get the itunes img first
let itunes_img = chan.itunes_ext() 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()); .map(|s| s.to_owned());
// If itunes is None, try to get the channel.image from the rss spec // 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() NewPodcastBuilder::default()
.title(title) .title(title)
+2
View File
@@ -1,3 +1,5 @@
#![allow(unused_mut)]
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use url::Url; use url::Url;
+2 -4
View File
@@ -25,12 +25,10 @@ pub struct Podcast {
source_id: i32, source_id: i32,
} }
impl Save<Podcast> for Podcast { impl Save<Podcast, DataError> for Podcast {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the /// Helper method to easily save/"sync" current state of self to the
/// Database. /// Database.
fn save(&self) -> Result<Podcast, Self::Error> { fn save(&self) -> Result<Podcast, DataError> {
let db = connection(); let db = connection();
let tempdb = db.get()?; let tempdb = db.get()?;
+32 -40
View File
@@ -3,22 +3,20 @@ use diesel::SaveChangesDsl;
use rss::Channel; use rss::Channel;
use url::Url; use url::Url;
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::header::{ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified, use hyper::header::{ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified,
Location, UserAgent}; Location, UserAgent};
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper_tls::HttpsConnector; use hyper_tls::HttpsConnector;
// use futures::future::ok; // use futures::future::ok;
use futures::future::{loop_fn, Future, Loop};
use futures::prelude::*; use futures::prelude::*;
use database::connection; use database::connection;
use errors::*; use errors::DataError;
use feed::{Feed, FeedBuilder}; use feed::{Feed, FeedBuilder};
use models::{NewSource, Save}; use models::{NewSource, Save};
use schema::source; use schema::source;
use USER_AGENT;
use std::str::FromStr; use std::str::FromStr;
@@ -34,12 +32,10 @@ pub struct Source {
http_etag: Option<String>, http_etag: Option<String>,
} }
impl Save<Source> for Source { impl Save<Source, DataError> for Source {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the /// Helper method to easily save/"sync" current state of self to the
/// Database. /// Database.
fn save(&self) -> Result<Source, Self::Error> { fn save(&self) -> Result<Source, DataError> {
let db = connection(); let db = connection();
let con = db.get()?; let con = db.get()?;
@@ -106,7 +102,11 @@ impl Source {
} }
fn make_err(self, context: &str, code: StatusCode) -> DataError { 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 // TODO match on more stuff
@@ -119,7 +119,7 @@ impl Source {
// 408: Timeout // 408: Timeout
// 410: Feed deleted // 410: Feed deleted
// TODO: Rething this api, // 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)?; self.update_etag(&res)?;
let code = res.status(); let code = res.status();
@@ -128,7 +128,7 @@ impl Source {
StatusCode::MovedPermanently => { StatusCode::MovedPermanently => {
error!("Feed was moved permanently."); error!("Feed was moved permanently.");
self.handle_301(&res)?; 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::TemporaryRedirect => debug!("307: Temporary Redirect."),
StatusCode::PermanentRedirect => warn!("308: Permanent 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)), StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
_ => info!("HTTP StatusCode: {}", code), _ => info!("HTTP StatusCode: {}", code),
}; };
Ok(res) Ok((self, res))
} }
fn handle_301(&mut self, res: &Response) -> Result<(), DataError> { fn handle_301(&mut self, res: &Response) -> Result<(), DataError> {
@@ -151,6 +151,8 @@ impl Source {
self.last_modified = None; self.last_modified = None;
self.save()?; self.save()?;
info!("Feed url was updated succesfully."); 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(()) Ok(())
@@ -175,27 +177,12 @@ impl Source {
// Refactor into TryInto once it lands on stable. // Refactor into TryInto once it lands on stable.
pub fn into_feed( pub fn into_feed(
self, self,
client: Client<HttpsConnector<HttpConnector>>, client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool, ignore_etags: bool,
) -> Box<Future<Item = Feed, Error = DataError>> { ) -> Box<Future<Item = Feed, Error = DataError>> {
let id = self.id(); let id = self.id();
let response = loop_fn(self, move |source| { let feed = self.request_constructor(client, ignore_etags)
source .and_then(move |(_, res)| response_to_channel(res))
.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)
.and_then(move |chan| { .and_then(move |chan| {
FeedBuilder::default() FeedBuilder::default()
.channel(chan) .channel(chan)
@@ -213,23 +200,27 @@ impl Source {
self, self,
client: &Client<HttpsConnector<HttpConnector>>, client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool, ignore_etags: bool,
) -> Box<Future<Item = Response, Error = DataError>> { ) -> Box<Future<Item = (Self, Response), Error = DataError>> {
// FIXME: remove unwrap somehow // FIXME: remove unwrap somehow
let uri = Uri::from_str(self.uri()).unwrap(); let uri = Uri::from_str(self.uri()).unwrap();
let mut req = Request::new(Method::Get, uri); let mut req = Request::new(Method::Get, uri);
// Set the UserAgent cause ppl still seem to check it for some reason... // Set the user agent as a fix for issue #53
req.headers_mut().set(UserAgent::new(USER_AGENT)); // 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 !ignore_etags {
if let Some(etag) = self.http_etag() { if let Some(foo) = self.http_etag() {
let tag = vec![EntityTag::new(true, etag.to_owned())]; req.headers_mut().set(IfNoneMatch::Items(vec![
req.headers_mut().set(IfNoneMatch::Items(tag)); EntityTag::new(true, foo.to_owned()),
]));
} }
if let Some(lmod) = self.last_modified() { if let Some(foo) = self.last_modified() {
if let Ok(date) = lmod.parse::<HttpDate>() { if let Ok(x) = foo.parse::<HttpDate>() {
req.headers_mut().set(IfModifiedSince(date)); req.headers_mut().set(IfModifiedSince(x));
} }
} }
} }
@@ -237,6 +228,7 @@ impl Source {
let work = client let work = client
.request(req) .request(req)
.map_err(From::from) .map_err(From::from)
// TODO: tail recursion loop that would follow redirects directly
.and_then(move |res| self.match_status(res)); .and_then(move |res| self.match_status(res));
Box::new(work) Box::new(work)
} }
@@ -277,7 +269,7 @@ mod tests {
let source = Source::from_url(url).unwrap(); let source = Source::from_url(url).unwrap();
let id = source.id(); 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 feed = core.run(feed).unwrap();
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id); let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
+131 -64
View File
@@ -2,119 +2,186 @@
//! Docs. //! Docs.
use futures::future::*; use futures::future::*;
use futures::prelude::*; // use futures::prelude::*;
use futures::stream::*;
use hyper::client::HttpConnector;
use hyper::Client; use hyper::Client;
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector; use hyper_tls::HttpsConnector;
use tokio_core::reactor::Core; use tokio_core::reactor::Core;
use num_cpus; use num_cpus;
use rayon; use rss;
use rayon_futures::ScopeFutureExt;
use errors::DataError;
use Source; use Source;
use dbqueries;
use errors::DataError;
use models::{IndexState, NewEpisode, NewEpisodeMinimal};
// use std::sync::{Arc, Mutex}; // 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 /// The pipline to be run for indexing and updating a Podcast feed that originates from
/// `Source.uri`. /// `Source.uri`.
/// ///
/// Messy temp diagram: /// Messy temp diagram:
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` -> /// Source -> GET Request -> Update Etags -> Check Status -> Parse xml/Rss ->
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes. /// Convert `rss::Channel` into Feed -> Index Podcast -> Index Episodes.
pub fn pipeline<'a, S>( pub fn pipeline<S: IntoIterator<Item = Source>>(
sources: S, sources: S,
ignore_etags: bool, ignore_etags: bool,
client: &HttpsClient, tokio_core: &mut Core,
) -> Box<Future<Item = Vec<()>, Error = DataError> + 'a> client: Client<HttpsConnector<HttpConnector>>,
where ) -> Result<(), DataError> {
S: Stream<Item = Source, Error = DataError> + 'a, let list: Vec<_> = sources
{ .into_iter()
let pipeline = sources .map(move |s| s.into_feed(&client, ignore_etags))
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags))) .map(|fut| fut.and_then(|feed| feed.index()))
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index()))) .map(|fut| fut.map(|_| ()).map_err(|err| error!("Error: {}", err)))
// 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>(()))
.collect(); .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 /// Creates a tokio `reactor::Core`, and a `hyper::Client` and
/// runs the pipeline. /// runs the pipeline.
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError> pub fn run(sources: Vec<Source>, ignore_etags: bool) -> Result<(), DataError> {
where if sources.is_empty() {
S: IntoIterator<Item = Source>, return Ok(());
{ }
let mut core = Core::new()?; let mut core = Core::new()?;
let handle = core.handle(); let handle = core.handle();
let client = Client::configure() let client = Client::configure()
.connector(HttpsConnector::new(num_cpus::get(), &handle)?) .connector(HttpsConnector::new(num_cpus::get(), &handle)?)
.build(&handle); .build(&handle);
let stream = iter_ok::<_, DataError>(sources); pipeline(sources, ignore_etags, &mut core, client)
let p = pipeline(stream, ignore_etags, &client); }
core.run(p).map(|_| ())
/// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use database::truncate_db;
use dbqueries;
use Source; use Source;
use database::truncate_db;
// (path, url) tuples. // (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.\ "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill", 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/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.\ "https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
com/feed/podcast", com/feed/podcast",
]; ),
]
};
#[test] #[test]
/// Insert feeds and update/index them. /// Insert feeds and update/index them.
fn test_pipeline() { fn test_pipeline() {
truncate_db().unwrap(); truncate_db().unwrap();
let bad_url = "https://gitlab.gnome.org/World/hammond.atom"; URLS.iter().for_each(|&(_, url)| {
// 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| {
// Index the urls into the source table. // Index the urls into the source table.
Source::from_url(url).unwrap(); Source::from_url(url).unwrap();
}); });
let sources = dbqueries::get_sources().unwrap(); let sources = dbqueries::get_sources().unwrap();
run(sources, true).unwrap(); run(sources, true).unwrap();
@@ -123,7 +190,7 @@ mod tests {
run(sources, true).unwrap(); run(sources, true).unwrap();
// Assert the index rows equal the controlled results // 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_podcasts().unwrap().len(), 5);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354); assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
} }
+10 -9
View File
@@ -28,10 +28,10 @@ fn download_checker() -> Result<(), DataError> {
}) })
.for_each(|ep| { .for_each(|ep| {
ep.set_local_uri(None); ep.set_local_uri(None);
ep.save() if let Err(err) = ep.save() {
.map_err(|err| error!("{}", err)) error!("Error while trying to update episode: {:#?}", ep);
.map_err(|_| error!("Error while trying to update episode: {:#?}", ep)) error!("{}", err);
.ok(); };
}); });
Ok(()) Ok(())
@@ -48,11 +48,12 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
.for_each(|ep| { .for_each(|ep| {
let limit = ep.played().unwrap(); let limit = ep.played().unwrap();
if now_utc > limit { if now_utc > limit {
delete_local_content(ep) if let Err(err) = delete_local_content(ep) {
.map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri())) error!("Error while trying to delete file: {:?}", ep.local_uri());
.map_err(|err| error!("Error: {}", err)) error!("{}", err);
.map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri())) } else {
.ok(); info!("Episode {:?} was deleted succesfully.", ep.local_uri());
};
} }
}); });
Ok(()) Ok(())
+1 -1
View File
@@ -6,7 +6,7 @@ workspace = "../"
[dependencies] [dependencies]
error-chain = "0.11.0" error-chain = "0.11.0"
hyper = "0.11.25" hyper = "0.11.24"
log = "0.4.1" log = "0.4.1"
mime_guess = "1.8.4" mime_guess = "1.8.4"
reqwest = "0.8.5" reqwest = "0.8.5"
+2 -2
View File
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save}; use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
// use failure::Error; // use failure::Error;
use errors::DownloadError; use errors::DownloadError;
@@ -234,9 +234,9 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use hammond_data::Source;
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_data::pipeline; use hammond_data::pipeline;
use hammond_data::Source;
use std::fs; use std::fs;
+2 -3
View File
@@ -1,8 +1,7 @@
#![recursion_limit = "1024"] #![recursion_limit = "1024"]
#![warn(unused_extern_crates, unused)] #![deny(unused_extern_crates, unused)]
#![allow(unknown_lints)] #![allow(unknown_lints)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))] #![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
#![deny(warnings)]
extern crate failure; extern crate failure;
#[macro_use] #[macro_use]
+3 -3
View File
@@ -6,7 +6,7 @@ version = "0.1.0"
workspace = "../" workspace = "../"
[dependencies] [dependencies]
chrono = "0.4.2" chrono = "0.4.1"
crossbeam-channel = "0.1.2" crossbeam-channel = "0.1.2"
gdk = "0.8.0" gdk = "0.8.0"
gdk-pixbuf = "0.4.0" gdk-pixbuf = "0.4.0"
@@ -17,14 +17,14 @@ log = "0.4.1"
loggerv = "0.7.1" loggerv = "0.7.1"
open = "1.2.1" open = "1.2.1"
rayon = "1.0.1" rayon = "1.0.1"
send-cell = "0.1.3" send-cell = "0.1.2"
url = "1.7.0" url = "1.7.0"
failure = "0.1.1" failure = "0.1.1"
failure_derive = "0.1.1" failure_derive = "0.1.1"
take_mut = "0.2.2" take_mut = "0.2.2"
regex = "0.2.10" regex = "0.2.10"
reqwest = "0.8.5" reqwest = "0.8.5"
serde_json = "1.0.15" serde_json = "1.0.13"
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" } html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
[dependencies.gtk] [dependencies.gtk]
+1 -1
View File
@@ -56,11 +56,11 @@ Tobias Bernard
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Episode Title</property> <property name="label" translatable="yes">Episode Title</property>
<property name="ellipsize">end</property> <property name="ellipsize">end</property>
<property name="width_chars">55</property>
<property name="single_line_mode">True</property> <property name="single_line_mode">True</property>
<property name="track_visited_links">False</property> <property name="track_visited_links">False</property>
<property name="lines">1</property> <property name="lines">1</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="yalign">0</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
+9 -3
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 <!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018 Copyright (C) 2017 - 2018
@@ -32,14 +32,13 @@ Tobias Bernard
<!-- interface-authors Jordan Petridis\nTobias Bernard --> <!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container"> <object class="GtkBox" id="container">
<property name="name">container</property> <property name="name">container</property>
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolled_window"> <object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property> <property name="name">scrolled_window</property>
<property name="height_request">400</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property> <property name="hscrollbar_policy">never</property>
@@ -69,6 +68,8 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox" id="frame_parent"> <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="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">32</property> <property name="margin_left">32</property>
@@ -80,6 +81,7 @@ Tobias Bernard
<property name="spacing">24</property> <property name="spacing">24</property>
<child> <child>
<object class="GtkBox" id="today_box"> <object class="GtkBox" id="today_box">
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
@@ -137,6 +139,7 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox" id="yday_box"> <object class="GtkBox" id="yday_box">
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
@@ -193,6 +196,7 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox" id="week_box"> <object class="GtkBox" id="week_box">
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
@@ -249,6 +253,7 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox" id="month_box"> <object class="GtkBox" id="month_box">
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
@@ -305,6 +310,7 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox" id="rest_box"> <object class="GtkBox" id="rest_box">
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
+31 -36
View File
@@ -31,17 +31,16 @@ Tobias Bernard
<!-- interface-copyright 2017 - 2018 --> <!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard --> <!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container"> <object class="GtkBox" id="container">
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolled_window"> <object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property> <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="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property> <property name="hscrollbar_policy">never</property>
<child> <child>
<object class="GtkViewport"> <object class="GtkViewport">
@@ -71,26 +70,26 @@ Tobias Bernard
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="width_request">700</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">32</property> <property name="margin_left">32</property>
<property name="margin_right">32</property> <property name="margin_right">32</property>
<property name="margin_top">32</property> <property name="margin_top">32</property>
<property name="margin_bottom">32</property> <property name="margin_bottom">32</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">24</property> <property name="spacing">24</property>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="valign">center</property>
<property name="spacing">6</property> <property name="spacing">12</property>
<child> <child>
<object class="GtkImage" id="cover"> <object class="GtkImage" id="cover">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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> <property name="icon_name">image-x-generic-symbolic</property>
</object> </object>
<packing> <packing>
@@ -100,32 +99,23 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkBox">
<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">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="shadow_type">none</property> <property name="valign">end</property>
<child> <property name="orientation">vertical</property>
<property name="spacing">6</property>
<child type="center">
<object class="GtkLabel" id="description"> <object class="GtkLabel" id="description">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">center</property> <property name="halign">start</property>
<property name="valign">center</property> <property name="valign">end</property>
<property name="label">This is embarrasing! <property name="label">foo</property>
Sorry, we could not find a description for this Show.</property>
<property name="use_markup">True</property> <property name="use_markup">True</property>
<property name="justify">center</property>
<property name="wrap">True</property> <property name="wrap">True</property>
<property name="max_width_chars">70</property> <property name="wrap_mode">word-char</property>
</object> <property name="max_width_chars">90</property>
</child>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -137,6 +127,7 @@ Sorry, we could not find a description for this Show.</property>
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<child> <child>
<object class="GtkMenuButton" id="settings_button"> <object class="GtkMenuButton" id="settings_button">
@@ -198,8 +189,16 @@ Sorry, we could not find a description for this Show.</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">True</property>
<property name="position">2</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> </packing>
</child> </child>
</object> </object>
@@ -210,17 +209,13 @@ Sorry, we could not find a description for this Show.</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkFrame"> <object class="GtkFrame" id="episodes">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label_xalign">0</property> <property name="label_xalign">0</property>
<property name="shadow_type">in</property> <property name="shadow_type">in</property>
<child> <child>
<object class="GtkListBox" id="episodes"> <placeholder/>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
</object>
</child> </child>
<child type="label_item"> <child type="label_item">
<placeholder/> <placeholder/>
@@ -235,7 +230,7 @@ Sorry, we could not find a description for this Show.</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">False</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
@@ -262,7 +257,7 @@ Sorry, we could not find a description for this Show.</property>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
+11 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 <!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018 Copyright (C) 2017 - 2018
@@ -39,6 +39,13 @@ Tobias Bernard
<property name="valign">center</property> <property name="valign">center</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <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"> <object class="GtkImage" id="pd_cover">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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_name">image-x-generic-symbolic</property>
<property name="icon_size">0</property> <property name="icon_size">0</property>
</object> </object>
</child>
</object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
@@ -12,25 +12,25 @@
desktop written in Rust. desktop written in Rust.
</p> </p>
</description> </description>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url> <url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<screenshots> <screenshots>
<screenshot> <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> <caption>Page 1</caption>
</screenshot> </screenshot>
<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> <caption>Page 2</caption>
</screenshot> </screenshot>
<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> <caption>Page 3</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="0.3.2" date="2018-05-07"/> <release version="0.3.1" date="2018-03-28"/>
</releases> </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> <update_contact>jpetridis@gnome.org</update_contact>
<developer_name>Jordan Petridis and others</developer_name> <developer_name>Jordan Petridis and others</developer_name>
</component> </component>
+94 -50
View File
@@ -3,27 +3,35 @@
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt}; use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
use glib; use glib;
use gtk; use gtk;
use gtk::prelude::*;
use gtk::SettingsExt as GtkSettingsExt; 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 headerbar::Header;
use settings::{self, WindowGeometry}; use settings::WindowGeometry;
use stacks::{Content, PopulatedState}; use stacks::Content;
use utils; 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::Arc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Duration;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Action { pub enum Action {
UpdateSources(Option<Source>),
RefreshAllViews, RefreshAllViews,
RefreshEpisodesView, RefreshEpisodesView,
RefreshEpisodesViewBGR, RefreshEpisodesViewBGR,
RefreshShowsView, RefreshShowsView,
RefreshWidget,
RefreshWidgetIfVis,
ReplaceWidget(Arc<Podcast>), ReplaceWidget(Arc<Podcast>),
RefreshWidgetIfSame(i32), RefreshWidgetIfSame(i32),
ShowWidgetAnimated, ShowWidgetAnimated,
@@ -41,8 +49,8 @@ pub struct App {
app_instance: gtk::Application, app_instance: gtk::Application,
window: gtk::Window, window: gtk::Window,
overlay: gtk::Overlay, overlay: gtk::Overlay,
header: Rc<Header>, header: Arc<Header>,
content: Rc<Content>, content: Arc<Content>,
receiver: Receiver<Action>, receiver: Receiver<Action>,
sender: Sender<Action>, sender: Sender<Action>,
settings: Settings, settings: Settings,
@@ -58,9 +66,6 @@ impl App {
glib::set_application_name("Hammond"); glib::set_application_name("Hammond");
glib::set_prgname(Some("Hammond")); glib::set_prgname(Some("Hammond"));
let cleanup_date = settings::get_cleanup_date(&settings);
utils::cleanup(cleanup_date);
// Create the main window // Create the main window
let window = gtk::Window::new(gtk::WindowType::Toplevel); let window = gtk::Window::new(gtk::WindowType::Toplevel);
@@ -79,10 +84,10 @@ impl App {
// Create a content instance // Create a content instance
let content = 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 // 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. // Add the content main stack to the overlay.
let overlay = gtk::Overlay::new(); let overlay = gtk::Overlay::new();
@@ -119,35 +124,34 @@ impl App {
fn setup_refresh_on_startup(&self) { fn setup_refresh_on_startup(&self) {
// Update the feeds right after the Application is initialized. // Update the feeds right after the Application is initialized.
if self.settings.get_boolean("refresh-on-startup") { if self.settings.get_boolean("refresh-on-startup") {
let cleanup_date = utils::get_cleanup_date(&self.settings);
let sender = self.sender.clone(); let sender = self.sender.clone();
info!("Refresh on startup."); info!("Refresh on startup.");
// The ui loads async, after initialization
// so we need to delay this a bit so it won't block utils::cleanup(cleanup_date);
// requests that will come from loading the gui on startup.
gtk::timeout_add(1500, move || { gtk::timeout_add_seconds(2, move || {
let s: Option<Vec<_>> = None; utils::refresh(None, sender.clone());
utils::refresh(s, sender.clone());
glib::Continue(false) glib::Continue(false)
}); });
} }
} }
fn setup_auto_refresh(&self) { 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(); let sender = self.sender.clone();
info!("Auto-refresh every {:?} seconds.", refresh_interval); info!("Auto-refresh every {:?} seconds.", refresh_interval);
gtk::timeout_add_seconds(refresh_interval, move || { gtk::timeout_add_seconds(refresh_interval, move || {
let s: Option<Vec<_>> = None; utils::refresh(None, sender.clone());
utils::refresh(s, sender.clone());
glib::Continue(true) glib::Continue(true)
}); });
} }
#[inline]
pub fn run(self) { pub fn run(self) {
WindowGeometry::from_settings(&self.settings).apply(&self.window); WindowGeometry::from_settings(&self.settings).apply(&self.window);
@@ -163,46 +167,86 @@ impl App {
let sender = self.sender.clone(); let sender = self.sender.clone();
let overlay = self.overlay.clone(); let overlay = self.overlay.clone();
let receiver = self.receiver; let receiver = self.receiver;
gtk::timeout_add(50, move || { gtk::idle_add(move || {
match receiver.try_recv() { 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::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(), 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::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(), Ok(Action::RefreshEpisodesView) => content.update_episode_view(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_home_if_background(), Ok(Action::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(),
Ok(Action::ReplaceWidget(pd)) => { Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows(); if let Err(err) = content.get_shows().replace_widget(pd) {
let mut pop = shows.borrow().populated(); error!("Something went wrong while trying to update the ShowWidget.");
pop.borrow_mut() error!("Error: {}", err);
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_| error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
} }
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::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(), Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(), Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(), Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => { 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); notif.show(&overlay);
} }
Ok(Action::RemoveShow(pd)) => { 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); notif.show(&overlay);
} }
Err(_) => (), Err(_) => (),
+13 -1
View File
@@ -2,8 +2,11 @@ use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use app::Action;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct InAppNotification { pub struct InAppNotification {
@@ -32,7 +35,12 @@ impl Default for InAppNotification {
} }
impl 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 where
F: FnMut() -> glib::Continue + 'static, F: FnMut() -> glib::Continue + 'static,
U: Fn() + 'static, U: Fn() + 'static,
@@ -59,6 +67,10 @@ impl InAppNotification {
// Hide the notification // Hide the notification
revealer.set_reveal_child(false); 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 // Hide the revealer when the close button is clicked
+21 -25
View File
@@ -1,4 +1,3 @@
use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
@@ -6,14 +5,14 @@ use failure::Error;
use failure::ResultExt; use failure::ResultExt;
use url::Url; use url::Url;
use hammond_data::dbqueries;
use hammond_data::Source; use hammond_data::Source;
use hammond_data::dbqueries;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use app::Action; use app::Action;
use stacks::Content; use stacks::Content;
use utils::{itunes_to_rss, refresh}; use utils::itunes_to_rss;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Header { pub struct Header {
@@ -61,13 +60,13 @@ impl Default for Header {
// TODO: Refactor components into smaller state machines // TODO: Refactor components into smaller state machines
impl Header { 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(); let h = Header::default();
h.init(content, window, &sender); h.init(content, window, sender);
h 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 builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap(); let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
@@ -77,15 +76,15 @@ impl Header {
self.switch.set_stack(&content.get_stack()); self.switch.set_stack(&content.get_stack());
new_url.connect_changed(clone!(add_button => move |url| { new_url.connect_changed(clone!(add_button => move |url| {
on_url_change(url, &result_label, &add_button) if let Err(err) = on_url_change(url, &result_label, &add_button) {
.map_err(|err| error!("Error: {}", err)) error!("Error: {}", err);
.ok(); }
})); }));
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| { add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
on_add_bttn_clicked(&new_url, sender.clone()) if let Err(err) = on_add_bttn_clicked(&new_url, sender.clone()) {
.map_err(|err| error!("Error: {}", err)) error!("Error: {}", err);
.ok(); }
add_popover.hide(); add_popover.hide();
})); }));
@@ -93,11 +92,9 @@ impl Header {
self.update_button self.update_button
.connect_clicked(clone!(sender => move |_| { .connect_clicked(clone!(sender => move |_| {
gtk::idle_add(clone!(sender => move || { sender
let s: Option<Vec<_>> = None; .send(Action::UpdateSources(None))
refresh(s, sender.clone()); .expect("Action channel blew up.");
glib::Continue(false)
}));
})); }));
self.about_button self.about_button
@@ -117,9 +114,9 @@ impl Header {
add_toggle.show(); add_toggle.show();
back.hide(); back.hide();
show_title.hide(); show_title.hide();
sender.send(Action::ShowShowsAnimated) if let Err(err) = sender.send(Action::ShowShowsAnimated) {
.map_err(|err| error!("Action Sender: {}", err)) error!("Action channel blew up: {}", err);
.ok(); }
}), }),
); );
} }
@@ -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.")?; let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text(""); entry.set_text("");
gtk::idle_add(move || { sender
refresh(Some(vec![source.clone()]), sender.clone()); .send(Action::UpdateSources(Some(source)))
glib::Continue(false) .context("App channel blew up.")?;
});
Ok(()) Ok(())
} }
@@ -243,7 +239,7 @@ fn about_dialog(window: &gtk::Window) {
dialog.set_modal(true); dialog.set_modal(true);
// TODO: make it show it fetches the commit hash from which it was built // TODO: make it show it fetches the commit hash from which it was built
// and the version number is kept in sync automaticly // 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"); dialog.set_program_name("Hammond");
// TODO: Need a wiki page first. // TODO: Need a wiki page first.
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts"); // dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
+16 -15
View File
@@ -1,10 +1,8 @@
#![cfg_attr( #![cfg_attr(feature = "cargo-clippy",
feature = "cargo-clippy", allow(clone_on_ref_ptr, needless_pass_by_value, useless_format, blacklisted_name,
allow(clone_on_ref_ptr, blacklisted_name, match_same_arms, option_map_unit_fn) match_same_arms))]
)]
#![allow(unknown_lints)] #![allow(unknown_lints)]
#![warn(unused_extern_crates, unused)] #![deny(unused_extern_crates, unused)]
#![deny(warnings)]
extern crate gdk; extern crate gdk;
extern crate gdk_pixbuf; extern crate gdk_pixbuf;
@@ -64,17 +62,20 @@ macro_rules! clone {
); );
} }
mod stacks; // They do not need to be public
mod widgets; // But it helps when looking at the generated docs.
pub mod views;
pub mod widgets;
pub mod stacks;
mod app; pub mod headerbar;
mod headerbar; pub mod app;
mod appnotif; pub mod settings;
mod manager; pub mod utils;
mod settings; pub mod manager;
mod static_resource; pub mod static_resource;
mod utils; pub mod appnotif;
use app::App; use app::App;
+23 -8
View File
@@ -5,8 +5,11 @@ use rayon;
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_downloader::downloader::{get_episode, DownloadProgress}; use hammond_downloader::downloader::{get_episode, DownloadProgress};
use app::Action;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::sync::mpsc::Sender;
// use std::sync::atomic::AtomicUsize; // use std::sync::atomic::AtomicUsize;
// use std::path::PathBuf; // use std::path::PathBuf;
@@ -75,7 +78,7 @@ lazy_static! {
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap(); 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. // Create a new `Progress` struct to keep track of dl progress.
let prog = Arc::new(Mutex::new(Progress::default())); let prog = Arc::new(Mutex::new(Progress::default()));
@@ -85,12 +88,14 @@ pub fn add(id: i32, directory: String) -> Result<(), Error> {
}; };
DLPOOL.spawn(move || { 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(); let id = episode.rowid();
get_episode(&mut episode, directory.as_str(), Some(prog)) if let Err(err) = get_episode(&mut episode.into(), directory.as_str(), Some(prog)) {
.map_err(|err| error!("Download Failed: {}", err)) error!("Error while trying to download an episode");
.ok(); error!("Error: {}", err);
}
if let Ok(mut m) = ACTIVE_DOWNLOADS.write() { if let Ok(mut m) = ACTIVE_DOWNLOADS.write() {
let foo = m.remove(&id); 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() { // if let Ok(m) = ACTIVE_DOWNLOADS.read() {
// debug!("ACTIVE DOWNLOADS: {:#?}", m); // 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 { mod tests {
use super::*; use super::*;
use hammond_data::{Episode, Source};
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_data::pipeline; use hammond_data::pipeline;
use hammond_data::utils::get_download_folder; use hammond_data::utils::get_download_folder;
use hammond_data::{Episode, Source};
use hammond_downloader::downloader::get_episode; use hammond_downloader::downloader::get_episode;
use std::{thread, time};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::{thread, time}; use std::sync::mpsc::channel;
#[test] #[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit // 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 // Get an episode
let episode: Episode = dbqueries::get_episode_from_pk(title, pd.id()).unwrap(); 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 download_fold = get_download_folder(&pd.title()).unwrap();
let fold2 = download_fold.clone(); 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); assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
// Give it soem time to download the file // Give it soem time to download the file
+1 -47
View File
@@ -1,11 +1,8 @@
use gio; use gio;
use gio::{Settings, SettingsExt}; use gio::SettingsExt;
use gtk; use gtk;
use gtk::GtkWindowExt; use gtk::GtkWindowExt;
use chrono::prelude::*;
use chrono::Duration;
pub struct WindowGeometry { pub struct WindowGeometry {
left: i32, left: i32,
top: 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] // #[test]
// fn test_apply_window_geometry() { // fn test_apply_window_geometry() {
// gtk::init().expect("Error initializing gtk."); // gtk::init().expect("Error initializing gtk.");
+46 -40
View File
@@ -4,85 +4,91 @@ use gtk::prelude::*;
use failure::Error; use failure::Error;
use app::Action; use app::Action;
use stacks::{HomeStack, ShowStack}; use stacks::EpisodeStack;
use stacks::ShowStack;
use std::cell::RefCell; use std::sync::Arc;
use std::rc::Rc;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Content { pub struct Content {
stack: gtk::Stack, stack: gtk::Stack,
shows: Rc<RefCell<ShowStack>>, shows: Arc<ShowStack>,
home: Rc<RefCell<HomeStack>>, episodes: Arc<EpisodeStack>,
sender: Sender<Action>, sender: Sender<Action>,
} }
impl Content { impl Content {
pub fn new(sender: Sender<Action>) -> Result<Content, Error> { pub fn new(sender: Sender<Action>) -> Result<Content, Error> {
let stack = gtk::Stack::new(); let stack = gtk::Stack::new();
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?)); let episodes = Arc::new(EpisodeStack::new(sender.clone())?);
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())?)); let shows = Arc::new(ShowStack::new(sender.clone())?);
stack.add_titled(&home.borrow().get_stack(), "home", "Recent"); stack.add_titled(&episodes.get_stack(), "episodes", "Episodes");
stack.add_titled(&shows.borrow().get_stack(), "shows", "Shows"); stack.add_titled(&shows.get_stack(), "shows", "Shows");
Ok(Content { Ok(Content {
stack, stack,
shows, shows,
home, episodes,
sender, sender,
}) })
} }
pub fn update(&self) { pub fn update(&self) {
self.update_home(); self.update_episode_view();
self.update_shows(); self.update_shows_view();
self.update_widget()
} }
pub fn update_home(&self) { // TODO: Maybe propagate the error?
self.home pub fn update_episode_view(&self) {
.borrow_mut() if let Err(err) = self.episodes.update() {
.update() error!("Something went wrong while trying to update the episode view.");
.map_err(|err| error!("Failed to update HomeView: {}", err)) error!("Error: {}", err);
.ok();
}
pub fn update_home_if_background(&self) {
if self.stack.get_visible_child_name() != Some("home".into()) {
self.update_home();
} }
} }
fn update_shows(&self) { pub fn update_episode_view_if_baground(&self) {
self.shows if self.stack.get_visible_child_name() != Some("episodes".into()) {
.borrow_mut() self.update_episode_view();
.update() }
.map_err(|err| error!("Failed to update ShowsView: {}", err))
.ok();
} }
pub fn update_shows_view(&self) { pub fn update_shows_view(&self) {
self.shows if let Err(err) = self.shows.update_podcasts() {
.borrow_mut() error!("Something went wrong while trying to update the ShowsView.");
.update() error!("Error: {}", err);
.map_err(|err| error!("Failed to update ShowsView: {}", err)) }
.ok(); }
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) { pub fn update_widget_if_same(&self, pid: i32) {
let pop = self.shows.borrow().populated(); if let Err(err) = self.shows.update_widget_if_same(pid) {
pop.borrow_mut() error!("Something went wrong while trying to update the Show Widget.");
.update_widget_if_same(pid) error!("Error: {}", err);
.map_err(|err| error!("Failed to update ShowsWidget: {}", err)) }
.ok(); }
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 { pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone() self.stack.clone()
} }
pub fn get_shows(&self) -> Rc<RefCell<ShowStack>> { pub fn get_shows(&self) -> Arc<ShowStack> {
self.shows.clone() 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 content;
mod home; mod episode;
mod populated;
mod show; mod show;
pub use self::content::Content; pub use self::content::Content;
pub use self::home::HomeStack; pub use self::episode::EpisodeStack;
pub use self::populated::{PopulatedStack, PopulatedState}; pub use self::show::ShowStack;
pub use self::show::{ShowStack, ShowState};
-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;
use gtk::Cast;
use gtk::prelude::*; use gtk::prelude::*;
use failure::Error; 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 app::Action;
use stacks::PopulatedStack; use widgets::ShowWidget;
use utils::get_ignored_shows;
use widgets::EmptyView;
use std::cell::RefCell; use std::sync::Arc;
use std::rc::Rc;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
#[derive(Debug, Clone, Copy)]
pub enum ShowState {
Populated,
Empty,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShowStack { pub struct ShowStack {
empty: EmptyView,
populated: Rc<RefCell<PopulatedStack>>,
stack: gtk::Stack, stack: gtk::Stack,
state: ShowState,
sender: Sender<Action>, sender: Sender<Action>,
} }
impl ShowStack { impl ShowStack {
pub fn new(sender: Sender<Action>) -> Result<Self, Error> { pub fn new(sender: Sender<Action>) -> Result<ShowStack, Error> {
let populated = Rc::new(RefCell::new(PopulatedStack::new(sender.clone())?));
let empty = EmptyView::new();
let stack = gtk::Stack::new(); let stack = gtk::Stack::new();
let state = ShowState::Empty;
stack.add_named(&populated.borrow().container(), "populated"); let show = ShowStack {
stack.add_named(&empty.container, "empty");
let mut show = ShowStack {
empty,
populated,
stack, stack,
state, sender: sender.clone(),
sender,
}; };
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) 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 { pub fn get_stack(&self) -> gtk::Stack {
self.stack.clone() 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))] #![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
use gdk::FrameClockExt;
use gdk_pixbuf::Pixbuf; use gdk_pixbuf::Pixbuf;
use gio::{Settings, SettingsExt};
use glib; use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IsA, Widget};
use chrono::prelude::*;
use failure::Error; use failure::Error;
use rayon; use rayon;
use regex::Regex; use regex::Regex;
@@ -16,128 +14,21 @@ use send_cell::SendCell;
use serde_json::Value; use serde_json::Value;
// use hammond_data::feed; // use hammond_data::feed;
use hammond_data::{PodcastCoverQuery, Source};
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_data::pipeline; use hammond_data::pipeline;
use hammond_data::utils::checkup; use hammond_data::utils::checkup;
use hammond_data::Source;
use hammond_downloader::downloader; use hammond_downloader::downloader;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::mpsc::*;
use std::sync::Arc;
use std::sync::{Mutex, RwLock}; use std::sync::{Mutex, RwLock};
use std::sync::Arc;
use std::sync::mpsc::*;
use app::Action; use app::Action;
/// Lazy evaluates and loads widgets to the parent `container` widget. use chrono::Duration;
/// use chrono::prelude::*;
/// 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
}
lazy_static! { lazy_static! {
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new())); 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>) { pub fn cleanup(cleanup_date: DateTime<Utc>) {
checkup(cleanup_date) if let Err(err) = checkup(cleanup_date) {
.map_err(|err| error!("Check up failed: {}", err)) error!("Check up failed: {}", err);
.ok(); }
} }
pub fn refresh<S>(source: Option<S>, sender: Sender<Action>) pub fn refresh(source: Option<Vec<Source>>, sender: Sender<Action>) {
where if let Err(err) = refresh_feed(source, sender) {
S: IntoIterator<Item = Source> + Send + 'static, error!("An error occured while trying to update the feeds.");
{ error!("Error: {}", err);
refresh_feed(source, sender) }
.map_err(|err| error!("Failed to update feeds: {}", err)) }
.ok();
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`. /// Update the rss feed(s) originating from `source`.
/// If `source` is None, Fetches all the `Source` entries in the database and updates them. /// 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. /// When It's done,it queues up a `RefreshViews` action.
fn refresh_feed<S>(source: Option<S>, sender: Sender<Action>) -> Result<(), Error> fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(), Error> {
where sender.send(Action::HeaderBarShowUpdateIndicator)?;
S: IntoIterator<Item = Source> + Send + 'static,
{
sender
.send(Action::HeaderBarShowUpdateIndicator)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
rayon::spawn(move || { rayon::spawn(move || {
if let Some(s) = source { let mut sources = source.unwrap_or_else(|| {
// Refresh only specified feeds dbqueries::get_sources().expect("Failed to retrieve Sources from the database.")
pipeline::run(s, false) });
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Error While trying to update the database.")) // Work around to improve the feed addition experience.
.ok(); // Many times links to rss feeds are just redirects(usually to an https
} else { // version). Sadly I haven't figured yet a nice way to follow up links
// Refresh all the feeds // redirects without getting to lifetime hell with futures and hyper.
dbqueries::get_sources() // So the requested refresh is only of 1 feed, and the feed fails to be indexed,
.map(|s| s.into_iter()) // (as a 301 redict would update the source entry and exit), another refresh is
.and_then(|s| pipeline::run(s, false)) // run. For more see hammond_data/src/models/source.rs `fn
.map_err(|err| error!("Error: {}", err)) // request_constructor`. also ping me on irc if or open an issue if you
.ok(); // 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 sender
.send(Action::HeaderBarHideUpdateIndicator) .send(Action::HeaderBarHideUpdateIndicator)
.map_err(|err| error!("Action Sender: {}", err)) .expect("Action channel blew up.");
.ok();
sender sender
.send(Action::RefreshAllViews) .send(Action::RefreshAllViews)
.map_err(|err| error!("Action Sender: {}", err)) .expect("Action channel blew up.");
.ok();
}); });
Ok(()) Ok(())
} }
@@ -233,16 +146,19 @@ lazy_static! {
// GObjects do not implement Send trait, so SendCell is a way around that. // 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. // Also lazy_static requires Sync trait, so that's what the mutexes are.
// TODO: maybe use something that would just scale to requested size? // TODO: maybe use something that would just scale to requested size?
#[inline] pub fn set_image_from_path(
pub fn set_image_from_path(image: &gtk::Image, podcast_id: i32, size: u32) -> Result<(), Error> { image: &gtk::Image,
pd: Arc<PodcastCoverQuery>,
size: u32,
) -> Result<(), Error> {
// Check if there's an active download about this show cover. // 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 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 the download succedes, there should be a quick return from the pixbuf cache_image
// If it fails another download will be scheduled. // If it fails another download will be scheduled.
if let Ok(guard) = COVER_DL_REGISTRY.read() { if let Ok(guard) = COVER_DL_REGISTRY.read() {
if guard.contains(&podcast_id) { if guard.contains(&pd.id()) {
let callback = clone!(image => move || { let callback = clone!(image, pd => move || {
let _ = set_image_from_path(&image, podcast_id, size); let _ = set_image_from_path(&image, pd.clone(), size);
glib::Continue(false) glib::Continue(false)
}); });
gtk::timeout_add(250, callback); 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() { if let Ok(hashmap) = CACHED_PIXBUFS.read() {
// Check if the requested (cover + size) is already in the chache // Check if the requested (cover + size) is already in the chache
// and if so do an early return after that. // 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 guard
.lock() .lock()
.map_err(|err| format_err!("SendCell Mutex: {}", err)) .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 (sender, receiver) = channel();
let pd_ = pd.clone();
THREADPOOL.spawn(move || { THREADPOOL.spawn(move || {
if let Ok(mut guard) = COVER_DL_REGISTRY.write() { 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) { let _ = sender.send(downloader::cache_image(&pd_));
sender
.send(downloader::cache_image(&pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}
if let Ok(mut guard) = COVER_DL_REGISTRY.write() { 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(path) = path {
if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) { if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) {
if let Ok(mut hashmap) = CACHED_PIXBUFS.write() { 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); 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")) .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// use hammond_data::Source; // use hammond_data::Source;
// use hammond_data::dbqueries; // 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] // #[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit // This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
// to run it. // to run it.
// #[ignore] // #[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() { // fn test_set_image_from_path() {
// let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff"; // let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
// Create and index a source // Create and index a source
@@ -6,7 +6,6 @@ pub struct EmptyView {
} }
impl Default for EmptyView { impl Default for EmptyView {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui");
let view: gtk::Box = builder.get_object("empty_view").unwrap(); let view: gtk::Box = builder.get_object("empty_view").unwrap();
@@ -16,7 +15,6 @@ impl Default for EmptyView {
} }
impl EmptyView { impl EmptyView {
#[inline]
pub fn new() -> EmptyView { pub fn new() -> EmptyView {
EmptyView::default() EmptyView::default()
} }
@@ -1,25 +1,17 @@
use chrono::prelude::*; use chrono::prelude::*;
use failure::Error; use failure::Error;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use hammond_data::dbqueries;
use hammond_data::EpisodeWidgetQuery; use hammond_data::EpisodeWidgetQuery;
use send_cell::SendCell; use hammond_data::dbqueries;
use app::Action; use app::Action;
use utils::{self, lazy_load_full}; use utils::{get_ignored_shows, set_image_from_path};
use widgets::EpisodeWidget; use widgets::EpisodeWidget;
use std::rc::Rc; use std::sync::Arc;
use std::sync::mpsc::Sender; 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)] #[derive(Debug, Clone)]
enum ListSplit { enum ListSplit {
@@ -31,7 +23,7 @@ enum ListSplit {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HomeView { pub struct EpisodesView {
pub container: gtk::Box, pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow, scrolled_window: gtk::ScrolledWindow,
frame_parent: gtk::Box, frame_parent: gtk::Box,
@@ -47,8 +39,7 @@ pub struct HomeView {
rest_list: gtk::ListBox, rest_list: gtk::ListBox,
} }
impl Default for HomeView { impl Default for EpisodesView {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui");
let container: gtk::Box = builder.get_object("container").unwrap(); 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 month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap(); let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
HomeView { EpisodesView {
container, container,
scrolled_window, scrolled_window,
frame_parent, frame_parent,
@@ -84,82 +75,91 @@ impl Default for HomeView {
} }
// TODO: REFACTOR ME // TODO: REFACTOR ME
impl HomeView { impl EpisodesView {
#[inline] pub fn new(sender: Sender<Action>) -> Result<EpisodesView, Error> {
pub fn new(sender: Sender<Action>) -> Result<Rc<HomeView>, Error> { let view = EpisodesView::default();
use self::ListSplit::*; let ignore = get_ignored_shows()?;
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 50)?;
let view = Rc::new(HomeView::default());
let ignore = utils::get_ignored_shows()?;
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 100)?;
let now_utc = Utc::now(); let now_utc = Utc::now();
let view_ = view.clone(); episodes.into_iter().for_each(|ep| {
let func = move |ep: EpisodeWidgetQuery| {
let epoch = ep.epoch(); let epoch = ep.epoch();
let widget = EpisodesViewWidget::new(ep, &sender); let viewep = EpisodesViewWidget::new(ep, sender.clone());
match split(&now_utc, i64::from(epoch)) { let t = split(&now_utc, i64::from(epoch));
Today => add_to_box(&widget, &view_.today_list, &view_.today_box), match t {
Yday => add_to_box(&widget, &view_.yday_list, &view_.yday_box), ListSplit::Today => {
Week => add_to_box(&widget, &view_.week_list, &view_.week_box), view.today_list.add(&viewep.container);
Month => add_to_box(&widget, &view_.month_list, &view_.month_box),
Rest => add_to_box(&widget, &view_.rest_list, &view_.rest_box),
} }
}; 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(); if view.today_list.get_children().is_empty() {
let callback = move || { view.today_box.hide();
view_ }
.set_vadjustment()
.map_err(|err| format!("{}", err)) if view.yday_list.get_children().is_empty() {
.ok(); 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(); view.container.show_all();
Ok(view) 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. /// Set scrolled window vertical adjustment.
fn set_vadjustment(&self) -> Result<(), Error> { pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
let guard = EPISODES_VIEW_VALIGNMENT self.scrolled_window.set_vadjustment(vadjustment)
.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(())
} }
} }
#[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 { fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
let ep = Utc.timestamp(epoch, 0); let ep = Utc.timestamp(epoch, 0);
@@ -184,7 +184,6 @@ struct EpisodesViewWidget {
} }
impl Default for EpisodesViewWidget { impl Default for EpisodesViewWidget {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui"); gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
@@ -202,13 +201,13 @@ impl Default for EpisodesViewWidget {
} }
impl EpisodesViewWidget { impl EpisodesViewWidget {
fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodesViewWidget { fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodesViewWidget {
let builder = let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui"); gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap(); let container: gtk::Box = builder.get_object("container").unwrap();
let image: gtk::Image = builder.get_object("cover").unwrap(); let image: gtk::Image = builder.get_object("cover").unwrap();
let pid = episode.podcast_id(); let pid = episode.podcast_id();
let ep = EpisodeWidget::new(episode, sender); let ep = EpisodeWidget::new(episode, sender.clone());
let view = EpisodesViewWidget { let view = EpisodesViewWidget {
container, container,
@@ -220,17 +219,16 @@ impl EpisodesViewWidget {
view view
} }
#[inline]
fn init(&self, podcast_id: i32) { fn init(&self, podcast_id: i32) {
self.set_cover(podcast_id) if let Err(err) = self.set_cover(podcast_id) {
.map_err(|err| error!("Failed to set a cover: {}", err)) error!("Failed to set a cover: {}", err)
.ok(); }
self.container.pack_start(&self.episode, true, true, 6); self.container.pack_start(&self.episode, true, true, 6);
} }
#[inline]
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> { 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;
use gtk::prelude::*; use gtk::prelude::*;
use hammond_data::{Podcast, PodcastCoverQuery};
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_data::Podcast;
use app::Action; 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::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShowsView { pub struct ShowsPopulated {
pub container: gtk::Box, pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow, scrolled_window: gtk::ScrolledWindow,
flowbox: gtk::FlowBox, flowbox: gtk::FlowBox,
} }
impl Default for ShowsView { impl Default for ShowsPopulated {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_view.ui"); 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 container: gtk::Box = builder.get_object("fb_parent").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap(); let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap(); let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
ShowsView { ShowsPopulated {
container, container,
scrolled_window, scrolled_window,
flowbox, flowbox,
@@ -35,43 +33,49 @@ impl Default for ShowsView {
} }
} }
impl ShowsView { impl ShowsPopulated {
#[inline] pub fn new(sender: Sender<Action>) -> Result<ShowsPopulated, Error> {
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> { let pop = ShowsPopulated::default();
let pop = Rc::new(ShowsView::default());
pop.init(sender)?; pop.init(sender)?;
Ok(pop) Ok(pop)
} }
#[inline]
pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> { pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> {
self.flowbox.connect_child_activated(move |_, child| { self.flowbox.connect_child_activated(move |_, child| {
on_child_activate(child, &sender) if let Err(err) = on_child_activate(child, sender.clone()) {
.map_err(|err| error!("Error along flowbox child activation: {}", err)) error!(
.ok(); "Something went wrong during flowbox child activation: {}.",
err
)
};
}); });
// Populate the flowbox with the Podcasts. // Populate the flowbox with the Podcasts.
self.populate_flowbox() self.populate_flowbox()
} }
#[inline]
fn populate_flowbox(&self) -> Result<(), Error> { fn populate_flowbox(&self) -> Result<(), Error> {
let ignore = get_ignored_shows()?; let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?; let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
let flowbox = self.flowbox.clone(); podcasts.into_iter().for_each(|parent| {
let constructor = |parent| ShowsChild::new(&parent).child; let flowbox_child = ShowsChild::new(parent);
let callback = move || flowbox.show_all(); self.flowbox.add(&flowbox_child.child);
});
let flowbox = self.flowbox.clone(); self.flowbox.show_all();
lazy_load(podcasts, flowbox, constructor, callback);
Ok(()) Ok(())
} }
pub fn is_empty(&self) -> bool {
self.flowbox.get_children().is_empty()
} }
#[inline] /// Set scrolled window vertical adjustment.
fn on_child_activate(child: &gtk::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> { 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; use gtk::WidgetExt;
// This is such an ugly hack... // This is such an ugly hack...
@@ -94,7 +98,6 @@ struct ShowsChild {
} }
impl Default for ShowsChild { impl Default for ShowsChild {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui");
@@ -113,25 +116,23 @@ impl Default for ShowsChild {
} }
impl ShowsChild { impl ShowsChild {
#[inline] pub fn new(pd: Podcast) -> ShowsChild {
pub fn new(pd: &Podcast) -> ShowsChild {
let child = ShowsChild::default(); let child = ShowsChild::default();
child.init(pd); child.init(pd);
child child
} }
#[inline] fn init(&self, pd: Podcast) {
fn init(&self, pd: &Podcast) {
self.container.set_tooltip_text(pd.title()); self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string()); WidgetExt::set_name(&self.child, &pd.id().to_string());
self.set_cover(pd.id()) let pd = Arc::new(pd.into());
.map_err(|err| error!("Failed to set a cover: {}", err)) if let Err(err) = self.set_cover(pd) {
.ok(); error!("Failed to set a cover: {}", err)
}
} }
#[inline] fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> { set_image_from_path(&self.cover, pd, 256)
set_image_from_path(&self.cover, podcast_id, 256)
} }
} }
+124 -108
View File
@@ -5,11 +5,12 @@ use gtk::prelude::*;
use failure::Error; use failure::Error;
use humansize::FileSize; use humansize::FileSize;
use open; use open;
use rayon;
use take_mut; use take_mut;
use hammond_data::{EpisodeWidgetQuery, Podcast};
use hammond_data::dbqueries; use hammond_data::dbqueries;
use hammond_data::utils::get_download_folder; use hammond_data::utils::get_download_folder;
use hammond_data::EpisodeWidgetQuery;
use app::Action; use app::Action;
use manager; use manager;
@@ -19,8 +20,8 @@ use std::cell::RefCell;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::sync::mpsc::Sender;
#[derive(Debug)] #[derive(Debug)]
pub struct EpisodeWidget { pub struct EpisodeWidget {
@@ -28,11 +29,10 @@ pub struct EpisodeWidget {
date: DateMachine, date: DateMachine,
duration: DurationMachine, duration: DurationMachine,
title: Rc<RefCell<TitleMachine>>, title: Rc<RefCell<TitleMachine>>,
media: Rc<RefCell<MediaMachine>>, media: Arc<Mutex<MediaMachine>>,
} }
impl Default for EpisodeWidget { impl Default for EpisodeWidget {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
@@ -66,7 +66,7 @@ impl Default for EpisodeWidget {
separator2, separator2,
prog_separator, prog_separator,
); );
let media_machine = Rc::new(RefCell::new(media)); let media_machine = Arc::new(Mutex::new(media));
EpisodeWidget { EpisodeWidget {
container, container,
@@ -79,15 +79,15 @@ impl Default for EpisodeWidget {
} }
impl 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(); let mut widget = EpisodeWidget::default();
widget.init(episode, sender); widget.init(episode, sender);
widget 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. // Set the date label.
self.set_date(episode.epoch()); self.set_date(episode.epoch());
@@ -98,49 +98,40 @@ impl EpisodeWidget {
self.set_duration(episode.duration()); self.set_duration(episode.duration());
// Determine what the state of the media widgets should be. // Determine what the state of the media widgets should be.
determine_media_state(&self.media, &episode) if let Err(err) = self.determine_media_state(&episode) {
.map_err(|err| error!("Error: {}", err)) error!("Something went wrong determining the Media State.");
.map_err(|_| error!("Could not determine Media State")) error!("Error: {}", err);
.ok(); }
let episode = Arc::new(Mutex::new(episode)); 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(); 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 |_| { media.play_connect_clicked(clone!(episode, sender => move |_| {
if let Ok(mut ep) = episode.lock() { if let Ok(mut ep) = episode.lock() {
on_play_bttn_clicked(&mut ep, &title, &sender) if let Err(err) = on_play_bttn_clicked(&mut ep, title.clone(), sender.clone()){
.map_err(|err| error!("Error: {}", err)) error!("Error: {}", err);
.ok(); };
} }
})); }));
let media_machine = self.media.clone(); media.download_connect_clicked(clone!(episode, sender => move |dl| {
media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| {
// Make the button insensitive so it won't be pressed twice
dl.set_sensitive(false); dl.set_sensitive(false);
if let Ok(ep) = episode.lock() { if let Ok(ep) = episode.lock() {
on_download_clicked(&ep, &sender) if let Err(err) = on_download_clicked(&ep, sender.clone()) {
.and_then(|_| { error!("Download failed to start.");
error!("Error: {}", err);
} else {
info!("Donwload started succesfully."); 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. /// Determine the title state.
fn set_title(&mut self, episode: &EpisodeWidgetQuery) { fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
let mut machine = self.title.borrow_mut(); let mut machine = self.title.borrow_mut();
@@ -150,27 +141,23 @@ impl EpisodeWidget {
}); });
} }
#[inline]
/// Set the date label depending on the current time. /// Set the date label depending on the current time.
fn set_date(&mut self, epoch: i32) { fn set_date(&mut self, epoch: i32) {
let machine = &mut self.date; let machine = &mut self.date;
take_mut::take(machine, |date| date.determine_state(i64::from(epoch))); take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
} }
#[inline]
/// Set the duration label. /// Set the duration label.
fn set_duration(&mut self, seconds: Option<i32>) { fn set_duration(&mut self, seconds: Option<i32>) {
let machine = &mut self.duration; let machine = &mut self.duration;
take_mut::take(machine, |duration| duration.determine_state(seconds)); take_mut::take(machine, |duration| duration.determine_state(seconds));
} }
}
#[inline] fn determine_media_state(&self, episode: &EpisodeWidgetQuery) -> Result<(), Error> {
fn determine_media_state( let id = WidgetExt::get_name(&self.container)
media_machine: &Rc<RefCell<MediaMachine>>, .ok_or_else(|| format_err!("Failed to get widget Name"))?
episode: &EpisodeWidgetQuery, .parse::<i32>()?;
) -> Result<(), Error> {
let id = episode.rowid();
let active_dl = || -> Result<Option<_>, Error> { let active_dl = || -> Result<Option<_>, Error> {
let m = manager::ACTIVE_DOWNLOADS let m = manager::ACTIVE_DOWNLOADS
.read() .read()
@@ -179,7 +166,7 @@ fn determine_media_state(
Ok(m.get(&id).cloned()) 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| { take_mut::take(lock.deref_mut(), |media| {
media.determine_state( media.determine_state(
episode.length(), episode.length(),
@@ -190,67 +177,33 @@ fn determine_media_state(
// Show or hide the play/delete/download buttons upon widget initialization. // Show or hide the play/delete/download buttons upon widget initialization.
if let Some(prog) = active_dl { if let Some(prog) = active_dl {
// set a callback that will update the state when the download finishes lock.cancel_connect_clicked(prog.clone());
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(),
)
});
}
}
}));
drop(lock); drop(lock);
// Setup a callback that will update the progress bar. // 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 // Setup a callback that will update the total_size label
// with the http ContentLength header number rather than // with the http ContentLength header number rather than
// relying to the RSS feed. // relying to the RSS feed.
update_total_size_callback(&prog, &media_machine); update_total_size_callback(prog.clone(), self.media.clone());
} }
Ok(()) Ok(())
} }
}
#[inline] #[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 pd = dbqueries::get_podcast_from_id(ep.podcast_id())?;
let download_fold = get_download_folder(&pd.title())?; let download_fold = get_download_folder(&pd.title())?;
// Start a new download. // Start a new download.
manager::add(ep.rowid(), download_fold)?; manager::add(ep.rowid(), download_fold, sender.clone())?;
// Update Views // Update Views
sender.send(Action::RefreshEpisodesViewBGR)?; sender.send(Action::RefreshEpisodesView)?;
sender.send(Action::RefreshWidgetIfVis)?;
Ok(()) Ok(())
} }
@@ -258,8 +211,8 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Resu
#[inline] #[inline]
fn on_play_bttn_clicked( fn on_play_bttn_clicked(
episode: &mut EpisodeWidgetQuery, episode: &mut EpisodeWidgetQuery,
title: &Rc<RefCell<TitleMachine>>, title: Rc<RefCell<TitleMachine>>,
sender: &Sender<Action>, sender: Sender<Action>,
) -> Result<(), Error> { ) -> Result<(), Error> {
open_uri(episode.rowid())?; open_uri(episode.rowid())?;
episode.set_played_now()?; episode.set_played_now()?;
@@ -273,7 +226,6 @@ fn on_play_bttn_clicked(
Ok(()) Ok(())
} }
#[inline]
fn open_uri(rowid: i32) -> Result<(), Error> { fn open_uri(rowid: i32) -> Result<(), Error> {
let uri = dbqueries::get_episode_local_uri_from_id(rowid)? let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
.ok_or_else(|| format_err!("Expected Some found None."))?; .ok_or_else(|| format_err!("Expected Some found None."))?;
@@ -292,22 +244,24 @@ fn open_uri(rowid: i32) -> Result<(), Error> {
#[inline] #[inline]
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))] #[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
fn update_progressbar_callback( fn update_progressbar_callback(
prog: &Arc<Mutex<manager::Progress>>, prog: Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>, media: Arc<Mutex<MediaMachine>>,
episode_rowid: i32, episode_rowid: i32,
) { ) {
let callback = clone!(prog, media => move || { timeout_add(
progress_bar_helper(&prog, &media, episode_rowid) 400,
clone!(prog, media => move || {
progress_bar_helper(prog.clone(), media.clone(), episode_rowid)
.unwrap_or(glib::Continue(false)) .unwrap_or(glib::Continue(false))
}); }),
timeout_add(300, callback); );
} }
#[inline] #[inline]
#[allow(if_same_then_else)] #[allow(if_same_then_else)]
fn progress_bar_helper( fn progress_bar_helper(
prog: &Arc<Mutex<manager::Progress>>, prog: Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>, media: Arc<Mutex<MediaMachine>>,
episode_rowid: i32, episode_rowid: i32,
) -> Result<glib::Continue, Error> { ) -> Result<glib::Continue, Error> {
let (fraction, downloaded) = { let (fraction, downloaded) = {
@@ -324,10 +278,9 @@ fn progress_bar_helper(
.file_size(SIZE_OPTS.clone()) .file_size(SIZE_OPTS.clone())
.map_err(|err| format_err!("{}", err))?; .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); m.update_progress(&size, fraction);
} }
}
// info!("Fraction: {}", progress_bar.get_fraction()); // info!("Fraction: {}", progress_bar.get_fraction());
// info!("Fraction: {}", fraction); // info!("Fraction: {}", fraction);
@@ -354,19 +307,21 @@ fn progress_bar_helper(
// relying to the RSS feed. // relying to the RSS feed.
#[inline] #[inline]
fn update_total_size_callback( fn update_total_size_callback(
prog: &Arc<Mutex<manager::Progress>>, prog: Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>, media: Arc<Mutex<MediaMachine>>,
) { ) {
let callback = clone!(prog, media => move || { timeout_add(
total_size_helper(&prog, &media).unwrap_or(glib::Continue(true)) 500,
}); clone!(prog, media => move || {
timeout_add(500, callback); total_size_helper(prog.clone(), media.clone()).unwrap_or(glib::Continue(true))
}),
);
} }
#[inline] #[inline]
fn total_size_helper( fn total_size_helper(
prog: &Arc<Mutex<manager::Progress>>, prog: Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>, media: Arc<Mutex<MediaMachine>>,
) -> Result<glib::Continue, Error> { ) -> Result<glib::Continue, Error> {
// Get the total_bytes. // Get the total_bytes.
let total_bytes = { let total_bytes = {
@@ -378,7 +333,7 @@ fn total_size_helper(
debug!("Total Size: {}", total_bytes); debug!("Total Size: {}", total_bytes);
if total_bytes != 0 { if total_bytes != 0 {
// Update the total_size label // 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| { take_mut::take(m.deref_mut(), |machine| {
machine.set_size(Some(total_bytes as i32)) 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(); // let mut ep = dbqueries::get_episode_from_rowid(episode_id)?.into();
// delete_local_content(&mut ep).map_err(From::from).map(|_| ()) // 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 gtk::prelude::*;
use humansize::{file_size_opts as size_opts, FileSize}; 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! { lazy_static! {
pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = { pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
@@ -61,7 +63,6 @@ pub struct Title<S> {
impl<S> Title<S> { impl<S> Title<S> {
#[allow(unused_must_use)] #[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership // 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. // But I think it wouldn't hurt if we treat it as a Rust api.
fn set_title(&mut self, s: &str) { fn set_title(&mut self, s: &str) {
@@ -70,7 +71,6 @@ impl<S> Title<S> {
} }
impl Title<Normal> { impl Title<Normal> {
#[inline]
fn new(title: gtk::Label) -> Self { fn new(title: gtk::Label) -> Self {
Title { Title {
title, title,
@@ -80,7 +80,6 @@ impl Title<Normal> {
} }
impl From<Title<Normal>> for Title<GreyedOut> { impl From<Title<Normal>> for Title<GreyedOut> {
#[inline]
fn from(f: Title<Normal>) -> Self { fn from(f: Title<Normal>) -> Self {
f.title f.title
.get_style_context() .get_style_context()
@@ -94,7 +93,6 @@ impl From<Title<Normal>> for Title<GreyedOut> {
} }
impl From<Title<GreyedOut>> for Title<Normal> { impl From<Title<GreyedOut>> for Title<Normal> {
#[inline]
fn from(f: Title<GreyedOut>) -> Self { fn from(f: Title<GreyedOut>) -> Self {
f.title f.title
.get_style_context() .get_style_context()
@@ -114,13 +112,11 @@ pub enum TitleMachine {
} }
impl TitleMachine { impl TitleMachine {
#[inline]
pub fn new(label: gtk::Label, is_played: bool) -> Self { pub fn new(label: gtk::Label, is_played: bool) -> Self {
let m = TitleMachine::Normal(Title::<Normal>::new(label)); let m = TitleMachine::Normal(Title::<Normal>::new(label));
m.determine_state(is_played) m.determine_state(is_played)
} }
#[inline]
pub fn determine_state(self, is_played: bool) -> Self { pub fn determine_state(self, is_played: bool) -> Self {
use self::TitleMachine::*; use self::TitleMachine::*;
@@ -132,7 +128,6 @@ impl TitleMachine {
} }
} }
#[inline]
pub fn set_title(&mut self, s: &str) { pub fn set_title(&mut self, s: &str) {
use self::TitleMachine::*; use self::TitleMachine::*;
@@ -156,7 +151,6 @@ pub struct Date<S> {
} }
impl<S> Date<S> { impl<S> Date<S> {
#[inline]
fn into_usual(self, epoch: i64) -> Date<Usual> { fn into_usual(self, epoch: i64) -> Date<Usual> {
let ts = Utc.timestamp(epoch, 0); let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b").to_string().trim()); 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> { fn into_year_shown(self, epoch: i64) -> Date<YearShown> {
let ts = Utc.timestamp(epoch, 0); let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b %Y").to_string().trim()); self.date.set_text(ts.format("%e %b %Y").to_string().trim());
@@ -182,7 +175,6 @@ impl<S> Date<S> {
} }
impl Date<UnInitialized> { impl Date<UnInitialized> {
#[inline]
fn new(date: gtk::Label, epoch: i64) -> Self { fn new(date: gtk::Label, epoch: i64) -> Self {
let ts = Utc.timestamp(epoch, 0); let ts = Utc.timestamp(epoch, 0);
date.set_text(ts.format("%e %b %Y").to_string().trim()); date.set_text(ts.format("%e %b %Y").to_string().trim());
@@ -203,18 +195,16 @@ pub enum DateMachine {
} }
impl DateMachine { impl DateMachine {
#[inline]
pub fn new(label: gtk::Label, epoch: i64) -> Self { pub fn new(label: gtk::Label, epoch: i64) -> Self {
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch)); let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
m.determine_state(epoch) m.determine_state(epoch)
} }
#[inline]
pub fn determine_state(self, epoch: i64) -> Self { pub fn determine_state(self, epoch: i64) -> Self {
use self::DateMachine::*; use self::DateMachine::*;
let ts = Utc.timestamp(epoch, 0); let ts = Utc.timestamp(epoch, 0);
let is_old = NOW.year() != ts.year(); let is_old = !(NOW.year() == ts.year());
match (self, is_old) { match (self, is_old) {
// Into Usual // Into Usual
@@ -239,7 +229,6 @@ pub struct Duration<S: Visibility> {
} }
impl<S: Visibility> Duration<S> { impl<S: Visibility> Duration<S> {
#[inline]
// This needs a better name. // This needs a better name.
// TODO: make me mut // TODO: make me mut
fn set_duration(&self, minutes: i64) { fn set_duration(&self, minutes: i64) {
@@ -248,7 +237,6 @@ impl<S: Visibility> Duration<S> {
} }
impl Duration<Hidden> { impl Duration<Hidden> {
#[inline]
fn new(duration: gtk::Label, separator: gtk::Label) -> Self { fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
duration.hide(); duration.hide();
separator.hide(); separator.hide();
@@ -262,7 +250,6 @@ impl Duration<Hidden> {
} }
impl From<Duration<Hidden>> for Duration<Shown> { impl From<Duration<Hidden>> for Duration<Shown> {
#[inline]
fn from(f: Duration<Hidden>) -> Self { fn from(f: Duration<Hidden>) -> Self {
f.duration.show(); f.duration.show();
f.separator.show(); f.separator.show();
@@ -276,7 +263,6 @@ impl From<Duration<Hidden>> for Duration<Shown> {
} }
impl From<Duration<Shown>> for Duration<Hidden> { impl From<Duration<Shown>> for Duration<Hidden> {
#[inline]
fn from(f: Duration<Shown>) -> Self { fn from(f: Duration<Shown>) -> Self {
f.duration.hide(); f.duration.hide();
f.separator.hide(); f.separator.hide();
@@ -296,13 +282,11 @@ pub enum DurationMachine {
} }
impl DurationMachine { impl DurationMachine {
#[inline]
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self { pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator)); let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
m.determine_state(seconds) m.determine_state(seconds)
} }
#[inline]
pub fn determine_state(self, seconds: Option<i32>) -> Self { pub fn determine_state(self, seconds: Option<i32>) -> Self {
match (self, seconds) { match (self, seconds) {
(d @ DurationMachine::Hidden(_), None) => d, (d @ DurationMachine::Hidden(_), None) => d,
@@ -337,7 +321,6 @@ pub struct Size<S> {
} }
impl<S> Size<S> { impl<S> Size<S> {
#[inline]
fn set_size(self, s: &str) -> Size<Shown> { fn set_size(self, s: &str) -> Size<Shown> {
self.size.set_text(s); self.size.set_text(s);
self.size.show(); self.size.show();
@@ -349,7 +332,6 @@ impl<S> Size<S> {
} }
} }
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable // https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization. // It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844 // https://github.com/rust-lang/rust/issues/31844
@@ -364,7 +346,6 @@ impl<S> Size<S> {
} }
} }
#[inline]
fn into_hidden(self) -> Size<Hidden> { fn into_hidden(self) -> Size<Hidden> {
self.size.hide(); self.size.hide();
self.separator.hide(); self.separator.hide();
@@ -378,7 +359,6 @@ impl<S> Size<S> {
} }
impl Size<UnInitialized> { impl Size<UnInitialized> {
#[inline]
fn new(size: gtk::Label, separator: gtk::Label) -> Self { fn new(size: gtk::Label, separator: gtk::Label) -> Self {
size.hide(); size.hide();
separator.hide(); separator.hide();
@@ -412,7 +392,6 @@ pub struct DownloadPlay<S> {
} }
impl<S> DownloadPlay<S> { impl<S> DownloadPlay<S> {
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable // https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization. // It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844 // https://github.com/rust-lang/rust/issues/31844
@@ -427,7 +406,6 @@ impl<S> DownloadPlay<S> {
} }
} }
#[inline]
fn into_fetchable(self) -> DownloadPlay<Download> { fn into_fetchable(self) -> DownloadPlay<Download> {
self.play.hide(); self.play.hide();
self.download.show(); self.download.show();
@@ -439,7 +417,6 @@ impl<S> DownloadPlay<S> {
} }
} }
#[inline]
fn into_hidden(self) -> DownloadPlay<Hidden> { fn into_hidden(self) -> DownloadPlay<Hidden> {
self.play.hide(); self.play.hide();
self.download.hide(); self.download.hide();
@@ -451,7 +428,6 @@ impl<S> DownloadPlay<S> {
} }
} }
#[inline]
fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>( fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@@ -459,14 +435,12 @@ impl<S> DownloadPlay<S> {
self.download.connect_clicked(f) self.download.connect_clicked(f)
} }
#[inline]
fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId { fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.play.connect_clicked(f) self.play.connect_clicked(f)
} }
} }
impl DownloadPlay<UnInitialized> { impl DownloadPlay<UnInitialized> {
#[inline]
fn new(play: gtk::Button, download: gtk::Button) -> Self { fn new(play: gtk::Button, download: gtk::Button) -> Self {
play.hide(); play.hide();
download.hide(); download.hide();
@@ -489,7 +463,6 @@ pub struct Progress<S> {
} }
impl<S> Progress<S> { impl<S> Progress<S> {
#[inline]
fn into_shown(self) -> Progress<Shown> { fn into_shown(self) -> Progress<Shown> {
self.bar.show(); self.bar.show();
self.cancel.show(); self.cancel.show();
@@ -505,7 +478,6 @@ impl<S> Progress<S> {
} }
} }
#[inline]
fn into_hidden(self) -> Progress<Hidden> { fn into_hidden(self) -> Progress<Hidden> {
self.bar.hide(); self.bar.hide();
self.cancel.hide(); self.cancel.hide();
@@ -522,7 +494,6 @@ impl<S> Progress<S> {
} }
#[allow(unused_must_use)] #[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership // 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. // 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) { fn update_progress(&mut self, local_size: &str, fraction: f64) {
@@ -530,14 +501,17 @@ impl<S> Progress<S> {
self.bar.set_fraction(fraction); self.bar.set_fraction(fraction);
} }
#[inline] fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId { self.cancel.connect_clicked(move |cancel| {
self.cancel.connect_clicked(f) if let Ok(mut m) = prog.lock() {
m.cancel();
cancel.set_sensitive(false);
}
})
} }
} }
impl Progress<UnInitialized> { impl Progress<UnInitialized> {
#[inline]
fn new( fn new(
bar: gtk::ProgressBar, bar: gtk::ProgressBar,
cancel: gtk::Button, cancel: gtk::Button,
@@ -569,9 +543,73 @@ pub struct Media<X, Y, Z> {
type New<Y> = Media<Download, Y, Hidden>; type New<Y> = Media<Download, Y, Hidden>;
type Playable<Y> = Media<Play, Y, Hidden>; type Playable<Y> = Media<Play, Y, Hidden>;
type InProgress = Media<Hidden, Shown, Shown>; 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> { impl<X, Y, Z> Media<X, Y, Z> {
#[inline]
fn set_size(self, s: &str) -> Media<X, Shown, Z> { fn set_size(self, s: &str) -> Media<X, Shown, Z> {
Media { Media {
dl: self.dl, dl: self.dl,
@@ -580,7 +618,6 @@ impl<X, Y, Z> Media<X, Y, Z> {
} }
} }
#[inline]
fn hide_size(self) -> Media<X, Hidden, Z> { fn hide_size(self) -> Media<X, Hidden, Z> {
Media { Media {
dl: self.dl, dl: self.dl,
@@ -589,7 +626,6 @@ impl<X, Y, Z> Media<X, Y, Z> {
} }
} }
#[inline]
fn into_new(self, size: &str) -> New<Shown> { fn into_new(self, size: &str) -> New<Shown> {
Media { Media {
dl: self.dl.into_fetchable(), 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> { fn into_playable(self, size: &str) -> Playable<Shown> {
Media { Media {
dl: self.dl.into_playable(), dl: self.dl.into_playable(),
@@ -615,19 +641,9 @@ impl<X, Y, Z> Media<X, Y, Z> {
progress: self.progress.into_hidden(), 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> { impl<X, Z> Media<X, Shown, Z> {
#[inline]
fn into_progress(self) -> InProgress { fn into_progress(self) -> InProgress {
Media { Media {
dl: self.dl.into_hidden(), dl: self.dl.into_hidden(),
@@ -638,7 +654,6 @@ impl<X, Z> Media<X, Shown, Z> {
} }
impl<X, Z> Media<X, Hidden, Z> { impl<X, Z> Media<X, Hidden, Z> {
#[inline]
fn into_progress(self) -> InProgress { fn into_progress(self) -> InProgress {
Media { Media {
dl: self.dl.into_hidden(), dl: self.dl.into_hidden(),
@@ -649,7 +664,6 @@ impl<X, Z> Media<X, Hidden, Z> {
} }
impl<X, Z> Media<X, UnInitialized, Z> { impl<X, Z> Media<X, UnInitialized, Z> {
#[inline]
fn into_progress(self, size: Option<String>) -> InProgress { fn into_progress(self, size: Option<String>) -> InProgress {
if let Some(s) = size { if let Some(s) = size {
Media { Media {
@@ -668,7 +682,6 @@ impl<X, Z> Media<X, UnInitialized, Z> {
} }
impl InProgress { impl InProgress {
#[inline]
#[allow(unused_must_use)] #[allow(unused_must_use)]
// This does not need to be &mut since gtk-rs does not model ownership // 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. // But I think it wouldn't hurt if we treat it as a Rust api.
@@ -686,7 +699,6 @@ pub enum ButtonsState {
} }
impl ButtonsState { impl ButtonsState {
#[inline]
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self { pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
use self::ButtonsState::*; use self::ButtonsState::*;
@@ -707,33 +719,32 @@ impl ButtonsState {
// From whatever to NewWithoutSize // From whatever to NewWithoutSize
(New(m), None, false) => NewWithoutSize(m.hide_size()), (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, (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 // 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()), (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, (b @ PlayableWithoutSize(_), None, true) => b,
// _ => unimplemented!()
} }
} }
#[inline]
fn into_progress(self) -> InProgress { fn into_progress(self) -> InProgress {
use self::ButtonsState::*; use self::ButtonsState::*;
match self { match self {
New(m) => m.into_progress(), New(m) => m.into(),
Playable(m) => m.into_progress(), Playable(m) => m.into(),
NewWithoutSize(m) => m.into_progress(), NewWithoutSize(m) => m.into(),
PlayableWithoutSize(m) => m.into_progress(), PlayableWithoutSize(m) => m.into(),
} }
} }
#[inline]
fn set_size(self, size: Option<String>) -> Self { fn set_size(self, size: Option<String>) -> Self {
use self::ButtonsState::*; use self::ButtonsState::*;
@@ -749,7 +760,6 @@ impl ButtonsState {
} }
} }
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>( pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@@ -764,7 +774,6 @@ impl ButtonsState {
} }
} }
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>( pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@@ -779,15 +788,14 @@ impl ButtonsState {
} }
} }
#[inline] fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
use self::ButtonsState::*; use self::ButtonsState::*;
match *self { match *self {
New(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(f), NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
Playable(ref val) => val.progress.cancel_connect_clicked(f), Playable(ref val) => val.progress.cancel_connect_clicked(prog),
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(f), PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
} }
} }
} }
@@ -801,7 +809,6 @@ pub enum MediaMachine {
impl MediaMachine { impl MediaMachine {
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))] #[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
#[inline]
pub fn new( pub fn new(
play: gtk::Button, play: gtk::Button,
download: gtk::Button, download: gtk::Button,
@@ -819,7 +826,6 @@ impl MediaMachine {
MediaMachine::UnInitialized(Media { dl, progress, size }) MediaMachine::UnInitialized(Media { dl, progress, size })
} }
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>( pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@@ -833,7 +839,6 @@ impl MediaMachine {
} }
} }
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>( pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@@ -847,21 +852,16 @@ impl MediaMachine {
} }
} }
#[inline] pub fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*; use self::MediaMachine::*;
match *self { match *self {
UnInitialized(ref val) => val.progress.cancel_connect_clicked(f), UnInitialized(ref val) => val.progress.cancel_connect_clicked(prog),
Initialized(ref val) => val.cancel_connect_clicked(f), Initialized(ref val) => val.cancel_connect_clicked(prog),
InProgress(ref val) => val.progress.cancel_connect_clicked(f), 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 { pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
use self::ButtonsState::*; use self::ButtonsState::*;
use self::MediaMachine::*; use self::MediaMachine::*;
@@ -871,36 +871,18 @@ impl MediaMachine {
// Into New // Into New
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))), (UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(UnInitialized(m), None, false, false) => { (UnInitialized(m), None, false, false) => Initialized(NewWithoutSize(m.into())),
Initialized(NewWithoutSize(m.into_new_without()))
}
// Into Playable // Into Playable
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))), (UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(UnInitialized(m), None, true, false) => { (UnInitialized(m), None, true, false) => Initialized(PlayableWithoutSize(m.into())),
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)), (Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()), (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, (i @ InProgress(_), _, _, _) => i,
} }
} }
#[inline]
pub fn set_size(self, bytes: Option<i32>) -> Self { pub fn set_size(self, bytes: Option<i32>) -> Self {
use self::MediaMachine::*; use self::MediaMachine::*;
let size = size_helper(bytes); let size = size_helper(bytes);
@@ -913,7 +895,6 @@ impl MediaMachine {
} }
} }
#[inline]
pub fn update_progress(&mut self, local_size: &str, fraction: f64) { pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
use self::MediaMachine::*; use self::MediaMachine::*;
+2 -8
View File
@@ -1,13 +1,7 @@
mod empty; mod show;
mod episode; mod episode;
mod episode_states; mod episode_states;
mod home_view;
mod show;
mod shows_view;
pub use self::empty::EmptyView;
pub use self::episode::EpisodeWidget; pub use self::episode::EpisodeWidget;
pub use self::home_view::HomeView;
pub use self::show::ShowWidget; pub use self::show::ShowWidget;
pub use self::show::{mark_all_notif, remove_show_notif}; pub use self::show::mark_all_watched;
pub use self::shows_view::ShowsView;
+53 -223
View File
@@ -1,30 +1,19 @@
use glib; use failure::Error;
// use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use failure::Error;
use html2pango::markup_from_raw; use html2pango::markup_from_raw;
use open; 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::Podcast;
use hammond_data::dbqueries;
use app::Action; use app::Action;
use appnotif::InAppNotification; use utils::set_image_from_path;
use utils::{self, lazy_load}; use widgets::episode::episodes_listbox;
use widgets::EpisodeWidget;
use std::rc::Rc; use std::sync::Arc;
use std::sync::mpsc::{SendError, Sender}; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
lazy_static! {
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
Mutex::new(None);
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShowWidget { pub struct ShowWidget {
@@ -35,17 +24,15 @@ pub struct ShowWidget {
link: gtk::Button, link: gtk::Button,
settings: gtk::MenuButton, settings: gtk::MenuButton,
unsub: gtk::Button, unsub: gtk::Button,
episodes: gtk::ListBox, episodes: gtk::Frame,
podcast_id: Option<i32>,
} }
impl Default for ShowWidget { impl Default for ShowWidget {
#[inline]
fn default() -> Self { fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui"); let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap(); let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").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 cover: gtk::Image = builder.get_object("cover").unwrap();
let description: gtk::Label = builder.get_object("description").unwrap(); let description: gtk::Label = builder.get_object("description").unwrap();
@@ -62,48 +49,45 @@ impl Default for ShowWidget {
link, link,
settings, settings,
episodes, episodes,
podcast_id: None,
} }
} }
} }
impl ShowWidget { impl ShowWidget {
#[inline] pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> ShowWidget {
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> { let pdw = ShowWidget::default();
let mut pdw = ShowWidget::default(); pdw.init(pd, sender);
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();
pdw pdw
} }
#[inline] pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
pub fn init(&mut self, pd: &Arc<Podcast>, sender: &Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui"); 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 self.unsub
.connect_clicked(clone!(pd, sender => move |bttn| { .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.set_description(pd.description());
self.podcast_id = Some(pd.id());
self.set_cover(&pd) if let Err(err) = self.set_cover(pd.clone()) {
.map_err(|err| error!("Failed to set a cover: {}", err)) error!("Failed to set a cover: {}", err)
.ok(); }
let link = pd.link().to_owned(); let link = pd.link().to_owned();
self.link.set_tooltip_text(Some(link.as_str())); self.link.set_tooltip_text(Some(link.as_str()));
self.link.connect_clicked(move |_| { self.link.connect_clicked(move |_| {
info!("Opening link: {}", &link); info!("Opening link: {}", &link);
open::that(&link) if let Err(err) = open::that(&link) {
.map_err(|err| error!("Error: {}", err)) error!("Failed to open link: {}", &link);
.map_err(|_| error!("Failed open link: {}", &link)) error!("Error: {}", err);
.ok(); }
}); });
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap(); let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
@@ -114,130 +98,42 @@ impl ShowWidget {
on_played_button_clicked( on_played_button_clicked(
pd.clone(), pd.clone(),
&episodes, &episodes,
&sender sender.clone()
) )
})); }));
self.settings.set_popover(&show_menu); 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. /// Set the show cover.
fn set_cover(&self, pd: &Arc<Podcast>) -> Result<(), Error> { fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
utils::set_image_from_path(&self.cover, pd.id(), 256) set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
} }
#[inline]
/// Set the descripton text. /// Set the descripton text.
fn set_description(&self, text: &str) { fn set_description(&self, text: &str) {
self.description.set_markup(&markup_from_raw(text)); 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. /// Set scrolled window vertical adjustment.
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> { pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
let guard = SHOW_WIDGET_VALIGNMENT self.scrolled_window.set_vadjustment(vadjustment)
.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
} }
} }
#[inline] fn on_unsub_button_clicked(
/// Populate the listbox with the shows episodes.
fn populate_listbox(
show: &Rc<ShowWidget>,
pd: Arc<Podcast>, pd: Arc<Podcast>,
unsub_button: &gtk::Button,
sender: Sender<Action>, sender: Sender<Action>,
) -> Result<(), Error> { ) -> 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. // hack to get away without properly checking for none.
// if pressed twice would panic. // if pressed twice would panic.
unsub_button.set_sensitive(false); unsub_button.hide();
let wrap = || -> Result<(), SendError<_>> {
sender.send(Action::RemoveShow(pd))?; sender.send(Action::RemoveShow(pd))?;
sender.send(Action::HeaderBarNormal)?; 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. // Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshShowsView)?; sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?; sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
wrap().map_err(|err| error!("Action Sender: {}", err)).ok(); Ok(())
unsub_button.set_sensitive(true);
} }
#[inline] fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::Frame, sender: Sender<Action>) {
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
if dim_titles(episodes).is_none() { if dim_titles(episodes).is_none() {
error!("Something went horribly wrong when dimming the titles."); error!("Something went horribly wrong when dimming the titles.");
warn!("RUN WHILE YOU STILL CAN!"); warn!("RUN WHILE YOU STILL CAN!");
} }
sender sender.send(Action::MarkAllPlayerNotification(pd)).unwrap();
.send(Action::MarkAllPlayerNotification(pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
} }
#[inline] pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?; dbqueries::update_none_to_played_now(pd)?;
// Not all widgets migth have been loaded when the mark_all is hit sender.send(Action::RefreshWidgetIfVis)?;
// 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::RefreshEpisodesView)?; sender.send(Action::RefreshEpisodesView)?;
Ok(()) 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: // 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(); }` // `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 // 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. // position relative to the EpisodeWidget container gtk::Box.
fn dim_titles(episodes: &gtk::ListBox) -> Option<()> { fn dim_titles(episodes: &gtk::Frame) -> Option<()> {
let children = episodes.get_children(); let listbox = episodes
.get_children()
.remove(0)
.downcast::<gtk::ListBox>()
.ok()?;
let children = listbox.get_children();
for row in children { for row in children {
let row = row.downcast::<gtk::ListBoxRow>().ok()?; let row = row.downcast::<gtk::ListBoxRow>().ok()?;
+1 -1
View File
@@ -16,7 +16,7 @@
</p> </p>
</description> </description>
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> --> <!-- <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" /> <category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
<programming-language>Rust</programming-language> <programming-language>Rust</programming-language>
+1 -1
View File
@@ -3,7 +3,7 @@
project( project(
'hammond', 'rust', 'hammond', 'rust',
version: '0.3.2', version: '0.3.1',
license: 'GPLv3', license: 'GPLv3',
) )
+2 -4
View File
@@ -28,9 +28,7 @@
"--share=network" "--share=network"
], ],
"env" : { "env" : {
"CARGO_HOME" : "/run/build/Hammond/cargo", "CARGO_HOME" : "/run/build/Hammond/cargo"
"RUST_BACKTRACE" : "1",
"RUSTFLAGS" : "--cfg rayon_unstable"
} }
}, },
"modules" : [ "modules" : [
@@ -40,7 +38,7 @@
"sources" : [ "sources" : [
{ {
"type" : "git", "type" : "git",
"url" : "https://gitlab.gnome.org/World/hammond.git", "url" : "https://gitlab.gnome.org/alatiera/Hammond.git",
"branch" : "master" "branch" : "master"
} }
] ]
+2
View File
@@ -11,3 +11,5 @@ condense_wildcard_suffixes = false
format_strings = true format_strings = true
normalize_comments = true normalize_comments = true
reorder_imports = true reorder_imports = true
reorder_imported_names = true
reorder_imports_in_group = true
-1
View File
@@ -1,6 +1,5 @@
#!/bin/sh #!/bin/sh
export CARGO_HOME=$1/target/cargo-home 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 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-gtk $DIST
cp -rf hammond-downloader $DIST cp -rf hammond-downloader $DIST
cp Cargo.toml $DIST cp Cargo.toml $DIST
cp Cargo.lock $DIST
cp configure $DIST cp configure $DIST
cp meson.build $DIST cp meson.build $DIST
cp Hammond.doap $DIST cp Hammond.doap $DIST