Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e47e9b4e | ||
|
|
13751e3708 | ||
|
|
c92e40e3cf | ||
|
|
2051f0f58e | ||
|
|
c07215d357 | ||
|
|
e4a7a7becf | ||
|
|
7dd8755bcb | ||
|
|
76f9f36609 | ||
|
|
4d669ef297 | ||
|
|
3707b70b41 | ||
|
|
d81f9c6ad7 | ||
|
|
34e808182c | ||
|
|
c4030e51df | ||
|
|
cb65782829 | ||
|
|
ee4f08c5cf | ||
|
|
9e03aaae27 | ||
|
|
866904a687 | ||
|
|
9d0df9de46 | ||
|
|
7eff93ecf1 | ||
|
|
6336f7cac1 | ||
|
|
39dff5e27a | ||
|
|
ce3a76aee1 | ||
|
|
39cccf85a5 | ||
|
|
2f801043c1 | ||
|
|
f83f894e44 | ||
|
|
f0a0fdfa83 | ||
|
|
7498a37f68 | ||
|
|
faab32ba17 | ||
|
|
62da8bbb52 | ||
|
|
12ffe5c231 | ||
|
|
d9ef4fed53 | ||
|
|
4a195eb1b7 | ||
|
|
f7f0a16858 | ||
|
|
91f4a30c01 | ||
|
|
5570fdd118 | ||
|
|
aa7c493e81 | ||
|
|
a58671ea9a | ||
|
|
02b27d33b6 | ||
|
|
6098f0f54a | ||
|
|
3bcb23b39a | ||
|
|
31328355b1 | ||
|
|
9bc6df2cba | ||
|
|
ee7c5ca26e | ||
|
|
7be0b5bbf8 | ||
|
|
5890fe2bb4 | ||
|
|
0ce4107be4 | ||
|
|
ce29602431 | ||
|
|
e07683c046 | ||
|
|
5ad52fa4b2 | ||
|
|
beaeeffba8 | ||
|
|
47b32ac000 | ||
|
|
55442529a8 | ||
|
|
052988a2b1 | ||
|
|
a6dbf65575 | ||
|
|
563f249a48 | ||
|
|
5c84b77434 | ||
|
|
494761beaf | ||
|
|
1bf5187e48 | ||
|
|
6894f33895 | ||
|
|
450643371c | ||
|
|
1ec4370c7b | ||
|
|
e66a337468 | ||
|
|
282a29e7dd | ||
|
|
8eca46871d | ||
|
|
2152aa9034 | ||
|
|
3d108d4a7a | ||
|
|
54a0f17588 | ||
|
|
3c6176b1c0 | ||
|
|
e2403255cf | ||
|
|
cfb876e006 | ||
|
|
118846f255 | ||
|
|
8b4701f1a1 | ||
|
|
a286014a62 | ||
|
|
6996759db5 | ||
|
|
58e11f17b7 | ||
|
|
edd694e7d6 | ||
|
|
03637903c6 | ||
|
|
ab002ebcd7 | ||
|
|
1e45adc034 | ||
|
|
4bf84ed170 | ||
|
|
e3f0263dc0 |
@@ -7,3 +7,7 @@ Cargo.lock
|
||||
_build
|
||||
vendor/
|
||||
.flatpak-builder/
|
||||
flatpak-build/
|
||||
flatpak-repo/
|
||||
repo/
|
||||
Makefile
|
||||
|
||||
+6
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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.
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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)?)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod insertables;
|
||||
pub(crate) mod queryables;
|
||||
@@ -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 \
|
||||
Blackwater’s 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 York’s 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 didn’t 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 Clinton’s Failed Bid for President",
|
||||
)
|
||||
i2.title(),
|
||||
Some("The Breakthrough: Behind the Scenes of Hillary Clinton’s 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. It’s 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 \
|
||||
Wayland’s 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, \
|
||||
Microsoft’s 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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_));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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,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>
|
||||
|
||||
@@ -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: >k::Stack) -> gtk::HeaderBar {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/headerbar.ui");
|
||||
pub fn get_headerbar(stack: >k::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: >k::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: >k::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: >k::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: >k::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
@@ -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: >k::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: >k::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: >k::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: >k::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, &[]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: >k::Stack,
|
||||
source: Option<Vec<Source>>,
|
||||
delay: Option<u64>,
|
||||
) {
|
||||
pub fn refresh_feed(stack: >k::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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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: >k::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: >k::Stack) {
|
||||
info!("Empty view.");
|
||||
}
|
||||
|
||||
fn populate_flowbox(db: &Database, flowbox: >k::FlowBox) {
|
||||
let podcasts = {
|
||||
let db = db.lock().unwrap();
|
||||
dbqueries::get_podcasts(&db)
|
||||
};
|
||||
fn populate_flowbox(flowbox: >k::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: >k::Image, banner_title: >k::Label) {
|
||||
let bann = Pixbuf::new_from_resource_at_scale("/org/gtk/hammond/banner.png", 256, 256, true);
|
||||
fn configure_banner(pd: &Podcast, banner: >k::Image, banner_title: >k::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: >k::Image, banner_tit
|
||||
}
|
||||
}
|
||||
|
||||
fn on_flowbox_child_activate(db: &Database, stack: >k::Stack, parent: &Podcast) {
|
||||
fn on_flowbox_child_activate(stack: >k::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: >k::Stack, parent: &Podcast
|
||||
old.destroy();
|
||||
}
|
||||
|
||||
fn setup_podcasts_flowbox(db: &Database, stack: >k::Stack) -> gtk::FlowBox {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gtk/hammond/gtk/podcasts_view.ui");
|
||||
fn setup_podcasts_flowbox(stack: >k::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: >k::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: >k::Stack) {
|
||||
pub fn update_podcasts_view(stack: >k::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: >k::Stack) {
|
||||
old.destroy();
|
||||
}
|
||||
|
||||
fn init_flowbox(db: &Database, stack: >k::Stack, flowbox: >k::FlowBox) {
|
||||
fn init_flowbox(stack: >k::Stack, flowbox: >k::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);
|
||||
}
|
||||
|
||||
@@ -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: >k::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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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: >k::Stack, pd: &Podcast) -> gtk::Box {
|
||||
pub fn podcast_widget(stack: >k::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: >k::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: >k::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: >k::Stack,
|
||||
pd: &Podcast,
|
||||
unsub_button: >k::Button,
|
||||
) {
|
||||
let res = dbqueries::remove_feed(db, pd);
|
||||
fn on_unsub_button_clicked(stack: >k::Stack, pd: &Podcast, unsub_button: >k::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: >k::Stack, pd: &Podcast) {
|
||||
{
|
||||
let tempdb = db.lock().unwrap();
|
||||
let _ = dbqueries::update_none_to_played_now(&tempdb, pd);
|
||||
}
|
||||
fn on_played_button_clicked(stack: >k::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: >k::Button) {
|
||||
let new_episodes = {
|
||||
let tempdb = db.lock().unwrap();
|
||||
dbqueries::get_pd_unplayed_episodes(&tempdb, pd)
|
||||
};
|
||||
fn show_played_button(pd: &Podcast, played_button: >k::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: >k::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: >k::Stack, pd: &Podcast) {
|
||||
pub fn update_podcast_widget(stack: >k::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: >k::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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user