Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d6f62dd70 | |||
| 77cc5c99ed | |||
| 513134fd65 | |||
| 906be45758 | |||
| c4a15e77e4 | |||
| baffdfdd5a | |||
| 827afb5a4d | |||
| 26939b2cd3 | |||
| 34d05c5416 | |||
| b9376e02fa |
@@ -8,4 +8,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dockerRepository: flaminel/cleanuperr
|
dockerRepository: flaminel/cleanuperr
|
||||||
githubContext: ${{ toJSON(github) }}
|
githubContext: ${{ toJSON(github) }}
|
||||||
|
outputName: cleanuperr
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
# paths:
|
||||||
|
# - 'code/**'
|
||||||
|
# branches: [ main ]
|
||||||
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'code/**'
|
- 'code/**'
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. Add excluded file names to prevent malicious files from being downloaded by qBittorrent.
|
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.
|
2. cleanuperr goes through all items in Sonarr's queue at every 5th minute.
|
||||||
3. For each queue item, a call is made to qBittorrent to get the stats of the torrent.
|
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.
|
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.
|
5. If any malicious torrents have been found, cleanuperr calls Sonarr to automatically search again.
|
||||||
@@ -12,37 +12,49 @@
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
```
|
```
|
||||||
docker run \
|
docker run -d \
|
||||||
-e QuartzConfig__BlockedTorrentTrigger="0 0/10 * * * ?" \
|
-e TRIGGERS__QUEUECLEANER="0 0/5 * * * ?" \
|
||||||
-e QBitConfig__Url="http://localhost:8080" \
|
-e QBITTORRENT__URL="http://localhost:8080" \
|
||||||
-e QBitConfig__Username="user" \
|
-e QBITTORRENT__USERNAME="user" \
|
||||||
-e QBitConfig__Password="pass" \
|
-e QBITTORRENT__PASSWORD="pass" \
|
||||||
-e SonarrConfig__Instances__0__Url="http://localhost:8989" \
|
-e SONARR__ENABLED=true \
|
||||||
-e SonarrConfig__Instances__0__ApiKey="secret1" \
|
-e SONARR__INSTANCES__0__URL="http://localhost:8989" \
|
||||||
-e SonarrConfig__Instances__1__Url="http://localhost:8990" \
|
-e SONARR__INSTANCES__0__APIKEY="secret1" \
|
||||||
-e SonarrConfig__Instances__1__ApiKey="secret2" \
|
-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
|
flaminel/cleanuperr:1.1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Required | Description | Default value |
|
| Variable | Required | Description | Default value |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| QuartzConfig__BlockedTorrentTrigger | No | Quartz cron trigger | 0 0/5 * * * ? |
|
| TRIGGERS__QUEUECLEANER | No | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) | 0 0/5 * * * ? |
|
||||||
| QBitConfig__Url | Yes | qBittorrent instance url | http://localhost:8080 |
|
| QBITTORRENT__URL | Yes | qBittorrent instance url | http://localhost:8080 |
|
||||||
| QBitConfig__Username | Yes | qBittorrent user | empty |
|
| QBITTORRENT__USERNAME | Yes | qBittorrent user | empty |
|
||||||
| QBitConfig__Password | Yes | qBittorrent password | empty |
|
| QBITTORRENT__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 |
|
| 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 |
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
Multiple Sonarr instances can be specified using this format:
|
Multiple Sonarr/Radarr instances can be specified using this format:
|
||||||
|
|
||||||
```
|
```
|
||||||
SonarrConfig__Instances__<NUMBER>__Url
|
SONARR__INSTANCES__<NUMBER>__URL
|
||||||
SonarrConfig__Instances__<NUMBER>__ApiKey
|
SONARR__INSTANCES__<NUMBER>__APIKEY
|
||||||
```
|
```
|
||||||
|
|
||||||
where `<NUMBER>` starts from 0.
|
where `<NUMBER>` starts from 0.
|
||||||
@@ -56,96 +68,524 @@ where `<NUMBER>` starts from 0.
|
|||||||
## Extensions to block in qBittorrent
|
## Extensions to block in qBittorrent
|
||||||
<details>
|
<details>
|
||||||
<summary>Extensions</summary>
|
<summary>Extensions</summary>
|
||||||
<pre><code>*.apk
|
<pre><code>*(sample).*
|
||||||
*.bat
|
*.0xe
|
||||||
*.bin
|
*.73k
|
||||||
*.bmp
|
*.73p
|
||||||
*.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
|
*.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
|
*.bdjo
|
||||||
*.bdmv
|
*.bdmv
|
||||||
|
*.beam
|
||||||
*.bin
|
*.bin
|
||||||
*.bmp
|
*.bmp
|
||||||
|
*.bms
|
||||||
|
*.bns
|
||||||
|
*.bsa
|
||||||
|
*.btm
|
||||||
|
*.bz2
|
||||||
|
*.c
|
||||||
|
*.cab
|
||||||
|
*.caction
|
||||||
*.cci
|
*.cci
|
||||||
|
*.cda
|
||||||
|
*.cdb
|
||||||
|
*.cel
|
||||||
|
*.celx
|
||||||
|
*.cfs
|
||||||
|
*.cgi
|
||||||
|
*.cheat
|
||||||
|
*.chm
|
||||||
|
*.ckpt
|
||||||
|
*.cla
|
||||||
|
*.class
|
||||||
*.clpi
|
*.clpi
|
||||||
|
*.cmd
|
||||||
|
*.cof
|
||||||
|
*.coffee
|
||||||
|
*.com
|
||||||
|
*.command
|
||||||
|
*.conf
|
||||||
|
*.config
|
||||||
|
*.cpl
|
||||||
*.crt
|
*.crt
|
||||||
|
*.cs
|
||||||
|
*.csh
|
||||||
|
*.csharp
|
||||||
|
*.csproj
|
||||||
|
*.css
|
||||||
|
*.csv
|
||||||
|
*.cue
|
||||||
|
*.cur
|
||||||
|
*.cyw
|
||||||
|
*.daemon
|
||||||
|
*.dat
|
||||||
|
*.data-00000-of-00001
|
||||||
|
*.db
|
||||||
|
*.deamon
|
||||||
|
*.deb
|
||||||
|
*.dek
|
||||||
|
*.diz
|
||||||
|
*.dld
|
||||||
*.dll
|
*.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
|
||||||
|
*.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
|
*.html
|
||||||
|
*.icd
|
||||||
|
*.icns
|
||||||
|
*.ico
|
||||||
*.idx
|
*.idx
|
||||||
|
*.iim
|
||||||
|
*.img
|
||||||
|
*.index
|
||||||
*.inf
|
*.inf
|
||||||
|
*.ini
|
||||||
|
*.ink
|
||||||
|
*.ins
|
||||||
|
*.ipa
|
||||||
|
*.ipf
|
||||||
|
*.ipk
|
||||||
|
*.ipsw
|
||||||
|
*.iqylink
|
||||||
|
*.iso
|
||||||
|
*.isp
|
||||||
|
*.isu
|
||||||
|
*.ita
|
||||||
|
*.izh
|
||||||
|
*.izma ace
|
||||||
*.jar
|
*.jar
|
||||||
|
*.java
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.jpg
|
*.jpg
|
||||||
|
*.js
|
||||||
|
*.js_be
|
||||||
|
*.js_le
|
||||||
|
*.jse
|
||||||
|
*.jsf
|
||||||
|
*.json
|
||||||
|
*.jsp
|
||||||
|
*.jsx
|
||||||
|
*.kix
|
||||||
|
*.ksh
|
||||||
|
*.kx
|
||||||
|
*.lck
|
||||||
|
*.ldb
|
||||||
|
*.lib
|
||||||
|
*.link
|
||||||
*.lnk
|
*.lnk
|
||||||
|
*.lo
|
||||||
|
*.lock
|
||||||
|
*.log
|
||||||
|
*.loop-vbs
|
||||||
|
*.ls
|
||||||
|
*.m3u
|
||||||
*.m4a
|
*.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
|
*.mpls
|
||||||
|
*.mrc
|
||||||
|
*.mrp
|
||||||
|
*.ms
|
||||||
|
*.msc
|
||||||
|
*.msh
|
||||||
|
*.msh1
|
||||||
|
*.msh1xml
|
||||||
|
*.msh2
|
||||||
|
*.msh2xml
|
||||||
|
*.mshxml
|
||||||
*.msi
|
*.msi
|
||||||
|
*.msi-nouac
|
||||||
|
*.msix
|
||||||
|
*.msl
|
||||||
|
*.msp
|
||||||
|
*.mst
|
||||||
|
*.msu
|
||||||
|
*.mxe
|
||||||
|
*.n
|
||||||
|
*.ncl
|
||||||
|
*.net
|
||||||
|
*.nexe
|
||||||
*.nfo
|
*.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
|
*.pdf
|
||||||
|
*.pea
|
||||||
|
*.perl
|
||||||
|
*.pex
|
||||||
|
*.phar
|
||||||
|
*.php
|
||||||
|
*.php5
|
||||||
|
*.pif
|
||||||
|
*.pkg
|
||||||
|
*.pl
|
||||||
|
*.plsc
|
||||||
|
*.plx
|
||||||
*.png
|
*.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
|
*.rar
|
||||||
*(sample).*
|
*.raw
|
||||||
*sample.mkv
|
*.rb
|
||||||
*sample.mp4
|
*.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
|
*.sfv
|
||||||
|
*.sh
|
||||||
|
*.shb
|
||||||
|
*.shell
|
||||||
|
*.shortcut
|
||||||
|
*.shs
|
||||||
|
*.shtml
|
||||||
|
*.sit
|
||||||
|
*.sitx
|
||||||
|
*.sk
|
||||||
|
*.sldm
|
||||||
|
*.sln
|
||||||
|
*.smm
|
||||||
|
*.snap
|
||||||
|
*.snd
|
||||||
|
*.spr
|
||||||
|
*.sql
|
||||||
|
*.sqx
|
||||||
|
*.srec
|
||||||
*.srt
|
*.srt
|
||||||
|
*.ssm
|
||||||
|
*.sts
|
||||||
*.sub
|
*.sub
|
||||||
|
*.svg
|
||||||
|
*.swf
|
||||||
|
*.sys
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
*.tbl
|
*.tbl
|
||||||
Trailer.*
|
*.tbz
|
||||||
|
*.tcp
|
||||||
|
*.text
|
||||||
|
*.tf
|
||||||
|
*.tgz
|
||||||
|
*.thm
|
||||||
|
*.thmx
|
||||||
|
*.thumb
|
||||||
|
*.tiapp
|
||||||
|
*.tif
|
||||||
|
*.tiff
|
||||||
|
*.tipa
|
||||||
|
*.tmp
|
||||||
|
*.tms
|
||||||
|
*.toast
|
||||||
|
*.torrent
|
||||||
|
*.tpk
|
||||||
*.txt
|
*.txt
|
||||||
|
*.u3p
|
||||||
|
*.udf
|
||||||
|
*.upk
|
||||||
|
*.upx
|
||||||
*.url
|
*.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
|
*.xig
|
||||||
|
*.xla
|
||||||
|
*.xlam
|
||||||
|
*.xll
|
||||||
|
*.xlm
|
||||||
|
*.xls
|
||||||
|
*.xlsb
|
||||||
|
*.xlsm
|
||||||
|
*.xlsx
|
||||||
|
*.xlt
|
||||||
|
*.xltb
|
||||||
|
*.xltm
|
||||||
|
*.xlw
|
||||||
*.xml
|
*.xml
|
||||||
|
*.xqt
|
||||||
*.xrt
|
*.xrt
|
||||||
|
*.xys
|
||||||
|
*.xz
|
||||||
|
*.ygh
|
||||||
|
*.z
|
||||||
*.zip
|
*.zip
|
||||||
*.zipx
|
*.zipx
|
||||||
*.Lnk
|
*.zl9
|
||||||
|
*.zoo
|
||||||
|
*sample.avchd
|
||||||
|
*sample.avi
|
||||||
|
*sample.mkv
|
||||||
|
*sample.mov
|
||||||
|
*sample.mp4
|
||||||
|
*sample.webm
|
||||||
|
*sample.wmv
|
||||||
|
Trailer.*
|
||||||
|
VOSTFR
|
||||||
|
api
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public abstract record ArrConfig
|
||||||
|
{
|
||||||
|
public required bool Enabled { get; init; }
|
||||||
|
|
||||||
|
public required List<ArrInstance> Instances { get; init; }
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
namespace Common.Configuration;
|
namespace Common.Configuration;
|
||||||
|
|
||||||
public sealed class SonarrInstance
|
public sealed class ArrInstance
|
||||||
{
|
{
|
||||||
public required Uri Url { get; set; }
|
public required Uri Url { get; set; }
|
||||||
|
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
public sealed class QBitConfig
|
public sealed class QBitConfig
|
||||||
{
|
{
|
||||||
|
public const string SectionName = "qBittorrent";
|
||||||
|
|
||||||
public required Uri Url { get; set; }
|
public required Uri Url { get; set; }
|
||||||
|
|
||||||
public required string Username { get; set; }
|
public required string Username { get; set; }
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Common.Configuration;
|
|
||||||
|
|
||||||
public sealed class QuartzConfig
|
|
||||||
{
|
|
||||||
public required string BlockedTorrentTrigger { get; init; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed record RadarrConfig : ArrConfig
|
||||||
|
{
|
||||||
|
public const string SectionName = "Radarr";
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Common.Configuration;
|
namespace Common.Configuration;
|
||||||
|
|
||||||
public sealed class SonarrConfig
|
public sealed record SonarrConfig : ArrConfig
|
||||||
{
|
{
|
||||||
public required List<SonarrInstance> Instances { get; set; }
|
public const string SectionName = "Sonarr";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Common.Configuration;
|
||||||
|
|
||||||
|
public sealed class TriggersConfig
|
||||||
|
{
|
||||||
|
public const string SectionName = "Triggers";
|
||||||
|
|
||||||
|
public required string QueueCleaner { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Domain.Arr.Enums;
|
||||||
|
|
||||||
|
public enum InstanceType
|
||||||
|
{
|
||||||
|
Sonarr,
|
||||||
|
Radarr,
|
||||||
|
Lidarr,
|
||||||
|
Readarr
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Domain.Arr.Queue;
|
||||||
|
|
||||||
|
public record QueueListResponse
|
||||||
|
{
|
||||||
|
public required int TotalRecords { get; init; }
|
||||||
|
public required IReadOnlyList<QueueRecord> Records { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Domain.Arr.Queue;
|
||||||
|
|
||||||
|
public record QueueRecord
|
||||||
|
{
|
||||||
|
public int SeriesId { get; init; }
|
||||||
|
public int MovieId { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public string Status { get; init; }
|
||||||
|
public string TrackedDownloadStatus { get; init; }
|
||||||
|
public string TrackedDownloadState { get; init; }
|
||||||
|
public required string DownloadId { get; init; }
|
||||||
|
public required string Protocol { get; init; }
|
||||||
|
public required int Id { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Radarr;
|
||||||
|
|
||||||
|
public sealed record RadarrCommand
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
public required HashSet<int> MovieIds { get; init; }
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record CustomFormat(
|
|
||||||
int Id,
|
|
||||||
string Name
|
|
||||||
);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record Language(
|
|
||||||
int Id,
|
|
||||||
string Name
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record QueueListResponse(
|
|
||||||
int Page,
|
|
||||||
int PageSize,
|
|
||||||
string SortKey,
|
|
||||||
string SortDirection,
|
|
||||||
int TotalRecords,
|
|
||||||
IReadOnlyList<Record> Records
|
|
||||||
);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record Record(
|
|
||||||
int SeriesId,
|
|
||||||
int EpisodeId,
|
|
||||||
int SeasonNumber,
|
|
||||||
IReadOnlyList<Language> Languages,
|
|
||||||
IReadOnlyList<CustomFormat> CustomFormats,
|
|
||||||
int CustomFormatScore,
|
|
||||||
int Size,
|
|
||||||
string Title,
|
|
||||||
int Sizeleft,
|
|
||||||
string Timeleft,
|
|
||||||
DateTime EstimatedCompletionTime,
|
|
||||||
DateTime Added,
|
|
||||||
string Status,
|
|
||||||
string TrackedDownloadStatus,
|
|
||||||
string TrackedDownloadState,
|
|
||||||
IReadOnlyList<StatusMessage> StatusMessages,
|
|
||||||
string DownloadId,
|
|
||||||
string Protocol,
|
|
||||||
string DownloadClient,
|
|
||||||
bool DownloadClientHasPostImportCategory,
|
|
||||||
string Indexer,
|
|
||||||
string OutputPath,
|
|
||||||
bool EpisodeHasFile,
|
|
||||||
int Id
|
|
||||||
);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record Revision(
|
|
||||||
int Version,
|
|
||||||
int Real,
|
|
||||||
bool IsRepack
|
|
||||||
);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Domain.Sonarr.Queue;
|
|
||||||
|
|
||||||
public record StatusMessage(
|
|
||||||
string Title,
|
|
||||||
IReadOnlyList<string> Messages
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Sonarr;
|
||||||
|
|
||||||
|
public sealed record SonarrCommand
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
public required int SeriesId { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Common.Configuration;
|
using Common.Configuration;
|
||||||
using Executable.Jobs;
|
using Executable.Jobs;
|
||||||
using Infrastructure.Verticals.BlockedTorrent;
|
using Infrastructure.Verticals.Arr;
|
||||||
|
using Infrastructure.Verticals.QueueCleaner;
|
||||||
|
|
||||||
namespace Executable;
|
namespace Executable;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
@@ -17,44 +18,46 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
private static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
private static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
services
|
services
|
||||||
.Configure<QuartzConfig>(configuration.GetSection(nameof(QuartzConfig)))
|
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
|
||||||
.Configure<QBitConfig>(configuration.GetSection(nameof(QBitConfig)))
|
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
|
||||||
.Configure<SonarrConfig>(configuration.GetSection(nameof(SonarrConfig)));
|
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName));
|
||||||
|
|
||||||
private static IServiceCollection AddServices(this IServiceCollection services) =>
|
private static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||||
services
|
services
|
||||||
.AddTransient<BlockedTorrentJob>()
|
.AddTransient<SonarrClient>()
|
||||||
.AddTransient<BlockedTorrentHandler>();
|
.AddTransient<RadarrClient>()
|
||||||
|
.AddTransient<QueueCleanerJob>()
|
||||||
|
.AddTransient<QueueCleanerHandler>();
|
||||||
|
|
||||||
private static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
private static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
services
|
services
|
||||||
.AddQuartz(q =>
|
.AddQuartz(q =>
|
||||||
{
|
{
|
||||||
QuartzConfig? config = configuration.GetRequiredSection(nameof(QuartzConfig)).Get<QuartzConfig>();
|
TriggersConfig? config = configuration.GetRequiredSection(TriggersConfig.SectionName).Get<TriggersConfig>();
|
||||||
|
|
||||||
if (config is null)
|
if (config is null)
|
||||||
{
|
{
|
||||||
throw new NullReferenceException("Quartz configuration is null");
|
throw new NullReferenceException("Quartz configuration is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
q.AddBlockedTorrentJob(config.BlockedTorrentTrigger);
|
q.AddQueueCleanerJob(config.QueueCleaner);
|
||||||
})
|
})
|
||||||
.AddQuartzHostedService(opt =>
|
.AddQuartzHostedService(opt =>
|
||||||
{
|
{
|
||||||
opt.WaitForJobsToComplete = true;
|
opt.WaitForJobsToComplete = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
private static void AddBlockedTorrentJob(this IServiceCollectionQuartzConfigurator q, string trigger)
|
private static void AddQueueCleanerJob(this IServiceCollectionQuartzConfigurator q, string trigger)
|
||||||
{
|
{
|
||||||
q.AddJob<BlockedTorrentJob>(opts =>
|
q.AddJob<QueueCleanerJob>(opts =>
|
||||||
{
|
{
|
||||||
opts.WithIdentity(nameof(BlockedTorrentJob));
|
opts.WithIdentity(nameof(QueueCleanerJob));
|
||||||
});
|
});
|
||||||
|
|
||||||
q.AddTrigger(opts =>
|
q.AddTrigger(opts =>
|
||||||
{
|
{
|
||||||
opts.ForJob(nameof(BlockedTorrentJob))
|
opts.ForJob(nameof(QueueCleanerJob))
|
||||||
.WithIdentity($"{nameof(BlockedTorrentJob)}-trigger")
|
.WithIdentity($"{nameof(QueueCleanerJob)}-trigger")
|
||||||
.WithCronSchedule(trigger);
|
.WithCronSchedule(trigger);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Infrastructure.Verticals.QueueCleaner;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace Executable.Jobs;
|
||||||
|
|
||||||
|
[DisallowConcurrentExecution]
|
||||||
|
public sealed class QueueCleanerJob : IJob
|
||||||
|
{
|
||||||
|
private ILogger<QueueCleanerJob> _logger;
|
||||||
|
private QueueCleanerHandler _handler;
|
||||||
|
|
||||||
|
public QueueCleanerJob(ILogger<QueueCleanerJob> logger, QueueCleanerHandler handler)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _handler.HandleAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"{nameof(QueueCleanerJob)} failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"QuartzConfig": {
|
"Triggers": {
|
||||||
"BlockedTorrentTrigger": "0 0/1 * * * ?"
|
"QueueCleaner": "0 0/1 * * * ?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,30 @@
|
|||||||
"System.Net.Http.HttpClient": "Error"
|
"System.Net.Http.HttpClient": "Error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"QuartzConfig": {
|
"Triggers": {
|
||||||
"BlockedTorrentTrigger": "0 0/5 * * * ?"
|
"QueueCleaner": "0 0/5 * * * ?"
|
||||||
},
|
},
|
||||||
"QBitConfig": {
|
"qBittorrent": {
|
||||||
"Url": "http://localhost:8080",
|
"Url": "http://localhost:8080",
|
||||||
"Username": "",
|
"Username": "",
|
||||||
"Password": ""
|
"Password": ""
|
||||||
},
|
},
|
||||||
"SonarrConfig": {
|
"Sonarr": {
|
||||||
|
"Enabled": true,
|
||||||
"Instances": [
|
"Instances": [
|
||||||
{
|
{
|
||||||
"Url": "http://localhost:8989",
|
"Url": "http://localhost:8989",
|
||||||
"ApiKey": ""
|
"ApiKey": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"Radarr": {
|
||||||
|
"Enabled": false,
|
||||||
|
"Instances": [
|
||||||
|
{
|
||||||
|
"Url": "http://localhost:7878",
|
||||||
|
"ApiKey": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Common.Configuration;
|
||||||
|
using Domain.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Common.Configuration;
|
||||||
|
using Domain.Sonarr;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.Arr;
|
||||||
|
|
||||||
|
public sealed class SonarrClient : ArrClient
|
||||||
|
{
|
||||||
|
public SonarrClient(ILogger<SonarrClient> logger, IHttpClientFactory httpClientFactory)
|
||||||
|
: base(logger, httpClientFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds)
|
||||||
|
{
|
||||||
|
foreach (int itemId in itemIds)
|
||||||
|
{
|
||||||
|
Uri uri = new(arrInstance.Url, "/api/v3/command");
|
||||||
|
SonarrCommand command = new()
|
||||||
|
{
|
||||||
|
Name = "SeriesSearch",
|
||||||
|
SeriesId = itemId
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Post, uri);
|
||||||
|
request.Content = new StringContent(
|
||||||
|
JsonConvert.SerializeObject(command),
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
SetApiKey(request, arrInstance.ApiKey);
|
||||||
|
|
||||||
|
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
_logger.LogInformation("series search triggered | {url} | series id: {id}", arrInstance.Url, itemId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_logger.LogError("series search failed | {url} | series id: {id}", arrInstance.Url, itemId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Common.Configuration;
|
|
||||||
using Domain.Sonarr.Queue;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using QBittorrent.Client;
|
|
||||||
|
|
||||||
namespace Infrastructure.Verticals.BlockedTorrent;
|
|
||||||
|
|
||||||
public sealed class BlockedTorrentHandler
|
|
||||||
{
|
|
||||||
private readonly ILogger<BlockedTorrentHandler> _logger;
|
|
||||||
private readonly QBitConfig _qBitConfig;
|
|
||||||
private readonly SonarrConfig _sonarrConfig;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
private const string QueueListPathTemplate = "/api/v3/queue?page={0}&pageSize=200&sortKey=timeleft";
|
|
||||||
private const string QueueDeletePathTemplate = "/api/v3/queue/{0}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
|
|
||||||
private const string SonarrCommandUriPath = "/api/v3/command";
|
|
||||||
private const string SearchCommandPayloadTemplate = "{\"name\":\"SeriesSearch\",\"seriesId\":{0}}";
|
|
||||||
|
|
||||||
public BlockedTorrentHandler(
|
|
||||||
ILogger<BlockedTorrentHandler> logger,
|
|
||||||
IOptions<QBitConfig> qBitConfig,
|
|
||||||
IOptions<SonarrConfig> sonarrConfig,
|
|
||||||
IHttpClientFactory httpClientFactory)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_qBitConfig = qBitConfig.Value;
|
|
||||||
_sonarrConfig = sonarrConfig.Value;
|
|
||||||
_httpClient = httpClientFactory.CreateClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleAsync()
|
|
||||||
{
|
|
||||||
QBittorrentClient qBitClient = new(_qBitConfig.Url);
|
|
||||||
|
|
||||||
await qBitClient.LoginAsync(_qBitConfig.Username, _qBitConfig.Password);
|
|
||||||
|
|
||||||
foreach (SonarrInstance sonarrInstance in _sonarrConfig.Instances)
|
|
||||||
{
|
|
||||||
ushort page = 1;
|
|
||||||
int totalRecords = 0;
|
|
||||||
int processedRecords = 0;
|
|
||||||
HashSet<int> seriesToBeRefreshed = [];
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
QueueListResponse queueResponse = await ListQueuedTorrentsAsync(sonarrInstance, page);
|
|
||||||
|
|
||||||
if (totalRecords is 0)
|
|
||||||
{
|
|
||||||
totalRecords = queueResponse.TotalRecords;
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"{items} items found in queue | {url}",
|
|
||||||
queueResponse.TotalRecords, sonarrInstance.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Record record in queueResponse.Records)
|
|
||||||
{
|
|
||||||
var torrent = (await qBitClient.GetTorrentListAsync(new TorrentListQuery { Hashes = [record.DownloadId] }))
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (torrent is not { CompletionOn: not null, Downloaded: null or 0 })
|
|
||||||
{
|
|
||||||
_logger.LogInformation("skip | {torrent}", record.Title);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesToBeRefreshed.Add(record.SeriesId);
|
|
||||||
|
|
||||||
await DeleteTorrentFromQueueAsync(sonarrInstance, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queueResponse.Records.Count is 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
processedRecords += queueResponse.Records.Count;
|
|
||||||
|
|
||||||
if (processedRecords >= totalRecords)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
page++;
|
|
||||||
} while (processedRecords < totalRecords);
|
|
||||||
|
|
||||||
foreach (int id in seriesToBeRefreshed)
|
|
||||||
{
|
|
||||||
await RefreshSeriesAsync(sonarrInstance, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<QueueListResponse> ListQueuedTorrentsAsync(SonarrInstance sonarrInstance, int page)
|
|
||||||
{
|
|
||||||
Uri sonarrUri = new(sonarrInstance.Url, string.Format(QueueListPathTemplate, page));
|
|
||||||
|
|
||||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Get, sonarrUri);
|
|
||||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.LogError("queue list failed | {uri}", sonarrUri);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
string responseBody = await response.Content.ReadAsStringAsync();
|
|
||||||
QueueListResponse? queueResponse = JsonConvert.DeserializeObject<QueueListResponse>(responseBody);
|
|
||||||
|
|
||||||
if (queueResponse is null)
|
|
||||||
{
|
|
||||||
throw new Exception($"unrecognized response | {responseBody}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return queueResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteTorrentFromQueueAsync(SonarrInstance sonarrInstance, Record record)
|
|
||||||
{
|
|
||||||
Uri sonarrUri = new(sonarrInstance.Url, string.Format(QueueDeletePathTemplate, record.Id));
|
|
||||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Delete, sonarrUri);
|
|
||||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
_logger.LogInformation("queue item deleted | {record}", record.Title);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.LogError("queue delete failed | {uri}", sonarrUri);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshSeriesAsync(SonarrInstance sonarrInstance, int seriesId)
|
|
||||||
{
|
|
||||||
Uri sonarrUri = new(sonarrInstance.Url, SonarrCommandUriPath);
|
|
||||||
using HttpRequestMessage sonarrRequest = new(HttpMethod.Post, sonarrUri);
|
|
||||||
sonarrRequest.Content = new StringContent(
|
|
||||||
SearchCommandPayloadTemplate.Replace("{0}", seriesId.ToString()),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json"
|
|
||||||
);
|
|
||||||
sonarrRequest.Headers.Add("x-api-key", sonarrInstance.ApiKey);
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.SendAsync(sonarrRequest);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
_logger.LogInformation("series search triggered | series id: {id}", seriesId);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.LogError("series search failed | series id: {id}", seriesId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using Common.Configuration;
|
||||||
|
using Domain.Arr.Enums;
|
||||||
|
using Domain.Arr.Queue;
|
||||||
|
using Infrastructure.Verticals.Arr;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using QBittorrent.Client;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.QueueCleaner;
|
||||||
|
|
||||||
|
public sealed class QueueCleanerHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<QueueCleanerHandler> _logger;
|
||||||
|
private readonly QBitConfig _qBitConfig;
|
||||||
|
private readonly SonarrConfig _sonarrConfig;
|
||||||
|
private readonly RadarrConfig _radarrConfig;
|
||||||
|
private readonly SonarrClient _sonarrClient;
|
||||||
|
private readonly RadarrClient _radarrClient;
|
||||||
|
|
||||||
|
public QueueCleanerHandler(
|
||||||
|
ILogger<QueueCleanerHandler> logger,
|
||||||
|
IOptions<QBitConfig> qBitConfig,
|
||||||
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
|
SonarrClient sonarrClient,
|
||||||
|
RadarrClient radarrClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_qBitConfig = qBitConfig.Value;
|
||||||
|
_sonarrConfig = sonarrConfig.Value;
|
||||||
|
_radarrConfig = radarrConfig.Value;
|
||||||
|
_sonarrClient = sonarrClient;
|
||||||
|
_radarrClient = radarrClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleAsync()
|
||||||
|
{
|
||||||
|
QBittorrentClient qBitClient = new(_qBitConfig.Url);
|
||||||
|
await qBitClient.LoginAsync(_qBitConfig.Username, _qBitConfig.Password);
|
||||||
|
|
||||||
|
await ProcessArrConfigAsync(qBitClient, _sonarrConfig, InstanceType.Sonarr);
|
||||||
|
await ProcessArrConfigAsync(qBitClient, _radarrConfig, InstanceType.Radarr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessArrConfigAsync(QBittorrentClient qBitClient, ArrConfig config, InstanceType instanceType)
|
||||||
|
{
|
||||||
|
if (!config.Enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ArrInstance arrInstance in config.Instances)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessInstanceAsync(qBitClient, arrInstance, instanceType);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError(exception, "failed to clean {type} instance | {url}", instanceType, arrInstance.Url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessInstanceAsync(QBittorrentClient qBitClient, ArrInstance instance, InstanceType instanceType)
|
||||||
|
{
|
||||||
|
ushort page = 1;
|
||||||
|
int totalRecords = 0;
|
||||||
|
int processedRecords = 0;
|
||||||
|
HashSet<int> itemsToBeRefreshed = [];
|
||||||
|
ArrClient arrClient = GetClient(instanceType);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
QueueListResponse queueResponse = await arrClient.GetQueueItemsAsync(instance, page);
|
||||||
|
|
||||||
|
if (totalRecords is 0)
|
||||||
|
{
|
||||||
|
totalRecords = queueResponse.TotalRecords;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"{items} items found in queue | {url}",
|
||||||
|
queueResponse.TotalRecords, instance.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (QueueRecord record in queueResponse.Records)
|
||||||
|
{
|
||||||
|
if (record.Protocol is not "torrent")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TorrentInfo? 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsToBeRefreshed.Add(GetRecordId(instanceType, record));
|
||||||
|
|
||||||
|
await arrClient.DeleteQueueItemAsync(instance, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueResponse.Records.Count is 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedRecords += queueResponse.Records.Count;
|
||||||
|
|
||||||
|
if (processedRecords >= totalRecords)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
|
} while (processedRecords < totalRecords);
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
InstanceType.Sonarr => record.SeriesId,
|
||||||
|
InstanceType.Radarr => record.MovieId,
|
||||||
|
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user