Compare commits

...

27 Commits

Author SHA1 Message Date
Marius Nechifor
54cabd98b4 remove empty creds restriction (#10)
* removed empty checks on qbit and deluge credentials

* updated configuration readme
2024-11-19 23:54:16 +02:00
Marius Nechifor
cbc5c571b3 fixed missing torrent check on content blocker (#9) 2024-11-19 23:23:22 +02:00
Marius Nechifor
beea640d49 fixed content blocker crashing on empty download id (#8) 2024-11-19 23:02:43 +02:00
Marius Nechifor
65b8a7f988 removed Transmission validation on empty credentials (#6) 2024-11-19 13:46:40 +02:00
Flaminel
b86173d6c0 updated readme with more details 2024-11-19 00:16:43 +02:00
Flaminel
09c4b2a28e added permissive blacklist 2024-11-19 00:16:20 +02:00
Flaminel
6e8545e4ca updated readme to include a short description 2024-11-19 00:05:38 +02:00
Marius Nechifor
e0a6c7842b add content blocker (#5)
* refactored code
added deluge support
added transmission support
added content blocker
added blacklist and whitelist

* increased level on some logs; updated test docker compose; updated dev appsettings

* updated docker compose and readme

* moved some logs

* fixed env var typo; fixed sonarr and radarr default download client
2024-11-18 20:08:01 +02:00
Flaminel
b323cb40ae added blocklists 2024-11-18 17:34:23 +02:00
Flaminel
95a35f9988 updated readme with Windows Service instructions 2024-11-15 08:24:34 +02:00
Flaminel
a7f3bad191 updated readme to make it clear what binaries are for 2024-11-15 08:24:30 +02:00
Flaminel
ed20b67b92 added readme discord server announcement 2024-11-14 22:58:23 +02:00
Manuel González Martínez
42a3f75d94 Update README.md (#4)
Added docker compose yaml
2024-11-14 22:43:02 +02:00
Flaminel
6de882d12e changed image tag in readme 2024-11-14 22:27:04 +02:00
Flaminel
6587014e8d fixed exiting after one torrent processed 2024-11-14 22:24:01 +02:00
Marius Nechifor
36a07b251a add test environment 2024-11-14 13:23:49 +02:00
Marius Nechifor
c48eed7f77 create LICENSE 2024-11-14 10:41:12 +02:00
Flaminel
48f3c3b35b fixed readme 2024-11-14 09:14:19 +02:00
Flaminel
0d6f62dd70 disabled main branch pipeline; enabled pipeline on tag 2024-11-13 22:43:10 +02:00
Flaminel
77cc5c99ed updated readme 2024-11-13 22:39:57 +02:00
Marius Nechifor
513134fd65 added support for Radarr 2024-11-13 22:37:00 +02:00
Flaminel
906be45758 enabled pull request builds 2024-11-13 10:22:05 +02:00
Flaminel
c4a15e77e4 fixed project and output name missmatch 2024-11-12 18:43:00 +02:00
Flaminel
baffdfdd5a revert pipeline branch filter 2024-11-12 16:54:00 +02:00
Flaminel
827afb5a4d disabled branch pipeline filter 2024-11-12 15:57:21 +02:00
Flaminel
26939b2cd3 fixed readme typo 2024-11-12 13:18:34 +02:00
Flaminel
34d05c5416 updated extensions list 2024-11-12 09:34:58 +02:00
181 changed files with 6126 additions and 482 deletions

View File

@@ -8,4 +8,5 @@ jobs:
with:
dockerRepository: flaminel/cleanuperr
githubContext: ${{ toJSON(github) }}
outputName: cleanuperr
secrets: inherit

View File

@@ -1,8 +1,13 @@
on:
push:
tags:
- "v*.*.*"
# paths:
# - 'code/**'
# branches: [ main ]
pull_request:
paths:
- 'code/**'
branches: [ main ]
jobs:
build:

View File

@@ -1,5 +1,3 @@
name: Release
on:
push:
tags:

4
.gitignore vendored
View File

@@ -165,3 +165,7 @@ src/.idea/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/
**/logs/
**/MediaCover/
**/archive/

21
LICENSE Normal file
View 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
View File

@@ -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
View 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
View 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

View 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; }
}

View File

@@ -1,6 +1,6 @@
namespace Common.Configuration;
public sealed class SonarrInstance
public sealed class ArrInstance
{
public required Uri Url { get; set; }

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.ContentBlocker;
public sealed record PatternConfig
{
public bool Enabled { get; init; }
public string? Path { get; init; }
}

View 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));
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public interface IConfig
{
void Validate();
}

View File

@@ -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));
}
}
}

