Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd52c5afdf |
+3
-13
@@ -1,19 +1,9 @@
|
|||||||
target/
|
target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
.vscode
|
.vscode
|
||||||
*.ui~
|
*.ui~
|
||||||
resources.gresource
|
resources.gresource
|
||||||
_build/
|
_build
|
||||||
build/
|
|
||||||
vendor/
|
vendor/
|
||||||
.criterion/
|
.criterion
|
||||||
org.gnome.*.json~
|
|
||||||
podcasts-gtk/po/gnome-podcasts.pot
|
|
||||||
|
|
||||||
# scripts/test.sh
|
|
||||||
target_*/
|
|
||||||
|
|
||||||
# flatpak-builder stuff
|
|
||||||
.flatpak-builder/
|
|
||||||
app/
|
|
||||||
repo/
|
|
||||||
|
|||||||
+85
-37
@@ -4,56 +4,90 @@ stages:
|
|||||||
- review
|
- review
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
BUNDLE: "org.gnome.Podcasts.${CI_COMMIT_SHA}.flatpak"
|
BUNDLE: "hammond-dev.flatpak"
|
||||||
|
|
||||||
|
.cargo_cache_template: &cargo_cache
|
||||||
|
cache:
|
||||||
|
# JOB_NAME - Each job will have it's own cache
|
||||||
|
# COMMIT_REF_SLUG = Lowercase name of the branch
|
||||||
|
# ^ Keep diffrerent caches for each branch
|
||||||
|
key: "$CI_JOB_NAME"
|
||||||
|
paths:
|
||||||
|
- target/
|
||||||
|
- .cargo_cache/
|
||||||
|
|
||||||
|
.cargo_test_template: &cargo_test
|
||||||
|
stage: test
|
||||||
|
|
||||||
|
variables:
|
||||||
|
RUSTFLAGS: "--cfg rayon_unstable"
|
||||||
|
RUST_BACKTRACE: "FULL"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- apt-get update -yqq
|
||||||
|
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
|
||||||
|
|
||||||
|
- mkdir -p .cargo_cache
|
||||||
|
# Only stuff inside the repo directory can be cached
|
||||||
|
# Override the CARGO_HOME variable to force it location
|
||||||
|
- export CARGO_HOME="${PWD}/.cargo_cache"
|
||||||
|
script:
|
||||||
|
- rustc -Vv && cargo -Vv
|
||||||
|
# Force regeneration of gresources regardless of artifacts chage
|
||||||
|
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
|
||||||
|
|
||||||
|
- cargo build
|
||||||
|
- cargo test -- --test-threads=1
|
||||||
|
- cargo test -- --test-threads=1 --ignored
|
||||||
|
<<: *cargo_cache
|
||||||
|
|
||||||
|
rust:stable:
|
||||||
|
# https://hub.docker.com/_/rust/
|
||||||
|
image: "rust"
|
||||||
|
<<: *cargo_test
|
||||||
|
|
||||||
|
rust:nightly:
|
||||||
|
# https://hub.docker.com/r/rustlang/rust/
|
||||||
|
image: "rustlang/rust:nightly"
|
||||||
|
<<: *cargo_test
|
||||||
|
only:
|
||||||
|
- schedule
|
||||||
|
- web
|
||||||
|
|
||||||
flatpak:
|
flatpak:
|
||||||
image: "registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:master"
|
image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.28
|
||||||
stage: "test"
|
stage: test
|
||||||
variables:
|
|
||||||
MANIFEST_PATH: "org.gnome.PodcastsDevel.json"
|
|
||||||
FLATPAK_MODULE: "gnome-podcasts"
|
|
||||||
CONFIGURE_ARGS: "-Dprofile=development"
|
|
||||||
DBUS_ID: "org.gnome.PodcastsDevel"
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- flatpak-builder --stop-at=${FLATPAK_MODULE} app ${MANIFEST_PATH}
|
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
|
||||||
# https://gitlab.gnome.org/World/podcasts/issues/55
|
# https://gitlab.gnome.org/World/hammond/issues/55
|
||||||
# Force regeneration of gresources regardless of artifacts chage
|
# Force regeneration of gresources regardless of artifacts chage
|
||||||
- flatpak-builder --run app ${MANIFEST_PATH} glib-compile-resources --sourcedir=podcasts-gtk/resources/ podcasts-gtk/resources/resources.xml
|
- flatpak-builder --run app org.gnome.Hammond.json glib-compile-resources --sourcedir=hammond-gtk/resources/ hammond-gtk/resources/resources.xml
|
||||||
|
|
||||||
# Build the flatpak repo
|
# Build the flatpak repo
|
||||||
- flatpak-builder --run app ${MANIFEST_PATH} meson --prefix=/app ${CONFIGURE_ARGS} _build
|
- flatpak-builder --run app org.gnome.Hammond.json meson --prefix=/app --libdir=/app/lib _build
|
||||||
- flatpak-builder --run app ${MANIFEST_PATH} ninja -C _build install
|
- flatpak-builder --run app org.gnome.Hammond.json ninja -C _build install
|
||||||
|
- flatpak-builder --finish-only app org.gnome.Hammond.json
|
||||||
# Run the tests
|
- flatpak build-export repo app
|
||||||
- |
|
|
||||||
xvfb-run -a -s "-screen 0 1024x768x24" \
|
|
||||||
flatpak-builder --run \
|
|
||||||
--env=APP_ID="org.gnome.PodcastsDevel" \
|
|
||||||
--env=LOCALEDIR="./podcasts-gtk/po" \
|
|
||||||
--env=VERSION="0.0.0" \
|
|
||||||
--env=RUSTFLAGS="--cfg rayon_unstable" \
|
|
||||||
--env=CARGO_HOME="target/cargo-home" \
|
|
||||||
--env=CARGO_TARGET_DIR="target_test/" \
|
|
||||||
app ${MANIFEST_PATH} \
|
|
||||||
cargo test -j 1 -- --test-threads=1
|
|
||||||
|
|
||||||
# Create a flatpak bundle
|
# Create a flatpak bundle
|
||||||
- flatpak-builder --finish-only app ${MANIFEST_PATH}
|
- flatpak build-bundle repo ${BUNDLE} org.gnome.Hammond
|
||||||
- flatpak build-export repo app
|
# Run the tests
|
||||||
- flatpak build-bundle repo ${BUNDLE} ${DBUS_ID}
|
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1
|
||||||
|
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1 --ignored
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- $BUNDLE
|
- $BUNDLE
|
||||||
expire_in: 2 days
|
expire_in: 30 days
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
key: "flatpak"
|
# JOB_NAME - Each job will have it's own cache
|
||||||
|
# COMMIT_REF_SLUG = Lowercase name of the branch
|
||||||
|
# ^ Keep diffrerent caches for each branch
|
||||||
|
key: "$CI_JOB_NAME"
|
||||||
paths:
|
paths:
|
||||||
- .flatpak-builder/cache/
|
- .flatpak-builder/cache/
|
||||||
- target/
|
- target/
|
||||||
- target_test/
|
|
||||||
|
|
||||||
review:
|
review:
|
||||||
stage: review
|
stage: review
|
||||||
@@ -70,8 +104,7 @@ review:
|
|||||||
url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
|
url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
|
||||||
on_stop: stop_review
|
on_stop: stop_review
|
||||||
except:
|
except:
|
||||||
- master@World/podcasts
|
- master@World/hammond
|
||||||
- tags
|
|
||||||
|
|
||||||
stop_review:
|
stop_review:
|
||||||
stage: review
|
stage: review
|
||||||
@@ -82,8 +115,7 @@ stop_review:
|
|||||||
name: review/$CI_COMMIT_REF_NAME
|
name: review/$CI_COMMIT_REF_NAME
|
||||||
action: stop
|
action: stop
|
||||||
except:
|
except:
|
||||||
- master@World/podcasts
|
- master@World/hammond
|
||||||
- tags
|
|
||||||
|
|
||||||
# Configure and run rustfmt on nightly
|
# Configure and run rustfmt on nightly
|
||||||
# Exits and builds fails if on bad format
|
# Exits and builds fails if on bad format
|
||||||
@@ -94,3 +126,19 @@ rustfmt:
|
|||||||
- rustc -Vv && cargo -Vv
|
- rustc -Vv && cargo -Vv
|
||||||
- cargo fmt --version
|
- cargo fmt --version
|
||||||
- cargo fmt --all -- --check
|
- cargo fmt --all -- --check
|
||||||
|
|
||||||
|
# Configure and run clippy on nightly
|
||||||
|
# Only fails on errors atm.
|
||||||
|
clippy:
|
||||||
|
image: "registry.gitlab.gnome.org/alatiera/hammond-container-images/clippy:nightly"
|
||||||
|
stage: lint
|
||||||
|
variables:
|
||||||
|
RUSTFLAGS: "--cfg rayon_unstable"
|
||||||
|
CARGO_HOME: ".cargo_cache"
|
||||||
|
script:
|
||||||
|
- rustc --version && cargo --version
|
||||||
|
- cargo clippy --version
|
||||||
|
# Force regeneration of gresources regardless of artifacts chage
|
||||||
|
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
|
||||||
|
- cargo clippy --all
|
||||||
|
<<: *cargo_cache
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Some common cases might be:
|
|||||||
|
|
||||||
Steps to reproduce:
|
Steps to reproduce:
|
||||||
|
|
||||||
1. Open GNOME Podcasts
|
1. Open Hammond
|
||||||
2. Do an action
|
2. Do an action
|
||||||
3. ...
|
3. ...
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,9 @@
|
|||||||
# Steps to reproduce
|
Detailed description of the issue. Put as much information as you can, potentially
|
||||||
<!--
|
with images showing the issue.
|
||||||
Explain in detail the steps on how the issue can be reproduced.
|
|
||||||
-->
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
Reproducible in:
|
Steps to reproduce:
|
||||||
<!--
|
|
||||||
Please test if the issue was already fixed in the unstable version of the app.
|
|
||||||
For that, follow these steps:
|
|
||||||
1. Make sure Flatpak is installed or install it following these steps https://flatpak.org/setup
|
|
||||||
2. Install the unstable version of the app following, flatpak bundles can be found in the CI artifacts.
|
|
||||||
|
|
||||||
If these steps failed, write in 'Other' the distribution you’re using and
|
1. Open Hammond
|
||||||
the version of the app.
|
2. Do an action
|
||||||
-->
|
3. ...
|
||||||
- Flatpak unstable: (yes or no) <!-- Write "yes" or "no" after the semicolon. -->
|
|
||||||
- Other:
|
|
||||||
|
|
||||||
# Current behavior
|
|
||||||
<!-- Describe the current behavior. -->
|
|
||||||
|
|
||||||
|
|
||||||
# Expected behavior
|
|
||||||
<!-- Describe the expected behavior. -->
|
|
||||||
|
|
||||||
|
|
||||||
# Additional information
|
|
||||||
<!--
|
|
||||||
Provide more information that could be relevant.
|
|
||||||
|
|
||||||
If the issue is a crash, provide a stack trace following the steps in:
|
|
||||||
https://wiki.gnome.org/Community/GettingInTouch/Bugzilla/GettingTraces
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Ignore the text under this line. -->
|
|
||||||
/label ~"Bug"
|
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
# Current problems
|
Current problems
|
||||||
<!--
|
<!--
|
||||||
What are the problems that the current project has?
|
What are the problems that the current project has?
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
* User cannot use the keyboard to perform most common actions
|
* User cannot use the keyboard to perform most common actions
|
||||||
or
|
or
|
||||||
* User cannot see documents from cloud services
|
* User cannot see documents from cloud services
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Goals & use cases
|
# Goals & use cases
|
||||||
<!--
|
<!--
|
||||||
What are the use cases that this proposal will cover? What are the end goals?
|
What are the use cases that this proposal will cover? What are the end goals?
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
* User needs to share a file with their friends.
|
* User needs to share a file with their friends.
|
||||||
or
|
or
|
||||||
* It should be easy to edit a picture within the app.
|
* It should be easy to edit a picture within the app.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
<!--
|
<!--
|
||||||
What does the solution needs to ensure for being succesful?
|
What does the solution needs to ensure for being succesful?
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
* Work on small form factors and touch
|
* Work on small form factors and touch
|
||||||
or
|
or
|
||||||
* Use the Meson build system and integrate with it
|
* Use the Meson build system and integrate with it
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Relevant art
|
# Relevant art
|
||||||
<!--
|
<!--
|
||||||
Is there any product that has implemented something similar? Put links to other
|
Is there any product that has implemented something similar? Put links to other
|
||||||
projects, pictures, links to other code, etc.
|
projects, pictures, links to other code, etc.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Proposal & plan
|
# Proposal & plan
|
||||||
<!-- What's the solution and how should be achieved? It can be split in smaller
|
<!-- What's the solution and how should be achieved? It can be split in smaller
|
||||||
tasks of minimum change, so they can be delivered across several releases. -->
|
tasks of minimum change, so they can be delivered across several releases. -->
|
||||||
|
|
||||||
|
/label ~"Epic"
|
||||||
|
|
||||||
/label ~"Epic"
|
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
### Use cases
|
Detailed description of the feature. Put as much information as you can.
|
||||||
<!--
|
|
||||||
Describe what problem(s) the user is experiencing and that this request
|
|
||||||
is trying to solve.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
Proposed Mockups:
|
||||||
|
|
||||||
### Desired behavior
|
(Add mockups of the proposed feature)
|
||||||
<!-- Describe the desired functionality. -->
|
|
||||||
|
|
||||||
|
## Design Tasks
|
||||||
|
|
||||||
### Benefits of the solution
|
* [ ] design tasks
|
||||||
<!-- List the possible benefits of the solution and how it fits in the project. -->
|
|
||||||
|
|
||||||
|
## Development Tasks
|
||||||
|
|
||||||
### Possible drawbacks
|
* [ ] development tasks
|
||||||
<!--
|
|
||||||
Describe possible drawbacks of the feature and list how it could affect
|
|
||||||
the project i.e. UI discoverability, complexity, impact in more or less
|
|
||||||
number of users, etc.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Ignore the text under this line. -->
|
## QA Tasks
|
||||||
/label ~"Feature"
|
|
||||||
|
* [ ] qa (quality assurance) tasks
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
### Please attach a relevant issue to this MR, if this doesn't exist please create one.
|
|
||||||
+18
-116
@@ -6,115 +6,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added:
|
### Added:
|
||||||
|
- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33)
|
||||||
|
|
||||||
### Changed:
|
### Changed:
|
||||||
|
- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33)
|
||||||
### Fixed:
|
- The `EpisdeWidget` was refactored. [!38](https://gitlab.gnome.org/World/hammond/merge_requests/38)
|
||||||
|
|
||||||
### Removed:
|
|
||||||
|
|
||||||
### Translations:
|
|
||||||
|
|
||||||
|
|
||||||
## [0.4.5] - 2018-08-31
|
|
||||||
|
|
||||||
### Added:
|
|
||||||
- [OARS](https://hughsie.github.io/oars/) Tags where added for compatibility with Store clients b0c94dd9
|
|
||||||
- Daniel added support for Translations !46
|
|
||||||
- Svitozar Cherepii(@svito) created a [wiki page](https://wiki.gnome.org/Apps/Podcasts) 70e79e50
|
|
||||||
- Libhandy was added as a dependancy #70
|
|
||||||
- Development builds can now be installed in parallel with stable builds !64
|
|
||||||
|
|
||||||
### Changed:
|
|
||||||
- The update indication was moved to an In-App notification #72
|
|
||||||
- The app icon's accent color was changed from orange to red 0dfb4859
|
|
||||||
- The stack switcher in the Headerbar is now insesitive on Empty Views !63
|
|
||||||
|
|
||||||
### Fixed:
|
|
||||||
- Improved handling of HTTP redirections #64 !61 !62
|
|
||||||
- Fixed a major performance regression when loading show covers !67
|
|
||||||
- More refference cycles have been fixed !59
|
|
||||||
- OPML import dialog now exits properly and no longer keeps the application from shuting down !65
|
|
||||||
- Update action is disabled if there isn't something to update #71
|
|
||||||
|
|
||||||
### Translations:
|
|
||||||
- Added Finish 93696026
|
|
||||||
- Added Polish 1bd6efc0
|
|
||||||
- Added Turkish 73929f2d
|
|
||||||
- Added Spanish !46
|
|
||||||
- Added German 6b6c390c
|
|
||||||
- Added Galician 0060a634
|
|
||||||
- Added Indonesian ded0224f
|
|
||||||
- Added Korean 36f16963
|
|
||||||
|
|
||||||
|
|
||||||
## [0.4.4] - 2018-07-31
|
|
||||||
|
|
||||||
### Changed:
|
|
||||||
- `SendCell` crate was replaced with `Fragile`. (Jorda Petridis) 838320785ebbea94e009698b473495cfec076f54
|
|
||||||
- Update dependancies (Jorda Petridis) 91bea8551998b16e44e5358fdd43c53422bcc6f3
|
|
||||||
|
|
||||||
### Fixed:
|
|
||||||
- Fix more refference cycles. (Jorda Petridis) 3496df24f8d8bfa8c8a53d8f00262d42ee39b41c
|
|
||||||
- Actually fix cargo-vendor (Jorda Petridis)
|
|
||||||
|
|
||||||
## [0.4.3] - 2018-07-27
|
|
||||||
|
|
||||||
### Fixed:
|
|
||||||
|
|
||||||
- Fix the cargo vendor config for the tarball releash script. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
|
|
||||||
|
|
||||||
## [0.4.2] - 2018-07-27
|
|
||||||
|
|
||||||
### Changed:
|
|
||||||
|
|
||||||
- Minimum size requested by the Views. (Jorda Petridis) 7c96152f3f53f271247230dccf1c9cd5947b685f
|
|
||||||
|
|
||||||
### Fixed:
|
|
||||||
|
|
||||||
- Screenshot metadata in appstream data. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
|
|
||||||
|
|
||||||
## [0.4.1] - 2018-07-26
|
|
||||||
### Added:
|
|
||||||
|
|
||||||
- Custom icons for the fast-forward and rewind actions in the Player were added. (Tobias Bernard) e77000076b3d78b8625f4c7ef367376d0130ece6
|
|
||||||
- Hicolor and symbolic icons for the Application. (Tobias Bernard and Sam Hewitt) edae1b04801dba9d91d5d4145db79b287f0eec2c
|
|
||||||
- Basic prefferences dialog (Zander Brown). [34](https://gitlab.gnome.org/World/podcasts/merge_requests/34)
|
|
||||||
- Dbus service preperation. Not used till the MPRIS2 integration has landed. (Zander Brown) [42](https://gitlab.gnome.org/World/podcasts/merge_requests/42)
|
|
||||||
- Episodes and Images will only get drawn when needed. Big Performance impact. (Jordan Petridis) [43](https://gitlab.gnome.org/World/podcasts/merge_requests/43)
|
|
||||||
|
|
||||||
### Changed:
|
|
||||||
|
|
||||||
- The `ShowWidget` control button were moved to a secondary menu in the Headerbar. (Jordan Petridis) 536805791e336a3e112799be554706bb804d2bef
|
|
||||||
- EmptyView layout improvements. (Jorda Petridis) 3c3d6c1e7f15b88308a9054b15a6ca0d8fa233ce 518ea9c8b57885c44bda9c418b19fef26ae0e55d
|
|
||||||
- Improved the `AddButton` behavior. (Jorda Petridis) 67ab54f8203f19aad198dc49e935127d25432b41
|
|
||||||
|
|
||||||
### Fixed:
|
|
||||||
|
|
||||||
- A couple reffence cycles where fixed. (Jorda Petridis)
|
|
||||||
|
|
||||||
### Removed:
|
|
||||||
|
|
||||||
- The delay between the application startup and the `update_on_startup` action. (Jorda Petridis) 7569465a612ee5ef84d0e58f4e1010c8d14080d4
|
|
||||||
|
|
||||||
## [0.4.0] - 2018-07-04
|
|
||||||
### Added:
|
|
||||||
- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown)
|
|
||||||
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
|
|
||||||
|
|
||||||
### Changed:
|
|
||||||
- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown)
|
|
||||||
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
|
|
||||||
- The `EpisdeWidget` was refactored.
|
|
||||||
[!38](https://gitlab.gnome.org/World/podcasts/merge_requests/38)
|
|
||||||
- `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012
|
- `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012
|
||||||
- An embeded audio player was implemented!
|
|
||||||
[!40](https://gitlab.gnome.org/World/podcasts/merge_requests/40)
|
|
||||||
- Various Database changes.
|
|
||||||
[!41](https://gitlab.gnome.org/World/podcasts/merge_requests/41)
|
|
||||||
|
|
||||||
### Fixed:
|
### Fixed:
|
||||||
- Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/podcasts/merge_requests/37)
|
- Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/hammond/merge_requests/37)
|
||||||
|
|
||||||
|
### Removed:
|
||||||
|
|
||||||
## [0.3.4] - 2018-05-20
|
## [0.3.4] - 2018-05-20
|
||||||
### Fixed:
|
### Fixed:
|
||||||
@@ -140,9 +42,9 @@ not being able to access any file.
|
|||||||
|
|
||||||
### Changed:
|
### Changed:
|
||||||
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
|
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
|
||||||
[#7](https://gitlab.gnome.org/World/podcasts/issues/7)
|
[#7](https://gitlab.gnome.org/World/hammond/issues/7)
|
||||||
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
|
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
|
||||||
[#25](https://gitlab.gnome.org/World/podcasts/issues/25)
|
[#25](https://gitlab.gnome.org/World/hammond/issues/25)
|
||||||
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
|
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
|
||||||
- `EpisdeWidget`s are now loaded asynchronously accross views.
|
- `EpisdeWidget`s are now loaded asynchronously accross views.
|
||||||
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
|
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
|
||||||
@@ -150,7 +52,7 @@ not being able to access any file.
|
|||||||
- `ShowWidget` Description is inside a scrolled window now
|
- `ShowWidget` Description is inside a scrolled window now
|
||||||
|
|
||||||
### Fixed:
|
### Fixed:
|
||||||
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/podcasts/issues/57)
|
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/hammond/issues/57)
|
||||||
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
|
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
|
||||||
|
|
||||||
### Removed:
|
### Removed:
|
||||||
@@ -159,27 +61,27 @@ not being able to access any file.
|
|||||||
## [0.3.1] - 2018-03-28
|
## [0.3.1] - 2018-03-28
|
||||||
### Added:
|
### Added:
|
||||||
- Ability to mark all episodes of a Show as watched.
|
- Ability to mark all episodes of a Show as watched.
|
||||||
[#47](https://gitlab.gnome.org/World/podcasts/issues/47)
|
[#47](https://gitlab.gnome.org/World/hammond/issues/47)
|
||||||
- Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
|
- Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
|
||||||
[#49](https://gitlab.gnome.org/World/podcasts/issues/49)
|
[#49](https://gitlab.gnome.org/World/hammond/issues/49)
|
||||||
- Hammond now remembers the window size and position. (Rowan Lewis)
|
- Hammond now remembers the window size and position. (Rowan Lewis)
|
||||||
[#50](https://gitlab.gnome.org/World/podcasts/issues/50)
|
[#50](https://gitlab.gnome.org/World/hammond/issues/50)
|
||||||
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
|
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
|
||||||
[!22](https://gitlab.gnome.org/World/podcasts/merge_requests/22) [!23](https://gitlab.gnome.org/World/podcasts/merge_requests/23)
|
[!22](https://gitlab.gnome.org/World/hammond/merge_requests/22) [!23](https://gitlab.gnome.org/World/hammond/merge_requests/23)
|
||||||
- Shows without episodes now display an empty message similar to EmptyView.
|
- Shows without episodes now display an empty message similar to EmptyView.
|
||||||
[#44](https://gitlab.gnome.org/World/podcasts/issues/44)
|
[#44](https://gitlab.gnome.org/World/hammond/issues/44)
|
||||||
|
|
||||||
### Changed:
|
### Changed:
|
||||||
- EpisdeWidget has been reimplemented as a compile time state machine.
|
- EpisdeWidget has been reimplemented as a compile time state machine.
|
||||||
[!18](https://gitlab.gnome.org/World/podcasts/merge_requests/18)
|
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18)
|
||||||
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
|
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
|
||||||
[#35](https://gitlab.gnome.org/World/podcasts/issues/35)
|
[#35](https://gitlab.gnome.org/World/hammond/issues/35)
|
||||||
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
|
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
|
||||||
[#53](https://gitlab.gnome.org/World/podcasts/issues/53)
|
[#53](https://gitlab.gnome.org/World/hammond/issues/53)
|
||||||
|
|
||||||
### Fixed:
|
### Fixed:
|
||||||
- Double border aroun the main window was fixed. (Rowan Lewis)
|
- Double border aroun the main window was fixed. (Rowan Lewis)
|
||||||
[#52](https://gitlab.gnome.org/World/podcasts/issues/52)
|
[#52](https://gitlab.gnome.org/World/hammond/issues/52)
|
||||||
|
|
||||||
## [0.3.0] - 2018-02-11
|
## [0.3.0] - 2018-02-11
|
||||||
- Tobias Bernard Redesigned the whole Gtk+ client.
|
- Tobias Bernard Redesigned the whole Gtk+ client.
|
||||||
|
|||||||
+8
-8
@@ -1,18 +1,18 @@
|
|||||||
## Contributing to GNOME Podcasts
|
## Contributing to Hammond
|
||||||
|
|
||||||
Thank you for looking in this file!
|
Thank you for looking in this file!
|
||||||
|
|
||||||
When contributing to the development of GNOME Podcasts, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
|
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.
|
||||||
|
|
||||||
If you have any questions regarding the use or development of GNOME Podcasts,
|
If you have any questions regarding the use or development of Hammond,
|
||||||
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
|
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
|
||||||
|
|
||||||
Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOfConduc), please follow it in all your interactions with the project.
|
Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOfConduc), please follow it in all your interactions with the project.
|
||||||
|
|
||||||
## Source repository
|
## Source repository
|
||||||
|
|
||||||
GNOME Podcasts's main source repository is at gitlab.gnome.org. You can view
|
Hammond's main source repository is at gitlab.gnome.org. You can view
|
||||||
the web interface [here](https://gitlab.gnome.org/World/podcasts)
|
the web interface [here](https://gitlab.gnome.org/World/hammond)
|
||||||
|
|
||||||
Development happens in the master branch.
|
Development happens in the master branch.
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ In order to run the test suite use the following: `cargo test -- --test-threads=
|
|||||||
|
|
||||||
# Issues, issues and more issues!
|
# Issues, issues and more issues!
|
||||||
|
|
||||||
There are many ways you can contribute to GNOME Podcasts, and all of them involve creating issues
|
There are many ways you can contribute to Hammond, and all of them involve creating issues
|
||||||
in [GNOME Podcasts issue tracker](https://gitlab.gnome.org/World/podcasts/issues). This is the entry point for your contribution.
|
in [Hammond issue tracker](https://gitlab.gnome.org/World/hammond/issues). This is the entry point for your contribution.
|
||||||
|
|
||||||
To create an effective and high quality ticket, try to put the following information on your
|
To create an effective and high quality ticket, try to put the following information on your
|
||||||
ticket:
|
ticket:
|
||||||
@@ -74,7 +74,7 @@ If it's an issue, add the steps to reproduce like this:
|
|||||||
|
|
||||||
Steps to reproduce:
|
Steps to reproduce:
|
||||||
|
|
||||||
1. Open GNOME Podcasts
|
1. Open Hammond
|
||||||
2. Do an Action
|
2. Do an Action
|
||||||
3. ...
|
3. ...
|
||||||
|
|
||||||
|
|||||||
Generated
+815
-1133
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"podcasts-data",
|
"hammond-data",
|
||||||
"podcasts-downloader",
|
"hammond-downloader",
|
||||||
"podcasts-gtk"
|
"hammond-gtk"
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
|
|||||||
state the exclusion of warranty; and each file should have at least
|
state the exclusion of warranty; and each file should have at least
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
GNOME Podcasts
|
Hammond
|
||||||
Copyright (C) 2017 Jordan Petridis
|
Copyright (C) 2017 Jordan Petridis
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
GNOME Podcasts Copyright (C) 2017 Jordan Petridis
|
Hammond Copyright (C) 2017 Jordan Petridis
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|||||||
@@ -1,63 +1,66 @@
|
|||||||
# GNOME Podcasts
|
# Hammond
|
||||||
|
|
||||||
### A Podcast application for GNOME.
|
### A Podcast Client for GNOME written in Rust.
|
||||||
Listen to your favorite podcasts, right from your desktop.
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Available on Flathub
|
|
||||||
|
|
||||||
[](https://flathub.org/apps/details/org.gnome.Podcasts)
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
GNOME Podcasts can be built and run with [Gnome Builder][builder] >= 3.28.
|
Hammond can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
|
||||||
Just clone the repo and hit the run button!
|
|
||||||
|
|
||||||
You can get Builder from [here][get_builder].
|
Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
|
||||||
|
|
||||||
## Broken Feeds
|
## Broken Feeds
|
||||||
|
|
||||||
Found a feed that does not work in GNOME Podcasts?
|
Found a feed that does not work in Hammond?
|
||||||
Please [open an issue][new_issue] and choose the `BrokenFeed` template so we will know and fix it!
|
Please [open an issue](https://gitlab.gnome.org/World/hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
|
||||||
|
|
||||||
## Getting in Touch
|
## Getting in Touch
|
||||||
|
|
||||||
If you have any questions regarding the use or development of GNOME Podcasts,
|
If you have any questions regarding the use or development of Hammond,
|
||||||
want to discuss design or simply hang out, please join us on our [irc][irc] or [matrix][matrix] channel.
|
want to discuss design or simply hang out, please join us in `#hammond` on
|
||||||
|
[irc.gnome.org.][irc] or [matrix][matrix].
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
There isn't much documentation yet, so you will probably have question about parts of the Code.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Flatpak
|
### Flatpak
|
||||||
|
|
||||||
Flatpak is the recommended way of building and installing GNOME Podcasts.
|
Flatpak is the reccomended way of building and installing Hammond.
|
||||||
Here are the depencancies you will need.
|
|
||||||
|
|
||||||
```sh
|
#### Building a Flatpak
|
||||||
# Add flathub and the gnome-nightly repo
|
|
||||||
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
|
||||||
flatpak remote-add --user --if-not-exists gnome-nightly https://sdk.gnome.org/gnome-nightly.flatpakrepo
|
|
||||||
|
|
||||||
# Install the gnome-nightly Sdk and Platform runtime
|
Download the `org.gnome.Hammond.json` flatpak manifest from this repo.
|
||||||
flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add flathub repo
|
||||||
|
flatpak --user remote-add flathub --if-not-exists https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
|
# Add the gnome-nightly repo
|
||||||
|
flatpak --user remote-add gnome-nightly --if-not-exists https://sdk.gnome.org/gnome-nightly.flatpakrepo
|
||||||
|
# Install the gnome-nightly Sdk and Platform runtim
|
||||||
|
flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
|
||||||
# Install the required rust-stable extension from flathub
|
# Install the required rust-stable extension from flathub
|
||||||
flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//18.08
|
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
|
||||||
|
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
|
||||||
```
|
```
|
||||||
|
|
||||||
To install the resulting flatpak you can do:
|
To install the resulting flatpak you can do:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flatpak-builder --user --install --force-clean --repo=repo podcasts org.gnome.Podcasts.json
|
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
|
||||||
|
flatpak install --user --bundle hammond.flatpak
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building from source
|
### Building from soure
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://gitlab.gnome.org/World/podcasts.git
|
git clone https://gitlab.gnome.org/World/hammond.git
|
||||||
cd gnome-podcasts/
|
cd hammond/
|
||||||
meson --prefix=/usr build
|
meson --prefix=/usr build
|
||||||
ninja -C build
|
ninja -C build
|
||||||
sudo ninja -C build install
|
sudo ninja -C build install
|
||||||
@@ -65,33 +68,41 @@ sudo ninja -C build install
|
|||||||
|
|
||||||
#### Dependencies
|
#### Dependencies
|
||||||
|
|
||||||
* Rust stable 1.27 or later along with cargo.
|
* Rust stable 1.26 or later along with cargo.
|
||||||
* Gtk+ 3.22 or later
|
* Gtk+ 3.22 or later
|
||||||
* Gstreamer 1.12 or later
|
|
||||||
* libhandy
|
|
||||||
* Meson
|
* Meson
|
||||||
* A network connection
|
* A network connection
|
||||||
|
|
||||||
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
|
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
|
||||||
|
|
||||||
|
**Debian/Ubuntu**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
apt-get update -yqq
|
||||||
|
apt-get install -yqq --no-install-recommends build-essential
|
||||||
|
apt-get install -yqq --no-install-recommends rustc cargo libgtk-3-dev meson
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fedora**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson
|
||||||
|
```
|
||||||
|
|
||||||
|
If you happen to build it on other distributions please let me know the names
|
||||||
|
of the corresponding libraries. Feel free to open a MR or an Issue to note it.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
There are a lot of things yet to be done.
|
There alot of thins yet to be done.
|
||||||
|
|
||||||
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
|
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
|
||||||
|
|
||||||
You can start by taking a look at [Issues][issues] or by opening a [New issue][new_issue].
|
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/World/hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
|
||||||
|
|
||||||
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
|
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
|
||||||
|
|
||||||
[contribution-guidelines]: https://gitlab.gnome.org/World/podcasts/blob/master/CONTRIBUTING.md
|
[contribution-guidelines]: https://gitlab.gnome.org/World/hammond/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
### Translations
|
|
||||||
|
|
||||||
Translation of this project takes place on the GNOME translation platform,
|
|
||||||
[Damned Lies](https://l10n.gnome.org/module/podcasts). For further
|
|
||||||
information on how to join a language team, or even to create one, please see
|
|
||||||
[GNOME Translation Project wiki page](https://wiki.gnome.org/TranslationProject).
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -99,15 +110,15 @@ information on how to join a language team, or even to create one, please see
|
|||||||
```sh
|
```sh
|
||||||
$ tree -d
|
$ tree -d
|
||||||
├── screenshots # png's used in the README.md
|
├── screenshots # png's used in the README.md
|
||||||
├── podcasts-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
|
├── hammond-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
|
||||||
│ ├── migrations # Diesel SQL migrations.
|
│ ├── migrations # Diesel SQL migrations.
|
||||||
│ │ └── ...
|
│ │ └── ...
|
||||||
│ ├── src
|
│ ├── src
|
||||||
│ └── tests
|
│ └── tests
|
||||||
│ └── feeds # Raw RSS Feeds used for tests.
|
│ └── feeds # Raw RSS Feeds used for tests.
|
||||||
├── podcasts-downloader # Really basic, Really crappy downloader.
|
├── hammond-downloader # Really basic, Really crappy downloader.
|
||||||
│ └── src
|
│ └── src
|
||||||
├── podcasts-gtk # The Gtk+ Client
|
├── hammond-gtk # The Gtk+ Client
|
||||||
│ ├── resources # GResources folder
|
│ ├── resources # GResources folder
|
||||||
│ │ └── gtk # Contains the glade.ui files.
|
│ │ └── gtk # Contains the glade.ui files.
|
||||||
│ └── src
|
│ └── src
|
||||||
@@ -117,27 +128,18 @@ $ tree -d
|
|||||||
|
|
||||||
## A note about the project's name
|
## A note about the project's name
|
||||||
|
|
||||||
The project used to be called Hammond, after Allan Moore's character [Evey Hammond][hammond] from the graphic novel V for Vendetta.
|
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.
|
||||||
It was renamed to GNOME Podcasts on 2018/07/24 shortly before its first public release.
|
|
||||||
|
It has nothing to do with the horrible headlines on the news.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
GNOME Podcasts's design is heavily inspired by [GNOME Music][music] and [Vocal][vocal].
|
Hammond's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Design/Apps/Music) and [Vocal](http://vocalproject.net/).
|
||||||
|
|
||||||
We also copied some elements from [GNOME News][news].
|
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
|
||||||
|
|
||||||
And almost the entirety of the build system is copied from the [Fractal][fractal] project.
|
And almost the entirety of the build system is copied from the [Fractal](https://gitlab.gnome.org/danigm/fractal) project.
|
||||||
|
|
||||||
[vendor]: https://github.com/alexcrichton/cargo-vendor
|
[vendor]: https://github.com/alexcrichton/cargo-vendor
|
||||||
[irc]: irc://irc.gnome.org/#hammond
|
[irc]: irc://irc.gnome.org/#hammond
|
||||||
[matrix]: https://matrix.to/#/#gnome-podcasts:matrix.org
|
[matrix]: https://matrix.to/#/#hammond:matrix.org
|
||||||
[flatpak_setup]: https://flatpak.org/setup/
|
|
||||||
[music]: https://wiki.gnome.org/Design/Apps/Music
|
|
||||||
[vocal]: http://vocalproject.net/
|
|
||||||
[news]: https://wiki.gnome.org/Design/Apps/Potential/News
|
|
||||||
[fractal]: https://gitlab.gnome.org/World/fractal
|
|
||||||
[hammond]: https://en.wikipedia.org/wiki/Evey_Hammond
|
|
||||||
[issues]: https://gitlab.gnome.org/World/podcasts/issues
|
|
||||||
[new_issue]: https://gitlab.gnome.org/World/podcasts/issues/new
|
|
||||||
[builder]: https://wiki.gnome.org/Apps/Builder
|
|
||||||
[get_builder]: https://wiki.gnome.org/Apps/Builder/Downloads
|
|
||||||
@@ -1,41 +1,46 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
||||||
name = "podcasts-data"
|
name = "hammond-data"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "1.2.0"
|
ammonia = "1.1.0"
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.2"
|
||||||
derive_builder = "0.5.1"
|
derive_builder = "0.5.1"
|
||||||
lazy_static = "1.1.0"
|
lazy_static = "1.0.0"
|
||||||
log = "0.4.4"
|
log = "0.4.1"
|
||||||
rayon = "1.0.2"
|
rayon = "1.0.1"
|
||||||
rayon-futures = "0.1.0"
|
rayon-futures = "0.1.0"
|
||||||
rfc822_sanitizer = "0.3.3"
|
rfc822_sanitizer = "0.3.3"
|
||||||
rss = "1.5.0"
|
rss = "1.5.0"
|
||||||
url = "1.7.1"
|
url = "1.7.0"
|
||||||
xdg = "2.1.0"
|
xdg = "2.1.0"
|
||||||
xml-rs = "0.8.0"
|
xml-rs = "0.8.0"
|
||||||
futures = "0.1.23"
|
futures = "0.1.21"
|
||||||
hyper = "0.11.27"
|
hyper = "0.11.27"
|
||||||
tokio-core = "0.1.17"
|
tokio-core = "0.1.17"
|
||||||
hyper-tls = "0.1.3"
|
hyper-tls = "0.1.3"
|
||||||
native-tls = "0.1.5"
|
native-tls = "0.1.5"
|
||||||
num_cpus = "1.8.0"
|
num_cpus = "1.8.0"
|
||||||
failure = "0.1.2"
|
failure = "0.1.1"
|
||||||
failure_derive = "0.1.2"
|
failure_derive = "0.1.1"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["sqlite", "r2d2"]
|
features = ["sqlite", "r2d2"]
|
||||||
version = "1.3.2"
|
version = "= 1.2.2"
|
||||||
|
|
||||||
[dependencies.diesel_migrations]
|
[dependencies.diesel_migrations]
|
||||||
features = ["sqlite"]
|
features = ["sqlite"]
|
||||||
version = "1.3.0"
|
version = "= 1.2.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.5.5"
|
rand = "0.5.0"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
|
criterion = "0.2.3"
|
||||||
pretty_assertions = "0.5.1"
|
pretty_assertions = "0.5.1"
|
||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "bench"
|
||||||
|
harness = false
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate criterion;
|
||||||
|
use criterion::Criterion;
|
||||||
|
|
||||||
|
// extern crate futures;
|
||||||
|
extern crate hammond_data;
|
||||||
|
extern crate hyper;
|
||||||
|
extern crate hyper_tls;
|
||||||
|
extern crate rand;
|
||||||
|
extern crate tokio_core;
|
||||||
|
// extern crate rayon;
|
||||||
|
extern crate rss;
|
||||||
|
|
||||||
|
// use rayon::prelude::*;
|
||||||
|
|
||||||
|
// use futures::future::*;
|
||||||
|
use tokio_core::reactor::Core;
|
||||||
|
|
||||||
|
use hammond_data::database::truncate_db;
|
||||||
|
use hammond_data::pipeline;
|
||||||
|
use hammond_data::FeedBuilder;
|
||||||
|
use hammond_data::Source;
|
||||||
|
// use hammond_data::errors::*;
|
||||||
|
|
||||||
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
// RSS feeds
|
||||||
|
const INTERCEPTED: &[u8] = include_bytes!("../tests/feeds/2018-01-20-Intercepted.xml");
|
||||||
|
const INTERCEPTED_URL: &str = "https://web.archive.org/web/20180120083840if_/https://feeds.\
|
||||||
|
feedburner.com/InterceptedWithJeremyScahill";
|
||||||
|
|
||||||
|
const UNPLUGGED: &[u8] = include_bytes!("../tests/feeds/2018-01-20-LinuxUnplugged.xml");
|
||||||
|
const UNPLUGGED_URL: &str =
|
||||||
|
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.com/linuxunplugged";
|
||||||
|
|
||||||
|
const TIPOFF: &[u8] = include_bytes!("../tests/feeds/2018-01-20-TheTipOff.xml");
|
||||||
|
const TIPOFF_URL: &str =
|
||||||
|
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||||
|
|
||||||
|
// This feed has HUGE descripion and summary fields which can be very
|
||||||
|
// very expensive to parse.
|
||||||
|
const CODE: &[u8] = include_bytes!("../tests/feeds/2018-01-20-GreaterThanCode.xml");
|
||||||
|
const CODE_URL: &str =
|
||||||
|
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.com/feed/podcast";
|
||||||
|
|
||||||
|
// Relative small feed
|
||||||
|
const STARS: &[u8] = include_bytes!("../tests/feeds/2018-01-20-StealTheStars.xml");
|
||||||
|
const STARS_URL: &str =
|
||||||
|
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
|
||||||
|
|
||||||
|
static FEEDS: &[(&[u8], &str)] = &[
|
||||||
|
(INTERCEPTED, INTERCEPTED_URL),
|
||||||
|
(UNPLUGGED, UNPLUGGED_URL),
|
||||||
|
(TIPOFF, TIPOFF_URL),
|
||||||
|
(CODE, CODE_URL),
|
||||||
|
(STARS, STARS_URL),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn bench_index_large_feed(c: &mut Criterion) {
|
||||||
|
truncate_db().unwrap();
|
||||||
|
let url = "https://www.greaterthancode.com/feed/podcast";
|
||||||
|
let mut core = Core::new().unwrap();
|
||||||
|
|
||||||
|
c.bench_function("index_large_feed", move |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let s = Source::from_url(url).unwrap();
|
||||||
|
// parse it into a channel
|
||||||
|
let chan = rss::Channel::read_from(BufReader::new(CODE)).unwrap();
|
||||||
|
let feed = FeedBuilder::default()
|
||||||
|
.channel(chan)
|
||||||
|
.source_id(s.id())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
core.run(feed.index()).unwrap();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
truncate_db().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_index_small_feed(c: &mut Criterion) {
|
||||||
|
truncate_db().unwrap();
|
||||||
|
let url = "https://rss.art19.com/steal-the-stars";
|
||||||
|
let mut core = Core::new().unwrap();
|
||||||
|
|
||||||
|
c.bench_function("index_small_feed", move |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let s = Source::from_url(url).unwrap();
|
||||||
|
// parse it into a channel
|
||||||
|
let chan = rss::Channel::read_from(BufReader::new(STARS)).unwrap();
|
||||||
|
let feed = FeedBuilder::default()
|
||||||
|
.channel(chan)
|
||||||
|
.source_id(s.id())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
core.run(feed.index()).unwrap();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
truncate_db().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_index_large_feed, bench_index_small_feed);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -24,9 +24,7 @@ lazy_static! {
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DB_PATH: PathBuf = xdg_dirs::PODCASTS_XDG
|
static ref DB_PATH: PathBuf = xdg_dirs::HAMMOND_XDG.place_data_file("hammond.db").unwrap();
|
||||||
.place_data_file("podcasts.db")
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -34,8 +32,8 @@ extern crate tempdir;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("podcasts_unit_test").unwrap() };
|
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
|
||||||
static ref DB_PATH: PathBuf = TEMPDIR.path().join("podcasts.db");
|
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an r2d2 `SqliteConnection`.
|
/// Get an r2d2 `SqliteConnection`.
|
||||||
@@ -66,12 +64,12 @@ fn run_migration_on(connection: &SqliteConnection) -> Result<(), DataError> {
|
|||||||
|
|
||||||
/// Reset the database into a clean state.
|
/// Reset the database into a clean state.
|
||||||
// Test share a Temp file db.
|
// Test share a Temp file db.
|
||||||
#[cfg(test)]
|
#[allow(dead_code)]
|
||||||
pub fn truncate_db() -> Result<(), DataError> {
|
pub fn truncate_db() -> Result<(), DataError> {
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
con.execute("DELETE FROM episodes")?;
|
con.execute("DELETE FROM episode")?;
|
||||||
con.execute("DELETE FROM shows")?;
|
con.execute("DELETE FROM podcast")?;
|
||||||
con.execute("DELETE FROM source")?;
|
con.execute("DELETE FROM source")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -23,156 +23,169 @@ pub fn get_sources() -> Result<Vec<Source>, DataError> {
|
|||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_podcasts() -> Result<Vec<Show>, DataError> {
|
pub fn get_podcasts() -> Result<Vec<Podcast>, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
shows
|
podcast
|
||||||
.order(title.asc())
|
.order(title.asc())
|
||||||
.load::<Show>(&con)
|
.load::<Podcast>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError> {
|
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
shows
|
podcast
|
||||||
.order(title.asc())
|
.order(title.asc())
|
||||||
.filter(id.ne_all(filter_ids))
|
.filter(id.ne_all(filter_ids))
|
||||||
.load::<Show>(&con)
|
.load::<Podcast>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
|
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.order(epoch.desc())
|
.order(epoch.desc())
|
||||||
.load::<Episode>(&con)
|
.load::<Episode>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
|
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select((rowid, local_uri, played))
|
.select((rowid, local_uri, played))
|
||||||
.filter(local_uri.is_not_null())
|
.filter(local_uri.is_not_null())
|
||||||
.load::<EpisodeCleanerModel>(&con)
|
.load::<EpisodeCleanerQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
|
// pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
|
||||||
// use schema::episodes::dsl::*;
|
// use schema::episode::dsl::*;
|
||||||
|
|
||||||
// let db = connection();
|
// let db = connection();
|
||||||
// let con = db.get()?;
|
// let con = db.get()?;
|
||||||
// episodes
|
// episode
|
||||||
// .filter(played.is_not_null())
|
// .filter(played.is_not_null())
|
||||||
// .load::<Episode>(&con)
|
// .load::<Episode>(&con)
|
||||||
// .map_err(From::from)
|
// .map_err(From::from)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
|
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select((rowid, local_uri, played))
|
.select((rowid, local_uri, played))
|
||||||
.filter(played.is_not_null())
|
.filter(played.is_not_null())
|
||||||
.load::<EpisodeCleanerModel>(&con)
|
.load::<EpisodeCleanerQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
|
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.filter(rowid.eq(ep_id))
|
.filter(rowid.eq(ep_id))
|
||||||
.get_result::<Episode>(&con)
|
.get_result::<Episode>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetModel, DataError> {
|
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetQuery, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select((
|
.select((
|
||||||
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
|
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
|
||||||
)).filter(rowid.eq(ep_id))
|
))
|
||||||
.get_result::<EpisodeWidgetModel>(&con)
|
.filter(rowid.eq(ep_id))
|
||||||
|
.get_result::<EpisodeWidgetQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
|
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.filter(rowid.eq(ep_id))
|
.filter(rowid.eq(ep_id))
|
||||||
.select(local_uri)
|
.select(local_uri)
|
||||||
.get_result::<Option<String>>(&con)
|
.get_result::<Option<String>>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_episode_description(rowid_: i32) -> Result<Option<String>, DataError> {
|
||||||
|
use schema::episode::dsl::*;
|
||||||
|
let db = connection();
|
||||||
|
let con = db.get()?;
|
||||||
|
|
||||||
|
episode
|
||||||
|
.filter(rowid.eq(rowid_))
|
||||||
|
.select(description)
|
||||||
|
.get_result::<Option<String>>(&con)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_episodes_widgets_filter_limit(
|
pub fn get_episodes_widgets_filter_limit(
|
||||||
filter_ids: &[i32],
|
filter_ids: &[i32],
|
||||||
limit: u32,
|
limit: u32,
|
||||||
) -> Result<Vec<EpisodeWidgetModel>, DataError> {
|
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
let columns = (
|
let columns = (
|
||||||
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
|
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select(columns)
|
.select(columns)
|
||||||
.order(epoch.desc())
|
.order(epoch.desc())
|
||||||
.filter(show_id.ne_all(filter_ids))
|
.filter(podcast_id.ne_all(filter_ids))
|
||||||
.limit(i64::from(limit))
|
.limit(i64::from(limit))
|
||||||
.load::<EpisodeWidgetModel>(&con)
|
.load::<EpisodeWidgetQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_podcast_from_id(pid: i32) -> Result<Show, DataError> {
|
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
shows
|
podcast
|
||||||
.filter(id.eq(pid))
|
.filter(id.eq(pid))
|
||||||
.get_result::<Show>(&con)
|
.get_result::<Podcast>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError> {
|
pub fn get_podcast_cover_from_id(pid: i32) -> Result<PodcastCoverQuery, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
shows
|
podcast
|
||||||
.select((id, title, image_uri))
|
.select((id, title, image_uri))
|
||||||
.filter(id.eq(pid))
|
.filter(id.eq(pid))
|
||||||
.get_result::<ShowCoverModel>(&con)
|
.get_result::<PodcastCoverQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
|
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
@@ -182,7 +195,7 @@ pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
|
|||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
|
pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
@@ -192,24 +205,24 @@ pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
|
|||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pd_episodeswidgets(parent: &Show) -> Result<Vec<EpisodeWidgetModel>, DataError> {
|
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
let columns = (
|
let columns = (
|
||||||
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
|
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select(columns)
|
.select(columns)
|
||||||
.filter(show_id.eq(parent.id()))
|
.filter(podcast_id.eq(parent.id()))
|
||||||
.order(epoch.desc())
|
.order(epoch.desc())
|
||||||
.load::<EpisodeWidgetModel>(&con)
|
.load::<EpisodeWidgetQuery>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
|
pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
@@ -220,8 +233,8 @@ pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError
|
|||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub(crate) fn get_pd_episodes_limit(parent: &Show, limit: u32) ->
|
// pub(crate) fn get_pd_episodes_limit(parent: &Podcast, limit: u32) ->
|
||||||
// Result<Vec<Episode>, DataError> { use schema::episodes::dsl::*;
|
// Result<Vec<Episode>, DataError> { use schema::episode::dsl::*;
|
||||||
|
|
||||||
// let db = connection();
|
// let db = connection();
|
||||||
// let con = db.get()?;
|
// let con = db.get()?;
|
||||||
@@ -255,25 +268,25 @@ pub fn get_source_from_id(id_: i32) -> Result<Source, DataError> {
|
|||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_podcast_from_source_id(sid: i32) -> Result<Show, DataError> {
|
pub fn get_podcast_from_source_id(sid: i32) -> Result<Podcast, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
shows
|
podcast
|
||||||
.filter(source_id.eq(sid))
|
.filter(source_id.eq(sid))
|
||||||
.get_result::<Show>(&con)
|
.get_result::<Podcast>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
|
pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.filter(title.eq(title_))
|
.filter(title.eq(title_))
|
||||||
.filter(show_id.eq(pid))
|
.filter(podcast_id.eq(pid))
|
||||||
.get_result::<Episode>(&con)
|
.get_result::<Episode>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
@@ -282,36 +295,19 @@ pub(crate) fn get_episode_minimal_from_pk(
|
|||||||
title_: &str,
|
title_: &str,
|
||||||
pid: i32,
|
pid: i32,
|
||||||
) -> Result<EpisodeMinimal, DataError> {
|
) -> Result<EpisodeMinimal, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
episodes
|
episode
|
||||||
.select((rowid, title, uri, epoch, length, duration, guid, show_id))
|
.select((rowid, title, uri, epoch, duration, guid, podcast_id))
|
||||||
.filter(title.eq(title_))
|
.filter(title.eq(title_))
|
||||||
.filter(show_id.eq(pid))
|
.filter(podcast_id.eq(pid))
|
||||||
.get_result::<EpisodeMinimal>(&con)
|
.get_result::<EpisodeMinimal>(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub(crate) fn remove_feed(pd: &Podcast) -> Result<(), DataError> {
|
||||||
pub(crate) fn get_episode_cleaner_from_pk(
|
|
||||||
title_: &str,
|
|
||||||
pid: i32,
|
|
||||||
) -> Result<EpisodeCleanerModel, DataError> {
|
|
||||||
use schema::episodes::dsl::*;
|
|
||||||
let db = connection();
|
|
||||||
let con = db.get()?;
|
|
||||||
|
|
||||||
episodes
|
|
||||||
.select((rowid, local_uri, played))
|
|
||||||
.filter(title.eq(title_))
|
|
||||||
.filter(show_id.eq(pid))
|
|
||||||
.get_result::<EpisodeCleanerModel>(&con)
|
|
||||||
.map_err(From::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn remove_feed(pd: &Show) -> Result<(), DataError> {
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
@@ -330,16 +326,16 @@ fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
|
|||||||
diesel::delete(source.filter(id.eq(source_id))).execute(con)
|
diesel::delete(source.filter(id.eq(source_id))).execute(con)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize> {
|
fn delete_podcast(con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
|
|
||||||
diesel::delete(shows.filter(id.eq(show_id))).execute(con)
|
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(con)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
|
fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
diesel::delete(episodes.filter(show_id.eq(parent_id))).execute(con)
|
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(con)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source_exists(url: &str) -> Result<bool, DataError> {
|
pub fn source_exists(url: &str) -> Result<bool, DataError> {
|
||||||
@@ -354,84 +350,81 @@ pub fn source_exists(url: &str) -> Result<bool, DataError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
|
pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
select(exists(shows.filter(source_id.eq(source_id_))))
|
select(exists(podcast.filter(source_id.eq(source_id_))))
|
||||||
.get_result(&con)
|
.get_result(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||||
pub(crate) fn episode_exists(title_: &str, show_id_: i32) -> Result<bool, DataError> {
|
pub(crate) fn episode_exists(title_: &str, podcast_id_: i32) -> Result<bool, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
select(exists(episodes.filter(show_id.eq(show_id_)).filter(title.eq(title_))))
|
select(exists(episode.filter(podcast_id.eq(podcast_id_)).filter(title.eq(title_))))
|
||||||
.get_result(&con)
|
.get_result(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the `episodes table contains any rows
|
pub fn episode_had_description(rowid_: i32) -> Result<bool, DataError> {
|
||||||
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
|
let db = connection();
|
||||||
|
let con = db.get()?;
|
||||||
|
|
||||||
|
select(exists(episode.filter(rowid.eq(rowid_))))
|
||||||
|
.get_result(&con)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the `episode` table contains any rows
|
||||||
///
|
///
|
||||||
/// Return true if `episodes` table is populated.
|
/// Return true if `episode` table is populated.
|
||||||
pub fn is_episodes_populated() -> Result<bool, DataError> {
|
pub fn is_episodes_populated() -> Result<bool, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
select(exists(episodes.as_query()))
|
select(exists(episode.as_query()))
|
||||||
.get_result(&con)
|
.get_result(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the `shows` table contains any rows
|
/// Check if the `podcast` table contains any rows
|
||||||
///
|
///
|
||||||
/// Return true if `shows` table is populated.
|
/// Return true if `podcast table is populated.
|
||||||
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
|
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
select(exists(shows.filter(id.ne_all(filter_ids))))
|
select(exists(podcast.filter(id.ne_all(filter_ids))))
|
||||||
.get_result(&con)
|
|
||||||
.map_err(From::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the `source` table contains any rows
|
|
||||||
///
|
|
||||||
/// Return true if `source` table is populated.
|
|
||||||
pub fn is_source_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
|
|
||||||
use schema::source::dsl::*;
|
|
||||||
|
|
||||||
let db = connection();
|
|
||||||
let con = db.get()?;
|
|
||||||
|
|
||||||
select(exists(source.filter(id.ne_all(filter_ids))))
|
|
||||||
.get_result(&con)
|
.get_result(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
|
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
diesel::insert_into(episodes)
|
diesel::insert_into(episode)
|
||||||
.values(eps)
|
.values(eps)
|
||||||
.execute(&*con)
|
.execute(&*con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
|
pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
@@ -448,26 +441,24 @@ pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use database::*;
|
use database::*;
|
||||||
use failure::Error;
|
|
||||||
use pipeline;
|
use pipeline;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_none_to_played_now() -> Result<(), Error> {
|
fn test_update_none_to_played_now() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||||
com/InterceptedWithJeremyScahill";
|
com/InterceptedWithJeremyScahill";
|
||||||
let source = Source::from_url(url)?;
|
let source = Source::from_url(url).unwrap();
|
||||||
let id = source.id();
|
let id = source.id();
|
||||||
pipeline::run(vec![source])?;
|
pipeline::run(vec![source], true).unwrap();
|
||||||
let pd = get_podcast_from_source_id(id)?;
|
let pd = get_podcast_from_source_id(id).unwrap();
|
||||||
|
|
||||||
let eps_num = get_pd_unplayed_episodes(&pd)?.len();
|
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
|
||||||
assert_ne!(eps_num, 0);
|
assert_ne!(eps_num, 0);
|
||||||
|
|
||||||
update_none_to_played_now(&pd)?;
|
update_none_to_played_now(&pd).unwrap();
|
||||||
let eps_num2 = get_pd_unplayed_episodes(&pd)?.len();
|
let eps_num2 = get_pd_unplayed_episodes(&pd).unwrap().len();
|
||||||
assert_eq!(eps_num2, 0);
|
assert_eq!(eps_num2, 0);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,7 @@ use std::io;
|
|||||||
|
|
||||||
use models::Source;
|
use models::Source;
|
||||||
|
|
||||||
#[fail(
|
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
|
||||||
display = "Request to {} returned {}. Context: {}",
|
|
||||||
url,
|
|
||||||
status_code,
|
|
||||||
context
|
|
||||||
)]
|
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Fail, Debug)]
|
||||||
pub struct HttpStatusError {
|
pub struct HttpStatusError {
|
||||||
url: String,
|
url: String,
|
||||||
@@ -47,6 +42,7 @@ pub enum DataError {
|
|||||||
#[fail(display = "Hyper Error: {}", _0)]
|
#[fail(display = "Hyper Error: {}", _0)]
|
||||||
HyperError(#[cause] hyper::Error),
|
HyperError(#[cause] hyper::Error),
|
||||||
#[fail(display = "Failed to parse a url: {}", _0)]
|
#[fail(display = "Failed to parse a url: {}", _0)]
|
||||||
|
// TODO: print the url too
|
||||||
UrlError(#[cause] url::ParseError),
|
UrlError(#[cause] url::ParseError),
|
||||||
#[fail(display = "TLS Error: {}", _0)]
|
#[fail(display = "TLS Error: {}", _0)]
|
||||||
TLSError(#[cause] native_tls::Error),
|
TLSError(#[cause] native_tls::Error),
|
||||||
@@ -62,10 +58,7 @@ pub enum DataError {
|
|||||||
HttpStatusGeneral(HttpStatusError),
|
HttpStatusGeneral(HttpStatusError),
|
||||||
#[fail(display = "FIXME: This should be better")]
|
#[fail(display = "FIXME: This should be better")]
|
||||||
F301(Source),
|
F301(Source),
|
||||||
#[fail(
|
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
|
||||||
display = "Error occured while Parsing an Episode. Reason: {}",
|
|
||||||
reason
|
|
||||||
)]
|
|
||||||
ParseEpisodeError { reason: String, parent_id: i32 },
|
ParseEpisodeError { reason: String, parent_id: i32 },
|
||||||
#[fail(display = "Episode was not changed and thus skipped.")]
|
#[fail(display = "Episode was not changed and thus skipped.")]
|
||||||
EpisodeNotChanged,
|
EpisodeNotChanged,
|
||||||
@@ -9,7 +9,7 @@ use rss;
|
|||||||
use dbqueries;
|
use dbqueries;
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use models::{Index, IndexState, Update};
|
use models::{Index, IndexState, Update};
|
||||||
use models::{NewEpisode, NewEpisodeMinimal, NewShow, Show};
|
use models::{NewEpisode, NewEpisodeMinimal, NewPodcast, Podcast};
|
||||||
|
|
||||||
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
|
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
|
||||||
/// that corresponds to the `Source.uri` field.
|
/// that corresponds to the `Source.uri` field.
|
||||||
@@ -31,15 +31,15 @@ impl Feed {
|
|||||||
.and_then(move |pd| self.index_channel_items(pd))
|
.and_then(move |pd| self.index_channel_items(pd))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_podcast(&self) -> NewShow {
|
fn parse_podcast(&self) -> NewPodcast {
|
||||||
NewShow::new(&self.channel, self.source_id)
|
NewPodcast::new(&self.channel, self.source_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_podcast_async(&self) -> impl Future<Item = NewShow, Error = DataError> + Send {
|
fn parse_podcast_async(&self) -> impl Future<Item = NewPodcast, Error = DataError> + Send {
|
||||||
ok(self.parse_podcast())
|
ok(self.parse_podcast())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index_channel_items(self, pd: Show) -> impl Future<Item = (), Error = DataError> + Send {
|
fn index_channel_items(self, pd: Podcast) -> impl Future<Item = (), Error = DataError> + Send {
|
||||||
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
|
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
|
||||||
|
|
||||||
// Parse the episodes
|
// Parse the episodes
|
||||||
@@ -65,12 +65,12 @@ fn determine_ep_state(
|
|||||||
item: &rss::Item,
|
item: &rss::Item,
|
||||||
) -> Result<IndexState<NewEpisode>, DataError> {
|
) -> Result<IndexState<NewEpisode>, DataError> {
|
||||||
// Check if feed exists
|
// Check if feed exists
|
||||||
let exists = dbqueries::episode_exists(ep.title(), ep.show_id())?;
|
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
Ok(IndexState::Index(ep.into_new_episode(item)))
|
Ok(IndexState::Index(ep.into_new_episode(item)))
|
||||||
} else {
|
} else {
|
||||||
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.show_id())?;
|
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
|
||||||
let rowid = old.rowid();
|
let rowid = old.rowid();
|
||||||
|
|
||||||
if ep != old {
|
if ep != old {
|
||||||
@@ -87,22 +87,21 @@ fn filter_episodes<'a, S>(
|
|||||||
where
|
where
|
||||||
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
|
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
|
||||||
{
|
{
|
||||||
stream
|
stream.filter_map(|state| match state {
|
||||||
.filter_map(|state| match state {
|
IndexState::NotChanged => None,
|
||||||
IndexState::NotChanged => None,
|
// Update individual rows, and filter them
|
||||||
// Update individual rows, and filter them
|
IndexState::Update((ref ep, rowid)) => {
|
||||||
IndexState::Update((ref ep, rowid)) => {
|
ep.update(rowid)
|
||||||
ep.update(rowid)
|
.map_err(|err| error!("{}", err))
|
||||||
.map_err(|err| error!("{}", err))
|
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
||||||
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
.ok();
|
||||||
.ok();
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
},
|
||||||
IndexState::Index(s) => Some(s),
|
IndexState::Index(s) => Some(s),
|
||||||
})
|
})
|
||||||
// only Index is left, collect them for batch index
|
// only Index is left, collect them for batch index
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
||||||
@@ -115,7 +114,8 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
|||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!("Failed batch indexng: {}", err);
|
error!("Failed batch indexng: {}", err);
|
||||||
info!("Fallign back to individual indexing.");
|
info!("Fallign back to individual indexing.");
|
||||||
}).unwrap_or_else(|_| {
|
})
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
episodes.iter().for_each(|ep| {
|
episodes.iter().for_each(|ep| {
|
||||||
ep.index()
|
ep.index()
|
||||||
.map_err(|err| error!("Error: {}.", err))
|
.map_err(|err| error!("Error: {}.", err))
|
||||||
@@ -127,7 +127,6 @@ fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use failure::Error;
|
|
||||||
use rss::Channel;
|
use rss::Channel;
|
||||||
use tokio_core::reactor::Core;
|
use tokio_core::reactor::Core;
|
||||||
|
|
||||||
@@ -172,8 +171,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_complete_index() -> Result<(), Error> {
|
fn test_complete_index() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let feeds: Vec<_> = URLS
|
let feeds: Vec<_> = URLS
|
||||||
.iter()
|
.iter()
|
||||||
@@ -181,46 +180,44 @@ mod tests {
|
|||||||
// Create and insert a Source into db
|
// Create and insert a Source into db
|
||||||
let s = Source::from_url(url).unwrap();
|
let s = Source::from_url(url).unwrap();
|
||||||
get_feed(path, s.id())
|
get_feed(path, s.id())
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut core = Core::new()?;
|
let mut core = Core::new().unwrap();
|
||||||
// Index the channes
|
// Index the channels
|
||||||
let list: Vec<_> = feeds.into_iter().map(|x| x.index()).collect();
|
let list: Vec<_> = feeds.into_iter().map(|x| x.index()).collect();
|
||||||
let _foo = core.run(join_all(list));
|
let _foo = core.run(join_all(list));
|
||||||
|
|
||||||
// Assert the index rows equal the controlled results
|
// Assert the index rows equal the controlled results
|
||||||
assert_eq!(dbqueries::get_sources()?.len(), 5);
|
assert_eq!(dbqueries::get_sources().unwrap().len(), 5);
|
||||||
assert_eq!(dbqueries::get_podcasts()?.len(), 5);
|
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
|
||||||
assert_eq!(dbqueries::get_episodes()?.len(), 354);
|
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_feed_parse_podcast() -> Result<(), Error> {
|
fn test_feed_parse_podcast() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let path = "tests/feeds/2018-01-20-Intercepted.xml";
|
let path = "tests/feeds/2018-01-20-Intercepted.xml";
|
||||||
let feed = get_feed(path, 42);
|
let feed = get_feed(path, 42);
|
||||||
|
|
||||||
let file = fs::File::open(path)?;
|
let file = fs::File::open(path).unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(feed.parse_podcast(), pd);
|
assert_eq!(feed.parse_podcast(), pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_feed_index_channel_items() -> Result<(), Error> {
|
fn test_feed_index_channel_items() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let path = "tests/feeds/2018-01-20-Intercepted.xml";
|
let path = "tests/feeds/2018-01-20-Intercepted.xml";
|
||||||
let feed = get_feed(path, 42);
|
let feed = get_feed(path, 42);
|
||||||
let pd = feed.parse_podcast().to_podcast()?;
|
let pd = feed.parse_podcast().to_podcast().unwrap();
|
||||||
|
|
||||||
feed.index_channel_items(pd).wait()?;
|
feed.index_channel_items(pd).wait().unwrap();
|
||||||
assert_eq!(dbqueries::get_podcasts()?.len(), 1);
|
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 1);
|
||||||
assert_eq!(dbqueries::get_episodes()?.len(), 43);
|
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#![recursion_limit = "1024"]
|
||||||
|
#![allow(unknown_lints)]
|
||||||
|
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
|
||||||
|
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
|
||||||
|
#![cfg_attr(
|
||||||
|
feature = "clippy",
|
||||||
|
warn(
|
||||||
|
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut,
|
||||||
|
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else,
|
||||||
|
items_after_statements, used_underscore_binding
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#![warn(
|
||||||
|
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
|
||||||
|
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements,
|
||||||
|
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns,
|
||||||
|
private_no_mangle_statics, safe_extern_statics, unconditional_recursion,
|
||||||
|
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true,
|
||||||
|
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
|
||||||
|
unused_extern_crates, unused
|
||||||
|
)]
|
||||||
|
#![deny(warnings)]
|
||||||
|
|
||||||
|
//! FIXME: Docs
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate pretty_assertions;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate maplit;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate derive_builder;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
// #[macro_use]
|
||||||
|
extern crate failure;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
extern crate ammonia;
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate futures;
|
||||||
|
extern crate hyper;
|
||||||
|
extern crate hyper_tls;
|
||||||
|
extern crate native_tls;
|
||||||
|
extern crate num_cpus;
|
||||||
|
extern crate rayon;
|
||||||
|
extern crate rayon_futures;
|
||||||
|
extern crate rfc822_sanitizer;
|
||||||
|
extern crate rss;
|
||||||
|
extern crate tokio_core;
|
||||||
|
extern crate url;
|
||||||
|
extern crate xdg;
|
||||||
|
extern crate xml;
|
||||||
|
|
||||||
|
pub mod database;
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub mod dbqueries;
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub mod errors;
|
||||||
|
mod feed;
|
||||||
|
pub(crate) mod models;
|
||||||
|
pub mod opml;
|
||||||
|
mod parser;
|
||||||
|
pub mod pipeline;
|
||||||
|
mod schema;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use feed::{Feed, FeedBuilder};
|
||||||
|
pub use models::Save;
|
||||||
|
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
|
||||||
|
|
||||||
|
// Set the user agent, See #53 for more
|
||||||
|
// Keep this in sync with Tor-browser releases
|
||||||
|
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
|
||||||
|
|
||||||
|
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub mod xdg_dirs {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use xdg;
|
||||||
|
|
||||||
|
lazy_static!{
|
||||||
|
pub(crate) static ref HAMMOND_XDG: xdg::BaseDirectories = {
|
||||||
|
xdg::BaseDirectories::with_prefix("hammond").unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
/// XDG_DATA Directory `Pathbuf`.
|
||||||
|
pub static ref HAMMOND_DATA: PathBuf = {
|
||||||
|
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
/// XDG_CONFIG Directory `Pathbuf`.
|
||||||
|
pub static ref HAMMOND_CONFIG: PathBuf = {
|
||||||
|
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
/// XDG_CACHE Directory `Pathbuf`.
|
||||||
|
pub static ref HAMMOND_CACHE: PathBuf = {
|
||||||
|
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Hammond Download Direcotry `PathBuf`.
|
||||||
|
pub static ref DL_DIR: PathBuf = {
|
||||||
|
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,14 @@ use diesel::SaveChangesDsl;
|
|||||||
|
|
||||||
use database::connection;
|
use database::connection;
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use models::{Save, Show};
|
use models::{Podcast, Save};
|
||||||
use schema::episodes;
|
use schema::episode;
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
|
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
#[primary_key(title, show_id)]
|
#[primary_key(title, podcast_id)]
|
||||||
#[belongs_to(Show, foreign_key = "show_id")]
|
#[belongs_to(Podcast, foreign_key = "podcast_id")]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Diesel Model of the episode table.
|
/// Diesel Model of the episode table.
|
||||||
pub struct Episode {
|
pub struct Episode {
|
||||||
@@ -26,7 +26,9 @@ pub struct Episode {
|
|||||||
duration: Option<i32>,
|
duration: Option<i32>,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
played: Option<i32>,
|
played: Option<i32>,
|
||||||
show_id: i32,
|
favorite: bool,
|
||||||
|
archive: bool,
|
||||||
|
podcast_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Save<Episode> for Episode {
|
impl Save<Episode> for Episode {
|
||||||
@@ -53,6 +55,11 @@ impl Episode {
|
|||||||
&self.title
|
&self.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `title`.
|
||||||
|
pub fn set_title(&mut self, value: &str) {
|
||||||
|
self.title = value.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the value of the `uri`.
|
/// Get the value of the `uri`.
|
||||||
///
|
///
|
||||||
/// Represents the url(usually) that the media file will be located at.
|
/// Represents the url(usually) that the media file will be located at.
|
||||||
@@ -60,6 +67,11 @@ impl Episode {
|
|||||||
self.uri.as_ref().map(|s| s.as_str())
|
self.uri.as_ref().map(|s| s.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `uri`.
|
||||||
|
pub fn set_uri(&mut self, value: Option<&str>) {
|
||||||
|
self.uri = value.map(|x| x.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the value of the `local_uri`.
|
/// Get the value of the `local_uri`.
|
||||||
///
|
///
|
||||||
/// Represents the local uri,usually filesystem path,
|
/// Represents the local uri,usually filesystem path,
|
||||||
@@ -68,16 +80,31 @@ impl Episode {
|
|||||||
self.local_uri.as_ref().map(|s| s.as_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`.
|
/// Get the `description`.
|
||||||
pub fn description(&self) -> Option<&str> {
|
pub fn description(&self) -> Option<&str> {
|
||||||
self.description.as_ref().map(|s| s.as_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 Episode's `guid`.
|
/// Get the Episode's `guid`.
|
||||||
pub fn guid(&self) -> Option<&str> {
|
pub fn guid(&self) -> Option<&str> {
|
||||||
self.guid.as_ref().map(|s| s.as_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.
|
/// Get the `epoch` value.
|
||||||
///
|
///
|
||||||
/// Retrieved from the rss Item publish date.
|
/// Retrieved from the rss Item publish date.
|
||||||
@@ -86,6 +113,11 @@ impl Episode {
|
|||||||
self.epoch
|
self.epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `epoch`.
|
||||||
|
pub fn set_epoch(&mut self, value: i32) {
|
||||||
|
self.epoch = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the `length`.
|
/// Get the `length`.
|
||||||
///
|
///
|
||||||
/// The number represents the size of the file in bytes.
|
/// The number represents the size of the file in bytes.
|
||||||
@@ -93,6 +125,11 @@ impl Episode {
|
|||||||
self.length
|
self.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `length`.
|
||||||
|
pub fn set_length(&mut self, value: Option<i32>) {
|
||||||
|
self.length = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the `duration` value.
|
/// Get the `duration` value.
|
||||||
///
|
///
|
||||||
/// The number represents the duration of the item/episode in seconds.
|
/// The number represents the duration of the item/episode in seconds.
|
||||||
@@ -100,6 +137,11 @@ impl Episode {
|
|||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `duration`.
|
||||||
|
pub fn set_duration(&mut self, value: Option<i32>) {
|
||||||
|
self.duration = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Epoch representation of the last time the episode was played.
|
/// Epoch representation of the last time the episode was played.
|
||||||
///
|
///
|
||||||
/// None/Null for unplayed.
|
/// None/Null for unplayed.
|
||||||
@@ -107,19 +149,54 @@ impl Episode {
|
|||||||
self.played
|
self.played
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Show` table foreign key.
|
/// Set the `played` value.
|
||||||
pub fn show_id(&self) -> i32 {
|
pub fn set_played(&mut self, value: Option<i32>) {
|
||||||
self.show_id
|
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<(), DataError> {
|
||||||
|
let epoch = Utc::now().timestamp() as i32;
|
||||||
|
self.set_played(Some(epoch));
|
||||||
|
self.save().map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, AsChangeset, PartialEq)]
|
#[derive(Queryable, AsChangeset, PartialEq)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
#[primary_key(title, show_id)]
|
#[primary_key(title, podcast_id)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Diesel Model to be used for constructing `EpisodeWidgets`.
|
/// Diesel Model to be used for constructing `EpisodeWidgets`.
|
||||||
pub struct EpisodeWidgetModel {
|
pub struct EpisodeWidgetQuery {
|
||||||
rowid: i32,
|
rowid: i32,
|
||||||
title: String,
|
title: String,
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
@@ -128,12 +205,14 @@ pub struct EpisodeWidgetModel {
|
|||||||
length: Option<i32>,
|
length: Option<i32>,
|
||||||
duration: Option<i32>,
|
duration: Option<i32>,
|
||||||
played: Option<i32>,
|
played: Option<i32>,
|
||||||
show_id: i32,
|
// favorite: bool,
|
||||||
|
// archive: bool,
|
||||||
|
podcast_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Episode> for EpisodeWidgetModel {
|
impl From<Episode> for EpisodeWidgetQuery {
|
||||||
fn from(e: Episode) -> EpisodeWidgetModel {
|
fn from(e: Episode) -> EpisodeWidgetQuery {
|
||||||
EpisodeWidgetModel {
|
EpisodeWidgetQuery {
|
||||||
rowid: e.rowid,
|
rowid: e.rowid,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
uri: e.uri,
|
uri: e.uri,
|
||||||
@@ -142,30 +221,30 @@ impl From<Episode> for EpisodeWidgetModel {
|
|||||||
length: e.length,
|
length: e.length,
|
||||||
duration: e.duration,
|
duration: e.duration,
|
||||||
played: e.played,
|
played: e.played,
|
||||||
show_id: e.show_id,
|
podcast_id: e.podcast_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Save<usize> for EpisodeWidgetModel {
|
impl Save<usize> for EpisodeWidgetQuery {
|
||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
/// Helper method to easily save/"sync" current state of self to the
|
/// Helper method to easily save/"sync" current state of self to the
|
||||||
/// Database.
|
/// Database.
|
||||||
fn save(&self) -> Result<usize, Self::Error> {
|
fn save(&self) -> Result<usize, Self::Error> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let tempdb = db.get()?;
|
let tempdb = db.get()?;
|
||||||
|
|
||||||
diesel::update(episodes.filter(rowid.eq(self.rowid)))
|
diesel::update(episode.filter(rowid.eq(self.rowid)))
|
||||||
.set(self)
|
.set(self)
|
||||||
.execute(&*tempdb)
|
.execute(&*tempdb)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpisodeWidgetModel {
|
impl EpisodeWidgetQuery {
|
||||||
/// Get the value of the sqlite's `ROW_ID`
|
/// Get the value of the sqlite's `ROW_ID`
|
||||||
pub fn rowid(&self) -> i32 {
|
pub fn rowid(&self) -> i32 {
|
||||||
self.rowid
|
self.rowid
|
||||||
@@ -223,6 +302,11 @@ impl EpisodeWidgetModel {
|
|||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `duration`.
|
||||||
|
pub fn set_duration(&mut self, value: Option<i32>) {
|
||||||
|
self.duration = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Epoch representation of the last time the episode was played.
|
/// Epoch representation of the last time the episode was played.
|
||||||
///
|
///
|
||||||
/// None/Null for unplayed.
|
/// None/Null for unplayed.
|
||||||
@@ -231,13 +315,36 @@ impl EpisodeWidgetModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the `played` value.
|
/// Set the `played` value.
|
||||||
fn set_played(&mut self, value: Option<i32>) {
|
pub fn set_played(&mut self, value: Option<i32>) {
|
||||||
self.played = value;
|
self.played = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Show` table foreign key.
|
// /// Represents the archiving policy for the episode.
|
||||||
pub fn show_id(&self) -> i32 {
|
// pub fn archive(&self) -> bool {
|
||||||
self.show_id
|
// 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.
|
/// Sets the `played` value with the current `epoch` timestap and save it.
|
||||||
@@ -249,38 +356,38 @@ impl EpisodeWidgetModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, AsChangeset, PartialEq)]
|
#[derive(Queryable, AsChangeset, PartialEq)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
#[primary_key(title, show_id)]
|
#[primary_key(title, podcast_id)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Diesel Model to be used internal with the `utils::checkup` function.
|
/// Diesel Model to be used internal with the `utils::checkup` function.
|
||||||
pub struct EpisodeCleanerModel {
|
pub struct EpisodeCleanerQuery {
|
||||||
rowid: i32,
|
rowid: i32,
|
||||||
local_uri: Option<String>,
|
local_uri: Option<String>,
|
||||||
played: Option<i32>,
|
played: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Save<usize> for EpisodeCleanerModel {
|
impl Save<usize> for EpisodeCleanerQuery {
|
||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
/// Helper method to easily save/"sync" current state of self to the
|
/// Helper method to easily save/"sync" current state of self to the
|
||||||
/// Database.
|
/// Database.
|
||||||
fn save(&self) -> Result<usize, Self::Error> {
|
fn save(&self) -> Result<usize, Self::Error> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let tempdb = db.get()?;
|
let tempdb = db.get()?;
|
||||||
|
|
||||||
diesel::update(episodes.filter(rowid.eq(self.rowid)))
|
diesel::update(episode.filter(rowid.eq(self.rowid)))
|
||||||
.set(self)
|
.set(self)
|
||||||
.execute(&*tempdb)
|
.execute(&*tempdb)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Episode> for EpisodeCleanerModel {
|
impl From<Episode> for EpisodeCleanerQuery {
|
||||||
fn from(e: Episode) -> EpisodeCleanerModel {
|
fn from(e: Episode) -> EpisodeCleanerQuery {
|
||||||
EpisodeCleanerModel {
|
EpisodeCleanerQuery {
|
||||||
rowid: e.rowid(),
|
rowid: e.rowid(),
|
||||||
local_uri: e.local_uri,
|
local_uri: e.local_uri,
|
||||||
played: e.played,
|
played: e.played,
|
||||||
@@ -288,7 +395,7 @@ impl From<Episode> for EpisodeCleanerModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpisodeCleanerModel {
|
impl EpisodeCleanerQuery {
|
||||||
/// Get the value of the sqlite's `ROW_ID`
|
/// Get the value of the sqlite's `ROW_ID`
|
||||||
pub fn rowid(&self) -> i32 {
|
pub fn rowid(&self) -> i32 {
|
||||||
self.rowid
|
self.rowid
|
||||||
@@ -321,9 +428,9 @@ impl EpisodeCleanerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, AsChangeset, PartialEq)]
|
#[derive(Queryable, AsChangeset, PartialEq)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
#[primary_key(title, show_id)]
|
#[primary_key(title, podcast_id)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Diesel Model to be used for FIXME.
|
/// Diesel Model to be used for FIXME.
|
||||||
pub struct EpisodeMinimal {
|
pub struct EpisodeMinimal {
|
||||||
@@ -331,10 +438,9 @@ pub struct EpisodeMinimal {
|
|||||||
title: String,
|
title: String,
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
length: Option<i32>,
|
|
||||||
duration: Option<i32>,
|
duration: Option<i32>,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
show_id: i32,
|
podcast_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Episode> for EpisodeMinimal {
|
impl From<Episode> for EpisodeMinimal {
|
||||||
@@ -343,11 +449,10 @@ impl From<Episode> for EpisodeMinimal {
|
|||||||
rowid: e.rowid,
|
rowid: e.rowid,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
uri: e.uri,
|
uri: e.uri,
|
||||||
length: e.length,
|
|
||||||
guid: e.guid,
|
guid: e.guid,
|
||||||
epoch: e.epoch,
|
epoch: e.epoch,
|
||||||
duration: e.duration,
|
duration: e.duration,
|
||||||
show_id: e.show_id,
|
podcast_id: e.podcast_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,13 +488,6 @@ impl EpisodeMinimal {
|
|||||||
self.epoch
|
self.epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the `length`.
|
|
||||||
///
|
|
||||||
/// The number represents the size of the file in bytes.
|
|
||||||
pub fn length(&self) -> Option<i32> {
|
|
||||||
self.length
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `duration` value.
|
/// Get the `duration` value.
|
||||||
///
|
///
|
||||||
/// The number represents the duration of the item/episode in seconds.
|
/// The number represents the duration of the item/episode in seconds.
|
||||||
@@ -397,8 +495,8 @@ impl EpisodeMinimal {
|
|||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Show` table foreign key.
|
/// `Podcast` table foreign key.
|
||||||
pub fn show_id(&self) -> i32 {
|
pub fn podcast_id(&self) -> i32 {
|
||||||
self.show_id
|
self.podcast_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
mod new_episode;
|
mod new_episode;
|
||||||
mod new_show;
|
mod new_podcast;
|
||||||
mod new_source;
|
mod new_source;
|
||||||
|
|
||||||
mod episode;
|
mod episode;
|
||||||
mod show;
|
mod podcast;
|
||||||
mod source;
|
mod source;
|
||||||
|
|
||||||
// use futures::prelude::*;
|
// use futures::prelude::*;
|
||||||
// use futures::future::*;
|
// use futures::future::*;
|
||||||
|
|
||||||
pub(crate) use self::episode::EpisodeCleanerModel;
|
pub(crate) use self::episode::EpisodeCleanerQuery;
|
||||||
pub(crate) use self::new_episode::{NewEpisode, NewEpisodeMinimal};
|
pub(crate) use self::new_episode::{NewEpisode, NewEpisodeMinimal};
|
||||||
pub(crate) use self::new_show::NewShow;
|
pub(crate) use self::new_podcast::NewPodcast;
|
||||||
pub(crate) use self::new_source::NewSource;
|
pub(crate) use self::new_source::NewSource;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use self::new_episode::NewEpisodeBuilder;
|
pub(crate) use self::new_episode::NewEpisodeBuilder;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use self::new_show::NewShowBuilder;
|
pub(crate) use self::new_podcast::NewPodcastBuilder;
|
||||||
|
|
||||||
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetModel};
|
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetQuery};
|
||||||
pub use self::show::{Show, ShowCoverModel};
|
pub use self::podcast::{Podcast, PodcastCoverQuery};
|
||||||
pub use self::source::Source;
|
pub use self::source::Source;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
+126
-129
@@ -9,11 +9,11 @@ use dbqueries;
|
|||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
|
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
|
||||||
use parser;
|
use parser;
|
||||||
use schema::episodes;
|
use schema::episode;
|
||||||
use utils::url_cleaner;
|
use utils::url_cleaner;
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset)]
|
#[derive(Insertable, AsChangeset)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[derive(Debug, Clone, Default, Builder, PartialEq)]
|
#[derive(Debug, Clone, Default, Builder, PartialEq)]
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
#[builder(derive(Debug))]
|
#[builder(derive(Debug))]
|
||||||
@@ -26,7 +26,7 @@ pub(crate) struct NewEpisode {
|
|||||||
duration: Option<i32>,
|
duration: Option<i32>,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
show_id: i32,
|
podcast_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<NewEpisodeMinimal> for NewEpisode {
|
impl From<NewEpisodeMinimal> for NewEpisode {
|
||||||
@@ -36,7 +36,7 @@ impl From<NewEpisodeMinimal> for NewEpisode {
|
|||||||
.uri(e.uri)
|
.uri(e.uri)
|
||||||
.duration(e.duration)
|
.duration(e.duration)
|
||||||
.epoch(e.epoch)
|
.epoch(e.epoch)
|
||||||
.show_id(e.show_id)
|
.podcast_id(e.podcast_id)
|
||||||
.guid(e.guid)
|
.guid(e.guid)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -47,12 +47,12 @@ impl Insert<()> for NewEpisode {
|
|||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
fn insert(&self) -> Result<(), DataError> {
|
fn insert(&self) -> Result<(), DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
info!("Inserting {:?}", self.title);
|
info!("Inserting {:?}", self.title);
|
||||||
diesel::insert_into(episodes)
|
diesel::insert_into(episode)
|
||||||
.values(self)
|
.values(self)
|
||||||
.execute(&con)
|
.execute(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
@@ -64,12 +64,12 @@ impl Update<()> for NewEpisode {
|
|||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
fn update(&self, episode_id: i32) -> Result<(), DataError> {
|
fn update(&self, episode_id: i32) -> Result<(), DataError> {
|
||||||
use schema::episodes::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
info!("Updating {:?}", self.title);
|
info!("Updating {:?}", self.title);
|
||||||
diesel::update(episodes.filter(rowid.eq(episode_id)))
|
diesel::update(episode.filter(rowid.eq(episode_id)))
|
||||||
.set(self)
|
.set(self)
|
||||||
.execute(&con)
|
.execute(&con)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
@@ -83,10 +83,10 @@ impl Index<()> for NewEpisode {
|
|||||||
// Does not update the episode description if it's the only thing that has
|
// Does not update the episode description if it's the only thing that has
|
||||||
// changed.
|
// changed.
|
||||||
fn index(&self) -> Result<(), DataError> {
|
fn index(&self) -> Result<(), DataError> {
|
||||||
let exists = dbqueries::episode_exists(self.title(), self.show_id())?;
|
let exists = dbqueries::episode_exists(self.title(), self.podcast_id())?;
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.show_id())?;
|
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.podcast_id())?;
|
||||||
|
|
||||||
if self != &other {
|
if self != &other {
|
||||||
self.update(other.rowid())
|
self.update(other.rowid())
|
||||||
@@ -106,7 +106,7 @@ impl PartialEq<EpisodeMinimal> for NewEpisode {
|
|||||||
&& (self.duration() == other.duration())
|
&& (self.duration() == other.duration())
|
||||||
&& (self.epoch() == other.epoch())
|
&& (self.epoch() == other.epoch())
|
||||||
&& (self.guid() == other.guid())
|
&& (self.guid() == other.guid())
|
||||||
&& (self.show_id() == other.show_id())
|
&& (self.podcast_id() == other.podcast_id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ impl PartialEq<Episode> for NewEpisode {
|
|||||||
&& (self.duration() == other.duration())
|
&& (self.duration() == other.duration())
|
||||||
&& (self.epoch() == other.epoch())
|
&& (self.epoch() == other.epoch())
|
||||||
&& (self.guid() == other.guid())
|
&& (self.guid() == other.guid())
|
||||||
&& (self.show_id() == other.show_id())
|
&& (self.podcast_id() == other.podcast_id())
|
||||||
&& (self.description() == other.description())
|
&& (self.description() == other.description())
|
||||||
&& (self.length() == other.length())
|
&& (self.length() == other.length())
|
||||||
}
|
}
|
||||||
@@ -126,14 +126,14 @@ impl PartialEq<Episode> for NewEpisode {
|
|||||||
impl NewEpisode {
|
impl NewEpisode {
|
||||||
/// Parses an `rss::Item` into a `NewEpisode` Struct.
|
/// Parses an `rss::Item` into a `NewEpisode` Struct.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn new(item: &rss::Item, show_id: i32) -> Result<Self, DataError> {
|
pub(crate) fn new(item: &rss::Item, podcast_id: i32) -> Result<Self, DataError> {
|
||||||
NewEpisodeMinimal::new(item, show_id).map(|ep| ep.into_new_episode(item))
|
NewEpisodeMinimal::new(item, podcast_id).map(|ep| ep.into_new_episode(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn to_episode(&self) -> Result<Episode, DataError> {
|
pub(crate) fn to_episode(&self) -> Result<Episode, DataError> {
|
||||||
self.index()?;
|
self.index()?;
|
||||||
dbqueries::get_episode_from_pk(&self.title, self.show_id).map_err(From::from)
|
dbqueries::get_episode_from_pk(&self.title, self.podcast_id).map_err(From::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,24 +167,23 @@ impl NewEpisode {
|
|||||||
self.length
|
self.length
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn show_id(&self) -> i32 {
|
pub(crate) fn podcast_id(&self) -> i32 {
|
||||||
self.show_id
|
self.podcast_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset)]
|
#[derive(Insertable, AsChangeset)]
|
||||||
#[table_name = "episodes"]
|
#[table_name = "episode"]
|
||||||
#[derive(Debug, Clone, Builder, PartialEq)]
|
#[derive(Debug, Clone, Builder, PartialEq)]
|
||||||
#[builder(derive(Debug))]
|
#[builder(derive(Debug))]
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
pub(crate) struct NewEpisodeMinimal {
|
pub(crate) struct NewEpisodeMinimal {
|
||||||
title: String,
|
title: String,
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
length: Option<i32>,
|
|
||||||
duration: Option<i32>,
|
duration: Option<i32>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
show_id: i32,
|
podcast_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
|
impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
|
||||||
@@ -194,7 +193,7 @@ impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
|
|||||||
&& (self.duration() == other.duration())
|
&& (self.duration() == other.duration())
|
||||||
&& (self.epoch() == other.epoch())
|
&& (self.epoch() == other.epoch())
|
||||||
&& (self.guid() == other.guid())
|
&& (self.guid() == other.guid())
|
||||||
&& (self.show_id() == other.show_id())
|
&& (self.podcast_id() == other.podcast_id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,20 +211,12 @@ impl NewEpisodeMinimal {
|
|||||||
let title = item.title().unwrap().trim().to_owned();
|
let title = item.title().unwrap().trim().to_owned();
|
||||||
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
||||||
|
|
||||||
// Get the mime type, the `http` url and the length from the enclosure
|
let uri = item.enclosure()
|
||||||
// http://www.rssboard.org/rss-specification#ltenclosuregtSubelementOfLtitemgt
|
|
||||||
let enc = item.enclosure();
|
|
||||||
|
|
||||||
// Get the url
|
|
||||||
let uri = enc
|
|
||||||
.map(|s| url_cleaner(s.url().trim()))
|
.map(|s| url_cleaner(s.url().trim()))
|
||||||
// Fallback to Rss.Item.link if enclosure is None.
|
// Fallback to Rss.Item.link if enclosure is None.
|
||||||
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
|
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
|
||||||
|
|
||||||
// Get the size of the content, it should be in bytes
|
// If url is still None return an Error as this behaviour is
|
||||||
let length = enc.and_then(|x| x.length().parse().ok());
|
|
||||||
|
|
||||||
// If url is still None return an Error as this behaviour is not
|
|
||||||
// compliant with the RSS Spec.
|
// compliant with the RSS Spec.
|
||||||
if uri.is_none() {
|
if uri.is_none() {
|
||||||
let err = DataError::ParseEpisodeError {
|
let err = DataError::ParseEpisodeError {
|
||||||
@@ -247,18 +238,17 @@ impl NewEpisodeMinimal {
|
|||||||
NewEpisodeMinimalBuilder::default()
|
NewEpisodeMinimalBuilder::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.length(length)
|
|
||||||
.duration(duration)
|
.duration(duration)
|
||||||
.epoch(epoch)
|
.epoch(epoch)
|
||||||
.guid(guid)
|
.guid(guid)
|
||||||
.show_id(parent_id)
|
.podcast_id(parent_id)
|
||||||
.build()
|
.build()
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: TryInto is stabilizing in rustc v1.26!
|
// TODO: TryInto is stabilizing in rustc v1.26!
|
||||||
// ^ Jokes on you past self!
|
|
||||||
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
|
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
|
||||||
|
let length = item.enclosure().and_then(|x| x.length().parse().ok());
|
||||||
let description = item.description().and_then(|s| {
|
let description = item.description().and_then(|s| {
|
||||||
let sanitized_html = ammonia::Builder::new()
|
let sanitized_html = ammonia::Builder::new()
|
||||||
// Remove `rel` attributes from `<a>` tags
|
// Remove `rel` attributes from `<a>` tags
|
||||||
@@ -273,9 +263,9 @@ impl NewEpisodeMinimal {
|
|||||||
.uri(self.uri)
|
.uri(self.uri)
|
||||||
.duration(self.duration)
|
.duration(self.duration)
|
||||||
.epoch(self.epoch)
|
.epoch(self.epoch)
|
||||||
.show_id(self.show_id)
|
.podcast_id(self.podcast_id)
|
||||||
.guid(self.guid)
|
.guid(self.guid)
|
||||||
.length(self.length)
|
.length(length)
|
||||||
.description(description)
|
.description(description)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -304,16 +294,14 @@ impl NewEpisodeMinimal {
|
|||||||
self.epoch
|
self.epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn show_id(&self) -> i32 {
|
pub(crate) fn podcast_id(&self) -> i32 {
|
||||||
self.show_id
|
self.podcast_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use database::truncate_db;
|
use database::truncate_db;
|
||||||
use dbqueries;
|
use dbqueries;
|
||||||
use failure::Error;
|
|
||||||
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
|
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
|
||||||
use models::*;
|
use models::*;
|
||||||
|
|
||||||
@@ -332,11 +320,11 @@ mod tests {
|
|||||||
.title("The Super Bowl of Racism")
|
.title("The Super Bowl of Racism")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
||||||
))).guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
)))
|
||||||
|
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
||||||
.epoch(1505296800)
|
.epoch(1505296800)
|
||||||
.length(Some(66738886))
|
|
||||||
.duration(Some(4171))
|
.duration(Some(4171))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -345,11 +333,11 @@ mod tests {
|
|||||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://traffic.megaphone.fm/FL5331443769.mp3",
|
"http://traffic.megaphone.fm/FL5331443769.mp3",
|
||||||
))).guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
|
)))
|
||||||
|
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
|
||||||
.epoch(1502272800)
|
.epoch(1502272800)
|
||||||
.length(Some(67527575))
|
|
||||||
.duration(Some(4415))
|
.duration(Some(4415))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -364,12 +352,13 @@ mod tests {
|
|||||||
.title("The Super Bowl of Racism")
|
.title("The Super Bowl of Racism")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
||||||
))).description(Some(String::from(descr)))
|
)))
|
||||||
|
.description(Some(String::from(descr)))
|
||||||
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
||||||
.length(Some(66738886))
|
.length(Some(66738886))
|
||||||
.epoch(1505296800)
|
.epoch(1505296800)
|
||||||
.duration(Some(4171))
|
.duration(Some(4171))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -387,12 +376,13 @@ mod tests {
|
|||||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://traffic.megaphone.fm/FL5331443769.mp3",
|
"http://traffic.megaphone.fm/FL5331443769.mp3",
|
||||||
))).description(Some(String::from(descr)))
|
)))
|
||||||
|
.description(Some(String::from(descr)))
|
||||||
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
|
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
|
||||||
.length(Some(67527575))
|
.length(Some(67527575))
|
||||||
.epoch(1502272800)
|
.epoch(1502272800)
|
||||||
.duration(Some(4415))
|
.duration(Some(4415))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -401,12 +391,13 @@ mod tests {
|
|||||||
.title("The Super Bowl of Racism")
|
.title("The Super Bowl of Racism")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
"http://traffic.megaphone.fm/PPY6458293736.mp3",
|
||||||
))).description(Some(String::from("New description")))
|
)))
|
||||||
|
.description(Some(String::from("New description")))
|
||||||
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
|
||||||
.length(Some(66738886))
|
.length(Some(66738886))
|
||||||
.epoch(1505296800)
|
.epoch(1505296800)
|
||||||
.duration(Some(424242))
|
.duration(Some(424242))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -415,11 +406,11 @@ mod tests {
|
|||||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
|
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
|
||||||
))).guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
|
)))
|
||||||
.length(Some(46479789))
|
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
|
||||||
.epoch(1505280282)
|
.epoch(1505280282)
|
||||||
.duration(Some(5733))
|
.duration(Some(5733))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -428,11 +419,11 @@ mod tests {
|
|||||||
.title("Gnome Does it Again | LUP 213")
|
.title("Gnome Does it Again | LUP 213")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
|
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
|
||||||
))).guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
|
)))
|
||||||
|
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
|
||||||
.epoch(1504670247)
|
.epoch(1504670247)
|
||||||
.length(Some(36544272))
|
|
||||||
.duration(Some(4491))
|
.duration(Some(4491))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -446,12 +437,13 @@ mod tests {
|
|||||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
|
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
|
||||||
))).description(Some(String::from(descr)))
|
)))
|
||||||
|
.description(Some(String::from(descr)))
|
||||||
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
|
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
|
||||||
.length(Some(46479789))
|
.length(Some(46479789))
|
||||||
.epoch(1505280282)
|
.epoch(1505280282)
|
||||||
.duration(Some(5733))
|
.duration(Some(5733))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
@@ -467,93 +459,86 @@ mod tests {
|
|||||||
.title("Gnome Does it Again | LUP 213")
|
.title("Gnome Does it Again | LUP 213")
|
||||||
.uri(Some(String::from(
|
.uri(Some(String::from(
|
||||||
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
|
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0213.mp3",
|
||||||
))).description(Some(String::from(descr)))
|
)))
|
||||||
|
.description(Some(String::from(descr)))
|
||||||
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
|
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
|
||||||
.length(Some(36544272))
|
.length(Some(36544272))
|
||||||
.epoch(1504670247)
|
.epoch(1504670247)
|
||||||
.duration(Some(4491))
|
.duration(Some(4491))
|
||||||
.show_id(42)
|
.podcast_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_minimal_intercepted() -> Result<(), Error> {
|
fn test_new_episode_minimal_intercepted() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(14).unwrap();
|
let episode = channel.items().iter().nth(14).unwrap();
|
||||||
let ep = NewEpisodeMinimal::new(&episode, 42)?;
|
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_1);
|
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_1);
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(15).unwrap();
|
let episode = channel.items().iter().nth(15).unwrap();
|
||||||
let ep = NewEpisodeMinimal::new(&episode, 42)?;
|
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_2);
|
assert_eq!(ep, *EXPECTED_MINIMAL_INTERCEPTED_2);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_intercepted() -> Result<(), Error> {
|
fn test_new_episode_intercepted() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(14).unwrap();
|
let episode = channel.items().iter().nth(14).unwrap();
|
||||||
let ep = NewEpisode::new(&episode, 42)?;
|
let ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
|
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(15).unwrap();
|
let episode = channel.items().iter().nth(15).unwrap();
|
||||||
let ep = NewEpisode::new(&episode, 42)?;
|
let ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
|
|
||||||
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
|
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_minimal_lup() -> Result<(), Error> {
|
fn test_new_episode_minimal_lup() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(18).unwrap();
|
let episode = channel.items().iter().nth(18).unwrap();
|
||||||
let ep = NewEpisodeMinimal::new(&episode, 42)?;
|
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_1);
|
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_1);
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(19).unwrap();
|
let episode = channel.items().iter().nth(19).unwrap();
|
||||||
let ep = NewEpisodeMinimal::new(&episode, 42)?;
|
let ep = NewEpisodeMinimal::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_2);
|
assert_eq!(ep, *EXPECTED_MINIMAL_LUP_2);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_lup() -> Result<(), Error> {
|
fn test_new_episode_lup() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(18).unwrap();
|
let episode = channel.items().iter().nth(18).unwrap();
|
||||||
let ep = NewEpisode::new(&episode, 42)?;
|
let ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_LUP_1);
|
assert_eq!(ep, *EXPECTED_LUP_1);
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(19).unwrap();
|
let episode = channel.items().iter().nth(19).unwrap();
|
||||||
let ep = NewEpisode::new(&episode, 42)?;
|
let ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
assert_eq!(ep, *EXPECTED_LUP_2);
|
assert_eq!(ep, *EXPECTED_LUP_2);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_minimal_into_new_episode() -> Result<(), Error> {
|
fn test_minimal_into_new_episode() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let item = channel.items().iter().nth(14).unwrap();
|
let item = channel.items().iter().nth(14).unwrap();
|
||||||
let ep = EXPECTED_MINIMAL_INTERCEPTED_1
|
let ep = EXPECTED_MINIMAL_INTERCEPTED_1
|
||||||
.clone()
|
.clone()
|
||||||
.into_new_episode(&item);
|
.into_new_episode(&item);
|
||||||
println!(
|
|
||||||
"EPISODE: {:#?}\nEXPECTED: {:#?}",
|
|
||||||
ep, *EXPECTED_INTERCEPTED_1
|
|
||||||
);
|
|
||||||
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
|
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
|
||||||
|
|
||||||
let item = channel.items().iter().nth(15).unwrap();
|
let item = channel.items().iter().nth(15).unwrap();
|
||||||
@@ -561,58 +546,61 @@ mod tests {
|
|||||||
.clone()
|
.clone()
|
||||||
.into_new_episode(&item);
|
.into_new_episode(&item);
|
||||||
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
|
assert_eq!(ep, *EXPECTED_INTERCEPTED_2);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_insert() -> Result<(), Error> {
|
fn test_new_episode_insert() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(14).unwrap();
|
let episode = channel.items().iter().nth(14).unwrap();
|
||||||
let new_ep = NewEpisode::new(&episode, 42)?;
|
let new_ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
new_ep.insert()?;
|
new_ep.insert().unwrap();
|
||||||
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id())?;
|
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
|
||||||
|
|
||||||
assert_eq!(new_ep, ep);
|
assert_eq!(new_ep, ep);
|
||||||
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_1);
|
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_1);
|
||||||
assert_eq!(&*EXPECTED_INTERCEPTED_1, &ep);
|
assert_eq!(&*EXPECTED_INTERCEPTED_1, &ep);
|
||||||
|
|
||||||
let episode = channel.items().iter().nth(15).unwrap();
|
let episode = channel.items().iter().nth(15).unwrap();
|
||||||
let new_ep = NewEpisode::new(&episode, 42)?;
|
let new_ep = NewEpisode::new(&episode, 42).unwrap();
|
||||||
new_ep.insert()?;
|
new_ep.insert().unwrap();
|
||||||
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id())?;
|
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
|
||||||
|
|
||||||
assert_eq!(new_ep, ep);
|
assert_eq!(new_ep, ep);
|
||||||
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_2);
|
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_2);
|
||||||
assert_eq!(&*EXPECTED_INTERCEPTED_2, &ep);
|
assert_eq!(&*EXPECTED_INTERCEPTED_2, &ep);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_update() -> Result<(), Error> {
|
fn test_new_episode_update() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let old = EXPECTED_INTERCEPTED_1.clone().to_episode()?;
|
let old = EXPECTED_INTERCEPTED_1.clone().to_episode().unwrap();
|
||||||
|
|
||||||
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
|
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
|
||||||
updated.update(old.rowid())?;
|
updated.update(old.rowid()).unwrap();
|
||||||
let new = dbqueries::get_episode_from_pk(old.title(), old.show_id())?;
|
let mut new = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
|
||||||
|
|
||||||
// Assert that updating does not change the rowid and show_id
|
// Assert that updating does not change the rowid and podcast_id
|
||||||
assert_ne!(old, new);
|
assert_ne!(old, new);
|
||||||
assert_eq!(old.rowid(), new.rowid());
|
assert_eq!(old.rowid(), new.rowid());
|
||||||
assert_eq!(old.show_id(), new.show_id());
|
assert_eq!(old.podcast_id(), new.podcast_id());
|
||||||
|
|
||||||
assert_eq!(updated, &new);
|
assert_eq!(updated, &new);
|
||||||
assert_ne!(updated, &old);
|
assert_ne!(updated, &old);
|
||||||
Ok(())
|
|
||||||
|
new.set_archive(true);
|
||||||
|
new.save().unwrap();
|
||||||
|
|
||||||
|
let new2 = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
|
||||||
|
assert_eq!(true, new2.archive());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_index() -> Result<(), Error> {
|
fn test_new_episode_index() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let expected = &*EXPECTED_INTERCEPTED_1;
|
let expected = &*EXPECTED_INTERCEPTED_1;
|
||||||
|
|
||||||
// First insert
|
// First insert
|
||||||
@@ -620,7 +608,7 @@ mod tests {
|
|||||||
// Second identical, This should take the early return path
|
// Second identical, This should take the early return path
|
||||||
assert!(expected.index().is_ok());
|
assert!(expected.index().is_ok());
|
||||||
// Get the episode
|
// Get the episode
|
||||||
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
|
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
|
||||||
// Assert that NewPodcast is equal to the Indexed one
|
// Assert that NewPodcast is equal to the Indexed one
|
||||||
assert_eq!(*expected, old);
|
assert_eq!(*expected, old);
|
||||||
|
|
||||||
@@ -629,33 +617,42 @@ mod tests {
|
|||||||
// Update the podcast
|
// Update the podcast
|
||||||
assert!(updated.index().is_ok());
|
assert!(updated.index().is_ok());
|
||||||
// Get the new Podcast
|
// Get the new Podcast
|
||||||
let new = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
|
let new = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
|
||||||
// Assert it's diff from the old one.
|
// Assert it's diff from the old one.
|
||||||
assert_ne!(new, old);
|
assert_ne!(new, old);
|
||||||
assert_eq!(*updated, new);
|
assert_eq!(*updated, new);
|
||||||
assert_eq!(new.rowid(), old.rowid());
|
assert_eq!(new.rowid(), old.rowid());
|
||||||
assert_eq!(new.show_id(), old.show_id());
|
assert_eq!(new.podcast_id(), old.podcast_id());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_episode_to_episode() -> Result<(), Error> {
|
fn test_new_episode_to_episode() {
|
||||||
let expected = &*EXPECTED_INTERCEPTED_1;
|
let expected = &*EXPECTED_INTERCEPTED_1;
|
||||||
|
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
|
||||||
|
|
||||||
// Assert insert() produces the same result that you would get with to_podcast()
|
// Assert insert() produces the same result that you would get with to_podcast()
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
expected.insert()?;
|
expected.insert().unwrap();
|
||||||
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
|
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
|
||||||
let ep = expected.to_episode()?;
|
let ep = expected.to_episode().unwrap();
|
||||||
assert_eq!(old, ep);
|
assert_eq!(old, ep);
|
||||||
|
|
||||||
// Same as above, diff order
|
// Same as above, diff order
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let ep = expected.to_episode()?;
|
let ep = expected.to_episode().unwrap();
|
||||||
// This should error as a unique constrain violation
|
// This should error as a unique constrain violation
|
||||||
assert!(expected.insert().is_err());
|
assert!(expected.insert().is_err());
|
||||||
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id())?;
|
let mut old =
|
||||||
|
dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
|
||||||
assert_eq!(old, ep);
|
assert_eq!(old, ep);
|
||||||
Ok(())
|
|
||||||
|
old.set_archive(true);
|
||||||
|
old.save().unwrap();
|
||||||
|
|
||||||
|
// Assert that it does not mess with user preferences
|
||||||
|
let ep = updated.to_episode().unwrap();
|
||||||
|
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
|
||||||
|
assert_eq!(old, ep);
|
||||||
|
assert_eq!(old.archive(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,21 +4,21 @@ use diesel::prelude::*;
|
|||||||
use rss;
|
use rss;
|
||||||
|
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use models::Show;
|
use models::Podcast;
|
||||||
use models::{Index, Insert, Update};
|
use models::{Index, Insert, Update};
|
||||||
use schema::shows;
|
use schema::podcast;
|
||||||
|
|
||||||
use database::connection;
|
use database::connection;
|
||||||
use dbqueries;
|
use dbqueries;
|
||||||
use utils::url_cleaner;
|
use utils::url_cleaner;
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset)]
|
#[derive(Insertable, AsChangeset)]
|
||||||
#[table_name = "shows"]
|
#[table_name = "podcast"]
|
||||||
#[derive(Debug, Clone, Default, Builder, PartialEq)]
|
#[derive(Debug, Clone, Default, Builder, PartialEq)]
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
#[builder(derive(Debug))]
|
#[builder(derive(Debug))]
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
pub(crate) struct NewShow {
|
pub(crate) struct NewPodcast {
|
||||||
title: String,
|
title: String,
|
||||||
link: String,
|
link: String,
|
||||||
description: String,
|
description: String,
|
||||||
@@ -26,15 +26,15 @@ pub(crate) struct NewShow {
|
|||||||
source_id: i32,
|
source_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Insert<()> for NewShow {
|
impl Insert<()> for NewPodcast {
|
||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
fn insert(&self) -> Result<(), Self::Error> {
|
fn insert(&self) -> Result<(), Self::Error> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
diesel::insert_into(shows)
|
diesel::insert_into(podcast)
|
||||||
.values(self)
|
.values(self)
|
||||||
.execute(&con)
|
.execute(&con)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -42,16 +42,16 @@ impl Insert<()> for NewShow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Update<()> for NewShow {
|
impl Update<()> for NewPodcast {
|
||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
fn update(&self, show_id: i32) -> Result<(), Self::Error> {
|
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
|
||||||
use schema::shows::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
info!("Updating {}", self.title);
|
info!("Updating {}", self.title);
|
||||||
diesel::update(shows.filter(id.eq(show_id)))
|
diesel::update(podcast.filter(id.eq(podcast_id)))
|
||||||
.set(self)
|
.set(self)
|
||||||
.execute(&con)
|
.execute(&con)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -61,7 +61,7 @@ impl Update<()> for NewShow {
|
|||||||
|
|
||||||
// TODO: Maybe return an Enum<Action(Resut)> Instead.
|
// TODO: Maybe return an Enum<Action(Resut)> Instead.
|
||||||
// It would make unti testing better too.
|
// It would make unti testing better too.
|
||||||
impl Index<()> for NewShow {
|
impl Index<()> for NewPodcast {
|
||||||
type Error = DataError;
|
type Error = DataError;
|
||||||
|
|
||||||
fn index(&self) -> Result<(), DataError> {
|
fn index(&self) -> Result<(), DataError> {
|
||||||
@@ -81,8 +81,8 @@ impl Index<()> for NewShow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<Show> for NewShow {
|
impl PartialEq<Podcast> for NewPodcast {
|
||||||
fn eq(&self, other: &Show) -> bool {
|
fn eq(&self, other: &Podcast) -> bool {
|
||||||
(self.link() == other.link())
|
(self.link() == other.link())
|
||||||
&& (self.title() == other.title())
|
&& (self.title() == other.title())
|
||||||
&& (self.image_uri() == other.image_uri())
|
&& (self.image_uri() == other.image_uri())
|
||||||
@@ -91,9 +91,9 @@ impl PartialEq<Show> for NewShow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewShow {
|
impl NewPodcast {
|
||||||
/// Parses a `rss::Channel` into a `NewShow` Struct.
|
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
|
||||||
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewShow {
|
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
|
||||||
let title = chan.title().trim();
|
let title = chan.title().trim();
|
||||||
let link = url_cleaner(chan.link().trim());
|
let link = url_cleaner(chan.link().trim());
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ impl NewShow {
|
|||||||
// If itunes is None, try to get the channel.image from the rss spec
|
// If itunes is None, try to get the channel.image from the rss spec
|
||||||
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().trim().to_owned()));
|
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().trim().to_owned()));
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.description(description)
|
.description(description)
|
||||||
.link(link)
|
.link(link)
|
||||||
@@ -122,14 +122,14 @@ impl NewShow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Look out for when tryinto lands into stable.
|
// Look out for when tryinto lands into stable.
|
||||||
pub(crate) fn to_podcast(&self) -> Result<Show, DataError> {
|
pub(crate) fn to_podcast(&self) -> Result<Podcast, DataError> {
|
||||||
self.index()?;
|
self.index()?;
|
||||||
dbqueries::get_podcast_from_source_id(self.source_id).map_err(From::from)
|
dbqueries::get_podcast_from_source_id(self.source_id).map_err(From::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore the following geters. They are used in unit tests mainly.
|
// Ignore the following geters. They are used in unit tests mainly.
|
||||||
impl NewShow {
|
impl NewPodcast {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn source_id(&self) -> i32 {
|
pub(crate) fn source_id(&self) -> i32 {
|
||||||
self.source_id
|
self.source_id
|
||||||
@@ -157,18 +157,17 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
// use tokio_core::reactor::Core;
|
// use tokio_core::reactor::Core;
|
||||||
|
|
||||||
use failure::Error;
|
|
||||||
use rss::Channel;
|
use rss::Channel;
|
||||||
|
|
||||||
use database::truncate_db;
|
use database::truncate_db;
|
||||||
use models::NewShowBuilder;
|
use models::{NewPodcastBuilder, Save};
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
||||||
// Pre-built expected NewShow structs.
|
// Pre-built expected NewPodcast structs.
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EXPECTED_INTERCEPTED: NewShow = {
|
static ref EXPECTED_INTERCEPTED: NewPodcast = {
|
||||||
let descr = "The people behind The Intercept’s fearless reporting and incisive \
|
let descr = "The people behind The Intercept’s fearless reporting and incisive \
|
||||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
|
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
|
||||||
others—discuss the crucial issues of our time: national security, civil \
|
others—discuss the crucial issues of our time: national security, civil \
|
||||||
@@ -176,7 +175,7 @@ mod tests {
|
|||||||
artists, thinkers, and newsmakers who challenge our preconceptions about \
|
artists, thinkers, and newsmakers who challenge our preconceptions about \
|
||||||
the world we live in.";
|
the world we live in.";
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("Intercepted with Jeremy Scahill")
|
.title("Intercepted with Jeremy Scahill")
|
||||||
.link("https://theintercept.com/podcasts")
|
.link("https://theintercept.com/podcasts")
|
||||||
.description(descr)
|
.description(descr)
|
||||||
@@ -189,22 +188,23 @@ mod tests {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref EXPECTED_LUP: NewShow = {
|
static ref EXPECTED_LUP: NewPodcast = {
|
||||||
let descr = "An open show powered by community LINUX Unplugged takes the best \
|
let descr = "An open show powered by community LINUX Unplugged takes the best \
|
||||||
attributes of open collaboration and focuses them into a weekly \
|
attributes of open collaboration and focuses them into a weekly \
|
||||||
lifestyle show about Linux.";
|
lifestyle show about Linux.";
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("LINUX Unplugged Podcast")
|
.title("LINUX Unplugged Podcast")
|
||||||
.link("http://www.jupiterbroadcasting.com/")
|
.link("http://www.jupiterbroadcasting.com/")
|
||||||
.description(descr)
|
.description(descr)
|
||||||
.image_uri(Some(String::from(
|
.image_uri(Some(String::from(
|
||||||
"http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg",
|
"http://www.jupiterbroadcasting.com/images/LASUN-Badge1400.jpg",
|
||||||
))).source_id(42)
|
)))
|
||||||
|
.source_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref EXPECTED_TIPOFF: NewShow = {
|
static ref EXPECTED_TIPOFF: NewPodcast = {
|
||||||
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
|
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
|
||||||
scenes of some of the best investigative journalism from recent years. \
|
scenes of some of the best investigative journalism from recent years. \
|
||||||
Each episode we’ll be digging into an investigative scoop- hearing from \
|
Each episode we’ll be digging into an investigative scoop- hearing from \
|
||||||
@@ -215,18 +215,19 @@ mod tests {
|
|||||||
complicated detective work that goes into doing great investigative \
|
complicated detective work that goes into doing great investigative \
|
||||||
journalism- then this is the podcast for you.</p>";
|
journalism- then this is the podcast for you.</p>";
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("The Tip Off")
|
.title("The Tip Off")
|
||||||
.link("http://www.acast.com/thetipoff")
|
.link("http://www.acast.com/thetipoff")
|
||||||
.description(desc)
|
.description(desc)
|
||||||
.image_uri(Some(String::from(
|
.image_uri(Some(String::from(
|
||||||
"https://imagecdn.acast.com/image?h=1500&w=1500&source=http%3A%2F%2Fi1.sndcdn.\
|
"https://imagecdn.acast.com/image?h=1500&w=1500&source=http%3A%2F%2Fi1.sndcdn.\
|
||||||
com%2Favatars-000317856075-a2coqz-original.jpg",
|
com%2Favatars-000317856075-a2coqz-original.jpg",
|
||||||
))).source_id(42)
|
)))
|
||||||
|
.source_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref EXPECTED_STARS: NewShow = {
|
static ref EXPECTED_STARS: NewPodcast = {
|
||||||
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
|
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
|
||||||
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
|
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
|
||||||
love, a crashed UFO, an alien body, and an impossible heist unlike any \
|
love, a crashed UFO, an alien body, and an impossible heist unlike any \
|
||||||
@@ -236,7 +237,7 @@ mod tests {
|
|||||||
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
|
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
|
||||||
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
|
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("Steal the Stars")
|
.title("Steal the Stars")
|
||||||
.link("http://tor-labs.com/")
|
.link("http://tor-labs.com/")
|
||||||
.description(descr)
|
.description(descr)
|
||||||
@@ -245,23 +246,24 @@ mod tests {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref EXPECTED_CODE: NewShow = {
|
static ref EXPECTED_CODE: NewPodcast = {
|
||||||
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
|
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
|
||||||
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
|
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
|
||||||
Livingston-Gray. Brought to you by @therubyrep.";
|
Livingston-Gray. Brought to you by @therubyrep.";
|
||||||
|
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("Greater Than Code")
|
.title("Greater Than Code")
|
||||||
.link("https://www.greaterthancode.com/")
|
.link("https://www.greaterthancode.com/")
|
||||||
.description(descr)
|
.description(descr)
|
||||||
.image_uri(Some(String::from(
|
.image_uri(Some(String::from(
|
||||||
"http://www.greaterthancode.com/wp-content/uploads/2016/10/code1400-4.jpg",
|
"http://www.greaterthancode.com/wp-content/uploads/2016/10/code1400-4.jpg",
|
||||||
))).source_id(42)
|
)))
|
||||||
|
.source_id(42)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref EXPECTED_ELLINOFRENEIA: NewShow = {
|
static ref EXPECTED_ELLINOFRENEIA: NewPodcast = {
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("Ελληνοφρένεια")
|
.title("Ελληνοφρένεια")
|
||||||
.link("https://ellinofreneia.sealabs.net/feed.rss")
|
.link("https://ellinofreneia.sealabs.net/feed.rss")
|
||||||
.description("Ανεπίσημο feed της Ελληνοφρένειας")
|
.description("Ανεπίσημο feed της Ελληνοφρένειας")
|
||||||
@@ -270,8 +272,8 @@ mod tests {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
static ref UPDATED_DESC_INTERCEPTED: NewShow = {
|
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
|
||||||
NewShowBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
.title("Intercepted with Jeremy Scahill")
|
.title("Intercepted with Jeremy Scahill")
|
||||||
.link("https://theintercept.com/podcasts")
|
.link("https://theintercept.com/podcasts")
|
||||||
.description("New Description")
|
.description("New Description")
|
||||||
@@ -287,80 +289,73 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_intercepted() -> Result<(), Error> {
|
fn test_new_podcast_intercepted() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_INTERCEPTED, pd);
|
assert_eq!(*EXPECTED_INTERCEPTED, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_lup() -> Result<(), Error> {
|
fn test_new_podcast_lup() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_LUP, pd);
|
assert_eq!(*EXPECTED_LUP, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_thetipoff() -> Result<(), Error> {
|
fn test_new_podcast_thetipoff() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-TheTipOff.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-TheTipOff.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_TIPOFF, pd);
|
assert_eq!(*EXPECTED_TIPOFF, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_steal_the_stars() -> Result<(), Error> {
|
fn test_new_podcast_steal_the_stars() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-StealTheStars.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-StealTheStars.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_STARS, pd);
|
assert_eq!(*EXPECTED_STARS, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_greater_than_code() -> Result<(), Error> {
|
fn test_new_podcast_greater_than_code() {
|
||||||
let file = File::open("tests/feeds/2018-01-20-GreaterThanCode.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-GreaterThanCode.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_CODE, pd);
|
assert_eq!(*EXPECTED_CODE, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_ellinofreneia() -> Result<(), Error> {
|
fn test_new_podcast_ellinofreneia() {
|
||||||
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml")?;
|
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let pd = NewShow::new(&channel, 42);
|
let pd = NewPodcast::new(&channel, 42);
|
||||||
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
|
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// This maybe could be a doc test on insert.
|
// This maybe could be a doc test on insert.
|
||||||
fn test_new_podcast_insert() -> Result<(), Error> {
|
fn test_new_podcast_insert() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml")?;
|
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
|
||||||
let channel = Channel::read_from(BufReader::new(file))?;
|
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||||
|
|
||||||
let npd = NewShow::new(&channel, 42);
|
let npd = NewPodcast::new(&channel, 42);
|
||||||
npd.insert()?;
|
npd.insert().unwrap();
|
||||||
let pd = dbqueries::get_podcast_from_source_id(42)?;
|
let pd = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
|
|
||||||
assert_eq!(npd, pd);
|
assert_eq!(npd, pd);
|
||||||
assert_eq!(*EXPECTED_INTERCEPTED, npd);
|
assert_eq!(*EXPECTED_INTERCEPTED, npd);
|
||||||
assert_eq!(&*EXPECTED_INTERCEPTED, &pd);
|
assert_eq!(&*EXPECTED_INTERCEPTED, &pd);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -368,64 +363,77 @@ mod tests {
|
|||||||
// Currently there's a test that only checks new description or title.
|
// Currently there's a test that only checks new description or title.
|
||||||
// If you have time and want to help, implement the test for the other fields
|
// If you have time and want to help, implement the test for the other fields
|
||||||
// too.
|
// too.
|
||||||
fn test_new_podcast_update() -> Result<(), Error> {
|
fn test_new_podcast_update() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let old = EXPECTED_INTERCEPTED.to_podcast()?;
|
let old = EXPECTED_INTERCEPTED.to_podcast().unwrap();
|
||||||
|
|
||||||
let updated = &*UPDATED_DESC_INTERCEPTED;
|
let updated = &*UPDATED_DESC_INTERCEPTED;
|
||||||
updated.update(old.id())?;
|
updated.update(old.id()).unwrap();
|
||||||
let new = dbqueries::get_podcast_from_source_id(42)?;
|
let mut new = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
|
|
||||||
assert_ne!(old, new);
|
assert_ne!(old, new);
|
||||||
assert_eq!(old.id(), new.id());
|
assert_eq!(old.id(), new.id());
|
||||||
assert_eq!(old.source_id(), new.source_id());
|
assert_eq!(old.source_id(), new.source_id());
|
||||||
assert_eq!(updated, &new);
|
assert_eq!(updated, &new);
|
||||||
assert_ne!(updated, &old);
|
assert_ne!(updated, &old);
|
||||||
Ok(())
|
|
||||||
|
// Chech that the update does not override user preferences.
|
||||||
|
new.set_archive(true);
|
||||||
|
new.save().unwrap();
|
||||||
|
|
||||||
|
let new2 = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
|
assert_eq!(true, new2.archive());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_podcast_index() -> Result<(), Error> {
|
fn test_new_podcast_index() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
// First insert
|
// First insert
|
||||||
assert!(EXPECTED_INTERCEPTED.index().is_ok());
|
assert!(EXPECTED_INTERCEPTED.index().is_ok());
|
||||||
// Second identical, This should take the early return path
|
// Second identical, This should take the early return path
|
||||||
assert!(EXPECTED_INTERCEPTED.index().is_ok());
|
assert!(EXPECTED_INTERCEPTED.index().is_ok());
|
||||||
// Get the podcast
|
// Get the podcast
|
||||||
let old = dbqueries::get_podcast_from_source_id(42)?;
|
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
// Assert that NewShow is equal to the Indexed one
|
// Assert that NewPodcast is equal to the Indexed one
|
||||||
assert_eq!(&*EXPECTED_INTERCEPTED, &old);
|
assert_eq!(&*EXPECTED_INTERCEPTED, &old);
|
||||||
|
|
||||||
let updated = &*UPDATED_DESC_INTERCEPTED;
|
let updated = &*UPDATED_DESC_INTERCEPTED;
|
||||||
|
|
||||||
// Update the podcast
|
// Update the podcast
|
||||||
assert!(updated.index().is_ok());
|
assert!(updated.index().is_ok());
|
||||||
// Get the new Show
|
// Get the new Podcast
|
||||||
let new = dbqueries::get_podcast_from_source_id(42)?;
|
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
// Assert it's diff from the old one.
|
// Assert it's diff from the old one.
|
||||||
assert_ne!(new, old);
|
assert_ne!(new, old);
|
||||||
assert_eq!(new.id(), old.id());
|
assert_eq!(new.id(), old.id());
|
||||||
assert_eq!(new.source_id(), old.source_id());
|
assert_eq!(new.source_id(), old.source_id());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_podcast() -> Result<(), Error> {
|
fn test_to_podcast() {
|
||||||
// Assert insert() produces the same result that you would get with to_podcast()
|
// Assert insert() produces the same result that you would get with to_podcast()
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
EXPECTED_INTERCEPTED.insert()?;
|
EXPECTED_INTERCEPTED.insert().unwrap();
|
||||||
let old = dbqueries::get_podcast_from_source_id(42)?;
|
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
let pd = EXPECTED_INTERCEPTED.to_podcast()?;
|
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
|
||||||
assert_eq!(old, pd);
|
assert_eq!(old, pd);
|
||||||
|
|
||||||
// Same as above, diff order
|
// Same as above, diff order
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let pd = EXPECTED_INTERCEPTED.to_podcast()?;
|
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
|
||||||
// This should error as a unique constrain violation
|
// This should error as a unique constrain violation
|
||||||
assert!(EXPECTED_INTERCEPTED.insert().is_err());
|
assert!(EXPECTED_INTERCEPTED.insert().is_err());
|
||||||
let old = dbqueries::get_podcast_from_source_id(42)?;
|
let mut old = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
assert_eq!(old, pd);
|
assert_eq!(old, pd);
|
||||||
Ok(())
|
|
||||||
|
old.set_archive(true);
|
||||||
|
old.save().unwrap();
|
||||||
|
|
||||||
|
// Assert that it does not mess with user preferences
|
||||||
|
let pd = UPDATED_DESC_INTERCEPTED.to_podcast().unwrap();
|
||||||
|
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
|
||||||
|
assert_eq!(old, pd);
|
||||||
|
assert_eq!(old.archive(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
use diesel::SaveChangesDsl;
|
||||||
|
|
||||||
|
use database::connection;
|
||||||
|
use errors::DataError;
|
||||||
|
use models::{Save, Source};
|
||||||
|
use schema::podcast;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[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 Save<Podcast> for Podcast {
|
||||||
|
type Error = DataError;
|
||||||
|
|
||||||
|
/// Helper method to easily save/"sync" current state of self to the
|
||||||
|
/// Database.
|
||||||
|
fn save(&self) -> Result<Podcast, Self::Error> {
|
||||||
|
let db = connection();
|
||||||
|
let tempdb = db.get()?;
|
||||||
|
|
||||||
|
self.save_changes::<Podcast>(&*tempdb).map_err(From::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Podcast {
|
||||||
|
/// Get the Feed `id`.
|
||||||
|
pub fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug, Clone)]
|
||||||
|
/// Diesel Model of the podcast cover query.
|
||||||
|
/// Used for fetching information about a Podcast's cover.
|
||||||
|
pub struct PodcastCoverQuery {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
image_uri: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Podcast> for PodcastCoverQuery {
|
||||||
|
fn from(p: Podcast) -> PodcastCoverQuery {
|
||||||
|
PodcastCoverQuery {
|
||||||
|
id: p.id(),
|
||||||
|
title: p.title,
|
||||||
|
image_uri: p.image_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<Podcast>> for PodcastCoverQuery {
|
||||||
|
fn from(p: Arc<Podcast>) -> PodcastCoverQuery {
|
||||||
|
PodcastCoverQuery {
|
||||||
|
id: p.id(),
|
||||||
|
title: p.title.clone(),
|
||||||
|
image_uri: p.image_uri.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PodcastCoverQuery {
|
||||||
|
/// Get the Feed `id`.
|
||||||
|
pub fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Feed `title`.
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ impl Source {
|
|||||||
|
|
||||||
/// Extract Etag and LastModifier from res, and update self and the
|
/// Extract Etag and LastModifier from res, and update self and the
|
||||||
/// corresponding db row.
|
/// corresponding db row.
|
||||||
fn update_etag(mut self, res: &Response) -> Result<Self, DataError> {
|
fn update_etag(&mut self, res: &Response) -> Result<(), DataError> {
|
||||||
let headers = res.headers();
|
let headers = res.headers();
|
||||||
|
|
||||||
let etag = headers.get::<ETag>().map(|x| x.tag());
|
let etag = headers.get::<ETag>().map(|x| x.tag());
|
||||||
@@ -100,24 +100,10 @@ impl Source {
|
|||||||
if (self.http_etag() != etag) || (self.last_modified != lmod) {
|
if (self.http_etag() != etag) || (self.last_modified != lmod) {
|
||||||
self.set_http_etag(etag);
|
self.set_http_etag(etag);
|
||||||
self.set_last_modified(lmod);
|
self.set_last_modified(lmod);
|
||||||
self = self.save()?;
|
self.save()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self)
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the `HTTP` `Etag` and `Last-modified` headers.
|
|
||||||
fn clear_etags(mut self) -> Result<Self, DataError> {
|
|
||||||
if self.http_etag().is_some() || self.last_modified().is_some() {
|
|
||||||
debug!("Source etags before clear: {:#?}", &self);
|
|
||||||
self.http_etag = None;
|
|
||||||
self.last_modified = None;
|
|
||||||
self = self.save()?;
|
|
||||||
debug!("Source etags after clear: {:#?}", &self);
|
|
||||||
info!("Etag fields cleared succesfully for Source: {}", self.uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_err(self, context: &str, code: StatusCode) -> DataError {
|
fn make_err(self, context: &str, code: StatusCode) -> DataError {
|
||||||
@@ -135,25 +121,18 @@ impl Source {
|
|||||||
// 410: Feed deleted
|
// 410: Feed deleted
|
||||||
// TODO: Rething this api,
|
// TODO: Rething this api,
|
||||||
fn match_status(mut self, res: Response) -> Result<Response, DataError> {
|
fn match_status(mut self, res: Response) -> Result<Response, DataError> {
|
||||||
|
self.update_etag(&res)?;
|
||||||
let code = res.status();
|
let code = res.status();
|
||||||
match code {
|
|
||||||
// If request is succesful save the etag
|
|
||||||
StatusCode::NotModified | StatusCode::Ok => self = self.update_etag(&res)?,
|
|
||||||
// Clear the Etag/lmod else
|
|
||||||
_ => self = self.clear_etags()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
match code {
|
match code {
|
||||||
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
|
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
|
||||||
StatusCode::MovedPermanently | StatusCode::Found | StatusCode::PermanentRedirect => {
|
StatusCode::MovedPermanently => {
|
||||||
error!("Feed was moved permanently.");
|
error!("Feed was moved permanently.");
|
||||||
self = self.update_url(&res)?;
|
self.handle_301(&res)?;
|
||||||
return Err(DataError::F301(self));
|
|
||||||
}
|
|
||||||
StatusCode::TemporaryRedirect => {
|
|
||||||
debug!("307: Temporary Redirect.");
|
|
||||||
return Err(DataError::F301(self));
|
return Err(DataError::F301(self));
|
||||||
}
|
}
|
||||||
|
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
|
||||||
|
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
|
||||||
StatusCode::Unauthorized => return Err(self.make_err("401: Unauthorized.", code)),
|
StatusCode::Unauthorized => return Err(self.make_err("401: Unauthorized.", code)),
|
||||||
StatusCode::Forbidden => return Err(self.make_err("403: Forbidden.", code)),
|
StatusCode::Forbidden => return Err(self.make_err("403: Forbidden.", code)),
|
||||||
StatusCode::NotFound => return Err(self.make_err("404: Not found.", code)),
|
StatusCode::NotFound => return Err(self.make_err("404: Not found.", code)),
|
||||||
@@ -161,27 +140,21 @@ impl Source {
|
|||||||
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
|
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
|
||||||
_ => info!("HTTP StatusCode: {}", code),
|
_ => info!("HTTP StatusCode: {}", code),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_url(mut self, res: &Response) -> Result<Self, DataError> {
|
fn handle_301(&mut self, res: &Response) -> Result<(), DataError> {
|
||||||
let code = res.status();
|
|
||||||
let headers = res.headers();
|
let headers = res.headers();
|
||||||
info!("HTTP StatusCode: {}", code);
|
|
||||||
debug!("Headers {:#?}", headers);
|
|
||||||
|
|
||||||
if let Some(url) = headers.get::<Location>() {
|
if let Some(url) = headers.get::<Location>() {
|
||||||
debug!("Previous Source: {:#?}", &self);
|
|
||||||
|
|
||||||
self.set_uri(url.to_string());
|
self.set_uri(url.to_string());
|
||||||
self = self.clear_etags()?;
|
self.http_etag = None;
|
||||||
|
self.last_modified = None;
|
||||||
debug!("Updated Source: {:#?}", &self);
|
self.save()?;
|
||||||
info!("Feed url of Source {}, was updated succesfully.", self.id());
|
info!("Feed url was updated succesfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a new `Source` with the given `uri` and index it.
|
/// Construct a new `Source` with the given `uri` and index it.
|
||||||
@@ -204,11 +177,12 @@ impl Source {
|
|||||||
pub fn into_feed(
|
pub fn into_feed(
|
||||||
self,
|
self,
|
||||||
client: Client<HttpsConnector<HttpConnector>>,
|
client: Client<HttpsConnector<HttpConnector>>,
|
||||||
|
ignore_etags: bool,
|
||||||
) -> impl Future<Item = Feed, Error = DataError> {
|
) -> impl Future<Item = Feed, Error = DataError> {
|
||||||
let id = self.id();
|
let id = self.id();
|
||||||
let response = loop_fn(self, move |source| {
|
let response = loop_fn(self, move |source| {
|
||||||
source
|
source
|
||||||
.request_constructor(&client.clone())
|
.request_constructor(&client.clone(), ignore_etags)
|
||||||
.then(|res| match res {
|
.then(|res| match res {
|
||||||
Ok(response) => Ok(Loop::Break(response)),
|
Ok(response) => Ok(Loop::Break(response)),
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
@@ -232,9 +206,12 @@ impl Source {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make ignore_etags an Enum for better ergonomics.
|
||||||
|
// #bools_are_just_2variant_enmus
|
||||||
fn request_constructor(
|
fn request_constructor(
|
||||||
self,
|
self,
|
||||||
client: &Client<HttpsConnector<HttpConnector>>,
|
client: &Client<HttpsConnector<HttpConnector>>,
|
||||||
|
ignore_etags: bool,
|
||||||
) -> impl Future<Item = Response, Error = DataError> {
|
) -> impl Future<Item = Response, Error = DataError> {
|
||||||
// FIXME: remove unwrap somehow
|
// FIXME: remove unwrap somehow
|
||||||
let uri = Uri::from_str(self.uri()).unwrap();
|
let uri = Uri::from_str(self.uri()).unwrap();
|
||||||
@@ -243,14 +220,16 @@ impl Source {
|
|||||||
// Set the UserAgent cause ppl still seem to check it for some reason...
|
// Set the UserAgent cause ppl still seem to check it for some reason...
|
||||||
req.headers_mut().set(UserAgent::new(USER_AGENT));
|
req.headers_mut().set(UserAgent::new(USER_AGENT));
|
||||||
|
|
||||||
if let Some(etag) = self.http_etag() {
|
if !ignore_etags {
|
||||||
let tag = vec![EntityTag::new(true, etag.to_owned())];
|
if let Some(etag) = self.http_etag() {
|
||||||
req.headers_mut().set(IfNoneMatch::Items(tag));
|
let tag = vec![EntityTag::new(true, etag.to_owned())];
|
||||||
}
|
req.headers_mut().set(IfNoneMatch::Items(tag));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(lmod) = self.last_modified() {
|
if let Some(lmod) = self.last_modified() {
|
||||||
if let Ok(date) = lmod.parse::<HttpDate>() {
|
if let Ok(date) = lmod.parse::<HttpDate>() {
|
||||||
req.headers_mut().set(IfModifiedSince(date));
|
req.headers_mut().set(IfModifiedSince(date));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,31 +254,29 @@ fn response_to_channel(res: Response) -> impl Future<Item = Channel, Error = Dat
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use failure::Error;
|
|
||||||
use tokio_core::reactor::Core;
|
use tokio_core::reactor::Core;
|
||||||
|
|
||||||
use database::truncate_db;
|
use database::truncate_db;
|
||||||
use utils::get_feed;
|
use utils::get_feed;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_into_feed() -> Result<(), Error> {
|
fn test_into_feed() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
|
|
||||||
let mut core = Core::new()?;
|
let mut core = Core::new().unwrap();
|
||||||
let client = Client::configure()
|
let client = Client::configure()
|
||||||
.connector(HttpsConnector::new(4, &core.handle())?)
|
.connector(HttpsConnector::new(4, &core.handle()).unwrap())
|
||||||
.build(&core.handle());
|
.build(&core.handle());
|
||||||
|
|
||||||
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||||
com/InterceptedWithJeremyScahill";
|
com/InterceptedWithJeremyScahill";
|
||||||
let source = Source::from_url(url)?;
|
let source = Source::from_url(url).unwrap();
|
||||||
let id = source.id();
|
let id = source.id();
|
||||||
|
|
||||||
let feed = source.into_feed(client);
|
let feed = source.into_feed(client, true);
|
||||||
let feed = core.run(feed)?;
|
let feed = core.run(feed).unwrap();
|
||||||
|
|
||||||
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
|
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
|
||||||
assert_eq!(expected, feed);
|
assert_eq!(expected, feed);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,14 +35,15 @@ pub fn import_to_db<R: Read>(reader: R) -> Result<Vec<Source>, reader::Error> {
|
|||||||
.filter_map(|s| {
|
.filter_map(|s| {
|
||||||
if let Err(ref err) = s {
|
if let Err(ref err) = s {
|
||||||
let txt = "If you think this might be a bug please consider filling a report over \
|
let txt = "If you think this might be a bug please consider filling a report over \
|
||||||
at https://gitlab.gnome.org/World/podcasts/issues/new";
|
at https://gitlab.gnome.org/World/hammond/issues/new";
|
||||||
|
|
||||||
error!("Failed to import a Show: {}", err);
|
error!("Failed to import a Show: {}", err);
|
||||||
error!("{}", txt);
|
error!("{}", txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ok()
|
s.ok()
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(feeds)
|
Ok(feeds)
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,8 @@ pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Erro
|
|||||||
}
|
}
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}).collect::<Result<Vec<_>, reader::Error>>()?;
|
})
|
||||||
|
.collect::<Result<Vec<_>, reader::Error>>()?;
|
||||||
|
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
@@ -100,10 +102,9 @@ pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Erro
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use failure::Error;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract() -> Result<(), Error> {
|
fn test_extract() {
|
||||||
let int_title = String::from("Intercepted with Jeremy Scahill");
|
let int_title = String::from("Intercepted with Jeremy Scahill");
|
||||||
let int_url = String::from("https://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
let int_url = String::from("https://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
||||||
let int_desc =
|
let int_desc =
|
||||||
@@ -161,7 +162,6 @@ mod tests {
|
|||||||
url: dec_url
|
url: dec_url
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
assert_eq!(extract_sources(sample1.as_bytes())?, map);
|
assert_eq!(extract_sources(sample1.as_bytes()).unwrap(), map);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,13 +48,14 @@ type HttpsClient = Client<HttpsConnector<HttpConnector>>;
|
|||||||
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
|
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
|
||||||
pub fn pipeline<'a, S>(
|
pub fn pipeline<'a, S>(
|
||||||
sources: S,
|
sources: S,
|
||||||
|
ignore_etags: bool,
|
||||||
client: &HttpsClient,
|
client: &HttpsClient,
|
||||||
) -> impl Future<Item = Vec<()>, Error = DataError> + 'a
|
) -> impl Future<Item = Vec<()>, Error = DataError> + 'a
|
||||||
where
|
where
|
||||||
S: Stream<Item = Source, Error = DataError> + 'a,
|
S: Stream<Item = Source, Error = DataError> + 'a,
|
||||||
{
|
{
|
||||||
sources
|
sources
|
||||||
.and_then(clone!(client => move |s| s.into_feed(client.clone())))
|
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags)))
|
||||||
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
|
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
|
||||||
// the stream will stop at the first error so
|
// the stream will stop at the first error so
|
||||||
// we ensure that everything will succeded regardless.
|
// we ensure that everything will succeded regardless.
|
||||||
@@ -65,7 +66,7 @@ where
|
|||||||
|
|
||||||
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
|
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
|
||||||
/// runs the pipeline to completion. The `reactor::Core` is dropped afterwards.
|
/// runs the pipeline to completion. The `reactor::Core` is dropped afterwards.
|
||||||
pub fn run<S>(sources: S) -> Result<(), DataError>
|
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError>
|
||||||
where
|
where
|
||||||
S: IntoIterator<Item = Source>,
|
S: IntoIterator<Item = Source>,
|
||||||
{
|
{
|
||||||
@@ -76,7 +77,7 @@ where
|
|||||||
.build(&handle);
|
.build(&handle);
|
||||||
|
|
||||||
let stream = iter_ok::<_, DataError>(sources);
|
let stream = iter_ok::<_, DataError>(sources);
|
||||||
let p = pipeline(stream, &client);
|
let p = pipeline(stream, ignore_etags, &client);
|
||||||
core.run(p).map(|_| ())
|
core.run(p).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +86,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use database::truncate_db;
|
use database::truncate_db;
|
||||||
use dbqueries;
|
use dbqueries;
|
||||||
use failure::Error;
|
|
||||||
use Source;
|
use Source;
|
||||||
|
|
||||||
// (path, url) tuples.
|
// (path, url) tuples.
|
||||||
@@ -101,29 +101,28 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Insert feeds and update/index them.
|
/// Insert feeds and update/index them.
|
||||||
fn test_pipeline() -> Result<(), Error> {
|
fn test_pipeline() {
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
let bad_url = "https://gitlab.gnome.org/World/podcasts.atom";
|
let bad_url = "https://gitlab.gnome.org/World/hammond.atom";
|
||||||
// if a stream returns error/None it stops
|
// if a stream returns error/None it stops
|
||||||
// bad we want to parse all feeds regardless if one fails
|
// bad we want to parse all feeds regardless if one fails
|
||||||
Source::from_url(bad_url)?;
|
Source::from_url(bad_url).unwrap();
|
||||||
|
|
||||||
URLS.iter().for_each(|url| {
|
URLS.iter().for_each(|url| {
|
||||||
// Index the urls into the source table.
|
// Index the urls into the source table.
|
||||||
Source::from_url(url).unwrap();
|
Source::from_url(url).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let sources = dbqueries::get_sources()?;
|
let sources = dbqueries::get_sources().unwrap();
|
||||||
run(sources)?;
|
run(sources, true).unwrap();
|
||||||
|
|
||||||
let sources = dbqueries::get_sources()?;
|
let sources = dbqueries::get_sources().unwrap();
|
||||||
// Run again to cover Unique constrains erros.
|
// Run again to cover Unique constrains erros.
|
||||||
run(sources)?;
|
run(sources, true).unwrap();
|
||||||
|
|
||||||
// Assert the index rows equal the controlled results
|
// Assert the index rows equal the controlled results
|
||||||
assert_eq!(dbqueries::get_sources()?.len(), 6);
|
assert_eq!(dbqueries::get_sources().unwrap().len(), 6);
|
||||||
assert_eq!(dbqueries::get_podcasts()?.len(), 5);
|
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
|
||||||
assert_eq!(dbqueries::get_episodes()?.len(), 354);
|
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
#![allow(warnings)]
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
episodes (title, show_id) {
|
episode (title, podcast_id) {
|
||||||
rowid -> Integer,
|
rowid -> Integer,
|
||||||
title -> Text,
|
title -> Text,
|
||||||
uri -> Nullable<Text>,
|
uri -> Nullable<Text>,
|
||||||
@@ -12,17 +10,22 @@ table! {
|
|||||||
duration -> Nullable<Integer>,
|
duration -> Nullable<Integer>,
|
||||||
guid -> Nullable<Text>,
|
guid -> Nullable<Text>,
|
||||||
played -> Nullable<Integer>,
|
played -> Nullable<Integer>,
|
||||||
show_id -> Integer,
|
favorite -> Bool,
|
||||||
|
archive -> Bool,
|
||||||
|
podcast_id -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
shows (id) {
|
podcast (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
title -> Text,
|
title -> Text,
|
||||||
link -> Text,
|
link -> Text,
|
||||||
description -> Text,
|
description -> Text,
|
||||||
image_uri -> Nullable<Text>,
|
image_uri -> Nullable<Text>,
|
||||||
|
favorite -> Bool,
|
||||||
|
archive -> Bool,
|
||||||
|
always_dl -> Bool,
|
||||||
source_id -> Integer,
|
source_id -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,4 +39,4 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(episodes, shows, source);
|
allow_tables_to_appear_in_same_query!(episode, podcast, source,);
|
||||||
@@ -7,7 +7,7 @@ use url::{Position, Url};
|
|||||||
|
|
||||||
use dbqueries;
|
use dbqueries;
|
||||||
use errors::DataError;
|
use errors::DataError;
|
||||||
use models::{EpisodeCleanerModel, Save, Show};
|
use models::{EpisodeCleanerQuery, Podcast, Save};
|
||||||
use xdg_dirs::DL_DIR;
|
use xdg_dirs::DL_DIR;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -25,7 +25,8 @@ fn download_checker() -> Result<(), DataError> {
|
|||||||
return Some(ep);
|
return Some(ep);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}).for_each(|ep| {
|
})
|
||||||
|
.for_each(|ep| {
|
||||||
ep.set_local_uri(None);
|
ep.set_local_uri(None);
|
||||||
ep.save()
|
ep.save()
|
||||||
.map_err(|err| error!("{}", err))
|
.map_err(|err| error!("{}", err))
|
||||||
@@ -58,7 +59,7 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check `ep.local_uri` field and delete the file it points to.
|
/// Check `ep.local_uri` field and delete the file it points to.
|
||||||
fn delete_local_content(ep: &mut EpisodeCleanerModel) -> Result<(), DataError> {
|
fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<(), DataError> {
|
||||||
if ep.local_uri().is_some() {
|
if ep.local_uri().is_some() {
|
||||||
let uri = ep.local_uri().unwrap().to_owned();
|
let uri = ep.local_uri().unwrap().to_owned();
|
||||||
if Path::new(&uri).exists() {
|
if Path::new(&uri).exists() {
|
||||||
@@ -107,9 +108,9 @@ pub fn url_cleaner(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URI of a Show Downloads given it's title.
|
/// Returns the URI of a Podcast Downloads given it's title.
|
||||||
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
|
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
|
||||||
// It might be better to make it a hash of the title or the Show rowid
|
// It might be better to make it a hash of the title or the podcast rowid
|
||||||
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
|
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
|
||||||
|
|
||||||
// Create the folder
|
// Create the folder
|
||||||
@@ -122,7 +123,7 @@ pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
|
|||||||
/// Removes all the entries associated with the given show from the database,
|
/// Removes all the entries associated with the given show from the database,
|
||||||
/// and deletes all of the downloaded content.
|
/// and deletes all of the downloaded content.
|
||||||
// TODO: Write Tests
|
// TODO: Write Tests
|
||||||
pub fn delete_show(pd: &Show) -> Result<(), DataError> {
|
pub fn delete_show(pd: &Podcast) -> Result<(), DataError> {
|
||||||
dbqueries::remove_feed(pd)?;
|
dbqueries::remove_feed(pd)?;
|
||||||
info!("{} was removed succesfully.", pd.title());
|
info!("{} was removed succesfully.", pd.title());
|
||||||
|
|
||||||
@@ -162,7 +163,6 @@ mod tests {
|
|||||||
use self::tempdir::TempDir;
|
use self::tempdir::TempDir;
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use failure::Error;
|
|
||||||
|
|
||||||
use database::truncate_db;
|
use database::truncate_db;
|
||||||
use models::NewEpisodeBuilder;
|
use models::NewEpisodeBuilder;
|
||||||
@@ -170,47 +170,49 @@ mod tests {
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
fn helper_db() -> Result<TempDir, Error> {
|
fn helper_db() -> TempDir {
|
||||||
// Clean the db
|
// Clean the db
|
||||||
truncate_db()?;
|
truncate_db().unwrap();
|
||||||
// Setup tmp file stuff
|
// Setup tmp file stuff
|
||||||
let tmp_dir = TempDir::new("podcasts_test")?;
|
let tmp_dir = TempDir::new("hammond_test").unwrap();
|
||||||
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
|
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
|
||||||
let bad_path = tmp_dir.path().join("invalid_thing.mp3");
|
let bad_path = tmp_dir.path().join("invalid_thing.mp3");
|
||||||
let mut tmp_file = File::create(&valid_path)?;
|
let mut tmp_file = File::create(&valid_path).unwrap();
|
||||||
writeln!(tmp_file, "Foooo")?;
|
writeln!(tmp_file, "Foooo").unwrap();
|
||||||
|
|
||||||
// Setup episodes
|
// Setup episodes
|
||||||
let n1 = NewEpisodeBuilder::default()
|
let n1 = NewEpisodeBuilder::default()
|
||||||
.title("foo_bar".to_string())
|
.title("foo_bar".to_string())
|
||||||
.show_id(0)
|
.podcast_id(0)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_episode()?;
|
.to_episode()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let n2 = NewEpisodeBuilder::default()
|
let n2 = NewEpisodeBuilder::default()
|
||||||
.title("bar_baz".to_string())
|
.title("bar_baz".to_string())
|
||||||
.show_id(1)
|
.podcast_id(1)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_episode()?;
|
.to_episode()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut ep1 = dbqueries::get_episode_cleaner_from_pk(n1.title(), n1.show_id())?;
|
let mut ep1 = dbqueries::get_episode_from_pk(n1.title(), n1.podcast_id()).unwrap();
|
||||||
let mut ep2 = dbqueries::get_episode_cleaner_from_pk(n2.title(), n2.show_id())?;
|
let mut ep2 = dbqueries::get_episode_from_pk(n2.title(), n2.podcast_id()).unwrap();
|
||||||
ep1.set_local_uri(Some(valid_path.to_str().unwrap()));
|
ep1.set_local_uri(Some(valid_path.to_str().unwrap()));
|
||||||
ep2.set_local_uri(Some(bad_path.to_str().unwrap()));
|
ep2.set_local_uri(Some(bad_path.to_str().unwrap()));
|
||||||
|
|
||||||
ep1.save()?;
|
ep1.save().unwrap();
|
||||||
ep2.save()?;
|
ep2.save().unwrap();
|
||||||
|
|
||||||
Ok(tmp_dir)
|
tmp_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_checker() -> Result<(), Error> {
|
fn test_download_checker() {
|
||||||
let tmp_dir = helper_db()?;
|
let tmp_dir = helper_db();
|
||||||
download_checker()?;
|
download_checker().unwrap();
|
||||||
let episodes = dbqueries::get_downloaded_episodes()?;
|
let episodes = dbqueries::get_downloaded_episodes().unwrap();
|
||||||
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
|
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
|
||||||
|
|
||||||
assert_eq!(episodes.len(), 1);
|
assert_eq!(episodes.len(), 1);
|
||||||
@@ -219,75 +221,69 @@ mod tests {
|
|||||||
episodes.first().unwrap().local_uri()
|
episodes.first().unwrap().local_uri()
|
||||||
);
|
);
|
||||||
|
|
||||||
let _tmp_dir = helper_db()?;
|
let _tmp_dir = helper_db();
|
||||||
download_checker()?;
|
download_checker().unwrap();
|
||||||
let episode = dbqueries::get_episode_cleaner_from_pk("bar_baz", 1)?;
|
let episode = dbqueries::get_episode_from_pk("bar_baz", 1).unwrap();
|
||||||
assert!(episode.local_uri().is_none());
|
assert!(episode.local_uri().is_none());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_cleaner() -> Result<(), Error> {
|
fn test_download_cleaner() {
|
||||||
let _tmp_dir = helper_db()?;
|
let _tmp_dir = helper_db();
|
||||||
let mut episode: EpisodeCleanerModel =
|
let mut episode: EpisodeCleanerQuery =
|
||||||
dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?.into();
|
dbqueries::get_episode_from_pk("foo_bar", 0).unwrap().into();
|
||||||
|
|
||||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||||
delete_local_content(&mut episode)?;
|
delete_local_content(&mut episode).unwrap();
|
||||||
assert_eq!(Path::new(&valid_path).exists(), false);
|
assert_eq!(Path::new(&valid_path).exists(), false);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_played_cleaner_expired() -> Result<(), Error> {
|
fn test_played_cleaner_expired() {
|
||||||
let _tmp_dir = helper_db()?;
|
let _tmp_dir = helper_db();
|
||||||
let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?;
|
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
|
||||||
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
||||||
let epoch = cleanup_date.timestamp() as i32 - 1;
|
let epoch = cleanup_date.timestamp() as i32 - 1;
|
||||||
episode.set_played(Some(epoch));
|
episode.set_played(Some(epoch));
|
||||||
episode.save()?;
|
episode.save().unwrap();
|
||||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||||
|
|
||||||
// This should delete the file
|
// This should delete the file
|
||||||
played_cleaner(cleanup_date)?;
|
played_cleaner(cleanup_date).unwrap();
|
||||||
assert_eq!(Path::new(&valid_path).exists(), false);
|
assert_eq!(Path::new(&valid_path).exists(), false);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_played_cleaner_none() -> Result<(), Error> {
|
fn test_played_cleaner_none() {
|
||||||
let _tmp_dir = helper_db()?;
|
let _tmp_dir = helper_db();
|
||||||
let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?;
|
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
|
||||||
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
||||||
let epoch = cleanup_date.timestamp() as i32 + 1;
|
let epoch = cleanup_date.timestamp() as i32 + 1;
|
||||||
episode.set_played(Some(epoch));
|
episode.set_played(Some(epoch));
|
||||||
episode.save()?;
|
episode.save().unwrap();
|
||||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||||
|
|
||||||
// This should not delete the file
|
// This should not delete the file
|
||||||
played_cleaner(cleanup_date)?;
|
played_cleaner(cleanup_date).unwrap();
|
||||||
assert_eq!(Path::new(&valid_path).exists(), true);
|
assert_eq!(Path::new(&valid_path).exists(), true);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_url_cleaner() -> Result<(), Error> {
|
fn test_url_cleaner() {
|
||||||
let good_url = "http://traffic.megaphone.fm/FL8608731318.mp3";
|
let good_url = "http://traffic.megaphone.fm/FL8608731318.mp3";
|
||||||
let bad_url = "http://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184";
|
let bad_url = "http://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184";
|
||||||
|
|
||||||
assert_eq!(url_cleaner(bad_url), good_url);
|
assert_eq!(url_cleaner(bad_url), good_url);
|
||||||
assert_eq!(url_cleaner(good_url), good_url);
|
assert_eq!(url_cleaner(good_url), good_url);
|
||||||
assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url);
|
assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// This test needs access to local system so we ignore it by default.
|
// This test needs access to local system so we ignore it by default.
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_get_dl_folder() -> Result<(), Error> {
|
fn test_get_dl_folder() {
|
||||||
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
|
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
|
||||||
assert_eq!(get_download_folder("foo")?, foo_);
|
assert_eq!(get_download_folder("foo").unwrap(), foo_);
|
||||||
let _ = fs::remove_dir_all(foo_);
|
let _ = fs::remove_dir_all(foo_);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
||||||
name = "podcasts-downloader"
|
name = "hammond-downloader"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
workspace = "../"
|
workspace = "../"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
error-chain = "0.12.0"
|
error-chain = "0.11.0"
|
||||||
hyper = "0.11.27"
|
hyper = "0.11.27"
|
||||||
log = "0.4.4"
|
log = "0.4.1"
|
||||||
mime_guess = "1.8.6"
|
mime_guess = "1.8.4"
|
||||||
reqwest = "0.8.8"
|
reqwest = "0.8.5"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
glob = "0.2.11"
|
glob = "0.2.11"
|
||||||
failure = "0.1.2"
|
failure = "0.1.1"
|
||||||
failure_derive = "0.1.2"
|
failure_derive = "0.1.1"
|
||||||
|
|
||||||
[dependencies.podcasts-data]
|
[dependencies.hammond-data]
|
||||||
path = "../podcasts-data"
|
path = "../hammond-data"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "0.5.1"
|
pretty_assertions = "0.5.1"
|
||||||
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use podcasts_data::xdg_dirs::PODCASTS_CACHE;
|
use hammond_data::xdg_dirs::HAMMOND_CACHE;
|
||||||
use podcasts_data::{EpisodeWidgetModel, Save, ShowCoverModel};
|
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
|
||||||
|
|
||||||
// use failure::Error;
|
// use failure::Error;
|
||||||
use errors::DownloadError;
|
use errors::DownloadError;
|
||||||
@@ -38,7 +38,7 @@ fn download_into(
|
|||||||
dir: &str,
|
dir: &str,
|
||||||
file_title: &str,
|
file_title: &str,
|
||||||
url: &str,
|
url: &str,
|
||||||
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
|
progress: Option<Arc<Mutex<DownloadProgress>>>,
|
||||||
) -> Result<String, DownloadError> {
|
) -> Result<String, DownloadError> {
|
||||||
info!("GET request to: {}", url);
|
info!("GET request to: {}", url);
|
||||||
// Haven't included the loop check as
|
// Haven't included the loop check as
|
||||||
@@ -78,7 +78,7 @@ fn download_into(
|
|||||||
// Construct a temp file to save desired content.
|
// Construct a temp file to save desired content.
|
||||||
// It has to be a `new_in` instead of new cause rename can't move cross
|
// It has to be a `new_in` instead of new cause rename can't move cross
|
||||||
// filesystems.
|
// filesystems.
|
||||||
let tempdir = TempDir::new_in(PODCASTS_CACHE.to_str().unwrap(), "temp_download")?;
|
let tempdir = TempDir::new_in(HAMMOND_CACHE.to_str().unwrap(), "temp_download")?;
|
||||||
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
|
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
|
||||||
|
|
||||||
ct_len.map(|x| {
|
ct_len.map(|x| {
|
||||||
@@ -122,12 +122,12 @@ fn save_io(
|
|||||||
file: &str,
|
file: &str,
|
||||||
resp: &mut reqwest::Response,
|
resp: &mut reqwest::Response,
|
||||||
content_lenght: Option<u64>,
|
content_lenght: Option<u64>,
|
||||||
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
|
progress: Option<Arc<Mutex<DownloadProgress>>>,
|
||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
info!("Downloading into: {}", file);
|
info!("Downloading into: {}", file);
|
||||||
let chunk_size = match content_lenght {
|
let chunk_size = match content_lenght {
|
||||||
Some(x) => x as usize / 99,
|
Some(x) => x as usize / 99,
|
||||||
None => 1024, // default chunk size
|
None => 1024 as usize, // default chunk size
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut writer = BufWriter::new(File::create(&file)?);
|
let mut writer = BufWriter::new(File::create(&file)?);
|
||||||
@@ -161,9 +161,9 @@ fn save_io(
|
|||||||
|
|
||||||
// TODO: Refactor
|
// TODO: Refactor
|
||||||
pub fn get_episode(
|
pub fn get_episode(
|
||||||
ep: &mut EpisodeWidgetModel,
|
ep: &mut EpisodeWidgetQuery,
|
||||||
download_folder: &str,
|
download_folder: &str,
|
||||||
progress: Option<Arc<Mutex<dyn DownloadProgress>>>,
|
progress: Option<Arc<Mutex<DownloadProgress>>>,
|
||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
// Check if its alrdy downloaded
|
// Check if its alrdy downloaded
|
||||||
if ep.local_uri().is_some() {
|
if ep.local_uri().is_some() {
|
||||||
@@ -196,7 +196,7 @@ pub fn get_episode(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache_image(pd: &ShowCoverModel) -> Result<String, DownloadError> {
|
pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
|
||||||
let url = pd
|
let url = pd
|
||||||
.image_uri()
|
.image_uri()
|
||||||
.ok_or_else(|| DownloadError::NoImageLocation)?
|
.ok_or_else(|| DownloadError::NoImageLocation)?
|
||||||
@@ -206,7 +206,7 @@ pub fn cache_image(pd: &ShowCoverModel) -> Result<String, DownloadError> {
|
|||||||
return Err(DownloadError::NoImageLocation);
|
return Err(DownloadError::NoImageLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache_path = PODCASTS_CACHE
|
let cache_path = HAMMOND_CACHE
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or_else(|| DownloadError::InvalidCacheLocation)?;
|
.ok_or_else(|| DownloadError::InvalidCacheLocation)?;
|
||||||
let cache_download_fold = format!("{}{}", cache_path, pd.title().to_owned());
|
let cache_download_fold = format!("{}{}", cache_path, pd.title().to_owned());
|
||||||
@@ -236,37 +236,35 @@ pub fn cache_image(pd: &ShowCoverModel) -> Result<String, DownloadError> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use failure::Error;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::pipeline;
|
||||||
use podcasts_data::pipeline;
|
use hammond_data::Source;
|
||||||
use podcasts_data::Source;
|
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// This test inserts an rss feed to your `XDG_DATA/podcasts/podcasts.db` so we make it explicit
|
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
|
||||||
// to run it.
|
// to run it.
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_cache_image() -> Result<(), Error> {
|
fn test_cache_image() {
|
||||||
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||||
// Create and index a source
|
// Create and index a source
|
||||||
let source = Source::from_url(url)?;
|
let source = Source::from_url(url).unwrap();
|
||||||
// Copy it's id
|
// Copy it's id
|
||||||
let sid = source.id();
|
let sid = source.id();
|
||||||
// Convert Source it into a future Feed and index it
|
// Convert Source it into a future Feed and index it
|
||||||
pipeline::run(vec![source])?;
|
pipeline::run(vec![source], true).unwrap();
|
||||||
|
|
||||||
// Get the Podcast
|
// Get the Podcast
|
||||||
let pd = dbqueries::get_podcast_from_source_id(sid)?.into();
|
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap().into();
|
||||||
|
|
||||||
let img_path = cache_image(&pd);
|
let img_path = cache_image(&pd);
|
||||||
let foo_ = format!(
|
let foo_ = format!(
|
||||||
"{}{}/cover.jpeg",
|
"{}{}/cover.jpeg",
|
||||||
PODCASTS_CACHE.to_str().unwrap(),
|
HAMMOND_CACHE.to_str().unwrap(),
|
||||||
pd.title()
|
pd.title()
|
||||||
);
|
);
|
||||||
assert_eq!(img_path?, foo_);
|
assert_eq!(img_path.unwrap(), foo_);
|
||||||
fs::remove_file(foo_)?;
|
fs::remove_file(foo_).unwrap();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
#![allow(bare_trait_objects)]
|
use hammond_data::errors::DataError;
|
||||||
|
|
||||||
use podcasts_data::errors::DataError;
|
|
||||||
use reqwest;
|
use reqwest;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#![recursion_limit = "1024"]
|
||||||
|
#![warn(unused_extern_crates, unused)]
|
||||||
|
#![allow(unknown_lints)]
|
||||||
|
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))]
|
||||||
|
#![deny(warnings)]
|
||||||
|
|
||||||
|
extern crate failure;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate pretty_assertions;
|
||||||
|
|
||||||
|
extern crate glob;
|
||||||
|
extern crate hammond_data;
|
||||||
|
extern crate hyper;
|
||||||
|
extern crate mime_guess;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate tempdir;
|
||||||
|
|
||||||
|
pub mod downloader;
|
||||||
|
pub mod errors;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
|
||||||
|
build = "build.rs"
|
||||||
|
name = "hammond-gtk"
|
||||||
|
version = "0.1.0"
|
||||||
|
workspace = "../"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.2"
|
||||||
|
crossbeam-channel = "0.1.2"
|
||||||
|
gdk = "0.8.0"
|
||||||
|
gdk-pixbuf = "0.4.0"
|
||||||
|
glib = "0.5.0"
|
||||||
|
humansize = "1.1.0"
|
||||||
|
lazy_static = "1.0.0"
|
||||||
|
log = "0.4.1"
|
||||||
|
loggerv = "0.7.1"
|
||||||
|
open = "1.2.1"
|
||||||
|
rayon = "1.0.1"
|
||||||
|
send-cell = "0.1.3"
|
||||||
|
url = "1.7.0"
|
||||||
|
failure = "0.1.1"
|
||||||
|
failure_derive = "0.1.1"
|
||||||
|
regex = "1.0.0"
|
||||||
|
reqwest = "0.8.5"
|
||||||
|
serde_json = "1.0.17"
|
||||||
|
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
|
||||||
|
|
||||||
|
[dependencies.gtk]
|
||||||
|
features = ["v3_22"]
|
||||||
|
version = "0.4.1"
|
||||||
|
|
||||||
|
[dependencies.gio]
|
||||||
|
features = ["v2_50"]
|
||||||
|
version = "0.4.0"
|
||||||
|
|
||||||
|
[dependencies.hammond-data]
|
||||||
|
path = "../hammond-data"
|
||||||
|
|
||||||
|
[dependencies.hammond-downloader]
|
||||||
|
path = "../hammond-downloader"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "0.5.1"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Command::new("glib-compile-resources")
|
||||||
|
.args(&["--generate", "resources.xml"])
|
||||||
|
.current_dir("resources")
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.22.0 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<object class="GtkBox" id="empty_show">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="pixel_size">128</property>
|
||||||
|
<property name="icon_name">application-rss+xml-symbolic</property>
|
||||||
|
<property name="use_fallback">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">This show does not have any episodes</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.4399999999999999"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">If you think this is an Error, Plese consider opening a bug report.</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.21.0
|
||||||
|
|
||||||
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
|
This file is part of Hammond.
|
||||||
|
|
||||||
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Hammond is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Jordan Petridis
|
||||||
|
Tobias Bernard
|
||||||
|
|
||||||
|
-->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<!-- interface-license-type gplv3 -->
|
||||||
|
<!-- interface-name Hammond -->
|
||||||
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
|
<object class="GtkBox" id="empty_view">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="pixel_size">128</property>
|
||||||
|
<property name="icon_name">application-rss+xml-symbolic</property>
|
||||||
|
<property name="use_fallback">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">No Feed Subscription Found</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.4399999999999999"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">You can subscribe to feeds using the "+" button</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
+61
-42
@@ -3,20 +3,20 @@
|
|||||||
|
|
||||||
Copyright (C) 2017 - 2018
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
This file is part of GNOME Podcasts.
|
This file is part of Hammond.
|
||||||
|
|
||||||
GNOME Podcasts is free software: you can redistribute it and/or modify
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
GNOME Podcasts is distributed in the hope that it will be useful,
|
Hammond is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with GNOME Podcasts. If not, see <http://www.gnu.org/licenses/>.
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Authors:
|
Authors:
|
||||||
Jordan Petridis
|
Jordan Petridis
|
||||||
@@ -24,9 +24,9 @@ Tobias Bernard
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.22"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<!-- interface-license-type gplv3 -->
|
<!-- interface-license-type gplv3 -->
|
||||||
<!-- interface-name GNOME Podcasts -->
|
<!-- interface-name Hammond -->
|
||||||
<!-- interface-description A podcast client for the GNOME Desktop -->
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
<!-- interface-copyright 2017 - 2018 -->
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
@@ -35,8 +35,6 @@ Tobias Bernard
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="margin_start">6</property>
|
|
||||||
<property name="margin_end">6</property>
|
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
@@ -52,8 +50,9 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="title_label">
|
<object class="GtkLabel" id="title_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="label">Episode Title</property>
|
<property name="label" translatable="yes">Episode Title</property>
|
||||||
<property name="ellipsize">end</property>
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="width_chars">55</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<property name="lines">1</property>
|
<property name="lines">1</property>
|
||||||
@@ -74,7 +73,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="date_label">
|
<object class="GtkLabel" id="date_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="label">3 Jan</property>
|
<property name="label" translatable="yes">3 Jan</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@@ -91,7 +90,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="separator1">
|
<object class="GtkLabel" id="separator1">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label">·</property>
|
<property name="label" translatable="yes">·</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="dim-label"/>
|
<class name="dim-label"/>
|
||||||
@@ -107,7 +106,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="duration_label">
|
<object class="GtkLabel" id="duration_label">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label">42 min</property>
|
<property name="label" translatable="yes">42 min</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@@ -124,7 +123,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="separator2">
|
<object class="GtkLabel" id="separator2">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label">·</property>
|
<property name="label" translatable="yes">·</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="dim-label"/>
|
<class name="dim-label"/>
|
||||||
@@ -140,8 +139,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="local_size">
|
<object class="GtkLabel" id="local_size">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label">0 MB</property>
|
<property name="label" translatable="yes">0 MB</property>
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@@ -158,7 +156,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="prog_separator">
|
<object class="GtkLabel" id="prog_separator">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label">/</property>
|
<property name="label" translatable="yes">/</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@@ -175,8 +173,7 @@ Tobias Bernard
|
|||||||
<object class="GtkLabel" id="total_size">
|
<object class="GtkLabel" id="total_size">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="label" translatable="yes">Calculating episode size…</property>
|
<property name="label" translatable="yes">XX MB</property>
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@@ -210,41 +207,24 @@ Tobias Bernard
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="layout_style">center</property>
|
<property name="layout_style">center</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="cancel_button">
|
<object class="GtkButton" id="desc_button">
|
||||||
<property name="label" translatable="yes">Cancel</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="halign">center</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="pack_type">end</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="download_button">
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="tooltip_text" translatable="yes">Download this episode</property>
|
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="always_show_image">True</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage">
|
<object class="GtkImage">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="icon_name">document-save-symbolic</property>
|
<property name="icon_name">view-paged-symbolic</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">True</property>
|
||||||
<property name="fill">False</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">1</property>
|
<property name="position">0</property>
|
||||||
<property name="non_homogeneous">True</property>
|
<property name="non_homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
@@ -271,6 +251,45 @@ Tobias Bernard
|
|||||||
<property name="non_homogeneous">True</property>
|
<property name="non_homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="download_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="tooltip_text" translatable="yes">Download this episode</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="icon_name">document-save-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
<property name="non_homogeneous">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="cancel_button">
|
||||||
|
<property name="label" translatable="yes">Cancel</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="pack_type">end</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
@@ -290,8 +309,8 @@ Tobias Bernard
|
|||||||
<object class="GtkProgressBar" id="progress_bar">
|
<object class="GtkProgressBar" id="progress_bar">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="no_show_all">True</property>
|
<property name="no_show_all">True</property>
|
||||||
<property name="margin_start">6</property>
|
<property name="margin_left">6</property>
|
||||||
<property name="margin_end">6</property>
|
<property name="margin_right">6</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="pulse_step">0</property>
|
<property name="pulse_step">0</property>
|
||||||
</object>
|
</object>
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.22.0
|
||||||
|
|
||||||
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
|
This file is part of Hammond.
|
||||||
|
|
||||||
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Hammond is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Jordan Petridis
|
||||||
|
Tobias Bernard
|
||||||
|
|
||||||
|
-->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<!-- interface-license-type gplv3 -->
|
||||||
|
<!-- interface-name Hammond -->
|
||||||
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
|
<object class="GtkBox" id="container">
|
||||||
|
<property name="name">container</property>
|
||||||
|
<property name="width_request">400</property>
|
||||||
|
<property name="height_request">600</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||||
|
<property name="name">scrolled_window</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="hscrollbar_policy">never</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="frame_parent">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_left">32</property>
|
||||||
|
<property name="margin_right">32</property>
|
||||||
|
<property name="margin_top">32</property>
|
||||||
|
<property name="margin_bottom">32</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">24</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="today_box">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label" translatable="yes">Today</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="today_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="yday_box">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Yesterday</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="yday_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="week_box">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">This Week</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="week_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="month_box">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">This Month</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="month_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="rest_box">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label" translatable="yes">Older</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="rest_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">5</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
+8
-7
@@ -3,20 +3,20 @@
|
|||||||
|
|
||||||
Copyright (C) 2017 - 2018
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
This file is part of GNOME Podcasts.
|
This file is part of Hammond.
|
||||||
|
|
||||||
GNOME Podcasts is free software: you can redistribute it and/or modify
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
GNOME Podcasts is distributed in the hope that it will be useful,
|
Hammond is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with GNOME Podcasts. If not, see <http://www.gnu.org/licenses/>.
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Authors:
|
Authors:
|
||||||
Jordan Petridis
|
Jordan Petridis
|
||||||
@@ -24,9 +24,9 @@ Tobias Bernard
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.22"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<!-- interface-license-type gplv3 -->
|
<!-- interface-license-type gplv3 -->
|
||||||
<!-- interface-name GNOME Podcasts -->
|
<!-- interface-name Hammond -->
|
||||||
<!-- interface-description A podcast client for the GNOME Desktop -->
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
<!-- interface-copyright 2017 - 2018 -->
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
@@ -36,11 +36,12 @@ Tobias Bernard
|
|||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="margin_top">6</property>
|
<property name="margin_top">6</property>
|
||||||
<property name="margin_bottom">6</property>
|
<property name="margin_bottom">6</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage" id="cover">
|
<object class="GtkImage" id="cover">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="margin_start">8</property>
|
<property name="margin_left">6</property>
|
||||||
<property name="pixel_size">64</property>
|
<property name="pixel_size">64</property>
|
||||||
<property name="icon_name">image-x-generic-symbolic</property>
|
<property name="icon_name">image-x-generic-symbolic</property>
|
||||||
</object>
|
</object>
|
||||||
@@ -3,20 +3,20 @@
|
|||||||
|
|
||||||
Copyright (C) 2017 - 2018
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
This file is part of GNOEM Podcasts.
|
This file is part of Hammond.
|
||||||
|
|
||||||
GNOEM Podcasts is free software: you can redistribute it and/or modify
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
GNOEM Podcasts is distributed in the hope that it will be useful,
|
Hammond is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with GNOEM Podcasts. If not, see <http://www.gnu.org/licenses/>.
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Authors:
|
Authors:
|
||||||
Jordan Petridis
|
Jordan Petridis
|
||||||
@@ -24,9 +24,9 @@ Tobias Bernard
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.22"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<!-- interface-license-type gplv3 -->
|
<!-- interface-license-type gplv3 -->
|
||||||
<!-- interface-name GNOEM Podcasts -->
|
<!-- interface-name Hammond -->
|
||||||
<!-- interface-description A podcast client for the GNOME Desktop -->
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
<!-- interface-copyright 2017 - 2018 -->
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
@@ -41,7 +41,10 @@ Tobias Bernard
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="border_width">6</property>
|
<property name="margin_left">6</property>
|
||||||
|
<property name="margin_right">6</property>
|
||||||
|
<property name="margin_top">6</property>
|
||||||
|
<property name="margin_bottom">6</property>
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
@@ -144,51 +147,13 @@ Tobias Bernard
|
|||||||
<object class="GtkHeaderBar" id="headerbar">
|
<object class="GtkHeaderBar" id="headerbar">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="show_close_button">True</property>
|
<property name="show_close_button">True</property>
|
||||||
<child type="title">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkStackSwitcher" id="switch">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="halign">center</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="show_title">
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="no_show_all">True</property>
|
|
||||||
<property name="label" translatable="yes">Show Title</property>
|
|
||||||
<property name="ellipsize">end</property>
|
|
||||||
<property name="track_visited_links">False</property>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="weight" value="bold"/>
|
|
||||||
</attributes>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkMenuButton" id="add_toggle">
|
<object class="GtkMenuButton" id="add_toggle">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">False</property>
|
||||||
<property name="tooltip_text" translatable="yes">Add a new feed</property>
|
<property name="tooltip_text" translatable="yes">Add a new feed</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="popover">add_popover</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage" id="add-button-image2">
|
<object class="GtkImage" id="add-button-image2">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -226,11 +191,78 @@ Tobias Bernard
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkMenuButton" id="hamburger">
|
<object class="GtkBox" id="update_notification">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSpinner" id="update_spinner">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="padding">6</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="update_label">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">Fetching new episodes</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child type="title">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkStackSwitcher" id="switch">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="show_title">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="label" translatable="yes">Show Title</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuButton" id="menu_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">False</property>
|
<property name="receives_default">False</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
|
<property name="use_popover">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage">
|
<object class="GtkImage">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -248,28 +280,5 @@ Tobias Bernard
|
|||||||
<property name="position">2</property>
|
<property name="position">2</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkMenuButton" id="secondary_menu">
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="no_show_all">True</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="icon_name">view-more-symbolic</property>
|
|
||||||
<property name="icon_size">1</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<style>
|
|
||||||
<class name="image-button"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="pack_type">end</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
@@ -18,13 +18,6 @@
|
|||||||
<property name="title" translatable="yes" context="shortcut window">Check for new episodes</property>
|
<property name="title" translatable="yes" context="shortcut window">Check for new episodes</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkShortcutsShortcut">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="accelerator"><primary>e</property>
|
|
||||||
<property name="title" translatable="yes" context="shortcut window">Preferences</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkShortcutsShortcut">
|
<object class="GtkShortcutsShortcut">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
+14
-53
@@ -1,35 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated with glade 3.22.0
|
<!-- Generated with glade 3.21.0 -->
|
||||||
|
|
||||||
Copyright (C) 2017 - 2018
|
|
||||||
|
|
||||||
This file is part of GNOME Podcast.
|
|
||||||
|
|
||||||
GNOME Podcast is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
GNOME Podcast is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with GNOME Podcast. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Authors:
|
|
||||||
Jordan Petridis
|
|
||||||
Tobias Bernard
|
|
||||||
|
|
||||||
-->
|
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.22"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<!-- interface-license-type gplv3 -->
|
|
||||||
<!-- interface-name GNOME Podcast -->
|
|
||||||
<!-- interface-description A podcast client for the GNOME Desktop -->
|
|
||||||
<!-- interface-copyright 2017 - 2018 -->
|
|
||||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
|
||||||
<object class="GtkRevealer" id="revealer">
|
<object class="GtkRevealer" id="revealer">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
@@ -49,16 +21,22 @@ Tobias Bernard
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="margin_start">3</property>
|
<property name="margin_left">3</property>
|
||||||
<property name="margin_end">3</property>
|
<property name="margin_right">3</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkSpinner" id="spinner">
|
<object class="GtkLabel" id="text">
|
||||||
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="margin_left">12</property>
|
||||||
|
<property name="margin_right">12</property>
|
||||||
|
<property name="label" translatable="yes">An in-app action notification</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">False</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
@@ -89,27 +67,10 @@ Tobias Bernard
|
|||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="text">
|
|
||||||
<property name="width_request">150</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="halign">center</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
<property name="margin_start">12</property>
|
|
||||||
<property name="margin_end">12</property>
|
|
||||||
<property name="label" translatable="yes">An in-app action notification</property>
|
|
||||||
<property name="ellipsize">start</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="undo">
|
<object class="GtkButton" id="undo">
|
||||||
<property name="label" translatable="yes">Undo</property>
|
<property name="label" translatable="yes">Undo</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
@@ -122,7 +83,7 @@ Tobias Bernard
|
|||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">False</property>
|
<property name="fill">False</property>
|
||||||
<property name="pack_type">end</property>
|
<property name="pack_type">end</property>
|
||||||
<property name="position">3</property>
|
<property name="position">2</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<interface>
|
||||||
|
<!-- interface-requires gtk+ 3.0 -->
|
||||||
|
<menu id="menu">
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Check for new episodes</attribute>
|
||||||
|
<attribute name="action">app.refresh</attribute>
|
||||||
|
<attribute name="accel"><primary>r</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Import Shows</attribute>
|
||||||
|
<attribute name="action">app.import</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Export Shows</attribute>
|
||||||
|
<attribute name="action">app.export</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||||
|
<attribute name="action">app.preferences</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
|
||||||
|
<attribute name="action">win.show-help-overlay</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Help</attribute>
|
||||||
|
<attribute name="action">app.help</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_About</attribute>
|
||||||
|
<attribute name="action">app.about</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
</menu>
|
||||||
|
</interface>
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.22.0
|
||||||
|
|
||||||
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
|
This file is part of Hammond.
|
||||||
|
|
||||||
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Hammond is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Jordan Petridis
|
||||||
|
Tobias Bernard
|
||||||
|
|
||||||
|
-->
|
||||||
|
<interface domain="">
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<!-- interface-license-type gplv3 -->
|
||||||
|
<!-- interface-name Hammond -->
|
||||||
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
|
<object class="GtkBox" id="container">
|
||||||
|
<property name="width_request">400</property>
|
||||||
|
<property name="height_request">600</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||||
|
<property name="name">scrolled_window</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="hscrollbar_policy">never</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="can_default">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_left">32</property>
|
||||||
|
<property name="margin_right">32</property>
|
||||||
|
<property name="margin_top">32</property>
|
||||||
|
<property name="margin_bottom">32</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">24</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="cover">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="pixel_size">256</property>
|
||||||
|
<property name="icon_name">image-x-generic-symbolic</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="hscrollbar_policy">never</property>
|
||||||
|
<property name="min_content_height">80</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="shadow_type">none</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="description">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="label">This is embarrasing!
|
||||||
|
Sorry, we could not find a description for this Show.</property>
|
||||||
|
<property name="use_markup">True</property>
|
||||||
|
<property name="justify">center</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max_width_chars">70</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuButton" id="settings_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="icon_name">emblem-system-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="link_button">
|
||||||
|
<property name="label" translatable="yes">Website</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="padding">5</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="unsub_button">
|
||||||
|
<property name="label" translatable="yes">Unsubscribe</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<style>
|
||||||
|
<class name="destructive-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="padding">5</property>
|
||||||
|
<property name="pack_type">end</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="episodes">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="pack_type">end</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<object class="GtkPopover" id="show_menu">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="position">bottom</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_left">6</property>
|
||||||
|
<property name="margin_right">6</property>
|
||||||
|
<property name="margin_top">6</property>
|
||||||
|
<property name="margin_bottom">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkModelButton" id="mark_all_watched">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="text" translatable="yes">Mark all episodes as listened</property>
|
||||||
|
<property name="centered">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.22.0
|
||||||
|
|
||||||
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
|
This file is part of Hammond.
|
||||||
|
|
||||||
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Hammond is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Jordan Petridis
|
||||||
|
Tobias Bernard
|
||||||
|
|
||||||
|
-->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<!-- interface-license-type gplv3 -->
|
||||||
|
<!-- interface-name Hammond -->
|
||||||
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
|
<object class="GtkBox" id="fb_child">
|
||||||
|
<property name="width_request">256</property>
|
||||||
|
<property name="height_request">256</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="pd_cover">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="pixel_size">256</property>
|
||||||
|
<property name="icon_name">image-x-generic-symbolic</property>
|
||||||
|
<property name="icon_size">0</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.21.0
|
||||||
|
|
||||||
|
Copyright (C) 2017 - 2018
|
||||||
|
|
||||||
|
This file is part of Hammond.
|
||||||
|
|
||||||
|
Hammond is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Hammond is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Hammond. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Jordan Petridis
|
||||||
|
Tobias Bernard
|
||||||
|
|
||||||
|
-->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<!-- interface-license-type gplv3 -->
|
||||||
|
<!-- interface-name Hammond -->
|
||||||
|
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||||
|
<!-- interface-copyright 2017 - 2018 -->
|
||||||
|
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||||
|
<object class="GtkBox" id="fb_parent">
|
||||||
|
<property name="name">fb_parent</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||||
|
<property name="name">scrolled_window</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFlowBox" id="flowbox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="valign">start</property>
|
||||||
|
<property name="margin_top">24</property>
|
||||||
|
<property name="margin_bottom">24</property>
|
||||||
|
<property name="homogeneous">True</property>
|
||||||
|
<property name="column_spacing">12</property>
|
||||||
|
<property name="row_spacing">12</property>
|
||||||
|
<property name="max_children_per_line">20</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
row {
|
||||||
|
border-bottom: solid 1px rgba(0,0,0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
list, border {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# subdir('icons')
|
||||||
|
|
||||||
|
install_data('org.gnome.Hammond.desktop', install_dir : datadir + '/applications')
|
||||||
|
install_data('org.gnome.Hammond.appdata.xml', install_dir : datadir + '/appdata')
|
||||||
|
install_data('org.gnome.Hammond.gschema.xml', install_dir: join_paths(datadir, 'glib-2.0', 'schemas'))
|
||||||
|
meson.add_install_script('../../scripts/compile-gschema.py')
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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>Jordan Petridis</developer_name>
|
||||||
|
<summary>Modern Podcast Client for the GNOME desktop</summary>
|
||||||
|
<description xml:lang="en">
|
||||||
|
<p>
|
||||||
|
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
|
||||||
|
desktop written in Rust.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/episodes_view.png</image>
|
||||||
|
<caption>Page 1</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/shows_view.png</image>
|
||||||
|
<caption>Page 2</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/show_widget.png</image>
|
||||||
|
<caption>Page 3</caption>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<releases>
|
||||||
|
<release version="0.3.4" date="2018-05-20"/>
|
||||||
|
</releases>
|
||||||
|
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
|
||||||
|
<update_contact>jpetridis@gnome.org</update_contact>
|
||||||
|
<developer_name>Jordan Petridis and others</developer_name>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Hammond
|
||||||
|
GenericName=Podcast Client
|
||||||
|
Comment=Play, Subscribe and Manage Podcast Feeds.
|
||||||
|
Icon=multimedia-player
|
||||||
|
Exec=hammond
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=AudioVideo;Audio;Video;
|
||||||
|
Keywords=Podcast
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<schemalist>
|
<schemalist>
|
||||||
<enum id="org.gnome.Podcasts.timePeriods">
|
<enum id="org.gnome.Hammond.timePeriods">
|
||||||
<value nick="seconds" value="1"/>
|
<value nick="seconds" value="1"/>
|
||||||
<value nick="minutes" value="2"/>
|
<value nick="minutes" value="2"/>
|
||||||
<value nick="hours" value="3"/>
|
<value nick="hours" value="3"/>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<value nick="months" value="6"/>
|
<value nick="months" value="6"/>
|
||||||
</enum>
|
</enum>
|
||||||
|
|
||||||
<schema path="/org/gnome/Podcasts/" id="org.gnome.Podcasts" gettext-domain="gnome-podcasts">
|
<schema path="/org/gnome/hammond/" id="org.gnome.Hammond">
|
||||||
<key name="persist-window-geometry-top" type="i">
|
<key name="persist-window-geometry-top" type="i">
|
||||||
<default>-1</default>
|
<default>-1</default>
|
||||||
<summary>Top position of the last open main window</summary>
|
<summary>Top position of the last open main window</summary>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<default>1</default>
|
<default>1</default>
|
||||||
<summary>How many periods of time to wait between automatic refreshes</summary>
|
<summary>How many periods of time to wait between automatic refreshes</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="refresh-interval-period" enum="org.gnome.Podcasts.timePeriods">
|
<key name="refresh-interval-period" enum="org.gnome.Hammond.timePeriods">
|
||||||
<default>'hours'</default>
|
<default>'hours'</default>
|
||||||
<summary>What period of time to wait between automatic refreshes</summary>
|
<summary>What period of time to wait between automatic refreshes</summary>
|
||||||
</key>
|
</key>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<default>2</default>
|
<default>2</default>
|
||||||
<summary>How many periods of time to wait between automatic cleanups</summary>
|
<summary>How many periods of time to wait between automatic cleanups</summary>
|
||||||
</key>
|
</key>
|
||||||
<key name="cleanup-age-period" enum="org.gnome.Podcasts.timePeriods">
|
<key name="cleanup-age-period" enum="org.gnome.Hammond.timePeriods">
|
||||||
<default>'days'</default>
|
<default>'days'</default>
|
||||||
<summary>What period of time to wait between automatic cleanups</summary>
|
<summary>What period of time to wait between automatic cleanups</summary>
|
||||||
</key>
|
</key>
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/org/gnome/Podcasts/">
|
<gresource prefix="/org/gnome/Hammond/">
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/show_widget.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/show_widget.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_view.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_view.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/home_view.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_show.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/home_episode.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view_widget.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_view.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_child.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/headerbar.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/headerbar.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/hamburger.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/menus.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/show_menu.ui</file>
|
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/player_toolbar.ui</file>
|
|
||||||
<file compressed="true" preprocess="xml-stripblanks">icons/skip-back-symbolic.svg</file>
|
|
||||||
<file compressed="true" preprocess="xml-stripblanks">icons/skip-forward-symbolic.svg</file>
|
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/prefs.ui</file>
|
|
||||||
<file compressed="true">gtk/style.css</file>
|
<file compressed="true">gtk/style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
#![allow(new_without_default)]
|
||||||
|
|
||||||
|
use gio::{
|
||||||
|
ActionMapExt, ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt,
|
||||||
|
SimpleAction, SimpleActionExt,
|
||||||
|
};
|
||||||
|
use glib;
|
||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::SettingsExt as GtkSettingsExt;
|
||||||
|
|
||||||
|
use crossbeam_channel::{unbounded, Sender};
|
||||||
|
use hammond_data::{Podcast, dbqueries};
|
||||||
|
|
||||||
|
use headerbar::Header;
|
||||||
|
use settings::{self, WindowGeometry};
|
||||||
|
use stacks::{Content, PopulatedState};
|
||||||
|
use utils;
|
||||||
|
use widgets::appnotif::{InAppNotification, UndoState};
|
||||||
|
use widgets::{about_dialog, mark_all_notif, remove_show_notif};
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Creates an action named $called in the action map $on with the handler $handle
|
||||||
|
macro_rules! action {
|
||||||
|
($on:expr, $called:expr, $handle:expr) => {{
|
||||||
|
// Create a stateless, parameterless action
|
||||||
|
let act = SimpleAction::new($called, None);
|
||||||
|
// Connect the handler
|
||||||
|
act.connect_activate($handle);
|
||||||
|
// Add it to the map
|
||||||
|
$on.add_action(&act);
|
||||||
|
// Return the action
|
||||||
|
act
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Action {
|
||||||
|
RefreshAllViews,
|
||||||
|
RefreshEpisodesView,
|
||||||
|
RefreshEpisodesViewBGR,
|
||||||
|
RefreshShowsView,
|
||||||
|
ReplaceWidget(Arc<Podcast>),
|
||||||
|
RefreshWidgetIfSame(i32),
|
||||||
|
ShowWidgetAnimated,
|
||||||
|
ShowShowsAnimated,
|
||||||
|
HeaderBarShowTile(String),
|
||||||
|
HeaderBarNormal,
|
||||||
|
HeaderBarShowUpdateIndicator,
|
||||||
|
HeaderBarHideUpdateIndicator,
|
||||||
|
MarkAllPlayerNotification(Arc<Podcast>),
|
||||||
|
RemoveShow(Arc<Podcast>),
|
||||||
|
ErrorNotification(String),
|
||||||
|
AboutWidget(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct App {
|
||||||
|
app_instance: gtk::Application,
|
||||||
|
settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> App {
|
||||||
|
let settings = Settings::new("org.gnome.Hammond");
|
||||||
|
let application = gtk::Application::new("org.gnome.Hammond", ApplicationFlags::empty())
|
||||||
|
.expect("Application Initialization failed...");
|
||||||
|
|
||||||
|
// Weird magic I copy-pasted that sets the Application Name in the Shell.
|
||||||
|
glib::set_application_name("Hammond");
|
||||||
|
glib::set_prgname(Some("Hammond"));
|
||||||
|
|
||||||
|
let cleanup_date = settings::get_cleanup_date(&settings);
|
||||||
|
utils::cleanup(cleanup_date);
|
||||||
|
|
||||||
|
application.connect_startup(clone!(settings => move |app| {
|
||||||
|
let (sender, receiver) = unbounded();
|
||||||
|
|
||||||
|
App::setup_gactions(&app, &sender);
|
||||||
|
|
||||||
|
app.connect_activate(clone!(sender, settings => move |app| {
|
||||||
|
// Get the current window (if any)
|
||||||
|
if let Some(window) = app.get_active_window() {
|
||||||
|
// Already open, just raise the window
|
||||||
|
window.present();
|
||||||
|
} else {
|
||||||
|
// Time to open one!
|
||||||
|
// Create the main window
|
||||||
|
let window = gtk::ApplicationWindow::new(&app);
|
||||||
|
window.set_title("Hammond");
|
||||||
|
|
||||||
|
window.connect_delete_event(clone!(app, settings => move |window, _| {
|
||||||
|
WindowGeometry::from_window(&window).write(&settings);
|
||||||
|
app.quit();
|
||||||
|
Inhibit(false)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create a content instance
|
||||||
|
let content =
|
||||||
|
Rc::new(Content::new(sender.clone()).expect(
|
||||||
|
"Content Initialization failed."));
|
||||||
|
|
||||||
|
// Create the headerbar
|
||||||
|
let header = Rc::new(Header::new(&content, &window, &sender));
|
||||||
|
|
||||||
|
action!(window, "menu", clone!(header => move |_, _| {
|
||||||
|
header.open_menu();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add the content main stack to the overlay.
|
||||||
|
let overlay = gtk::Overlay::new();
|
||||||
|
overlay.add(&content.get_stack());
|
||||||
|
|
||||||
|
// Add the overlay to the main window
|
||||||
|
window.add(&overlay);
|
||||||
|
|
||||||
|
WindowGeometry::from_settings(&settings).apply(&window);
|
||||||
|
|
||||||
|
App::setup_timed_callbacks(&sender, &settings);
|
||||||
|
|
||||||
|
window.show_all();
|
||||||
|
window.activate();
|
||||||
|
|
||||||
|
gtk::timeout_add(50, clone!(sender, receiver => move || {
|
||||||
|
// Uses receiver, content, header, sender, overlay
|
||||||
|
match receiver.try_recv() {
|
||||||
|
Ok(Action::RefreshAllViews) => content.update(),
|
||||||
|
Ok(Action::RefreshShowsView) => content.update_shows_view(),
|
||||||
|
Ok(Action::RefreshWidgetIfSame(id)) =>
|
||||||
|
content.update_widget_if_same(id),
|
||||||
|
Ok(Action::RefreshEpisodesView) => content.update_home(),
|
||||||
|
Ok(Action::RefreshEpisodesViewBGR) =>
|
||||||
|
content.update_home_if_background(),
|
||||||
|
Ok(Action::ReplaceWidget(pd)) => {
|
||||||
|
let shows = content.get_shows();
|
||||||
|
let mut pop = shows.borrow().populated();
|
||||||
|
pop.borrow_mut()
|
||||||
|
.replace_widget(pd.clone())
|
||||||
|
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
|
||||||
|
.map_err(|_|
|
||||||
|
error!("Failed ot update ShowWidget {}", pd.title()))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Ok(Action::ShowWidgetAnimated) => {
|
||||||
|
let shows = content.get_shows();
|
||||||
|
let mut pop = shows.borrow().populated();
|
||||||
|
pop.borrow_mut().switch_visible(
|
||||||
|
PopulatedState::Widget,
|
||||||
|
gtk::StackTransitionType::SlideLeft,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Action::ShowShowsAnimated) => {
|
||||||
|
let shows = content.get_shows();
|
||||||
|
let mut pop = shows.borrow().populated();
|
||||||
|
pop.borrow_mut()
|
||||||
|
.switch_visible(PopulatedState::View,
|
||||||
|
gtk::StackTransitionType::SlideRight);
|
||||||
|
}
|
||||||
|
Ok(Action::HeaderBarShowTile(title)) =>
|
||||||
|
header.switch_to_back(&title),
|
||||||
|
Ok(Action::HeaderBarNormal) => header.switch_to_normal(),
|
||||||
|
Ok(Action::HeaderBarShowUpdateIndicator) =>
|
||||||
|
header.show_update_notification(),
|
||||||
|
Ok(Action::HeaderBarHideUpdateIndicator) =>
|
||||||
|
header.hide_update_notification(),
|
||||||
|
Ok(Action::MarkAllPlayerNotification(pd)) => {
|
||||||
|
let notif = mark_all_notif(pd, &sender);
|
||||||
|
notif.show(&overlay);
|
||||||
|
}
|
||||||
|
Ok(Action::RemoveShow(pd)) => {
|
||||||
|
let notif = remove_show_notif(pd, sender.clone());
|
||||||
|
notif.show(&overlay);
|
||||||
|
}
|
||||||
|
Ok(Action::ErrorNotification(err)) => {
|
||||||
|
error!("An error notification was triggered: {}", err);
|
||||||
|
let callback = || glib::Continue(false);
|
||||||
|
let notif = InAppNotification::new(&err, callback,
|
||||||
|
|| {}, UndoState::Hidden);
|
||||||
|
notif.show(&overlay);
|
||||||
|
}
|
||||||
|
Ok(Action::AboutWidget(rowid)) => {
|
||||||
|
if let Ok(d) = dbqueries::get_episode_description(rowid) {
|
||||||
|
if let Some(_desc) = d {
|
||||||
|
let _stack = content.get_visible_stack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
App {
|
||||||
|
app_instance: application,
|
||||||
|
settings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_timed_callbacks(sender: &Sender<Action>, settings: &Settings) {
|
||||||
|
App::setup_dark_theme(settings);
|
||||||
|
App::setup_refresh_on_startup(&sender, settings);
|
||||||
|
App::setup_auto_refresh(&sender, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_dark_theme(settings: &Settings) {
|
||||||
|
let gtk_settings = gtk::Settings::get_default().unwrap();
|
||||||
|
let enabled = settings.get_boolean("dark-theme");
|
||||||
|
|
||||||
|
gtk_settings.set_property_gtk_application_prefer_dark_theme(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_refresh_on_startup(sender: &Sender<Action>, settings: &Settings) {
|
||||||
|
// Update the feeds right after the Application is initialized.
|
||||||
|
let sender = sender.clone();
|
||||||
|
if settings.get_boolean("refresh-on-startup") {
|
||||||
|
info!("Refresh on startup.");
|
||||||
|
// The ui loads async, after initialization
|
||||||
|
// so we need to delay this a bit so it won't block
|
||||||
|
// requests that will come from loading the gui on startup.
|
||||||
|
gtk::timeout_add(1500, move || {
|
||||||
|
let s: Option<Vec<_>> = None;
|
||||||
|
utils::refresh(s, sender.clone());
|
||||||
|
glib::Continue(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_auto_refresh(sender: &Sender<Action>, settings: &Settings) {
|
||||||
|
let refresh_interval = settings::get_refresh_interval(&settings).num_seconds() as u32;
|
||||||
|
|
||||||
|
info!("Auto-refresh every {:?} seconds.", refresh_interval);
|
||||||
|
|
||||||
|
let sender = sender.clone();
|
||||||
|
gtk::timeout_add_seconds(refresh_interval, move || {
|
||||||
|
let s: Option<Vec<_>> = None;
|
||||||
|
utils::refresh(s, sender.clone());
|
||||||
|
|
||||||
|
glib::Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define the `GAction`s.
|
||||||
|
///
|
||||||
|
/// Used in menus and the keyboard shortcuts dialog.
|
||||||
|
fn setup_gactions(app: >k::Application, sender: &Sender<Action>) {
|
||||||
|
// Create the `refresh` action.
|
||||||
|
//
|
||||||
|
// This will trigger a refresh of all the shows in the database.
|
||||||
|
action!(
|
||||||
|
app,
|
||||||
|
"refresh",
|
||||||
|
clone!(sender => move |_, _| {
|
||||||
|
gtk::idle_add(clone!(sender => move || {
|
||||||
|
let s: Option<Vec<_>> = None;
|
||||||
|
utils::refresh(s, sender.clone());
|
||||||
|
glib::Continue(false)
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.set_accels_for_action("app.refresh", &["<primary>r"]);
|
||||||
|
|
||||||
|
// Create the `OPML` import action
|
||||||
|
action!(
|
||||||
|
app,
|
||||||
|
"import",
|
||||||
|
clone!(sender, app => move |_, _| {
|
||||||
|
let window = app.get_active_window().expect("Failed to get active window");
|
||||||
|
utils::on_import_clicked(&window, &sender);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the action that shows a `gtk::AboutDialog`
|
||||||
|
action!(
|
||||||
|
app,
|
||||||
|
"about",
|
||||||
|
clone!(app => move |_, _| {
|
||||||
|
let window = app.get_active_window().expect("Failed to get active window");
|
||||||
|
about_dialog(&window);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the quit action
|
||||||
|
action!(app, "quit", clone!(app => move |_, _| app.quit()));
|
||||||
|
app.set_accels_for_action("app.quit", &["<primary>q"]);
|
||||||
|
|
||||||
|
// Bind the hamburger menu button to `F10`
|
||||||
|
app.set_accels_for_action("win.menu", &["F10"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self) {
|
||||||
|
ApplicationExtManual::run(&self.app_instance, &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
use gio::MenuModel;
|
||||||
|
use glib;
|
||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
use failure::ResultExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use hammond_data::{dbqueries, Source};
|
||||||
|
|
||||||
|
use app::Action;
|
||||||
|
use stacks::Content;
|
||||||
|
use utils::{itunes_to_rss, refresh};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
// TODO: split this into smaller
|
||||||
|
pub struct Header {
|
||||||
|
container: gtk::HeaderBar,
|
||||||
|
add_toggle: gtk::MenuButton,
|
||||||
|
switch: gtk::StackSwitcher,
|
||||||
|
back: gtk::Button,
|
||||||
|
show_title: gtk::Label,
|
||||||
|
update_box: gtk::Box,
|
||||||
|
update_label: gtk::Label,
|
||||||
|
update_spinner: gtk::Spinner,
|
||||||
|
menu_button: gtk::MenuButton,
|
||||||
|
app_menu: MenuModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Header {
|
||||||
|
fn default() -> Header {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/headerbar.ui");
|
||||||
|
let menus = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/menus.ui");
|
||||||
|
|
||||||
|
let header = builder.get_object("headerbar").unwrap();
|
||||||
|
let add_toggle = builder.get_object("add_toggle").unwrap();
|
||||||
|
let switch = builder.get_object("switch").unwrap();
|
||||||
|
let back = builder.get_object("back").unwrap();
|
||||||
|
let show_title = builder.get_object("show_title").unwrap();
|
||||||
|
let update_box = builder.get_object("update_notification").unwrap();
|
||||||
|
let update_label = builder.get_object("update_label").unwrap();
|
||||||
|
let update_spinner = builder.get_object("update_spinner").unwrap();
|
||||||
|
let menu_button = builder.get_object("menu_button").unwrap();
|
||||||
|
let app_menu = menus.get_object("menu").unwrap();
|
||||||
|
|
||||||
|
Header {
|
||||||
|
container: header,
|
||||||
|
add_toggle,
|
||||||
|
switch,
|
||||||
|
back,
|
||||||
|
show_title,
|
||||||
|
update_box,
|
||||||
|
update_label,
|
||||||
|
update_spinner,
|
||||||
|
menu_button,
|
||||||
|
app_menu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor components into smaller state machines
|
||||||
|
impl Header {
|
||||||
|
pub fn new(
|
||||||
|
content: &Content,
|
||||||
|
window: >k::ApplicationWindow,
|
||||||
|
sender: &Sender<Action>,
|
||||||
|
) -> Header {
|
||||||
|
let h = Header::default();
|
||||||
|
h.init(content, window, &sender);
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
&self,
|
||||||
|
content: &Content,
|
||||||
|
window: >k::ApplicationWindow,
|
||||||
|
sender: &Sender<Action>,
|
||||||
|
) {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/headerbar.ui");
|
||||||
|
|
||||||
|
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
|
||||||
|
let new_url: gtk::Entry = builder.get_object("new_url").unwrap();
|
||||||
|
let add_button: gtk::Button = builder.get_object("add_button").unwrap();
|
||||||
|
let result_label: gtk::Label = builder.get_object("result_label").unwrap();
|
||||||
|
self.switch.set_stack(&content.get_stack());
|
||||||
|
|
||||||
|
new_url.connect_changed(clone!(add_button => move |url| {
|
||||||
|
on_url_change(url, &result_label, &add_button)
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
|
||||||
|
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
|
||||||
|
on_add_bttn_clicked(&new_url, sender.clone())
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.ok();
|
||||||
|
add_popover.hide();
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.add_toggle.set_popover(&add_popover);
|
||||||
|
|
||||||
|
// Add the Headerbar to the window.
|
||||||
|
window.set_titlebar(&self.container);
|
||||||
|
|
||||||
|
let switch = &self.switch;
|
||||||
|
let add_toggle = &self.add_toggle;
|
||||||
|
let show_title = &self.show_title;
|
||||||
|
self.back.connect_clicked(
|
||||||
|
clone!(switch, add_toggle, show_title, sender => move |back| {
|
||||||
|
switch.show();
|
||||||
|
add_toggle.show();
|
||||||
|
back.hide();
|
||||||
|
show_title.hide();
|
||||||
|
sender.send(Action::ShowShowsAnimated)
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.menu_button.set_menu_model(Some(&self.app_menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_to_back(&self, title: &str) {
|
||||||
|
self.switch.hide();
|
||||||
|
self.add_toggle.hide();
|
||||||
|
self.back.show();
|
||||||
|
self.set_show_title(title);
|
||||||
|
self.show_title.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_to_normal(&self) {
|
||||||
|
self.switch.show();
|
||||||
|
self.add_toggle.show();
|
||||||
|
self.back.hide();
|
||||||
|
self.show_title.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_title(&self, title: &str) {
|
||||||
|
self.show_title.set_text(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_update_notification(&self) {
|
||||||
|
self.update_spinner.start();
|
||||||
|
self.update_box.show();
|
||||||
|
self.update_spinner.show();
|
||||||
|
self.update_label.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide_update_notification(&self) {
|
||||||
|
self.update_spinner.stop();
|
||||||
|
self.update_box.hide();
|
||||||
|
self.update_spinner.hide();
|
||||||
|
self.update_label.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_menu(&self) {
|
||||||
|
self.menu_button.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: THIS ALSO SUCKS!
|
||||||
|
fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(), Error> {
|
||||||
|
let url = entry.get_text().unwrap_or_default();
|
||||||
|
let url = if url.contains("itunes.com") || url.contains("apple.com") {
|
||||||
|
info!("Detected itunes url.");
|
||||||
|
let foo = itunes_to_rss(&url)?;
|
||||||
|
info!("Resolved to {}", foo);
|
||||||
|
foo
|
||||||
|
} else {
|
||||||
|
url.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
|
||||||
|
entry.set_text("");
|
||||||
|
|
||||||
|
gtk::idle_add(move || {
|
||||||
|
refresh(Some(vec![source.clone()]), sender.clone());
|
||||||
|
glib::Continue(false)
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: THIS SUCKS!
|
||||||
|
fn on_url_change(
|
||||||
|
entry: >k::Entry,
|
||||||
|
result: >k::Label,
|
||||||
|
add_button: >k::Button,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let uri = entry
|
||||||
|
.get_text()
|
||||||
|
.ok_or_else(|| format_err!("GtkEntry blew up somehow."))?;
|
||||||
|
debug!("Url: {}", uri);
|
||||||
|
|
||||||
|
let url = Url::parse(&uri);
|
||||||
|
// TODO: refactor to avoid duplication
|
||||||
|
match url {
|
||||||
|
Ok(u) => {
|
||||||
|
if !dbqueries::source_exists(u.as_str())? {
|
||||||
|
add_button.set_sensitive(true);
|
||||||
|
result.hide();
|
||||||
|
result.set_label("");
|
||||||
|
} else {
|
||||||
|
add_button.set_sensitive(false);
|
||||||
|
result.set_label("Show already exists.");
|
||||||
|
result.show();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
add_button.set_sensitive(false);
|
||||||
|
if !uri.is_empty() {
|
||||||
|
result.set_label("Invalid url.");
|
||||||
|
result.show();
|
||||||
|
error!("Error: {}", err);
|
||||||
|
} else {
|
||||||
|
result.hide();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
feature = "cargo-clippy",
|
||||||
|
allow(clone_on_ref_ptr, blacklisted_name, match_same_arms, option_map_unit_fn)
|
||||||
|
)]
|
||||||
|
#![allow(unknown_lints)]
|
||||||
|
#![warn(unused_extern_crates, unused)]
|
||||||
|
#![deny(warnings)]
|
||||||
|
|
||||||
|
extern crate gdk;
|
||||||
|
extern crate gdk_pixbuf;
|
||||||
|
extern crate gio;
|
||||||
|
extern crate glib;
|
||||||
|
extern crate gtk;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure;
|
||||||
|
// #[macro_use]
|
||||||
|
// extern crate failure_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate pretty_assertions;
|
||||||
|
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate crossbeam_channel;
|
||||||
|
extern crate hammond_data;
|
||||||
|
extern crate hammond_downloader;
|
||||||
|
extern crate html2pango;
|
||||||
|
extern crate humansize;
|
||||||
|
extern crate loggerv;
|
||||||
|
extern crate open;
|
||||||
|
extern crate rayon;
|
||||||
|
extern crate regex;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate send_cell;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate url;
|
||||||
|
|
||||||
|
use log::Level;
|
||||||
|
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
// http://gtk-rs.org/tuto/closures
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! clone {
|
||||||
|
(@param _) => ( _ );
|
||||||
|
(@param $x:ident) => ( $x );
|
||||||
|
($($n:ident),+ => move || $body:expr) => (
|
||||||
|
{
|
||||||
|
$( let $n = $n.clone(); )+
|
||||||
|
move || $body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
|
||||||
|
{
|
||||||
|
$( let $n = $n.clone(); )+
|
||||||
|
move |$(clone!(@param $p),)+| $body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod stacks;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod headerbar;
|
||||||
|
|
||||||
|
mod manager;
|
||||||
|
mod settings;
|
||||||
|
mod static_resource;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// TODO: make the the logger a cli -vv option
|
||||||
|
loggerv::init_with_level(Level::Info).expect("Error initializing loggerv.");
|
||||||
|
gtk::init().expect("Error initializing gtk.");
|
||||||
|
static_resource::init().expect("Something went wrong with the resource file initialization.");
|
||||||
|
|
||||||
|
// Add custom style
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
gtk::CssProvider::load_from_resource(&provider, "/org/gnome/Hammond/gtk/style.css");
|
||||||
|
gtk::StyleContext::add_provider_for_screen(
|
||||||
|
&gdk::Screen::get_default().expect("Error initializing gtk css provider."),
|
||||||
|
&provider,
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
|
||||||
|
App::new().run();
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
use rayon;
|
use rayon;
|
||||||
|
|
||||||
// use podcasts_data::Episode;
|
// use hammond_data::Episode;
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_downloader::downloader::{get_episode, DownloadProgress};
|
use hammond_downloader::downloader::{get_episode, DownloadProgress};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
@@ -11,10 +11,10 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||||||
// use std::path::PathBuf;
|
// use std::path::PathBuf;
|
||||||
|
|
||||||
// This is messy, undocumented and hacky af.
|
// This is messy, undocumented and hacky af.
|
||||||
// I am terrible at writing downloaders and download managers.
|
// I am terrible at writting downloaders and download managers.
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Progress {
|
pub struct Progress {
|
||||||
total_bytes: u64,
|
total_bytes: u64,
|
||||||
downloaded_bytes: u64,
|
downloaded_bytes: u64,
|
||||||
cancel: bool,
|
cancel: bool,
|
||||||
@@ -31,7 +31,7 @@ impl Default for Progress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Progress {
|
impl Progress {
|
||||||
pub(crate) fn get_fraction(&self) -> f64 {
|
pub fn get_fraction(&self) -> f64 {
|
||||||
let ratio = self.downloaded_bytes as f64 / self.total_bytes as f64;
|
let ratio = self.downloaded_bytes as f64 / self.total_bytes as f64;
|
||||||
debug!("{:?}", self);
|
debug!("{:?}", self);
|
||||||
debug!("Ratio completed: {}", ratio);
|
debug!("Ratio completed: {}", ratio);
|
||||||
@@ -42,15 +42,15 @@ impl Progress {
|
|||||||
ratio
|
ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_total_size(&self) -> u64 {
|
pub fn get_total_size(&self) -> u64 {
|
||||||
self.total_bytes
|
self.total_bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_downloaded(&self) -> u64 {
|
pub fn get_downloaded(&self) -> u64 {
|
||||||
self.downloaded_bytes
|
self.downloaded_bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cancel(&mut self) {
|
pub fn cancel(&mut self) {
|
||||||
self.cancel = true;
|
self.cancel = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,18 +70,18 @@ impl DownloadProgress for Progress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub(crate) static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
|
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
|
||||||
{ Arc::new(RwLock::new(HashMap::new())) };
|
{ Arc::new(RwLock::new(HashMap::new())) };
|
||||||
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add(id: i32, directory: String) -> Result<(), Error> {
|
pub fn add(id: i32, directory: String) -> Result<(), Error> {
|
||||||
// Create a new `Progress` struct to keep track of dl progress.
|
// Create a new `Progress` struct to keep track of dl progress.
|
||||||
let prog = Arc::new(Mutex::new(Progress::default()));
|
let prog = Arc::new(Mutex::new(Progress::default()));
|
||||||
|
|
||||||
match ACTIVE_DOWNLOADS.write() {
|
match ACTIVE_DOWNLOADS.write() {
|
||||||
Ok(mut guard) => guard.insert(id, prog.clone()),
|
Ok(mut guard) => guard.insert(id, prog.clone()),
|
||||||
Err(err) => return Err(format_err!("ActiveDownloads: {}.", err)),
|
Err(err) => return Err(format_err!("ActiveDonwloads: {}.", err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
DLPOOL.spawn(move || {
|
DLPOOL.spawn(move || {
|
||||||
@@ -110,82 +110,75 @@ pub(crate) fn add(id: i32, directory: String) -> Result<(), Error> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::pipeline;
|
use hammond_data::pipeline;
|
||||||
use podcasts_data::utils::get_download_folder;
|
use hammond_data::utils::get_download_folder;
|
||||||
use podcasts_data::{Episode, Save, Source};
|
use hammond_data::{Episode, Source};
|
||||||
|
|
||||||
use podcasts_downloader::downloader::get_episode;
|
use hammond_downloader::downloader::get_episode;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// This test inserts an rss feed to your `XDG_DATA/podcasts/podcasts.db` so we make it explicit
|
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
|
||||||
// to run it.
|
// to run it.
|
||||||
#[ignore]
|
#[ignore]
|
||||||
// THIS IS NOT A RELIABLE TEST
|
// THIS IS NOT A RELIABLE TEST
|
||||||
// Just quick sanity check
|
// Just quick sanity check
|
||||||
fn test_start_dl() -> Result<(), Error> {
|
fn test_start_dl() {
|
||||||
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||||
// Create and index a source
|
// Create and index a source
|
||||||
let mut source = Source::from_url(url)?;
|
let source = Source::from_url(url).unwrap();
|
||||||
// Copy its id
|
// Copy it's id
|
||||||
let sid = source.id();
|
let sid = source.id();
|
||||||
source.set_http_etag(None);
|
pipeline::run(vec![source], true).unwrap();
|
||||||
source.set_last_modified(None);
|
|
||||||
source.save()?;
|
|
||||||
pipeline::run(vec![source])?;
|
|
||||||
|
|
||||||
// Get the podcast
|
// Get the Podcast
|
||||||
let pd = dbqueries::get_podcast_from_source_id(sid)?;
|
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
||||||
let title = "Coming soon... The Tip Off";
|
let title = "Coming soon... The Tip Off";
|
||||||
// Get an episode
|
// Get an episode
|
||||||
let episode: Episode = dbqueries::get_episode_from_pk(title, pd.id())?;
|
let episode: Episode = dbqueries::get_episode_from_pk(title, pd.id()).unwrap();
|
||||||
|
|
||||||
let download_fold = get_download_folder(&pd.title())?;
|
let download_fold = get_download_folder(&pd.title()).unwrap();
|
||||||
let fold2 = download_fold.clone();
|
let fold2 = download_fold.clone();
|
||||||
add(episode.rowid(), download_fold)?;
|
add(episode.rowid(), download_fold).unwrap();
|
||||||
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
|
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
|
||||||
|
|
||||||
// Give it some time to download the file
|
// Give it soem time to download the file
|
||||||
thread::sleep(time::Duration::from_secs(20));
|
thread::sleep(time::Duration::from_secs(20));
|
||||||
|
|
||||||
let final_path = format!("{}/{}.mp3", &fold2, episode.rowid());
|
let final_path = format!("{}/{}.mp3", &fold2, episode.rowid());
|
||||||
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 0);
|
|
||||||
assert!(Path::new(&final_path).exists());
|
assert!(Path::new(&final_path).exists());
|
||||||
fs::remove_file(final_path)?;
|
fs::remove_file(final_path).unwrap();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// This test needs access to local system so we ignore it by default.
|
// This test needs access to local system so we ignore it by default.
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_dl_steal_the_stars() -> Result<(), Error> {
|
fn test_dl_steal_the_stars() {
|
||||||
let url =
|
let url =
|
||||||
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
|
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
|
||||||
// Create and index a source
|
// Create and index a source
|
||||||
let mut source = Source::from_url(url)?;
|
let source = Source::from_url(url).unwrap();
|
||||||
// Copy its id
|
// Copy it's id
|
||||||
let sid = source.id();
|
let sid = source.id();
|
||||||
source.set_http_etag(None);
|
pipeline::run(vec![source], true).unwrap();
|
||||||
source.set_last_modified(None);
|
|
||||||
source.save()?;
|
|
||||||
pipeline::run(vec![source])?;
|
|
||||||
|
|
||||||
// Get the podcast
|
// Get the Podcast
|
||||||
let pd = dbqueries::get_podcast_from_source_id(sid)?;
|
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
||||||
let title = "Introducing Steal the Stars";
|
let title = "Introducing Steal the Stars";
|
||||||
// Get an episode
|
// Get an episode
|
||||||
let mut episode = dbqueries::get_episode_from_pk(title, pd.id())?.into();
|
let mut episode = dbqueries::get_episode_from_pk(title, pd.id())
|
||||||
let download_fold = get_download_folder(&pd.title())?;
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
let download_fold = get_download_folder(&pd.title()).unwrap();
|
||||||
|
|
||||||
get_episode(&mut episode, &download_fold, None)?;
|
get_episode(&mut episode, &download_fold, None).unwrap();
|
||||||
|
|
||||||
let final_path = format!("{}/{}.mp3", &download_fold, episode.rowid());
|
let final_path = format!("{}/{}.mp3", &download_fold, episode.rowid());
|
||||||
assert!(Path::new(&final_path).exists());
|
assert!(Path::new(&final_path).exists());
|
||||||
fs::remove_file(final_path)?;
|
fs::remove_file(final_path).unwrap();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ use gtk::GtkWindowExt;
|
|||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
|
||||||
pub(crate) struct WindowGeometry {
|
pub struct WindowGeometry {
|
||||||
left: i32,
|
left: i32,
|
||||||
top: i32,
|
top: i32,
|
||||||
width: i32,
|
width: i32,
|
||||||
@@ -15,7 +15,7 @@ pub(crate) struct WindowGeometry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WindowGeometry {
|
impl WindowGeometry {
|
||||||
pub(crate) fn from_window(window: >k::ApplicationWindow) -> WindowGeometry {
|
pub fn from_window(window: >k::ApplicationWindow) -> WindowGeometry {
|
||||||
let position = window.get_position();
|
let position = window.get_position();
|
||||||
let size = window.get_size();
|
let size = window.get_size();
|
||||||
let left = position.0;
|
let left = position.0;
|
||||||
@@ -33,7 +33,7 @@ impl WindowGeometry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_settings(settings: &gio::Settings) -> WindowGeometry {
|
pub fn from_settings(settings: &gio::Settings) -> WindowGeometry {
|
||||||
let top = settings.get_int("persist-window-geometry-top");
|
let top = settings.get_int("persist-window-geometry-top");
|
||||||
let left = settings.get_int("persist-window-geometry-left");
|
let left = settings.get_int("persist-window-geometry-left");
|
||||||
let width = settings.get_int("persist-window-geometry-width");
|
let width = settings.get_int("persist-window-geometry-width");
|
||||||
@@ -49,7 +49,7 @@ impl WindowGeometry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply(&self, window: >k::ApplicationWindow) {
|
pub fn apply(&self, window: >k::ApplicationWindow) {
|
||||||
if self.width > 0 && self.height > 0 {
|
if self.width > 0 && self.height > 0 {
|
||||||
window.resize(self.width, self.height);
|
window.resize(self.width, self.height);
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ impl WindowGeometry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write(&self, settings: &gio::Settings) {
|
pub fn write(&self, settings: &gio::Settings) {
|
||||||
settings.set_int("persist-window-geometry-left", self.left);
|
settings.set_int("persist-window-geometry-left", self.left);
|
||||||
settings.set_int("persist-window-geometry-top", self.top);
|
settings.set_int("persist-window-geometry-top", self.top);
|
||||||
settings.set_int("persist-window-geometry-width", self.width);
|
settings.set_int("persist-window-geometry-width", self.width);
|
||||||
@@ -70,14 +70,14 @@ impl WindowGeometry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_refresh_interval(settings: &Settings) -> Duration {
|
pub fn get_refresh_interval(settings: &Settings) -> Duration {
|
||||||
let time = i64::from(settings.get_int("refresh-interval-time"));
|
let time = i64::from(settings.get_int("refresh-interval-time"));
|
||||||
let period = settings.get_string("refresh-interval-period").unwrap();
|
let period = settings.get_string("refresh-interval-period").unwrap();
|
||||||
|
|
||||||
time_period_to_duration(time, period.as_str())
|
time_period_to_duration(time, period.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
|
pub fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
|
||||||
let time = i64::from(settings.get_int("cleanup-age-time"));
|
let time = i64::from(settings.get_int("cleanup-age-time"));
|
||||||
let period = settings.get_string("cleanup-age-period").unwrap();
|
let period = settings.get_string("cleanup-age-period").unwrap();
|
||||||
let duration = time_period_to_duration(time, period.as_str());
|
let duration = time_period_to_duration(time, period.as_str());
|
||||||
@@ -85,7 +85,7 @@ pub(crate) fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
|
|||||||
Utc::now() - duration
|
Utc::now() - duration
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn time_period_to_duration(time: i64, period: &str) -> Duration {
|
pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
|
||||||
match period {
|
match period {
|
||||||
"weeks" => Duration::weeks(time),
|
"weeks" => Duration::weeks(time),
|
||||||
"days" => Duration::days(time),
|
"days" => Duration::days(time),
|
||||||
@@ -10,16 +10,8 @@ use stacks::{HomeStack, ShowStack};
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use i18n::i18n;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub(crate) enum State {
|
|
||||||
Populated,
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Content {
|
pub struct Content {
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
shows: Rc<RefCell<ShowStack>>,
|
shows: Rc<RefCell<ShowStack>>,
|
||||||
home: Rc<RefCell<HomeStack>>,
|
home: Rc<RefCell<HomeStack>>,
|
||||||
@@ -27,29 +19,28 @@ pub(crate) struct Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Content {
|
impl Content {
|
||||||
pub(crate) fn new(sender: &Sender<Action>) -> Result<Rc<Content>, Error> {
|
pub fn new(sender: Sender<Action>) -> Result<Content, Error> {
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?));
|
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?));
|
||||||
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())));
|
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())?));
|
||||||
|
|
||||||
stack.add_titled(&home.borrow().get_stack(), "home", &i18n("New"));
|
stack.add_titled(&home.borrow().get_stack(), "home", "Recent");
|
||||||
stack.add_titled(&shows.borrow().get_stack(), "shows", &i18n("Shows"));
|
stack.add_titled(&shows.borrow().get_stack(), "shows", "Shows");
|
||||||
|
|
||||||
let con = Content {
|
Ok(Content {
|
||||||
stack,
|
stack,
|
||||||
shows,
|
shows,
|
||||||
home,
|
home,
|
||||||
sender: sender.clone(),
|
sender,
|
||||||
};
|
})
|
||||||
Ok(Rc::new(con))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update(&self) {
|
pub fn update(&self) {
|
||||||
self.update_home();
|
self.update_home();
|
||||||
self.update_shows();
|
self.update_shows();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_home(&self) {
|
pub fn update_home(&self) {
|
||||||
self.home
|
self.home
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.update()
|
.update()
|
||||||
@@ -57,7 +48,7 @@ impl Content {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_home_if_background(&self) {
|
pub fn update_home_if_background(&self) {
|
||||||
if self.stack.get_visible_child_name() != Some("home".into()) {
|
if self.stack.get_visible_child_name() != Some("home".into()) {
|
||||||
self.update_home();
|
self.update_home();
|
||||||
}
|
}
|
||||||
@@ -71,7 +62,7 @@ impl Content {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_shows_view(&self) {
|
pub fn update_shows_view(&self) {
|
||||||
self.shows
|
self.shows
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.update()
|
.update()
|
||||||
@@ -79,7 +70,7 @@ impl Content {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_widget_if_same(&self, pid: i32) {
|
pub fn update_widget_if_same(&self, pid: i32) {
|
||||||
let pop = self.shows.borrow().populated();
|
let pop = self.shows.borrow().populated();
|
||||||
pop.borrow_mut()
|
pop.borrow_mut()
|
||||||
.update_widget_if_same(pid)
|
.update_widget_if_same(pid)
|
||||||
@@ -87,29 +78,15 @@ impl Content {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_stack(&self) -> gtk::Stack {
|
pub fn get_stack(&self) -> gtk::Stack {
|
||||||
self.stack.clone()
|
self.stack.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_shows(&self) -> Rc<RefCell<ShowStack>> {
|
pub fn get_shows(&self) -> Rc<RefCell<ShowStack>> {
|
||||||
self.shows.clone()
|
self.shows.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn switch_to_empty_views(&self) {
|
pub fn get_visible_stack(&self) -> Option<gtk::Stack> {
|
||||||
use gtk::StackTransitionType::*;
|
self.stack.get_visible_child().and_then(|w| w.downcast::<gtk::Stack>().ok())
|
||||||
|
|
||||||
self.home
|
|
||||||
.borrow_mut()
|
|
||||||
.switch_visible(State::Empty, Crossfade);
|
|
||||||
self.shows.borrow_mut().switch_visible(State::Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn switch_to_populated(&self) {
|
|
||||||
use gtk::StackTransitionType::*;
|
|
||||||
|
|
||||||
self.home
|
|
||||||
.borrow_mut()
|
|
||||||
.switch_visible(State::Populated, Crossfade);
|
|
||||||
self.shows.borrow_mut().switch_visible(State::Populated);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::StackTransitionType;
|
||||||
|
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use failure::Error;
|
||||||
|
use hammond_data::dbqueries::is_episodes_populated;
|
||||||
|
use hammond_data::errors::DataError;
|
||||||
|
|
||||||
|
use app::Action;
|
||||||
|
use widgets::{EmptyView, HomeView};
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum State {
|
||||||
|
Home,
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HomeStack {
|
||||||
|
empty: EmptyView,
|
||||||
|
episodes: Rc<HomeView>,
|
||||||
|
stack: gtk::Stack,
|
||||||
|
state: State,
|
||||||
|
sender: Sender<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HomeStack {
|
||||||
|
pub fn new(sender: Sender<Action>) -> Result<HomeStack, Error> {
|
||||||
|
let episodes = HomeView::new(sender.clone())?;
|
||||||
|
let empty = EmptyView::new();
|
||||||
|
let stack = gtk::Stack::new();
|
||||||
|
let state = State::Empty;
|
||||||
|
|
||||||
|
stack.add_named(&episodes.container, "home");
|
||||||
|
stack.add_named(&empty.container, "empty");
|
||||||
|
|
||||||
|
let mut home = HomeStack {
|
||||||
|
empty,
|
||||||
|
episodes,
|
||||||
|
stack,
|
||||||
|
state,
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
|
||||||
|
home.determine_state()?;
|
||||||
|
Ok(home)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stack(&self) -> gtk::Stack {
|
||||||
|
self.stack.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self) -> Result<(), Error> {
|
||||||
|
// Copy the vertical scrollbar adjustment from the old view.
|
||||||
|
self.episodes
|
||||||
|
.save_alignment()
|
||||||
|
.map_err(|err| error!("Failed to set episodes_view allignment: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
self.replace_view()?;
|
||||||
|
// Determine the actuall state.
|
||||||
|
self.determine_state().map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_view(&mut self) -> Result<(), Error> {
|
||||||
|
// Get the container of the view
|
||||||
|
let old = &self.episodes.container.clone();
|
||||||
|
let eps = HomeView::new(self.sender.clone())?;
|
||||||
|
|
||||||
|
// Remove the old widget and add the new one
|
||||||
|
// during this the previous view is removed,
|
||||||
|
// and the visibile child fallsback to empty view.
|
||||||
|
self.stack.remove(old);
|
||||||
|
self.stack.add_named(&eps.container, "home");
|
||||||
|
// Keep the previous state.
|
||||||
|
let s = self.state;
|
||||||
|
// Set the visible child back to the previous one to avoid
|
||||||
|
// the stack transition animation to show the empty view
|
||||||
|
self.switch_visible(s, StackTransitionType::None);
|
||||||
|
|
||||||
|
// replace view in the struct too
|
||||||
|
self.episodes = eps;
|
||||||
|
|
||||||
|
// This might not be needed
|
||||||
|
old.destroy();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_visible(&mut self, s: State, animation: StackTransitionType) {
|
||||||
|
use self::State::*;
|
||||||
|
|
||||||
|
match s {
|
||||||
|
Home => {
|
||||||
|
self.stack.set_visible_child_full("home", animation);
|
||||||
|
self.state = Home;
|
||||||
|
}
|
||||||
|
Empty => {
|
||||||
|
self.stack.set_visible_child_full("empty", animation);
|
||||||
|
self.state = Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_state(&mut self) -> Result<(), DataError> {
|
||||||
|
if is_episodes_populated()? {
|
||||||
|
self.switch_visible(State::Home, StackTransitionType::Crossfade);
|
||||||
|
} else {
|
||||||
|
self.switch_visible(State::Empty, StackTransitionType::Crossfade);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
mod content;
|
||||||
|
mod home;
|
||||||
|
mod populated;
|
||||||
|
mod show;
|
||||||
|
|
||||||
|
pub use self::content::Content;
|
||||||
|
pub use self::home::HomeStack;
|
||||||
|
pub use self::populated::{PopulatedStack, PopulatedState};
|
||||||
|
pub use self::show::{ShowStack, ShowState};
|
||||||
@@ -5,8 +5,8 @@ use gtk::StackTransitionType;
|
|||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::Show;
|
use hammond_data::Podcast;
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
use widgets::{ShowWidget, ShowsView};
|
use widgets::{ShowWidget, ShowsView};
|
||||||
@@ -15,13 +15,13 @@ use std::rc::Rc;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub(crate) enum PopulatedState {
|
pub enum PopulatedState {
|
||||||
View,
|
View,
|
||||||
Widget,
|
Widget,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct PopulatedStack {
|
pub struct PopulatedStack {
|
||||||
container: gtk::Box,
|
container: gtk::Box,
|
||||||
populated: Rc<ShowsView>,
|
populated: Rc<ShowsView>,
|
||||||
show: Rc<ShowWidget>,
|
show: Rc<ShowWidget>,
|
||||||
@@ -31,34 +31,36 @@ pub(crate) struct PopulatedStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PopulatedStack {
|
impl PopulatedStack {
|
||||||
pub(crate) fn new(sender: Sender<Action>) -> PopulatedStack {
|
pub fn new(sender: Sender<Action>) -> Result<PopulatedStack, Error> {
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
let state = PopulatedState::View;
|
let state = PopulatedState::View;
|
||||||
let populated = ShowsView::new(sender.clone(), None);
|
let populated = ShowsView::new(sender.clone())?;
|
||||||
let show = Rc::new(ShowWidget::default());
|
let show = Rc::new(ShowWidget::default());
|
||||||
let container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
let container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
|
||||||
stack.add_named(populated.view.container(), "shows");
|
stack.add_named(&populated.container, "shows");
|
||||||
stack.add_named(show.view.container(), "widget");
|
stack.add_named(&show.container, "widget");
|
||||||
container.add(&stack);
|
container.add(&stack);
|
||||||
container.show_all();
|
container.show_all();
|
||||||
|
|
||||||
PopulatedStack {
|
let show = PopulatedStack {
|
||||||
container,
|
container,
|
||||||
stack,
|
stack,
|
||||||
populated,
|
populated,
|
||||||
show,
|
show,
|
||||||
state,
|
state,
|
||||||
sender,
|
sender,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(show)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
self.update_widget().map_err(|err| format!("{}", err)).ok();
|
self.update_widget().map_err(|err| format!("{}", err)).ok();
|
||||||
self.update_shows().map_err(|err| format!("{}", err)).ok();
|
self.update_shows().map_err(|err| format!("{}", err)).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_shows(&mut self) -> Result<(), Error> {
|
pub fn update_shows(&mut self) -> Result<(), Error> {
|
||||||
// The current visible child might change depending on
|
// The current visible child might change depending on
|
||||||
// removal and insertion in the gtk::Stack, so we have
|
// removal and insertion in the gtk::Stack, so we have
|
||||||
// to make sure it will stay the same.
|
// to make sure it will stay the same.
|
||||||
@@ -69,36 +71,36 @@ impl PopulatedStack {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_shows(&mut self) -> Result<(), Error> {
|
pub fn replace_shows(&mut self) -> Result<(), Error> {
|
||||||
let old = &self.populated.view.container().clone();
|
let old = &self.populated.container.clone();
|
||||||
debug!("Name: {:?}", WidgetExt::get_name(old));
|
debug!("Name: {:?}", WidgetExt::get_name(old));
|
||||||
|
|
||||||
let vadj = self.populated.view.get_vadjustment();
|
self.populated
|
||||||
let pop = ShowsView::new(self.sender.clone(), vadj);
|
.save_alignment()
|
||||||
|
.map_err(|err| error!("Failed to set episodes_view allignment: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let pop = ShowsView::new(self.sender.clone())?;
|
||||||
self.populated = pop;
|
self.populated = pop;
|
||||||
self.stack.remove(old);
|
self.stack.remove(old);
|
||||||
self.stack
|
self.stack.add_named(&self.populated.container, "shows");
|
||||||
.add_named(self.populated.view.container(), "shows");
|
|
||||||
|
|
||||||
old.destroy();
|
old.destroy();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_widget(&mut self, pd: Arc<Show>) -> Result<(), Error> {
|
pub fn replace_widget(&mut self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||||
let old = self.show.view.container().clone();
|
let old = self.show.container.clone();
|
||||||
|
|
||||||
// Get the ShowWidget vertical alignment
|
// save the ShowWidget vertical scrollabar alignment
|
||||||
let vadj = self.show.view.get_vadjustment();
|
self.show
|
||||||
let new = match self.show.show_id() {
|
.podcast_id()
|
||||||
// If the previous show was the same, restore the alignment
|
.map(|id| self.show.save_vadjustment(id));
|
||||||
Some(id) if id == pd.id() => ShowWidget::new(pd, self.sender.clone(), vadj),
|
|
||||||
// else leave the valignemnt to default
|
|
||||||
_ => ShowWidget::new(pd.clone(), self.sender.clone(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let new = ShowWidget::new(pd, self.sender.clone());
|
||||||
self.show = new;
|
self.show = new;
|
||||||
self.stack.remove(&old);
|
self.stack.remove(&old);
|
||||||
self.stack.add_named(self.show.view.container(), "widget");
|
self.stack.add_named(&self.show.container, "widget");
|
||||||
|
|
||||||
// The current visible child might change depending on
|
// The current visible child might change depending on
|
||||||
// removal and insertion in the gtk::Stack, so we have
|
// removal and insertion in the gtk::Stack, so we have
|
||||||
@@ -109,9 +111,9 @@ impl PopulatedStack {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_widget(&mut self) -> Result<(), Error> {
|
pub fn update_widget(&mut self) -> Result<(), Error> {
|
||||||
let old = self.show.view.container().clone();
|
let old = self.show.container.clone();
|
||||||
let id = self.show.show_id();
|
let id = self.show.podcast_id();
|
||||||
if id.is_none() {
|
if id.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -129,9 +131,9 @@ impl PopulatedStack {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update widget if its show_id is equal to pid.
|
// Only update widget if its podcast_id is equal to pid.
|
||||||
pub(crate) fn update_widget_if_same(&mut self, pid: i32) -> Result<(), Error> {
|
pub fn update_widget_if_same(&mut self, pid: i32) -> Result<(), Error> {
|
||||||
if self.show.show_id() != Some(pid) {
|
if self.show.podcast_id() != Some(pid) {
|
||||||
debug!("Different widget. Early return");
|
debug!("Different widget. Early return");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -139,11 +141,11 @@ impl PopulatedStack {
|
|||||||
self.update_widget()
|
self.update_widget()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn container(&self) -> gtk::Box {
|
pub fn container(&self) -> gtk::Box {
|
||||||
self.container.clone()
|
self.container.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn switch_visible(&mut self, state: PopulatedState, animation: StackTransitionType) {
|
pub fn switch_visible(&mut self, state: PopulatedState, animation: StackTransitionType) {
|
||||||
use self::PopulatedState::*;
|
use self::PopulatedState::*;
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
@@ -3,36 +3,40 @@ use gtk::prelude::*;
|
|||||||
|
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use podcasts_data::dbqueries::is_podcasts_populated;
|
use hammond_data::dbqueries::is_podcasts_populated;
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
use stacks::content::State;
|
|
||||||
use stacks::PopulatedStack;
|
use stacks::PopulatedStack;
|
||||||
use utils::get_ignored_shows;
|
use utils::get_ignored_shows;
|
||||||
use widgets::EmptyView;
|
use widgets::EmptyView;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ops::Deref;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ShowState {
|
||||||
|
Populated,
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ShowStack {
|
pub struct ShowStack {
|
||||||
empty: EmptyView,
|
empty: EmptyView,
|
||||||
populated: Rc<RefCell<PopulatedStack>>,
|
populated: Rc<RefCell<PopulatedStack>>,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
state: State,
|
state: ShowState,
|
||||||
sender: Sender<Action>,
|
sender: Sender<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShowStack {
|
impl ShowStack {
|
||||||
pub(crate) fn new(sender: Sender<Action>) -> Self {
|
pub fn new(sender: Sender<Action>) -> Result<Self, Error> {
|
||||||
let populated = Rc::new(RefCell::new(PopulatedStack::new(sender.clone())));
|
let populated = Rc::new(RefCell::new(PopulatedStack::new(sender.clone())?));
|
||||||
let empty = EmptyView::default();
|
let empty = EmptyView::new();
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
let state = State::Empty;
|
let state = ShowState::Empty;
|
||||||
|
|
||||||
stack.add_named(&populated.borrow().container(), "populated");
|
stack.add_named(&populated.borrow().container(), "populated");
|
||||||
stack.add_named(empty.deref(), "empty");
|
stack.add_named(&empty.container, "empty");
|
||||||
|
|
||||||
let mut show = ShowStack {
|
let mut show = ShowStack {
|
||||||
empty,
|
empty,
|
||||||
@@ -42,26 +46,25 @@ impl ShowStack {
|
|||||||
sender,
|
sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = show.determine_state();
|
show.determine_state()?;
|
||||||
debug_assert!(res.is_ok());
|
Ok(show)
|
||||||
show
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_stack(&self) -> gtk::Stack {
|
pub fn get_stack(&self) -> gtk::Stack {
|
||||||
self.stack.clone()
|
self.stack.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn populated(&self) -> Rc<RefCell<PopulatedStack>> {
|
pub fn populated(&self) -> Rc<RefCell<PopulatedStack>> {
|
||||||
self.populated.clone()
|
self.populated.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update(&mut self) -> Result<(), Error> {
|
pub fn update(&mut self) -> Result<(), Error> {
|
||||||
self.populated.borrow_mut().update();
|
self.populated.borrow_mut().update();
|
||||||
self.determine_state()
|
self.determine_state()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn switch_visible(&mut self, s: State) {
|
fn switch_visible(&mut self, s: ShowState) {
|
||||||
use self::State::*;
|
use self::ShowState::*;
|
||||||
|
|
||||||
match s {
|
match s {
|
||||||
Populated => {
|
Populated => {
|
||||||
@@ -76,12 +79,14 @@ impl ShowStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn determine_state(&mut self) -> Result<(), Error> {
|
fn determine_state(&mut self) -> Result<(), Error> {
|
||||||
|
use self::ShowState::*;
|
||||||
|
|
||||||
let ign = get_ignored_shows()?;
|
let ign = get_ignored_shows()?;
|
||||||
debug!("IGNORED SHOWS {:?}", ign);
|
debug!("IGNORED SHOWS {:?}", ign);
|
||||||
if is_podcasts_populated(&ign)? {
|
if is_podcasts_populated(&ign)? {
|
||||||
self.sender.send(Action::PopulatedState);
|
self.switch_visible(Populated);
|
||||||
} else {
|
} else {
|
||||||
self.sender.send(Action::EmptyState);
|
self.switch_visible(Empty);
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use gio::{resources_register, Error, Resource};
|
use gio::{resources_register, Error, Resource};
|
||||||
use glib::Bytes;
|
use glib::Bytes;
|
||||||
|
|
||||||
pub(crate) fn init() -> Result<(), Error> {
|
pub fn init() -> Result<(), Error> {
|
||||||
// load the gresource binary at build time and include/link it into the final
|
// load the gresource binary at build time and include/link it into the final
|
||||||
// binary.
|
// binary.
|
||||||
let res_bytes = include_bytes!("../resources/resources.gresource");
|
let res_bytes = include_bytes!("../resources/resources.gresource");
|
||||||
@@ -10,7 +10,7 @@ pub(crate) fn init() -> Result<(), Error> {
|
|||||||
let gbytes = Bytes::from_static(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(&gbytes)?;
|
||||||
|
|
||||||
// Register the resource so it won't be dropped and will continue to live in
|
// Register the resource so It wont be dropped and will continue to live in
|
||||||
// memory.
|
// memory.
|
||||||
resources_register(&resource);
|
resources_register(&resource);
|
||||||
|
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
|
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
|
||||||
|
|
||||||
use gdk::FrameClockExt;
|
use gdk::FrameClockExt;
|
||||||
use gdk_pixbuf::{Object, Pixbuf};
|
use gdk_pixbuf::Pixbuf;
|
||||||
use glib::{self, object::WeakRef};
|
use glib;
|
||||||
use gtk;
|
use gtk;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IsA, Widget};
|
use gtk::{IsA, Widget};
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use crossbeam_channel::{bounded, unbounded, Sender};
|
use crossbeam_channel::Sender;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use fragile::Fragile;
|
|
||||||
use rayon;
|
use rayon;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest;
|
use reqwest;
|
||||||
|
use send_cell::SendCell;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
// use podcasts_data::feed;
|
// use hammond_data::feed;
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::opml;
|
use hammond_data::opml;
|
||||||
use podcasts_data::pipeline;
|
use hammond_data::pipeline;
|
||||||
use podcasts_data::utils::checkup;
|
use hammond_data::utils::checkup;
|
||||||
use podcasts_data::Source;
|
use hammond_data::Source;
|
||||||
use podcasts_downloader::downloader;
|
use hammond_downloader::downloader;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
|
|
||||||
use i18n::i18n;
|
|
||||||
|
|
||||||
/// Lazy evaluates and loads widgets to the parent `container` widget.
|
/// Lazy evaluates and loads widgets to the parent `container` widget.
|
||||||
///
|
///
|
||||||
/// Accepts an `IntoIterator`, `data`, as the source from which each widget
|
/// Accepts an `IntoIterator`, `data`, as the source from which each widget
|
||||||
@@ -64,25 +63,16 @@ use i18n::i18n;
|
|||||||
/// let list = gtk::ListBox::new();
|
/// let list = gtk::ListBox::new();
|
||||||
/// lazy_load(widgets, list, |w| w, || {});
|
/// lazy_load(widgets, list, |w| w, || {});
|
||||||
/// ```
|
/// ```
|
||||||
pub(crate) fn lazy_load<T, C, F, W, U>(
|
pub fn lazy_load<T, C, F, W, U>(data: T, container: C, mut contructor: F, callback: U)
|
||||||
data: T,
|
where
|
||||||
container: WeakRef<C>,
|
|
||||||
mut contructor: F,
|
|
||||||
callback: U,
|
|
||||||
) where
|
|
||||||
T: IntoIterator + 'static,
|
T: IntoIterator + 'static,
|
||||||
T::Item: 'static,
|
T::Item: 'static,
|
||||||
C: IsA<Object> + ContainerExt + 'static,
|
C: ContainerExt + 'static,
|
||||||
F: FnMut(T::Item) -> W + 'static,
|
F: FnMut(T::Item) -> W + 'static,
|
||||||
W: IsA<Widget> + WidgetExt,
|
W: IsA<Widget> + WidgetExt,
|
||||||
U: Fn() + 'static,
|
U: Fn() + 'static,
|
||||||
{
|
{
|
||||||
let func = move |x| {
|
let func = move |x| {
|
||||||
let container = match container.upgrade() {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let widget = contructor(x);
|
let widget = contructor(x);
|
||||||
container.add(&widget);
|
container.add(&widget);
|
||||||
widget.show();
|
widget.show();
|
||||||
@@ -97,7 +87,7 @@ pub(crate) fn lazy_load<T, C, F, W, U>(
|
|||||||
/// If you just want to lazy add `widgets` to a `container` check if
|
/// If you just want to lazy add `widgets` to a `container` check if
|
||||||
/// `lazy_load` fits your needs first.
|
/// `lazy_load` fits your needs first.
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))]
|
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))]
|
||||||
pub(crate) fn lazy_load_full<T, F, U>(data: T, mut func: F, finish_callback: U)
|
pub fn lazy_load_full<T, F, U>(data: T, mut func: F, finish_callback: U)
|
||||||
where
|
where
|
||||||
T: IntoIterator + 'static,
|
T: IntoIterator + 'static,
|
||||||
T::Item: 'static,
|
T::Item: 'static,
|
||||||
@@ -119,7 +109,7 @@ where
|
|||||||
// Kudos to Julian Sparber
|
// Kudos to Julian Sparber
|
||||||
// https://blogs.gnome.org/jsparber/2018/04/29/animate-a-scrolledwindow/
|
// https://blogs.gnome.org/jsparber/2018/04/29/animate-a-scrolledwindow/
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(float_cmp))]
|
#[cfg_attr(feature = "cargo-clippy", allow(float_cmp))]
|
||||||
pub(crate) fn smooth_scroll_to(view: >k::ScrolledWindow, target: >k::Adjustment) {
|
pub fn smooth_scroll_to(view: >k::ScrolledWindow, target: >k::Adjustment) {
|
||||||
if let Some(adj) = view.get_vadjustment() {
|
if let Some(adj) = view.get_vadjustment() {
|
||||||
if let Some(clock) = view.get_frame_clock() {
|
if let Some(clock) = view.get_frame_clock() {
|
||||||
let duration = 200;
|
let duration = 200;
|
||||||
@@ -156,127 +146,105 @@ lazy_static! {
|
|||||||
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
|
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ignore_show(id: i32) -> Result<bool, Error> {
|
pub fn ignore_show(id: i32) -> Result<bool, Error> {
|
||||||
IGNORESHOWS
|
IGNORESHOWS
|
||||||
.lock()
|
.lock()
|
||||||
.map(|mut guard| guard.insert(id))
|
.map(|mut guard| guard.insert(id))
|
||||||
.map_err(|err| format_err!("{}", err))
|
.map_err(|err| format_err!("{}", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn uningore_show(id: i32) -> Result<bool, Error> {
|
pub fn uningore_show(id: i32) -> Result<bool, Error> {
|
||||||
IGNORESHOWS
|
IGNORESHOWS
|
||||||
.lock()
|
.lock()
|
||||||
.map(|mut guard| guard.remove(&id))
|
.map(|mut guard| guard.remove(&id))
|
||||||
.map_err(|err| format_err!("{}", err))
|
.map_err(|err| format_err!("{}", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_ignored_shows() -> Result<Vec<i32>, Error> {
|
pub fn get_ignored_shows() -> Result<Vec<i32>, Error> {
|
||||||
IGNORESHOWS
|
IGNORESHOWS
|
||||||
.lock()
|
.lock()
|
||||||
.map(|guard| guard.iter().cloned().collect::<Vec<_>>())
|
.map(|guard| guard.iter().cloned().collect::<Vec<_>>())
|
||||||
.map_err(|err| format_err!("{}", err))
|
.map_err(|err| format_err!("{}", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cleanup(cleanup_date: DateTime<Utc>) {
|
pub fn cleanup(cleanup_date: DateTime<Utc>) {
|
||||||
checkup(cleanup_date)
|
checkup(cleanup_date)
|
||||||
.map_err(|err| error!("Check up failed: {}", err))
|
.map_err(|err| error!("Check up failed: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn refresh<S>(source: Option<S>, sender: Sender<Action>)
|
pub fn refresh<S>(source: Option<S>, sender: Sender<Action>)
|
||||||
where
|
where
|
||||||
S: IntoIterator<Item = Source> + Send + 'static,
|
S: IntoIterator<Item = Source> + Send + 'static,
|
||||||
{
|
{
|
||||||
// If we try to update the whole db,
|
|
||||||
// Exit early if `source` table is empty
|
|
||||||
if source.is_none() {
|
|
||||||
match dbqueries::is_source_populated(&[]) {
|
|
||||||
Ok(false) => {
|
|
||||||
info!("No source of feeds where found, returning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(err) => debug_assert!(false, err),
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh_feed(source, sender)
|
refresh_feed(source, sender)
|
||||||
|
.map_err(|err| error!("Failed to update feeds: {}", err))
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the rss feed(s) originating from `source`.
|
/// Update the rss feed(s) originating from `source`.
|
||||||
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
|
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
|
||||||
fn refresh_feed<S>(source: Option<S>, sender: Sender<Action>)
|
/// When It's done,it queues up a `RefreshViews` action.
|
||||||
|
fn refresh_feed<S>(source: Option<S>, sender: Sender<Action>) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
S: IntoIterator<Item = Source> + Send + 'static,
|
S: IntoIterator<Item = Source> + Send + 'static,
|
||||||
{
|
{
|
||||||
rayon::spawn(move || {
|
sender
|
||||||
let (up_sender, up_receiver) = bounded(1);
|
.send(Action::HeaderBarShowUpdateIndicator)
|
||||||
sender.send(Action::ShowUpdateNotif(up_receiver));
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
rayon::spawn(move || {
|
||||||
if let Some(s) = source {
|
if let Some(s) = source {
|
||||||
// Refresh only specified feeds
|
// Refresh only specified feeds
|
||||||
pipeline::run(s)
|
pipeline::run(s, false)
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.map_err(|_| error!("Error while trying to update the database."))
|
.map_err(|_| error!("Error While trying to update the database."))
|
||||||
.ok();
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
// Refresh all the feeds
|
// Refresh all the feeds
|
||||||
dbqueries::get_sources()
|
dbqueries::get_sources()
|
||||||
.map(|s| s.into_iter())
|
.map(|s| s.into_iter())
|
||||||
.and_then(pipeline::run)
|
.and_then(|s| pipeline::run(s, false))
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
};
|
};
|
||||||
|
|
||||||
up_sender.send(true);
|
sender
|
||||||
|
.send(Action::HeaderBarHideUpdateIndicator)
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
sender
|
||||||
|
.send(Action::RefreshAllViews)
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<Fragile<Pixbuf>>>> =
|
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
|
||||||
{ RwLock::new(HashMap::new()) };
|
{ RwLock::new(HashMap::new()) };
|
||||||
static ref COVER_DL_REGISTRY: RwLock<HashSet<i32>> = RwLock::new(HashSet::new());
|
static ref COVER_DL_REGISTRY: RwLock<HashSet<i32>> = RwLock::new(HashSet::new());
|
||||||
static ref THREADPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
static ref THREADPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since gdk_pixbuf::Pixbuf is reference counted and every episode,
|
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
|
||||||
// use the cover of the Podcast Feed/Show, We can only create a Pixbuf
|
// use the cover of the Podcast Feed/Show, We can only create a Pixbuf
|
||||||
// cover per show and pass around the Rc pointer.
|
// cover per show and pass around the Rc pointer.
|
||||||
//
|
//
|
||||||
// GObjects do not implement Send trait, so SendCell is a way around that.
|
// GObjects do not implement Send trait, so SendCell is a way around that.
|
||||||
// Also lazy_static requires Sync trait, so that's what the mutexes are.
|
// Also lazy_static requires Sync trait, so that's what the mutexes are.
|
||||||
// TODO: maybe use something that would just scale to requested size?
|
// TODO: maybe use something that would just scale to requested size?
|
||||||
pub(crate) fn set_image_from_path(
|
pub fn set_image_from_path(image: >k::Image, podcast_id: i32, size: u32) -> Result<(), Error> {
|
||||||
image: >k::Image,
|
|
||||||
show_id: i32,
|
|
||||||
size: u32,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if let Ok(hashmap) = CACHED_PIXBUFS.read() {
|
|
||||||
// Check if the requested (cover + size) is already in the cache
|
|
||||||
// and if so do an early return after that.
|
|
||||||
if let Some(guard) = hashmap.get(&(show_id, size)) {
|
|
||||||
guard
|
|
||||||
.lock()
|
|
||||||
.map_err(|err| format_err!("Fragile Mutex: {}", err))
|
|
||||||
.and_then(|fragile| {
|
|
||||||
fragile
|
|
||||||
.try_get()
|
|
||||||
.map(|px| image.set_from_pixbuf(px))
|
|
||||||
.map_err(From::from)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's an active download about this show cover.
|
// Check if there's an active download about this show cover.
|
||||||
// If there is, a callback will be set so this function will be called again.
|
// If there is, a callback will be set so this function will be called again.
|
||||||
// If the download succeeds, there should be a quick return from the pixbuf cache_image
|
// If the download succedes, there should be a quick return from the pixbuf cache_image
|
||||||
// If it fails another download will be scheduled.
|
// If it fails another download will be scheduled.
|
||||||
if let Ok(guard) = COVER_DL_REGISTRY.read() {
|
if let Ok(guard) = COVER_DL_REGISTRY.read() {
|
||||||
if guard.contains(&show_id) {
|
if guard.contains(&podcast_id) {
|
||||||
let callback = clone!(image => move || {
|
let callback = clone!(image => move || {
|
||||||
let _ = set_image_from_path(&image, show_id, size);
|
let _ = set_image_from_path(&image, podcast_id, size);
|
||||||
glib::Continue(false)
|
glib::Continue(false)
|
||||||
});
|
});
|
||||||
gtk::timeout_add(250, callback);
|
gtk::timeout_add(250, callback);
|
||||||
@@ -284,32 +252,50 @@ pub(crate) fn set_image_from_path(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (sender, receiver) = unbounded();
|
if let Ok(hashmap) = CACHED_PIXBUFS.read() {
|
||||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
// Check if the requested (cover + size) is already in the chache
|
||||||
// Add the id to the hashmap from the main thread to avoid queuing more than one downloads.
|
// and if so do an early return after that.
|
||||||
guard.insert(show_id);
|
if let Some(guard) = hashmap.get(&(podcast_id, size)) {
|
||||||
drop(guard);
|
guard
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| format_err!("SendCell Mutex: {}", err))
|
||||||
|
.and_then(|sendcell| {
|
||||||
|
sendcell
|
||||||
|
.try_get()
|
||||||
|
.map(|px| image.set_from_pixbuf(px))
|
||||||
|
.ok_or_else(|| format_err!("Pixbuf was accessed from a different thread"))
|
||||||
|
})?;
|
||||||
|
|
||||||
THREADPOOL.spawn(move || {
|
return Ok(());
|
||||||
// This operation is polling and will block the thread till the download is finished
|
}
|
||||||
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(show_id) {
|
|
||||||
sender.send(downloader::cache_image(&pd));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
|
||||||
guard.remove(&show_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (sender, receiver) = channel();
|
||||||
|
THREADPOOL.spawn(move || {
|
||||||
|
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||||
|
guard.insert(podcast_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(podcast_id) {
|
||||||
|
sender
|
||||||
|
.send(downloader::cache_image(&pd))
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||||
|
guard.remove(&podcast_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let image = image.clone();
|
let image = image.clone();
|
||||||
let s = size as i32;
|
let s = size as i32;
|
||||||
gtk::timeout_add(25, move || {
|
gtk::timeout_add(25, move || {
|
||||||
if let Some(path) = receiver.try_recv() {
|
if let Ok(path) = receiver.try_recv() {
|
||||||
if let Ok(path) = path {
|
if let Ok(path) = path {
|
||||||
if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) {
|
if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) {
|
||||||
if let Ok(mut hashmap) = CACHED_PIXBUFS.write() {
|
if let Ok(mut hashmap) = CACHED_PIXBUFS.write() {
|
||||||
hashmap.insert((show_id, size), Mutex::new(Fragile::new(px.clone())));
|
hashmap.insert((podcast_id, size), Mutex::new(SendCell::new(px.clone())));
|
||||||
image.set_from_pixbuf(&px);
|
image.set_from_pixbuf(&px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,8 +309,8 @@ pub(crate) fn set_image_from_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: the signature should be `fn foo(s: Url) -> Result<Url, Error>`
|
// FIXME: the signature should be `fn foo(s: Url) -> Result<Url, Error>`
|
||||||
pub(crate) fn itunes_to_rss(url: &str) -> Result<String, Error> {
|
pub fn itunes_to_rss(url: &str) -> Result<String, Error> {
|
||||||
let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an iTunes ID."))?;
|
let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an Itunes ID."))?;
|
||||||
lookup_id(id)
|
lookup_id(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,16 +334,18 @@ fn lookup_id(id: u32) -> Result<String, Error> {
|
|||||||
.ok_or_else(|| format_err!("Failed to get url from itunes response"))
|
.ok_or_else(|| format_err!("Failed to get url from itunes response"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_import_clicked(window: >k::ApplicationWindow, sender: &Sender<Action>) {
|
pub fn on_import_clicked(window: >k::Window, sender: &Sender<Action>) {
|
||||||
use glib::translate::ToGlib;
|
use glib::translate::ToGlib;
|
||||||
use gtk::{FileChooserAction, FileChooserNative, FileFilter, ResponseType};
|
use gtk::{FileChooserAction, FileChooserNative, FileFilter, ResponseType};
|
||||||
|
|
||||||
|
// let dialog = FileChooserDialog::new(title, Some(&window), FileChooserAction::Open);
|
||||||
|
// TODO: It might be better to use a FileChooserNative widget.
|
||||||
// Create the FileChooser Dialog
|
// Create the FileChooser Dialog
|
||||||
let dialog = FileChooserNative::new(
|
let dialog = FileChooserNative::new(
|
||||||
Some(i18n("Select the file from which to you want to import shows.").as_str()),
|
Some("Select the file from which to you want to Import Shows."),
|
||||||
Some(window),
|
Some(window),
|
||||||
FileChooserAction::Open,
|
FileChooserAction::Open,
|
||||||
Some(i18n("_Import").as_str()),
|
Some("_Import"),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -366,46 +354,55 @@ pub(crate) fn on_import_clicked(window: >k::ApplicationWindow, sender: &Sender
|
|||||||
|
|
||||||
// Set a filter to show only xml files
|
// Set a filter to show only xml files
|
||||||
let filter = FileFilter::new();
|
let filter = FileFilter::new();
|
||||||
FileFilterExt::set_name(&filter, Some(i18n("OPML file").as_str()));
|
FileFilterExt::set_name(&filter, Some("OPML file"));
|
||||||
filter.add_mime_type("application/xml");
|
filter.add_mime_type("application/xml");
|
||||||
filter.add_mime_type("text/xml");
|
filter.add_mime_type("text/xml");
|
||||||
dialog.add_filter(&filter);
|
dialog.add_filter(&filter);
|
||||||
|
|
||||||
let resp = dialog.run();
|
dialog.connect_response(clone!(sender => move |dialog, resp| {
|
||||||
debug!("Dialog Response {}", resp);
|
debug!("Dialong Response {}", resp);
|
||||||
if resp == ResponseType::Accept.to_glib() {
|
if resp == ResponseType::Accept.to_glib() {
|
||||||
if let Some(filename) = dialog.get_filename() {
|
// TODO: Show an in-app notifictaion if the file can not be accessed
|
||||||
debug!("File selected: {:?}", filename);
|
if let Some(filename) = dialog.get_filename() {
|
||||||
|
debug!("File selected: {:?}", filename);
|
||||||
|
|
||||||
rayon::spawn(clone!(sender => move || {
|
rayon::spawn(clone!(sender => move || {
|
||||||
// Parse the file and import the feeds
|
// Parse the file and import the feeds
|
||||||
if let Ok(sources) = opml::import_from_file(filename) {
|
if let Ok(sources) = opml::import_from_file(filename) {
|
||||||
// Refresh the successfully parsed feeds to index them
|
// Refresh the succesfully parsed feeds to index them
|
||||||
refresh(Some(sources), sender)
|
refresh(Some(sources), sender)
|
||||||
} else {
|
} else {
|
||||||
let text = i18n("Failed to parse the imported file");
|
let text = String::from("Failed to parse the Imported file");
|
||||||
sender.send(Action::ErrorNotification(text));
|
sender.send(Action::ErrorNotification(text))
|
||||||
}
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
}))
|
.ok();
|
||||||
} else {
|
}
|
||||||
let text = i18n("Selected file could not be accessed.");
|
}))
|
||||||
sender.send(Action::ErrorNotification(text));
|
} else {
|
||||||
|
let text = String::from("Selected File could not be accessed.");
|
||||||
|
sender.send(Action::ErrorNotification(text))
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
dialog.destroy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
dialog.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use failure::Error;
|
// use hammond_data::Source;
|
||||||
// use podcasts_data::Source;
|
// use hammond_data::dbqueries;
|
||||||
// use podcasts_data::dbqueries;
|
|
||||||
|
|
||||||
// #[test]
|
// #[test]
|
||||||
// This test inserts an rss feed to your `XDG_DATA/podcasts/podcasts.db` so we make it explicit
|
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
|
||||||
// to run it.
|
// to run it.
|
||||||
// #[ignore]
|
// #[ignore]
|
||||||
// Disabled till https://gitlab.gnome.org/World/podcasts/issues/56
|
// Disabled till https://gitlab.gnome.org/World/hammond/issues/56
|
||||||
// fn test_set_image_from_path() {
|
// fn test_set_image_from_path() {
|
||||||
// let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
// let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||||
// Create and index a source
|
// Create and index a source
|
||||||
@@ -422,32 +419,29 @@ mod tests {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_itunes_to_rss() -> Result<(), Error> {
|
fn test_itunes_to_rss() {
|
||||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||||
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
||||||
assert_eq!(rss_url, itunes_to_rss(itunes_url)?);
|
assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap());
|
||||||
|
|
||||||
let itunes_url = "https://itunes.apple.com/podcast/id000000000000000";
|
let itunes_url = "https://itunes.apple.com/podcast/id000000000000000";
|
||||||
assert!(itunes_to_rss(itunes_url).is_err());
|
assert!(itunes_to_rss(itunes_url).is_err());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_itunes_id() -> Result<(), Error> {
|
fn test_itunes_id() {
|
||||||
let id = 1195206601;
|
let id = 1195206601;
|
||||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||||
assert_eq!(id, itunes_id_from_url(itunes_url).unwrap());
|
assert_eq!(id, itunes_id_from_url(itunes_url).unwrap());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_itunes_lookup_id() -> Result<(), Error> {
|
fn test_itunes_lookup_id() {
|
||||||
let id = 1195206601;
|
let id = 1195206601;
|
||||||
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
|
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
|
||||||
assert_eq!(rss_url, lookup_id(id)?);
|
assert_eq!(rss_url, lookup_id(id).unwrap());
|
||||||
|
|
||||||
let id = 000000000;
|
let id = 000000000;
|
||||||
assert!(lookup_id(id).is_err());
|
assert!(lookup_id(id).is_err());
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
use app::{APP_ID, VERSION};
|
|
||||||
use gtk;
|
use gtk;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
use i18n::i18n;
|
|
||||||
|
|
||||||
// Totally copied it from fractal.
|
// Totally copied it from fractal.
|
||||||
// https://gitlab.gnome.org/danigm/fractal/blob/503e311e22b9d7540089d735b92af8e8f93560c5/fractal-gtk/src/app.rs#L1883-1912
|
// https://gitlab.gnome.org/danigm/fractal/blob/503e311e22b9d7540089d735b92af8e8f93560c5/fractal-gtk/src/app.rs#L1883-1912
|
||||||
/// Given a `window` create and attach an `gtk::AboutDialog` to it.
|
/// Given a `window` create and attach an `gtk::AboutDialog` to it.
|
||||||
pub(crate) fn about_dialog(window: >k::ApplicationWindow) {
|
pub fn about_dialog(window: >k::Window) {
|
||||||
// Feel free to add yourself if you contributed.
|
// Feel free to add yourself if you contribured.
|
||||||
let authors = &[
|
let authors = &[
|
||||||
"Carlos Soriano",
|
|
||||||
"Constantin Nickel",
|
"Constantin Nickel",
|
||||||
"Gabriele Musco",
|
"Gabriele Musco",
|
||||||
"Ivan Augusto",
|
|
||||||
"James Wykeham-Martin",
|
"James Wykeham-Martin",
|
||||||
"Jordan Petridis",
|
"Jordan Petridis",
|
||||||
"Julian Sparber",
|
"Julian Sparber",
|
||||||
@@ -22,20 +17,24 @@ pub(crate) fn about_dialog(window: >k::ApplicationWindow) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let dialog = gtk::AboutDialog::new();
|
let dialog = gtk::AboutDialog::new();
|
||||||
dialog.set_logo_icon_name(APP_ID);
|
// Waiting for a logo.
|
||||||
dialog.set_comments(i18n("Podcast Client for the GNOME Desktop.").as_str());
|
// dialog.set_logo_icon_name("org.gnome.Hammond");
|
||||||
|
dialog.set_logo_icon_name("multimedia-player");
|
||||||
|
dialog.set_comments("Podcast Client for the GNOME Desktop.");
|
||||||
dialog.set_copyright("© 2017, 2018 Jordan Petridis");
|
dialog.set_copyright("© 2017, 2018 Jordan Petridis");
|
||||||
dialog.set_license_type(gtk::License::Gpl30);
|
dialog.set_license_type(gtk::License::Gpl30);
|
||||||
dialog.set_modal(true);
|
dialog.set_modal(true);
|
||||||
dialog.set_version(VERSION);
|
// TODO: make it show it fetches the commit hash from which it was built
|
||||||
dialog.set_program_name(&i18n("Podcasts"));
|
// and the version number is kept in sync automaticly
|
||||||
dialog.set_website("https://wiki.gnome.org/Apps/Podcasts");
|
dialog.set_version("0.3.4");
|
||||||
dialog.set_website_label(i18n("Learn more about GNOME Podcasts").as_str());
|
dialog.set_program_name("Hammond");
|
||||||
|
// TODO: Need a wiki page first.
|
||||||
|
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
|
||||||
|
// dialog.set_website_label("Learn more about Hammond");
|
||||||
dialog.set_transient_for(window);
|
dialog.set_transient_for(window);
|
||||||
|
|
||||||
dialog.set_artists(&["Tobias Bernard", "Sam Hewitt"]);
|
dialog.set_artists(&["Tobias Bernard"]);
|
||||||
dialog.set_authors(authors);
|
dialog.set_authors(authors);
|
||||||
dialog.set_translator_credits(i18n("translator-credits").as_str());
|
|
||||||
|
|
||||||
dialog.connect_response(|dlg, _| dlg.destroy());
|
dialog.connect_response(|dlg, _| dlg.destroy());
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use glib;
|
||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum UndoState {
|
||||||
|
Shown,
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InAppNotification {
|
||||||
|
revealer: gtk::Revealer,
|
||||||
|
text: gtk::Label,
|
||||||
|
undo: gtk::Button,
|
||||||
|
close: gtk::Button,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InAppNotification {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/inapp_notif.ui");
|
||||||
|
|
||||||
|
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
|
||||||
|
let text: gtk::Label = builder.get_object("text").unwrap();
|
||||||
|
let undo: gtk::Button = builder.get_object("undo").unwrap();
|
||||||
|
let close: gtk::Button = builder.get_object("close").unwrap();
|
||||||
|
|
||||||
|
InAppNotification {
|
||||||
|
revealer,
|
||||||
|
text,
|
||||||
|
undo,
|
||||||
|
close,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InAppNotification {
|
||||||
|
pub fn new<F, U>(text: &str, mut callback: F, undo_callback: U, show_undo: UndoState) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut() -> glib::Continue + 'static,
|
||||||
|
U: Fn() + 'static,
|
||||||
|
{
|
||||||
|
let notif = InAppNotification::default();
|
||||||
|
notif.text.set_text(&text);
|
||||||
|
|
||||||
|
let revealer = notif.revealer.clone();
|
||||||
|
let id = timeout_add_seconds(6, move || {
|
||||||
|
revealer.set_reveal_child(false);
|
||||||
|
callback()
|
||||||
|
});
|
||||||
|
let id = Rc::new(RefCell::new(Some(id)));
|
||||||
|
|
||||||
|
// Cancel the callback
|
||||||
|
let revealer = notif.revealer.clone();
|
||||||
|
notif.undo.connect_clicked(move |_| {
|
||||||
|
let foo = id.borrow_mut().take();
|
||||||
|
if let Some(id) = foo {
|
||||||
|
glib::source::source_remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo_callback();
|
||||||
|
|
||||||
|
// Hide the notification
|
||||||
|
revealer.set_reveal_child(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the revealer when the close button is clicked
|
||||||
|
let revealer = notif.revealer.clone();
|
||||||
|
notif.close.connect_clicked(move |_| {
|
||||||
|
revealer.set_reveal_child(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
match show_undo {
|
||||||
|
UndoState::Shown => (),
|
||||||
|
UndoState::Hidden => notif.undo.hide(),
|
||||||
|
}
|
||||||
|
|
||||||
|
notif
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a seperate method cause in order to get a nice animation
|
||||||
|
// the revealer should be attached to something that displays it.
|
||||||
|
// Previously we where doing it in the constructor, which had the result
|
||||||
|
// of the animation being skipped cause there was no parent widget to display it.
|
||||||
|
pub fn show(&self, overlay: >k::Overlay) {
|
||||||
|
overlay.add_overlay(&self.revealer);
|
||||||
|
// We need to display the notification after the widget is added to the overlay
|
||||||
|
// so there will be a nice animation.
|
||||||
|
self.revealer.set_reveal_child(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use gtk;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EmptyView {
|
||||||
|
pub container: gtk::Box,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EmptyView {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/empty_view.ui");
|
||||||
|
let view: gtk::Box = builder.get_object("empty_view").unwrap();
|
||||||
|
|
||||||
|
EmptyView { container: view }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmptyView {
|
||||||
|
pub fn new() -> EmptyView {
|
||||||
|
EmptyView::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,22 +7,19 @@ use chrono::prelude::*;
|
|||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use humansize::{file_size_opts as size_opts, FileSize};
|
use humansize::{file_size_opts as size_opts, FileSize};
|
||||||
#[allow(unused_imports)]
|
|
||||||
use open;
|
use open;
|
||||||
|
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::utils::get_download_folder;
|
use hammond_data::utils::get_download_folder;
|
||||||
use podcasts_data::EpisodeWidgetModel;
|
use hammond_data::EpisodeWidgetQuery;
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
use manager;
|
use manager;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::path::Path;
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex, TryLockError};
|
use std::sync::{Arc, Mutex, TryLockError};
|
||||||
|
|
||||||
use i18n::i18n_f;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
|
static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
|
||||||
// Declare a custom humansize option struct
|
// Declare a custom humansize option struct
|
||||||
@@ -42,8 +39,8 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct EpisodeWidget {
|
pub struct EpisodeWidget {
|
||||||
pub(crate) container: gtk::Box,
|
pub container: gtk::Box,
|
||||||
info: InfoLabels,
|
info: InfoLabels,
|
||||||
buttons: Buttons,
|
buttons: Buttons,
|
||||||
progressbar: gtk::ProgressBar,
|
progressbar: gtk::ProgressBar,
|
||||||
@@ -68,24 +65,25 @@ struct Buttons {
|
|||||||
play: gtk::Button,
|
play: gtk::Button,
|
||||||
download: gtk::Button,
|
download: gtk::Button,
|
||||||
cancel: gtk::Button,
|
cancel: gtk::Button,
|
||||||
|
desc: gtk::Button,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfoLabels {
|
impl InfoLabels {
|
||||||
fn init(&self, episode: &EpisodeWidgetModel) {
|
fn init(&self, episode: &EpisodeWidgetQuery) {
|
||||||
// Set the title label state.
|
// Set the title label state.
|
||||||
self.set_title(episode);
|
self.set_title(episode);
|
||||||
|
|
||||||
// Set the date label.
|
// Set the date label.
|
||||||
self.set_date(episode.epoch());
|
self.set_date(episode.epoch());
|
||||||
|
|
||||||
// Set the duration label.
|
// Set the duaration label.
|
||||||
self.set_duration(episode.duration());
|
self.set_duration(episode.duration());
|
||||||
|
|
||||||
// Set the total_size label.
|
// Set the total_size label.
|
||||||
self.set_size(episode.length())
|
self.set_size(episode.length())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_title(&self, episode: &EpisodeWidgetModel) {
|
fn set_title(&self, episode: &EpisodeWidgetQuery) {
|
||||||
self.title.set_text(episode.title());
|
self.title.set_text(episode.title());
|
||||||
|
|
||||||
if episode.played().is_some() {
|
if episode.played().is_some() {
|
||||||
@@ -118,15 +116,14 @@ impl InfoLabels {
|
|||||||
|
|
||||||
// Set the duration label of the episode widget.
|
// Set the duration label of the episode widget.
|
||||||
fn set_duration(&self, seconds: Option<i32>) {
|
fn set_duration(&self, seconds: Option<i32>) {
|
||||||
// If length is provided
|
// If lenght is provided
|
||||||
if let Some(s) = seconds {
|
if let Some(s) = seconds {
|
||||||
// Convert seconds to minutes
|
// Convert seconds to minutes
|
||||||
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
|
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
|
||||||
// If the length is 1 or more minutes
|
// If the lenght is 1 or more minutes
|
||||||
if minutes != 0 {
|
if minutes != 0 {
|
||||||
// Set the label and show them.
|
// Set the label and show them.
|
||||||
self.duration
|
self.duration.set_text(&format!("{} min", minutes));
|
||||||
.set_text(&i18n_f("{} min", &[&minutes.to_string()]));
|
|
||||||
self.duration.show();
|
self.duration.show();
|
||||||
self.separator1.show();
|
self.separator1.show();
|
||||||
return;
|
return;
|
||||||
@@ -163,7 +160,7 @@ impl InfoLabels {
|
|||||||
|
|
||||||
impl Default for EpisodeWidget {
|
impl Default for EpisodeWidget {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/episode_widget.ui");
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episode_widget.ui");
|
||||||
|
|
||||||
let container = builder.get_object("episode_container").unwrap();
|
let container = builder.get_object("episode_container").unwrap();
|
||||||
let progressbar = builder.get_object("progress_bar").unwrap();
|
let progressbar = builder.get_object("progress_bar").unwrap();
|
||||||
@@ -172,6 +169,7 @@ impl Default for EpisodeWidget {
|
|||||||
let download = builder.get_object("download_button").unwrap();
|
let download = builder.get_object("download_button").unwrap();
|
||||||
let play = builder.get_object("play_button").unwrap();
|
let play = builder.get_object("play_button").unwrap();
|
||||||
let cancel = builder.get_object("cancel_button").unwrap();
|
let cancel = builder.get_object("cancel_button").unwrap();
|
||||||
|
let desc_button = builder.get_object("desc_button").unwrap();
|
||||||
|
|
||||||
let info_container = builder.get_object("info_container").unwrap();
|
let info_container = builder.get_object("info_container").unwrap();
|
||||||
let title = builder.get_object("title_label").unwrap();
|
let title = builder.get_object("title_label").unwrap();
|
||||||
@@ -202,6 +200,7 @@ impl Default for EpisodeWidget {
|
|||||||
play,
|
play,
|
||||||
download,
|
download,
|
||||||
cancel,
|
cancel,
|
||||||
|
desc: desc_button,
|
||||||
},
|
},
|
||||||
progressbar,
|
progressbar,
|
||||||
container,
|
container,
|
||||||
@@ -210,38 +209,39 @@ impl Default for EpisodeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EpisodeWidget {
|
impl EpisodeWidget {
|
||||||
pub(crate) fn new(episode: EpisodeWidgetModel, sender: &Sender<Action>) -> Rc<Self> {
|
pub fn new(episode: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Rc<Self> {
|
||||||
let widget = Rc::new(Self::default());
|
let widget = Rc::new(Self::default());
|
||||||
let episode = RefCell::new(Some(episode));
|
widget.info.init(episode);
|
||||||
let weak = Rc::downgrade(&widget);
|
Self::determine_buttons_state(&widget, episode, sender)
|
||||||
widget.container.connect_draw(clone!(sender => move |_, _| {
|
.map_err(|err| error!("Error: {}", err))
|
||||||
episode.borrow_mut().take().map(|ep| {
|
.ok();
|
||||||
weak.upgrade().map(|w| w.info.init(&ep));
|
|
||||||
Self::determine_buttons_state(&weak, &ep, &sender)
|
|
||||||
.map_err(|err| error!("Error: {}", err))
|
|
||||||
.ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
gtk::Inhibit(false)
|
|
||||||
}));
|
|
||||||
|
|
||||||
// When the widget is attached to a parent,
|
|
||||||
// since it's a rust struct and not a widget the
|
|
||||||
// compiler drops the reference to it at the end of
|
|
||||||
// scope. That's cause we only attach the `self.container`
|
|
||||||
// to the parent.
|
|
||||||
//
|
|
||||||
// So this callback keeps a reference to the Rust Struct
|
|
||||||
// so the compiler won't drop it.
|
|
||||||
//
|
|
||||||
// When the widget is detached from its parent view this
|
|
||||||
// callback runs freeing the last reference we were holding.
|
|
||||||
let foo = RefCell::new(Some(widget.clone()));
|
|
||||||
widget.container.connect_remove(move |_, _| {
|
|
||||||
foo.borrow_mut().take();
|
|
||||||
});
|
|
||||||
|
|
||||||
widget
|
widget
|
||||||
|
.on_desc_clicked(episode, sender)
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.ok();
|
||||||
|
widget
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_desc_clicked(
|
||||||
|
&self,
|
||||||
|
episode: &EpisodeWidgetQuery,
|
||||||
|
sender: &Sender<Action>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let id = episode.rowid();
|
||||||
|
if dbqueries::episode_had_description(id)? {
|
||||||
|
self.buttons.desc.show();
|
||||||
|
self.buttons
|
||||||
|
.desc
|
||||||
|
.connect_clicked(clone!(sender => move |_| {
|
||||||
|
sender.send(Action::AboutWidget(id))
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.buttons.desc.hide();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn init(widget: Rc<Self>, sender: &Sender<Action>) {}
|
// fn init(widget: Rc<Self>, sender: &Sender<Action>) {}
|
||||||
@@ -325,13 +325,10 @@ impl EpisodeWidget {
|
|||||||
/// | ToDownload |
|
/// | ToDownload |
|
||||||
/// -------------------
|
/// -------------------
|
||||||
fn determine_buttons_state(
|
fn determine_buttons_state(
|
||||||
weak: &Weak<Self>,
|
widget: &Rc<Self>,
|
||||||
episode: &EpisodeWidgetModel,
|
episode: &EpisodeWidgetQuery,
|
||||||
sender: &Sender<Action>,
|
sender: &Sender<Action>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let widget = weak
|
|
||||||
.upgrade()
|
|
||||||
.ok_or_else(|| format_err!("Widget is already dropped"))?;
|
|
||||||
// Reset the buttons state no matter the glade file.
|
// Reset the buttons state no matter the glade file.
|
||||||
// This is just to make it easier to port to relm in the future.
|
// This is just to make it easier to port to relm in the future.
|
||||||
widget.buttons.cancel.hide();
|
widget.buttons.cancel.hide();
|
||||||
@@ -348,14 +345,14 @@ impl EpisodeWidget {
|
|||||||
Ok(m.get(&id).cloned())
|
Ok(m.get(&id).cloned())
|
||||||
};
|
};
|
||||||
|
|
||||||
// State: InProgress
|
// State: InProggress
|
||||||
if let Some(prog) = active_dl()? {
|
if let Some(prog) = active_dl()? {
|
||||||
// set a callback that will update the state when the download finishes
|
// set a callback that will update the state when the download finishes
|
||||||
let callback = clone!(weak, sender => move || {
|
let callback = clone!(widget, sender => move || {
|
||||||
if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() {
|
if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() {
|
||||||
if !guard.contains_key(&id) {
|
if !guard.contains_key(&id) {
|
||||||
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||||
Self::determine_buttons_state(&weak, &ep, &sender)
|
Self::determine_buttons_state(&widget, &ep, &sender)
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
@@ -372,20 +369,20 @@ impl EpisodeWidget {
|
|||||||
widget
|
widget
|
||||||
.buttons
|
.buttons
|
||||||
.cancel
|
.cancel
|
||||||
.connect_clicked(clone!(prog, weak, sender => move |_| {
|
.connect_clicked(clone!(prog, widget, sender => move |_| {
|
||||||
// Cancel the download
|
// Cancel the download
|
||||||
if let Ok(mut m) = prog.lock() {
|
if let Ok(mut m) = prog.lock() {
|
||||||
m.cancel();
|
m.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel is not instant so we have to wait a bit
|
// Cancel is not instant so we have to wait a bit
|
||||||
timeout_add(50, clone!(weak, sender => move || {
|
timeout_add(50, clone!(widget, sender => move || {
|
||||||
if let Ok(thing) = active_dl() {
|
if let Ok(thing) = active_dl() {
|
||||||
if thing.is_none() {
|
if thing.is_none() {
|
||||||
// Recalculate the widget state
|
// Recalculate the widget state
|
||||||
dbqueries::get_episode_widget_from_rowid(id)
|
dbqueries::get_episode_widget_from_rowid(id)
|
||||||
.map_err(From::from)
|
.map_err(From::from)
|
||||||
.and_then(|ep| Self::determine_buttons_state(&weak, &ep, &sender))
|
.and_then(|ep| Self::determine_buttons_state(&widget, &ep, &sender))
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
@@ -400,10 +397,10 @@ impl EpisodeWidget {
|
|||||||
// Setup a callback that will update the total_size label
|
// Setup a callback that will update the total_size label
|
||||||
// with the http ContentLength header number rather than
|
// with the http ContentLength header number rather than
|
||||||
// relying to the RSS feed.
|
// relying to the RSS feed.
|
||||||
update_total_size_callback(&weak, &prog);
|
update_total_size_callback(&widget, &prog);
|
||||||
|
|
||||||
// Setup a callback that will update the progress bar.
|
// Setup a callback that will update the progress bar.
|
||||||
update_progressbar_callback(&weak, &prog, id);
|
update_progressbar_callback(&widget, &prog, id);
|
||||||
|
|
||||||
// Change the widget layout/state
|
// Change the widget layout/state
|
||||||
widget.state_prog();
|
widget.state_prog();
|
||||||
@@ -420,9 +417,9 @@ impl EpisodeWidget {
|
|||||||
widget
|
widget
|
||||||
.buttons
|
.buttons
|
||||||
.play
|
.play
|
||||||
.connect_clicked(clone!(weak, sender => move |_| {
|
.connect_clicked(clone!(widget, sender => move |_| {
|
||||||
if let Ok(mut ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
if let Ok(mut ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||||
on_play_bttn_clicked(&weak, &mut ep, &sender)
|
on_play_bttn_clicked(&widget, &mut ep, &sender)
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -436,14 +433,14 @@ impl EpisodeWidget {
|
|||||||
widget
|
widget
|
||||||
.buttons
|
.buttons
|
||||||
.download
|
.download
|
||||||
.connect_clicked(clone!(weak, sender => move |dl| {
|
.connect_clicked(clone!(widget, sender => move |dl| {
|
||||||
// Make the button insensitive so it won't be pressed twice
|
// Make the button insensitive so it won't be pressed twice
|
||||||
dl.set_sensitive(false);
|
dl.set_sensitive(false);
|
||||||
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||||
on_download_clicked(&ep, &sender)
|
on_download_clicked(&ep, &sender)
|
||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
info!("Download started successfully.");
|
info!("Donwload started succesfully.");
|
||||||
Self::determine_buttons_state(&weak, &ep, &sender)
|
Self::determine_buttons_state(&widget, &ep, &sender)
|
||||||
})
|
})
|
||||||
.map_err(|err| error!("Error: {}", err))
|
.map_err(|err| error!("Error: {}", err))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -460,36 +457,44 @@ impl EpisodeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_download_clicked(ep: &EpisodeWidgetModel, sender: &Sender<Action>) -> Result<(), Error> {
|
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Result<(), Error> {
|
||||||
let pd = dbqueries::get_podcast_from_id(ep.show_id())?;
|
let pd = dbqueries::get_podcast_from_id(ep.podcast_id())?;
|
||||||
let download_fold = get_download_folder(&pd.title())?;
|
let download_fold = get_download_folder(&pd.title())?;
|
||||||
|
|
||||||
// Start a new download.
|
// Start a new download.
|
||||||
manager::add(ep.rowid(), download_fold)?;
|
manager::add(ep.rowid(), download_fold)?;
|
||||||
|
|
||||||
// Update Views
|
// Update Views
|
||||||
sender.send(Action::RefreshEpisodesViewBGR);
|
sender
|
||||||
Ok(())
|
.send(Action::RefreshEpisodesViewBGR)
|
||||||
|
.map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_play_bttn_clicked(
|
fn on_play_bttn_clicked(
|
||||||
widget: &Weak<EpisodeWidget>,
|
widget: &Rc<EpisodeWidget>,
|
||||||
episode: &mut EpisodeWidgetModel,
|
episode: &mut EpisodeWidgetQuery,
|
||||||
sender: &Sender<Action>,
|
sender: &Sender<Action>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let widget = widget
|
open_uri(episode.rowid())?;
|
||||||
.upgrade()
|
|
||||||
.ok_or_else(|| format_err!("Widget is already dropped"))?;
|
|
||||||
|
|
||||||
// Mark played
|
|
||||||
episode.set_played_now()?;
|
episode.set_played_now()?;
|
||||||
// Grey out the title
|
|
||||||
widget.info.set_title(&episode);
|
|
||||||
|
|
||||||
// Play the episode
|
widget.info.set_title(&episode);
|
||||||
sender.send(Action::InitEpisode(episode.rowid()));
|
sender
|
||||||
// Refresh background views to match the normal/greyout title state
|
.send(Action::RefreshEpisodesViewBGR)
|
||||||
sender.send(Action::RefreshEpisodesViewBGR);
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_uri(rowid: i32) -> Result<(), Error> {
|
||||||
|
let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
|
||||||
|
.ok_or_else(|| format_err!("Expected Some found None."))?;
|
||||||
|
|
||||||
|
if Path::new(&uri).exists() {
|
||||||
|
info!("Opening {}", uri);
|
||||||
|
open::that(&uri)?;
|
||||||
|
} else {
|
||||||
|
bail!("File \"{}\" does not exist.", uri);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +502,7 @@ fn on_play_bttn_clicked(
|
|||||||
#[inline]
|
#[inline]
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
|
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
|
||||||
fn update_progressbar_callback(
|
fn update_progressbar_callback(
|
||||||
widget: &Weak<EpisodeWidget>,
|
widget: &Rc<EpisodeWidget>,
|
||||||
prog: &Arc<Mutex<manager::Progress>>,
|
prog: &Arc<Mutex<manager::Progress>>,
|
||||||
episode_rowid: i32,
|
episode_rowid: i32,
|
||||||
) {
|
) {
|
||||||
@@ -510,15 +515,10 @@ fn update_progressbar_callback(
|
|||||||
|
|
||||||
#[allow(if_same_then_else)]
|
#[allow(if_same_then_else)]
|
||||||
fn progress_bar_helper(
|
fn progress_bar_helper(
|
||||||
widget: &Weak<EpisodeWidget>,
|
widget: &Rc<EpisodeWidget>,
|
||||||
prog: &Arc<Mutex<manager::Progress>>,
|
prog: &Arc<Mutex<manager::Progress>>,
|
||||||
episode_rowid: i32,
|
episode_rowid: i32,
|
||||||
) -> Result<glib::Continue, Error> {
|
) -> Result<glib::Continue, Error> {
|
||||||
let widget = match widget.upgrade() {
|
|
||||||
Some(w) => w,
|
|
||||||
None => return Ok(glib::Continue(false)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (fraction, downloaded) = match prog.try_lock() {
|
let (fraction, downloaded) = match prog.try_lock() {
|
||||||
Ok(guard) => (guard.get_fraction(), guard.get_downloaded()),
|
Ok(guard) => (guard.get_fraction(), guard.get_downloaded()),
|
||||||
Err(TryLockError::WouldBlock) => return Ok(glib::Continue(true)),
|
Err(TryLockError::WouldBlock) => return Ok(glib::Continue(true)),
|
||||||
@@ -558,7 +558,7 @@ fn progress_bar_helper(
|
|||||||
// with the http ContentLength header number rather than
|
// with the http ContentLength header number rather than
|
||||||
// relying to the RSS feed.
|
// relying to the RSS feed.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn update_total_size_callback(widget: &Weak<EpisodeWidget>, prog: &Arc<Mutex<manager::Progress>>) {
|
fn update_total_size_callback(widget: &Rc<EpisodeWidget>, prog: &Arc<Mutex<manager::Progress>>) {
|
||||||
let callback = clone!(prog, widget => move || {
|
let callback = clone!(prog, widget => move || {
|
||||||
total_size_helper(&widget, &prog).unwrap_or(glib::Continue(true))
|
total_size_helper(&widget, &prog).unwrap_or(glib::Continue(true))
|
||||||
});
|
});
|
||||||
@@ -566,14 +566,9 @@ fn update_total_size_callback(widget: &Weak<EpisodeWidget>, prog: &Arc<Mutex<man
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn total_size_helper(
|
fn total_size_helper(
|
||||||
widget: &Weak<EpisodeWidget>,
|
widget: &Rc<EpisodeWidget>,
|
||||||
prog: &Arc<Mutex<manager::Progress>>,
|
prog: &Arc<Mutex<manager::Progress>>,
|
||||||
) -> Result<glib::Continue, Error> {
|
) -> Result<glib::Continue, Error> {
|
||||||
let widget = match widget.upgrade() {
|
|
||||||
Some(w) => w,
|
|
||||||
None => return Ok(glib::Continue(false)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the total_bytes.
|
// Get the total_bytes.
|
||||||
let total_bytes = match prog.try_lock() {
|
let total_bytes = match prog.try_lock() {
|
||||||
Ok(guard) => guard.get_total_size(),
|
Ok(guard) => guard.get_total_size(),
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
use gtk::{self, prelude::*, Adjustment};
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use libhandy::{Column, ColumnExt};
|
use hammond_data::dbqueries;
|
||||||
use podcasts_data::dbqueries;
|
use hammond_data::EpisodeWidgetQuery;
|
||||||
use podcasts_data::EpisodeWidgetModel;
|
use send_cell::SendCell;
|
||||||
|
|
||||||
use app::Action;
|
use app::Action;
|
||||||
use utils::{self, lazy_load_full};
|
use utils::{self, lazy_load_full};
|
||||||
use widgets::{BaseView, EpisodeWidget};
|
use widgets::EpisodeWidget;
|
||||||
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref EPISODES_VIEW_VALIGNMENT: Mutex<Option<SendCell<gtk::Adjustment>>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum ListSplit {
|
enum ListSplit {
|
||||||
@@ -25,8 +31,9 @@ enum ListSplit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct HomeView {
|
pub struct HomeView {
|
||||||
pub(crate) view: BaseView,
|
pub container: gtk::Box,
|
||||||
|
scrolled_window: gtk::ScrolledWindow,
|
||||||
frame_parent: gtk::Box,
|
frame_parent: gtk::Box,
|
||||||
today_box: gtk::Box,
|
today_box: gtk::Box,
|
||||||
yday_box: gtk::Box,
|
yday_box: gtk::Box,
|
||||||
@@ -42,8 +49,9 @@ pub(crate) struct HomeView {
|
|||||||
|
|
||||||
impl Default for HomeView {
|
impl Default for HomeView {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let view = BaseView::default();
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episodes_view.ui");
|
||||||
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/home_view.ui");
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
|
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
|
||||||
let frame_parent: gtk::Box = builder.get_object("frame_parent").unwrap();
|
let frame_parent: gtk::Box = builder.get_object("frame_parent").unwrap();
|
||||||
let today_box: gtk::Box = builder.get_object("today_box").unwrap();
|
let today_box: gtk::Box = builder.get_object("today_box").unwrap();
|
||||||
let yday_box: gtk::Box = builder.get_object("yday_box").unwrap();
|
let yday_box: gtk::Box = builder.get_object("yday_box").unwrap();
|
||||||
@@ -56,19 +64,9 @@ impl Default for HomeView {
|
|||||||
let month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
|
let month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
|
||||||
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
|
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
|
||||||
|
|
||||||
let column = Column::new();
|
|
||||||
column.show();
|
|
||||||
column.set_maximum_width(700);
|
|
||||||
// For some reason the Column is not seen as a gtk::container
|
|
||||||
// and therefore we can't call add() without the cast
|
|
||||||
let column = column.upcast::<gtk::Widget>();
|
|
||||||
let column = column.downcast::<gtk::Container>().unwrap();
|
|
||||||
|
|
||||||
column.add(&frame_parent);
|
|
||||||
view.add(&column);
|
|
||||||
|
|
||||||
HomeView {
|
HomeView {
|
||||||
view,
|
container,
|
||||||
|
scrolled_window,
|
||||||
frame_parent,
|
frame_parent,
|
||||||
today_box,
|
today_box,
|
||||||
yday_box,
|
yday_box,
|
||||||
@@ -86,51 +84,69 @@ impl Default for HomeView {
|
|||||||
|
|
||||||
// TODO: REFACTOR ME
|
// TODO: REFACTOR ME
|
||||||
impl HomeView {
|
impl HomeView {
|
||||||
pub(crate) fn new(
|
pub fn new(sender: Sender<Action>) -> Result<Rc<HomeView>, Error> {
|
||||||
sender: Sender<Action>,
|
|
||||||
vadj: Option<Adjustment>,
|
|
||||||
) -> Result<Rc<HomeView>, Error> {
|
|
||||||
use self::ListSplit::*;
|
use self::ListSplit::*;
|
||||||
|
|
||||||
let home = Rc::new(HomeView::default());
|
let view = Rc::new(HomeView::default());
|
||||||
let ignore = utils::get_ignored_shows()?;
|
let ignore = utils::get_ignored_shows()?;
|
||||||
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 100)?;
|
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 100)?;
|
||||||
let now_utc = Utc::now();
|
let now_utc = Utc::now();
|
||||||
|
|
||||||
let home_weak = Rc::downgrade(&home);
|
let view_ = view.clone();
|
||||||
let func = move |ep: EpisodeWidgetModel| {
|
let func = move |ep: EpisodeWidgetQuery| {
|
||||||
let home = match home_weak.upgrade() {
|
|
||||||
Some(h) => h,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let epoch = ep.epoch();
|
let epoch = ep.epoch();
|
||||||
let widget = HomeEpisode::new(ep, &sender);
|
let widget = HomeEpisode::new(&ep, &sender);
|
||||||
|
|
||||||
match split(&now_utc, i64::from(epoch)) {
|
match split(&now_utc, i64::from(epoch)) {
|
||||||
Today => add_to_box(&widget, &home.today_list, &home.today_box),
|
Today => add_to_box(&widget, &view_.today_list, &view_.today_box),
|
||||||
Yday => add_to_box(&widget, &home.yday_list, &home.yday_box),
|
Yday => add_to_box(&widget, &view_.yday_list, &view_.yday_box),
|
||||||
Week => add_to_box(&widget, &home.week_list, &home.week_box),
|
Week => add_to_box(&widget, &view_.week_list, &view_.week_box),
|
||||||
Month => add_to_box(&widget, &home.month_list, &home.month_box),
|
Month => add_to_box(&widget, &view_.month_list, &view_.month_box),
|
||||||
Rest => add_to_box(&widget, &home.rest_list, &home.rest_box),
|
Rest => add_to_box(&widget, &view_.rest_list, &view_.rest_box),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let home_weak = Rc::downgrade(&home);
|
let view_ = view.clone();
|
||||||
let callback = move || {
|
let callback = move || {
|
||||||
let home = match home_weak.upgrade() {
|
view_
|
||||||
Some(h) => h,
|
.set_vadjustment()
|
||||||
None => return,
|
.map_err(|err| format!("{}", err))
|
||||||
};
|
.ok();
|
||||||
|
|
||||||
if let Some(ref v) = vadj {
|
|
||||||
home.view.set_adjutments(None, Some(v))
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_load_full(episodes, func, callback);
|
lazy_load_full(episodes, func, callback);
|
||||||
home.view.container().show_all();
|
view.container.show_all();
|
||||||
Ok(home)
|
Ok(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set scrolled window vertical adjustment.
|
||||||
|
fn set_vadjustment(&self) -> Result<(), Error> {
|
||||||
|
let guard = EPISODES_VIEW_VALIGNMENT
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
|
||||||
|
|
||||||
|
if let Some(ref sendcell) = *guard {
|
||||||
|
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||||
|
sendcell
|
||||||
|
.try_get()
|
||||||
|
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the vertical scrollbar position.
|
||||||
|
pub fn save_alignment(&self) -> Result<(), Error> {
|
||||||
|
if let Ok(mut guard) = EPISODES_VIEW_VALIGNMENT.lock() {
|
||||||
|
let adj = self
|
||||||
|
.scrolled_window
|
||||||
|
.get_vadjustment()
|
||||||
|
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
|
||||||
|
*guard = Some(SendCell::new(adj));
|
||||||
|
info!("Saved episodes_view alignment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,19 +172,21 @@ fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct HomeEpisode {
|
struct HomeEpisode {
|
||||||
container: gtk::Box,
|
container: gtk::Box,
|
||||||
image: gtk::Image,
|
image: gtk::Image,
|
||||||
|
// FIXME: Change it to `EpisodeWidget` instead of a `Box`?
|
||||||
episode: gtk::Box,
|
episode: gtk::Box,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HomeEpisode {
|
impl Default for HomeEpisode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/home_episode.ui");
|
let builder =
|
||||||
|
gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episodes_view_widget.ui");
|
||||||
let container: gtk::Box = builder.get_object("container").unwrap();
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
let image: gtk::Image = builder.get_object("cover").unwrap();
|
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
let ep = EpisodeWidget::default();
|
let ep = EpisodeWidget::default();
|
||||||
container.pack_start(&ep.container, true, true, 0);
|
container.pack_start(&ep.container, true, true, 6);
|
||||||
|
|
||||||
HomeEpisode {
|
HomeEpisode {
|
||||||
container,
|
container,
|
||||||
@@ -179,11 +197,12 @@ impl Default for HomeEpisode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HomeEpisode {
|
impl HomeEpisode {
|
||||||
fn new(episode: EpisodeWidgetModel, sender: &Sender<Action>) -> HomeEpisode {
|
fn new(episode: &EpisodeWidgetQuery, sender: &Sender<Action>) -> HomeEpisode {
|
||||||
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/home_episode.ui");
|
let builder =
|
||||||
|
gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episodes_view_widget.ui");
|
||||||
let container: gtk::Box = builder.get_object("container").unwrap();
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
let image: gtk::Image = builder.get_object("cover").unwrap();
|
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
let pid = episode.show_id();
|
let pid = episode.podcast_id();
|
||||||
let ep = EpisodeWidget::new(episode, sender);
|
let ep = EpisodeWidget::new(episode, sender);
|
||||||
|
|
||||||
let view = HomeEpisode {
|
let view = HomeEpisode {
|
||||||
@@ -196,28 +215,15 @@ impl HomeEpisode {
|
|||||||
view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(&self, show_id: i32) {
|
fn init(&self, podcast_id: i32) {
|
||||||
self.set_cover(show_id);
|
self.set_cover(podcast_id)
|
||||||
self.container.pack_start(&self.episode, true, true, 0);
|
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
self.container.pack_start(&self.episode, true, true, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cover(&self, show_id: i32) {
|
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
|
||||||
// The closure above is a regular `Fn` closure.
|
utils::set_image_from_path(&self.image, podcast_id, 64)
|
||||||
// which means we can't mutate stuff inside it easily,
|
|
||||||
// so Cell is used.
|
|
||||||
//
|
|
||||||
// `Option<T>` along with the `.take()` method ensure
|
|
||||||
// that the function will only be run once, during the first execution.
|
|
||||||
let show_id = Cell::new(Some(show_id));
|
|
||||||
|
|
||||||
self.image.connect_draw(move |image, _| {
|
|
||||||
show_id.take().map(|id| {
|
|
||||||
utils::set_image_from_path(image, id, 64)
|
|
||||||
.map_err(|err| error!("Failed to set a cover: {}", err))
|
|
||||||
.ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
gtk::Inhibit(false)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
mod aboutdialog;
|
||||||
|
pub mod appnotif;
|
||||||
|
mod empty;
|
||||||
|
mod episode;
|
||||||
|
mod home_view;
|
||||||
|
mod show;
|
||||||
|
mod shows_view;
|
||||||
|
|
||||||
|
pub use self::aboutdialog::about_dialog;
|
||||||
|
pub use self::empty::EmptyView;
|
||||||
|
pub use self::episode::EpisodeWidget;
|
||||||
|
pub use self::home_view::HomeView;
|
||||||
|
pub use self::show::ShowWidget;
|
||||||
|
pub use self::show::{mark_all_notif, remove_show_notif};
|
||||||
|
pub use self::shows_view::ShowsView;
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
use glib;
|
||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
use crossbeam_channel::{SendError, Sender};
|
||||||
|
use failure::Error;
|
||||||
|
use html2pango::markup_from_raw;
|
||||||
|
use open;
|
||||||
|
use rayon;
|
||||||
|
use send_cell::SendCell;
|
||||||
|
|
||||||
|
use hammond_data::dbqueries;
|
||||||
|
use hammond_data::utils::delete_show;
|
||||||
|
use hammond_data::Podcast;
|
||||||
|
|
||||||
|
use app::Action;
|
||||||
|
use utils::{self, lazy_load};
|
||||||
|
use widgets::appnotif::{InAppNotification, UndoState};
|
||||||
|
use widgets::EpisodeWidget;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShowWidget {
|
||||||
|
pub container: gtk::Box,
|
||||||
|
scrolled_window: gtk::ScrolledWindow,
|
||||||
|
cover: gtk::Image,
|
||||||
|
description: gtk::Label,
|
||||||
|
link: gtk::Button,
|
||||||
|
settings: gtk::MenuButton,
|
||||||
|
unsub: gtk::Button,
|
||||||
|
episodes: gtk::ListBox,
|
||||||
|
podcast_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShowWidget {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
|
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
|
||||||
|
let episodes = builder.get_object("episodes").unwrap();
|
||||||
|
|
||||||
|
let cover: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
|
let description: gtk::Label = builder.get_object("description").unwrap();
|
||||||
|
let unsub: gtk::Button = builder.get_object("unsub_button").unwrap();
|
||||||
|
let link: gtk::Button = builder.get_object("link_button").unwrap();
|
||||||
|
let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap();
|
||||||
|
|
||||||
|
ShowWidget {
|
||||||
|
container,
|
||||||
|
scrolled_window,
|
||||||
|
cover,
|
||||||
|
description,
|
||||||
|
unsub,
|
||||||
|
link,
|
||||||
|
settings,
|
||||||
|
episodes,
|
||||||
|
podcast_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShowWidget {
|
||||||
|
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> {
|
||||||
|
let mut pdw = ShowWidget::default();
|
||||||
|
pdw.init(&pd, &sender);
|
||||||
|
let pdw = Rc::new(pdw);
|
||||||
|
populate_listbox(&pdw, pd, sender)
|
||||||
|
.map_err(|err| error!("Failed to populate the listbox: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
pdw
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&mut self, pd: &Arc<Podcast>, sender: &Sender<Action>) {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui");
|
||||||
|
|
||||||
|
self.unsub
|
||||||
|
.connect_clicked(clone!(pd, sender => move |bttn| {
|
||||||
|
on_unsub_button_clicked(pd.clone(), bttn, &sender);
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.set_description(pd.description());
|
||||||
|
self.podcast_id = Some(pd.id());
|
||||||
|
|
||||||
|
self.set_cover(&pd)
|
||||||
|
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let link = pd.link().to_owned();
|
||||||
|
self.link.set_tooltip_text(Some(link.as_str()));
|
||||||
|
self.link.connect_clicked(move |_| {
|
||||||
|
info!("Opening link: {}", &link);
|
||||||
|
open::that(&link)
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.map_err(|_| error!("Failed open link: {}", &link))
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
|
||||||
|
let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap();
|
||||||
|
|
||||||
|
let episodes = self.episodes.clone();
|
||||||
|
mark_all.connect_clicked(clone!(pd, sender => move |_| {
|
||||||
|
on_played_button_clicked(
|
||||||
|
pd.clone(),
|
||||||
|
&episodes,
|
||||||
|
&sender
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
self.settings.set_popover(&show_menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the show cover.
|
||||||
|
fn set_cover(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
|
||||||
|
utils::set_image_from_path(&self.cover, pd.id(), 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the descripton text.
|
||||||
|
fn set_description(&self, text: &str) {
|
||||||
|
self.description.set_markup(&markup_from_raw(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the scrollabar vajustment to the cache.
|
||||||
|
pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> {
|
||||||
|
if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() {
|
||||||
|
let adj = self
|
||||||
|
.scrolled_window
|
||||||
|
.get_vadjustment()
|
||||||
|
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
|
||||||
|
*guard = Some((oldid, SendCell::new(adj)));
|
||||||
|
debug!("Widget Alignment was saved with ID: {}.", oldid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set scrolled window vertical adjustment.
|
||||||
|
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
|
||||||
|
let guard = SHOW_WIDGET_VALIGNMENT
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
|
||||||
|
|
||||||
|
if let Some((oldid, ref sendcell)) = *guard {
|
||||||
|
// Only copy the old scrollbar if both widget's represent the same podcast.
|
||||||
|
debug!("PID: {}", pd.id());
|
||||||
|
debug!("OLDID: {}", oldid);
|
||||||
|
if pd.id() != oldid {
|
||||||
|
debug!("Early return");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||||
|
sendcell
|
||||||
|
.try_get()
|
||||||
|
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn podcast_id(&self) -> Option<i32> {
|
||||||
|
self.podcast_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populate the listbox with the shows episodes.
|
||||||
|
fn populate_listbox(
|
||||||
|
show: &Rc<ShowWidget>,
|
||||||
|
pd: Arc<Podcast>,
|
||||||
|
sender: Sender<Action>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use crossbeam_channel::bounded;
|
||||||
|
use crossbeam_channel::TryRecvError::*;
|
||||||
|
|
||||||
|
let count = dbqueries::get_pd_episodes_count(&pd)?;
|
||||||
|
|
||||||
|
let (sender_, receiver) = bounded(1);
|
||||||
|
rayon::spawn(clone!(pd => move || {
|
||||||
|
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
|
||||||
|
// The receiver can be dropped if there's an early return
|
||||||
|
// like on show without episodes for example.
|
||||||
|
sender_.send(episodes).ok();
|
||||||
|
}));
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/empty_show.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("empty_show").unwrap();
|
||||||
|
show.episodes.add(&container);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let show_ = show.clone();
|
||||||
|
gtk::idle_add(move || {
|
||||||
|
let episodes = match receiver.try_recv() {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(Empty) => return glib::Continue(true),
|
||||||
|
Err(Disconnected) => return glib::Continue(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = show_.episodes.clone();
|
||||||
|
|
||||||
|
let constructor = clone!(sender => move |ep| {
|
||||||
|
EpisodeWidget::new(&ep, &sender).container.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
let callback = clone!(pd, show_ => move || {
|
||||||
|
show_.set_vadjustment(&pd)
|
||||||
|
.map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err))
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
lazy_load(episodes, list.clone(), constructor, callback);
|
||||||
|
|
||||||
|
glib::Continue(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: >k::Button, sender: &Sender<Action>) {
|
||||||
|
// hack to get away without properly checking for none.
|
||||||
|
// if pressed twice would panic.
|
||||||
|
unsub_button.set_sensitive(false);
|
||||||
|
|
||||||
|
let wrap = || -> Result<(), SendError<_>> {
|
||||||
|
sender.send(Action::RemoveShow(pd))?;
|
||||||
|
|
||||||
|
sender.send(Action::HeaderBarNormal)?;
|
||||||
|
sender.send(Action::ShowShowsAnimated)?;
|
||||||
|
// Queue a refresh after the switch to avoid blocking the db.
|
||||||
|
sender.send(Action::RefreshShowsView)?;
|
||||||
|
sender.send(Action::RefreshEpisodesView)?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
wrap().map_err(|err| error!("Action Sender: {}", err)).ok();
|
||||||
|
unsub_button.set_sensitive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: >k::ListBox, sender: &Sender<Action>) {
|
||||||
|
if dim_titles(episodes).is_none() {
|
||||||
|
error!("Something went horribly wrong when dimming the titles.");
|
||||||
|
warn!("RUN WHILE YOU STILL CAN!");
|
||||||
|
}
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(Action::MarkAllPlayerNotification(pd))
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
|
||||||
|
dbqueries::update_none_to_played_now(pd)?;
|
||||||
|
// Not all widgets migth have been loaded when the mark_all is hit
|
||||||
|
// So we will need to refresh again after it's done.
|
||||||
|
sender.send(Action::RefreshWidgetIfSame(pd.id()))?;
|
||||||
|
sender.send(Action::RefreshEpisodesView).map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_all_notif(pd: Arc<Podcast>, sender: &Sender<Action>) -> InAppNotification {
|
||||||
|
let id = pd.id();
|
||||||
|
let callback = clone!(sender => move || {
|
||||||
|
mark_all_watched(&pd, &sender)
|
||||||
|
.map_err(|err| error!("Notif Callback Error: {}", err))
|
||||||
|
.ok();
|
||||||
|
glib::Continue(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
let undo_callback = clone!(sender => move || {
|
||||||
|
sender.send(Action::RefreshWidgetIfSame(id))
|
||||||
|
.map_err(|err| error!("Action Sender: {}", err))
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = "Marked all episodes as listened";
|
||||||
|
InAppNotification::new(text, callback, undo_callback, UndoState::Shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
|
||||||
|
let text = format!("Unsubscribed from {}", pd.title());
|
||||||
|
|
||||||
|
utils::ignore_show(pd.id())
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.map_err(|_| error!("Could not insert {} to the ignore list.", pd.title()))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let callback = clone!(pd, sender => move || {
|
||||||
|
utils::uningore_show(pd.id())
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.map_err(|_| error!("Could not remove {} from the ignore list.", pd.title()))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Spawn a thread so it won't block the ui.
|
||||||
|
rayon::spawn(clone!(pd, sender => move || {
|
||||||
|
delete_show(&pd)
|
||||||
|
.map_err(|err| error!("Error: {}", err))
|
||||||
|
.map_err(|_| error!("Failed to delete {}", pd.title()))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
sender.send(Action::RefreshEpisodesView).ok();
|
||||||
|
}));
|
||||||
|
glib::Continue(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
let undo_wrap = move || -> Result<(), Error> {
|
||||||
|
utils::uningore_show(pd.id())?;
|
||||||
|
sender.send(Action::RefreshShowsView)?;
|
||||||
|
sender.send(Action::RefreshEpisodesView)?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let undo_callback = move || {
|
||||||
|
undo_wrap().map_err(|err| error!("{}", err)).ok();
|
||||||
|
};
|
||||||
|
|
||||||
|
InAppNotification::new(&text, callback, undo_callback, UndoState::Shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ideally if we had a custom widget this would have been as simple as:
|
||||||
|
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
|
||||||
|
// But now I can't think of a better way to do it than hardcoding the title
|
||||||
|
// position relative to the EpisodeWidget container gtk::Box.
|
||||||
|
fn dim_titles(episodes: >k::ListBox) -> Option<()> {
|
||||||
|
let children = episodes.get_children();
|
||||||
|
|
||||||
|
for row in children {
|
||||||
|
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
|
||||||
|
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||||
|
let foo = container
|
||||||
|
.get_children()
|
||||||
|
.remove(0)
|
||||||
|
.downcast::<gtk::Box>()
|
||||||
|
.ok()?;
|
||||||
|
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||||
|
let title = bar.get_children().remove(0).downcast::<gtk::Label>().ok()?;
|
||||||
|
|
||||||
|
title.get_style_context().map(|c| c.add_class("dim-label"));
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
use failure::Error;
|
||||||
|
use send_cell::SendCell;
|
||||||
|
|
||||||
|
use hammond_data::dbqueries;
|
||||||
|
use hammond_data::Podcast;
|
||||||
|
|
||||||
|
use app::Action;
|
||||||
|
use utils::{self, get_ignored_shows, lazy_load, set_image_from_path};
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SHOWS_VIEW_VALIGNMENT: Mutex<Option<SendCell<gtk::Adjustment>>> = Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShowsView {
|
||||||
|
pub container: gtk::Box,
|
||||||
|
scrolled_window: gtk::ScrolledWindow,
|
||||||
|
flowbox: gtk::FlowBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShowsView {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/shows_view.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
|
||||||
|
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
|
||||||
|
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
|
||||||
|
|
||||||
|
ShowsView {
|
||||||
|
container,
|
||||||
|
scrolled_window,
|
||||||
|
flowbox,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShowsView {
|
||||||
|
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> {
|
||||||
|
let pop = Rc::new(ShowsView::default());
|
||||||
|
pop.init(sender);
|
||||||
|
// Populate the flowbox with the Podcasts.
|
||||||
|
populate_flowbox(&pop)?;
|
||||||
|
Ok(pop)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self, sender: Sender<Action>) {
|
||||||
|
self.flowbox.connect_child_activated(move |_, child| {
|
||||||
|
on_child_activate(child, &sender)
|
||||||
|
.map_err(|err| error!("Error along flowbox child activation: {}", err))
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set scrolled window vertical adjustment.
|
||||||
|
#[allow(unused)]
|
||||||
|
fn set_vadjustment(&self) -> Result<(), Error> {
|
||||||
|
let guard = SHOWS_VIEW_VALIGNMENT
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
|
||||||
|
|
||||||
|
if let Some(ref sendcell) = *guard {
|
||||||
|
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||||
|
sendcell
|
||||||
|
.try_get()
|
||||||
|
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the vertical scrollbar position.
|
||||||
|
pub fn save_alignment(&self) -> Result<(), Error> {
|
||||||
|
if let Ok(mut guard) = SHOWS_VIEW_VALIGNMENT.lock() {
|
||||||
|
let adj = self
|
||||||
|
.scrolled_window
|
||||||
|
.get_vadjustment()
|
||||||
|
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
|
||||||
|
*guard = Some(SendCell::new(adj));
|
||||||
|
info!("Saved episodes_view alignment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_flowbox(shows: &Rc<ShowsView>) -> Result<(), Error> {
|
||||||
|
let ignore = get_ignored_shows()?;
|
||||||
|
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
|
||||||
|
|
||||||
|
let constructor = move |parent| ShowsChild::new(&parent).child;
|
||||||
|
let callback = clone!(shows => move || {
|
||||||
|
shows.set_vadjustment()
|
||||||
|
.map_err(|err| error!("Failed to set ShowsView Alignment: {}", err))
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let flowbox = shows.flowbox.clone();
|
||||||
|
lazy_load(podcasts, flowbox, constructor, callback);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_child_activate(child: >k::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> {
|
||||||
|
use gtk::WidgetExt;
|
||||||
|
|
||||||
|
// This is such an ugly hack...
|
||||||
|
let id = WidgetExt::get_name(child)
|
||||||
|
.ok_or_else(|| format_err!("Faild to get \"episodes\" child from the stack."))?
|
||||||
|
.parse::<i32>()?;
|
||||||
|
let pd = Arc::new(dbqueries::get_podcast_from_id(id)?);
|
||||||
|
|
||||||
|
sender.send(Action::HeaderBarShowTile(pd.title().into()))?;
|
||||||
|
sender.send(Action::ReplaceWidget(pd))?;
|
||||||
|
sender.send(Action::ShowWidgetAnimated)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ShowsChild {
|
||||||
|
container: gtk::Box,
|
||||||
|
cover: gtk::Image,
|
||||||
|
child: gtk::FlowBoxChild,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShowsChild {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/shows_child.ui");
|
||||||
|
|
||||||
|
let container: gtk::Box = builder.get_object("fb_child").unwrap();
|
||||||
|
let cover: gtk::Image = builder.get_object("pd_cover").unwrap();
|
||||||
|
|
||||||
|
let child = gtk::FlowBoxChild::new();
|
||||||
|
child.add(&container);
|
||||||
|
|
||||||
|
ShowsChild {
|
||||||
|
container,
|
||||||
|
cover,
|
||||||
|
child,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShowsChild {
|
||||||
|
pub fn new(pd: &Podcast) -> ShowsChild {
|
||||||
|
let child = ShowsChild::default();
|
||||||
|
child.init(pd);
|
||||||
|
child
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&self, pd: &Podcast) {
|
||||||
|
self.container.set_tooltip_text(pd.title());
|
||||||
|
WidgetExt::set_name(&self.child, &pd.id().to_string());
|
||||||
|
|
||||||
|
self.set_cover(pd.id())
|
||||||
|
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
|
||||||
|
set_image_from_path(&self.cover, podcast_id, 256)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,22 +5,25 @@
|
|||||||
xmlns:gnome="http://api.gnome.org/doap-extensions#"
|
xmlns:gnome="http://api.gnome.org/doap-extensions#"
|
||||||
xmlns="http://usefulinc.com/ns/doap#">
|
xmlns="http://usefulinc.com/ns/doap#">
|
||||||
|
|
||||||
<name xml:lang="en">Podcasts</name>
|
<name xml:lang="en">Hammond</name>
|
||||||
<shortdesc xml:lang="en">
|
<shortdesc xml:lang="en">
|
||||||
Podcast app for GNOME
|
Modern Podcast Client for the GNOME desktop
|
||||||
</shortdesc>
|
</shortdesc>
|
||||||
<description xml:lang="en">
|
<description xml:lang="en">
|
||||||
Listen to your favorite podcasts, right from your desktop.
|
<p>
|
||||||
|
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
|
||||||
|
desktop written in Rust.
|
||||||
|
</p>
|
||||||
</description>
|
</description>
|
||||||
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Podcasts" /> -->
|
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> -->
|
||||||
<bug-database rdf:resource="https://gitlab.gnome.org/World/podcasts/issues" />
|
<bug-database rdf:resource="https://gitlab.gnome.org/World/hammond/issues" />
|
||||||
|
|
||||||
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
|
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
|
||||||
<programming-language>Rust</programming-language>
|
<programming-language>Rust</programming-language>
|
||||||
|
|
||||||
<maintainer>
|
<maintainer>
|
||||||
<foaf:Person>
|
<foaf:Person>
|
||||||
<foaf:name>Jordan Petridis</foaf:name>
|
<foaf:name>Jordan petridis</foaf:name>
|
||||||
<foaf:mbox rdf:resource="mailto:jpetridis@gnome.org" />
|
<foaf:mbox rdf:resource="mailto:jpetridis@gnome.org" />
|
||||||
<gnome:userid>jpetridis</gnome:userid>
|
<gnome:userid>jpetridis</gnome:userid>
|
||||||
</foaf:Person>
|
</foaf:Person>
|
||||||
+17
-46
@@ -2,46 +2,23 @@
|
|||||||
# https://gitlab.gnome.org/danigm/fractal/blob/6e2911f9d2353c99a18a6c19fab7f903c4bbb431/meson.build
|
# https://gitlab.gnome.org/danigm/fractal/blob/6e2911f9d2353c99a18a6c19fab7f903c4bbb431/meson.build
|
||||||
|
|
||||||
project(
|
project(
|
||||||
'gnome-podcasts', 'rust',
|
'hammond', 'rust',
|
||||||
version: '0.4.5',
|
version: '0.3.4',
|
||||||
license: 'GPLv3',
|
license: 'GPLv3',
|
||||||
)
|
)
|
||||||
|
|
||||||
if get_option('profile') == 'development'
|
hammond_version = meson.project_version()
|
||||||
profile = 'Devel'
|
version_array = hammond_version.split('.')
|
||||||
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip()
|
hammond_major_version = version_array[0].to_int()
|
||||||
if vcs_tag == ''
|
hammond_minor_version = version_array[1].to_int()
|
||||||
version_suffix = '-devel'
|
hammond_version_micro = version_array[2].to_int()
|
||||||
else
|
|
||||||
version_suffix = '-@0@'.format (vcs_tag)
|
|
||||||
endif
|
|
||||||
else
|
|
||||||
profile = ''
|
|
||||||
version_suffix = ''
|
|
||||||
endif
|
|
||||||
|
|
||||||
application_id = 'org.gnome.Podcasts@0@'.format(profile)
|
hammond_prefix = get_option('prefix')
|
||||||
i18n = import('i18n')
|
hammond_bindir = join_paths(hammond_prefix, get_option('bindir'))
|
||||||
|
|
||||||
subdir('podcasts-gtk/po')
|
|
||||||
podir = join_paths (meson.source_root (), 'podcasts-gtk', 'po')
|
|
||||||
|
|
||||||
podcasts_version = meson.project_version()
|
|
||||||
version_array = podcasts_version.split('.')
|
|
||||||
podcasts_major_version = version_array[0].to_int()
|
|
||||||
podcasts_minor_version = version_array[1].to_int()
|
|
||||||
podcasts_version_micro = version_array[2].to_int()
|
|
||||||
|
|
||||||
podcasts_prefix = get_option('prefix')
|
|
||||||
podcasts_bindir = join_paths(podcasts_prefix, get_option('bindir'))
|
|
||||||
podcasts_localedir = join_paths(podcasts_prefix, get_option('localedir'))
|
|
||||||
|
|
||||||
podcasts_conf = configuration_data()
|
|
||||||
podcasts_conf.set('appid', application_id)
|
|
||||||
podcasts_conf.set('bindir', podcasts_bindir)
|
|
||||||
|
|
||||||
datadir = get_option('datadir')
|
datadir = get_option('datadir')
|
||||||
subdir('podcasts-gtk/resources')
|
icondir = join_paths(datadir, 'icons')
|
||||||
|
subdir('hammond-gtk/resources')
|
||||||
|
|
||||||
cargo = find_program('cargo', required: false)
|
cargo = find_program('cargo', required: false)
|
||||||
gresource = find_program('glib-compile-resources', required: false)
|
gresource = find_program('glib-compile-resources', required: false)
|
||||||
@@ -49,19 +26,13 @@ cargo_vendor = find_program('cargo-vendor', required: false)
|
|||||||
cargo_script = find_program('scripts/cargo.sh')
|
cargo_script = find_program('scripts/cargo.sh')
|
||||||
|
|
||||||
cargo_release = custom_target('cargo-build',
|
cargo_release = custom_target('cargo-build',
|
||||||
build_always_stale: true,
|
build_by_default: true,
|
||||||
output: ['gnome-podcasts'],
|
build_always: true,
|
||||||
|
output: ['hammond'],
|
||||||
install: true,
|
install: true,
|
||||||
install_dir: podcasts_bindir,
|
install_dir: hammond_bindir,
|
||||||
command: [cargo_script,
|
command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
|
||||||
'@CURRENT_SOURCE_DIR@',
|
|
||||||
'@OUTPUT@',
|
|
||||||
podcasts_localedir,
|
|
||||||
application_id,
|
|
||||||
podcasts_version + version_suffix,
|
|
||||||
profile
|
|
||||||
])
|
|
||||||
|
|
||||||
run_target('release', command: ['scripts/release.sh',
|
run_target('release', command: ['scripts/release.sh',
|
||||||
meson.project_name() + '-' + podcasts_version
|
meson.project_name() + '-' + hammond_version
|
||||||
])
|
])
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
option (
|
|
||||||
'profile',
|
|
||||||
type: 'combo',
|
|
||||||
choices: [
|
|
||||||
'default',
|
|
||||||
'development'
|
|
||||||
],
|
|
||||||
value: 'default'
|
|
||||||
)
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"app-id" : "org.gnome.PodcastsDevel",
|
"app-id" : "org.gnome.Hammond",
|
||||||
"runtime" : "org.gnome.Platform",
|
"runtime" : "org.gnome.Platform",
|
||||||
"runtime-version" : "master",
|
"runtime-version" : "3.28",
|
||||||
"sdk" : "org.gnome.Sdk",
|
"sdk" : "org.gnome.Sdk",
|
||||||
"sdk-extensions" : [
|
"sdk-extensions" : [
|
||||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||||
],
|
],
|
||||||
"command" : "gnome-podcasts",
|
"command" : "hammond",
|
||||||
"tags" : [
|
"tags" : [
|
||||||
"nightly"
|
"nightly"
|
||||||
],
|
],
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=x11",
|
"--socket=x11",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--socket=pulseaudio",
|
"--talk-name=org.freedesktop.Desktop"
|
||||||
"--device=dri"
|
|
||||||
],
|
],
|
||||||
"build-options" : {
|
"build-options" : {
|
||||||
"append-path" : "/usr/lib/sdk/rust-stable/bin",
|
"append-path" : "/usr/lib/sdk/rust-stable/bin",
|
||||||
@@ -29,29 +28,19 @@
|
|||||||
"--share=network"
|
"--share=network"
|
||||||
],
|
],
|
||||||
"env" : {
|
"env" : {
|
||||||
"CARGO_HOME" : "/run/build/Podcasts/cargo",
|
"CARGO_HOME" : "/run/build/Hammond/cargo",
|
||||||
"RUST_BACKTRACE" : "1"
|
"RUST_BACKTRACE" : "1",
|
||||||
|
"DEBUG" : "true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modules" : [
|
"modules" : [
|
||||||
{
|
{
|
||||||
"name" : "libhandy",
|
"name" : "hammond",
|
||||||
"buildsystem" : "meson",
|
"buildsystem" : "meson",
|
||||||
"sources" : [
|
"sources" : [
|
||||||
{
|
{
|
||||||
"type" : "git",
|
"type" : "git",
|
||||||
"url" : "https://source.puri.sm/Librem5/libhandy.git"
|
"url" : "https://gitlab.gnome.org/World/hammond.git",
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name" : "gnome-podcasts",
|
|
||||||
"buildsystem" : "meson",
|
|
||||||
"config-opts" : [ "-Dprofile=development" ],
|
|
||||||
"sources" : [
|
|
||||||
{
|
|
||||||
"type" : "git",
|
|
||||||
"url" : "https://gitlab.gnome.org/World/podcasts.git",
|
|
||||||
"branch" : "master"
|
"branch" : "master"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# For documentation on how to configure this file,
|
|
||||||
# see diesel.rs/guides/configuring-diesel-cli
|
|
||||||
|
|
||||||
[print_schema]
|
|
||||||
file = "src/schema.rs"
|
|
||||||
patch_file = "src/schema.patch"
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
ALTER TABLE episode RENAME TO old_table;
|
|
||||||
|
|
||||||
CREATE TABLE episode (
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
uri TEXT,
|
|
||||||
local_uri TEXT,
|
|
||||||
description TEXT,
|
|
||||||
epoch INTEGER NOT NULL DEFAULT 0,
|
|
||||||
length INTEGER,
|
|
||||||
duration INTEGER,
|
|
||||||
guid TEXT,
|
|
||||||
played INTEGER,
|
|
||||||
podcast_id INTEGER NOT NULL,
|
|
||||||
favorite INTEGER DEFAULT 0,
|
|
||||||
archive INTEGER DEFAULT 0,
|
|
||||||
PRIMARY KEY (title, podcast_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO episode (title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, favorite, archive)
|
|
||||||
SELECT title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, 0, 0
|
|
||||||
FROM old_table;
|
|
||||||
|
|
||||||
Drop table old_table;
|
|
||||||
|
|
||||||
ALTER TABLE podcast RENAME TO old_table;
|
|
||||||
CREATE TABLE `podcast` (
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
||||||
`title` TEXT NOT NULL,
|
|
||||||
`link` TEXT NOT NULL,
|
|
||||||
`description` TEXT NOT NULL,
|
|
||||||
`image_uri` TEXT,
|
|
||||||
`source_id` INTEGER NOT NULL UNIQUE,
|
|
||||||
`favorite` INTEGER NOT NULL DEFAULT 0,
|
|
||||||
`archive` INTEGER NOT NULL DEFAULT 0,
|
|
||||||
`always_dl` INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO podcast (
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
link,
|
|
||||||
description,
|
|
||||||
image_uri,
|
|
||||||
source_id
|
|
||||||
) SELECT id,
|
|
||||||
title,
|
|
||||||
link,
|
|
||||||
description,
|
|
||||||
image_uri,
|
|
||||||
source_id
|
|
||||||
FROM old_table;
|
|
||||||
|
|
||||||
Drop table old_table;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
ALTER TABLE episode RENAME TO old_table;
|
|
||||||
|
|
||||||
CREATE TABLE episode (
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
uri TEXT,
|
|
||||||
local_uri TEXT,
|
|
||||||
description TEXT,
|
|
||||||
epoch INTEGER NOT NULL DEFAULT 0,
|
|
||||||
length INTEGER,
|
|
||||||
duration INTEGER,
|
|
||||||
guid TEXT,
|
|
||||||
played INTEGER,
|
|
||||||
podcast_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (title, podcast_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO episode (
|
|
||||||
title,
|
|
||||||
uri,
|
|
||||||
local_uri,
|
|
||||||
description,
|
|
||||||
epoch,
|
|
||||||
length,
|
|
||||||
duration,
|
|
||||||
guid,
|
|
||||||
played,
|
|
||||||
podcast_id
|
|
||||||
) SELECT title,
|
|
||||||
uri,
|
|
||||||
local_uri,
|
|
||||||
description,
|
|
||||||
epoch, length,
|
|
||||||
duration,
|
|
||||||
guid,
|
|
||||||
played,
|
|
||||||
podcast_id
|
|
||||||
FROM old_table;
|
|
||||||
|
|
||||||
Drop table old_table;
|
|
||||||
|
|
||||||
ALTER TABLE podcast RENAME TO old_table;
|
|
||||||
CREATE TABLE `podcast` (
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
||||||
`title` TEXT NOT NULL,
|
|
||||||
`link` TEXT NOT NULL,
|
|
||||||
`description` TEXT NOT NULL,
|
|
||||||
`image_uri` TEXT,
|
|
||||||
`source_id` INTEGER NOT NULL UNIQUE
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO podcast (
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
link,
|
|
||||||
description,
|
|
||||||
image_uri,
|
|
||||||
source_id
|
|
||||||
) SELECT id,
|
|
||||||
title,
|
|
||||||
link,
|
|
||||||
description,
|
|
||||||
image_uri,
|
|
||||||
source_id
|
|
||||||
FROM old_table;
|
|
||||||
|
|
||||||
Drop table old_table;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
ALTER TABLE episodes RENAME TO old_table;
|
|
||||||
ALTER TABLE shows RENAME TO podcast;
|
|
||||||
|
|
||||||
CREATE TABLE episode (
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
uri TEXT,
|
|
||||||
local_uri TEXT,
|
|
||||||
description TEXT,
|
|
||||||
epoch INTEGER NOT NULL DEFAULT 0,
|
|
||||||
length INTEGER,
|
|
||||||
duration INTEGER,
|
|
||||||
guid TEXT,
|
|
||||||
played INTEGER,
|
|
||||||
podcast_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (title, podcast_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO episode (
|
|
||||||
title,
|
|
||||||
uri,
|
|
||||||
local_uri,
|
|
||||||
description,
|
|
||||||
epoch,
|
|
||||||
length,
|
|
||||||
duration,
|
|
||||||
guid,
|
|
||||||
played,
|
|
||||||
podcast_id
|
|
||||||
) SELECT title,
|
|
||||||
uri,
|
|
||||||
local_uri,
|
|
||||||
description,
|
|
||||||
epoch, length,
|
|
||||||
duration,
|
|
||||||
guid,
|
|
||||||
played,
|
|
||||||
show_id
|
|
||||||
FROM old_table;
|
|
||||||
|
|
||||||
Drop table old_table;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user