81 Commits
Author SHA1 Message Date
Jordan Petridis 13e47e9b4e Version bump. 2017-11-28 10:46:03 +02:00
Jordan Petridis 13751e3708 Tweak gresource initialization. 2017-11-28 10:27:43 +02:00
Jordan Petridis c92e40e3cf Run download cleaner on startup. Closes #3. 2017-11-28 10:23:19 +02:00
Jordan Petridis 2051f0f58e Enable some extra clippy warnings. 2017-11-28 09:50:30 +02:00
Jordan Petridis c07215d357 Set hammond-data to build with Deny missing_docs. 2017-11-28 09:44:26 +02:00
Jordan Petridis e4a7a7becf Add module Doc comments. 2017-11-28 09:38:05 +02:00
Jordan Petridis 7dd8755bcb Document hammond-data/src/lib.rs xdg_dirs module. 2017-11-28 09:24:12 +02:00
Jordan Petridis 76f9f36609 Document Source Model. 2017-11-28 09:01:38 +02:00
Jordan Petridis 4d669ef297 Document Podcast Model. 2017-11-28 08:40:27 +02:00
Jordan Petridis 3707b70b41 Document Episode Model. 2017-11-28 08:25:04 +02:00
Jordan Petridis d81f9c6ad7 Fixed stable unit tests. 2017-11-28 04:42:15 +02:00
Jordan Petridis 34e808182c Documented a bit hammond-data/src/feed.rs. 2017-11-27 06:06:14 +02:00
Jordan Petridis c4030e51df Added some doc comments in hammond-data/src/utils.rs. 2017-11-27 05:25:16 +02:00
Jordan Petridis cb65782829 Extended the NewModel builder and added a bunch of unit tests. 2017-11-27 04:32:21 +02:00
Jordan Petridis ee4f08c5cf Implemented a BuildPattern for NewModels. Swiched stuff to pub(crate).
Added a Constructor for NewModels in order to make their fields private.
Also added getter methods that are mainly used in Unit tests.
2017-11-26 23:30:56 +02:00
Jordan Petridis 9e03aaae27 Upgrade gtk-rs crates. 2017-11-26 21:25:46 +02:00
Jordan Petridis 866904a687 Use generic IntoIterator instead of &[foo] slices. 2017-11-26 07:03:21 +02:00
Jordan Petridis 9d0df9de46 Tweak cargo privacy settings. 2017-11-26 06:21:14 +02:00
Jordan Petridis 7eff93ecf1 Remove rand from the downloader. 2017-11-26 03:47:43 +02:00
Jordan Petridis 6336f7cac1 Moved update funtions into a Trait and impl for NewModels. 2017-11-26 00:56:38 +02:00
Jordan Petridis 39dff5e27a Moved insert functions into an Insert trait and implemented it for each NewModel. 2017-11-26 00:40:03 +02:00
Jordan Petridis ce3a76aee1 Update episode rows instead of replacing them. 2017-11-26 00:13:51 +02:00
Jordan Petridis 39cccf85a5 Change the ci config. 2017-11-25 18:50:32 +02:00
Jordan Petridis 2f801043c1 In place update a podcast row instead of replace. 2017-11-25 18:22:56 +02:00
Jordan Petridis f83f894e44 Replaced pool.get().unwrap() statements with get()?. 2017-11-25 03:29:06 +02:00
Jordan Petridis f0a0fdfa83 Added a truncate_db helper function. 2017-11-25 03:01:21 +02:00
Jordan Petridis 7498a37f68 Fixed unit test. 2017-11-25 02:19:16 +02:00
Jordan Petridis faab32ba17 Fix podcast flowbox_child title label. 2017-11-25 02:00:37 +02:00
Jordan Petridis 62da8bbb52 Use source_id as the unique identifieble field for the podcast table. 2017-11-25 01:56:10 +02:00
Jordan Petridis 12ffe5c231 Use a db transaction when deleting a feed. 2017-11-24 20:40:32 +02:00
Jordan Petridis d9ef4fed53 Truncate the temp db before running test_complete_index. Should close #17. 2017-11-24 20:31:10 +02:00
Jordan Petridis 4a195eb1b7 Moved set_played_now into a method of the Episode model. 2017-11-24 19:48:20 +02:00
Jordan Petridis f7f0a16858 Minor cleanup. 2017-11-24 19:28:34 +02:00
Jordan Petridis 91f4a30c01 Moved episode indexing into a transaction. 2017-11-23 21:30:17 +02:00
Jordan Petridis 5570fdd118 Switched to using r2d2 instead of a databaseConnection object. 2017-11-23 21:09:50 +02:00
Jordan Petridis aa7c493e81 hammond-data/src/feed.rs api changes. 2017-11-23 18:48:41 +02:00
Jordan Petridis a58671ea9a Moved database related things into their own module. 2017-11-22 21:54:09 +02:00
Jordan Petridis 02b27d33b6 Move XDG global variables into their own module. 2017-11-22 21:07:21 +02:00
Jordan Petridis 6098f0f54a Banner fix? Closes #14. 2017-11-22 10:44:32 +02:00
Jordan Petridis 3bcb23b39a Redefine NewModels to hold Option<String> instead of Option<&str>. 2017-11-22 09:53:52 +02:00
Jordan Petridis 31328355b1 Methods renaming, Api changes. 2017-11-22 06:25:34 +02:00
Jordan Petridis 9bc6df2cba Made Disel NewModels private. 2017-11-22 05:50:25 +02:00
Jordan Petridis ee7c5ca26e Use bail! instead of panic!. 2017-11-20 22:23:04 +02:00
Jordan Petridis 7be0b5bbf8 Added a url cleanr. Closes #4. 2017-11-20 22:06:29 +02:00
Jordan Petridis 5890fe2bb4 Moved all diesel queries into hammond-data/src/dbquerries.rs module. 2017-11-20 20:05:00 +02:00
Jordan Petridis 0ce4107be4 Revert back to using an Arc<Mutex<Connection>> instead of the pool. 2017-11-20 18:09:53 +02:00
Jordan Petridis ce29602431 I hate everything. 2017-11-20 16:57:27 +02:00
Jordan Petridis e07683c046 partially-fixed the tests and benchmarks. 2017-11-20 01:08:34 +02:00
Jordan Petridis 5ad52fa4b2 Refactored to remove from Database refferences. 2017-11-19 23:32:37 +02:00
Jordan Petridis beaeeffba8 Refactored dbquerries.
Refactored so dbquerries.rs module *should* be the only piece of code
that interacts with a Database connection. Insert stuff will be moved too.
2017-11-19 23:08:36 +02:00
Jordan Petridis 47b32ac000 Removed the Database type alias. 2017-11-19 22:39:51 +02:00
Jordan Petridis 55442529a8 Initial switch to using r2d2. 2017-11-19 22:06:10 +02:00
Jordan Petridis 052988a2b1 Update dependancies. 2017-11-19 18:14:56 +02:00
Jordan Petridis a6dbf65575 Move stuff from hammond_data/src/lib.rs into utils. 2017-11-18 20:55:19 +02:00
Jordan Petridis 563f249a48 Reverted index_channel items to the synchronous version.
Using a transaction yields ~1/3 better performance as of now.
2017-11-18 19:57:50 +02:00
Jordan Petridis 5c84b77434 Converted iter_mut into into_iter wherever possible. 2017-11-18 19:23:25 +02:00
Jordan Petridis 494761beaf Removed id function from diesel models.
It's automaticly declared upon derive identifiable,
And there's no need to overwrite it.
2017-11-18 15:23:40 +02:00
Jordan Petridis 1bf5187e48 Renamed hammond-data/src/feedparser.rs module into parser.rs. 2017-11-18 15:05:55 +02:00
Jordan Petridis 6894f33895 Renamed hammond-data/src/index_feeds.rs module into feed.rs. 2017-11-18 14:32:58 +02:00
Jordan Petridis 450643371c Renamed index_feeds and fetch_feeds functions. 2017-11-18 14:24:52 +02:00
Jordan Petridis 1ec4370c7b Moved Database type-alias into hammond-data/src/lib.rs. 2017-11-17 21:37:55 +02:00
Jordan Petridis e66a337468 Moved indexing episode into a NewEpisode method.
There is a performance reggresion caused by the introduction of
Arc<Mutex<Connection>> instead of Connection that should be refactored.