View File

@@ -1,6 +0,0 @@
namespace Common.Configuration;
public sealed class QuartzConfig
{
public required string BlockedTorrentTrigger { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.QueueCleaner;
public sealed record QueueCleanerConfig
{
public const string SectionName = "QueueCleaner";
public required bool Enabled { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public sealed record RadarrConfig : ArrConfig
{
public const string SectionName = "Radarr";
}

View File

@@ -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";
}

View 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));
}
}
}

View 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; }
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace Domain.Enums;
public enum BlocklistType
{
Blacklist,
Whitelist
}

View File

@@ -0,0 +1,9 @@
namespace Domain.Enums;
public enum InstanceType
{
Sonarr,
Radarr,
Lidarr,
Readarr
}

View 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; }
}

View 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; }
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Deluge.Exceptions;
public class DelugeClientException : Exception
{
public DelugeClientException(string message) : base(message)
{
}
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Deluge.Exceptions;
public sealed class DelugeLoginException : DelugeClientException
{
public DelugeLoginException() : base("login failed")
{
}
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Deluge.Exceptions;
public sealed class DelugeLogoutException : DelugeClientException
{
public DelugeLogoutException() : base("logout failed")
{
}
}

View 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;
}
}

View 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
}

View 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; }
}

View 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; }
}

View File

@@ -0,0 +1,6 @@
namespace Domain.Models.Deluge.Response;
public sealed record DelugeMinimalStatus
{
public string? Hash { get; set; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View File

@@ -1,6 +0,0 @@
namespace Domain.Sonarr.Queue;
public record CustomFormat(
int Id,
string Name
);

View File

@@ -1,6 +0,0 @@
namespace Domain.Sonarr.Queue;
public record Language(
int Id,
string Name
);

View File

@@ -1,10 +0,0 @@
namespace Domain.Sonarr.Queue;
public record QueueListResponse(
int Page,
int PageSize,
string SortKey,
string SortDirection,
int TotalRecords,
IReadOnlyList<Record> Records
);

View File

@@ -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
);

View File

@@ -1,7 +0,0 @@
namespace Domain.Sonarr.Queue;
public record Revision(
int Version,
int Real,
bool IsRepack
);

View File

@@ -1,6 +0,0 @@
namespace Domain.Sonarr.Queue;
public record StatusMessage(
string Title,
IReadOnlyList<string> Messages
);

View File

@@ -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);
});
}
}

View 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));
}

View 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;
}
}

View 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());
});
}
}

View 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>();
}

View File

@@ -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");
}
}
}

View 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");
}
}
}

View 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");
}
}
}

View File

@@ -1,4 +1,4 @@
using Executable;
using Executable.DependencyInjection;
var builder = Host.CreateApplicationBuilder(args);

View File

@@ -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"
}
]
}
}

View File

@@ -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": ""
}
]
}
}

View File

@@ -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" />

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -0,0 +1,2 @@
.*sample.*
*.zipx

View File

@@ -0,0 +1 @@
*.mkv

View File

Binary file not shown.

View File

@@ -0,0 +1 @@
localclient:da4d4b43be734d48c1bb8b9ab0e39894520994e3:10

View 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": []
}

View 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
}

View 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
}

View File

@@ -0,0 +1,14 @@
{
"file": 3,
"format": 1
}{
"hosts": [
[
"b5408e9794dd432789c55d8c46d15275",
"127.0.0.1",
58846,
"localclient",
"da4d4b43be734d48c1bb8b9ab0e39894520994e3"
]
]
}

View 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"
}
}

View 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"
}
}

View File

@@ -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);

View File

@@ -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);

View File

Binary file not shown.

View File

Binary file not shown.

View 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-----

View 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-----

View File

@@ -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

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View 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"
}

View 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"
}

View File

@@ -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>

View File

Binary file not shown.

View File

@@ -0,0 +1 @@
145

View File

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More