mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-24 06:28:55 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54cabd98b4 | ||
|
|
cbc5c571b3 | ||
|
|
beea640d49 | ||
|
|
65b8a7f988 | ||
|
|
b86173d6c0 | ||
|
|
09c4b2a28e | ||
|
|
6e8545e4ca | ||
|
|
e0a6c7842b | ||
|
|
b323cb40ae | ||
|
|
95a35f9988 | ||
|
|
a7f3bad191 | ||
|
|
ed20b67b92 | ||
|
|
42a3f75d94 | ||
|
|
6de882d12e | ||
|
|
6587014e8d | ||
|
|
36a07b251a | ||
|
|
c48eed7f77 | ||
|
|
48f3c3b35b | ||
|
|
0d6f62dd70 | ||
|
|
77cc5c99ed | ||
|
|
513134fd65 | ||
|
|
906be45758 | ||
|
|
c4a15e77e4 | ||
|
|
baffdfdd5a | ||
|
|
827afb5a4d | ||
|
|
26939b2cd3 | ||
|
|
34d05c5416 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -8,4 +8,5 @@ jobs:
|
||||
with:
|
||||
dockerRepository: flaminel/cleanuperr
|
||||
githubContext: ${{ toJSON(github) }}
|
||||
outputName: cleanuperr
|
||||
secrets: inherit
|
||||
7
.github/workflows/pipeline.yml
vendored
7
.github/workflows/pipeline.yml
vendored
@@ -1,8 +1,13 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
# paths:
|
||||
# - 'code/**'
|
||||
# branches: [ main ]
|
||||
pull_request:
|
||||
paths:
|
||||
- 'code/**'
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -1,5 +1,3 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -165,3 +165,7 @@ src/.idea/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
||||
**/logs/
|
||||
**/MediaCover/
|
||||
**/archive/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Flaminel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
293
README.md
293
README.md
@@ -1,151 +1,188 @@
|
||||
# cleanuperr
|
||||
|
||||
## How it works
|
||||
cleanuperr is a tool for automating the cleanup of unwanted or blocked files in Sonarr, Radarr, and supported download clients like qBittorrent. It removes incomplete or blocked downloads, updates queues, and enforces blacklists or whitelists to manage file selection. After removing blocked content, cleanuperr can also trigger a search to replace the deleted shows/movies.
|
||||
|
||||
1. Add excluded file names to prevent malicious files from being downloaded by qBittorrent.
|
||||
2. cleanuperr goes through all items in Sonarr's queue every at every 5th minute.
|
||||
3. For each queue item, a call is made to qBittorrent to get the stats of the torrent.
|
||||
4. If a torrent is found to be marked as completed, but with 0 downloaded bytes, cleanuperr calls Sonarr to add that torrent to the blocklist.
|
||||
5. If any malicious torrents have been found, cleanuperr calls Sonarr to automatically search again.
|
||||
cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
|
||||
|
||||
The tool supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
|
||||
|
||||
Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process.
|
||||
|
||||
## Important note
|
||||
|
||||
Only the <b>latest versions</b> of qBittorrent, Deluge, Sonarr etc. are supported, or earlier versions that have the same API as the latest version.
|
||||
|
||||
This tool is actively developed and still a work in progress. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together: https://discord.gg/cJYPs9Bt
|
||||
|
||||
# Setup
|
||||
|
||||
## Using qBittorrent's built-in feature (works only with qBittorrent)
|
||||
|
||||
1. Go to qBittorrent -> Options -> Downloads -> make sure `Excluded file names` is checked -> Set an exclusion list.
|
||||
- [blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist)
|
||||
- [permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive)
|
||||
- create your own
|
||||
2. Start cleanuperr with `QUEUECLEANER__ENABLED` set to `true`.
|
||||
3. cleanuperr will execute a queue cleaner cron job at every 5 minutes that will:
|
||||
1. go through all items from Sonarr/Radarr's queue.
|
||||
2. each a queue item is checked:
|
||||
- if it has been <b>marked as completed and 0 bytes have been downloaded</b> (because qBittorrent blocked the files).
|
||||
- if all its files are skipped.
|
||||
3. if the item <b>IS NOT</b> as described, it is skipped.
|
||||
4. if the item <b>IS</b> as described, it is removed from Sonarr/Radarr's queue, removed from qBittorrent and a search is triggered for the show/movie.
|
||||
|
||||
## Using cleanuperr's blocklist (works with all supported download clients)
|
||||
|
||||
1. Start cleanuperr with both `QUEUECLEANER_ENABLED` and `CONTENTBLOCKER_ENABLED` set to `true`.
|
||||
2. Be sure to set and enable a blacklist or a whitelist as described in the [Environment variables](#Environment-variables) section.
|
||||
3. cleanuperr with execute the following jobs:
|
||||
- the same queue cleaner as described [here](#Using-qBittorrents-built-in-feature)
|
||||
- a content blocker cron job at every 5 minutes that will mark files as unwanted/skipped if:
|
||||
- they are in the blacklist.
|
||||
- they are not in the whitelist.
|
||||
|
||||
## Usage
|
||||
|
||||
### Docker
|
||||
|
||||
```
|
||||
docker run \
|
||||
-e QuartzConfig__BlockedTorrentTrigger="0 0/10 * * * ?" \
|
||||
-e QBitConfig__Url="http://localhost:8080" \
|
||||
-e QBitConfig__Username="user" \
|
||||
-e QBitConfig__Password="pass" \
|
||||
-e SonarrConfig__Instances__0__Url="http://localhost:8989" \
|
||||
-e SonarrConfig__Instances__0__ApiKey="secret1" \
|
||||
-e SonarrConfig__Instances__1__Url="http://localhost:8990" \
|
||||
-e SonarrConfig__Instances__1__ApiKey="secret2" \
|
||||
docker run -d \
|
||||
-e TRIGGERS__QUEUECLEANER="0 0/5 * * * ?" \
|
||||
-e QBITTORRENT__ENABLED=true \
|
||||
-e QBITTORRENT__URL="http://localhost:8080" \
|
||||
-e QBITTORRENT__USERNAME="user" \
|
||||
-e QBITTORRENT__PASSWORD="pass" \
|
||||
-e SONARR__ENABLED=true \
|
||||
-e SONARR__INSTANCES__0__URL="http://localhost:8989" \
|
||||
-e SONARR__INSTANCES__0__APIKEY="secret1" \
|
||||
-e SONARR__INSTANCES__1__URL="http://localhost:8990" \
|
||||
-e SONARR__INSTANCES__1__APIKEY="secret2" \
|
||||
-e RADARR__ENABLED=true \
|
||||
-e RADARR__INSTANCES__0__URL="http://localhost:7878" \
|
||||
-e RADARR__INSTANCES__0__APIKEY="secret3" \
|
||||
-e RADARR__INSTANCES__1__URL="http://localhost:7879" \
|
||||
-e RADARR__INSTANCES__1__APIKEY="secret4" \
|
||||
...
|
||||
flaminel/cleanuperr:latest
|
||||
```
|
||||
|
||||
### Docker compose yaml
|
||||
|
||||
```
|
||||
version: "3.3"
|
||||
services:
|
||||
cleanuperr:
|
||||
environment:
|
||||
- LOGGING__LOGLEVEL__DEFAULT=Information
|
||||
|
||||
- TRIGGERS__QUEUECLEANER=0 0/5 * * * ?
|
||||
- TRIGGERS__CONTENTBLOCKER=0 0/5 * * * ?
|
||||
|
||||
- QUEUECLEANER__ENABLED=true
|
||||
|
||||
- CONTENTBLOCKER__ENABLED=true
|
||||
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
||||
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||
# OR
|
||||
# - CONTENTBLOCKER__WHITELIST__ENABLED=true
|
||||
# - CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist
|
||||
|
||||
- QBITTORRENT__ENABLED=true
|
||||
- QBITTORRENT__URL=http://localhost:8080
|
||||
- QBITTORRENT__USERNAME=user
|
||||
- QBITTORRENT__PASSWORD=pass
|
||||
# OR
|
||||
# - DELUGE__ENABLED=true
|
||||
# - DELUGE__URL=http://localhost:8112
|
||||
# - DELUGE__PASSWORD=testing
|
||||
# OR
|
||||
# - TRANSMISSION__ENABLED=true
|
||||
# - TRANSMISSION__URL=http://localhost:9091
|
||||
# - TRANSMISSION__USERNAME=test
|
||||
# - TRANSMISSION__PASSWORD=testing
|
||||
|
||||
- SONARR__ENABLED=true
|
||||
- SONARR__INSTANCES__0__URL=http://localhost:8989
|
||||
- SONARR__INSTANCES__0__APIKEY=secret1
|
||||
- SONARR__INSTANCES__1__URL=http://localhost:8990
|
||||
- SONARR__INSTANCES__1__APIKEY=secret2
|
||||
|
||||
- RADARR__ENABLED=true
|
||||
- RADARR__INSTANCES__0__URL=http://localhost:7878
|
||||
- RADARR__INSTANCES__0__APIKEY=secret3
|
||||
- RADARR__INSTANCES__1__URL=http://localhost:7879
|
||||
- RADARR__INSTANCES__1__APIKEY=secret4
|
||||
image: flaminel/cleanuperr:latest
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Required | Description | Default value |
|
||||
|---|---|---|---|
|
||||
| QuartzConfig__BlockedTorrentTrigger | No | Quartz cron trigger | 0 0/5 * * * ? |
|
||||
| QBitConfig__Url | Yes | qBittorrent instance url | http://localhost:8080 |
|
||||
| QBitConfig__Username | Yes | qBittorrent user | empty |
|
||||
| QBitConfig__Password | Yes | qBittorrent password | empty |
|
||||
| SonarrConfig__Instances__0__Url | Yes | First Sonarr instance url | http://localhost:8989 |
|
||||
| SonarrConfig__Instances__0__ApiKey | Yes | First Sonarr instance API key | empty |
|
||||
| LOGGING__LOGLEVEL__DEFAULT | No | Can be `Debug`, `Information`, `Warning` or `Error` | Information |
|
||||
|||||
|
||||
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) | 0 0/5 * * * ? |
|
||||
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) | 0 0/5 * * * ? |
|
||||
|||||
|
||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
|
||||
|||||
|
||||
| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker | false |
|
||||
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
|
||||
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url); Needs to be json compatible | empty |
|
||||
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
|
||||
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url); Needs to be json compatible | empty |
|
||||
|||||
|
||||
| QBITTORRENT__ENABLED | No | Enable or disable qBittorrent | true |
|
||||
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
|
||||
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
|
||||
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|
||||
|||||
|
||||
| DELUGE__ENABLED | No | Enable or disable Deluge | false |
|
||||
| DELUGE__URL | No | Deluge instance url | http://localhost:8080 |
|
||||
| DELUGE__PASSWORD | No | Deluge password | empty |
|
||||
|||||
|
||||
| TRANSMISSION__ENABLED | No | Enable or disable Transmission | true |
|
||||
| TRANSMISSION__URL | No | Transmission instance url | http://localhost:9091 |
|
||||
| TRANSMISSION__USERNAME | No | Transmission user | empty |
|
||||
| TRANSMISSION__PASSWORD | No | Transmission password | empty |
|
||||
|||||
|
||||
| SONARR__ENABLED | No | Whether Sonarr cleanup is enabled or not | true |
|
||||
| SONARR__INSTANCES__0__URL | Yes | First Sonarr instance url | http://localhost:8989 |
|
||||
| SONARR__INSTANCES__0__APIKEY | Yes | First Sonarr instance API key | empty |
|
||||
|||||
|
||||
| RADARR__ENABLED | No | Whether Radarr cleanup is enabled or not | false |
|
||||
| RADARR__INSTANCES__0__URL | Yes | First Radarr instance url | http://localhost:8989 |
|
||||
| RADARR__INSTANCES__0__APIKEY | Yes | First Radarr instance API key | empty |
|
||||
|
||||
#
|
||||
### To be noted
|
||||
|
||||
1. The blacklist and the whitelist can not be both enabled at the same time.
|
||||
2. The queue cleaner and content blocker can be enabled or disabled separately, if you want to run only one of them.
|
||||
3. Only one download client can be enabled at a time. If you have more than one download client, you should deploy multiple instances of cleanuperr.
|
||||
4. The blocklists (blacklist/whitelist) should have a single pattern on each line and supports the following:
|
||||
```
|
||||
*example // file name ends with "example"
|
||||
example* // file name starts with "example"
|
||||
*example* // file name has "example" in the name
|
||||
example // file name is exactly the word "example"
|
||||
<ANY_REGEX> // regex
|
||||
```
|
||||
5. Multiple Sonarr/Radarr instances can be specified using this format, where `<NUMBER>` starts from 0:
|
||||
```
|
||||
SONARR__INSTANCES__<NUMBER>__URL
|
||||
SONARR__INSTANCES__<NUMBER>__APIKEY
|
||||
```
|
||||
|
||||
#
|
||||
|
||||
Multiple Sonarr instances can be specified using this format:
|
||||
### Binaries (if you're not using Docker)
|
||||
|
||||
```
|
||||
SonarrConfig__Instances__<NUMBER>__Url
|
||||
SonarrConfig__Instances__<NUMBER>__ApiKey
|
||||
```
|
||||
|
||||
where `<NUMBER>` starts from 0.
|
||||
|
||||
#
|
||||
|
||||
### Binaries
|
||||
1. Download the binaries from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||
2. Extract them from the zip file.
|
||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [above](/README.md#environment-variables).
|
||||
## Extensions to block in qBittorrent
|
||||
<details>
|
||||
<summary>Extensions</summary>
|
||||
<pre><code>*.apk
|
||||
*.bat
|
||||
*.bin
|
||||
*.bmp
|
||||
*.cmd
|
||||
*.com
|
||||
*.db
|
||||
*.diz
|
||||
*.dll
|
||||
*.dmg
|
||||
*.etc
|
||||
*.exe
|
||||
*.gif
|
||||
*.htm
|
||||
*.html
|
||||
*.ico
|
||||
*.ini
|
||||
*.iso
|
||||
*.jar
|
||||
*.jpg
|
||||
*.js
|
||||
*.link
|
||||
*.lnk
|
||||
*.msi
|
||||
*.nfo
|
||||
*.perl
|
||||
*.php
|
||||
*.pl
|
||||
*.png
|
||||
*.ps1
|
||||
*.psc1
|
||||
*.psd1
|
||||
*.psm1
|
||||
*.py
|
||||
*.pyd
|
||||
*.rb
|
||||
*.readme
|
||||
*.reg
|
||||
*.run
|
||||
*.scr
|
||||
*.sh
|
||||
*.sql
|
||||
*.text
|
||||
*.thumb
|
||||
*.torrent
|
||||
*.txt
|
||||
*.url
|
||||
*.vbs
|
||||
*.wsf
|
||||
*.xml
|
||||
*.zipx
|
||||
*.7z
|
||||
*.bdjo
|
||||
*.bdmv
|
||||
*.bin
|
||||
*.bmp
|
||||
*.cci
|
||||
*.clpi
|
||||
*.crt
|
||||
*.dll
|
||||
*.exe
|
||||
*.html
|
||||
*.idx
|
||||
*.inf
|
||||
*.jar
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.lnk
|
||||
*.m4a
|
||||
*.mpls
|
||||
*.msi
|
||||
*.nfo
|
||||
*.pdf
|
||||
*.png
|
||||
*.rar
|
||||
*(sample).*
|
||||
*sample.mkv
|
||||
*sample.mp4
|
||||
*.sfv
|
||||
*.srt
|
||||
*.sub
|
||||
*.tbl
|
||||
Trailer.*
|
||||
*.txt
|
||||
*.url
|
||||
*.xig
|
||||
*.xml
|
||||
*.xrt
|
||||
*.zip
|
||||
*.zipx
|
||||
*.Lnk
|
||||
</code></pre>
|
||||
</details>
|
||||
|
||||
### Run as a Windows Service
|
||||
|
||||
Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
|
||||
519
blacklist
Normal file
519
blacklist
Normal file
@@ -0,0 +1,519 @@
|
||||
*(sample).*
|
||||
*.0xe
|
||||
*.73k
|
||||
*.73p
|
||||
*.7z
|
||||
*.89k
|
||||
*.89z
|
||||
*.8ck
|
||||
*.a7r
|
||||
*.ac
|
||||
*.acc
|
||||
*.ace
|
||||
*.acr
|
||||
*.actc
|
||||
*.action
|
||||
*.actm
|
||||
*.ade
|
||||
*.adp
|
||||
*.afmacro
|
||||
*.afmacros
|
||||
*.ahk
|
||||
*.ai
|
||||
*.aif
|
||||
*.air
|
||||
*.alz
|
||||
*.api
|
||||
*.apk
|
||||
*.app
|
||||
*.appimage
|
||||
*.applescript
|
||||
*.application
|
||||
*.appx
|
||||
*.arc
|
||||
*.arj
|
||||
*.arscript
|
||||
*.asb
|
||||
*.asp
|
||||
*.aspx
|
||||
*.aspx-exe
|
||||
*.atmx
|
||||
*.azw2
|
||||
*.ba_
|
||||
*.bak
|
||||
*.bas
|
||||
*.bash
|
||||
*.bat
|
||||
*.bdjo
|
||||
*.bdmv
|
||||
*.beam
|
||||
*.bin
|
||||
*.bmp
|
||||
*.bms
|
||||
*.bns
|
||||
*.bsa
|
||||
*.btm
|
||||
*.bz2
|
||||
*.c
|
||||
*.cab
|
||||
*.caction
|
||||
*.cci
|
||||
*.cda
|
||||
*.cdb
|
||||
*.cel
|
||||
*.celx
|
||||
*.cfs
|
||||
*.cgi
|
||||
*.cheat
|
||||
*.chm
|
||||
*.ckpt
|
||||
*.cla
|
||||
*.class
|
||||
*.clpi
|
||||
*.cmd
|
||||
*.cof
|
||||
*.coffee
|
||||
*.com
|
||||
*.command
|
||||
*.conf
|
||||
*.config
|
||||
*.cpl
|
||||
*.crt
|
||||
*.cs
|
||||
*.csh
|
||||
*.csharp
|
||||
*.csproj
|
||||
*.css
|
||||
*.csv
|
||||
*.cue
|
||||
*.cur
|
||||
*.cyw
|
||||
*.daemon
|
||||
*.dat
|
||||
*.data-00000-of-00001
|
||||
*.db
|
||||
*.deamon
|
||||
*.deb
|
||||
*.dek
|
||||
*.diz
|
||||
*.dld
|
||||
*.dll
|
||||
*.dmc
|
||||
*.dmg
|
||||
*.doc
|
||||
*.docb
|
||||
*.docm
|
||||
*.docx
|
||||
*.dot
|
||||
*.dotb
|
||||
*.dotm
|
||||
*.drv
|
||||
*.ds
|
||||
*.dw
|
||||
*.dword
|
||||
*.dxl
|
||||
*.e_e
|
||||
*.ear
|
||||
*.ebacmd
|
||||
*.ebm
|
||||
*.ebs
|
||||
*.ebs2
|
||||
*.ecf
|
||||
*.eham
|
||||
*.elf
|
||||
*.elf-so
|
||||
*.email
|
||||
*.emu
|
||||
*.epk
|
||||
*.es
|
||||
*.esh
|
||||
*.etc
|
||||
*.ex4
|
||||
*.ex5
|
||||
*.ex_
|
||||
*.exe
|
||||
*.exe-only
|
||||
*.exe-service
|
||||
*.exe-small
|
||||
*.exe1
|
||||
*.exopc
|
||||
*.exz
|
||||
*.ezs
|
||||
*.ezt
|
||||
*.fas
|
||||
*.fba
|
||||
*.fky
|
||||
*.flac
|
||||
*.flatpak
|
||||
*.flv
|
||||
*.fpi
|
||||
*.frs
|
||||
*.fxp
|
||||
*.gadget
|
||||
*.gat
|
||||
*.gif
|
||||
*.gifv
|
||||
*.gm9
|
||||
*.gpe
|
||||
*.gpu
|
||||
*.gs
|
||||
*.gz
|
||||
*.h5
|
||||
*.ham
|
||||
*.hex
|
||||
*.hlp
|
||||
*.hms
|
||||
*.hpf
|
||||
*.hta
|
||||
*.hta-psh
|
||||
*.htaccess
|
||||
*.htm
|
||||
*.html
|
||||
*.icd
|
||||
*.icns
|
||||
*.ico
|
||||
*.idx
|
||||
*.iim
|
||||
*.img
|
||||
*.index
|
||||
*.inf
|
||||
*.ini
|
||||
*.ink
|
||||
*.ins
|
||||
*.ipa
|
||||
*.ipf
|
||||
*.ipk
|
||||
*.ipsw
|
||||
*.iqylink
|
||||
*.iso
|
||||
*.isp
|
||||
*.isu
|
||||
*.ita
|
||||
*.izh
|
||||
*.izma ace
|
||||
*.jar
|
||||
*.java
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.js
|
||||
*.js_be
|
||||
*.js_le
|
||||
*.jse
|
||||
*.jsf
|
||||
*.json
|
||||
*.jsp
|
||||
*.jsx
|
||||
*.kix
|
||||
*.ksh
|
||||
*.kx
|
||||
*.lck
|
||||
*.ldb
|
||||
*.lib
|
||||
*.link
|
||||
*.lnk
|
||||
*.lo
|
||||
*.lock
|
||||
*.log
|
||||
*.loop-vbs
|
||||
*.ls
|
||||
*.m3u
|
||||
*.m4a
|
||||
*.mac
|
||||
*.macho
|
||||
*.mamc
|
||||
*.manifest
|
||||
*.mcr
|
||||
*.md
|
||||
*.mda
|
||||
*.mdb
|
||||
*.mde
|
||||
*.mdf
|
||||
*.mdn
|
||||
*.mdt
|
||||
*.mel
|
||||
*.mem
|
||||
*.meta
|
||||
*.mgm
|
||||
*.mhm
|
||||
*.mht
|
||||
*.mhtml
|
||||
*.mid
|
||||
*.mio
|
||||
*.mlappinstall
|
||||
*.mlx
|
||||
*.mm
|
||||
*.mobileconfig
|
||||
*.model
|
||||
*.moo
|
||||
*.mp3
|
||||
*.mpa
|
||||
*.mpk
|
||||
*.mpls
|
||||
*.mrc
|
||||
*.mrp
|
||||
*.ms
|
||||
*.msc
|
||||
*.msh
|
||||
*.msh1
|
||||
*.msh1xml
|
||||
*.msh2
|
||||
*.msh2xml
|
||||
*.mshxml
|
||||
*.msi
|
||||
*.msi-nouac
|
||||
*.msix
|
||||
*.msl
|
||||
*.msp
|
||||
*.mst
|
||||
*.msu
|
||||
*.mxe
|
||||
*.n
|
||||
*.ncl
|
||||
*.net
|
||||
*.nexe
|
||||
*.nfo
|
||||
*.nrg
|
||||
*.num
|
||||
*.nzb.bz2
|
||||
*.nzb.gz
|
||||
*.nzbs
|
||||
*.ocx
|
||||
*.odt
|
||||
*.ore
|
||||
*.ost
|
||||
*.osx
|
||||
*.osx-app
|
||||
*.otm
|
||||
*.out
|
||||
*.ova
|
||||
*.p
|
||||
*.paf
|
||||
*.pak
|
||||
*.pb
|
||||
*.pcd
|
||||
*.pdb
|
||||
*.pdf
|
||||
*.pea
|
||||
*.perl
|
||||
*.pex
|
||||
*.phar
|
||||
*.php
|
||||
*.php5
|
||||
*.pif
|
||||
*.pkg
|
||||
*.pl
|
||||
*.plsc
|
||||
*.plx
|
||||
*.png
|
||||
*.pol
|
||||
*.pot
|
||||
*.potm
|
||||
*.powershell
|
||||
*.ppam
|
||||
*.ppkg
|
||||
*.pps
|
||||
*.ppsm
|
||||
*.ppt
|
||||
*.pptm
|
||||
*.pptx
|
||||
*.prc
|
||||
*.prg
|
||||
*.ps
|
||||
*.ps1
|
||||
*.ps1xml
|
||||
*.ps2
|
||||
*.ps2xml
|
||||
*.psc1
|
||||
*.psc2
|
||||
*.psd
|
||||
*.psd1
|
||||
*.psh
|
||||
*.psh-cmd
|
||||
*.psh-net
|
||||
*.psh-reflection
|
||||
*.psm1
|
||||
*.pst
|
||||
*.pt
|
||||
*.pvd
|
||||
*.pwc
|
||||
*.pxo
|
||||
*.py
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.pyo
|
||||
*.python
|
||||
*.pyz
|
||||
*.qit
|
||||
*.qpx
|
||||
*.ram
|
||||
*.rar
|
||||
*.raw
|
||||
*.rb
|
||||
*.rbf
|
||||
*.rbx
|
||||
*.readme
|
||||
*.reg
|
||||
*.resources
|
||||
*.resx
|
||||
*.rfs
|
||||
*.rfu
|
||||
*.rgs
|
||||
*.rm
|
||||
*.rox
|
||||
*.rpg
|
||||
*.rpj
|
||||
*.rpm
|
||||
*.ruby
|
||||
*.run
|
||||
*.rxe
|
||||
*.s2a
|
||||
*.sample
|
||||
*.sapk
|
||||
*.savedmodel
|
||||
*.sbs
|
||||
*.sca
|
||||
*.scar
|
||||
*.scb
|
||||
*.scf
|
||||
*.scpt
|
||||
*.scptd
|
||||
*.scr
|
||||
*.script
|
||||
*.sct
|
||||
*.seed
|
||||
*.server
|
||||
*.service
|
||||
*.sfv
|
||||
*.sh
|
||||
*.shb
|
||||
*.shell
|
||||
*.shortcut
|
||||
*.shs
|
||||
*.shtml
|
||||
*.sit
|
||||
*.sitx
|
||||
*.sk
|
||||
*.sldm
|
||||
*.sln
|
||||
*.smm
|
||||
*.snap
|
||||
*.snd
|
||||
*.spr
|
||||
*.sql
|
||||
*.sqx
|
||||
*.srec
|
||||
*.srt
|
||||
*.ssm
|
||||
*.sts
|
||||
*.sub
|
||||
*.svg
|
||||
*.swf
|
||||
*.sys
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.tbl
|
||||
*.tbz
|
||||
*.tcp
|
||||
*.text
|
||||
*.tf
|
||||
*.tgz
|
||||
*.thm
|
||||
*.thmx
|
||||
*.thumb
|
||||
*.tiapp
|
||||
*.tif
|
||||
*.tiff
|
||||
*.tipa
|
||||
*.tmp
|
||||
*.tms
|
||||
*.toast
|
||||
*.torrent
|
||||
*.tpk
|
||||
*.txt
|
||||
*.u3p
|
||||
*.udf
|
||||
*.upk
|
||||
*.upx
|
||||
*.url
|
||||
*.uvm
|
||||
*.uw8
|
||||
*.vb
|
||||
*.vba
|
||||
*.vba-exe
|
||||
*.vba-psh
|
||||
*.vbapplication
|
||||
*.vbe
|
||||
*.vbs
|
||||
*.vbscript
|
||||
*.vbscript
|
||||
*.vcd
|
||||
*.vdo
|
||||
*.vexe
|
||||
*.vhd
|
||||
*.vhdx
|
||||
*.vlx
|
||||
*.vm
|
||||
*.vmdk
|
||||
*.vob
|
||||
*.vocab
|
||||
*.vpm
|
||||
*.vxp
|
||||
*.war
|
||||
*.wav
|
||||
*.wbk
|
||||
*.wcm
|
||||
*.webm
|
||||
*.widget
|
||||
*.wim
|
||||
*.wiz
|
||||
*.wma
|
||||
*.workflow
|
||||
*.wpk
|
||||
*.wpl
|
||||
*.wpm
|
||||
*.wps
|
||||
*.ws
|
||||
*.wsc
|
||||
*.wsf
|
||||
*.wsh
|
||||
*.x86
|
||||
*.x86_64
|
||||
*.xaml
|
||||
*.xap
|
||||
*.xbap
|
||||
*.xbe
|
||||
*.xex
|
||||
*.xig
|
||||
*.xla
|
||||
*.xlam
|
||||
*.xll
|
||||
*.xlm
|
||||
*.xls
|
||||
*.xlsb
|
||||
*.xlsm
|
||||
*.xlsx
|
||||
*.xlt
|
||||
*.xltb
|
||||
*.xltm
|
||||
*.xlw
|
||||
*.xml
|
||||
*.xqt
|
||||
*.xrt
|
||||
*.xys
|
||||
*.xz
|
||||
*.ygh
|
||||
*.z
|
||||
*.zip
|
||||
*.zipx
|
||||
*.zl9
|
||||
*.zoo
|
||||
*sample.avchd
|
||||
*sample.avi
|
||||
*sample.mkv
|
||||
*sample.mov
|
||||
*sample.mp4
|
||||
*sample.webm
|
||||
*sample.wmv
|
||||
Trailer.*
|
||||
VOSTFR
|
||||
api
|
||||
51
blacklist_permissive
Normal file
51
blacklist_permissive
Normal file
@@ -0,0 +1,51 @@
|
||||
*.apk
|
||||
*.bat
|
||||
*.bin
|
||||
*.bmp
|
||||
*.cmd
|
||||
*.com
|
||||
*.db
|
||||
*.diz
|
||||
*.dll
|
||||
*.dmg
|
||||
*.etc
|
||||
*.exe
|
||||
*.gif
|
||||
*.htm
|
||||
*.html
|
||||
*.ico
|
||||
*.ini
|
||||
*.iso
|
||||
*.jar
|
||||
*.jpg
|
||||
*.js
|
||||
*.link
|
||||
*.lnk
|
||||
*.msi
|
||||
*.nfo
|
||||
*.perl
|
||||
*.php
|
||||
*.pl
|
||||
*.png
|
||||
*.ps1
|
||||
*.psc1
|
||||
*.psd1
|
||||
*.psm1
|
||||
*.py
|
||||
*.pyd
|
||||
*.rb
|
||||
*.readme
|
||||
*.reg
|
||||
*.run
|
||||
*.scr
|
||||
*.sh
|
||||
*.sql
|
||||
*.text
|
||||
*.thumb
|
||||
*.torrent
|
||||
*.txt
|
||||
*.url
|
||||
*.vbs
|
||||
*.wsf
|
||||
*.xml
|
||||
*.zipx
|
||||
8
code/Common/Configuration/ArrConfig.cs
Normal file
8
code/Common/Configuration/ArrConfig.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public abstract record ArrConfig
|
||||
{
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public required List<ArrInstance> Instances { get; init; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class SonarrInstance
|
||||
public sealed class ArrInstance
|
||||
{
|
||||
public required Uri Url { get; set; }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Common.Configuration.ContentBlocker;
|
||||
|
||||
public sealed record ContentBlockerConfig : IConfig
|
||||
{
|
||||
public const string SectionName = "ContentBlocker";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public PatternConfig? Blacklist { get; init; }
|
||||
|
||||
public PatternConfig? Whitelist { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Blacklist is null && Whitelist is null)
|
||||
{
|
||||
throw new Exception("content blocker is enabled, but both blacklist and whitelist are missing");
|
||||
}
|
||||
|
||||
if (Blacklist?.Enabled is true && Whitelist?.Enabled is true)
|
||||
{
|
||||
throw new Exception("only one exclusion (blacklist/whitelist) list is allowed");
|
||||
}
|
||||
|
||||
if (Blacklist?.Enabled is true && string.IsNullOrEmpty(Blacklist.Path))
|
||||
{
|
||||
throw new Exception("blacklist path is required");
|
||||
}
|
||||
|
||||
if (Whitelist?.Enabled is true && string.IsNullOrEmpty(Whitelist.Path))
|
||||
{
|
||||
throw new Exception("blacklist path is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Common.Configuration.ContentBlocker;
|
||||
|
||||
public sealed record PatternConfig
|
||||
{
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
27
code/Common/Configuration/DelugeConfig.cs
Normal file
27
code/Common/Configuration/DelugeConfig.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Security;
|
||||
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed record DelugeConfig : IConfig
|
||||
{
|
||||
public const string SectionName = "Deluge";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public Uri? Url { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Url is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Url));
|
||||
}
|
||||
}
|
||||
}
|
||||
6
code/Common/Configuration/IConfig.cs
Normal file
6
code/Common/Configuration/IConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public interface IConfig
|
||||
{
|
||||
void Validate();
|
||||
}
|
||||
@@ -1,10 +1,29 @@
|
||||
namespace Common.Configuration;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public sealed class QBitConfig
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class QBitConfig : IConfig
|
||||
{
|
||||
public required Uri Url { get; set; }
|
||||
public const string SectionName = "qBittorrent";
|
||||
|
||||
public required string Username { get; set; }
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public required string Password { get; set; }
|
||||
public Uri? Url { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Url is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Url));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class QuartzConfig
|
||||
{
|
||||
public required string BlockedTorrentTrigger { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Common.Configuration.QueueCleaner;
|
||||
|
||||
public sealed record QueueCleanerConfig
|
||||
{
|
||||
public const string SectionName = "QueueCleaner";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
}
|
||||
6
code/Common/Configuration/RadarrConfig.cs
Normal file
6
code/Common/Configuration/RadarrConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed record RadarrConfig : ArrConfig
|
||||
{
|
||||
public const string SectionName = "Radarr";
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class SonarrConfig
|
||||
public sealed record SonarrConfig : ArrConfig
|
||||
{
|
||||
public required List<SonarrInstance> Instances { get; set; }
|
||||
public const string SectionName = "Sonarr";
|
||||
}
|
||||
27
code/Common/Configuration/TransmissionConfig.cs
Normal file
27
code/Common/Configuration/TransmissionConfig.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public record TransmissionConfig
|
||||
{
|
||||
public const string SectionName = "Transmission";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public Uri? Url { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Url is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(Url));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
code/Common/Configuration/TriggersConfig.cs
Normal file
10
code/Common/Configuration/TriggersConfig.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class TriggersConfig
|
||||
{
|
||||
public const string SectionName = "Triggers";
|
||||
|
||||
public required string QueueCleaner { get; init; }
|
||||
|
||||
public required string ContentBlocker { get; init; }
|
||||
}
|
||||
@@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
7
code/Domain/Enums/BlocklistType.cs
Normal file
7
code/Domain/Enums/BlocklistType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Domain.Enums;
|
||||
|
||||
public enum BlocklistType
|
||||
{
|
||||
Blacklist,
|
||||
Whitelist
|
||||
}
|
||||
9
code/Domain/Enums/InstanceType.cs
Normal file
9
code/Domain/Enums/InstanceType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Domain.Enums;
|
||||
|
||||
public enum InstanceType
|
||||
{
|
||||
Sonarr,
|
||||
Radarr,
|
||||
Lidarr,
|
||||
Readarr
|
||||
}
|
||||
7
code/Domain/Models/Arr/Queue/QueueListResponse.cs
Normal file
7
code/Domain/Models/Arr/Queue/QueueListResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Domain.Arr.Queue;
|
||||
|
||||
public record QueueListResponse
|
||||
{
|
||||
public required int TotalRecords { get; init; }
|
||||
public required IReadOnlyList<QueueRecord> Records { get; init; }
|
||||
}
|
||||
15
code/Domain/Models/Arr/Queue/QueueRecord.cs
Normal file
15
code/Domain/Models/Arr/Queue/QueueRecord.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Domain.Arr.Queue;
|
||||
|
||||
public record QueueRecord
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int EpisodeId { get; init; }
|
||||
public int MovieId { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string Status { get; init; }
|
||||
public string TrackedDownloadStatus { get; init; }
|
||||
public string TrackedDownloadState { get; init; }
|
||||
public required string DownloadId { get; init; }
|
||||
public required string Protocol { get; init; }
|
||||
public required int Id { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public class DelugeClientException : Exception
|
||||
{
|
||||
public DelugeClientException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public sealed class DelugeLoginException : DelugeClientException
|
||||
{
|
||||
public DelugeLoginException() : base("login failed")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public sealed class DelugeLogoutException : DelugeClientException
|
||||
{
|
||||
public DelugeLogoutException() : base("logout failed")
|
||||
{
|
||||
}
|
||||
}
|
||||
32
code/Domain/Models/Deluge/Request/DelugeRequest.cs
Normal file
32
code/Domain/Models/Deluge/Request/DelugeRequest.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Request;
|
||||
|
||||
public class DelugeRequest
|
||||
{
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public int RequestId { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "method")]
|
||||
public String Method { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "params")]
|
||||
public List<Object> Params { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public NullValueHandling NullValueHandling { get; set; }
|
||||
|
||||
public DelugeRequest(int requestId, String method, params object[] parameters)
|
||||
{
|
||||
RequestId = requestId;
|
||||
Method = method;
|
||||
Params = new List<Object>();
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
Params.AddRange(parameters);
|
||||
}
|
||||
|
||||
NullValueHandling = NullValueHandling.Include;
|
||||
}
|
||||
}
|
||||
12
code/Domain/Models/Deluge/Response/DelugeContents.cs
Normal file
12
code/Domain/Models/Deluge/Response/DelugeContents.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeContents
|
||||
{
|
||||
[JsonPropertyName("contents")]
|
||||
public Dictionary<string, DelugeFileOrDirectory> Contents { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } // Always "dir" for the root
|
||||
}
|
||||
12
code/Domain/Models/Deluge/Response/DelugeError.cs
Normal file
12
code/Domain/Models/Deluge/Response/DelugeError.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeError
|
||||
{
|
||||
[JsonProperty(PropertyName = "message")]
|
||||
public String Message { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public int Code { get; set; }
|
||||
}
|
||||
33
code/Domain/Models/Deluge/Response/DelugeFileOrDirectory.cs
Normal file
33
code/Domain/Models/Deluge/Response/DelugeFileOrDirectory.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public class DelugeFileOrDirectory
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } // "file" or "dir"
|
||||
|
||||
[JsonPropertyName("contents")]
|
||||
public Dictionary<string, DelugeFileOrDirectory>? Contents { get; set; } // Recursive property for directories
|
||||
|
||||
[JsonPropertyName("index")]
|
||||
public required int Index { get; set; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int? Size { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int? Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("progress")]
|
||||
public double? Progress { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public required int Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("progresses")]
|
||||
public List<double> Progresses { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeMinimalStatus
|
||||
{
|
||||
public string? Hash { get; set; }
|
||||
}
|
||||
15
code/Domain/Models/Deluge/Response/DelugeResponse.cs
Normal file
15
code/Domain/Models/Deluge/Response/DelugeResponse.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeResponse<T>
|
||||
{
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public int ResponseId { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "result")]
|
||||
public T? Result { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "error")]
|
||||
public DelugeError? Error { get; set; }
|
||||
}
|
||||
30
code/Domain/Models/Deluge/Response/DelugeTorrent.cs
Normal file
30
code/Domain/Models/Deluge/Response/DelugeTorrent.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public record DelugeTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "comment")]
|
||||
public string Comment { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "is_seed")]
|
||||
public bool IsSeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "hash")]
|
||||
public string Hash { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "paused")]
|
||||
public bool Paused { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "ratio")]
|
||||
public double Ratio { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "label")]
|
||||
public string Label { get; set; }
|
||||
}
|
||||
110
code/Domain/Models/Deluge/Response/DelugeTorrentExtended.cs
Normal file
110
code/Domain/Models/Deluge/Response/DelugeTorrentExtended.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeTorrentExtended : DelugeTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "total_done")]
|
||||
public long TotalDone { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_payload_download")]
|
||||
public long TotalPayloadDownload { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_uploaded")]
|
||||
public long TotalUploaded { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "next_announce")]
|
||||
public int NextAnnounce { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tracker_status")]
|
||||
public string TrackerStatus { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_pieces")]
|
||||
public int NumPieces { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "piece_length")]
|
||||
public long PieceLength { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "is_auto_managed")]
|
||||
public bool IsAutoManaged { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "active_time")]
|
||||
public long ActiveTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seeding_time")]
|
||||
public long SeedingTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "time_since_transfer")]
|
||||
public long TimeSinceTransfer { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seed_rank")]
|
||||
public int SeedRank { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "last_seen_complete")]
|
||||
public long LastSeenComplete { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "completed_time")]
|
||||
public long CompletedTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "owner")] public string Owner { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "public")]
|
||||
public bool Public { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "shared")]
|
||||
public bool Shared { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "queue")] public int Queue { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_wanted")]
|
||||
public long TotalWanted { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "state")] public string State { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "progress")]
|
||||
public float Progress { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_seeds")]
|
||||
public int NumSeeds { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_seeds")]
|
||||
public int TotalSeeds { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_peers")]
|
||||
public int NumPeers { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_peers")]
|
||||
public int TotalPeers { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_payload_rate")]
|
||||
public long DownloadPayloadRate { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "upload_payload_rate")]
|
||||
public long UploadPayloadRate { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "eta")] public long Eta { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "distributed_copies")]
|
||||
public float DistributedCopies { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "time_added")]
|
||||
public int TimeAdded { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tracker_host")]
|
||||
public string TrackerHost { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_location")]
|
||||
public string DownloadLocation { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_remaining")]
|
||||
public long TotalRemaining { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "max_download_speed")]
|
||||
public long MaxDownloadSpeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "max_upload_speed")]
|
||||
public long MaxUploadSpeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seeds_peers_ratio")]
|
||||
public float SeedsPeersRatio { get; set; }
|
||||
}
|
||||
8
code/Domain/Models/Radarr/RadarrCommand.cs
Normal file
8
code/Domain/Models/Radarr/RadarrCommand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Radarr;
|
||||
|
||||
public sealed record RadarrCommand
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required HashSet<int> MovieIds { get; init; }
|
||||
}
|
||||
8
code/Domain/Models/Sonarr/SonarrCommand.cs
Normal file
8
code/Domain/Models/Sonarr/SonarrCommand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Sonarr;
|
||||
|
||||
public sealed record SonarrCommand
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required int SeriesId { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record CustomFormat(
|
||||
int Id,
|
||||
string Name
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record Language(
|
||||
int Id,
|
||||
string Name
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record QueueListResponse(
|
||||
int Page,
|
||||
int PageSize,
|
||||
string SortKey,
|
||||
string SortDirection,
|
||||
int TotalRecords,
|
||||
IReadOnlyList<Record> Records
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record Record(
|
||||
int SeriesId,
|
||||
int EpisodeId,
|
||||
int SeasonNumber,
|
||||
IReadOnlyList<Language> Languages,
|
||||
IReadOnlyList<CustomFormat> CustomFormats,
|
||||
int CustomFormatScore,
|
||||
long Size,
|
||||
string Title,
|
||||
long Sizeleft,
|
||||
string Timeleft,
|
||||
DateTime EstimatedCompletionTime,
|
||||
DateTime Added,
|
||||
string Status,
|
||||
string TrackedDownloadStatus,
|
||||
string TrackedDownloadState,
|
||||
IReadOnlyList<StatusMessage> StatusMessages,
|
||||
string DownloadId,
|
||||
string Protocol,
|
||||
string DownloadClient,
|
||||
bool DownloadClientHasPostImportCategory,
|
||||
string Indexer,
|
||||
string OutputPath,
|
||||
bool EpisodeHasFile,
|
||||
int Id
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record Revision(
|
||||
int Version,
|
||||
int Real,
|
||||
bool IsRepack
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Domain.Sonarr.Queue;
|
||||
|
||||
public record StatusMessage(
|
||||
string Title,
|
||||
IReadOnlyList<string> Messages
|
||||
);
|
||||
@@ -1,61 +0,0 @@
|
||||
using Common.Configuration;
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.BlockedTorrent;
|
||||
|
||||
namespace Executable;
|
||||
using Quartz;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddLogging(builder => builder.ClearProviders().AddConsole())
|
||||
.AddHttpClient()
|
||||
.AddConfiguration(configuration)
|
||||
.AddServices()
|
||||
.AddQuartzServices(configuration);
|
||||
|
||||
private static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.Configure<QuartzConfig>(configuration.GetSection(nameof(QuartzConfig)))
|
||||
.Configure<QBitConfig>(configuration.GetSection(nameof(QBitConfig)))
|
||||
.Configure<SonarrConfig>(configuration.GetSection(nameof(SonarrConfig)));
|
||||
|
||||
private static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddTransient<BlockedTorrentJob>()
|
||||
.AddTransient<BlockedTorrentHandler>();
|
||||
|
||||
private static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddQuartz(q =>
|
||||
{
|
||||
QuartzConfig? config = configuration.GetRequiredSection(nameof(QuartzConfig)).Get<QuartzConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException("Quartz configuration is null");
|
||||
}
|
||||
|
||||
q.AddBlockedTorrentJob(config.BlockedTorrentTrigger);
|
||||
})
|
||||
.AddQuartzHostedService(opt =>
|
||||
{
|
||||
opt.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
private static void AddBlockedTorrentJob(this IServiceCollectionQuartzConfigurator q, string trigger)
|
||||
{
|
||||
q.AddJob<BlockedTorrentJob>(opts =>
|
||||
{
|
||||
opts.WithIdentity(nameof(BlockedTorrentJob));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(BlockedTorrentJob))
|
||||
.WithIdentity($"{nameof(BlockedTorrentJob)}-trigger")
|
||||
.WithCronSchedule(trigger);
|
||||
});
|
||||
}
|
||||
}
|
||||
16
code/Executable/DependencyInjection/ConfigurationDI.cs
Normal file
16
code/Executable/DependencyInjection/ConfigurationDI.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class ConfigurationDI
|
||||
{
|
||||
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
|
||||
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
|
||||
.Configure<DelugeConfig>(configuration.GetSection(DelugeConfig.SectionName))
|
||||
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
|
||||
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
|
||||
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName));
|
||||
}
|
||||
50
code/Executable/DependencyInjection/MainDI.cs
Normal file
50
code/Executable/DependencyInjection/MainDI.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Net;
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
using Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class MainDI
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddLogging(builder => builder.ClearProviders().AddConsole())
|
||||
.AddHttpClients()
|
||||
.AddConfiguration(configuration)
|
||||
.AddServices()
|
||||
.AddQuartzServices(configuration);
|
||||
|
||||
private static IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
// add default HttpClient
|
||||
services.AddHttpClient();
|
||||
|
||||
// add Deluge HttpClient
|
||||
services
|
||||
.AddHttpClient(nameof(DelugeService), x =>
|
||||
{
|
||||
x.Timeout = TimeSpan.FromSeconds(5);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(_ =>
|
||||
{
|
||||
return new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
UseCookies = true,
|
||||
CookieContainer = new CookieContainer(),
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
99
code/Executable/DependencyInjection/QuartzDI.cs
Normal file
99
code/Executable/DependencyInjection/QuartzDI.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
using Quartz;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class QuartzDI
|
||||
{
|
||||
public static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddQuartz(q =>
|
||||
{
|
||||
TriggersConfig? config = configuration
|
||||
.GetRequiredSection(TriggersConfig.SectionName)
|
||||
.Get<TriggersConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException("triggers configuration is null");
|
||||
}
|
||||
|
||||
q.AddQueueCleanerJob(configuration, config.QueueCleaner);
|
||||
q.AddContentBlockerJob(configuration, config.ContentBlocker);
|
||||
})
|
||||
.AddQuartzHostedService(opt =>
|
||||
{
|
||||
opt.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
private static void AddQueueCleanerJob(
|
||||
this IServiceCollectionQuartzConfigurator q,
|
||||
IConfiguration configuration,
|
||||
string trigger
|
||||
)
|
||||
{
|
||||
QueueCleanerConfig? config = configuration
|
||||
.GetRequiredSection(QueueCleanerConfig.SectionName)
|
||||
.Get<QueueCleanerConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException($"{nameof(QueueCleaner)} configuration is null");
|
||||
}
|
||||
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
q.AddJob<QueueCleanerJob>(opts =>
|
||||
{
|
||||
opts.WithIdentity(nameof(QueueCleanerJob));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(QueueCleanerJob))
|
||||
.WithIdentity($"{nameof(QueueCleanerJob)}-trigger")
|
||||
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddContentBlockerJob(
|
||||
this IServiceCollectionQuartzConfigurator q,
|
||||
IConfiguration configuration,
|
||||
string trigger
|
||||
)
|
||||
{
|
||||
ContentBlockerConfig? config = configuration
|
||||
.GetRequiredSection(ContentBlockerConfig.SectionName)
|
||||
.Get<ContentBlockerConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException($"{nameof(ContentBlocker)} configuration is null");
|
||||
}
|
||||
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
q.AddJob<ContentBlockerJob>(opts =>
|
||||
{
|
||||
opts.WithIdentity(nameof(ContentBlockerJob));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(ContentBlockerJob))
|
||||
.WithIdentity($"{nameof(ContentBlockerJob)}-trigger")
|
||||
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
|
||||
});
|
||||
}
|
||||
}
|
||||
29
code/Executable/DependencyInjection/ServicesDI.cs
Normal file
29
code/Executable/DependencyInjection/ServicesDI.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
using Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class ServicesDI
|
||||
{
|
||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddTransient<SonarrClient>()
|
||||
.AddTransient<RadarrClient>()
|
||||
.AddTransient<QueueCleanerJob>()
|
||||
.AddTransient<ContentBlockerJob>()
|
||||
.AddTransient<QueueCleaner>()
|
||||
.AddTransient<ContentBlocker>()
|
||||
.AddTransient<FilenameEvaluator>()
|
||||
.AddTransient<QBitService>()
|
||||
.AddTransient<DelugeService>()
|
||||
.AddTransient<TransmissionService>()
|
||||
.AddTransient<ArrQueueIterator>()
|
||||
.AddTransient<DownloadServiceFactory>()
|
||||
.AddSingleton<BlocklistProvider>();
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Infrastructure.Verticals.BlockedTorrent;
|
||||
using Quartz;
|
||||
|
||||
namespace Executable.Jobs;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public sealed class BlockedTorrentJob : IJob
|
||||
{
|
||||
private ILogger<BlockedTorrentJob> _logger;
|
||||
private BlockedTorrentHandler _handler;
|
||||
|
||||
public BlockedTorrentJob(ILogger<BlockedTorrentJob> logger, BlockedTorrentHandler handler)
|
||||
{
|
||||
_logger = logger;
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _handler.HandleAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{nameof(BlockedTorrentJob)} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
32
code/Executable/Jobs/ContentBlockerJob.cs
Normal file
32
code/Executable/Jobs/ContentBlockerJob.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Quartz;
|
||||
|
||||
namespace Executable.Jobs;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public sealed class ContentBlockerJob : IJob
|
||||
{
|
||||
private readonly ILogger<QueueCleanerJob> _logger;
|
||||
private readonly ContentBlocker _contentBlocker;
|
||||
|
||||
public ContentBlockerJob(
|
||||
ILogger<QueueCleanerJob> logger,
|
||||
ContentBlocker contentBlocker
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_contentBlocker = contentBlocker;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _contentBlocker.ExecuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{nameof(ContentBlockerJob)} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
32
code/Executable/Jobs/QueueCleanerJob.cs
Normal file
32
code/Executable/Jobs/QueueCleanerJob.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
using Quartz;
|
||||
|
||||
namespace Executable.Jobs;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public sealed class QueueCleanerJob : IJob
|
||||
{
|
||||
private readonly ILogger<QueueCleanerJob> _logger;
|
||||
private readonly QueueCleaner _queueCleaner;
|
||||
|
||||
public QueueCleanerJob(
|
||||
ILogger<QueueCleanerJob> logger,
|
||||
QueueCleaner queueCleaner
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_queueCleaner = queueCleaner;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _queueCleaner.ExecuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{nameof(QueueCleanerJob)} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Executable;
|
||||
using Executable.DependencyInjection;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
|
||||
@@ -1,11 +1,63 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Default": "Debug",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Quartz": "Warning",
|
||||
"System.Net.Http.HttpClient": "Error"
|
||||
}
|
||||
},
|
||||
"QuartzConfig": {
|
||||
"BlockedTorrentTrigger": "0 0/1 * * * ?"
|
||||
"Triggers": {
|
||||
"QueueCleaner": "0/10 * * * * ?",
|
||||
"ContentBlocker": "0/10 * * * * ?"
|
||||
},
|
||||
"ContentBlocker": {
|
||||
"Enabled": false,
|
||||
"Blacklist": {
|
||||
"Enabled": false,
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||
},
|
||||
"Whitelist": {
|
||||
"Enabled": false,
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist"
|
||||
}
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true
|
||||
},
|
||||
"qBittorrent": {
|
||||
"Enabled": true,
|
||||
"Url": "http://localhost:8080",
|
||||
"Username": "test",
|
||||
"Password": "testing"
|
||||
},
|
||||
"Deluge": {
|
||||
"Enabled": false,
|
||||
"Url": "http://localhost:8112",
|
||||
"Password": "testing"
|
||||
},
|
||||
"Transmission": {
|
||||
"Enabled": false,
|
||||
"Url": "http://localhost:9091",
|
||||
"Username": "test",
|
||||
"Password": "testing"
|
||||
},
|
||||
"Sonarr": {
|
||||
"Enabled": true,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:8989",
|
||||
"ApiKey": "96736c3eb3144936b8f1d62d27be8cee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": true,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:7878",
|
||||
"ApiKey": "705b553732ab4167ab23909305d60600"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,20 +7,57 @@
|
||||
"System.Net.Http.HttpClient": "Error"
|
||||
}
|
||||
},
|
||||
"QuartzConfig": {
|
||||
"BlockedTorrentTrigger": "0 0/5 * * * ?"
|
||||
"Triggers": {
|
||||
"QueueCleaner": "0 0/5 * * * ?",
|
||||
"ContentBlocker": "0 0/5 * * * ?"
|
||||
},
|
||||
"QBitConfig": {
|
||||
"ContentBlocker": {
|
||||
"Enabled": false,
|
||||
"Blacklist": {
|
||||
"Enabled": false,
|
||||
"Path": ""
|
||||
},
|
||||
"Whitelist": {
|
||||
"Enabled": false,
|
||||
"Path": ""
|
||||
}
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true
|
||||
},
|
||||
"qBittorrent": {
|
||||
"Enabled": true,
|
||||
"Url": "http://localhost:8080",
|
||||
"Username": "",
|
||||
"Password": ""
|
||||
},
|
||||
"SonarrConfig": {
|
||||
"Deluge": {
|
||||
"Enabled": false,
|
||||
"Url": "http://localhost:8112",
|
||||
"Password": "testing"
|
||||
},
|
||||
"Transmission": {
|
||||
"Enabled": false,
|
||||
"Url": "http://localhost:9091",
|
||||
"Username": "test",
|
||||
"Password": "testing"
|
||||
},
|
||||
"Sonarr": {
|
||||
"Enabled": true,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:8989",
|
||||
"ApiKey": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": false,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:7878",
|
||||
"ApiKey": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FLM.Transmission" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.1" />
|
||||
<PackageReference Include="QBittorrent.Client" Version="1.9.24285.1" />
|
||||
|
||||
77
code/Infrastructure/Verticals/Arr/ArrClient.cs
Normal file
77
code/Infrastructure/Verticals/Arr/ArrClient.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.Arr;
|
||||
|
||||
public abstract class ArrClient
|
||||
{
|
||||
private protected ILogger<ArrClient> _logger;
|
||||
private protected HttpClient _httpClient;
|
||||
|
||||
protected ArrClient(ILogger<ArrClient> logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
}
|
||||
|
||||
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
|
||||
{
|
||||
Uri uri = new(arrInstance.Url, $"/api/v3/queue?page={page}&pageSize=200&sortKey=timeleft");
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("queue list failed | {uri}", uri);
|
||||
throw;
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
QueueListResponse? queueResponse = JsonConvert.DeserializeObject<QueueListResponse>(responseBody);
|
||||
|
||||
if (queueResponse is null)
|
||||
{
|
||||
throw new Exception($"unrecognized queue list response | {uri} | {responseBody}");
|
||||
}
|
||||
|
||||
return queueResponse;
|
||||
}
|
||||
|
||||
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord queueRecord)
|
||||
{
|
||||
Uri uri = new(arrInstance.Url, $"/api/v3/queue/{queueRecord.Id}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false");
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_logger.LogInformation("queue item deleted | {url} | {title}", arrInstance.Url, queueRecord.Title);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("queue delete failed | {uri} | {title}", uri, queueRecord.Title);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds);
|
||||
|
||||
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
||||
{
|
||||
request.Headers.Add("x-api-key", apiKey);
|
||||
}
|
||||
}
|
||||
53
code/Infrastructure/Verticals/Arr/ArrQueueIterator.cs
Normal file
53
code/Infrastructure/Verticals/Arr/ArrQueueIterator.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Arr;
|
||||
|
||||
public sealed class ArrQueueIterator
|
||||
{
|
||||
private readonly ILogger<ArrQueueIterator> _logger;
|
||||
|
||||
public ArrQueueIterator(ILogger<ArrQueueIterator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Iterate(ArrClient arrClient, ArrInstance arrInstance, Func<IReadOnlyList<QueueRecord>, Task> action)
|
||||
{
|
||||
const ushort maxPage = 100;
|
||||
ushort page = 1;
|
||||
int totalRecords = 0;
|
||||
int processedRecords = 0;
|
||||
|
||||
do
|
||||
{
|
||||
QueueListResponse queueResponse = await arrClient.GetQueueItemsAsync(arrInstance, page);
|
||||
|
||||
if (totalRecords is 0)
|
||||
{
|
||||
totalRecords = queueResponse.TotalRecords;
|
||||
|
||||
_logger.LogInformation(
|
||||
"{items} items found in queue | {url}",
|
||||
queueResponse.TotalRecords, arrInstance.Url);
|
||||
}
|
||||
|
||||
if (queueResponse.Records.Count is 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await action(queueResponse.Records);
|
||||
|
||||
processedRecords += queueResponse.Records.Count;
|
||||
|
||||
if (processedRecords >= totalRecords)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
} while (processedRecords < totalRecords && page < maxPage);
|
||||
}
|
||||
}
|
||||
52
code/Infrastructure/Verticals/Arr/RadarrClient.cs
Normal file
52
code/Infrastructure/Verticals/Arr/RadarrClient.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Text;
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Radarr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.Arr;
|
||||
|
||||
public sealed class RadarrClient : ArrClient
|
||||
{
|
||||
public RadarrClient(ILogger<ArrClient> logger, IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds)
|
||||
{
|
||||
if (itemIds.Count is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = new(arrInstance.Url, "/api/v3/command");
|
||||
RadarrCommand command = new()
|
||||
{
|
||||
Name = "MoviesSearch",
|
||||
MovieIds = itemIds
|
||||
};
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
||||
request.Content = new StringContent(
|
||||
JsonConvert.SerializeObject(command),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_logger.LogInformation("movie search triggered | {url} | movie ids: {ids}", arrInstance.Url, string.Join(",", itemIds));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("movie search failed | {url} | movie ids: {ids}", arrInstance.Url, string.Join(",", itemIds));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
code/Infrastructure/Verticals/Arr/SonarrClient.cs
Normal file
50
code/Infrastructure/Verticals/Arr/SonarrClient.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Text;
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Sonarr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.Arr;
|
||||
|
||||
public sealed class SonarrClient : ArrClient
|
||||
{
|
||||
public SonarrClient(ILogger<SonarrClient> logger, IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds)
|
||||
{
|
||||
foreach (int itemId in itemIds)
|
||||
{
|
||||
Uri uri = new(arrInstance.Url, "/api/v3/command");
|
||||
SonarrCommand command = new()
|
||||
{
|
||||
Name = "SeriesSearch",
|
||||
SeriesId = itemId
|
||||
};
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
||||
request.Content = new StringContent(
|
||||
JsonConvert.SerializeObject(command),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_logger.LogInformation("series search triggered | {url} | series id: {id}", arrInstance.Url, itemId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("series search failed | {url} | series id: {id}", arrInstance.Url, itemId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
using System.Text;
|
||||
using Common.Configuration;
|
||||
using Domain.Sonarr.Queue;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Verticals.BlockedTorrent;
|
||||
|
||||
public sealed class BlockedTorrentHandler
|
||||
{
|
||||
private readonly ILogger<BlockedTorrentHandler> _logger;
|
||||
private readonly QBitConfig _qBitConfig;
|
||||
private readonly SonarrConfig _sonarrConfig;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string QueueListPathTemplate = "/api/v3/queue?page={0}&pageSize=200&sortKey=timeleft";
|
||||
private const string QueueDeletePathTemplate = "/api/v3/queue/{0}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
private const string SonarrCommandUriPath = "/api/v3/command";
|
||||
private const string SearchCommandPayloadTemplate = "{\"name\":\"SeriesSearch\",\"seriesId\":{0}}";
|
||||
|
||||
public BlockedTorrentHandler(
|
||||
ILogger<BlockedTorrentHandler> logger,
|
||||
IOptions<QBitConfig> qBitConfig,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_qBitConfig = qBitConfig.Value;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
}
|
||||
|
||||
public async Task HandleAsync()
|
||||
{
|
||||
QBittorrentClient qBitClient = new(_qBitConfig.Url);
|
||||
|
||||
await qBitClient.LoginAsync(_qBitConfig.Username, _qBitConfig.Password);
|
||||
|
||||
foreach (SonarrInstance sonarrInstance in _sonarrConfig.Instances)
|
||||
{
|
||||
ushort page = 1;
|
||||
int totalRecords = 0;
|
||||
int processedRecords = 0;
|
||||
HashSet<int> seriesToBeRefreshed = [];
|
||||
|
||||
do
|
||||
{
|
||||
QueueListResponse queueResponse = await ListQueuedTorrentsAsync(sonarrInstance, page);
|
||||
|
||||
if (totalRecords is 0)
|
||||
{
|
||||
totalRecords = queueResponse.TotalRecords;
|
||||
|
||||
_logger.LogInformation(
|
||||
"{items} items found in queue | {url}",
|
||||
queueResponse.TotalRecords, sonarrInstance.Url);
|
||||
}
|
||||
|
||||
foreach (Record record in queueResponse.Records)
|
||||
{
|
||||
var torrent = (await qBitClient.GetTorrentListAsync(new TorrentListQuery { Hashes = [record.DownloadId] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is not { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
_logger.LogInformation("skip | {torrent}", record.Title);
|
||||
return;
|
||||
}
|
||||
|
||||
seriesToBeRefreshed.Add(record.SeriesId);
|
||||
|
||||
await DeleteTorrentFromQueueAsync(sonarrInstance, record);
|
||||
}
|
||||
|
||||
if (queueResponse.Records.Count is 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
processedRecords += queueResponse.Records.Count;
|
||||
|
||||
if (processedRecords >= totalRecords)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
} while (processedRecords < totalRecords);
|
||||
|
||||
foreach (int id in seriesToBeRefreshed)
|
||||
{
|
||||
await RefreshSeriesAsync(sonarrInstance, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<QueueListResponse> ListQueuedTorrentsAsync(SonarrInstance sonarrInstance, int page)
|
||||
{
|
||||
Uri sonarrUri = new(sonarrInstance.Url, string.Format(QueueListPathTemplate, page));
|
||||
|
||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Get, sonarrUri);
|
||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("queue list failed | {uri}", sonarrUri);
|
||||
throw;
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
QueueListResponse? queueResponse = JsonConvert.DeserializeObject<QueueListResponse>(responseBody);
|
||||
|
||||
if (queueResponse is null)
|
||||
{
|
||||
throw new Exception($"unrecognized response | {responseBody}");
|
||||
}
|
||||
|
||||
return queueResponse;
|
||||
}
|
||||
|
||||
private async Task DeleteTorrentFromQueueAsync(SonarrInstance sonarrInstance, Record record)
|
||||
{
|
||||
Uri sonarrUri = new(sonarrInstance.Url, string.Format(QueueDeletePathTemplate, record.Id));
|
||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Delete, sonarrUri);
|
||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_logger.LogInformation("queue item deleted | {record}", record.Title);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("queue delete failed | {uri}", sonarrUri);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSeriesAsync(SonarrInstance sonarrInstance, int seriesId)
|
||||
{
|
||||
Uri sonarrUri = new(sonarrInstance.Url, SonarrCommandUriPath);
|
||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Post, sonarrUri);
|
||||
sonarrRequest.Content = new StringContent(
|
||||
SearchCommandPayloadTemplate.Replace("{0}", seriesId.ToString()),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
||||
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_logger.LogInformation("series search triggered | series id: {id}", seriesId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("series search failed | series id: {id}", seriesId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class BlocklistProvider
|
||||
{
|
||||
private readonly ILogger<BlocklistProvider> _logger;
|
||||
private readonly ContentBlockerConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BlocklistType BlocklistType { get; }
|
||||
|
||||
public List<string> Patterns { get; } = [];
|
||||
|
||||
public List<Regex> Regexes { get; } = [];
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
IOptions<ContentBlockerConfig> config,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
_config.Validate();
|
||||
|
||||
if (_config.Blacklist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Blacklist;
|
||||
}
|
||||
|
||||
if (_config.Whitelist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Whitelist;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadBlocklistAsync()
|
||||
{
|
||||
if (Patterns.Count > 0 || Regexes.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("blocklist already loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await LoadPatternsAndRegexesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("failed to load {type}", BlocklistType.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync()
|
||||
{
|
||||
string[] patterns;
|
||||
|
||||
if (BlocklistType is BlocklistType.Blacklist)
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Blacklist.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Whitelist.Path);
|
||||
}
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
|
||||
Parallel.ForEach(patterns, options, pattern =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Regex regex = new(pattern, RegexOptions.Compiled);
|
||||
Regexes.Add(regex);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Patterns.Add(pattern);
|
||||
}
|
||||
});
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_logger.LogDebug("loaded {count} patterns", Patterns.Count);
|
||||
_logger.LogDebug("loaded {count} regexes", Regexes.Count);
|
||||
_logger.LogDebug("blocklist loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadContentAsync(string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
// http(s) url
|
||||
return await ReadFromUrlAsync(path);
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
// local file path
|
||||
return await File.ReadAllLinesAsync(path);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"blocklist not found | {path}");
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadFromUrlAsync(string url)
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return (await response.Content.ReadAsStringAsync())
|
||||
.Split(['\r','\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
109
code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
Normal file
109
code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class ContentBlocker : IDisposable
|
||||
{
|
||||
private readonly ILogger<ContentBlocker> _logger;
|
||||
private readonly SonarrConfig _sonarrConfig;
|
||||
private readonly RadarrConfig _radarrConfig;
|
||||
private readonly SonarrClient _sonarrClient;
|
||||
private readonly RadarrClient _radarrClient;
|
||||
private readonly ArrQueueIterator _arrArrQueueIterator;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
private readonly IDownloadService _downloadService;
|
||||
|
||||
public ContentBlocker(
|
||||
ILogger<ContentBlocker> logger,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
BlocklistProvider blocklistProvider,
|
||||
DownloadServiceFactory downloadServiceFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_arrArrQueueIterator = arrArrQueueIterator;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
await _blocklistProvider.LoadBlocklistAsync();
|
||||
await _downloadService.LoginAsync();
|
||||
|
||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
||||
}
|
||||
|
||||
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
|
||||
{
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ArrInstance arrInstance in config.Instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(arrInstance, instanceType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "failed to block content for {type} instance | {url}", instanceType, arrInstance.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
foreach (QueueRecord record in items)
|
||||
{
|
||||
if (record.Protocol is not "torrent")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(record.DownloadId))
|
||||
{
|
||||
_logger.LogDebug("skip | download id is null for {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||
await _downloadService.BlockUnwantedFilesAsync(record.DownloadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ArrClient GetClient(InstanceType type) =>
|
||||
type switch
|
||||
{
|
||||
InstanceType.Sonarr => _sonarrClient,
|
||||
InstanceType.Radarr => _radarrClient,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_downloadService.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class FilenameEvaluator
|
||||
{
|
||||
private readonly ILogger<FilenameEvaluator> _logger;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
|
||||
public FilenameEvaluator(ILogger<FilenameEvaluator> logger, BlocklistProvider blocklistProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
}
|
||||
|
||||
// TODO create unit tests
|
||||
public bool IsValid(string filename)
|
||||
{
|
||||
return IsValidAgainstPatterns(filename) && IsValidAgainstRegexes(filename);
|
||||
}
|
||||
|
||||
private bool IsValidAgainstPatterns(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Patterns.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsValidAgainstRegexes(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Regexes.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string filename, string pattern)
|
||||
{
|
||||
bool hasStartWildcard = pattern.StartsWith('*');
|
||||
bool hasEndWildcard = pattern.EndsWith('*');
|
||||
|
||||
if (hasStartWildcard && hasEndWildcard)
|
||||
{
|
||||
return filename.Contains(
|
||||
pattern.Substring(1, pattern.Length - 2),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
if (hasStartWildcard)
|
||||
{
|
||||
return filename.EndsWith(pattern.Substring(1), StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
if (hasEndWildcard)
|
||||
{
|
||||
return filename.StartsWith(
|
||||
pattern.Substring(0, pattern.Length - 1),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
return filename == pattern;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json.Serialization;
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Deluge.Exceptions;
|
||||
using Domain.Models.Deluge.Request;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
|
||||
public sealed class DelugeClient
|
||||
{
|
||||
private readonly DelugeConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_config = config.Value;
|
||||
_httpClient = httpClientFactory.CreateClient(nameof(DelugeService));
|
||||
}
|
||||
|
||||
public async Task<bool> LoginAsync()
|
||||
{
|
||||
return await SendRequest<bool>("auth.login", _config.Password);
|
||||
}
|
||||
|
||||
public async Task<bool> Logout()
|
||||
{
|
||||
return await SendRequest<bool>("auth.delete_session");
|
||||
}
|
||||
|
||||
public async Task<List<DelugeTorrent>> ListTorrents(Dictionary<string, string>? filters = null)
|
||||
{
|
||||
filters ??= new Dictionary<string, string>();
|
||||
var keys = typeof(DelugeTorrent).GetAllJsonPropertyFromType();
|
||||
Dictionary<string, DelugeTorrent> result =
|
||||
await SendRequest<Dictionary<string, DelugeTorrent>>("core.get_torrents_status", filters, keys);
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<DelugeTorrentExtended>> ListTorrentsExtended(Dictionary<string, string>? filters = null)
|
||||
{
|
||||
filters ??= new Dictionary<string, string>();
|
||||
var keys = typeof(DelugeTorrentExtended).GetAllJsonPropertyFromType();
|
||||
Dictionary<string, DelugeTorrentExtended> result =
|
||||
await SendRequest<Dictionary<string, DelugeTorrentExtended>>("core.get_torrents_status", filters, keys);
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task<DelugeTorrent?> GetTorrent(string hash)
|
||||
{
|
||||
List<DelugeTorrent> torrents = await ListTorrents(new Dictionary<string, string>() { { "hash", hash } });
|
||||
return torrents.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<DelugeTorrentExtended?> GetTorrentExtended(string hash)
|
||||
{
|
||||
List<DelugeTorrentExtended> torrents =
|
||||
await ListTorrentsExtended(new Dictionary<string, string> { { "hash", hash } });
|
||||
return torrents.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<DelugeContents?> GetTorrentFiles(string hash)
|
||||
{
|
||||
return await SendRequest<DelugeContents?>("web.get_torrent_files", hash);
|
||||
}
|
||||
|
||||
public async Task ChangeFilesPriority(string hash, List<int> priorities)
|
||||
{
|
||||
Dictionary<string, List<int>> filePriorities = new()
|
||||
{
|
||||
{ "file_priorities", priorities }
|
||||
};
|
||||
|
||||
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
|
||||
}
|
||||
|
||||
private async Task<String> PostJson(String json)
|
||||
{
|
||||
StringContent content = new StringContent(json);
|
||||
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
|
||||
|
||||
var responseMessage = await _httpClient.PostAsync(new Uri(_config.Url, "/json"), content);
|
||||
responseMessage.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
private DelugeRequest CreateRequest(string method, params object[] parameters)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(method))
|
||||
{
|
||||
throw new ArgumentException(nameof(method));
|
||||
}
|
||||
|
||||
return new DelugeRequest(1, method, parameters);
|
||||
}
|
||||
|
||||
public async Task<T> SendRequest<T>(string method, params object[] parameters)
|
||||
{
|
||||
return await SendRequest<T>(CreateRequest(method, parameters));
|
||||
}
|
||||
|
||||
public async Task<T> SendRequest<T>(DelugeRequest webRequest)
|
||||
{
|
||||
var requestJson = JsonConvert.SerializeObject(webRequest, Formatting.None, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = webRequest.NullValueHandling
|
||||
});
|
||||
|
||||
var responseJson = await PostJson(requestJson);
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Error = (_, args) =>
|
||||
{
|
||||
// Suppress the error and continue
|
||||
args.ErrorContext.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
DelugeResponse<T>? webResponse = JsonConvert.DeserializeObject<DelugeResponse<T>>(responseJson, settings);
|
||||
|
||||
if (webResponse?.Error != null)
|
||||
{
|
||||
throw new DelugeClientException(webResponse.Error.Message);
|
||||
}
|
||||
|
||||
if (webResponse?.ResponseId != webRequest.RequestId)
|
||||
{
|
||||
throw new DelugeClientException("desync");
|
||||
}
|
||||
|
||||
return webResponse.Result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
|
||||
public sealed class DelugeService : IDownloadService
|
||||
{
|
||||
private readonly ILogger<DelugeService> _logger;
|
||||
private readonly DelugeClient _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
|
||||
public DelugeService(
|
||||
ILogger<DelugeService> logger,
|
||||
IOptions<DelugeConfig> config,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = new (config, httpClientFactory);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
await _client.LoginAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
DelugeContents? contents = null;
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
contents = await _client.GetTorrentFiles(hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool shouldRemove = true;
|
||||
|
||||
ProcessFiles(contents.Contents, (_, file) =>
|
||||
{
|
||||
if (file.Priority > 0)
|
||||
{
|
||||
shouldRemove = false;
|
||||
}
|
||||
});
|
||||
|
||||
return shouldRemove;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DelugeContents? contents = null;
|
||||
|
||||
try
|
||||
{
|
||||
contents = await _client.GetTorrentFiles(hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<int, int> priorities = [];
|
||||
bool hasPriorityUpdates = false;
|
||||
|
||||
ProcessFiles(contents.Contents, (name, file) =>
|
||||
{
|
||||
int priority = file.Priority;
|
||||
|
||||
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name))
|
||||
{
|
||||
priority = 0;
|
||||
hasPriorityUpdates = true;
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
}
|
||||
|
||||
priorities.Add(file.Index, priority);
|
||||
});
|
||||
|
||||
if (!hasPriorityUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
List<int> sortedPriorities = priorities
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
|
||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||
}
|
||||
|
||||
private async Task<bool> HasMinimalStatus(string hash)
|
||||
{
|
||||
DelugeMinimalStatus? status = await _client.SendRequest<DelugeMinimalStatus?>(
|
||||
"web.get_torrent_status",
|
||||
hash,
|
||||
new[] { "hash" }
|
||||
);
|
||||
|
||||
if (status?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
|
||||
{
|
||||
foreach (var (name, data) in contents)
|
||||
{
|
||||
switch (data.Type)
|
||||
{
|
||||
case "file":
|
||||
processFile(name, data);
|
||||
break;
|
||||
case "dir" when data.Contents is not null:
|
||||
// Recurse into subdirectories
|
||||
ProcessFiles(data.Contents, processFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge.Extensions;
|
||||
|
||||
internal static class DelugeExtensions
|
||||
{
|
||||
public static List<String?> GetAllJsonPropertyFromType(this Type t)
|
||||
{
|
||||
var type = typeof(JsonPropertyAttribute);
|
||||
var props = t.GetProperties()
|
||||
.Where(prop => Attribute.IsDefined(prop, type))
|
||||
.ToList();
|
||||
|
||||
return props
|
||||
.Select(x => x.GetCustomAttributes(type, true).Single())
|
||||
.Cast<JsonPropertyAttribute>()
|
||||
.Select(x => x.PropertyName)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
using Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient;
|
||||
|
||||
public sealed class DownloadServiceFactory
|
||||
{
|
||||
private readonly QBitConfig _qBitConfig;
|
||||
private readonly DelugeConfig _delugeConfig;
|
||||
private readonly TransmissionConfig _transmissionConfig;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DownloadServiceFactory(
|
||||
IOptions<QBitConfig> qBitConfig,
|
||||
IOptions<DelugeConfig> delugeConfig,
|
||||
IOptions<TransmissionConfig> transmissionConfig,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_qBitConfig = qBitConfig.Value;
|
||||
_delugeConfig = delugeConfig.Value;
|
||||
_transmissionConfig = transmissionConfig.Value;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
_qBitConfig.Validate();
|
||||
_delugeConfig.Validate();
|
||||
_transmissionConfig.Validate();
|
||||
|
||||
int enabledCount = new[] { _qBitConfig.Enabled, _delugeConfig.Enabled, _transmissionConfig.Enabled }
|
||||
.Count(enabled => enabled);
|
||||
|
||||
if (enabledCount > 1)
|
||||
{
|
||||
throw new Exception("only one download client can be enabled");
|
||||
}
|
||||
|
||||
if (enabledCount == 0)
|
||||
{
|
||||
throw new Exception("no download client is enabled");
|
||||
}
|
||||
}
|
||||
|
||||
public IDownloadService CreateDownloadClient()
|
||||
{
|
||||
if (_qBitConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<QBitService>();
|
||||
}
|
||||
|
||||
if (_delugeConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<DelugeService>();
|
||||
}
|
||||
|
||||
if (_transmissionConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<TransmissionService>();
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Infrastructure.Verticals.DownloadClient;
|
||||
|
||||
public interface IDownloadService : IDisposable
|
||||
{
|
||||
public Task LoginAsync();
|
||||
|
||||
public Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
|
||||
|
||||
public Task BlockUnwantedFilesAsync(string hash);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
|
||||
public sealed class QBitService : IDownloadService
|
||||
{
|
||||
private readonly ILogger<QBitService> _logger;
|
||||
private readonly QBitConfig _config;
|
||||
private readonly QBittorrentClient _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
|
||||
public QBitService(
|
||||
ILogger<QBitService> logger,
|
||||
IOptions<QBitConfig> config,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_client = new(_config.Url);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_config.Username) && string.IsNullOrEmpty(_config.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _client.LoginAsync(_config.Username, _config.Password);
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if all files were blocked by qBittorrent
|
||||
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if all files are marked as skip
|
||||
if (files.All(x => x.Priority is TorrentContentPriority.Skip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip || _filenameEvaluator.IsValid(file.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
||||
await _client.SetFilePriorityAsync(hash, file.Index.Value, TorrentContentPriority.Skip);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Transmission.API.RPC;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
|
||||
public sealed class TransmissionService : IDownloadService
|
||||
{
|
||||
private readonly ILogger<TransmissionService> _logger;
|
||||
private readonly TransmissionConfig _config;
|
||||
private readonly Client _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
private TorrentInfo[]? _torrentsCache;
|
||||
|
||||
public TransmissionService(
|
||||
ILogger<TransmissionService> logger,
|
||||
IOptions<TransmissionConfig> config,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_client = new(
|
||||
new Uri(_config.Url, "/transmission/rpc").ToString(),
|
||||
login: _config.Username,
|
||||
password: _config.Password
|
||||
);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
await _client.GetSessionInformationAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
||||
{
|
||||
if (!stats.Wanted.HasValue)
|
||||
{
|
||||
// if any files stats are missing, do not remove
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stats.Wanted.HasValue && stats.Wanted.Value)
|
||||
{
|
||||
// if any files are wanted, do not remove
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// remove if all files are unwanted
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
if (torrent?.FileStats is null || torrent.Files is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<long> unwantedFiles = [];
|
||||
|
||||
for (int i = 0; i < torrent.Files.Length; i++)
|
||||
{
|
||||
if (torrent.FileStats?[i].Wanted == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!torrent.FileStats[i].Wanted.Value || _filenameEvaluator.IsValid(torrent.Files[i].Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
||||
unwantedFiles.Add(i);
|
||||
}
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
await _client.TorrentSetAsync(new TorrentSettings
|
||||
{
|
||||
Ids = [ torrent.Id ],
|
||||
FilesUnwanted = unwantedFiles.ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<TorrentInfo?> GetTorrentAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = _torrentsCache?
|
||||
.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (_torrentsCache is null || torrent is null)
|
||||
{
|
||||
string[] fields = [TorrentFields.FILES, TorrentFields.FILE_STATS, TorrentFields.HASH_STRING, TorrentFields.ID];
|
||||
|
||||
// refresh cache
|
||||
_torrentsCache = (await _client.TorrentGetAsync(fields))
|
||||
?.Torrents;
|
||||
}
|
||||
|
||||
if (_torrentsCache?.Length is null or 0)
|
||||
{
|
||||
_logger.LogDebug("could not list torrents | {url}", _config.Url);
|
||||
}
|
||||
|
||||
torrent = _torrentsCache?.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
_logger.LogDebug("could not find torrent | {hash} | {url}", hash, _config.Url);
|
||||
}
|
||||
|
||||
return torrent;
|
||||
}
|
||||
}
|
||||
126
code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs
Normal file
126
code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
public sealed class QueueCleaner : IDisposable
|
||||
{
|
||||
private readonly ILogger<QueueCleaner> _logger;
|
||||
private readonly SonarrConfig _sonarrConfig;
|
||||
private readonly RadarrConfig _radarrConfig;
|
||||
private readonly SonarrClient _sonarrClient;
|
||||
private readonly RadarrClient _radarrClient;
|
||||
private readonly ArrQueueIterator _arrArrQueueIterator;
|
||||
private readonly IDownloadService _downloadService;
|
||||
|
||||
public QueueCleaner(
|
||||
ILogger<QueueCleaner> logger,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
DownloadServiceFactory downloadServiceFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_arrArrQueueIterator = arrArrQueueIterator;
|
||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
await _downloadService.LoginAsync();
|
||||
|
||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
||||
|
||||
// await _downloadClient.LogoutAsync();
|
||||
}
|
||||
|
||||
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
|
||||
{
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ArrInstance arrInstance in config.Instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(arrInstance, instanceType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "failed to clean {type} instance | {url}", instanceType, arrInstance.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
HashSet<int> itemsToBeRefreshed = [];
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
foreach (QueueRecord record in items)
|
||||
{
|
||||
if (record.Protocol is not "torrent")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(record.DownloadId))
|
||||
{
|
||||
_logger.LogDebug("skip | download id is null for {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
|
||||
{
|
||||
_logger.LogInformation("skip | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
itemsToBeRefreshed.Add(GetRecordId(instanceType, record));
|
||||
|
||||
await arrClient.DeleteQueueItemAsync(instance, record);
|
||||
}
|
||||
});
|
||||
|
||||
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
|
||||
}
|
||||
|
||||
private ArrClient GetClient(InstanceType type) =>
|
||||
type switch
|
||||
{
|
||||
InstanceType.Sonarr => _sonarrClient,
|
||||
InstanceType.Radarr => _radarrClient,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
|
||||
private int GetRecordId(InstanceType type, QueueRecord record) =>
|
||||
type switch
|
||||
{
|
||||
// TODO add episode id
|
||||
InstanceType.Sonarr => record.SeriesId,
|
||||
InstanceType.Radarr => record.MovieId,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_downloadService.Dispose();
|
||||
}
|
||||
}
|
||||
2
code/test/data/cleanuperr/config/blacklist
Normal file
2
code/test/data/cleanuperr/config/blacklist
Normal file
@@ -0,0 +1,2 @@
|
||||
.*sample.*
|
||||
*.zipx
|
||||
1
code/test/data/cleanuperr/config/whitelist
Normal file
1
code/test/data/cleanuperr/config/whitelist
Normal file
@@ -0,0 +1 @@
|
||||
*.mkv
|
||||
Binary file not shown.
1
code/test/data/deluge/config/auth
Normal file
1
code/test/data/deluge/config/auth
Normal file
@@ -0,0 +1 @@
|
||||
localclient:da4d4b43be734d48c1bb8b9ab0e39894520994e3:10
|
||||
15
code/test/data/deluge/config/blocklist.conf
Normal file
15
code/test/data/deluge/config/blocklist.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"check_after_days": 4,
|
||||
"last_update": 0.0,
|
||||
"list_compression": "",
|
||||
"list_size": 0,
|
||||
"list_type": "",
|
||||
"load_on_start": false,
|
||||
"timeout": 180,
|
||||
"try_times": 3,
|
||||
"url": "",
|
||||
"whitelisted": []
|
||||
}
|
||||
97
code/test/data/deluge/config/core.conf
Normal file
97
code/test/data/deluge/config/core.conf
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"add_paused": false,
|
||||
"allow_remote": false,
|
||||
"auto_manage_prefer_seeds": false,
|
||||
"auto_managed": true,
|
||||
"cache_expiry": 60,
|
||||
"cache_size": 512,
|
||||
"copy_torrent_file": false,
|
||||
"daemon_port": 58846,
|
||||
"del_copy_torrent_file": false,
|
||||
"dht": true,
|
||||
"dont_count_slow_torrents": false,
|
||||
"download_location": "/downloads",
|
||||
"download_location_paths_list": [],
|
||||
"enabled_plugins": [
|
||||
"Label"
|
||||
],
|
||||
"enc_in_policy": 1,
|
||||
"enc_level": 2,
|
||||
"enc_out_policy": 1,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"ignore_limits_on_local_network": true,
|
||||
"info_sent": 0.0,
|
||||
"listen_interface": "",
|
||||
"listen_ports": [
|
||||
6882,
|
||||
6882
|
||||
],
|
||||
"listen_random_port": null,
|
||||
"listen_reuse_port": true,
|
||||
"listen_use_sys_port": false,
|
||||
"lsd": true,
|
||||
"max_active_downloading": 3,
|
||||
"max_active_limit": 8,
|
||||
"max_active_seeding": 5,
|
||||
"max_connections_global": 200,
|
||||
"max_connections_per_second": 20,
|
||||
"max_connections_per_torrent": -1,
|
||||
"max_download_speed": -1.0,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"max_half_open_connections": 50,
|
||||
"max_upload_slots_global": 4,
|
||||
"max_upload_slots_per_torrent": -1,
|
||||
"max_upload_speed": -1.0,
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "/downloads",
|
||||
"move_completed_paths_list": [],
|
||||
"natpmp": true,
|
||||
"new_release_check": true,
|
||||
"outgoing_interface": "",
|
||||
"outgoing_ports": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"path_chooser_accelerator_string": "Tab",
|
||||
"path_chooser_auto_complete_enabled": true,
|
||||
"path_chooser_max_popup_rows": 20,
|
||||
"path_chooser_show_chooser_button_on_localhost": true,
|
||||
"path_chooser_show_hidden_files": false,
|
||||
"peer_tos": "0x00",
|
||||
"plugins_location": "/config/plugins",
|
||||
"pre_allocate_storage": false,
|
||||
"prioritize_first_last_pieces": false,
|
||||
"proxy": {
|
||||
"anonymous_mode": false,
|
||||
"force_proxy": false,
|
||||
"hostname": "",
|
||||
"password": "",
|
||||
"port": 8080,
|
||||
"proxy_hostnames": true,
|
||||
"proxy_peer_connections": true,
|
||||
"proxy_tracker_connections": true,
|
||||
"type": 0,
|
||||
"username": ""
|
||||
},
|
||||
"queue_new_to_top": false,
|
||||
"random_outgoing_ports": true,
|
||||
"random_port": false,
|
||||
"rate_limit_ip_overhead": false,
|
||||
"remove_seed_at_ratio": false,
|
||||
"seed_time_limit": 180,
|
||||
"seed_time_ratio_limit": 7.0,
|
||||
"send_info": false,
|
||||
"sequential_download": false,
|
||||
"share_ratio_limit": 2.0,
|
||||
"shared": false,
|
||||
"stop_seed_at_ratio": false,
|
||||
"stop_seed_ratio": 2.0,
|
||||
"super_seeding": false,
|
||||
"torrentfiles_location": "/config/torrents",
|
||||
"upnp": true,
|
||||
"utpex": true
|
||||
}
|
||||
97
code/test/data/deluge/config/core.conf.bak
Normal file
97
code/test/data/deluge/config/core.conf.bak
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"add_paused": false,
|
||||
"allow_remote": false,
|
||||
"auto_manage_prefer_seeds": false,
|
||||
"auto_managed": true,
|
||||
"cache_expiry": 60,
|
||||
"cache_size": 512,
|
||||
"copy_torrent_file": false,
|
||||
"daemon_port": 58846,
|
||||
"del_copy_torrent_file": false,
|
||||
"dht": true,
|
||||
"dont_count_slow_torrents": false,
|
||||
"download_location": "/downloads",
|
||||
"download_location_paths_list": [],
|
||||
"enabled_plugins": [
|
||||
"Label"
|
||||
],
|
||||
"enc_in_policy": 1,
|
||||
"enc_level": 2,
|
||||
"enc_out_policy": 1,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"ignore_limits_on_local_network": true,
|
||||
"info_sent": 0.0,
|
||||
"listen_interface": "",
|
||||
"listen_ports": [
|
||||
6882,
|
||||
6882
|
||||
],
|
||||
"listen_random_port": null,
|
||||
"listen_reuse_port": true,
|
||||
"listen_use_sys_port": false,
|
||||
"lsd": true,
|
||||
"max_active_downloading": 3,
|
||||
"max_active_limit": 8,
|
||||
"max_active_seeding": 5,
|
||||
"max_connections_global": 200,
|
||||
"max_connections_per_second": 20,
|
||||
"max_connections_per_torrent": -1,
|
||||
"max_download_speed": -1.0,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"max_half_open_connections": 50,
|
||||
"max_upload_slots_global": 4,
|
||||
"max_upload_slots_per_torrent": -1,
|
||||
"max_upload_speed": -1.0,
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "/downloads",
|
||||
"move_completed_paths_list": [],
|
||||
"natpmp": true,
|
||||
"new_release_check": true,
|
||||
"outgoing_interface": "",
|
||||
"outgoing_ports": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"path_chooser_accelerator_string": "Tab",
|
||||
"path_chooser_auto_complete_enabled": true,
|
||||
"path_chooser_max_popup_rows": 20,
|
||||
"path_chooser_show_chooser_button_on_localhost": true,
|
||||
"path_chooser_show_hidden_files": false,
|
||||
"peer_tos": "0x00",
|
||||
"plugins_location": "/config/plugins",
|
||||
"pre_allocate_storage": false,
|
||||
"prioritize_first_last_pieces": false,
|
||||
"proxy": {
|
||||
"anonymous_mode": false,
|
||||
"force_proxy": false,
|
||||
"hostname": "",
|
||||
"password": "",
|
||||
"port": 8080,
|
||||
"proxy_hostnames": true,
|
||||
"proxy_peer_connections": true,
|
||||
"proxy_tracker_connections": true,
|
||||
"type": 0,
|
||||
"username": ""
|
||||
},
|
||||
"queue_new_to_top": false,
|
||||
"random_outgoing_ports": true,
|
||||
"random_port": false,
|
||||
"rate_limit_ip_overhead": true,
|
||||
"remove_seed_at_ratio": false,
|
||||
"seed_time_limit": 180,
|
||||
"seed_time_ratio_limit": 7.0,
|
||||
"send_info": false,
|
||||
"sequential_download": false,
|
||||
"share_ratio_limit": 2.0,
|
||||
"shared": false,
|
||||
"stop_seed_at_ratio": false,
|
||||
"stop_seed_ratio": 2.0,
|
||||
"super_seeding": false,
|
||||
"torrentfiles_location": "/config/torrents",
|
||||
"upnp": true,
|
||||
"utpex": true
|
||||
}
|
||||
14
code/test/data/deluge/config/hostlist.conf
Normal file
14
code/test/data/deluge/config/hostlist.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"file": 3,
|
||||
"format": 1
|
||||
}{
|
||||
"hosts": [
|
||||
[
|
||||
"b5408e9794dd432789c55d8c46d15275",
|
||||
"127.0.0.1",
|
||||
58846,
|
||||
"localclient",
|
||||
"da4d4b43be734d48c1bb8b9ab0e39894520994e3"
|
||||
]
|
||||
]
|
||||
}
|
||||
51
code/test/data/deluge/config/label.conf
Normal file
51
code/test/data/deluge/config/label.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"labels": {
|
||||
"radarr": {
|
||||
"apply_max": false,
|
||||
"apply_move_completed": false,
|
||||
"apply_queue": false,
|
||||
"auto_add": false,
|
||||
"auto_add_trackers": [],
|
||||
"is_auto_managed": false,
|
||||
"max_connections": -1,
|
||||
"max_download_speed": -1,
|
||||
"max_upload_slots": -1,
|
||||
"max_upload_speed": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "",
|
||||
"prioritize_first_last": false,
|
||||
"remove_at_ratio": false,
|
||||
"stop_at_ratio": false,
|
||||
"stop_ratio": 2.0
|
||||
},
|
||||
"tv-sonarr": {
|
||||
"apply_max": false,
|
||||
"apply_move_completed": false,
|
||||
"apply_queue": false,
|
||||
"auto_add": false,
|
||||
"auto_add_trackers": [],
|
||||
"is_auto_managed": false,
|
||||
"max_connections": -1,
|
||||
"max_download_speed": -1,
|
||||
"max_upload_slots": -1,
|
||||
"max_upload_speed": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "",
|
||||
"prioritize_first_last": false,
|
||||
"remove_at_ratio": false,
|
||||
"stop_at_ratio": false,
|
||||
"stop_ratio": 2.0
|
||||
}
|
||||
},
|
||||
"torrent_labels": {
|
||||
"2b2ec156461d77bc48b8fe4d62cede50dcdff8e0": "radarr",
|
||||
"59ab2bc053430fe53e06a93e2eadb7acb6a6bf2c": "tv-sonarr",
|
||||
"5a31d5f1689f5f45fd85c275a37acd2c7b82fde1": "tv-sonarr",
|
||||
"6c890ff85b5317d5df291c3c23a782774e10e6fe": "radarr",
|
||||
"a4a1d1dd1db25763caa8f5e4d25ad72ef304094b": "radarr",
|
||||
"b72541215214be2a1d96ef6b29ca1305f5e5e1f6": "tv-sonarr"
|
||||
}
|
||||
}
|
||||
50
code/test/data/deluge/config/label.conf.bak
Normal file
50
code/test/data/deluge/config/label.conf.bak
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"labels": {
|
||||
"radarr": {
|
||||
"apply_max": false,
|
||||
"apply_move_completed": false,
|
||||
"apply_queue": false,
|
||||
"auto_add": false,
|
||||
"auto_add_trackers": [],
|
||||
"is_auto_managed": false,
|
||||
"max_connections": -1,
|
||||
"max_download_speed": -1,
|
||||
"max_upload_slots": -1,
|
||||
"max_upload_speed": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "",
|
||||
"prioritize_first_last": false,
|
||||
"remove_at_ratio": false,
|
||||
"stop_at_ratio": false,
|
||||
"stop_ratio": 2.0
|
||||
},
|
||||
"tv-sonarr": {
|
||||
"apply_max": false,
|
||||
"apply_move_completed": false,
|
||||
"apply_queue": false,
|
||||
"auto_add": false,
|
||||
"auto_add_trackers": [],
|
||||
"is_auto_managed": false,
|
||||
"max_connections": -1,
|
||||
"max_download_speed": -1,
|
||||
"max_upload_slots": -1,
|
||||
"max_upload_speed": -1,
|
||||
"move_completed": false,
|
||||
"move_completed_path": "",
|
||||
"prioritize_first_last": false,
|
||||
"remove_at_ratio": false,
|
||||
"stop_at_ratio": false,
|
||||
"stop_ratio": 2.0
|
||||
}
|
||||
},
|
||||
"torrent_labels": {
|
||||
"59ab2bc053430fe53e06a93e2eadb7acb6a6bf2c": "tv-sonarr",
|
||||
"5a31d5f1689f5f45fd85c275a37acd2c7b82fde1": "tv-sonarr",
|
||||
"6c890ff85b5317d5df291c3c23a782774e10e6fe": "radarr",
|
||||
"a4a1d1dd1db25763caa8f5e4d25ad72ef304094b": "radarr",
|
||||
"b72541215214be2a1d96ef6b29ca1305f5e5e1f6": "tv-sonarr"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* blocklist.js
|
||||
*
|
||||
* Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es>
|
||||
*
|
||||
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
||||
* the additional special exception to link portions of this program with the OpenSSL library.
|
||||
* See LICENSE for more details.
|
||||
*
|
||||
*/
|
||||
|
||||
Ext.ns('Deluge.ux.preferences');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.preferences.BlocklistPage
|
||||
* @extends Ext.Panel
|
||||
*/
|
||||
Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
|
||||
title: _('Blocklist'),
|
||||
header: false,
|
||||
layout: 'fit',
|
||||
border: false,
|
||||
autoScroll: true,
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.preferences.BlocklistPage.superclass.initComponent.call(this);
|
||||
|
||||
this.URLFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('General'),
|
||||
autoHeight: true,
|
||||
defaultType: 'textfield',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 40,
|
||||
});
|
||||
|
||||
this.URL = this.URLFset.add({
|
||||
fieldLabel: _('URL:'),
|
||||
labelSeparator: '',
|
||||
name: 'url',
|
||||
width: '80%',
|
||||
});
|
||||
|
||||
this.SettingsFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Settings'),
|
||||
autoHeight: true,
|
||||
defaultType: 'spinnerfield',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 160,
|
||||
});
|
||||
|
||||
this.checkListDays = this.SettingsFset.add({
|
||||
fieldLabel: _('Check for new list every (days):'),
|
||||
labelSeparator: '',
|
||||
name: 'check_list_days',
|
||||
value: 4,
|
||||
decimalPrecision: 0,
|
||||
width: 80,
|
||||
});
|
||||
|
||||
this.chkImportOnStart = this.SettingsFset.add({
|
||||
xtype: 'checkbox',
|
||||
fieldLabel: _('Import blocklist on startup'),
|
||||
name: 'check_import_startup',
|
||||
});
|
||||
|
||||
this.OptionsFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Options'),
|
||||
autoHeight: true,
|
||||
defaultType: 'button',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: false,
|
||||
width: '80%',
|
||||
labelWidth: 0,
|
||||
});
|
||||
|
||||
this.checkDownload = this.OptionsFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'check_download',
|
||||
xtype: 'container',
|
||||
layout: 'hbox',
|
||||
margins: '4 0 0 5',
|
||||
items: [
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Check Download and Import ',
|
||||
scale: 'medium',
|
||||
},
|
||||
{
|
||||
xtype: 'box',
|
||||
autoEl: {
|
||||
tag: 'img',
|
||||
src: '../icons/ok.png',
|
||||
},
|
||||
margins: '4 0 0 3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.forceDownload = this.OptionsFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'force_download',
|
||||
text: ' Force Download and Import ',
|
||||
margins: '2 0 0 0',
|
||||
//icon: '../icons/blocklist_import24.png',
|
||||
scale: 'medium',
|
||||
});
|
||||
|
||||
this.ProgressFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Info'),
|
||||
autoHeight: true,
|
||||
defaultType: 'progress',
|
||||
style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 0,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
this.downProgBar = this.ProgressFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'progress_bar',
|
||||
width: '90%',
|
||||
});
|
||||
|
||||
this.InfoFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Info'),
|
||||
autoHeight: true,
|
||||
defaultType: 'label',
|
||||
style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
labelWidth: 60,
|
||||
});
|
||||
|
||||
this.lblFileSize = this.InfoFset.add({
|
||||
fieldLabel: _('File Size:'),
|
||||
labelSeparator: '',
|
||||
name: 'file_size',
|
||||
});
|
||||
|
||||
this.lblDate = this.InfoFset.add({
|
||||
fieldLabel: _('Date:'),
|
||||
labelSeparator: '',
|
||||
name: 'date',
|
||||
});
|
||||
|
||||
this.lblType = this.InfoFset.add({
|
||||
fieldLabel: _('Type:'),
|
||||
labelSeparator: '',
|
||||
name: 'type',
|
||||
});
|
||||
|
||||
this.lblURL = this.InfoFset.add({
|
||||
fieldLabel: _('URL:'),
|
||||
labelSeparator: '',
|
||||
name: 'lbl_URL',
|
||||
});
|
||||
|
||||
this.WhitelistFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Whitelist'),
|
||||
autoHeight: true,
|
||||
defaultType: 'editorgrid',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 0,
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _(''),
|
||||
name: 'whitelist',
|
||||
margins: '2 0 5 5',
|
||||
height: 100,
|
||||
width: 260,
|
||||
autoExpandColumn: 'ip',
|
||||
viewConfig: {
|
||||
emptyText: _('Add an IP...'),
|
||||
deferEmptyText: false,
|
||||
},
|
||||
colModel: new Ext.grid.ColumnModel({
|
||||
columns: [
|
||||
{
|
||||
id: 'ip',
|
||||
header: _('IP'),
|
||||
dataIndex: 'ip',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
editable: true,
|
||||
editor: {
|
||||
xtype: 'textfield',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
selModel: new Ext.grid.RowSelectionModel({
|
||||
singleSelect: false,
|
||||
moveEditorOnEnter: false,
|
||||
}),
|
||||
store: new Ext.data.ArrayStore({
|
||||
autoDestroy: true,
|
||||
fields: [{ name: 'ip' }],
|
||||
}),
|
||||
listeners: {
|
||||
afteredit: function (e) {
|
||||
e.record.commit();
|
||||
},
|
||||
},
|
||||
setEmptyText: function (text) {
|
||||
if (this.viewReady) {
|
||||
this.getView().emptyText = text;
|
||||
this.getView().refresh();
|
||||
} else {
|
||||
Ext.apply(this.viewConfig, { emptyText: text });
|
||||
}
|
||||
},
|
||||
loadData: function (data) {
|
||||
this.getStore().loadData(data);
|
||||
if (this.viewReady) {
|
||||
this.getView().updateHeaders();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.ipButtonsContainer = this.WhitelistFset.add({
|
||||
xtype: 'container',
|
||||
layout: 'hbox',
|
||||
margins: '4 0 0 5',
|
||||
items: [
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Add IP ',
|
||||
margins: '0 5 0 0',
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Delete IP ',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.updateTask = Ext.TaskMgr.start({
|
||||
interval: 2000,
|
||||
run: this.onUpdate,
|
||||
scope: this,
|
||||
});
|
||||
|
||||
this.on('show', this.updateConfig, this);
|
||||
|
||||
this.ipButtonsContainer.getComponent(0).setHandler(this.addIP, this);
|
||||
this.ipButtonsContainer.getComponent(1).setHandler(this.deleteIP, this);
|
||||
|
||||
this.checkDownload.getComponent(0).setHandler(this.checkDown, this);
|
||||
this.forceDownload.setHandler(this.forceDown, this);
|
||||
},
|
||||
|
||||
onApply: function () {
|
||||
var config = {};
|
||||
|
||||
config['url'] = this.URL.getValue();
|
||||
config['check_after_days'] = this.checkListDays.getValue();
|
||||
config['load_on_start'] = this.chkImportOnStart.getValue();
|
||||
|
||||
var ipList = [];
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
|
||||
for (var i = 0; i < store.getCount(); i++) {
|
||||
var record = store.getAt(i);
|
||||
var ip = record.get('ip');
|
||||
ipList.push(ip);
|
||||
}
|
||||
|
||||
config['whitelisted'] = ipList;
|
||||
|
||||
deluge.client.blocklist.set_config(config);
|
||||
},
|
||||
|
||||
onOk: function () {
|
||||
this.onApply();
|
||||
},
|
||||
|
||||
onUpdate: function () {
|
||||
deluge.client.blocklist.get_status({
|
||||
success: function (status) {
|
||||
if (status['state'] == 'Downloading') {
|
||||
this.InfoFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(true);
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
this.forceDownload.setDisabled(true);
|
||||
|
||||
this.ProgressFset.show();
|
||||
this.downProgBar.updateProgress(
|
||||
status['file_progress'],
|
||||
'Downloading '
|
||||
.concat((status['file_progress'] * 100).toFixed(2))
|
||||
.concat('%'),
|
||||
true
|
||||
);
|
||||
} else if (status['state'] == 'Importing') {
|
||||
this.InfoFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(true);
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
this.forceDownload.setDisabled(true);
|
||||
|
||||
this.ProgressFset.show();
|
||||
this.downProgBar.updateText(
|
||||
'Importing '.concat(status['num_blocked'])
|
||||
);
|
||||
} else if (status['state'] == 'Idle') {
|
||||
this.ProgressFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(false);
|
||||
this.forceDownload.setDisabled(false);
|
||||
if (status['up_to_date']) {
|
||||
this.checkDownload.getComponent(1).show();
|
||||
this.checkDownload.doLayout();
|
||||
} else {
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
}
|
||||
this.InfoFset.show();
|
||||
this.lblFileSize.setText(fsize(status['file_size']));
|
||||
this.lblDate.setText(fdate(status['file_date']));
|
||||
this.lblType.setText(status['file_type']);
|
||||
this.lblURL.setText(
|
||||
status['file_url'].substr(0, 40).concat('...')
|
||||
);
|
||||
}
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
checkDown: function () {
|
||||
this.onApply();
|
||||
deluge.client.blocklist.check_import();
|
||||
},
|
||||
|
||||
forceDown: function () {
|
||||
this.onApply();
|
||||
deluge.client.blocklist.check_import((force = true));
|
||||
},
|
||||
|
||||
updateConfig: function () {
|
||||
deluge.client.blocklist.get_config({
|
||||
success: function (config) {
|
||||
this.URL.setValue(config['url']);
|
||||
this.checkListDays.setValue(config['check_after_days']);
|
||||
this.chkImportOnStart.setValue(config['load_on_start']);
|
||||
|
||||
var data = [];
|
||||
var keys = Ext.keys(config['whitelisted']);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
data.push([config['whitelisted'][key]]);
|
||||
}
|
||||
|
||||
this.WhitelistFset.getComponent(0).loadData(data);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
|
||||
deluge.client.blocklist.get_status({
|
||||
success: function (status) {
|
||||
this.lblFileSize.setText(fsize(status['file_size']));
|
||||
this.lblDate.setText(fdate(status['file_date']));
|
||||
this.lblType.setText(status['file_type']);
|
||||
this.lblURL.setText(
|
||||
status['file_url'].substr(0, 40).concat('...')
|
||||
);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
addIP: function () {
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
var IP = store.recordType;
|
||||
var i = new IP({
|
||||
ip: '',
|
||||
});
|
||||
this.WhitelistFset.getComponent(0).stopEditing();
|
||||
store.insert(0, i);
|
||||
this.WhitelistFset.getComponent(0).startEditing(0, 0);
|
||||
},
|
||||
|
||||
deleteIP: function () {
|
||||
var selections = this.WhitelistFset.getComponent(0)
|
||||
.getSelectionModel()
|
||||
.getSelections();
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
|
||||
this.WhitelistFset.getComponent(0).stopEditing();
|
||||
for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
|
||||
store.commitChanges();
|
||||
},
|
||||
|
||||
onDestroy: function () {
|
||||
Ext.TaskMgr.stop(this.updateTask);
|
||||
|
||||
deluge.preferences.un('show', this.updateConfig, this);
|
||||
|
||||
Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
|
||||
name: 'Blocklist',
|
||||
|
||||
onDisable: function () {
|
||||
deluge.preferences.removePage(this.prefsPage);
|
||||
},
|
||||
|
||||
onEnable: function () {
|
||||
this.prefsPage = deluge.preferences.addPage(
|
||||
new Deluge.ux.preferences.BlocklistPage()
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin);
|
||||
@@ -0,0 +1,635 @@
|
||||
/**
|
||||
* label.js
|
||||
*
|
||||
* Copyright (C) Damien Churchill 2010 <damoxc@gmail.com>
|
||||
*
|
||||
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
||||
* the additional special exception to link portions of this program with the OpenSSL library.
|
||||
* See LICENSE for more details.
|
||||
*
|
||||
*/
|
||||
|
||||
Ext.ns('Deluge.ux.preferences');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.preferences.LabelPage
|
||||
* @extends Ext.Panel
|
||||
*/
|
||||
Deluge.ux.preferences.LabelPage = Ext.extend(Ext.Panel, {
|
||||
title: _('Label'),
|
||||
layout: 'fit',
|
||||
border: false,
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.preferences.LabelPage.superclass.initComponent.call(this);
|
||||
fieldset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Label Preferences'),
|
||||
autoHeight: true,
|
||||
labelWidth: 1,
|
||||
defaultType: 'panel',
|
||||
});
|
||||
fieldset.add({
|
||||
border: false,
|
||||
bodyCfg: {
|
||||
html: _(
|
||||
'<p>The Label plugin is enabled.</p><br>' +
|
||||
'<p>To add, remove or edit labels right-click on the Label filter ' +
|
||||
'entry in the sidebar.</p><br>' +
|
||||
'<p>To apply a label right-click on torrent(s).<p>'
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Ext.ns('Deluge.ux');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.AddLabelWindow
|
||||
* @extends Ext.Window
|
||||
*/
|
||||
Deluge.ux.AddLabelWindow = Ext.extend(Ext.Window, {
|
||||
title: _('Add Label'),
|
||||
width: 300,
|
||||
height: 100,
|
||||
closeAction: 'hide',
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.AddLabelWindow.superclass.initComponent.call(this);
|
||||
this.addButton(_('Cancel'), this.onCancelClick, this);
|
||||
this.addButton(_('Ok'), this.onOkClick, this);
|
||||
|
||||
this.form = this.add({
|
||||
xtype: 'form',
|
||||
height: 35,
|
||||
baseCls: 'x-plain',
|
||||
bodyStyle: 'padding:5px 5px 0',
|
||||
defaultType: 'textfield',
|
||||
labelWidth: 50,
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _('Name'),
|
||||
name: 'name',
|
||||
allowBlank: false,
|
||||
width: 220,
|
||||
listeners: {
|
||||
specialkey: {
|
||||
fn: function (field, e) {
|
||||
if (e.getKey() == 13) this.onOkClick();
|
||||
},
|
||||
scope: this,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function () {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onOkClick: function () {
|
||||
var label = this.form.getForm().getValues().name;
|
||||
deluge.client.label.add(label, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
this.fireEvent('labeladded', label);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onHide: function (comp) {
|
||||
Deluge.ux.AddLabelWindow.superclass.onHide.call(this, comp);
|
||||
this.form.getForm().reset();
|
||||
},
|
||||
|
||||
onShow: function (comp) {
|
||||
Deluge.ux.AddLabelWindow.superclass.onShow.call(this, comp);
|
||||
this.form.getForm().findField('name').focus(false, 150);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.LabelOptionsWindow
|
||||
* @extends Ext.Window
|
||||
*/
|
||||
Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
||||
title: _('Label Options'),
|
||||
width: 325,
|
||||
height: 240,
|
||||
closeAction: 'hide',
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.LabelOptionsWindow.superclass.initComponent.call(this);
|
||||
this.addButton(_('Cancel'), this.onCancelClick, this);
|
||||
this.addButton(_('Ok'), this.onOkClick, this);
|
||||
|
||||
this.form = this.add({
|
||||
xtype: 'form',
|
||||
});
|
||||
|
||||
this.tabs = this.form.add({
|
||||
xtype: 'tabpanel',
|
||||
height: 175,
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
title: _('Maximum'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_max',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply per torrent max settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
defaultType: 'spinnerfield',
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _('Download Speed'),
|
||||
name: 'max_download_speed',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Upload Speed'),
|
||||
name: 'max_upload_speed',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Upload Slots'),
|
||||
name: 'max_upload_slots',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Connections'),
|
||||
name: 'max_connections',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Queue'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_queue',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply queue settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
defaultType: 'checkbox',
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Auto Managed'),
|
||||
name: 'is_auto_managed',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
boxLabel: _('Stop seed at ratio:'),
|
||||
name: 'stop_at_ratio',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'spinnerfield',
|
||||
name: 'stop_ratio',
|
||||
width: 60,
|
||||
decimalPrecision: 2,
|
||||
incrementValue: 0.1,
|
||||
style: 'position: relative; left: 100px',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
boxLabel: _('Remove at ratio'),
|
||||
name: 'remove_at_ratio',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Folders'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_move_completed',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply folder settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
defaultType: 'checkbox',
|
||||
labelWidth: 1,
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Move completed to:'),
|
||||
name: 'move_completed',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'textfield',
|
||||
name: 'move_completed_path',
|
||||
width: 250,
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Trackers'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'auto_add',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Automatically apply label:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
defaultType: 'textarea',
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Move completed to:'),
|
||||
name: 'auto_add_trackers',
|
||||
width: 250,
|
||||
height: 100,
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
getLabelOptions: function () {
|
||||
deluge.client.label.get_options(this.label, {
|
||||
success: this.gotOptions,
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
gotOptions: function (options) {
|
||||
this.form.getForm().setValues(options);
|
||||
},
|
||||
|
||||
show: function (label) {
|
||||
Deluge.ux.LabelOptionsWindow.superclass.show.call(this);
|
||||
this.label = label;
|
||||
this.setTitle(_('Label Options') + ': ' + this.label);
|
||||
this.tabs.setActiveTab(0);
|
||||
this.getLabelOptions();
|
||||
},
|
||||
|
||||
onCancelClick: function () {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onOkClick: function () {
|
||||
var values = this.form.getForm().getFieldValues();
|
||||
if (values['auto_add_trackers']) {
|
||||
values['auto_add_trackers'] =
|
||||
values['auto_add_trackers'].split('\n');
|
||||
}
|
||||
deluge.client.label.set_options(this.label, values);
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onFieldChecked: function (field, checked) {
|
||||
var fs = field.ownerCt.nextSibling();
|
||||
fs.items.each(function (field) {
|
||||
field.setDisabled(!checked);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Ext.ns('Deluge.plugins');
|
||||
|
||||
/**
|
||||
* @class Deluge.plugins.LabelPlugin
|
||||
* @extends Deluge.Plugin
|
||||
*/
|
||||
Deluge.plugins.LabelPlugin = Ext.extend(Deluge.Plugin, {
|
||||
name: 'Label',
|
||||
|
||||
createMenu: function () {
|
||||
this.labelMenu = new Ext.menu.Menu({
|
||||
items: [
|
||||
{
|
||||
text: _('Add Label'),
|
||||
iconCls: 'icon-add',
|
||||
handler: this.onLabelAddClick,
|
||||
scope: this,
|
||||
},
|
||||
{
|
||||
text: _('Remove Label'),
|
||||
disabled: true,
|
||||
iconCls: 'icon-remove',
|
||||
handler: this.onLabelRemoveClick,
|
||||
scope: this,
|
||||
},
|
||||
{
|
||||
text: _('Label Options'),
|
||||
disabled: true,
|
||||
handler: this.onLabelOptionsClick,
|
||||
scope: this,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
setFilter: function (filter) {
|
||||
filter.show_zero = true;
|
||||
|
||||
filter.list.on('contextmenu', this.onLabelContextMenu, this);
|
||||
filter.header.on('contextmenu', this.onLabelHeaderContextMenu, this);
|
||||
this.filter = filter;
|
||||
},
|
||||
|
||||
updateTorrentMenu: function (states) {
|
||||
this.torrentMenu.removeAll(true);
|
||||
this.torrentMenu.addMenuItem({
|
||||
text: _('No Label'),
|
||||
label: '',
|
||||
handler: this.onTorrentMenuClick,
|
||||
scope: this,
|
||||
});
|
||||
for (var state in states) {
|
||||
if (!state || state == 'All') continue;
|
||||
this.torrentMenu.addMenuItem({
|
||||
text: state,
|
||||
label: state,
|
||||
handler: this.onTorrentMenuClick,
|
||||
scope: this,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onDisable: function () {
|
||||
deluge.sidebar.un('filtercreate', this.onFilterCreate);
|
||||
deluge.sidebar.un('afterfiltercreate', this.onAfterFilterCreate);
|
||||
delete Deluge.FilterPanel.templates.label;
|
||||
this.deregisterTorrentStatus('label');
|
||||
deluge.menus.torrent.remove(this.tmSep);
|
||||
deluge.menus.torrent.remove(this.tm);
|
||||
deluge.preferences.removePage(this.prefsPage);
|
||||
},
|
||||
|
||||
onEnable: function () {
|
||||
this.prefsPage = deluge.preferences.addPage(
|
||||
new Deluge.ux.preferences.LabelPage()
|
||||
);
|
||||
this.torrentMenu = new Ext.menu.Menu();
|
||||
|
||||
this.tmSep = deluge.menus.torrent.add({
|
||||
xtype: 'menuseparator',
|
||||
});
|
||||
|
||||
this.tm = deluge.menus.torrent.add({
|
||||
text: _('Label'),
|
||||
menu: this.torrentMenu,
|
||||
});
|
||||
|
||||
var lbltpl =
|
||||
'<div class="x-deluge-filter">' +
|
||||
'<tpl if="filter">{filter}</tpl>' +
|
||||
'<tpl if="!filter">No Label</tpl>' +
|
||||
' ({count})' +
|
||||
'</div>';
|
||||
|
||||
if (deluge.sidebar.hasFilter('label')) {
|
||||
var filter = deluge.sidebar.getFilter('label');
|
||||
filter.list.columns[0].tpl = new Ext.XTemplate(lbltpl);
|
||||
this.setFilter(filter);
|
||||
this.updateTorrentMenu(filter.getStates());
|
||||
filter.list.refresh();
|
||||
} else {
|
||||
deluge.sidebar.on('filtercreate', this.onFilterCreate, this);
|
||||
deluge.sidebar.on(
|
||||
'afterfiltercreate',
|
||||
this.onAfterFilterCreate,
|
||||
this
|
||||
);
|
||||
Deluge.FilterPanel.templates.label = lbltpl;
|
||||
}
|
||||
this.registerTorrentStatus('label', _('Label'));
|
||||
},
|
||||
|
||||
onAfterFilterCreate: function (sidebar, filter) {
|
||||
if (filter.filter != 'label') return;
|
||||
this.updateTorrentMenu(filter.getStates());
|
||||
},
|
||||
|
||||
onFilterCreate: function (sidebar, filter) {
|
||||
if (filter.filter != 'label') return;
|
||||
this.setFilter(filter);
|
||||
},
|
||||
|
||||
onLabelAddClick: function () {
|
||||
if (!this.addWindow) {
|
||||
this.addWindow = new Deluge.ux.AddLabelWindow();
|
||||
this.addWindow.on('labeladded', this.onLabelAdded, this);
|
||||
}
|
||||
this.addWindow.show();
|
||||
},
|
||||
|
||||
onLabelAdded: function (label) {
|
||||
var filter = deluge.sidebar.getFilter('label');
|
||||
var states = filter.getStates();
|
||||
var statesArray = [];
|
||||
|
||||
for (state in states) {
|
||||
if (!state || state == 'All') continue;
|
||||
statesArray.push(state);
|
||||
}
|
||||
|
||||
statesArray.push(label.toLowerCase());
|
||||
statesArray.sort();
|
||||
|
||||
//console.log(states);
|
||||
//console.log(statesArray);
|
||||
|
||||
states = {};
|
||||
|
||||
for (i = 0; i < statesArray.length; ++i) {
|
||||
states[statesArray[i]] = 0;
|
||||
}
|
||||
|
||||
this.updateTorrentMenu(states);
|
||||
},
|
||||
|
||||
onLabelContextMenu: function (dv, i, node, e) {
|
||||
e.preventDefault();
|
||||
if (!this.labelMenu) this.createMenu();
|
||||
var r = dv.getRecord(node).get('filter');
|
||||
if (!r || r == 'All') {
|
||||
this.labelMenu.items.get(1).setDisabled(true);
|
||||
this.labelMenu.items.get(2).setDisabled(true);
|
||||
} else {
|
||||
this.labelMenu.items.get(1).setDisabled(false);
|
||||
this.labelMenu.items.get(2).setDisabled(false);
|
||||
}
|
||||
dv.select(i);
|
||||
this.labelMenu.showAt(e.getXY());
|
||||
},
|
||||
|
||||
onLabelHeaderContextMenu: function (e, t) {
|
||||
e.preventDefault();
|
||||
if (!this.labelMenu) this.createMenu();
|
||||
this.labelMenu.items.get(1).setDisabled(true);
|
||||
this.labelMenu.items.get(2).setDisabled(true);
|
||||
this.labelMenu.showAt(e.getXY());
|
||||
},
|
||||
|
||||
onLabelOptionsClick: function () {
|
||||
if (!this.labelOpts)
|
||||
this.labelOpts = new Deluge.ux.LabelOptionsWindow();
|
||||
this.labelOpts.show(this.filter.getState());
|
||||
},
|
||||
|
||||
onLabelRemoveClick: function () {
|
||||
var state = this.filter.getState();
|
||||
deluge.client.label.remove(state, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
this.torrentMenu.items.each(function (item) {
|
||||
if (item.text != state) return;
|
||||
this.torrentMenu.remove(item);
|
||||
var i = item;
|
||||
}, this);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
onTorrentMenuClick: function (item, e) {
|
||||
var ids = deluge.torrents.getSelectedIds();
|
||||
Ext.each(ids, function (id, i) {
|
||||
if (ids.length == i + 1) {
|
||||
deluge.client.label.set_torrent(id, item.label, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deluge.client.label.set_torrent(id, item.label);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
Deluge.registerPlugin('Label', Deluge.plugins.LabelPlugin);
|
||||
BIN
code/test/data/deluge/config/session.state
Normal file
BIN
code/test/data/deluge/config/session.state
Normal file
Binary file not shown.
BIN
code/test/data/deluge/config/session.state.bak
Normal file
BIN
code/test/data/deluge/config/session.state.bak
Normal file
Binary file not shown.
17
code/test/data/deluge/config/ssl/daemon.cert
Normal file
17
code/test/data/deluge/config/ssl/daemon.cert
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCAQAwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNRGVsdWdlIERh
|
||||
ZW1vbjAeFw0yNDExMTQxMjI0MjVaFw0yNzExMTQxMjI0MjVaMBgxFjAUBgNVBAMM
|
||||
DURlbHVnZSBEYWVtb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDk
|
||||
hV3jiOW40yERiQ6F0GgsM8doBRaCzdJ5du7JRKY0bxNzpkgsmjJp8HFljeROPbOl
|
||||
plyWzJuol02sQ5WlcnUppPZCFlm3Hnw4wdsM2F7n4MC3i2M8/M73pIBbXw/7Ekro
|
||||
ZijmS02DT3o6c4Urdh89w3GRs6MWESikBdzTDAVPV8REASAfoI1JVUFznxqEMysx
|
||||
H9ANqdlkO0sMnBvOFvNxAyuVMOwCUEFsw7ynutJB/yrMUk1itoX21CigOH+pkNDe
|
||||
JnfIKRa6BvU4aLCFGynAR3bk7TcwRiPoIiWPmwxktFVc+sr26fuGWd8KSPjOJZGV
|
||||
+WZjYAqtiZRFX67VgAf1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAC3vWaRkzIue
|
||||
9onnmcpv0uXPNDANefAL4B9sPVmWVdBrWGJTGm9umWZGOa2Z1VRGaC8+LpHJ074f
|
||||
gmv/PE61GxL5usmfRRLJK4ZKIPzoIspqVKfuWJH6kbsAzg3x43RF0WvokJQKC+Y/
|
||||
tWY8a6ewJ1Uh1YDJEwxgR+WBguN64w4QdujPGoDnAWAEW7VJsc2PlYzYConpwKXy
|
||||
RUOpcnZnAV3z98zRU6m0G/RyYZF8H00hXsEitDeuh5Kdu1tLCbhUYvVXxB6BY559
|
||||
bJ+DrxblqpM71wamFq9MDv+Z78XgGZFymoLLLRV3gBO1RsKVnl81Ywvo2LMkRp52
|
||||
XuvUJJPD6po=
|
||||
-----END CERTIFICATE-----
|
||||
28
code/test/data/deluge/config/ssl/daemon.pkey
Normal file
28
code/test/data/deluge/config/ssl/daemon.pkey
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDkhV3jiOW40yER
|
||||
iQ6F0GgsM8doBRaCzdJ5du7JRKY0bxNzpkgsmjJp8HFljeROPbOlplyWzJuol02s
|
||||
Q5WlcnUppPZCFlm3Hnw4wdsM2F7n4MC3i2M8/M73pIBbXw/7EkroZijmS02DT3o6
|
||||
c4Urdh89w3GRs6MWESikBdzTDAVPV8REASAfoI1JVUFznxqEMysxH9ANqdlkO0sM
|
||||
nBvOFvNxAyuVMOwCUEFsw7ynutJB/yrMUk1itoX21CigOH+pkNDeJnfIKRa6BvU4
|
||||
aLCFGynAR3bk7TcwRiPoIiWPmwxktFVc+sr26fuGWd8KSPjOJZGV+WZjYAqtiZRF
|
||||
X67VgAf1AgMBAAECggEAA4T6ToghNu4pfYz60vIZaJ+I2//tZNOpVi2QEpF4GH6i
|
||||
x2PcNgj56x/E31Ixc0hdUpkeppk9cc9CvFBzfDooYR0lSHHyV8ZPFiCw2vXKIGVv
|
||||
EmSXLAKeErpPL8O7CfGHgyTE+dGsaUVOwJpe3AMptVh5O0vk9cYLNjDQ7H8sOxiQ
|
||||
0uCTu7JyKRVOtmp9EFy/KqnHPaGVFuNmQH5byiDhuHWFC3lbC2QeYrlhMnPv38jw
|
||||
NVuUI10E+ZlJJuhuSOwTdKTj0XxhtvMclsbkXOCGm4nL13EYqcyrTiHFbxDCV5c3
|
||||
V33xmtH+ABvdvF/68Ouk6ph1BRLTpAW/UmURcUvpUQKBgQD3O2clEmBac3yA487T
|
||||
/uBhqId5JU5vAYltH2Cfs46aaDUjHe4QshuMPUiyyiZgFh2oc2JXbmDgbXkyrkyq
|
||||
2K5sOcixKef/eoAIkT+o2Nd8PDMpApF0AcqyWAe0xNR0thWJvjdMon4aFHHZoCcW
|
||||
+zyWYB8cxVdZCcPnnykpvJgSdwKBgQDsoBZxrq9Ta3S0Ho7NEYu4b6sh9rCfXCU7
|
||||
94+eeWPc6+oO+jQDtIS+RYWKwOKmqMzhzq1MdTl9+2yGTCa1st05gsyUpEpso2r5
|
||||
BlHUVBzrDEMhRN5FqOcKuRZ+G2HMAsTxJbVZnOS9Z0mf5nwCvJxs8jUEPbFDWQVr
|
||||
AcKRApTH8wKBgQDw3T3TH0EiPks5Izh4z2L5ogBCZbcxbNTfrGctj/jJs+a5DMrI
|
||||
F03BZl9yWIHksQc5+xf/SDk3zU/7sVZeSHY+WFmPSN2OyGD+d8wGiyP9FIVfWfIt
|
||||
jCVXdW4kjnLSNidrqBcmIVUrwWld9aq/uAtCEemd1SERTPNAsI6g6+1YZwKBgQCL
|
||||
P55Voi4NElRoVv9EUMn/bL+xygGgllJXGtWKtfb9oFtqGvWXJJlle3Yd9GqtFvMT
|
||||
A1RahTWjHN19nry8+phTatTHuHMPwY+HIp/vKtylud6banK/XakxV0CUT7ramtqY
|
||||
6s7xAHJfv7PFBJb/6UzIlDR83W0+q9mTYkLEoVc63wKBgCV2ok/C7+7e189gxUir
|
||||
3ezxBlY9Cv+1/wIE5IjAwpmOPPzsZMKZ2SbDxJMoDBXACJtgOEleiUSYss9qkhVt
|
||||
o0kUkEOWPM3vydRLCLcsdfpM826WAmGA/w6MsqWyZDoc/kw7UhEBqMhQVXzc/cGF
|
||||
gRXRDfyZj4MpvAOBPTlTWbme
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,2 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731591387e4:infod6:lengthi6e4:name97:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG.mkv.zipx12:piece lengthi262144e6:pieces20:<3A><>s<EFBFBD><73><EFBFBD><EFBFBD>^<5E>^o
|
||||
z<EFBFBD><17><1F><><EFBFBD>ee
|
||||
BIN
code/test/data/deluge/config/state/torrents.fastresume
Normal file
BIN
code/test/data/deluge/config/state/torrents.fastresume
Normal file
Binary file not shown.
BIN
code/test/data/deluge/config/state/torrents.fastresume.bak
Normal file
BIN
code/test/data/deluge/config/state/torrents.fastresume.bak
Normal file
Binary file not shown.
BIN
code/test/data/deluge/config/state/torrents.state
Normal file
BIN
code/test/data/deluge/config/state/torrents.state
Normal file
Binary file not shown.
BIN
code/test/data/deluge/config/state/torrents.state.bak
Normal file
BIN
code/test/data/deluge/config/state/torrents.state.bak
Normal file
Binary file not shown.
590
code/test/data/deluge/config/web.conf
Normal file
590
code/test/data/deluge/config/web.conf
Normal file
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"file": 2,
|
||||
"format": 1
|
||||
}{
|
||||
"base": "/",
|
||||
"cert": "ssl/daemon.cert",
|
||||
"default_daemon": "",
|
||||
"enabled_plugins": [],
|
||||
"first_login": false,
|
||||
"https": false,
|
||||
"interface": "0.0.0.0",
|
||||
"language": "",
|
||||
"pkey": "ssl/daemon.pkey",
|
||||
"port": 8112,
|
||||
"pwd_salt": "2bc0ed67acc6876dda1a1632594090478fdeab60",
|
||||
"pwd_sha1": "3ac8756d294abe4f6c9dfa084b7fc2c84ce32f68",
|
||||
"session_timeout": 3600,
|
||||
"sessions": {
|
||||
"00390c773fafe30cb393f53a920b48ac353b58ca27ac9ed64a1cbc61d5026677": {
|
||||
"expires": 1731936939.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0379df23e58eb57a0ec781168c5acb1527be9ce1dc48a6dec201905358dbedd8": {
|
||||
"expires": 1731665164.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"03d38494090a758cbe3ecc1e8a004986528297c7200e58b36649e197276c95e3": {
|
||||
"expires": 1731718770.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"03fe5879beed5c299cd18472b64d31c4c610cd413059d7582312b002bb0eef03": {
|
||||
"expires": 1731689401.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"05c8e71ff1e411beb45e278e786fbde8c893854e2906f3111dd48f943082eba5": {
|
||||
"expires": 1731593711.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0cce3c4e10dcdebd921e19905c9ce3c162cefafadbf35b3c64a1932860af0e7d": {
|
||||
"expires": 1731721234.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0d5bce647f6368877290f7be8a0f63f070039dd76027158278143b2ea6078a42": {
|
||||
"expires": 1731665495.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0f95357a4b9994584b429a5facaad735bc1e0adb0f994b7fad82318f589de991": {
|
||||
"expires": 1731718993.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"15c8cf06252ea0039bc2569d4121378baa3287594f9148d4fb26e999966e5538": {
|
||||
"expires": 1731714201.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"15d5edb14093bb821dbef9080853e00f969860add39bd21e301172ae911713b1": {
|
||||
"expires": 1731665102.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"17f7a731a26bdde434e8f4edb6043c4699efa29b982ad1f5df26676747b400a7": {
|
||||
"expires": 1731658344.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1800115899b60e88b29483c4656f9c56c58d38c008d96149bb70fb5e9d26a10c": {
|
||||
"expires": 1731939230.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1970022246cbb41f07d1242920163980e93e4e96f11864ffcf047c8cb5cf9908": {
|
||||
"expires": 1731706575.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"19daad7642cf7f056083ff2868c9565ff8b2f6750eae91d5b235989089239bdc": {
|
||||
"expires": 1731689407.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1b525ddd164c645ec47f1b2b58044cdf32f90800bc1973afb7b56a5814b813da": {
|
||||
"expires": 1731711800.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1da54bf8d0c73023d11e1cb91586b088898ef5a37d146c108bfe2a9633499b63": {
|
||||
"expires": 1731693609.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2095c759cb9b9ce96ab3bf3f07301e8dd71de75aeed2d4db957d2227adbc56f5": {
|
||||
"expires": 1731664924.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2185646cfe4fa4f9892ea3df734b02b31ab7dfeb0f0868a6c730f04328b1a87a": {
|
||||
"expires": 1731716848.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2270be625fa3bf61e919e8c495bc6c7868e907709e6b06c533727d0469df61c9": {
|
||||
"expires": 1731714357.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2438a71a5850697dfb99ba24afa21f82a99ca32bf59a05f5dbd8c0f8bf645e4f": {
|
||||
"expires": 1731691862.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"24930581ae0aa9a0e0a520f3cccfb71f50308be43b15cb5b0fb3404a7d9a8a2f": {
|
||||
"expires": 1731617366.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"28920404a5f4638ebcdb4eb4addad4db19ee0bdd8505457cce7b6f81ba06b363": {
|
||||
"expires": 1731712508.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"29f26fc40c0be11ba12209d142e8a662ad3c5f58f4c9e2a4dcb9bf81a9eac0ef": {
|
||||
"expires": 1731692292.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2b4e3dc65d727790957c28d570f474ef0ffacc98bf1372b11ef4c2eacfde585c": {
|
||||
"expires": 1731711706.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2c1d2ff923df8718a46c89e575a2ebbcede10f9c585e5fe2ddb3a4a43ddabafa": {
|
||||
"expires": 1731719810.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2c9235163d9dc1af694609caf5624465e872a9a9efaae9c5dd7de97190911970": {
|
||||
"expires": 1731592559.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2ee0d1c4f504c080441ae4a8e61546077405ef9dce3ec291923d761b69f69586": {
|
||||
"expires": 1731659695.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"317679509ed59022afd20bd8a891cc759fadd7ca9c85c88ac0b05cf9b9ea1791": {
|
||||
"expires": 1731716722.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"31b14f8de2ba1de58011ecf8dfe7f8681ae4af543928f3903a9c080374a7fb08": {
|
||||
"expires": 1731692998.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"337641d938548a3261f67b1b1e295e8d09b248d2c7358a84a2914c803e2c9827": {
|
||||
"expires": 1731719800.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"33b864b8ab214816273040e0456297da90a7f2e5bf352368f906e64cc363ccdf": {
|
||||
"expires": 1731659084.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"36d3798ad875c70807a02eac3b7fd4279550cb9dfa6ccb530b66c74f0d577a52": {
|
||||
"expires": 1731939210.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3b14137d9b57080d81e0a4532ad703dc91e2542be56415f58feca56231851eb0": {
|
||||
"expires": 1731714081.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3b6c35a4cde25fa4a846a3df5d41562e798089db25eb21b9363cccdba2a3e093": {
|
||||
"expires": 1731939483.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3d7bf91cb3e15eb82297ab54bae4cc6e06a42b54679e5f963a580fdf0d4bcf57": {
|
||||
"expires": 1731719791.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e7234797204e1672caf4b5b5ef450898f931ee48dcffae0b3b89e138434c036": {
|
||||
"expires": 1731723178.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e79e3bd1e01d10a6017e61b152151150deeb5185781a6325cd0aa4b9bfec47d": {
|
||||
"expires": 1731719395.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e9020e47c34087dc02ff6a5b396b7f338cc3249c9812e320490c33d9e7ce245": {
|
||||
"expires": 1731719545.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"44c3d9b212384742b7e0ac2a8c9a2bb48cf146a403ba01b6137051f54953a38a": {
|
||||
"expires": 1731938866.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"470c34122403b8dec692f9079e892ca92d8bf13cdd6c2814b6d829996e5f8b67": {
|
||||
"expires": 1731664624.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"4a47ca4a624fff02d9970d7e8a341ec08b8076cade949348795e45002c18556d": {
|
||||
"expires": 1731718983.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"4e88bdb753b76965909d1a2eabcc6ef5c12dab11cb0f32185506a19192f9cae2": {
|
||||
"expires": 1731935979.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"51ce01dfe69d8ae6afb019a05046154f4b51ef64569e3424e78a80957a11ba8c": {
|
||||
"expires": 1731693589.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"538a9fd86a56727c571da77b70353ac0fa5568442ea17d69a817a579d37679ac": {
|
||||
"expires": 1731939264.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"566fe0345702f6e2f30effef42ef664c46ac0e7f21aa3d2b414a1f290230fba6": {
|
||||
"expires": 1731664385.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"57df9cb450fd2dd9717b9ab03bbce3492603188c11f5e83af8d1bb38ab36ed01": {
|
||||
"expires": 1731938872.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"587362acb21285d814a9861a93b3f0e017ee9efb1bfe63343c13c09e7ea80f91": {
|
||||
"expires": 1731716759.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"58f7b011411dbad96eadc573eb164e1cdb6f96d52febbb8d4adb2cdda6ed80ef": {
|
||||
"expires": 1731936289.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5cb4123b4b425f1d8a6accf4c02386feea7f20bf6171291f715c2cd99bfb02c0": {
|
||||
"expires": 1731665482.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5d9819c229e5f79b767a88db9c78b24998b424a66e8a7ba0039553b7c54051ec": {
|
||||
"expires": 1731617370.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5e6cb57d0f9d97aa3ed75fefd40c8060a085c04727886511f0e7db126b203d43": {
|
||||
"expires": 1731721196.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5e74fdaca0c6c7e7afec714793677646ff89d00ae35908a7125d6cf50ea0702c": {
|
||||
"expires": 1731603184.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5fbe1b057086c9f17b5a4d7d9fd9f41eab0305ea89b5b1de2ca633b0b38aab50": {
|
||||
"expires": 1731937913.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"644aae458bd0a092fe342d1f020d4b7eccff9cf6cfc0677fe0d9531754edaff0": {
|
||||
"expires": 1731692249.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"653493a22274078fc44d52acb337b56bcd4084de2f0a1b6b79be186550a30cb3": {
|
||||
"expires": 1731591365.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"66618b25addefa9f12946c5afb65e8d690c5a871fff3d03fb796751a9eee0d41": {
|
||||
"expires": 1731591683.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"684613e83cfde35d79917108a4091de4c585e1ff627eee0d904920759dc3ea53": {
|
||||
"expires": 1731712313.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"68ecec112a27957857622d4c0ba02824f5e03981db6f51fa01c0be8ac893c6f1": {
|
||||
"expires": 1731591683.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"69d6aa5b5eae433a357dc92fe12bbfb7fd29629425f0edbe55e0ac4e8df112aa": {
|
||||
"expires": 1731718574.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"6ade7fdf14f334646e2ad2f6b627146a69e0b896d0930a996b4f9df7bc4cf28e": {
|
||||
"expires": 1731939300.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"6c19cb32acd64f6c1543bf07dba3aee5ca5d3ba71f754e1e95acc3da5dc6aa27": {
|
||||
"expires": 1731718629.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"71c8d2cf33b22285f43c5855bdabd9ea25a18b177cfe28056c8bed41776d0ced": {
|
||||
"expires": 1731664874.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"745592d9be94482df01bc76010f58844175050e2bb7c0974de4e1f852a589554": {
|
||||
"expires": 1731689116.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"82ee9a0b4fde768d0580e85633012235fdf4683c0115ec121ba17282075483e7": {
|
||||
"expires": 1731708818.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8346f9faa70bd614131a9115bcb33168ae9af221be0270e967c01e9c1c58129e": {
|
||||
"expires": 1731693061.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"861dbf07e0df2c29fedc8fcd5a346b4dcb1a0ece0f741befc3c72d3fadc82268": {
|
||||
"expires": 1731722565.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"870afa2ad6a0832fb19680b3bbf0bfd99de377c9cbaaff3cf6bf5a633fe541c3": {
|
||||
"expires": 1731933969.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"88be8381b68afe4c56b949c2588dec6b57f2dbe5ae20faacb06c37b7cbdc8a8b": {
|
||||
"expires": 1731718780.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8a010b80566da20d356b77df700e2444292eea50a928eb7613bc874462436f36": {
|
||||
"expires": 1731602951.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8be44833b5b32f8ed1eb2e6dba5ec7aa49fc4307dbd18d03859c96d227a9058b": {
|
||||
"expires": 1731658179.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8d5c74abebafb8bfc72a3c33c17195d8e6a52c4505b1ccff9eda1a81f9a74ef7": {
|
||||
"expires": 1731939423.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8e2e998fabc8429ef4ba385a4d4ed401fbe2508192b65dc72280cdfc086948e0": {
|
||||
"expires": 1731692291.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8e8ed10c9cb8ced98d7736a81b68990fff019ca2b32dd9093209b132906b68c3": {
|
||||
"expires": 1731692267.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8fbfaad4b9fc517f848b6f052be51b3a2cd494247078bf053460f8b80e53065a": {
|
||||
"expires": 1731595963.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8fddab944e5a0d45750d5da5b78230387228e12f27ad040316394a4d6b166b5d": {
|
||||
"expires": 1731939300.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9364816c6b2e739b647c43ad27b2d52593c2d59a5181aefcda12d04ea31d9fc6": {
|
||||
"expires": 1731718765.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9a29ef6e50f415e5ccd36140eeab85a4409ae271470c1d51a32e0791c75ea588": {
|
||||
"expires": 1731658513.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9ac87d5bf293ab9dc0bb5dcefe1e65eb7e166c4b3f5a162529683d5d866d7f9f": {
|
||||
"expires": 1731937503.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9b266dafeef7c956bb4d9e987791975b64e7333de860a0d9173e211524cc8540": {
|
||||
"expires": 1731591553.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9c03a8e39a5ff42c889c73ad9d4d5d84e76747322dfdf714ccb74a0d37923682": {
|
||||
"expires": 1731719394.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9d1df8609ca6ada923c85b2721c0b3e606b92372478b5ca943cb52a7b8886951": {
|
||||
"expires": 1731939230.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9f9de499b714f5c7dcb873f1141ad463a34e7c34eaa62cc988167505e4f5ac54": {
|
||||
"expires": 1731939220.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a484b99afebac4ce74b6f52cad90448496fb80f2d8756e503776072739748a57": {
|
||||
"expires": 1731938872.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a48e673783b5c56edfd5b5bc73b1f9c527dc1ca6da3aef82f61692b40095b39c": {
|
||||
"expires": 1731939445.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a7ba418572d761f89d13fd7a11d682a3c821406ccd29f3e6d683966d0fb3d3ab": {
|
||||
"expires": 1731720041.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a7cc53afae41ab5e800893c7271acf272f34b604f7e1ace8c3f0232606d01e2b": {
|
||||
"expires": 1731719810.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a8c19794aaac3afd733588d42d7ffddc5f8de336d4bd8aa5cf9c1ff36cc9d590": {
|
||||
"expires": 1731933957.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"aa301c95f4890c38baa814503e940f28143304ad55dbfaea2db8aaec90169031": {
|
||||
"expires": 1731599734.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ac98337f159cdc03b4865b80c78d53ab39502291fa19cc0b60233209e1d92bb7": {
|
||||
"expires": 1731689363.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"adf9d353a020d93e54b32b14c28525a7e6f33fd735144d7b778a4e517192b7c5": {
|
||||
"expires": 1731691941.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ae14e4adfe0504f8dfb3fcb575606e67f57e950cdf88f00ca76e0af215c2b413": {
|
||||
"expires": 1731935980.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ae48fe0ecea0704704bc6327b6bf3258de377b08fa4d76b4f69b8470852960c6": {
|
||||
"expires": 1731591544.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ae967211fa06ac26d96e3207e67c136efc6367acc92d608e52e1f204b5bd3da4": {
|
||||
"expires": 1731664535.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"b0f55c6cad8bc2230754fd1bbeebfedd31ba244eac96e8d02be6d6a33b542b4f": {
|
||||
"expires": 1731592529.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"b689520f2e3ccf2164da5dba4bac111d4cbc6d3bdcf44361127baa0068623cd0": {
|
||||
"expires": 1731717015.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"bb92c09264296d8c12fac5d2abed2224b25a85d5a46d87f4a4351da76d00566e": {
|
||||
"expires": 1731720040.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"bbba0f20f9c1639ebfcaedb54a87d4b968bc953f72b06105f6d995f5eee9bff7": {
|
||||
"expires": 1731591455.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c1a50b58a364f294755666d0e748ba2a06fbe55de758453180920774525214c8": {
|
||||
"expires": 1731719336.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c6af2f6a6627cbbf18696f1b8b904346726800d05b137d251ee28b699fbd858c": {
|
||||
"expires": 1731665294.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c86527a0aae330a71fc324a233ad876d420a54d387794374d126e7d6a0f19f92": {
|
||||
"expires": 1731692356.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"cd65741faf869e193c4e2a51e9454cf88598f58987b2ce559ef3d6bfa98e7605": {
|
||||
"expires": 1731591572.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"cdb4f2d14ab7de91b8be1261be40bf59f2bfa0e5e6327166ffa08cfa74eb357a": {
|
||||
"expires": 1731692237.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d0f2c477a500bffcdcfbc56907df1b322200d9f7a801285bbe5440e5bca5e8c4": {
|
||||
"expires": 1731711859.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d3a9186f1c2c81d8085d86c09ddd4fc5e205f61f25f3cabbc1ee379e52304c77": {
|
||||
"expires": 1731718780.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d74ba4649e1a5a098d42c2f62472c94d4484f43b0979cbbd48b464ac5f20e49b": {
|
||||
"expires": 1731716625.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d769dedd53059553cb54961641c0cbdf818533db06764fbcd5557a7200247c28": {
|
||||
"expires": 1731718992.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d95d211a839a71c0bb00d2504f18deb81af2d0ab7183478543a5d28654a37197": {
|
||||
"expires": 1731658629.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"da6f45956cf8b0b4a5552166dfe1372437b25179e0d24bfbeaba745d28febb53": {
|
||||
"expires": 1731937514.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"df84bacd66f8dbf2d1f150d839029e11b3ce56c0536183fd33656685bd446c44": {
|
||||
"expires": 1731719340.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"e8be12b35aa13a67bc7d3332d373b4117493319625c1ed87e3335ab8d09b9054": {
|
||||
"expires": 1731686353.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f04dce449eb7dc69f7973906564c4aa224ff20595a3f2a3bfedbcd23ffaa9117": {
|
||||
"expires": 1731658418.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f4472d087a796ef1f431c44d4f6e9d46ecfe88acce21368a7d85fc31e1efc1e9": {
|
||||
"expires": 1731711611.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f62a451ea2933e56ff9dae2299e374477adb54d8c8285ea59d2ab15b9a80bd13": {
|
||||
"expires": 1731712500.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
}
|
||||
},
|
||||
"show_session_speed": false,
|
||||
"show_sidebar": true,
|
||||
"sidebar_multiple_filters": true,
|
||||
"sidebar_show_zero": false,
|
||||
"theme": "gray"
|
||||
}
|
||||
480
code/test/data/deluge/config/web.conf.bak
Normal file
480
code/test/data/deluge/config/web.conf.bak
Normal file
@@ -0,0 +1,480 @@
|
||||
{
|
||||
"file": 2,
|
||||
"format": 1
|
||||
}{
|
||||
"base": "/",
|
||||
"cert": "ssl/daemon.cert",
|
||||
"default_daemon": "",
|
||||
"enabled_plugins": [],
|
||||
"first_login": false,
|
||||
"https": false,
|
||||
"interface": "0.0.0.0",
|
||||
"language": "",
|
||||
"pkey": "ssl/daemon.pkey",
|
||||
"port": 8112,
|
||||
"pwd_salt": "2bc0ed67acc6876dda1a1632594090478fdeab60",
|
||||
"pwd_sha1": "3ac8756d294abe4f6c9dfa084b7fc2c84ce32f68",
|
||||
"session_timeout": 3600,
|
||||
"sessions": {
|
||||
"0379df23e58eb57a0ec781168c5acb1527be9ce1dc48a6dec201905358dbedd8": {
|
||||
"expires": 1731665164.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"03d38494090a758cbe3ecc1e8a004986528297c7200e58b36649e197276c95e3": {
|
||||
"expires": 1731718770.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"03fe5879beed5c299cd18472b64d31c4c610cd413059d7582312b002bb0eef03": {
|
||||
"expires": 1731689401.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"05c8e71ff1e411beb45e278e786fbde8c893854e2906f3111dd48f943082eba5": {
|
||||
"expires": 1731593711.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0cce3c4e10dcdebd921e19905c9ce3c162cefafadbf35b3c64a1932860af0e7d": {
|
||||
"expires": 1731721234.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0d5bce647f6368877290f7be8a0f63f070039dd76027158278143b2ea6078a42": {
|
||||
"expires": 1731665495.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"0f95357a4b9994584b429a5facaad735bc1e0adb0f994b7fad82318f589de991": {
|
||||
"expires": 1731718993.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"15c8cf06252ea0039bc2569d4121378baa3287594f9148d4fb26e999966e5538": {
|
||||
"expires": 1731714201.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"15d5edb14093bb821dbef9080853e00f969860add39bd21e301172ae911713b1": {
|
||||
"expires": 1731665102.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"17f7a731a26bdde434e8f4edb6043c4699efa29b982ad1f5df26676747b400a7": {
|
||||
"expires": 1731658344.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1970022246cbb41f07d1242920163980e93e4e96f11864ffcf047c8cb5cf9908": {
|
||||
"expires": 1731706575.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"19daad7642cf7f056083ff2868c9565ff8b2f6750eae91d5b235989089239bdc": {
|
||||
"expires": 1731689407.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1b525ddd164c645ec47f1b2b58044cdf32f90800bc1973afb7b56a5814b813da": {
|
||||
"expires": 1731711800.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"1da54bf8d0c73023d11e1cb91586b088898ef5a37d146c108bfe2a9633499b63": {
|
||||
"expires": 1731693609.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2095c759cb9b9ce96ab3bf3f07301e8dd71de75aeed2d4db957d2227adbc56f5": {
|
||||
"expires": 1731664924.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2185646cfe4fa4f9892ea3df734b02b31ab7dfeb0f0868a6c730f04328b1a87a": {
|
||||
"expires": 1731716848.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2270be625fa3bf61e919e8c495bc6c7868e907709e6b06c533727d0469df61c9": {
|
||||
"expires": 1731714357.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2438a71a5850697dfb99ba24afa21f82a99ca32bf59a05f5dbd8c0f8bf645e4f": {
|
||||
"expires": 1731691862.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"24930581ae0aa9a0e0a520f3cccfb71f50308be43b15cb5b0fb3404a7d9a8a2f": {
|
||||
"expires": 1731617366.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"28920404a5f4638ebcdb4eb4addad4db19ee0bdd8505457cce7b6f81ba06b363": {
|
||||
"expires": 1731712508.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"29f26fc40c0be11ba12209d142e8a662ad3c5f58f4c9e2a4dcb9bf81a9eac0ef": {
|
||||
"expires": 1731692292.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2b4e3dc65d727790957c28d570f474ef0ffacc98bf1372b11ef4c2eacfde585c": {
|
||||
"expires": 1731711706.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2c1d2ff923df8718a46c89e575a2ebbcede10f9c585e5fe2ddb3a4a43ddabafa": {
|
||||
"expires": 1731719810.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2c9235163d9dc1af694609caf5624465e872a9a9efaae9c5dd7de97190911970": {
|
||||
"expires": 1731592559.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"2ee0d1c4f504c080441ae4a8e61546077405ef9dce3ec291923d761b69f69586": {
|
||||
"expires": 1731659695.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"317679509ed59022afd20bd8a891cc759fadd7ca9c85c88ac0b05cf9b9ea1791": {
|
||||
"expires": 1731716722.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"31b14f8de2ba1de58011ecf8dfe7f8681ae4af543928f3903a9c080374a7fb08": {
|
||||
"expires": 1731692998.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"337641d938548a3261f67b1b1e295e8d09b248d2c7358a84a2914c803e2c9827": {
|
||||
"expires": 1731719800.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"33b864b8ab214816273040e0456297da90a7f2e5bf352368f906e64cc363ccdf": {
|
||||
"expires": 1731659084.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3b14137d9b57080d81e0a4532ad703dc91e2542be56415f58feca56231851eb0": {
|
||||
"expires": 1731714081.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3d7bf91cb3e15eb82297ab54bae4cc6e06a42b54679e5f963a580fdf0d4bcf57": {
|
||||
"expires": 1731719791.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e7234797204e1672caf4b5b5ef450898f931ee48dcffae0b3b89e138434c036": {
|
||||
"expires": 1731723178.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e79e3bd1e01d10a6017e61b152151150deeb5185781a6325cd0aa4b9bfec47d": {
|
||||
"expires": 1731719395.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"3e9020e47c34087dc02ff6a5b396b7f338cc3249c9812e320490c33d9e7ce245": {
|
||||
"expires": 1731719545.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"470c34122403b8dec692f9079e892ca92d8bf13cdd6c2814b6d829996e5f8b67": {
|
||||
"expires": 1731664624.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"4a47ca4a624fff02d9970d7e8a341ec08b8076cade949348795e45002c18556d": {
|
||||
"expires": 1731718983.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"51ce01dfe69d8ae6afb019a05046154f4b51ef64569e3424e78a80957a11ba8c": {
|
||||
"expires": 1731693589.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"566fe0345702f6e2f30effef42ef664c46ac0e7f21aa3d2b414a1f290230fba6": {
|
||||
"expires": 1731664385.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"587362acb21285d814a9861a93b3f0e017ee9efb1bfe63343c13c09e7ea80f91": {
|
||||
"expires": 1731716759.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5cb4123b4b425f1d8a6accf4c02386feea7f20bf6171291f715c2cd99bfb02c0": {
|
||||
"expires": 1731665482.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5d9819c229e5f79b767a88db9c78b24998b424a66e8a7ba0039553b7c54051ec": {
|
||||
"expires": 1731617370.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5e6cb57d0f9d97aa3ed75fefd40c8060a085c04727886511f0e7db126b203d43": {
|
||||
"expires": 1731721196.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"5e74fdaca0c6c7e7afec714793677646ff89d00ae35908a7125d6cf50ea0702c": {
|
||||
"expires": 1731603184.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"644aae458bd0a092fe342d1f020d4b7eccff9cf6cfc0677fe0d9531754edaff0": {
|
||||
"expires": 1731692249.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"653493a22274078fc44d52acb337b56bcd4084de2f0a1b6b79be186550a30cb3": {
|
||||
"expires": 1731591365.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"66618b25addefa9f12946c5afb65e8d690c5a871fff3d03fb796751a9eee0d41": {
|
||||
"expires": 1731591683.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"684613e83cfde35d79917108a4091de4c585e1ff627eee0d904920759dc3ea53": {
|
||||
"expires": 1731712313.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"68ecec112a27957857622d4c0ba02824f5e03981db6f51fa01c0be8ac893c6f1": {
|
||||
"expires": 1731591683.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"69d6aa5b5eae433a357dc92fe12bbfb7fd29629425f0edbe55e0ac4e8df112aa": {
|
||||
"expires": 1731718574.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"6c19cb32acd64f6c1543bf07dba3aee5ca5d3ba71f754e1e95acc3da5dc6aa27": {
|
||||
"expires": 1731718629.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"71c8d2cf33b22285f43c5855bdabd9ea25a18b177cfe28056c8bed41776d0ced": {
|
||||
"expires": 1731664874.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"745592d9be94482df01bc76010f58844175050e2bb7c0974de4e1f852a589554": {
|
||||
"expires": 1731689116.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"82ee9a0b4fde768d0580e85633012235fdf4683c0115ec121ba17282075483e7": {
|
||||
"expires": 1731708818.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8346f9faa70bd614131a9115bcb33168ae9af221be0270e967c01e9c1c58129e": {
|
||||
"expires": 1731693061.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"861dbf07e0df2c29fedc8fcd5a346b4dcb1a0ece0f741befc3c72d3fadc82268": {
|
||||
"expires": 1731722565.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"88be8381b68afe4c56b949c2588dec6b57f2dbe5ae20faacb06c37b7cbdc8a8b": {
|
||||
"expires": 1731718780.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8a010b80566da20d356b77df700e2444292eea50a928eb7613bc874462436f36": {
|
||||
"expires": 1731602951.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8be44833b5b32f8ed1eb2e6dba5ec7aa49fc4307dbd18d03859c96d227a9058b": {
|
||||
"expires": 1731658179.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8e2e998fabc8429ef4ba385a4d4ed401fbe2508192b65dc72280cdfc086948e0": {
|
||||
"expires": 1731692291.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8e8ed10c9cb8ced98d7736a81b68990fff019ca2b32dd9093209b132906b68c3": {
|
||||
"expires": 1731692267.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"8fbfaad4b9fc517f848b6f052be51b3a2cd494247078bf053460f8b80e53065a": {
|
||||
"expires": 1731595963.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9364816c6b2e739b647c43ad27b2d52593c2d59a5181aefcda12d04ea31d9fc6": {
|
||||
"expires": 1731718765.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9a29ef6e50f415e5ccd36140eeab85a4409ae271470c1d51a32e0791c75ea588": {
|
||||
"expires": 1731658513.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9b266dafeef7c956bb4d9e987791975b64e7333de860a0d9173e211524cc8540": {
|
||||
"expires": 1731591553.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"9c03a8e39a5ff42c889c73ad9d4d5d84e76747322dfdf714ccb74a0d37923682": {
|
||||
"expires": 1731719394.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a7ba418572d761f89d13fd7a11d682a3c821406ccd29f3e6d683966d0fb3d3ab": {
|
||||
"expires": 1731720041.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"a7cc53afae41ab5e800893c7271acf272f34b604f7e1ace8c3f0232606d01e2b": {
|
||||
"expires": 1731719810.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"aa301c95f4890c38baa814503e940f28143304ad55dbfaea2db8aaec90169031": {
|
||||
"expires": 1731599734.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ac98337f159cdc03b4865b80c78d53ab39502291fa19cc0b60233209e1d92bb7": {
|
||||
"expires": 1731689363.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"adf9d353a020d93e54b32b14c28525a7e6f33fd735144d7b778a4e517192b7c5": {
|
||||
"expires": 1731691941.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ae48fe0ecea0704704bc6327b6bf3258de377b08fa4d76b4f69b8470852960c6": {
|
||||
"expires": 1731591544.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"ae967211fa06ac26d96e3207e67c136efc6367acc92d608e52e1f204b5bd3da4": {
|
||||
"expires": 1731664535.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"b0f55c6cad8bc2230754fd1bbeebfedd31ba244eac96e8d02be6d6a33b542b4f": {
|
||||
"expires": 1731592529.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"b689520f2e3ccf2164da5dba4bac111d4cbc6d3bdcf44361127baa0068623cd0": {
|
||||
"expires": 1731717015.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"bb92c09264296d8c12fac5d2abed2224b25a85d5a46d87f4a4351da76d00566e": {
|
||||
"expires": 1731720040.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"bbba0f20f9c1639ebfcaedb54a87d4b968bc953f72b06105f6d995f5eee9bff7": {
|
||||
"expires": 1731591455.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c1a50b58a364f294755666d0e748ba2a06fbe55de758453180920774525214c8": {
|
||||
"expires": 1731719336.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c6af2f6a6627cbbf18696f1b8b904346726800d05b137d251ee28b699fbd858c": {
|
||||
"expires": 1731665294.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"c86527a0aae330a71fc324a233ad876d420a54d387794374d126e7d6a0f19f92": {
|
||||
"expires": 1731692356.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"cd65741faf869e193c4e2a51e9454cf88598f58987b2ce559ef3d6bfa98e7605": {
|
||||
"expires": 1731591572.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"cdb4f2d14ab7de91b8be1261be40bf59f2bfa0e5e6327166ffa08cfa74eb357a": {
|
||||
"expires": 1731692237.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d0f2c477a500bffcdcfbc56907df1b322200d9f7a801285bbe5440e5bca5e8c4": {
|
||||
"expires": 1731711859.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d3a9186f1c2c81d8085d86c09ddd4fc5e205f61f25f3cabbc1ee379e52304c77": {
|
||||
"expires": 1731718780.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d74ba4649e1a5a098d42c2f62472c94d4484f43b0979cbbd48b464ac5f20e49b": {
|
||||
"expires": 1731716625.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d769dedd53059553cb54961641c0cbdf818533db06764fbcd5557a7200247c28": {
|
||||
"expires": 1731718992.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"d95d211a839a71c0bb00d2504f18deb81af2d0ab7183478543a5d28654a37197": {
|
||||
"expires": 1731658629.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"df84bacd66f8dbf2d1f150d839029e11b3ce56c0536183fd33656685bd446c44": {
|
||||
"expires": 1731719340.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"e8be12b35aa13a67bc7d3332d373b4117493319625c1ed87e3335ab8d09b9054": {
|
||||
"expires": 1731686353.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f04dce449eb7dc69f7973906564c4aa224ff20595a3f2a3bfedbcd23ffaa9117": {
|
||||
"expires": 1731658418.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f4472d087a796ef1f431c44d4f6e9d46ecfe88acce21368a7d85fc31e1efc1e9": {
|
||||
"expires": 1731711611.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
},
|
||||
"f62a451ea2933e56ff9dae2299e374477adb54d8c8285ea59d2ab15b9a80bd13": {
|
||||
"expires": 1731712500.0,
|
||||
"level": 10,
|
||||
"login": "admin"
|
||||
}
|
||||
},
|
||||
"show_session_speed": false,
|
||||
"show_sidebar": true,
|
||||
"sidebar_multiple_filters": true,
|
||||
"sidebar_show_zero": false,
|
||||
"theme": "gray"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<key id="73140dfd-12c2-49d9-93d6-94dd1f0bc538" version="1">
|
||||
<creationDate>2024-11-12T08:27:40.5991235Z</creationDate>
|
||||
<activationDate>2024-11-12T08:27:40.5870855Z</activationDate>
|
||||
<expirationDate>2025-02-10T08:27:40.5870855Z</expirationDate>
|
||||
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<descriptor>
|
||||
<encryption algorithm="AES_256_CBC" />
|
||||
<validation algorithm="HMACSHA256" />
|
||||
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
|
||||
<!-- Warning: the key below is in an unencrypted form. -->
|
||||
<value>FJN9+ak89dkr+ZPZD/LymeCCwH/UI3kNdaMqxSnY6G8bui1yNjGtLpQQOJJlTOAdAyZvHUyPUvv99F70uZF7qg==</value>
|
||||
</masterKey>
|
||||
</descriptor>
|
||||
</descriptor>
|
||||
</key>
|
||||
BIN
code/test/data/lidarr/config/lidarr.db
Normal file
BIN
code/test/data/lidarr/config/lidarr.db
Normal file
Binary file not shown.
1
code/test/data/lidarr/config/lidarr.pid
Normal file
1
code/test/data/lidarr/config/lidarr.pid
Normal file
@@ -0,0 +1 @@
|
||||
145
|
||||
BIN
code/test/data/lidarr/config/logs.db
Normal file
BIN
code/test/data/lidarr/config/logs.db
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user