Also removed the db transcaction as it was incomplete.
2017-11-17 20:01:59 +02:00
Jordan Petridis 282a29e7dd Move some indexing functions into methods of insertable models. 2017-11-17 18:58:44 +02:00
Jordan Petridis 8eca46871d Moved insert_return functions into methods of insertable models. 2017-11-17 18:10:29 +02:00
Jordan Petridis 2152aa9034 Convert Feed struct-tuple into a plain struct. 2017-11-17 16:24:48 +02:00
Jordan Petridis 3d108d4a7a Make Feed fields private. 2017-11-17 16:12:11 +02:00
Jordan Petridis 54a0f17588 Initial draft of hammond-data/src/index_feed.rs API redesign. 2017-11-17 13:26:39 +02:00
Jordan Petridis 3c6176b1c0 Renamed hammond-data/src/dbcheckup.rs into utils.rs 2017-11-16 23:32:40 +02:00
Jordan Petridis e2403255cf Updated Readme and TODO. 2017-11-16 00:40:59 +02:00
Jordan Petridis cfb876e006 Split hammond-data/models into sub modules. 2017-11-15 17:42:22 +02:00
Jordan Petridis 118846f255 Added extra columns to podcast and episode tables. 2017-11-15 16:58:21 +02:00
Jordan Petridis 8b4701f1a1 Renamed Gresource path in order to be less confusing. 2017-11-15 14:55:26 +02:00
Jordan Petridis a286014a62 Refactored indexing functions of the Diesel models. 2017-11-14 17:56:28 +02:00
Jordan Petridis 6996759db5 Removed the file open syscall from the benchmark. 2017-11-14 15:02:32 +02:00
Jordan Petridis 58e11f17b7 Use bigger rss feeds for benchmarking. 2017-11-14 14:49:43 +02:00
Jordan Petridis edd694e7d6 Added initial feed indexing benchmark. 2017-11-14 14:09:13 +02:00
Jordan Petridis 03637903c6 Update dependancies. 2017-11-14 13:06:09 +02:00
Jordan Petridis ab002ebcd7 Kinda have copied the whole Contributing file from gnome-TODO. 2017-11-13 19:28:22 +02:00
Jordan Petridis 1e45adc034 Added some comments into the downloader module. 2017-11-13 18:48:37 +02:00
Jordan Petridis 4bf84ed170 Copied the boards layout and issue temlates from GNOME TODO 2017-11-13 17:37:44 +02:00
Jordan Petridis e3f0263dc0 Added appdata.xml and moved .desktop into hammond-gtk/resources. 2017-11-13 17:34:16 +02:00
51 changed files with 55063 additions and 1644 deletions
+4
View File
@@ -7,3 +7,7 @@ Cargo.lock
_build
vendor/
.flatpak-builder/
flatpak-build/
flatpak-repo/
repo/
Makefile
+6 -5
View File
@@ -14,8 +14,9 @@ before_script:
# kcov
# - apt-get install -y libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
# variables:
variables:
# RUSTFLAGS: "-C link-dead-code"
RUST_BACKTRACE: "FULL"
# Currently doesnt work.
# # Build with meson
@@ -43,8 +44,8 @@ test:stable:
image: "rust"
script:
- rustc --version && cargo --version
- cargo build --all
- cargo test --all --verbose
- cargo build
- cargo test --verbose -- --test-threads=1
test:nightly:
# Nightly
@@ -52,8 +53,8 @@ test:nightly:
image: "rustlang/rust:nightly"
script:
- rustc --version && cargo --version
- cargo build --all
- cargo test --all --verbose
- cargo build
- cargo test --verbose -- --test-threads=1
# - cargo bench
# Configure and run rustfmt on nightly
+20
View File
@@ -0,0 +1,20 @@
Detailed description of the issue. Put as much information as you can, potentially
with images showing the issue.
Steps to reproduce:
1. Open Hammond
2. Do an action
3. ...
## Design Tasks
* [ ] design tasks
## Development Tasks
* [ ] development tasks
## QA Tasks
* [ ] qa (quality assurance) tasks
+17
View File
@@ -0,0 +1,17 @@
Detailed description of the feature. Put as much information as you can.
Proposed Mockups:
(Add mockups of the proposed feature)
## Design Tasks
* [ ] design tasks
## Development Tasks
* [ ] development tasks
## QA Tasks
* [ ] qa (quality assurance) tasks
+45 -3
View File
@@ -1,7 +1,5 @@
## Contributing
Contributing
When contributing to the development of Hammond, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
@@ -19,9 +17,53 @@ Quick setup
It is recommended to add a pre-commit hook to run cargo test and cargo fmt
```
#!/bin/sh
cargo test --all && cargo fmt --all -- --write-mode=diff
cargo test -- --test-threads=1 && cargo fmt --all -- --write-mode=diff
```
# Issues, issues and more issues!
There are many ways you can contribute to Hammond, and all of them involve creating issues
in [Hammond issue tracker](https://gitlab.gnome.org/alatiera/Hammond/issues). This is the
entry point for your contribution.
To create an effective and high quality ticket, try to put the following information on your
ticket:
1. A detailed description of the issue or feature request
- For issues, please add the necessary steps to reproduce the issue.
- For feature requests, add a detailed description of your proposal.
2. A checklist of Development tasks
3. A checklist of Design tasks
4. A checklist of QA tasks
## Issue template
```
[Title of the issue or feature request]
Detailed description of the issue. Put as much information as you can, potentially
with images showing the issue or mockups of the proposed feature.
If it's an issue, add the steps to reproduce like this:
Steps to reproduce:
1. Open Hammond
2. Do an Action
3. ...
## Design Tasks
* [ ] design tasks
## Development Tasks
* [ ] development tasks
## QA Tasks
* [ ] qa (quality assurance) tasks
```
## Pull Request Process
1. Ensure your code compiles. Run `make` before creating the pull request.
Generated
+236 -212
View File
@@ -22,17 +22,22 @@ dependencies = [
[[package]]
name = "ansi_term"
version = "0.9.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "antidote"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "atk-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -50,7 +55,7 @@ dependencies = [
[[package]]
name = "backtrace"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -90,6 +95,11 @@ name = "bitflags"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "build_const"
version = "0.2.0"
@@ -116,20 +126,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cairo-rs"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"c_vec 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cairo-sys-rs"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -161,7 +171,7 @@ name = "coco"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -184,18 +194,12 @@ dependencies = [
[[package]]
name = "crc"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crc-core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crc-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "crypt32-sys"
version = "0.2.0"
@@ -253,7 +257,7 @@ dependencies = [
[[package]]
name = "diesel"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
source = "git+https://github.com/diesel-rs/diesel.git#07f80c3a0d07daa26efff3166fbf0297dc0f0a7b"
dependencies = [
"byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -262,10 +266,10 @@ dependencies = [
[[package]]
name = "diesel_codegen"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
source = "git+https://github.com/diesel-rs/diesel.git#07f80c3a0d07daa26efff3166fbf0297dc0f0a7b"
dependencies = [
"diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_infer_schema 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"diesel_infer_schema 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"dotenv 0.10.1 (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)",
@@ -274,9 +278,9 @@ dependencies = [
[[package]]
name = "diesel_infer_schema"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
source = "git+https://github.com/diesel-rs/diesel.git#07f80c3a0d07daa26efff3166fbf0297dc0f0a7b"
dependencies = [
"diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
]
[[package]]
@@ -305,7 +309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -321,7 +325,7 @@ name = "error-chain"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -329,7 +333,7 @@ name = "error-chain"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -378,158 +382,158 @@ dependencies = [
[[package]]
name = "gdk"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pango 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gdk-pixbuf"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gdk-pixbuf-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gdk-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gio"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gio-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "glib"
version = "0.3.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "glib-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gobject-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gtk"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gtk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pango 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gtk-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -538,18 +542,21 @@ name = "hammond-data"
version = "0.1.0"
dependencies = [
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"diesel_codegen 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2-diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.8.1 (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.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -557,13 +564,12 @@ dependencies = [
name = "hammond-downloader"
version = "0.1.0"
dependencies = [
"diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hammond-data 0.1.0",
"hyper 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -572,16 +578,18 @@ dependencies = [
name = "hammond-gtk"
version = "0.1.0"
dependencies = [
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"diesel_codegen 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"dissolve 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"gtk 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hammond-data 0.1.0",
"hammond-downloader 0.1.0",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"loggerv 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"loggerv 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -605,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hyper"
version = "0.11.6"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -616,14 +624,14 @@ dependencies = [
"language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.0 (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.0 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (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)",
"unicase 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -632,10 +640,10 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (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.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -680,7 +688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazy_static"
version = "0.2.9"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -700,7 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crc 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crc 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -719,10 +727,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "loggerv"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -771,7 +779,7 @@ name = "mime"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicase 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -830,7 +838,7 @@ name = "native-tls"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"openssl 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)",
"schannel 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -896,19 +904,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "openssl"
version = "0.9.20"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl-sys"
version = "0.9.20"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -919,32 +927,32 @@ dependencies = [
[[package]]
name = "pango"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pango-sys"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "percent-encoding"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -1007,6 +1015,25 @@ name = "quote"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "r2d2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"scheduled-thread-pool 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "r2d2-diesel"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)",
"r2d2 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.3.18"
@@ -1021,7 +1048,7 @@ name = "rayon"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon-core 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1031,7 +1058,7 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1082,17 +1109,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libflate 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 2.0.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.6 (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.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1104,7 +1131,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (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.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1127,14 +1154,6 @@ name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rustc_version"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "safemem"
version = "0.2.0"
@@ -1148,12 +1167,20 @@ dependencies = [
"advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "scoped-tls"
version = "0.1.0"
@@ -1193,25 +1220,20 @@ dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "semver"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.18"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1221,7 +1243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1251,10 +1273,10 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
"precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.21 (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)",
]
@@ -1331,7 +1353,7 @@ name = "thread_local"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1358,12 +1380,12 @@ dependencies = [
"mio 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
"scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tokio-io"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1384,7 +1406,7 @@ dependencies = [
"smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1404,7 +1426,7 @@ dependencies = [
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1417,10 +1439,10 @@ dependencies = [
[[package]]
name = "unicase"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1464,7 +1486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1531,41 +1553,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6cbd0b9af8587c72beadc9f72d35b9fbb070982c9e6203e46e93f10df25f8f45"
"checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a"
"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699"
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c69658a4e18d5c9575f716e24559645d08a4044d6946c30c2e0025952c84d842"
"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455"
"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
"checksum atk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "33a67fd81e1922dddc335887516f2f5254534e89c9d39fa89bca5d79bd150d34"
"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860"
"checksum backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "99f2ce94e22b8e664d95c57fff45b98a966c2252b60691d0b7aeeccd88d70983"
"checksum backtrace 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8709cc7ec06f6f0ae6c2c7e12f6ed41540781f72b488d83734978295ceae182e"
"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661"
"checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
"checksum build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e90dc84f5e62d2ebe7676b83c22d33b6db8bd27340fb6ffbff0a364efa0cb9c9"
"checksum byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff81738b726f5d099632ceaffe7fb65b90212e8dce59d518729e7e8634032d3d"
"checksum bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d828f97b58cc5de3e40c421d0cf2132d6b2da4ee0e11b8632fa838f0f9333ad6"
"checksum c_vec 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6237ac5a4b1e81c213c24c6437964c61e646df910a914b4ab1487b46df20bd13"
"checksum cairo-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9d336f1b2ff46c17475a14360de7f456707008da475c54824887e52e453ab00"
"checksum cairo-sys-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9e8a1e2a76ac09b959788c2c30a355d693ce6f7f7d7268f6d1dd5d8c3359c521"
"checksum cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6b5695f59fd036fe5741bc5a4eb20c78fbe42256e3b08a2af26bbcbe8070bf3"
"checksum cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c6e18fecaeac51809db57f45f4553cc0975225a7eb435a7a7e91e5e8113a84d"
"checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c20ebe0b2b08b0aeddba49c609fe7957ba2e33449882cb186a180bc60682fa9"
"checksum coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c06169f5beb7e31c7c67ebf5540b8b472d23e3eade3b2ec7d1f5b504a85f91bd"
"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67"
"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d"
"checksum crc 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fba69ea0e15e720f7e1cfe1cf3bc55007fbd41e32b8ae11cfa343e7e5961e79a"
"checksum crc-core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "003d1170779d405378223470f5864b41b79a91969be1260e4de7b4ec069af69c"
"checksum crc 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "64d4a687c40efbc7d376958117b34d5f1cece11709110a742405bf58e7a34f00"
"checksum crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34988f7e069e0b2f3bfc064295161e489b2d4e04a2e4248fb94360cdf00b4ec"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
"checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3"
"checksum derive-error-chain 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9ca9ade651388daad7c993f005d0d20c4f6fe78c1cdc93e95f161c6f5ede4a"
"checksum derive_builder 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "03600ae366b6eb2314e54d62adc833d9866da03798acc61c61789654ceaa227a"
"checksum derive_builder_core 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eed37eae64daa5511467b1a55cebdf472deeaef108d22f62f25e8bbcaffd56ac"
"checksum diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "304226fa7a3982b0405f6bb95dd9c10c3e2000709f194038a60ec2c277150951"
"checksum diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a42ca5c9b660add51d58bc5a50a87123380e1e458069c5504528a851ed7384"
"checksum diesel_infer_schema 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bf1957ff5cd3b04772e43c162c2f69c2aa918080ff9b020276792d236be8be52"
"checksum diesel 0.16.0 (git+https://github.com/diesel-rs/diesel.git)" = "<none>"
"checksum diesel_codegen 0.16.0 (git+https://github.com/diesel-rs/diesel.git)" = "<none>"
"checksum diesel_infer_schema 0.16.0 (git+https://github.com/diesel-rs/diesel.git)" = "<none>"
"checksum dissolve 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "898542be4716d992082c8e4fc331b792d626cfa71cb2b4790f828b9a8f921a90"
"checksum dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d6f0e2bb24d163428d8031d3ebd2d2bd903ad933205a97d0f18c7c1aade380f3"
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e311a7479512fbdf858fb54d91ec59f3b9f85bc0113659f46bba12b199d273ce"
"checksum either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3"
"checksum encoding_rs 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f5215aabf22b83153be3ee44dfe3f940214541b2ce13d419c55e7a115c8c51a9"
"checksum error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8"
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
@@ -1575,33 +1598,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum futf 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "51f93f3de6ba1794dcd5810b3546d004600a59a98266487c8407bc4b24e398f3"
"checksum futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "118b49cac82e04121117cbd3121ede3147e885627d82c4546b87c702debb90c1"
"checksum futures-cpupool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e86f49cc0d92fe1b97a5980ec32d56208272cbb00f15044ea9e2799dde766fdf"
"checksum gdk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f30018ecbbb1e6f1d59c4024ec08675850744b799abc5420be0629ac9ba0abd2"
"checksum gdk-pixbuf 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "caf05dab73febcc6e90abaff8f24cfe1cf1bd2222cd648ddfe337bf3b994489f"
"checksum gdk-pixbuf-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "85eb441420653b33e5a29d13227ea34995383e65bf4f33b16492ec95e44a8996"
"checksum gdk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "679d86da2a4522a623e3ce4b67f2126a12e057a1f7269eee7028199f78b5a854"
"checksum gio 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b7bc3126c94109e65871e4228b990d1ea2953259483d5b06eb96e8b36a7bf196"
"checksum gio-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "189969f8189604c371d42b613d928c9d17fcfbf6e175d6b0ce9475a950f76dc6"
"checksum glib 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d4da1d7f4bdc5c708d8ce4df1ac440dcb2f9d97d937c989032185a48aeef1d10"
"checksum glib-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd7d911c5dc610aabe37caae7d3b9d2cfe6d8f4c85ff4c062f3d6f490e75067"
"checksum gobject-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "edc95561e538381576425264a4ddd08c65d5da218f10b2a47b4479dd147775da"
"checksum gtk 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce17f98e7dcdc9d06b3a5f7621d796a24937c04953481205b1be267c5a02697a"
"checksum gtk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "854b56ce6d6b05945f7735651482835c5ac1f8582142ce67306726259a3dafb0"
"checksum gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e51db95be6565011bcd5cd99f9b17fdd585001057a999b21e09f1e8c28deb9"
"checksum gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "16160d212ae91abe9f3324c3fb233929ba322dde63585d15cda3336f8c529ed1"
"checksum gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "798f97101eea8180da363d0e80e07ec7ec6d1809306601c0100c1de5bc8b4f52"
"checksum gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4ee916f5f25c5f4b21bd9dcb12a216ae697406940ff9476358c308a8ececada"
"checksum gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "84ba5a2beb559059a0c9c2bd3681743cdede8d9a36c775840bca800333b22867"
"checksum gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a303bbf7a5e75ab3b627117ff10e495d1b9e97e1d68966285ac2b1f6270091bc"
"checksum glib 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "450247060df7d52fdad31e1d66f30d967e925c9d1d26a0ae050cfe33dcd00d08"
"checksum glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9693049613ff52b93013cc3d2590366d8e530366d288438724b73f6c7dc4be8"
"checksum gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60d507c87a71b1143c66ed21a969be9b99a76df234b342d733e787e6c9c7d7c2"
"checksum gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0847c507e52c1feaede13ef56fb4847742438602655449d5f1f782e8633f146f"
"checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f"
"checksum html5ever 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba3a1fd1857a714d410c191364c5d7bf8a6487c0ab5575146d37dd7eb17ef523"
"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
"checksum hyper 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b45eac8b696d59491b079bd04fcb0f3488c0f6ed62dcb36bcfea8a543e9cdc3"
"checksum hyper 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4959ca95f55df4265bff2ad63066147255e6fa733682cf6d1cb5eaff6e53324b"
"checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985"
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6e8b9c2247fcf6c6a1151f1156932be5606c9fd6f55a2d7f9fc1cb29386b2f7"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9e5e58fa1a4c3b915a561a78a22ee0cac6ab97dca2504428bc1cb074375f8d5"
"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
"checksum lazycell 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3b585b7a6811fb03aa10e74b278a0f00f8dd9b45dc681f148bb29fa5cb61859b"
"checksum libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "5ba3df4dcb460b9dfbd070d41c94c19209620c191b0340b929ce748a2bcd42d2"
"checksum libflate 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ae46bcdafa496981e996e57c5be82c0a7f130a071323764c6faa4803619f1e67"
"checksum libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "370090ad578ba845a3ad4f383ceb3deba7abd51ab1915ad1f2c982cc6035e31c"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
"checksum loggerv 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "316cb96c1a0fab30079d5c2e5ebb52c3076e2ff9a9dd25ea8aa6646198f9af0f"
"checksum loggerv 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5bb7c9e1ed99294067005cc7d4413441197ba354e8286c36b72a66a557c13661"
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
"checksum markup5ever 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2cf89d3e0486c32c9d99521455ddf9a438910a1ce2bd376936086edc15dff5fc"
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
@@ -1620,11 +1643,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
"checksum open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c281318d992e4432cfa799969467003d05921582a7489a8325e37f8a450d5113"
"checksum openssl 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)" = "8bf434ff6117485dc16478d77a4f5c84eccc9c3645c4da8323b287ad6a15a638"
"checksum openssl-sys 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)" = "0ad395f1cee51b64a8d07cc8063498dc7554db62d5f3ca87a67f4eed2791d0c8"
"checksum pango 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5afa4b4c5380315b12075e7767d9bdd62d53beeb6087d9287ef6990e57a6b643"
"checksum pango-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6ec8d90306b5ff43f5836f4363267ea95be02b3df71d2b31ba8fbb1680bdee1"
"checksum percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de154f638187706bde41d9b4738748933d64e6b37bdbffc0b47a97d16a6ae356"
"checksum openssl 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)" = "2225c305d8f57001a0d34263e046794aa251695f20773102fbbfeb1e7b189955"
"checksum openssl-sys 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)" = "92867746af30eea7a89feade385f7f5366776f1c52ec6f0de81360373fa88363"
"checksum pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e81c404ab81ea7ea2fc2431a0a7672507b80e4b8bf4b41eac3fc83cc665104e"
"checksum pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34f34a1be107fe16abb2744e0e206bee4b3b07460b5fddd3009a6aaf60bd69ab"
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"
"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03"
@@ -1633,6 +1656,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
"checksum quick-xml 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)" = "19a3a610544419c527d5f51ae1a6ae3db533e25c117d3eed8fce6434f70c5e95"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum r2d2 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2c8284508b38df440f8f3527395e23c4780b22f74226b270daf58fee38e4bcce"
"checksum r2d2-diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f6b921696a6c45991296d21b52ed973b9fb56f6c47524fda1f99458c2d6c0478"
"checksum rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6475140dfd8655aeb72e1fd4b7a1cc1c202be65d71669476e392fe62532b9edd"
"checksum rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed02d09394c94ffbdfdc755ad62a132e94c3224a8354e78a1200ced34df12edf"
"checksum rayon-core 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e64b609139d83da75902f88fd6c01820046840a18471e4dfcd5ac7c0f46bea53"
@@ -1646,17 +1671,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum rss 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "70294b2be2d620fed3939032067684c53b8ccae18e8ca0b8410447f0f07228c5"
"checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
"checksum schannel 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7554288337c1110e34d7a2433518d889374c1de1a45f856b7bcddb03702131fc"
"checksum scheduled-thread-pool 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d9fbe48ead32343b76f544c85953bf260ed39219a8bbbb62cd85f6a00f9644f"
"checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f412dfa83308d893101dd59c10d6fda8283465976c28c287c5c855bf8d216bc"
"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 semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac"
"checksum serde 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "395993cac4e3599c7c1b70a6a92d3b3f55f4443df9f0b5294e362285ad7c9ecb"
"checksum serde_json 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1e67ce320daa7e494c578e34d4b00689f23bb94512fe0ca0dfaf02ea53fb67"
"checksum serde 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6eda663e865517ee783b0891a3f6eb3a253e0b0dabb46418969ee9635beadd9e"
"checksum serde_json 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e4586746d1974a030c48919731ecffd0ed28d0c40749d0d18d43b3a7d6c9b20e"
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
@@ -1674,12 +1698,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
"checksum time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d788d3aa77bc0ef3e9621256885555368b47bd495c13dd2e7413c89f845520"
"checksum tokio-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "c843a027f7c1df5f81e7734a0df3f67bf329411781ebf36393ce67beef6071e3"
"checksum tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ab83e7adb5677e42e405fa4ceff75659d93c4d7d7dd22f52fcec59ee9f02af"
"checksum tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "514aae203178929dbf03318ad7c683126672d4d96eccb77b29603d33c9e25743"
"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162"
"checksum tokio-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d88e411cac1c87e405e4090be004493c5d8072a370661033b1a64ea205ec2e13"
"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
"checksum unicase 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e01da42520092d0cd2d6ac3ae69eb21a22ad43ff195676b86f8c37f487d6b80"
"checksum unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284b6d3db520d67fbe88fd778c21510d1b0ba4a551e5d0fbb023d33405f6de8a"
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
+5
View File
@@ -7,3 +7,8 @@ members = [
[profile.release]
debug = false
[patch.crates-io]
diesel = { git = "https://github.com/diesel-rs/diesel.git" }
diesel_infer_schema = { git = "https://github.com/diesel-rs/diesel.git" }
diesel_codegen = { git = "https://github.com/diesel-rs/diesel.git" }
+24 -12
View File
@@ -1,5 +1,5 @@
# Hammond
## Multithreaded, safe, and reliable Gtk+ Podcast client.
## Multithreaded and reliable Gtk+ Podcast client.
This is a prototype of a podcast client written in Rust.
[![pipeline status](https://gitlab.gnome.org/alatiera/Hammond/badges/master/pipeline.svg)](https://gitlab.gnome.org/alatiera/Hammond/commits/master)
@@ -46,7 +46,7 @@ Flatpak instructions... Soon™.
### Dependancies
* Rust stable 1.21 or later.
* Rust stable 1.22 or later.
* Gtk+ 3.22 or later
* Meson
@@ -63,12 +63,34 @@ dnf install -y gtk3-devel glib2-devel openssl-devel sqlite-devel meson
```
If you happen to build it on other distributions please let me know the names of the corresponding libraries. Feel free to open a PR or an Issue to note it.
```sh
git clone https://gitlab.gnome.org/alatiera/Hammond.git
cd Hammond/
cargo build --all
```
## Call for designers
Currently there no design plans or mockups. They are highly needed in order to advance the Gtk Client.
There is the will for a complete client re-write if a someone contributes the mockups.
If you happen to be a designer and want to contribute please hope on [#hammond](https://docs.python.org/3/library/exceptions.html) and get in touch with us.
## Contributing
There alot of thins yet to be done.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
You can find start by taking a look at [Issues](https://gitlab.gnome.org/alatiera/Hammond/issues) or Opening a [New one](https://gitlab.gnome.org/alatiera/Hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
[contribution-guidelines]: https://gitlab.gnome.org/alatiera/Hammond/blob/master/CONTRIBUTING.md
## Overview
```sh
@@ -90,16 +112,6 @@ $ tree -d
│   └── widgets # Contains custom widgets such as Podcast and Episode.
```
## Contributing
There alot of thins yet to be done.
You can find start by taking a look at [Issues](https://gitlab.gnome.org/alatiera/Hammond/issues) or Opening a [New one](https://gitlab.gnome.org/alatiera/Hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
You may also want to take a look at [TODO.md](https://gitlab.gnome.org/alatiera/Hammond/blob/master/TODO.md) or grep the source code for `TODO:` and `FIXME:` tags.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
[contribution-guidelines]: https://gitlab.gnome.org/GNOME/gnome-todo/blob/master/CONTRIBUTING.md
## A note about the project's name
The project was named after Allan Moore's character [Evey Hammond](https://en.wikipedia.org/wiki/Evey_Hammond) from the graphic novel V for Vendetta.
+23 -44
View File
@@ -2,61 +2,26 @@
**General:**
- [x] Add CONTRIBUTING.md
- [ ] Add Issues and Pull Request templates
- [ ] Write docs
## Planned Features
## Priorities:
**Would be nice:**
- [x] Use GResource for assets like the banner.png etc, instead of hardcoded paths.
- [x] Possibly Convert description labels to textview.
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
- [ ] Re-design EpisodeWidget.
- [ ] Lazy evaluate episode loading based on the podcast_widget's view scrolling.
- [ ] Headerbar back button and stack switching
- [x] New episode notifier on podcast_flowbox_child, like the one vocal has
- [ ] Polish the flowbox_child banner.
- [x] Update on startup
**Unhack stuff:**
- [ ] Url sanitization
- [x] Fix downloader .ext parsing
**FIXME:**
- [x] Fix Flowbox child activation. [#1](https://gitlab.gnome.org/alatiera/Hammond/issues/1)
- [ ] Fix Etag/Last-modified implementation. [#2](https://gitlab.gnome.org/alatiera/Hammond/issues/2)
**DB changes:**
- [x] episodes: add watched field
- [x] Podcast deletion
- [x] Download cleaner
## Secondary:
- [ ] Discuss and decide when to schedule the download cleaner. [#3](https://gitlab.gnome.org/alatiera/Hammond/issues/3)
- [ ] Unplayed Only and Downloaded only view.
- [ ] Auto-updater
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] OPML import/export // Probably need to create a crate.
**DB changes:**
**Proper Desing Mockups for the Gtk Client:**
- [ ] Mark episodes/podcast for archival
- [ ] Mark stuff as Favorite. Maybe auto-archive favorites?
- [ ] Re-design EpisodeWidget.
- [ ] Re-design PodcastWidget.
- [ ] Polish the flowbox_child banner.
## Second:
## Third:
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] Notifications
- [ ] Episode queue
- [ ] Embedded player
@@ -64,8 +29,22 @@
- [ ] Search Implementation
## Fourth:
## Third:
- [ ] Download Queue
- [ ] Ability to Stream content on demand
- [ ] soundcloud and itunes feeds // [This](http://getrssfeed.com) seems intresting.
- [ ] Integrate with Itunes API for various crap
- [ ] YoutubeFeeds
- [ ] YoutubeFeeds
## Rest Tasks
**Would be nice:**
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
- [ ] Lazy evaluate episode loading based on the podcast_widget's view scrolling.
- [ ] Headerbar back button and stack switching
**FIXME:**
- [ ] Fix Etag/Last-modified implementation. [#2](https://gitlab.gnome.org/alatiera/Hammond/issues/2)
+16 -13
View File
@@ -5,25 +5,28 @@ version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4"
chrono = "0.4.0"
dotenv = "0.10.1"
error-chain = "0.11"
lazy_static = "0.2"
log = "0.3"
rayon = "0.9"
reqwest = "0.8"
rfc822_sanitizer = "0.3"
rss = "1.1"
xdg = "2.1"
error-chain = "0.11.0"
lazy_static = "0.2.11"
log = "0.3.8"
r2d2 = "0.7.4"
r2d2-diesel = "0.16.0"
rayon = "0.9.0"
reqwest = "0.8.1"
rfc822_sanitizer = "0.3.3"
rss = "1.1.0"
url = "1.6.0"
xdg = "2.1.0"
[dependencies.diesel]
features = ["sqlite"]
version = "0.16"
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.diesel_codegen]
features = ["sqlite"]
version = "0.16"
git = "https://github.com/diesel-rs/diesel.git"
[dev-dependencies]
rand = "0.3"
tempdir = "0.3"
rand = "0.3.18"
tempdir = "0.3.5"
+66
View File
@@ -0,0 +1,66 @@
#![feature(test)]
extern crate diesel;
extern crate hammond_data;
extern crate rand;
extern crate rayon;
extern crate rss;
extern crate tempdir;
extern crate test;
use rayon::prelude::*;
use test::Bencher;
use hammond_data::Source;
use hammond_data::feed::{index, Feed};
use std::io::BufReader;
// Big rss feed
const PCPER: &[u8] = include_bytes!("feeds/pcpermp3.xml");
const UNPLUGGED: &[u8] = include_bytes!("feeds/linuxunplugged.xml");
const RADIO: &[u8] = include_bytes!("feeds/coderradiomp3.xml");
const SNAP: &[u8] = include_bytes!("feeds/techsnapmp3.xml");
const LAS: &[u8] = include_bytes!("feeds/TheLinuxActionShow.xml");
static URLS: &[(&[u8], &str)] = &[
(PCPER, "https://www.pcper.com/rss/podcasts-mp3.rss"),
(UNPLUGGED, "http://feeds.feedburner.com/linuxunplugged"),
(RADIO, "https://feeds.feedburner.com/coderradiomp3"),
(SNAP, "https://feeds.feedburner.com/techsnapmp3"),
(LAS, "https://feeds2.feedburner.com/TheLinuxActionShow"),
];
fn index_urls() {
let feeds: Vec<_> = URLS.par_iter()
.map(|&(buff, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(buff)).unwrap();
Feed::from_channel_source(chan, s)
})
.collect();
index(feeds);
}
#[bench]
fn bench_index_feeds(b: &mut Bencher) {
b.iter(|| {
index_urls();
});
}
#[bench]
fn bench_index_unchanged_feeds(b: &mut Bencher) {
// Index first so it will only bench the comparison test case.
index_urls();
b.iter(|| {
for _ in 0..10 {
index_urls();
}
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,31 +4,36 @@
-- in order to change the db schema.
CREATE TABLE `source` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`uri` TEXT NOT NULL UNIQUE,
`last_modified` TEXT,
`http_etag` TEXT
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`uri` TEXT NOT NULL UNIQUE,
`last_modified` TEXT,
`http_etag` TEXT
);
CREATE TABLE `episode` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT,
`uri` TEXT NOT NULL UNIQUE,
`local_uri` TEXT,
`description` TEXT,
`published_date` TEXT ,
`epoch` INTEGER NOT NULL DEFAULT 0,
`length` INTEGER,
`guid` TEXT,
`played` INTEGER ,
`podcast_id` INTEGER NOT NULL
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT,
`uri` TEXT NOT NULL UNIQUE,
`local_uri` TEXT,
`description` TEXT,
`published_date` TEXT,
`epoch` INTEGER NOT NULL DEFAULT 0,
`length` INTEGER,
`guid` TEXT,
`played` INTEGER,
`favorite` INTEGER NOT NULL DEFAULT 0,
`archive` INTEGER NOT NULL DEFAULT 0,
`podcast_id` INTEGER NOT NULL
);
CREATE TABLE `podcast` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL UNIQUE,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`source_id` INTEGER NOT NULL
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`favorite` INTEGER NOT NULL DEFAULT 0,
`archive` INTEGER NOT NULL DEFAULT 0,
`always_dl` INTEGER NOT NULL DEFAULT 0,
`source_id` INTEGER NOT NULL UNIQUE
);
+77
View File
@@ -0,0 +1,77 @@
use r2d2_diesel::ConnectionManager;
use diesel::prelude::*;
use r2d2;
use std::path::PathBuf;
use std::sync::Arc;
use std::io;
use std::time::Duration;
use errors::*;
#[cfg(not(test))]
use xdg_dirs;
type Pool = Arc<r2d2::Pool<ConnectionManager<SqliteConnection>>>;
embed_migrations!("migrations/");
lazy_static!{
static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap());
}
#[cfg(not(test))]
lazy_static! {
static ref DB_PATH: PathBuf = xdg_dirs::HAMMOND_XDG.place_data_file("hammond.db").unwrap();
}
#[cfg(test)]
extern crate tempdir;
#[cfg(test)]
lazy_static! {
static ref TEMPDIR: tempdir::TempDir = {
tempdir::TempDir::new("hammond_unit_test").unwrap()
};
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
}
pub(crate) fn connection() -> Pool {
Arc::clone(&POOL)
}
fn init_pool(db_path: &str) -> Pool {
let config = r2d2::Config::builder()
.pool_size(1)
.connection_timeout(Duration::from_secs(60))
.build();
let manager = ConnectionManager::<SqliteConnection>::new(db_path);
let pool = Arc::new(r2d2::Pool::new(config, manager).expect("Failed to create pool."));
{
let db = Arc::clone(&pool).get().expect("Failed to initialize pool.");
run_migration_on(&*db).expect("Failed to run migrations during init.");
}
info!("Database pool initialized.");
pool
}
fn run_migration_on(connection: &SqliteConnection) -> Result<()> {
info!("Running DB Migrations...");
// embedded_migrations::run(connection)?;
embedded_migrations::run_with_output(connection, &mut io::stdout())?;
Ok(())
}
/// Reset the database into a clean state.
// Test share a Temp file db.
#[allow(dead_code)]
pub fn truncate_db() -> Result<()> {
let db = connection();
let con = db.get()?;
con.execute("DELETE FROM episode")?;
con.execute("DELETE FROM podcast")?;
con.execute("DELETE FROM source")?;
Ok(())
}
-95
View File
@@ -1,95 +0,0 @@
use rayon::prelude::*;
use errors::*;
use dbqueries;
use index_feed::Database;
use models::Episode;
use chrono::prelude::*;
use std::path::Path;
use std::fs;
use std::sync::Arc;
// TODO: Write unit test.
fn download_checker(db: &Database) -> Result<()> {
let mut episodes = {
let tempdb = db.lock().unwrap();
dbqueries::get_downloaded_episodes(&tempdb)?
};
episodes.par_iter_mut().for_each(|ep| {
if !Path::new(ep.local_uri().unwrap()).exists() {
ep.set_local_uri(None);
let res = ep.save(&Arc::clone(db));
if let Err(err) = res {
error!("Error while trying to update episode: {:#?}", ep);
error!("Error: {}", err);
};
}
});
Ok(())
}
// TODO: Write unit test.
fn played_cleaner(db: &Database) -> Result<()> {
let mut episodes = {
let tempdb = db.lock().unwrap();
dbqueries::get_played_episodes(&tempdb)?
};
let now_utc = Utc::now().timestamp() as i32;
episodes.par_iter_mut().for_each(|mut ep| {
if ep.local_uri().is_some() && ep.played().is_some() {
let played = ep.played().unwrap();
// TODO: expose a config and a user set option.
let limit = played + 172_800; // add 2days in seconds
if now_utc > limit {
let e = delete_local_content(&Arc::clone(db), &mut ep);
if let Err(err) = e {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("Error: {}", err);
} else {
info!("Episode {:?} was deleted succesfully.", ep.title());
};
}
}
});
Ok(())
}
// TODO: Write unit test.
pub fn delete_local_content(db: &Database, ep: &mut Episode) -> Result<()> {
if ep.local_uri().is_some() {
let uri = ep.local_uri().unwrap().to_owned();
if Path::new(&uri).exists() {
let res = fs::remove_file(&uri);
if res.is_ok() {
ep.set_local_uri(None);
ep.save(db)?;
} else {
error!("Error while trying to delete file: {}", uri);
error!("Error: {}", res.unwrap_err());
};
}
} else {
error!(
"Something went wrong evaluating the following path: {:?}",
ep.local_uri(),
);
}
Ok(())
}
pub fn set_played_now(db: &Database, ep: &mut Episode) -> Result<()> {
let epoch = Utc::now().timestamp() as i32;
ep.set_played(Some(epoch));
ep.save(db)?;
Ok(())
}
pub fn run(db: &Database) -> Result<()> {
download_checker(db)?;
played_cleaner(db)?;
Ok(())
}
+109 -71
View File
@@ -1,168 +1,206 @@
//! Random CRUD helper functions.
use diesel::prelude::*;
use diesel;
use models::{Episode, Podcast, Source};
use index_feed::Database;
use models::queryables::{Episode, Podcast, Source};
use chrono::prelude::*;
use errors::*;
/// Random db querries helper functions.
/// Probably needs cleanup.
use database::connection;
pub fn get_sources(con: &SqliteConnection) -> QueryResult<Vec<Source>> {
pub fn get_sources() -> Result<Vec<Source>> {
use schema::source::dsl::*;
source.load::<Source>(con)
let db = connection();
let con = db.get()?;
Ok(source.load::<Source>(&*con)?)
}
pub fn get_podcasts(con: &SqliteConnection) -> QueryResult<Vec<Podcast>> {
pub fn get_podcasts() -> Result<Vec<Podcast>> {
use schema::podcast::dsl::*;
podcast.load::<Podcast>(con)
let db = connection();
let con = db.get()?;
Ok(podcast.load::<Podcast>(&*con)?)
}
pub fn get_episodes(con: &SqliteConnection) -> QueryResult<Vec<Episode>> {
pub fn get_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
episode.order(epoch.desc()).load::<Episode>(con)
let db = connection();
let con = db.get()?;
Ok(episode.order(epoch.desc()).load::<Episode>(&*con)?)
}
pub fn get_downloaded_episodes(con: &SqliteConnection) -> QueryResult<Vec<Episode>> {
pub fn get_downloaded_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
episode.filter(local_uri.is_not_null()).load::<Episode>(con)
let db = connection();
let con = db.get()?;
Ok(episode
.filter(local_uri.is_not_null())
.load::<Episode>(&*con)?)
}
pub fn get_played_episodes(con: &SqliteConnection) -> QueryResult<Vec<Episode>> {
pub fn get_played_episodes() -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
episode.filter(played.is_not_null()).load::<Episode>(con)
let db = connection();
let con = db.get()?;
Ok(episode.filter(played.is_not_null()).load::<Episode>(&*con)?)
}
pub fn get_episode_from_id(con: &SqliteConnection, ep_id: i32) -> QueryResult<Episode> {
pub fn get_episode_from_id(ep_id: i32) -> Result<Episode> {
use schema::episode::dsl::*;
episode.filter(id.eq(ep_id)).get_result::<Episode>(con)
let db = connection();
let con = db.get()?;
Ok(episode.filter(id.eq(ep_id)).get_result::<Episode>(&*con)?)
}
pub fn get_episode_local_uri_from_id(
con: &SqliteConnection,
ep_id: i32,
) -> QueryResult<Option<String>> {
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>> {
use schema::episode::dsl::*;
episode
let db = connection();
let con = db.get()?;
Ok(episode
.filter(id.eq(ep_id))
.select(local_uri)
.get_result::<Option<String>>(con)
.get_result::<Option<String>>(&*con)?)
}
pub fn get_episodes_with_limit(con: &SqliteConnection, limit: u32) -> QueryResult<Vec<Episode>> {
pub fn get_episodes_with_limit(limit: u32) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
episode
let db = connection();
let con = db.get()?;
Ok(episode
.order(epoch.desc())
.limit(i64::from(limit))
.load::<Episode>(con)
.load::<Episode>(&*con)?)
}
pub fn get_podcast_from_id(con: &SqliteConnection, pid: i32) -> QueryResult<Podcast> {
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast> {
use schema::podcast::dsl::*;
podcast.filter(id.eq(pid)).get_result::<Podcast>(con)
let db = connection();
let con = db.get()?;
Ok(podcast.filter(id.eq(pid)).get_result::<Podcast>(&*con)?)
}
pub fn get_pd_episodes(con: &SqliteConnection, parent: &Podcast) -> QueryResult<Vec<Episode>> {
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
Episode::belonging_to(parent)
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.order(epoch.desc())
.load::<Episode>(con)
.load::<Episode>(&*con)?)
}
pub fn get_pd_unplayed_episodes(
con: &SqliteConnection,
parent: &Podcast,
) -> QueryResult<Vec<Episode>> {
pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
Episode::belonging_to(parent)
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.filter(played.is_null())
.order(epoch.desc())
.load::<Episode>(con)
.load::<Episode>(&*con)?)
}
pub fn get_pd_episodes_limit(
con: &SqliteConnection,
parent: &Podcast,
limit: u32,
) -> QueryResult<Vec<Episode>> {
pub fn get_pd_episodes_limit(parent: &Podcast, limit: u32) -> Result<Vec<Episode>> {
use schema::episode::dsl::*;
Episode::belonging_to(parent)
let db = connection();
let con = db.get()?;
Ok(Episode::belonging_to(parent)
.order(epoch.desc())
.limit(i64::from(limit))
.load::<Episode>(con)
.load::<Episode>(&*con)?)
}
pub fn get_source_from_uri(con: &SqliteConnection, uri_: &str) -> QueryResult<Source> {
pub fn get_source_from_uri(uri_: &str) -> Result<Source> {
use schema::source::dsl::*;
source.filter(uri.eq(uri_)).get_result::<Source>(con)
let db = connection();
let con = db.get()?;
Ok(source.filter(uri.eq(uri_)).get_result::<Source>(&*con)?)
}
pub fn get_podcast_from_title(con: &SqliteConnection, title_: &str) -> QueryResult<Podcast> {
// pub fn get_podcast_from_title(title_: &str) -> QueryResult<Podcast> {
// use schema::podcast::dsl::*;
// let db = connection();
// let con = db.get()?;
// podcast
// .filter(title.eq(title_))
// .get_result::<Podcast>(&*con)
// }
pub fn get_podcast_from_source_id(sid: i32) -> Result<Podcast> {
use schema::podcast::dsl::*;
podcast.filter(title.eq(title_)).get_result::<Podcast>(con)
let db = connection();
let con = db.get()?;
Ok(podcast
.filter(source_id.eq(sid))
.get_result::<Podcast>(&*con)?)
}
pub fn get_episode_from_uri(con: &SqliteConnection, uri_: &str) -> QueryResult<Episode> {
use schema::episode::dsl::*;
episode.filter(uri.eq(uri_)).get_result::<Episode>(con)
episode.filter(uri.eq(uri_)).get_result::<Episode>(&*con)
}
pub fn remove_feed(db: &Database, pd: &Podcast) -> QueryResult<usize> {
let tempdb = db.lock().unwrap();
pub fn remove_feed(pd: &Podcast) -> Result<()> {
let db = connection();
let con = db.get()?;
tempdb.transaction(|| -> QueryResult<usize> {
delete_source(&tempdb, pd.source_id())?;
delete_podcast(&tempdb, pd.id())?;
delete_podcast_episodes(&tempdb, pd.id())
con.transaction(|| -> Result<()> {
delete_source(&con, pd.source_id())?;
delete_podcast(&con, *pd.id())?;
delete_podcast_episodes(&con, *pd.id())?;
info!("Feed removed from the Database.");
Ok(())
})
}
pub fn delete_source(connection: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
pub fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
use schema::source::dsl::*;
diesel::delete(source.filter(id.eq(source_id))).execute(connection)
diesel::delete(source.filter(id.eq(source_id))).execute(&*con)
}
pub fn delete_podcast(connection: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
pub fn delete_podcast(con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(connection)
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(&*con)
}
pub fn delete_podcast_episodes(
connection: &SqliteConnection,
parent_id: i32,
) -> QueryResult<usize> {
pub fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(connection)
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(&*con)
}
pub fn update_none_to_played_now(
connection: &SqliteConnection,
parent: &Podcast,
) -> QueryResult<usize> {
pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize> {
use schema::episode::dsl::*;
let db = connection();
let con = db.get()?;
let epoch_now = Utc::now().timestamp() as i32;
connection.transaction(|| -> QueryResult<usize> {
diesel::update(Episode::belonging_to(parent).filter(played.is_null()))
.set(played.eq(Some(epoch_now)))
.execute(connection)
con.transaction(|| -> Result<usize> {
Ok(diesel::update(
Episode::belonging_to(parent).filter(played.is_null()),
).set(played.eq(Some(epoch_now)))
.execute(&*con)?)
})
}
+4
View File
@@ -1,12 +1,16 @@
use diesel::result;
use diesel::migrations::RunMigrationsError;
use rss;
use reqwest;
use r2d2;
use std::io;
error_chain! {
foreign_links {
R2D2TimeoutError(r2d2::GetTimeout);
DieselResultError(result::Error);
DieselMigrationError(RunMigrationsError);
RSSError(rss::Error);
ReqError(reqwest::Error);
IoError(io::Error);
+288
View File
@@ -0,0 +1,288 @@
//! Index and retrieve Feeds.
use rayon::prelude::*;
use diesel::prelude::*;
use rayon::iter::IntoParallelIterator;
use diesel::Identifiable;
use rss;
use dbqueries;
use parser;
use models::queryables::{Episode, Podcast, Source};
use models::insertables::{NewEpisode, NewPodcast};
use database::connection;
use errors::*;
#[derive(Debug)]
/// Wrapper struct that hold a `Source` and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
pub struct Feed {
channel: rss::Channel,
source: Source,
}
impl Feed {
/// Constructor that consumes a `Source` and returns the corresponding `Feed` struct.
pub fn from_source(s: Source) -> Result<Feed> {
s.into_feed()
}
/// Constructor that consumes a `Source` and a `rss::Channel` returns a `Feed` struct.
pub fn from_channel_source(chan: rss::Channel, s: Source) -> Feed {
Feed {
channel: chan,
source: s,
}
}
fn index(&self) -> Result<()> {
let pd = self.get_podcast()?;
self.index_channel_items(&pd)
}
// #[allow(dead_code)]
// fn index_channel(&self) -> Result<()> {
// self.parse_channel().index()?;
// Ok(())
// }
// TODO: Refactor transcactions and find a way to do it in parallel.
fn index_channel_items(&self, pd: &Podcast) -> Result<()> {
let episodes = self.parse_channel_items(pd);
let db = connection();
let con = db.get()?;
let _ = con.transaction::<(), Error, _>(|| {
episodes.into_iter().for_each(|x| {
let e = x.index(&con);
if let Err(err) = e {
error!("Failed to index episode: {:?}.", x.title());
error!("Error msg: {}", err);
};
});
Ok(())
});
Ok(())
}
fn parse_channel(&self) -> NewPodcast {
parser::new_podcast(&self.channel, *self.source.id())
}
fn parse_channel_items(&self, pd: &Podcast) -> Vec<NewEpisode> {
let items = self.channel.items();
let new_episodes: Vec<_> = items
.into_par_iter()
.filter_map(|item| parser::new_episode(item, *pd.id()).ok())
.collect();
new_episodes
}
fn get_podcast(&self) -> Result<Podcast> {
self.parse_channel().into_podcast()
}
#[allow(dead_code)]
fn get_episodes(&self) -> Result<Vec<Episode>> {
let pd = self.get_podcast()?;
let eps = self.parse_channel_items(&pd);
let db = connection();
let con = db.get()?;
// TODO: Make it parallel
// This returns only the episodes in the xml feed.
let episodes: Vec<_> = eps.into_iter()
.filter_map(|ep| ep.into_episode(&con).ok())
.collect();
Ok(episodes)
// This would return every episode of the feed from the db.
// self.index_channel_items(&pd)?;
// Ok(dbqueries::get_pd_episodes(&pd)?)
}
}
/// Use's `fetch_all` to retrieve a list of `Feed`s and use index them using `feed::index`.
pub fn index_all() -> Result<()> {
let feeds = fetch_all()?;
index(feeds);
Ok(())
}
/// Handle the indexing of a feed `F` into the Database.
///
/// Consume a `ParallelIterator<Feed>` and index it.
pub fn index<F: IntoParallelIterator<Item = Feed>>(feeds: F) {
feeds.into_par_iter().for_each(|f| {
let e = f.index();
if e.is_err() {
error!("Error While trying to update the database.");
error!("Error msg: {}", e.unwrap_err());
};
});
info!("Indexing done.");
}
/// Retrieve a list of all the `Source` in the database,
/// then use `feed::fetch` to convert them into `Feed`s
/// and return them.
pub fn fetch_all() -> Result<Vec<Feed>> {
let feeds = dbqueries::get_sources()?;
Ok(fetch(feeds))
}
/// Consume a `ParallelIterator<Source>` and return a list of `Feed`s.
pub fn fetch<F: IntoParallelIterator<Item = Source>>(feeds: F) -> Vec<Feed> {
let results: Vec<_> = feeds
.into_par_iter()
.filter_map(|x| {
let uri = x.uri().to_owned();
let feed = Feed::from_source(x).ok();
if feed.is_none() {
error!("Error While trying to fetch from source url: {}.", uri);
}
feed
})
.collect();
results
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::BufReader;
use database::truncate_db;
use super::*;
#[test]
/// Insert feeds and update/index them.
fn test_index_loop() {
truncate_db().unwrap();
let inpt = vec![
"https://request-for-explanation.github.io/podcast/rss.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
"http://feeds.propublica.org/propublica/podcast",
"http://feeds.feedburner.com/linuxunplugged",
];
inpt.iter().for_each(|url| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
index_all().unwrap();
// Run again to cover Unique constrains erros.
index_all().unwrap();
}
#[test]
/// Insert feeds and update/index them.
fn test_fetch_loop() {
truncate_db().unwrap();
let inpt = vec![
"https://request-for-explanation.github.io/podcast/rss.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
"http://feeds.propublica.org/propublica/podcast",
"http://feeds.feedburner.com/linuxunplugged",
];
inpt.iter().for_each(|url| {
// Index the urls into the source table.
Source::from_url(url).unwrap();
});
fetch_all().unwrap();
}
#[test]
fn test_complete_index() {
// vec of (path, url) tuples.
let urls = vec![
(
"tests/feeds/Intercepted.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
),
(
"tests/feeds/LinuxUnplugged.xml",
"http://feeds.feedburner.com/linuxunplugged",
),
(
"tests/feeds/TheBreakthrough.xml",
"http://feeds.propublica.org/propublica/podcast",
),
(
"tests/feeds/R4Explanation.xml",
"https://request-for-explanation.github.io/podcast/rss.xml",
),
];
truncate_db().unwrap();
let feeds: Vec<_> = urls.iter()
.map(|&(path, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
// open the xml file
let feed = fs::File::open(path).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(feed)).unwrap();
Feed::from_channel_source(chan, s)
})
.collect();
// Index the channels
index(feeds);
// Assert the index rows equal the controlled results
assert_eq!(dbqueries::get_sources().unwrap().len(), 4);
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 4);
assert_eq!(dbqueries::get_episodes().unwrap().len(), 274);
}
#[test]
fn test_partial_index_podcast() {
truncate_db().unwrap();
let url = "https://feeds.feedburner.com/InterceptedWithJeremyScahill";
let s1 = Source::from_url(url).unwrap();
let s2 = Source::from_url(url).unwrap();
assert_eq!(s1, s2);
assert_eq!(s1.id(), s2.id());
let f1 = s1.into_feed().unwrap();
let f2 = s2.into_feed().unwrap();
let p1 = f1.get_podcast().unwrap();
let p2 = {
f2.index().unwrap();
f2.get_podcast().unwrap()
};
assert_eq!(p1, p2);
assert_eq!(p1.id(), p2.id());
assert_eq!(p1.source_id(), p2.source_id());
let eps1 = f1.get_episodes().unwrap();
let eps2 = {
f2.index().unwrap();
f2.get_episodes().unwrap()
};
eps1.into_par_iter()
.zip(eps2)
.into_par_iter()
.for_each(|(ep1, ep2): (Episode, Episode)| {
assert_eq!(ep1, ep2);
assert_eq!(ep1.id(), ep2.id());
assert_eq!(ep1.podcast_id(), ep2.podcast_id());
});
}
}
-329
View File
@@ -1,329 +0,0 @@
use diesel::prelude::*;
use diesel;
use rss;
use reqwest;
use rayon::prelude::*;
use schema;
use dbqueries;
use models::*;
use errors::*;
use feedparser;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub struct Feed(pub reqwest::Response, pub Source);
pub type Database = Arc<Mutex<SqliteConnection>>;
fn index_source(con: &SqliteConnection, foo: &NewSource) -> QueryResult<usize> {
match dbqueries::get_source_from_uri(con, foo.uri) {
Ok(_) => Ok(1),
Err(_) => diesel::insert(foo).into(schema::source::table).execute(con),
}
}
fn index_podcast(con: &SqliteConnection, pd: &NewPodcast) -> Result<()> {
match dbqueries::get_podcast_from_title(con, &pd.title) {
Ok(mut foo) => if foo.link() != pd.link || foo.description() != pd.description {
foo.set_link(&pd.link);
foo.set_description(&pd.description);
foo.set_image_uri(pd.image_uri.as_ref().map(|s| s.as_str()));
foo.save_changes::<Podcast>(con)?;
},
Err(_) => {
diesel::insert(pd)
.into(schema::podcast::table)
.execute(con)?;
}
}
Ok(())
}
fn index_episode(con: &SqliteConnection, ep: &NewEpisode) -> Result<()> {
match dbqueries::get_episode_from_uri(con, ep.uri.unwrap()) {
Ok(mut foo) => if foo.title() != ep.title
|| foo.published_date() != ep.published_date.as_ref().map(|x| x.as_str())
{
foo.set_title(ep.title);
foo.set_description(ep.description);
foo.set_published_date(ep.published_date.as_ref().map(|x| x.as_str()));
foo.set_guid(ep.guid);
foo.set_length(ep.length);
foo.set_epoch(ep.epoch);
foo.save_changes::<Episode>(con)?;
},
Err(_) => {
diesel::insert(ep)
.into(schema::episode::table)
.execute(con)?;
}
}
Ok(())
}
pub fn insert_return_source(con: &SqliteConnection, url: &str) -> Result<Source> {
let foo = NewSource::new_with_uri(url);
index_source(con, &foo)?;
Ok(dbqueries::get_source_from_uri(con, foo.uri)?)
}
fn insert_return_podcast(con: &SqliteConnection, pd: &NewPodcast) -> Result<Podcast> {
index_podcast(con, pd)?;
Ok(dbqueries::get_podcast_from_title(con, &pd.title)?)
}
// fn insert_return_episode(con: &SqliteConnection, ep: &NewEpisode) -> Result<Episode> {
// index_episode(con, ep)?;
// Ok(dbqueries::get_episode_from_uri(con, ep.uri.unwrap())?)
// }
pub fn full_index_loop(db: &Database) -> Result<()> {
let mut f = fetch_all_feeds(db)?;
index_feed(db, &mut f);
info!("Indexing done.");
Ok(())
}
pub fn index_feed(db: &Database, f: &mut [Feed]) {
f.par_iter_mut()
.for_each(|&mut Feed(ref mut req, ref source)| {
let e = complete_index_from_source(req, source, db);
if e.is_err() {
error!("Error While trying to update the database.");
error!("Error msg: {}", e.unwrap_err());
};
});
}
pub fn complete_index_from_source(
req: &mut reqwest::Response,
source: &Source,
db: &Database,
) -> Result<()> {
use std::io::Read;
use std::str::FromStr;
let mut buf = String::new();
req.read_to_string(&mut buf)?;
let chan = rss::Channel::from_str(&buf)?;
complete_index(db, &chan, source)
}
fn complete_index(db: &Database, chan: &rss::Channel, parent: &Source) -> Result<()> {
let pd = {
let conn = db.lock().unwrap();
index_channel(&conn, chan, parent)?
};
index_channel_items(db, chan.items(), &pd);
Ok(())
}
fn index_channel(con: &SqliteConnection, chan: &rss::Channel, parent: &Source) -> Result<Podcast> {
let pd = feedparser::parse_podcast(chan, parent.id());
// Convert NewPodcast to Podcast
insert_return_podcast(con, &pd)
}
fn index_channel_items(db: &Database, it: &[rss::Item], pd: &Podcast) {
let episodes: Vec<_> = it.par_iter()
.map(|x| feedparser::parse_episode(x, pd.id()))
.collect();
let conn = db.lock().unwrap();
let e = conn.transaction::<(), Error, _>(|| {
episodes.iter().for_each(|x| {
let e = index_episode(&conn, x);
if let Err(err) = e {
error!("Failed to index episode: {:?}.", x);
error!("Error msg: {}", err);
};
});
Ok(())
});
drop(conn);
if let Err(err) = e {
error!("Episodes Transcaction Failed.");
error!("Error msg: {}", err);
};
}
// Maybe this can be refactored into an Iterator for lazy evaluation.
pub fn fetch_all_feeds(db: &Database) -> Result<Vec<Feed>> {
let mut feeds = {
let conn = db.lock().unwrap();
dbqueries::get_sources(&conn)?
};
let results = fetch_feeds(db, &mut feeds);
Ok(results)
}
pub fn fetch_feeds(db: &Database, feeds: &mut [Source]) -> Vec<Feed> {
let results: Vec<Feed> = feeds
.par_iter_mut()
.filter_map(|x| {
let l = refresh_source(db, x);
if l.is_ok() {
l.ok()
} else {
error!("Error While trying to fetch from source: {}.", x.uri());
error!("Error msg: {}", l.unwrap_err());
None
}
})
.collect();
results
}
pub fn refresh_source(db: &Database, feed: &mut Source) -> Result<Feed> {
use reqwest::header::{ETag, EntityTag, Headers, HttpDate, LastModified};
let mut headers = Headers::new();
if let Some(foo) = feed.http_etag() {
headers.set(ETag(EntityTag::new(true, foo.to_owned())));
}
if let Some(foo) = feed.last_modified() {
if let Ok(x) = foo.parse::<HttpDate>() {
headers.set(LastModified(x));
}
}
// FIXME: I have fucked up somewhere here.
// Getting back 200 codes even though I supposedly sent etags.
// info!("Headers: {:?}", headers);
let client = reqwest::Client::builder().referer(false).build()?;
let req = client.get(feed.uri()).headers(headers).send()?;
info!("GET to {} , returned: {}", feed.uri(), req.status());
// TODO match on more stuff
// 301: Permanent redirect of the url
// 302: Temporary redirect of the url
// 304: Up to date Feed, checked with the Etag
// 410: Feed deleted
// match req.status() {
// reqwest::StatusCode::NotModified => (),
// _ => (),
// };
feed.update_etag(db, &req)?;
Ok(Feed(req, feed.clone()))
}
#[cfg(test)]
mod tests {
extern crate rand;
extern crate tempdir;
use diesel::prelude::*;
use rss;
use self::rand::Rng;
use std::io::BufReader;
use std::path::PathBuf;
use std::fs;
use super::*;
struct TempDB(tempdir::TempDir, PathBuf, SqliteConnection);
/// Create and return a Temporary DB.
/// Will be destroed once the returned variable(s) is dropped.
fn get_temp_db() -> TempDB {
let mut rng = rand::thread_rng();
let tmp_dir = tempdir::TempDir::new("hammond_unit_test").unwrap();
let db_path = tmp_dir
.path()
.join(format!("hammonddb_{}.db", rng.gen::<usize>()));
let db = SqliteConnection::establish(db_path.to_str().unwrap()).unwrap();
::run_migration_on(&db).unwrap();
TempDB(tmp_dir, db_path, db)
}
#[test]
/// Insert feeds and update/index them.
fn test_index_loop() {
let TempDB(_tmp_dir, _db_path, db) = get_temp_db();
let db = Arc::new(Mutex::new(db));
let inpt = vec![
"https://request-for-explanation.github.io/podcast/rss.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
"http://feeds.propublica.org/propublica/podcast",
"http://feeds.feedburner.com/linuxunplugged",
];
inpt.iter().for_each(|feed| {
let tempdb = db.lock().unwrap();
index_source(&tempdb, &NewSource::new_with_uri(feed)).unwrap();
});
full_index_loop(&db).unwrap();
// Run again to cover Unique constrains erros.
full_index_loop(&db).unwrap();
}
#[test]
fn test_complete_index() {
let TempDB(_tmp_dir, _db_path, db) = get_temp_db();
// complete_index runs in parallel so it requires a mutex as argument.
let m = Arc::new(Mutex::new(db));
// vec of (path, url) tuples.
let urls = vec![
(
"tests/feeds/Intercepted.xml",
"https://feeds.feedburner.com/InterceptedWithJeremyScahill",
),
(
"tests/feeds/LinuxUnplugged.xml",
"http://feeds.feedburner.com/linuxunplugged",
),
(
"tests/feeds/TheBreakthrough.xml",
"http://feeds.feedburner.com/propublica/podcast",
),
(
"tests/feeds/R4Explanation.xml",
"https://request-for-explanation.github.io/podcast/rss.xml",
),
];
urls.iter().for_each(|&(path, url)| {
let tempdb = m.lock().unwrap();
// Create and insert a Source into db
let s = insert_return_source(&tempdb, url).unwrap();
drop(tempdb);
// open the xml file
let feed = fs::File::open(path).unwrap();
// parse it into a channel
let chan = rss::Channel::read_from(BufReader::new(feed)).unwrap();
// Index the channel
complete_index(&m, &chan, &s).unwrap();
});
// Assert the index rows equal the controlled results
let tempdb = m.lock().unwrap();
assert_eq!(dbqueries::get_sources(&tempdb).unwrap().len(), 4);
assert_eq!(dbqueries::get_podcasts(&tempdb).unwrap().len(), 4);
assert_eq!(dbqueries::get_episodes(&tempdb).unwrap().len(), 274);
}
}
+44 -52
View File
@@ -1,5 +1,15 @@
#![recursion_limit = "1024"]
#![deny(missing_docs)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
#![cfg_attr(feature = "clippy",
warn(option_unwrap_used, result_unwrap_used, print_stdout,
wrong_pub_self_convention, mut_mut, non_ascii_literal, similar_names,
unicode_not_nfc, enum_glob_use, if_not_else, items_after_statements,
used_underscore_binding))]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
//! A libraty for parsing, indexing and retrieving podcast Feeds,
//! into and from a Database.
#[macro_use]
extern crate error_chain;
@@ -16,74 +26,56 @@ extern crate diesel;
extern crate diesel_codegen;
extern crate chrono;
extern crate r2d2;
extern crate r2d2_diesel;
extern crate rayon;
extern crate reqwest;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate url;
extern crate xdg;
#[allow(missing_docs)]
pub mod dbqueries;
pub mod dbcheckup;
pub mod models;
pub mod index_feed;
pub mod utils;
pub mod feed;
#[allow(missing_docs)]
pub mod errors;
mod feedparser;
pub(crate) mod database;
pub(crate) mod models;
mod parser;
mod schema;
use diesel::migrations::RunMigrationsError;
use diesel::prelude::*;
use std::path::PathBuf;
pub use models::queryables::{Episode, Podcast, Source};
embed_migrations!("migrations/");
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
pub mod xdg_dirs {
use std::path::PathBuf;
use xdg;
lazy_static!{
#[allow(dead_code)]
static ref HAMMOND_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("hammond").unwrap()
};
static ref _HAMMOND_DATA: PathBuf = {
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
};
static ref _HAMMOND_CONFIG: PathBuf = {
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
};
pub static ref HAMMOND_CACHE: PathBuf = {
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
};
static ref DB_PATH: PathBuf = {
HAMMOND_XDG.place_data_file("hammond.db").unwrap()
lazy_static!{
pub(crate) static ref HAMMOND_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("hammond").unwrap()
};
pub static ref DL_DIR: PathBuf = {
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
};
}
/// XDG_DATA Directory `Pathbuf`.
pub static ref HAMMOND_DATA: PathBuf = {
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
};
pub fn init() -> Result<(), RunMigrationsError> {
let conn = establish_connection();
run_migration_on(&conn)
}
/// XDG_CONFIG Directory `Pathbuf`.
pub static ref HAMMOND_CONFIG: PathBuf = {
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
};
pub fn run_migration_on(connection: &SqliteConnection) -> Result<(), RunMigrationsError> {
info!("Running DB Migrations...");
embedded_migrations::run(connection)
// embedded_migrations::run_with_output(connection, &mut std::io::stdout())
}
/// XDG_CACHE Directory `Pathbuf`.
pub static ref HAMMOND_CACHE: PathBuf = {
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
};
pub fn establish_connection() -> SqliteConnection {
let database_url = DB_PATH.to_str().unwrap();
SqliteConnection::establish(database_url)
.expect(&format!("Error connecting to {}", database_url))
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
/// Hammond Download Direcotry `PathBuf`.
pub static ref DL_DIR: PathBuf = {
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
};
}
}
-290
View File
@@ -1,290 +0,0 @@
use reqwest;
use diesel::SaveChangesDsl;
use diesel::result::QueryResult;
use reqwest::header::{ETag, LastModified};
use schema::{episode, podcast, source};
use index_feed::Database;
use errors::*;
#[derive(Queryable, Identifiable, AsChangeset, Associations)]
#[table_name = "episode"]
#[changeset_options(treat_none_as_null = "true")]
#[belongs_to(Podcast, foreign_key = "podcast_id")]
#[derive(Debug, Clone)]
pub struct Episode {
id: i32,
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
/// Representation of system time. Should be in UTC.
epoch: i32,
length: Option<i32>,
guid: Option<String>,
/// Represent the epoch value of when the episode was last played.
played: Option<i32>,
podcast_id: i32,
}
impl Episode {
pub fn id(&self) -> i32 {
self.id
}
pub fn title(&self) -> Option<&str> {
self.title.as_ref().map(|s| s.as_str())
}
pub fn set_title(&mut self, value: Option<&str>) {
self.title = value.map(|x| x.to_string());
}
/// uri is guaranted to exist based on the db rules
pub fn uri(&self) -> &str {
self.uri.as_ref()
}
pub fn set_uri(&mut self, value: &str) {
self.uri = value.to_string();
}
pub fn local_uri(&self) -> Option<&str> {
self.local_uri.as_ref().map(|s| s.as_str())
}
pub fn set_local_uri(&mut self, value: Option<&str>) {
self.local_uri = value.map(|x| x.to_string());
}
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
pub fn set_description(&mut self, value: Option<&str>) {
self.description = value.map(|x| x.to_string());
}
pub fn published_date(&self) -> Option<&str> {
self.published_date.as_ref().map(|s| s.as_str())
}
pub fn set_published_date(&mut self, value: Option<&str>) {
self.published_date = value.map(|x| x.to_string().to_owned());
}
pub fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
pub fn set_guid(&mut self, value: Option<&str>) {
self.guid = value.map(|x| x.to_string());
}
pub fn epoch(&self) -> i32 {
self.epoch
}
pub fn set_epoch(&mut self, value: i32) {
self.epoch = value;
}
pub fn length(&self) -> Option<i32> {
self.length
}
pub fn set_length(&mut self, value: Option<i32>) {
self.length = value;
}
pub fn played(&self) -> Option<i32> {
self.played
}
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
pub fn save(&self, db: &Database) -> QueryResult<Episode> {
let tempdb = db.lock().unwrap();
self.save_changes::<Episode>(&*tempdb)
}
}
#[derive(Queryable, Identifiable, AsChangeset, Associations)]
#[belongs_to(Source, foreign_key = "source_id")]
#[changeset_options(treat_none_as_null = "true")]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
pub struct Podcast {
id: i32,
title: String,
link: String,
description: String,
image_uri: Option<String>,
source_id: i32,
}
impl Podcast {
pub fn id(&self) -> i32 {
self.id
}
pub fn source_id(&self) -> i32 {
self.source_id
}
pub fn title(&self) -> &str {
&self.title
}
pub fn link(&self) -> &str {
&self.link
}
pub fn set_link(&mut self, value: &str) {
self.link = value.to_string();
}
pub fn description(&self) -> &str {
&self.description
}
pub fn set_description(&mut self, value: &str) {
self.description = value.to_string();
}
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
pub fn set_image_uri(&mut self, value: Option<&str>) {
self.image_uri = value.map(|x| x.to_string());
}
pub fn save(&self, db: &Database) -> QueryResult<Podcast> {
let tempdb = db.lock().unwrap();
self.save_changes::<Podcast>(&*tempdb)
}
}
#[derive(Queryable, Identifiable, AsChangeset)]
#[table_name = "source"]
#[changeset_options(treat_none_as_null = "true")]
#[derive(Debug, Clone)]
pub struct Source {
id: i32,
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl<'a> Source {
pub fn id(&self) -> i32 {
self.id
}
pub fn uri(&self) -> &str {
&self.uri
}
pub fn last_modified(&self) -> Option<&str> {
self.last_modified.as_ref().map(|s| s.as_str())
}
pub fn set_last_modified(&mut self, value: Option<&str>) {
self.last_modified = value.map(|x| x.to_string());
}
pub fn http_etag(&self) -> Option<&str> {
self.http_etag.as_ref().map(|s| s.as_str())
}
pub fn set_http_etag(&mut self, value: Option<&str>) {
self.http_etag = value.map(|x| x.to_string());
}
/// Extract Etag and LastModifier from req, and update self and the
/// corresponding db row.
pub fn update_etag(&mut self, db: &Database, req: &reqwest::Response) -> Result<()> {
let headers = req.headers();
// let etag = headers.get_raw("ETag").unwrap();
let etag = headers.get::<ETag>();
let lmod = headers.get::<LastModified>();
// FIXME: This dsnt work most of the time apparently
if self.http_etag() != etag.map(|x| x.tag())
|| self.last_modified != lmod.map(|x| format!("{}", x))
{
self.http_etag = etag.map(|x| x.tag().to_string().to_owned());
self.last_modified = lmod.map(|x| format!("{}", x));
self.save(db)?;
}
Ok(())
}
pub fn save(&self, db: &Database) -> QueryResult<Source> {
let tempdb = db.lock().unwrap();
self.save_changes::<Source>(&*tempdb)
}
}
#[derive(Insertable)]
#[table_name = "source"]
#[derive(Debug, Clone)]
pub struct NewSource<'a> {
pub uri: &'a str,
pub last_modified: Option<&'a str>,
pub http_etag: Option<&'a str>,
}
impl<'a> NewSource<'a> {
pub fn new_with_uri(uri: &'a str) -> NewSource {
NewSource {
uri,
last_modified: None,
http_etag: None,
}
}
}
#[derive(Insertable)]
#[table_name = "episode"]
#[derive(Debug, Clone)]
pub struct NewEpisode<'a> {
pub title: Option<&'a str>,
pub uri: Option<&'a str>,
pub description: Option<&'a str>,
pub published_date: Option<String>,
pub length: Option<i32>,
pub guid: Option<&'a str>,
pub epoch: i32,
pub podcast_id: i32,
}
#[derive(Insertable)]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
pub struct NewPodcast {
pub title: String,
pub link: String,
pub description: String,
pub image_uri: Option<String>,
pub source_id: i32,
}
impl NewPodcast {
pub fn into_podcast(self) -> Podcast {
Podcast {
id: 0,
title: self.title,
link: self.link,
description: self.description,
image_uri: self.image_uri,
source_id: self.source_id,
}
}
}
+424
View File
@@ -0,0 +1,424 @@
use diesel::prelude::*;
use schema::{episode, podcast, source};
use models::queryables::{Episode, Podcast, Source};
use utils::url_cleaner;
use errors::*;
use dbqueries;
use diesel;
use database::connection;
trait Insert {
fn insert(&self, &SqliteConnection) -> QueryResult<usize>;
}
trait Update {
fn update(&self, &SqliteConnection, i32) -> QueryResult<usize>;
}
#[derive(Insertable)]
#[table_name = "source"]
#[derive(Debug, Clone)]
pub(crate) struct NewSource {
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl Insert for NewSource {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::source::dsl::*;
diesel::insert_into(source).values(self).execute(&*con)
}
}
impl NewSource {
pub(crate) fn new_with_uri(uri: &str) -> NewSource {
let uri = url_cleaner(uri);
NewSource {
uri,
last_modified: None,
http_etag: None,
}
}
fn index(&self) -> Result<()> {
let db = connection();
let con = db.get()?;
// Throw away the result like `insert or ignore`
// Diesel deos not support `insert or ignore` yet.
let _ = self.insert(&con);
Ok(())
}
// Look out for when tryinto lands into stable.
pub(crate) fn into_source(self) -> Result<Source> {
self.index()?;
dbqueries::get_source_from_uri(&self.uri)
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
pub(crate) struct NewPodcast {
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
impl Insert for NewPodcast {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::insert_into(podcast).values(self).execute(&*con)
}
}
impl Update for NewPodcast {
fn update(&self, con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
diesel::update(podcast.filter(id.eq(podcast_id)))
.set(self)
.execute(&*con)
}
}
impl NewPodcast {
// Look out for when tryinto lands into stable.
pub(crate) fn into_podcast(self) -> Result<Podcast> {
self.index()?;
Ok(dbqueries::get_podcast_from_source_id(self.source_id)?)
}
pub(crate) fn index(&self) -> Result<()> {
let pd = dbqueries::get_podcast_from_source_id(self.source_id);
let db = connection();
let con = db.get()?;
match pd {
Ok(foo) => {
if foo.source_id() != self.source_id {
error!("NSPD sid: {}, SPD sid: {}", self.source_id, foo.source_id());
};
if (foo.link() != self.link) || (foo.title() != self.title)
|| (foo.image_uri() != self.image_uri.as_ref().map(|x| x.as_str()))
{
self.update(&con, *foo.id())?;
}
}
Err(_) => {
self.insert(&con)?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub(crate) struct NewPodcastBuilder {
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
#[allow(dead_code)]
impl NewPodcastBuilder {
pub(crate) fn new() -> NewPodcastBuilder {
NewPodcastBuilder::default()
}
pub(crate) fn title(mut self, s: String) -> NewPodcastBuilder {
self.title = s;
self
}
pub(crate) fn link(mut self, s: String) -> NewPodcastBuilder {
self.link = s;
self
}
pub(crate) fn description(mut self, s: String) -> NewPodcastBuilder {
self.description = s;
self
}
pub(crate) fn image_uri(mut self, s: Option<String>) -> NewPodcastBuilder {
self.image_uri = s;
self
}
pub(crate) fn source_id(mut self, s: i32) -> NewPodcastBuilder {
self.source_id = s;
self
}
pub(crate) fn favorite(mut self, s: bool) -> NewPodcastBuilder {
self.favorite = s;
self
}
pub(crate) fn archive(mut self, s: bool) -> NewPodcastBuilder {
self.archive = s;
self
}
pub(crate) fn always_dl(mut self, s: bool) -> NewPodcastBuilder {
self.always_dl = s;
self
}
pub(crate) fn build(self) -> NewPodcast {
NewPodcast {
title: self.title,
link: self.link,
description: self.description,
image_uri: self.image_uri,
favorite: self.favorite,
archive: self.archive,
always_dl: self.always_dl,
source_id: self.source_id,
}
}
}
#[allow(dead_code)]
// Ignore the following geters. They are used in unit tests mainly.
impl NewPodcast {
pub(crate) fn source_id(&self) -> i32 {
self.source_id
}
pub(crate) fn title(&self) -> &str {
&self.title
}
pub(crate) fn link(&self) -> &str {
&self.link
}
pub(crate) fn description(&self) -> &str {
&self.description
}
pub(crate) fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[derive(Debug, Clone, Default)]
pub(crate) struct NewEpisode {
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
length: Option<i32>,
guid: Option<String>,
epoch: i32,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
impl Insert for NewEpisode {
fn insert(&self, con: &SqliteConnection) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::insert_into(episode).values(self).execute(&*con)
}
}
impl Update for NewEpisode {
fn update(&self, con: &SqliteConnection, episode_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
diesel::update(episode.filter(id.eq(episode_id)))
.set(self)
.execute(&*con)
}
}
impl NewEpisode {
// TODO: Currently using diesel from master git.
// Watch out for v0.99.0 beta and change the toml.
// TODO: Refactor into batch indexes instead.
pub(crate) fn into_episode(self, con: &SqliteConnection) -> Result<Episode> {
self.index(con)?;
Ok(dbqueries::get_episode_from_uri(con, &self.uri)?)
}
pub(crate) fn index(&self, con: &SqliteConnection) -> QueryResult<()> {
let ep = dbqueries::get_episode_from_uri(con, &self.uri.clone());
match ep {
Ok(foo) => {
if foo.podcast_id() != self.podcast_id {
error!("NEP pid: {}, EP pid: {}", self.podcast_id, foo.podcast_id());
};
if foo.title() != self.title.as_ref().map(|x| x.as_str())
|| foo.published_date() != self.published_date.as_ref().map(|x| x.as_str())
{
self.update(con, *foo.id())?;
}
}
Err(_) => {
self.insert(con)?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
pub(crate) struct NewEpisodeBuilder {
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
length: Option<i32>,
guid: Option<String>,
epoch: i32,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
#[allow(dead_code)]
impl NewEpisodeBuilder {
pub(crate) fn new() -> NewEpisodeBuilder {
NewEpisodeBuilder::default()
}
pub(crate) fn title(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.title = s;
self
}
pub(crate) fn uri(mut self, s: String) -> NewEpisodeBuilder {
self.uri = s;
self
}
pub(crate) fn local_uri(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.local_uri = s;
self
}
pub(crate) fn description(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.description = s;
self
}
pub(crate) fn published_date(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.published_date = s;
self
}
pub(crate) fn length(mut self, s: Option<i32>) -> NewEpisodeBuilder {
self.length = s;
self
}
pub(crate) fn played(mut self, s: Option<i32>) -> NewEpisodeBuilder {
self.played = s;
self
}
pub(crate) fn guid(mut self, s: Option<String>) -> NewEpisodeBuilder {
self.guid = s;
self
}
pub(crate) fn epoch(mut self, s: i32) -> NewEpisodeBuilder {
self.epoch = s;
self
}
pub(crate) fn podcast_id(mut self, s: i32) -> NewEpisodeBuilder {
self.podcast_id = s;
self
}
pub(crate) fn favorite(mut self, s: bool) -> NewEpisodeBuilder {
self.favorite = s;
self
}
pub(crate) fn archive(mut self, s: bool) -> NewEpisodeBuilder {
self.archive = s;
self
}
pub(crate) fn build(self) -> NewEpisode {
NewEpisode {
title: self.title,
uri: self.uri,
local_uri: self.local_uri,
description: self.description,
published_date: self.published_date,
length: self.length,
guid: self.guid,
epoch: self.epoch,
played: self.played,
favorite: self.favorite,
archive: self.archive,
podcast_id: self.podcast_id,
}
}
}
#[allow(dead_code)]
// Ignore the following geters. They are used in unit tests mainly.
impl NewEpisode {
pub(crate) fn title(&self) -> Option<&str> {
self.title.as_ref().map(|s| s.as_str())
}
pub(crate) fn uri(&self) -> &str {
self.uri.as_ref()
}
pub(crate) fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
pub(crate) fn published_date(&self) -> Option<&str> {
self.published_date.as_ref().map(|s| s.as_str())
}
pub(crate) fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
pub(crate) fn epoch(&self) -> i32 {
self.epoch
}
pub(crate) fn length(&self) -> Option<i32> {
self.length
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
}
}
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod insertables;
pub(crate) mod queryables;
+418
View File
@@ -0,0 +1,418 @@
use chrono::prelude::*;
use reqwest;
use diesel::SaveChangesDsl;
use reqwest::header::{ETag, LastModified};
use rss::Channel;
use schema::{episode, podcast, source};
use feed::Feed;
use errors::*;
use models::insertables::NewSource;
use database::connection;
use std::io::Read;
use std::str::FromStr;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[table_name = "episode"]
#[changeset_options(treat_none_as_null = "true")]
#[belongs_to(Podcast, foreign_key = "podcast_id")]
#[derive(Debug, Clone)]
/// Diesel Model of the episode table.
pub struct Episode {
id: i32,
title: Option<String>,
uri: String,
local_uri: Option<String>,
description: Option<String>,
published_date: Option<String>,
epoch: i32,
length: Option<i32>,
guid: Option<String>,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
}
impl Episode {
/// Get the value of the `title` field.
pub fn title(&self) -> Option<&str> {
self.title.as_ref().map(|s| s.as_str())
}
/// Set the `title`.
pub fn set_title(&mut self, value: Option<&str>) {
self.title = value.map(|x| x.to_string());
}
/// Get the value of the `uri`.
///
/// Represents the url(usually) that the media file will be located at.
pub fn uri(&self) -> &str {
self.uri.as_ref()
}
/// Set the `uri`.
pub fn set_uri(&mut self, value: &str) {
self.uri = value.to_string();
}
/// Get the value of the `local_uri`.
///
/// Represents the local uri,usually filesystem path,
/// that the media file will be located at.
pub fn local_uri(&self) -> Option<&str> {
self.local_uri.as_ref().map(|s| s.as_str())
}
/// Set the `local_uri`.
pub fn set_local_uri(&mut self, value: Option<&str>) {
self.local_uri = value.map(|x| x.to_string());
}
/// Get the `description`.
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(|s| s.as_str())
}
/// Set the `description`.
pub fn set_description(&mut self, value: Option<&str>) {
self.description = value.map(|x| x.to_string());
}
/// Get the the `published_date`.
pub fn published_date(&self) -> Option<&str> {
self.published_date.as_ref().map(|s| s.as_str())
}
/// Set the `published_date`.
pub fn set_published_date(&mut self, value: Option<&str>) {
self.published_date = value.map(|x| x.to_string().to_owned());
}
/// Get the value of the `description`.
pub fn guid(&self) -> Option<&str> {
self.guid.as_ref().map(|s| s.as_str())
}
/// Set the `guid`.
pub fn set_guid(&mut self, value: Option<&str>) {
self.guid = value.map(|x| x.to_string());
}
/// Get the `epoch` value.
///
/// Retrieved from the rss Item publish date.
/// Value is set to Utc whenever possible.
pub fn epoch(&self) -> i32 {
self.epoch
}
/// Set the `epoch`.
pub fn set_epoch(&mut self, value: i32) {
self.epoch = value;
}
/// Get the `length`.
pub fn length(&self) -> Option<i32> {
self.length
}
/// Set the `length`.
pub fn set_length(&mut self, value: Option<i32>) {
self.length = value;
}
/// Epoch representation of the last time the episode was played.
///
/// None/Null for unplayed.
pub fn played(&self) -> Option<i32> {
self.played
}
/// Set the `played` value.
pub fn set_played(&mut self, value: Option<i32>) {
self.played = value;
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
///
/// If true, the download cleanr will ignore the episode
/// and the corresponding media value will never be automaticly deleted.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Episode`.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
}
/// Sets the `played` value with the current `epoch` timestap and save it.
pub fn set_played_now(&mut self) -> Result<()> {
let epoch = Utc::now().timestamp() as i32;
self.set_played(Some(epoch));
self.save()?;
Ok(())
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Episode> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Episode>(&*tempdb)?)
}
}
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[belongs_to(Source, foreign_key = "source_id")]
#[changeset_options(treat_none_as_null = "true")]
#[table_name = "podcast"]
#[derive(Debug, Clone)]
/// Diesel Model of the podcast table.
pub struct Podcast {
id: i32,
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
impl Podcast {
/// Get the Feed `title`.
pub fn title(&self) -> &str {
&self.title
}
/// Get the Feed `link`.
///
/// Usually the website/homepage of the content creator.
pub fn link(&self) -> &str {
&self.link
}
/// Set the Podcast/Feed `link`.
pub fn set_link(&mut self, value: &str) {
self.link = value.to_string();
}
/// Get the `description`.
pub fn description(&self) -> &str {
&self.description
}
/// Set the `description`.
pub fn set_description(&mut self, value: &str) {
self.description = value.to_string();
}
/// Get the `image_uri`.
///
/// Represents the uri(url usually) that the Feed cover image is located at.
pub fn image_uri(&self) -> Option<&str> {
self.image_uri.as_ref().map(|s| s.as_str())
}
/// Set the `image_uri`.
pub fn set_image_uri(&mut self, value: Option<&str>) {
self.image_uri = value.map(|x| x.to_string());
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Podcast` Feed.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// Represents the download policy for the `Podcast` Feed.
///
/// Reserved for the use with a Download manager, yet to be implemented.
///
/// If true Podcast Episode should be downloaded automaticly/skipping
/// the selection queue.
pub fn always_download(&self) -> bool {
self.always_dl
}
/// Set the download policy.
pub fn set_always_download(&mut self, b: bool) {
self.always_dl = b
}
/// `Source` table foreign key.
pub fn source_id(&self) -> i32 {
self.source_id
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Podcast> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Podcast>(&*tempdb)?)
}
}
#[derive(Queryable, Identifiable, AsChangeset, PartialEq)]
#[table_name = "source"]
#[changeset_options(treat_none_as_null = "true")]
#[derive(Debug, Clone)]
/// Diesel Model of the source table.
pub struct Source {
id: i32,
uri: String,
last_modified: Option<String>,
http_etag: Option<String>,
}
impl<'a> Source {
/// Represents the location(usually url) of the Feed xml file.
pub fn uri(&self) -> &str {
&self.uri
}
/// Represents the Http Last-Modified Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn last_modified(&self) -> Option<&str> {
self.last_modified.as_ref().map(|s| s.as_str())
}
/// Set `last_modified` value.
pub fn set_last_modified(&mut self, value: Option<&str>) {
self.last_modified = value.map(|x| x.to_string());
}
/// Represents the Http Etag Header field.
///
/// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.2) for more.
pub fn http_etag(&self) -> Option<&str> {
self.http_etag.as_ref().map(|s| s.as_str())
}
/// Set `http_etag` value.
pub fn set_http_etag(&mut self, value: Option<&str>) {
self.http_etag = value.map(|x| x.to_string());
}
/// Extract Etag and LastModifier from req, and update self and the
/// corresponding db row.
fn update_etag(&mut self, req: &reqwest::Response) -> Result<()> {
let headers = req.headers();
// let etag = headers.get_raw("ETag").unwrap();
let etag = headers.get::<ETag>();
let lmod = headers.get::<LastModified>();
// FIXME: This dsnt work most of the time apparently
if self.http_etag() != etag.map(|x| x.tag())
|| self.last_modified != lmod.map(|x| format!("{}", x))
{
self.http_etag = etag.map(|x| x.tag().to_string().to_owned());
self.last_modified = lmod.map(|x| format!("{}", x));
self.save()?;
}
Ok(())
}
/// Helper method to easily save/"sync" current state of self to the Database.
pub fn save(&self) -> Result<Source> {
let db = connection();
let tempdb = db.get()?;
Ok(self.save_changes::<Source>(&*tempdb)?)
}
/// `Feed` constructor.
///
/// Fetches the latest xml Feed.
///
/// Updates the validator Http Headers.
///
/// Consumes `self` and Returns the corresponding `Feed` Object.
// TODO: Refactor into TryInto once it lands on stable.
pub fn into_feed(mut self) -> Result<Feed> {
use reqwest::header::{ETag, EntityTag, Headers, HttpDate, LastModified};
let mut headers = Headers::new();
if let Some(foo) = self.http_etag() {
headers.set(ETag(EntityTag::new(true, foo.to_owned())));
}
if let Some(foo) = self.last_modified() {
if let Ok(x) = foo.parse::<HttpDate>() {
headers.set(LastModified(x));
}
}
// FIXME: I have fucked up somewhere here.
// Getting back 200 codes even though I supposedly sent etags.
// info!("Headers: {:?}", headers);
let client = reqwest::Client::builder().referer(false).build()?;
let mut req = client.get(self.uri()).headers(headers).send()?;
info!("GET to {} , returned: {}", self.uri(), req.status());
// TODO match on more stuff
// 301: Permanent redirect of the url
// 302: Temporary redirect of the url
// 304: Up to date Feed, checked with the Etag
// 410: Feed deleted
// match req.status() {
// reqwest::StatusCode::NotModified => (),
// _ => (),
// };
self.update_etag(&req)?;
let mut buf = String::new();
req.read_to_string(&mut buf)?;
let chan = Channel::from_str(&buf)?;
Ok(Feed::from_channel_source(chan, self))
}
/// Construct a new `Source` with the given `uri` and index it.
pub fn from_url(uri: &str) -> Result<Source> {
NewSource::new_with_uri(uri).into_source()
}
}
@@ -1,50 +1,51 @@
use rss::{Channel, Item};
use rfc822_sanitizer::parse_from_rfc2822_with_fallback;
use models;
use models::insertables::{NewEpisode, NewEpisodeBuilder, NewPodcast, NewPodcastBuilder};
use utils::url_cleaner;
use errors::*;
// TODO: Extend the support for parsing itunes extensions
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub fn parse_podcast(chan: &Channel, source_id: i32) -> models::NewPodcast {
pub(crate) fn new_podcast(chan: &Channel, source_id: i32) -> NewPodcast {
let title = chan.title().trim().to_owned();
let link = chan.link().trim().to_owned();
let description = chan.description().trim().to_owned();
// Some feeds miss baseurl and/or http://
// TODO: Sanitize the url,
// could also be reuse to sanitize the new-url gui entrybox.
let link = url_cleaner(chan.link()).to_owned();
let x = chan.itunes_ext().map(|s| s.image());
let image_uri = if let Some(img) = x {
img.map(|s| s.to_string())
img.map(|s| url_cleaner(s))
} else {
chan.image().map(|foo| foo.url().to_owned())
chan.image().map(|foo| url_cleaner(foo.url()))
};
models::NewPodcast {
title,
link,
description,
image_uri,
source_id,
}
NewPodcastBuilder::new()
.title(title)
.description(description)
.link(link)
.image_uri(image_uri)
.source_id(source_id)
.build()
}
/// Parses an `rss::Item` into a `NewEpisode` Struct.
pub fn parse_episode(item: &Item, parent_id: i32) -> models::NewEpisode {
let title = item.title().map(|s| s.trim());
let description = item.description().map(|s| s.trim());
let guid = item.guid().map(|x| x.value().trim());
pub(crate) fn new_episode(item: &Item, parent_id: i32) -> Result<NewEpisode> {
let title = item.title().map(|s| s.trim().to_owned());
let description = item.description().map(|s| s.trim().to_owned());
let guid = item.guid().map(|s| s.value().trim().to_owned());
// Its kinda weird this being an Option type.
// Rss 2.0 specified that it's optional.
// Though the db scema has a requirment of episode uri being Unique && Not Null.
// TODO: Restructure
let x = item.enclosure().map(|x| x.url().trim());
let x = item.enclosure().map(|s| url_cleaner(s.url()));
let uri = if x.is_some() {
x
x.unwrap()
} else if item.link().is_some() {
item.link()
item.link().map(|s| url_cleaner(s)).unwrap()
} else {
None
bail!("No url specified for the item.")
};
let date = parse_from_rfc2822_with_fallback(
@@ -59,16 +60,18 @@ pub fn parse_episode(item: &Item, parent_id: i32) -> models::NewEpisode {
let length = item.enclosure().map(|x| x.length().parse().unwrap_or(0));
models::NewEpisode {
title,
uri,
description,
length,
published_date: pub_date,
epoch,
guid,
podcast_id: parent_id,
}
Ok(
NewEpisodeBuilder::new()
.title(title)
.uri(uri)
.description(description)
.length(length)
.published_date(pub_date)
.epoch(epoch)
.guid(guid)
.podcast_id(parent_id)
.build(),
)
}
@@ -81,7 +84,7 @@ mod tests {
use super::*;
#[test]
fn test_parse_podcast_intercepted() {
fn test_new_podcast_intercepted() {
let file = File::open("tests/feeds/Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
@@ -90,82 +93,81 @@ mod tests {
the crucial issues of our time: national security, civil liberties, foreign \
policy, and criminal justice. Plus interviews with artists, thinkers, and \
newsmakers who challenge our preconceptions about the world we live in.";
let pd = parse_podcast(&channel, 0);
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title, "Intercepted with Jeremy Scahill".to_string());
assert_eq!(pd.link, "https://theintercept.com/podcasts".to_string());
assert_eq!(pd.description, descr.to_string());
assert_eq!(pd.title(), "Intercepted with Jeremy Scahill");
assert_eq!(pd.link(), "https://theintercept.com/podcasts");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri,
pd.image_uri(),
Some(
"http://static.megaphone.fm/podcasts/d5735a50-d904-11e6-8532-73c7de466ea6/image/\
uploads_2F1484252190700-qhn5krasklbce3dh-a797539282700ea0298a3a26f7e49b0b_\
2FIntercepted_COVER%2B_281_29.png"
.to_string()
)
);
}
#[test]
fn test_parse_podcast_breakthrough() {
fn test_new_podcast_breakthrough() {
let file = File::open("tests/feeds/TheBreakthrough.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let descr = "Latest Articles and Investigations from ProPublica, an independent, \
non-profit newsroom that produces investigative journalism in the public \
interest.";
let pd = parse_podcast(&channel, 0);
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title, "The Breakthrough".to_string());
assert_eq!(pd.link, "http://www.propublica.org/podcast".to_string());
assert_eq!(pd.description, descr.to_string());
assert_eq!(pd.title(), "The Breakthrough");
assert_eq!(pd.link(), "http://www.propublica.org/podcast");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri,
Some("http://www.propublica.org/images/podcast_logo_2.png".to_string())
pd.image_uri(),
Some("http://www.propublica.org/images/podcast_logo_2.png")
);
}
#[test]
fn test_parse_podcast_lup() {
fn test_new_podcast_lup() {
let file = File::open("tests/feeds/LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let descr = "An open show powered by community LINUX Unplugged takes the best attributes \
of open collaboration and focuses them into a weekly lifestyle show about \
Linux.";
let pd = parse_podcast(&channel, 0);
let pd = new_podcast(&channel, 0);
assert_eq!(pd.title, "LINUX Unplugged Podcast".to_string());
assert_eq!(pd.link, "http://www.jupiterbroadcasting.com/".to_string());
assert_eq!(pd.description, descr.to_string());
assert_eq!(pd.title(), "LINUX Unplugged Podcast");
assert_eq!(pd.link(), "http://www.jupiterbroadcasting.com/");
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri,
Some("http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg".to_string(),)
pd.image_uri(),
Some("http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg")
);
}
#[test]
fn test_parse_podcast_r4explanation() {
fn test_new_podcast_r4explanation() {
let file = File::open("tests/feeds/R4Explanation.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = parse_podcast(&channel, 0);
let pd = new_podcast(&channel, 0);
let descr = "A weekly discussion of Rust RFCs";
assert_eq!(pd.title, "Request For Explanation".to_string());
assert_eq!(pd.title(), "Request For Explanation");
assert_eq!(
pd.link,
"https://request-for-explanation.github.io/podcast/".to_string()
pd.link(),
"https://request-for-explanation.github.io/podcast/"
);
assert_eq!(pd.description, descr.to_string());
assert_eq!(pd.description(), descr);
assert_eq!(
pd.image_uri,
Some("https://request-for-explanation.github.io/podcast/podcast.png".to_string(),)
pd.image_uri(),
Some("https://request-for-explanation.github.io/podcast/podcast.png")
);
}
#[test]
fn test_parse_episode_intercepted() {
fn test_new_episode_intercepted() {
let file = File::open("tests/feeds/Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
@@ -175,21 +177,18 @@ mod tests {
Shaun King explains his call for a boycott of the NFL and talks about his \
campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle \
performs.";
let i = parse_episode(&firstitem, 0);
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title, Some("The Super Bowl of Racism"));
assert_eq!(i.uri, Some("http://traffic.megaphone.fm/PPY6458293736.mp3"));
assert_eq!(i.description, Some(descr));
assert_eq!(i.length, Some(66738886));
assert_eq!(i.guid, Some("7df4070a-9832-11e7-adac-cb37b05d5e24"));
assert_eq!(
i.published_date,
Some("Wed, 13 Sep 2017 10:00:00 +0000".to_string())
);
assert_eq!(i.epoch, 1505296800);
assert_eq!(i.title(), Some("The Super Bowl of Racism"));
assert_eq!(i.uri(), "http://traffic.megaphone.fm/PPY6458293736.mp3");
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(66738886));
assert_eq!(i.guid(), Some("7df4070a-9832-11e7-adac-cb37b05d5e24"));
assert_eq!(i.published_date(), Some("Wed, 13 Sep 2017 10:00:00 +0000"));
assert_eq!(i.epoch(), 1505296800);
let second = channel.items().iter().nth(1).unwrap();
let i2 = parse_episode(&second, 0);
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "This week on Intercepted: Jeremy gives an update on the aftermath of \
Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang \
@@ -200,87 +199,77 @@ mod tests {
Caracas-based band, La Pequeña Revancha, talk about her music and hopes for \
Venezuela.";
assert_eq!(
i2.title,
Some("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America",)
i2.title(),
Some("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
);
assert_eq!(i2.uri, Some("http://traffic.megaphone.fm/FL5331443769.mp3"));
assert_eq!(i2.description, Some(descr2));
assert_eq!(i2.length, Some(67527575));
assert_eq!(i2.guid, Some("7c207a24-e33f-11e6-9438-eb45dcf36a1d"));
assert_eq!(
i2.published_date,
Some("Wed, 9 Aug 2017 10:00:00 +0000".to_string())
);
assert_eq!(i2.epoch, 1502272800);
assert_eq!(i2.uri(), "http://traffic.megaphone.fm/FL5331443769.mp3");
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(67527575));
assert_eq!(i2.guid(), Some("7c207a24-e33f-11e6-9438-eb45dcf36a1d"));
assert_eq!(i2.published_date(), Some("Wed, 9 Aug 2017 10:00:00 +0000"));
assert_eq!(i2.epoch(), 1502272800);
}
#[test]
fn test_parse_episode_breakthrough() {
fn test_new_episode_breakthrough() {
let file = File::open("tests/feeds/TheBreakthrough.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let firstitem = channel.items().first().unwrap();
let descr = "<p>A reporter finds that homes meant to replace New Yorks troubled \
psychiatric hospitals might be just as bad.</p>";
let i = parse_episode(&firstitem, 0);
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(
i.title,
Some("The Breakthrough: Hopelessness and Exploitation Inside Homes for Mentally Ill",)
i.title(),
Some("The Breakthrough: Hopelessness and Exploitation Inside Homes for Mentally Ill")
);
assert_eq!(
i.uri,
Some("http://tracking.feedpress.it/link/10581/6726758/20170908-cliff-levy.mp3",)
i.uri(),
"http://tracking.feedpress.it/link/10581/6726758/20170908-cliff-levy.mp3"
);
assert_eq!(i.description, Some(descr));
assert_eq!(i.length, Some(33396551));
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(33396551));
assert_eq!(
i.guid,
i.guid(),
Some(
"https://www.propublica.org/podcast/\
the-breakthrough-hopelessness-exploitation-homes-for-mentally-ill#134472",
the-breakthrough-hopelessness-exploitation-homes-for-mentally-ill#134472"
)
);
assert_eq!(
i.published_date,
Some("Fri, 8 Sep 2017 12:00:00 +0000".to_string())
);
assert_eq!(i.epoch, 1504872000);
assert_eq!(i.published_date(), Some("Fri, 8 Sep 2017 12:00:00 +0000"));
assert_eq!(i.epoch(), 1504872000);
let second = channel.items().iter().nth(1).unwrap();
let i2 = parse_episode(&second, 0);
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "<p>Jonathan Allen and Amie Parnes didnt know their book would be called \
Shattered, or that their extraordinary access would let them chronicle \
the mounting signs of a doomed campaign.</p>";
assert_eq!(
i2.title,
Some(
"The Breakthrough: Behind the Scenes of Hillary Clintons Failed Bid for President",
)
i2.title(),
Some("The Breakthrough: Behind the Scenes of Hillary Clintons Failed Bid for \
President")
);
assert_eq!(
i2.uri,
Some("http://tracking.feedpress.it/link/10581/6726759/16_JohnAllen-CRAFT.mp3",)
i2.uri(),
"http://tracking.feedpress.it/link/10581/6726759/16_JohnAllen-CRAFT.mp3".to_string()
);
assert_eq!(i2.description, Some(descr2));
assert_eq!(i2.length, Some(17964071));
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(17964071));
assert_eq!(
i2.guid,
i2.guid(),
Some(
"https://www.propublica.\
org/podcast/the-breakthrough-hillary-clinton-failed-presidential-bid#133721",
org/podcast/the-breakthrough-hillary-clinton-failed-presidential-bid#133721"
)
);
assert_eq!(
i2.published_date,
Some("Fri, 25 Aug 2017 12:00:00 +0000".to_string())
);
assert_eq!(i2.epoch, 1503662400);
assert_eq!(i2.published_date(), Some("Fri, 25 Aug 2017 12:00:00 +0000"));
assert_eq!(i2.epoch(), 1503662400);
}
#[test]
fn test_parse_episode_lup() {
fn test_new_episode_lup() {
let file = File::open("tests/feeds/LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
@@ -289,24 +278,21 @@ mod tests {
"Audit your network with a couple of easy commands on Kali Linux. Chris decides to \
blow off a little steam by attacking his IoT devices, Wes has the scope on Equifax \
blaming open source & the Beard just saved the show. Its a really packed episode!";
let i = parse_episode(&firstitem, 0);
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title, Some("Hacking Devices with Kali Linux | LUP 214"));
assert_eq!(i.title(), Some("Hacking Devices with Kali Linux | LUP 214"));
assert_eq!(
i.uri,
Some("http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",)
i.uri(),
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3"
);
assert_eq!(i.description, Some(descr));
assert_eq!(i.length, Some(46479789));
assert_eq!(i.guid, Some("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D"));
assert_eq!(
i.published_date,
Some("Tue, 12 Sep 2017 22:24:42 -0700".to_string())
);
assert_eq!(i.epoch, 1505280282);
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(46479789));
assert_eq!(i.guid(), Some("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D"));
assert_eq!(i.published_date(), Some("Tue, 12 Sep 2017 22:24:42 -0700"));
assert_eq!(i.epoch(), 1505280282);
let second = channel.items().iter().nth(1).unwrap();
let i2 = parse_episode(&second, 0);
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "<p>The Gnome project is about to solve one of our audience's biggest \
Waylands concerns. But as the project takes on a new level of relevance, \
@@ -314,23 +300,20 @@ mod tests {
future.</p>\n\n<p>Plus we chat with Wimpy about the Ubuntu Rally in NYC, \
Microsofts sneaky move to turn Windows 10 into the ULTIMATE LINUX \
RUNTIME, community news & more!</p>";
assert_eq!(i2.title, Some("Gnome Does it Again | LUP 213"));
assert_eq!(i2.title(), Some("Gnome Does it Again | LUP 213"));
assert_eq!(
i2.uri,
Some("http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",)
i2.uri(),
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3"
);
assert_eq!(i2.description, Some(descr2));
assert_eq!(i2.length, Some(36544272));
assert_eq!(i2.guid, Some("1CE57548-B36C-4F14-832A-5D5E0A24E35B"));
assert_eq!(
i2.published_date,
Some("Tue, 5 Sep 2017 20:57:27 -0700".to_string())
);
assert_eq!(i2.epoch, 1504670247);
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(36544272));
assert_eq!(i2.guid(), Some("1CE57548-B36C-4F14-832A-5D5E0A24E35B"));
assert_eq!(i2.published_date(), Some("Tue, 5 Sep 2017 20:57:27 -0700"));
assert_eq!(i2.epoch(), 1504670247);
}
#[test]
fn test_parse_episode_r4expanation() {
fn test_new_episode_r4expanation() {
let file = File::open("tests/feeds/R4Explanation.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
@@ -338,52 +321,42 @@ mod tests {
let descr = "This week we look at <a \
href=\"https://github.com/rust-lang/rfcs/pull/2094\">RFC 2094</a> \
\"Non-lexical lifetimes\"";
let i = parse_episode(&firstitem, 0);
let i = new_episode(&firstitem, 0).unwrap();
assert_eq!(i.title, Some("Episode #9 - A Once in a Lifetime RFC"));
assert_eq!(i.title(), Some("Episode #9 - A Once in a Lifetime RFC"));
assert_eq!(
i.uri,
Some(
"http://request-for-explanation.github.\
io/podcast/ep9-a-once-in-a-lifetime-rfc/episode.mp3",
)
i.uri(),
"http://request-for-explanation.github.\
io/podcast/ep9-a-once-in-a-lifetime-rfc/episode.mp3"
);
assert_eq!(i.description, Some(descr));
assert_eq!(i.length, Some(15077388));
assert_eq!(i.description(), Some(descr));
assert_eq!(i.length(), Some(15077388));
assert_eq!(
i.guid,
Some("https://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/",)
i.guid(),
Some("https://request-for-explanation.github.io/podcast/ep9-a-once-in-a-lifetime-rfc/")
);
assert_eq!(
i.published_date,
Some("Mon, 28 Aug 2017 15:00:00 -0700".to_string())
);
assert_eq!(i.epoch, 1503957600);
assert_eq!(i.published_date(), Some("Mon, 28 Aug 2017 15:00:00 -0700"));
assert_eq!(i.epoch(), 1503957600);
let second = channel.items().iter().nth(8).unwrap();
let i2 = parse_episode(&second, 0);
let i2 = new_episode(&second, 0).unwrap();
let descr2 = "This week we look at <a \
href=\"https://github.com/rust-lang/rfcs/pull/2071\">RFC 2071</a> \"Add \
impl Trait type alias and variable declarations\"";
assert_eq!(i2.title, Some("Episode #8 - An Existential Crisis"));
assert_eq!(i2.title(), Some("Episode #8 - An Existential Crisis"));
assert_eq!(
i2.uri,
Some(
"http://request-for-explanation.github.\
io/podcast/ep8-an-existential-crisis/episode.mp3",
)
i2.uri(),
"http://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/episode.\
mp3"
);
assert_eq!(i2.description, Some(descr2));
assert_eq!(i2.length, Some(13713219));
assert_eq!(i2.description(), Some(descr2));
assert_eq!(i2.length(), Some(13713219));
assert_eq!(
i2.guid,
Some("https://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/",)
i2.guid(),
Some("https://request-for-explanation.github.io/podcast/ep8-an-existential-crisis/")
);
assert_eq!(
i2.published_date,
Some("Tue, 15 Aug 2017 17:00:00 -0700".to_string())
);
assert_eq!(i2.epoch, 1502841600);
assert_eq!(i2.published_date(), Some("Tue, 15 Aug 2017 17:00:00 -0700"));
assert_eq!(i2.epoch(), 1502841600);
}
}
+5
View File
@@ -10,6 +10,8 @@ table! {
length -> Nullable<Integer>,
guid -> Nullable<Text>,
played -> Nullable<Integer>,
favorite -> Bool,
archive -> Bool,
podcast_id -> Integer,
}
}
@@ -21,6 +23,9 @@ table! {
link -> Text,
description -> Text,
image_uri -> Nullable<Text>,
favorite -> Bool,
archive -> Bool,
always_dl -> Bool,
source_id -> Integer,
}
}
+235
View File
@@ -0,0 +1,235 @@
//! Helper utilities for accomplishing various tasks.
use rayon::prelude::*;
use chrono::prelude::*;
use url::{Position, Url};
use errors::*;
use dbqueries;
use models::queryables::Episode;
use std::path::Path;
use std::fs;
fn download_checker() -> Result<()> {
let episodes = dbqueries::get_downloaded_episodes()?;
episodes
.into_par_iter()
.for_each(|mut ep| checker_helper(&mut ep));
Ok(())
}
fn checker_helper(ep: &mut Episode) {
if !Path::new(ep.local_uri().unwrap()).exists() {
ep.set_local_uri(None);
let res = ep.save();
if let Err(err) = res {
error!("Error while trying to update episode: {:#?}", ep);
error!("Error: {}", err);
};
}
}
fn played_cleaner() -> Result<()> {
let episodes = dbqueries::get_played_episodes()?;
let now_utc = Utc::now().timestamp() as i32;
episodes.into_par_iter().for_each(|mut ep| {
if ep.local_uri().is_some() && ep.played().is_some() {
let played = ep.played().unwrap();
// TODO: expose a config and a user set option.
// Chnage the test too when exposed
let limit = played + 172_800; // add 2days in seconds
if now_utc > limit {
let e = delete_local_content(&mut ep);
if let Err(err) = e {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("Error: {}", err);
} else {
info!("Episode {:?} was deleted succesfully.", ep.title());
};
}
}
});
Ok(())
}
/// Check `ep.local_uri` field and delete the file it points to.
pub fn delete_local_content(ep: &mut Episode) -> Result<()> {
if ep.local_uri().is_some() {
let uri = ep.local_uri().unwrap().to_owned();
if Path::new(&uri).exists() {
let res = fs::remove_file(&uri);
if res.is_ok() {
ep.set_local_uri(None);
ep.save()?;
} else {
error!("Error while trying to delete file: {}", uri);
error!("Error: {}", res.unwrap_err());
};
}
} else {
error!(
"Something went wrong evaluating the following path: {:?}",
ep.local_uri(),
);
}
Ok(())
}
/// Database cleaning tasks.
///
/// Runs a download checker which looks for `Episode.local_uri` entries that
/// doesn't exist and sets them to None
///
/// Runs a cleaner for played Episode's that are pass the lifetime limit and
/// scheduled for removal.
pub fn checkup() -> Result<()> {
download_checker()?;
played_cleaner()?;
Ok(())
}
/// Remove fragment identifiers and query pairs from a URL
/// If url parsing fails, return's a trimmed version of the original input.
pub fn url_cleaner(s: &str) -> String {
// Copied from the cookbook.
// https://rust-lang-nursery.github.io/rust-cookbook/net.html
// #remove-fragment-identifiers-and-query-pairs-from-a-url
match Url::parse(s) {
Ok(parsed) => parsed[..Position::AfterPath].to_owned(),
_ => s.trim().to_owned(),
}
}
#[cfg(test)]
mod tests {
extern crate tempdir;
use super::*;
use database::{connection, truncate_db};
use models::insertables::NewEpisodeBuilder;
use self::tempdir::TempDir;
use std::fs::File;
use std::io::Write;
fn helper_db() -> TempDir {
// Clean the db
truncate_db().unwrap();
// Setup tmp file stuff
let tmp_dir = TempDir::new("hammond_test").unwrap();
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
let bad_path = tmp_dir.path().join("invalid_thing.mp3");
let mut tmp_file = File::create(&valid_path).unwrap();
writeln!(tmp_file, "Foooo").unwrap();
// Setup episodes
let db = connection();
let con = db.get().unwrap();
NewEpisodeBuilder::new()
.uri("foo_bar".to_string())
.local_uri(Some(valid_path.to_str().unwrap().to_owned()))
.build()
.into_episode(&con)
.unwrap();
NewEpisodeBuilder::new()
.uri("bar_baz".to_string())
.local_uri(Some(bad_path.to_str().unwrap().to_owned()))
.build()
.into_episode(&con)
.unwrap();
tmp_dir
}
#[test]
fn test_download_checker() {
let _tmp_dir = helper_db();
download_checker().unwrap();
let episodes = dbqueries::get_downloaded_episodes().unwrap();
assert_eq!(episodes.len(), 1);
assert_eq!("foo_bar", episodes.first().unwrap().uri());
}
#[test]
fn test_checker_helper() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "bar_baz").unwrap()
};
checker_helper(&mut episode);
assert!(episode.local_uri().is_none());
}
#[test]
fn test_download_cleaner() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let valid_path = episode.local_uri().unwrap().to_owned();
delete_local_content(&mut episode).unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_expired() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let now_utc = Utc::now().timestamp() as i32;
// let limit = now_utc - 172_800;
let epoch = now_utc - 200_000;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should delete the file
played_cleaner().unwrap();
assert_eq!(Path::new(&valid_path).exists(), false);
}
#[test]
fn test_played_cleaner_none() {
let _tmp_dir = helper_db();
let mut episode = {
let db = connection();
let con = db.get().unwrap();
dbqueries::get_episode_from_uri(&con, "foo_bar").unwrap()
};
let now_utc = Utc::now().timestamp() as i32;
// limit = 172_800;
let epoch = now_utc - 20_000;
episode.set_played(Some(epoch));
episode.save().unwrap();
let valid_path = episode.local_uri().unwrap().to_owned();
// This should not delete the file
played_cleaner().unwrap();
assert_eq!(Path::new(&valid_path).exists(), true);
}
#[test]
fn test_url_cleaner() {
let good_url = "http://traffic.megaphone.fm/FL8608731318.mp3";
let bad_url = "http://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184";
assert_eq!(url_cleaner(bad_url), good_url);
assert_eq!(url_cleaner(good_url), good_url);
assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url);
}
}
+7 -8
View File
@@ -5,17 +5,16 @@ version = "0.1.0"
workspace = "../"
[dependencies]
error-chain = "0.11"
hyper = "0.11"
log = "0.3"
mime_guess = "1.8"
rand = "0.3"
reqwest = "0.8"
tempdir = "0.3"
error-chain = "0.11.0"
hyper = "0.11.7"
log = "0.3.8"
mime_guess = "1.8.2"
reqwest = "0.8.1"
tempdir = "0.3.5"
[dependencies.diesel]
features = ["sqlite"]
version = "0.16"
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.hammond-data]
path = "../hammond-data"
+103 -96
View File
@@ -1,8 +1,6 @@
use reqwest;
use hyper::header::*;
use tempdir::TempDir;
use rand;
use rand::Rng;
use mime_guess;
use std::fs::{rename, DirBuilder, File};
@@ -10,9 +8,11 @@ use std::io::{BufWriter, Read, Write};
use std::path::Path;
use errors::*;
use hammond_data::index_feed::Database;
use hammond_data::models::{Episode, Podcast};
use hammond_data::{DL_DIR, HAMMOND_CACHE};
use hammond_data::{Episode, Podcast};
use hammond_data::xdg_dirs::{DL_DIR, HAMMOND_CACHE};
// TODO: Replace path that are of type &str with std::path.
// TODO: Have a convention/document absolute/relative paths, if they should end with / or not.
// Adapted from https://github.com/mattgathu/rget .
// I never wanted to write a custom downloader.
@@ -20,62 +20,62 @@ use hammond_data::{DL_DIR, HAMMOND_CACHE};
// Would much rather use a crate,
// or bindings for a lib like youtube-dl(python),
// But cant seem to find one.
// TODO: Write unit-tests.
fn download_into(dir: &str, file_title: &str, url: &str) -> Result<String> {
info!("GET request to: {}", url);
let client = reqwest::Client::builder().referer(false).build()?;
let mut resp = client.get(url).send()?;
info!("Status Resp: {}", resp.status());
if resp.status().is_success() {
let headers = resp.headers().clone();
if !resp.status().is_success() {
bail!("Unexpected server response: {}", resp.status())
}
let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);
let ct_type = headers.get::<ContentType>();
ct_len.map(|x| info!("File Lenght: {}", x));
ct_type.map(|x| info!("Content Type: {}", x));
let headers = resp.headers().clone();
// This could be prettier.
let ext = if let Some(t) = ct_type {
let mime = mime_guess::get_extensions(t.type_().as_ref(), t.subtype().as_ref());
if let Some(m) = mime {
if m.contains(&t.subtype().as_ref()) {
t.subtype().as_ref().to_string()
} else {
m.first().unwrap().to_string()
}
let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);
let ct_type = headers.get::<ContentType>();
ct_len.map(|x| info!("File Lenght: {}", x));
ct_type.map(|x| info!("Content Type: {}", x));
// This could be prettier.
// Determine the file extension from the http content-type header.
let ext = if let Some(t) = ct_type {
let mime = mime_guess::get_extensions(t.type_().as_ref(), t.subtype().as_ref());
if let Some(m) = mime {
if m.contains(&t.subtype().as_ref()) {
t.subtype().as_ref().to_string()
} else {
error!("Unkown mime type. {}", t);
"unkown".to_string()
m.first().unwrap().to_string()
}
} else {
error!("Unkown mime type.");
error!("Unkown mime type. {}", t);
"unkown".to_string()
};
info!("Extension: {}", ext);
}
} else {
error!("Unkown mime type.");
"unkown".to_string()
};
info!("Extension: {}", ext);
// Construct a temp file to save desired content.
let tempdir = TempDir::new_in(dir, "")?;
let mut rng = rand::thread_rng();
// Construct a temp file to save desired content.
let tempdir = TempDir::new_in(dir, "")?;
let out_file = format!(
"{}/{}.part",
tempdir.path().to_str().unwrap(),
rng.gen::<usize>()
);
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
save_io(&out_file, &mut resp, ct_len)?;
// Save requested content into the file.
save_io(&out_file, &mut resp, ct_len)?;
// Construct the desired path.
let target = format!("{}/{}.{}", dir, file_title, ext);
// Rename/move the tempfile into a permanent place.
rename(out_file, &target)?;
info!("Downloading of {} completed succesfully.", &target);
return Ok(target);
}
// Ok(String::from(""))
panic!("Bad request response.");
// Construct the desired path.
let target = format!("{}/{}.{}", dir, file_title, ext);
// Rename/move the tempfile into a permanent place upon success.
rename(out_file, &target)?;
info!("Downloading of {} completed succesfully.", &target);
Ok(target)
}
// TODO: Write unit-tests.
/// Handles the I/O of fetching a remote file and saving into a Buffer and A File.
fn save_io(file: &str, resp: &mut reqwest::Response, content_lenght: Option<u64>) -> Result<()> {
info!("Downloading into: {}", file);
let chunk_size = match content_lenght {
@@ -109,15 +109,16 @@ pub fn get_download_folder(pd_title: &str) -> Result<String> {
}
// TODO: Refactor
pub fn get_episode(connection: &Database, ep: &mut Episode, download_folder: &str) -> Result<()> {
pub fn get_episode(ep: &mut Episode, download_folder: &str) -> Result<()> {
// Check if its alrdy downloaded
if ep.local_uri().is_some() {
if Path::new(ep.local_uri().unwrap()).exists() {
return Ok(());
}
// If the path is not valid, then set it to None.
ep.set_local_uri(None);
ep.save(connection)?;
ep.save()?;
};
let res = download_into(download_folder, ep.title().unwrap(), ep.uri());
@@ -125,7 +126,7 @@ pub fn get_episode(connection: &Database, ep: &mut Episode, download_folder: &st
if let Ok(path) = res {
// If download succedes set episode local_uri to dlpath.
ep.set_local_uri(Some(&path));
ep.save(connection)?;
ep.save()?;
Ok(())
} else {
error!("Something whent wrong while downloading.");
@@ -134,57 +135,57 @@ pub fn get_episode(connection: &Database, ep: &mut Episode, download_folder: &st
}
pub fn cache_image(pd: &Podcast) -> Option<String> {
if pd.image_uri().is_some() {
let url = pd.image_uri().unwrap().to_owned();
if url == "" {
return None;
}
let url = pd.image_uri()?.to_owned();
if url == "" {
return None;
}
let download_fold = format!(
"{}{}",
HAMMOND_CACHE.to_str().unwrap(),
pd.title().to_owned()
);
let download_fold = format!(
"{}{}",
HAMMOND_CACHE.to_str().unwrap(),
pd.title().to_owned()
);
// Hacky way
// TODO: make it so it returns the first cover.* file encountered.
let png = format!("{}/cover.png", download_fold);
let jpg = format!("{}/cover.jpg", download_fold);
let jpe = format!("{}/cover.jpe", download_fold);
let jpeg = format!("{}/cover.jpeg", download_fold);
if Path::new(&png).exists() {
return Some(png);
} else if Path::new(&jpe).exists() {
return Some(jpe);
} else if Path::new(&jpg).exists() {
return Some(jpg);
} else if Path::new(&jpeg).exists() {
return Some(jpeg);
};
// Hacky way
// TODO: make it so it returns the first cover.* file encountered.
// Use glob instead
let png = format!("{}/cover.png", download_fold);
let jpg = format!("{}/cover.jpg", download_fold);
let jpe = format!("{}/cover.jpe", download_fold);
let jpeg = format!("{}/cover.jpeg", download_fold);
if Path::new(&png).exists() {
return Some(png);
} else if Path::new(&jpe).exists() {
return Some(jpe);
} else if Path::new(&jpg).exists() {
return Some(jpg);
} else if Path::new(&jpeg).exists() {
return Some(jpeg);
};
DirBuilder::new()
.recursive(true)
.create(&download_fold)
.unwrap();
DirBuilder::new()
.recursive(true)
.create(&download_fold)
.unwrap();
let dlpath = download_into(&download_fold, "cover", &url);
if let Ok(path) = dlpath {
info!("Cached img into: {}", &path);
return Some(path);
} else {
error!("Failed to get feed image.");
error!("Error: {}", dlpath.unwrap_err());
return None;
};
let dlpath = download_into(&download_fold, "cover", &url);
if let Ok(path) = dlpath {
info!("Cached img into: {}", &path);
Some(path)
} else {
error!("Failed to get feed image.");
error!("Error: {}", dlpath.unwrap_err());
None
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::{DL_DIR, HAMMOND_CACHE};
use hammond_data::models::NewPodcast;
use hammond_data::Source;
use hammond_data::feed::index;
use hammond_data::dbqueries;
use diesel::Identifiable;
use std::fs;
@@ -197,19 +198,25 @@ mod tests {
#[test]
fn test_cache_image() {
let pd = NewPodcast {
title: "New Rustacean".to_string(),
description: "".to_string(),
link: "".to_string(),
image_uri: Some("http://newrustacean.com/podcast.png".to_string()),
source_id: 0,
};
let pd = pd.into_podcast();
let url = "http://www.newrustacean.com/feed.xml";
// Create and index a source
let source = Source::from_url(url).unwrap();
// Copy it's id
let sid = source.id().clone();
// Convert Source it into a Feed and index it
let feed = source.into_feed().unwrap();
index(vec![feed]);
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
let img_path = cache_image(&pd);
let foo_ = format!(
"{}{}/cover.png",
HAMMOND_CACHE.to_str().unwrap(),
"New Rustacean"
pd.title()
);
assert_eq!(img_path, Some(foo_));
}
+2
View File
@@ -1,5 +1,6 @@
use diesel::result;
use reqwest;
use hammond_data;
use std::io;
error_chain! {
@@ -7,5 +8,6 @@ error_chain! {
ReqError(reqwest::Error);
IoError(io::Error);
DieselResultError(result::Error);
DataError(hammond_data::errors::Error);
}
}
-1
View File
@@ -8,7 +8,6 @@ extern crate hyper;
#[macro_use]
extern crate log;
extern crate mime_guess;
extern crate rand;
extern crate reqwest;
extern crate tempdir;
+18 -10
View File
@@ -6,19 +6,27 @@ version = "0.1.0"
workspace = "../"
[dependencies]
dissolve = "0.2"
gdk = "0.6"
gdk-pixbuf = "0.2"
gio = "0.2"
glib = "0.3"
log = "0.3"
loggerv = "0.4"
open = "1.2"
rayon = "0.9"
dissolve = "0.2.2"
gdk = "0.7.0"
gdk-pixbuf = "0.3.0"
gio = "0.3.0"
glib = "0.4.0"
log = "0.3.8"
loggerv = "0.5.1"
open = "1.2.1"
rayon = "0.9.0"
[dependencies.diesel]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.diesel_codegen]
features = ["sqlite"]
git = "https://github.com/diesel-rs/diesel.git"
[dependencies.gtk]
features = ["v3_22"]
version = "0.2"
version = "0.3.0"
[dependencies.hammond-data]
path = "../hammond-data"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 943 B

+1 -1
View File
@@ -77,7 +77,7 @@
<property name="justify">center</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="max_width_chars">35</property>
<property name="max_width_chars">33</property>
</object>
<packing>
<property name="expand">False</property>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>org.gnome.Hammond</id>
<name>Hammond</name>
<project_license>GPL-3.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Daniel García Moreno</developer_name>
<summary>Gtk+ Matrix.org client</summary>
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
<description>
Hammond is a Fast, Safe and Reliable Gtk+ Podcast client written in Rust
</description>
<screenshots>
<screenshot>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/assets/podcasts_view.png</image>
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/assets/podcast_widget.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.1.1" date="2017-11-13"/>
</releases>
<update_contact>jordanpetridis@protonmail.com</update_contact>
</component>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gtk/hammond/">
<gresource prefix="/org/gnome/hammond/">
<file>banner.png</file>
<file preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
<file preprocess="xml-stripblanks">gtk/podcast_widget.ui</file>
+16 -18
View File
@@ -1,14 +1,14 @@
use gtk;
use gtk::prelude::*;
use hammond_data::index_feed;
use hammond_data::index_feed::Database;
use hammond_data::Source;
use hammond_data::utils::url_cleaner;
use podcasts_view::update_podcasts_view;
use utils;
pub fn get_headerbar(db: &Database, stack: &gtk::Stack) -> gtk::HeaderBar {
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/headerbar.ui");
pub fn get_headerbar(stack: &gtk::Stack) -> gtk::HeaderBar {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let header: gtk::HeaderBar = builder.get_object("headerbar1").unwrap();
let home_button: gtk::Button = builder.get_object("homebutton").unwrap();
@@ -23,9 +23,10 @@ pub fn get_headerbar(db: &Database, stack: &gtk::Stack) -> gtk::HeaderBar {
println!("{:?}", url.get_text());
});
add_button.connect_clicked(clone!(db, stack, add_popover => move |_| {
add_button.connect_clicked(clone!(stack, add_popover => move |_| {
let url = new_url.get_text().unwrap_or_default();
on_add_bttn_clicked(&db, &stack, &url);
let url = url_cleaner(&url);
on_add_bttn_clicked(&stack, &url);
// TODO: lock the button instead of hiding and add notification of feed added.
// TODO: map the spinner
@@ -36,34 +37,31 @@ pub fn get_headerbar(db: &Database, stack: &gtk::Stack) -> gtk::HeaderBar {
// TODO: make it a back arrow button, that will hide when appropriate,
// and add a StackSwitcher when more views are added.
home_button.connect_clicked(clone!(db, stack => move |_| {
home_button.connect_clicked(clone!(stack => move |_| {
let vis = stack.get_visible_child_name().unwrap();
stack.set_visible_child_name("fb_parent");
if vis != "pdw" {
update_podcasts_view(&db, &stack);
update_podcasts_view(&stack);
}
}));
// FIXME: There appears to be a memmory leak here.
refresh_button.connect_clicked(clone!(stack, db => move |_| {
utils::refresh_feed(&db, &stack, None, None);
refresh_button.connect_clicked(clone!(stack => move |_| {
utils::refresh_feed(&stack, None, None);
}));
header
}
fn on_add_bttn_clicked(db: &Database, stack: &gtk::Stack, url: &str) {
let source = {
let tempdb = db.lock().unwrap();
index_feed::insert_return_source(&tempdb, url)
};
info!("{:?} feed added", url);
fn on_add_bttn_clicked(stack: &gtk::Stack, url: &str) {
let source = Source::from_url(url);
if let Ok(s) = source {
info!("{:?} feed added", url);
// update the db
utils::refresh_feed(db, stack, Some(vec![s]), None);
utils::refresh_feed(stack, Some(vec![s]), None);
} else {
error!("Expected Error, feed probably already exists.");
error!("Feed probably already exists.");
error!("Error: {:?}", source.unwrap_err());
}
}
+19 -16
View File
@@ -4,7 +4,7 @@ extern crate gio;
extern crate glib;
extern crate gtk;
// extern crate diesel;
extern crate diesel;
extern crate dissolve;
extern crate hammond_data;
extern crate hammond_downloader;
@@ -16,9 +16,7 @@ extern crate open;
// use rayon::prelude::*;
use log::LogLevel;
use hammond_data::dbcheckup;
use std::sync::{Arc, Mutex};
use hammond_data::utils::checkup;
use gtk::prelude::*;
use gio::{ActionMapExt, ApplicationExt, MenuExt, SimpleActionExt};
@@ -56,8 +54,6 @@ THIS IS STILL A PROTOTYPE.
*/
fn build_ui(app: &gtk::Application) {
let db = Arc::new(Mutex::new(hammond_data::establish_connection()));
let menu = gio::Menu::new();
menu.append("Quit", "app.quit");
menu.append("Checkup", "app.check");
@@ -67,7 +63,7 @@ fn build_ui(app: &gtk::Application) {
let window = gtk::ApplicationWindow::new(app);
window.set_default_size(1150, 650);
// Setup the Stack that will manage the switch between podcasts_view and podcast_widget.
let stack = podcasts_view::setup_stack(&db);
let stack = podcasts_view::setup_stack();
window.add(&stack);
window.connect_delete_event(|w, _| {
@@ -83,21 +79,26 @@ fn build_ui(app: &gtk::Application) {
});
app.add_action(&quit);
// Setup the dbcheckup in the app menu.
// Setup the checkup in the app menu.
let check = gio::SimpleAction::new("check", None);
check.connect_activate(clone!(db => move |_, _| {
let _ = dbcheckup::run(&db);
}));
check.connect_activate(move |_, _| {
let _ = checkup();
});
app.add_action(&check);
// queue a db update 1 minute after the startup.
gtk::idle_add(clone!(db, stack => move || {
utils::refresh_feed(&db, &stack, None, Some(60));
gtk::idle_add(clone!(stack => move || {
utils::refresh_feed(&stack, None, Some(60));
glib::Continue(false)
}));
gtk::idle_add(move || {
let _ = checkup();
glib::Continue(false)
});
// Get the headerbar
let header = headerbar::get_headerbar(&db, &stack);
let header = headerbar::get_headerbar(&stack);
window.set_titlebar(&header);
window.show_all();
@@ -106,10 +107,11 @@ fn build_ui(app: &gtk::Application) {
}
fn main() {
use gio::ApplicationExtManual;
// TODO: make the the logger a cli -vv option
loggerv::init_with_level(LogLevel::Info).unwrap();
static_resource::init().expect("Something went wrong with the resource file initialization.");
hammond_data::init().expect("Hammond Initialazation failed.");
let application = gtk::Application::new("org.gnome.Hammond", gio::ApplicationFlags::empty())
.expect("Initialization failed...");
@@ -118,5 +120,6 @@ fn main() {
build_ui(app);
});
application.run(&[]);
// application.run(&[]);
ApplicationExtManual::run(&application, &[]);
}
+1 -4
View File
@@ -6,11 +6,8 @@ pub fn init() -> Result<(), Error> {
let res_bytes = include_bytes!("../resources/resources.gresource");
// Create Resource it will live as long the value lives.
// TODO: change it into Bytes::From_static once the fix lands
// https://bugzilla.gnome.org/show_bug.cgi?id=790030
let gbytes = Bytes::from(&res_bytes.as_ref());
let gbytes = Bytes::from_static(res_bytes.as_ref());
let resource = Resource::new_from_data(&gbytes)?;
// let resource = Resource::new_from_data(&res_bytes.as_ref().into())?;
// Register the resource so It wont be dropped and will continue to live in memory.
resources_register(&resource);
+16 -23
View File
@@ -1,9 +1,8 @@
use glib;
use gtk;
use hammond_data::index_feed;
use hammond_data::models::Source;
use hammond_data::index_feed::Database;
use hammond_data::feed;
use hammond_data::Source;
use std::{thread, time};
use std::cell::RefCell;
@@ -11,7 +10,7 @@ use std::sync::mpsc::{channel, Receiver};
use views::podcasts_view;
type Foo = RefCell<Option<(Database, gtk::Stack, Receiver<bool>)>>;
type Foo = RefCell<Option<(gtk::Stack, Receiver<bool>)>>;
// Create a thread local storage that will store the arguments to be transfered.
thread_local!(static GLOBAL: Foo = RefCell::new(None));
@@ -20,49 +19,43 @@ thread_local!(static GLOBAL: Foo = RefCell::new(None));
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
/// `delay` represents the desired time in seconds for the thread to sleep before executing.
/// When It's done,it queues up a `podcast_view` refresh.
pub fn refresh_feed(
db: &Database,
stack: &gtk::Stack,
source: Option<Vec<Source>>,
delay: Option<u64>,
) {
pub fn refresh_feed(stack: &gtk::Stack, source: Option<Vec<Source>>, delay: Option<u64>) {
// Create a async channel.
let (sender, receiver) = channel();
// Pass the desired arguments into the Local Thread Storage.
GLOBAL.with(clone!(db, stack => move |global| {
*global.borrow_mut() = Some((db, stack, receiver));
GLOBAL.with(clone!(stack => move |global| {
*global.borrow_mut() = Some((stack, receiver));
}));
thread::spawn(clone!(db => move || {
if let Some(s) = delay{
thread::spawn(move || {
if let Some(s) = delay {
let t = time::Duration::from_secs(s);
thread::sleep(t);
}
let feeds = {
if let Some(mut vec) = source {
Ok(index_feed::fetch_feeds(&db, &mut vec))
if let Some(vec) = source {
Ok(feed::fetch(vec))
} else {
index_feed::fetch_all_feeds(&db)
feed::fetch_all()
}
};
if let Ok(mut x) = feeds {
index_feed::index_feed(&db, &mut x);
info!("Indexing done.");
if let Ok(x) = feeds {
feed::index(x);
sender.send(true).expect("Couldn't send data to channel");;
glib::idle_add(refresh_podcasts_view);
};
}));
});
}
fn refresh_podcasts_view() -> glib::Continue {
GLOBAL.with(|global| {
if let Some((ref db, ref stack, ref reciever)) = *global.borrow() {
if let Some((ref stack, ref reciever)) = *global.borrow() {
if reciever.try_recv().is_ok() {
podcasts_view::update_podcasts_view(db, stack);
podcasts_view::update_podcasts_view(stack);
}
}
});
+32 -37
View File
@@ -1,15 +1,15 @@
use gtk;
use gtk::prelude::*;
use gdk_pixbuf::Pixbuf;
use diesel::associations::Identifiable;
use hammond_data::dbqueries;
use hammond_data::models::Podcast;
use hammond_data::index_feed::Database;
use hammond_data::Podcast;
use widgets::podcast::*;
fn setup_empty_view(stack: &gtk::Stack) {
let builder = gtk::Builder::new_from_resource("/org/gtk/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();
stack.add_named(&view, "empty");
}
@@ -20,23 +20,20 @@ fn show_empty_view(stack: &gtk::Stack) {
info!("Empty view.");
}
fn populate_flowbox(db: &Database, flowbox: &gtk::FlowBox) {
let podcasts = {
let db = db.lock().unwrap();
dbqueries::get_podcasts(&db)
};
fn populate_flowbox(flowbox: &gtk::FlowBox) {
let podcasts = dbqueries::get_podcasts();
if let Ok(pds) = podcasts {
pds.iter().for_each(|parent| {
let f = create_flowbox_child(db, parent);
let f = create_flowbox_child(parent);
flowbox.add(&f);
});
flowbox.show_all();
}
}
fn create_flowbox_child(db: &Database, pd: &Podcast) -> gtk::FlowBoxChild {
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/podcasts_child.ui");
fn create_flowbox_child(pd: &Podcast) -> gtk::FlowBoxChild {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcasts_child.ui");
// Copy of gnome-music AlbumWidget
let box_: gtk::Box = builder.get_object("fb_child").unwrap();
@@ -52,24 +49,22 @@ fn create_flowbox_child(db: &Database, pd: &Podcast) -> gtk::FlowBoxChild {
pd_cover.set_from_pixbuf(&img);
};
configure_banner(db, pd, &banner, &banner_title);
configure_banner(pd, &banner, &banner_title);
let fbc = gtk::FlowBoxChild::new();
// There's probably a better way to store the id somewhere.
fbc.set_name(&pd.id().to_string());
// fbc.set_name(&pd.id().to_string());
WidgetExt::set_name(&fbc, &pd.id().to_string());
fbc.add(&box_);
fbc
}
fn configure_banner(db: &Database, pd: &Podcast, banner: &gtk::Image, banner_title: &gtk::Label) {
let bann = Pixbuf::new_from_resource_at_scale("/org/gtk/hammond/banner.png", 256, 256, true);
fn configure_banner(pd: &Podcast, banner: &gtk::Image, banner_title: &gtk::Label) {
let bann = Pixbuf::new_from_resource_at_scale("/org/gnome/hammond/banner.png", 256, 256, true);
if let Ok(b) = bann {
banner.set_from_pixbuf(&b);
let new_episodes = {
let tempdb = db.lock().unwrap();
dbqueries::get_pd_unplayed_episodes(&tempdb, pd)
};
let new_episodes = dbqueries::get_pd_unplayed_episodes(pd);
if let Ok(n) = new_episodes {
if !n.is_empty() {
@@ -81,9 +76,9 @@ fn configure_banner(db: &Database, pd: &Podcast, banner: &gtk::Image, banner_tit
}
}
fn on_flowbox_child_activate(db: &Database, stack: &gtk::Stack, parent: &Podcast) {
fn on_flowbox_child_activate(stack: &gtk::Stack, parent: &Podcast) {
let old = stack.get_child_by_name("pdw").unwrap();
let pdw = podcast_widget(db, stack, parent);
let pdw = podcast_widget(stack, parent);
stack.remove(&old);
stack.add_named(&pdw, "pdw");
@@ -94,11 +89,11 @@ fn on_flowbox_child_activate(db: &Database, stack: &gtk::Stack, parent: &Podcast
old.destroy();
}
fn setup_podcasts_flowbox(db: &Database, stack: &gtk::Stack) -> gtk::FlowBox {
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/podcasts_view.ui");
fn setup_podcasts_flowbox(stack: &gtk::Stack) -> gtk::FlowBox {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcasts_view.ui");
let fb_parent: gtk::Box = builder.get_object("fb_parent").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
init_flowbox(db, stack, &flowbox);
init_flowbox(stack, &flowbox);
stack.add_named(&fb_parent, "fb_parent");
@@ -111,21 +106,21 @@ fn setup_podcasts_flowbox(db: &Database, stack: &gtk::Stack) -> gtk::FlowBox {
flowbox
}
pub fn setup_stack(db: &Database) -> gtk::Stack {
pub fn setup_stack() -> gtk::Stack {
let stack = gtk::Stack::new();
stack.set_transition_type(gtk::StackTransitionType::SlideLeftRight);
setup_empty_view(&stack);
setup_podcast_widget(&stack);
setup_podcasts_flowbox(db, &stack);
setup_podcasts_flowbox(&stack);
stack
}
pub fn update_podcasts_view(db: &Database, stack: &gtk::Stack) {
pub fn update_podcasts_view(stack: &gtk::Stack) {
let vis = stack.get_visible_child_name().unwrap();
let old = stack.get_child_by_name("fb_parent").unwrap();
stack.remove(&old);
let flowbox = setup_podcasts_flowbox(db, stack);
let flowbox = setup_podcasts_flowbox(stack);
if vis == "empty" && !flowbox.get_children().is_empty() {
stack.set_visible_child_name("fb_parent");
@@ -141,17 +136,17 @@ pub fn update_podcasts_view(db: &Database, stack: &gtk::Stack) {
old.destroy();
}
fn init_flowbox(db: &Database, stack: &gtk::Stack, flowbox: &gtk::FlowBox) {
fn init_flowbox(stack: &gtk::Stack, flowbox: &gtk::FlowBox) {
use gtk::WidgetExt;
// TODO: handle unwraps.
flowbox.connect_child_activated(clone!(db, stack => move |_, child| {
flowbox.connect_child_activated(clone!(stack => move |_, child| {
// This is such an ugly hack...
let id = child.get_name().unwrap().parse::<i32>().unwrap();
let parent = {
let tempdb = db.lock().unwrap();
dbqueries::get_podcast_from_id(&tempdb, id).unwrap()
};
on_flowbox_child_activate(&db, &stack, &parent);
// let id = child.get_name().unwrap().parse::<i32>().unwrap();
let id = WidgetExt::get_name(child).unwrap().parse::<i32>().unwrap();
let parent = dbqueries::get_podcast_from_id(id).unwrap();
on_flowbox_child_activate(&stack, &parent);
}));
// Populate the flowbox with the Podcasts.
populate_flowbox(db, flowbox);
populate_flowbox(flowbox);
}
+32 -47
View File
@@ -1,13 +1,12 @@
use open;
use hammond_data::dbqueries;
use hammond_data::models::{Episode, Podcast};
use hammond_data::{Episode, Podcast};
use hammond_downloader::downloader;
use hammond_data::index_feed::Database;
use hammond_data::dbcheckup::*;
use hammond_data::utils::*;
use hammond_data::errors::*;
use dissolve::strip_html_tags;
use diesel::associations::Identifiable;
use std::thread;
use std::cell::RefCell;
@@ -23,9 +22,9 @@ type Foo = RefCell<Option<(gtk::Button, gtk::Button, gtk::Button, Receiver<bool>
thread_local!(static GLOBAL: Foo = RefCell::new(None));
fn epidose_widget(db: &Database, episode: &mut Episode, pd_title: &str) -> gtk::Box {
fn epidose_widget(episode: &mut Episode, pd_title: &str) -> gtk::Box {
// This is just a prototype and will be reworked probably.
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/episode_widget.ui");
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
let ep: gtk::Box = builder.get_object("episode_box").unwrap();
let download_button: gtk::Button = builder.get_object("download_button").unwrap();
@@ -69,58 +68,52 @@ fn epidose_widget(db: &Database, episode: &mut Episode, pd_title: &str) -> gtk::
delete_button.show();
}
play_button.connect_clicked(
clone!(db, episode, played_button, unplayed_button => move |_| {
on_play_bttn_clicked(&db, episode.id());
let _ = set_played_now(&db, &mut episode.clone());
play_button.connect_clicked(clone!(episode, played_button, unplayed_button => move |_| {
let mut episode = episode.clone();
on_play_bttn_clicked(*episode.id());
let _ = episode.set_played_now();
played_button.hide();
unplayed_button.show();
}),
);
}));
delete_button.connect_clicked(
clone!(episode, db, play_button, download_button => move |del| {
on_delete_bttn_clicked(&db, episode.id());
delete_button.connect_clicked(clone!(episode, play_button, download_button => move |del| {
on_delete_bttn_clicked(*episode.id());
del.hide();
play_button.hide();
download_button.show();
}),
);
}));
played_button.connect_clicked(clone!(db, episode, unplayed_button => move |played| {
let _ = set_played_now(&db, &mut episode.clone());
played_button.connect_clicked(clone!(episode, unplayed_button => move |played| {
let mut episode = episode.clone();
let _ = episode.set_played_now();
played.hide();
unplayed_button.show();
}));
unplayed_button.connect_clicked(clone!(db, episode, played_button => move |un| {
unplayed_button.connect_clicked(clone!(episode, played_button => move |un| {
let mut episode = episode.clone();
episode.set_played(None);
let _ = episode.save(&db);
let _ = episode.save();
un.hide();
played_button.show();
}));
let pd_title = pd_title.to_owned();
download_button.connect_clicked(
clone!(db, play_button, delete_button, episode => move |dl| {
download_button.connect_clicked(clone!(play_button, delete_button, episode => move |dl| {
on_download_clicked(
&db,
&pd_title,
&mut episode.clone(),
dl,
&play_button,
&delete_button,
);
}),
);
}));
ep
}
// TODO: show notification when dl is finished.
fn on_download_clicked(
db: &Database,
pd_title: &str,
ep: &mut Episode,
download_bttn: &gtk::Button,
@@ -137,23 +130,20 @@ fn on_download_clicked(
let pd_title = pd_title.to_owned();
let mut ep = ep.clone();
thread::spawn(clone!(db => move || {
thread::spawn(move || {
let download_fold = downloader::get_download_folder(&pd_title).unwrap();
let e = downloader::get_episode(&db, &mut ep, download_fold.as_str());
let e = downloader::get_episode(&mut ep, download_fold.as_str());
if let Err(err) = e {
error!("Error while trying to download: {}", ep.uri());
error!("Error: {}", err);
};
sender.send(true).expect("Couldn't send data to channel");;
glib::idle_add(receive);
}));
});
}
fn on_play_bttn_clicked(db: &Database, episode_id: i32) {
let local_uri = {
let tempdb = db.lock().unwrap();
dbqueries::get_episode_local_uri_from_id(&tempdb, episode_id).unwrap()
};
fn on_play_bttn_clicked(episode_id: i32) {
let local_uri = dbqueries::get_episode_local_uri_from_id(episode_id).unwrap();
if let Some(uri) = local_uri {
if Path::new(&uri).exists() {
@@ -172,13 +162,10 @@ fn on_play_bttn_clicked(db: &Database, episode_id: i32) {
}
}
fn on_delete_bttn_clicked(db: &Database, episode_id: i32) {
let mut ep = {
let tempdb = db.lock().unwrap();
dbqueries::get_episode_from_id(&tempdb, episode_id).unwrap()
};
fn on_delete_bttn_clicked(episode_id: i32) {
let mut ep = dbqueries::get_episode_from_id(episode_id).unwrap();
let e = delete_local_content(db, &mut ep);
let e = delete_local_content(&mut ep);
if let Err(err) = e {
error!("Error while trying to delete file: {:?}", ep.local_uri());
error!("Error: {}", err);
@@ -200,14 +187,12 @@ fn receive() -> glib::Continue {
glib::Continue(false)
}
pub fn episodes_listbox(db: &Database, pd: &Podcast) -> Result<gtk::ListBox> {
let conn = db.lock().unwrap();
let mut episodes = dbqueries::get_pd_episodes(&conn, pd)?;
drop(conn);
pub fn episodes_listbox(pd: &Podcast) -> Result<gtk::ListBox> {
let episodes = dbqueries::get_pd_episodes(pd)?;
let list = gtk::ListBox::new();
episodes.iter_mut().for_each(|ep| {
let w = epidose_widget(db, ep, pd.title());
episodes.into_iter().for_each(|mut ep| {
let w = epidose_widget(&mut ep, pd.title());
list.add(&w)
});
+35 -41
View File
@@ -5,16 +5,15 @@ use gdk_pixbuf::Pixbuf;
use std::fs;
use hammond_data::dbqueries;
use hammond_data::models::Podcast;
use hammond_data::index_feed::Database;
use hammond_data::Podcast;
use hammond_downloader::downloader;
use widgets::episode::episodes_listbox;
use podcasts_view::update_podcasts_view;
pub fn podcast_widget(db: &Database, stack: &gtk::Stack, pd: &Podcast) -> gtk::Box {
pub fn podcast_widget(stack: &gtk::Stack, pd: &Podcast) -> gtk::Box {
// Adapted from gnome-music AlbumWidget
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/podcast_widget.ui");
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcast_widget.ui");
let pd_widget: gtk::Box = builder.get_object("podcast_widget").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
@@ -25,12 +24,12 @@ pub fn podcast_widget(db: &Database, stack: &gtk::Stack, pd: &Podcast) -> gtk::B
let played_button: gtk::Button = builder.get_object("mark_all_played_button").unwrap();
// TODO: should spawn a thread to avoid locking the UI probably.
unsub_button.connect_clicked(clone!(db, stack, pd => move |bttn| {
on_unsub_button_clicked(&db, &stack, &pd, bttn);
unsub_button.connect_clicked(clone!(stack, pd => move |bttn| {
on_unsub_button_clicked(&stack, &pd, bttn);
}));
title_label.set_text(pd.title());
let listbox = episodes_listbox(db, pd);
let listbox = episodes_listbox(pd);
if let Ok(l) = listbox {
view.add(&l);
}
@@ -45,22 +44,17 @@ pub fn podcast_widget(db: &Database, stack: &gtk::Stack, pd: &Podcast) -> gtk::B
cover.set_from_pixbuf(&i);
}
played_button.connect_clicked(clone!(db, stack, pd => move |_| {
on_played_button_clicked(&db, &stack, &pd);
played_button.connect_clicked(clone!(stack, pd => move |_| {
on_played_button_clicked(&stack, &pd);
}));
show_played_button(db, pd, &played_button);
show_played_button(pd, &played_button);
pd_widget
}
fn on_unsub_button_clicked(
db: &Database,
stack: &gtk::Stack,
pd: &Podcast,
unsub_button: &gtk::Button,
) {
let res = dbqueries::remove_feed(db, pd);
fn on_unsub_button_clicked(stack: &gtk::Stack, pd: &Podcast, unsub_button: &gtk::Button) {
let res = dbqueries::remove_feed(pd);
if res.is_ok() {
info!("{} was removed succesfully.", pd.title());
// hack to get away without properly checking for none.
@@ -76,23 +70,17 @@ fn on_unsub_button_clicked(
};
}
stack.set_visible_child_name("fb_parent");
update_podcasts_view(db, stack);
update_podcasts_view(stack);
}
fn on_played_button_clicked(db: &Database, stack: &gtk::Stack, pd: &Podcast) {
{
let tempdb = db.lock().unwrap();
let _ = dbqueries::update_none_to_played_now(&tempdb, pd);
}
fn on_played_button_clicked(stack: &gtk::Stack, pd: &Podcast) {
let _ = dbqueries::update_none_to_played_now(pd);
update_podcast_widget(db, stack, pd);
update_podcast_widget(stack, pd);
}
fn show_played_button(db: &Database, pd: &Podcast, played_button: &gtk::Button) {
let new_episodes = {
let tempdb = db.lock().unwrap();
dbqueries::get_pd_unplayed_episodes(&tempdb, pd)
};
fn show_played_button(pd: &Podcast, played_button: &gtk::Button) {
let new_episodes = dbqueries::get_pd_unplayed_episodes(pd);
if let Ok(n) = new_episodes {
if !n.is_empty() {
@@ -111,15 +99,15 @@ pub fn get_pixbuf_from_path(pd: &Podcast) -> Option<Pixbuf> {
}
pub fn setup_podcast_widget(stack: &gtk::Stack) {
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/podcast_widget.ui");
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/podcast_widget.ui");
let pd_widget: gtk::Box = builder.get_object("podcast_widget").unwrap();
stack.add_named(&pd_widget, "pdw");
}
pub fn update_podcast_widget(db: &Database, stack: &gtk::Stack, pd: &Podcast) {
pub fn update_podcast_widget(stack: &gtk::Stack, pd: &Podcast) {
let old = stack.get_child_by_name("pdw").unwrap();
let pdw = podcast_widget(db, stack, pd);
let pdw = podcast_widget(stack, pd);
let vis = stack.get_visible_child_name().unwrap();
stack.remove(&old);
@@ -130,20 +118,26 @@ pub fn update_podcast_widget(db: &Database, stack: &gtk::Stack, pd: &Podcast) {
#[cfg(test)]
mod tests {
use hammond_data::models::NewPodcast;
use hammond_data::Source;
use hammond_data::feed::index;
use diesel::Identifiable;
use super::*;
#[test]
fn test_get_pixbuf_from_path() {
let pd = NewPodcast {
title: "New Rustacean".to_string(),
description: "".to_string(),
link: "".to_string(),
image_uri: Some("http://newrustacean.com/podcast.png".to_string()),
source_id: 0,
};
let pd = pd.into_podcast();
let url = "http://www.newrustacean.com/feed.xml";
// Create and index a source
let source = Source::from_url(url).unwrap();
// Copy it's id
let sid = source.id().clone();
// Convert Source it into a Feed and index it
let feed = source.into_feed().unwrap();
index(vec![feed]);
// Get the Podcast
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
let pxbuf = get_pixbuf_from_path(&pd);
assert!(pxbuf.is_some());
}
+3 -2
View File
@@ -3,7 +3,7 @@
project(
'hammond', 'rust',
version: '0.1.0',
version: '0.2.0',
license: 'GPLv3',
)
@@ -16,7 +16,8 @@ hammond_version_micro = version_array[2].to_int()
hammond_prefix = get_option('prefix')
hammond_bindir = join_paths(hammond_prefix, get_option('bindir'))
install_data('assets/org.gnome.Hammond.desktop', install_dir : get_option('datadir') + '/applications')
install_data('hammond-gtk/resources/org.gnome.Hammond.desktop', install_dir : get_option('datadir') + '/applications')
install_data('hammond-gtk/resources/org.gnome.Hammond.appdata.xml', install_dir : get_option('datadir') + '/appdata')
cargo = find_program('cargo', required: false)
gresource = find_program('glib-compile-resources', required: false)
+16 -15
View File
@@ -3,7 +3,9 @@
"runtime": "org.gnome.Platform",
"runtime-version": "3.26",
"sdk": "org.gnome.Sdk",
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "hammond",
"finish-args": [
"--share=network",
@@ -11,7 +13,7 @@
"--socket=wayland",
"--talk-name=org.freedesktop.Notifications"
],
"build-options" : {
"build-options": {
"env": {
"CARGO_HOME": "/run/build/Hammond/cargo"
},
@@ -19,19 +21,18 @@
},
"modules": [
{
"name": "Hammond",
"buildsystem": "simple",
"build-commands": [
"source /usr/lib/sdk/rust-stable/enable.sh && ./configure --prefix=/app && make && make install"
"name": "Hammond",
"buildsystem": "simple",
"build-commands": [
"source /usr/lib/sdk/rust-stable/enable.sh && ./configure --prefix=/app && make && make install"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/alatiera/Hammond.git",
"branch": "master"
}
]
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/alatiera/Hammond.git",
"branch": "master"
}
]
}
]
}
}
+1 -2
View File
@@ -5,9 +5,8 @@ max_width = 100
comment_width = 100
wrap_comments = true
error_on_line_overflow = true
error_on_line_overflow_comments = true
error_on_line_overflow_comments = false
tab_spaces = 4
fn_call_width = 60
newline_style = "Unix"
fn_call_style = "Block"
report_todo = "Never"