mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-06 12:58:00 -05:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e823b8d3 | ||
|
|
f2f11e3472 | ||
|
|
a3549c80a9 | ||
|
|
2b9c347ed6 | ||
|
|
98ccee866d | ||
|
|
911849c6dd | ||
|
|
cce3bb2c4a | ||
|
|
bcc117cd0d | ||
|
|
8e20a68ae2 | ||
|
|
736c146f25 | ||
|
|
6398ef1cc6 | ||
|
|
83e6a289be | ||
|
|
5662118b01 | ||
|
|
22dfc7b40d | ||
|
|
a51e387453 | ||
|
|
c7d2ec7311 | ||
|
|
bb9ac5b67b | ||
|
|
f93494adb2 | ||
|
|
7201520411 | ||
|
|
2a1e65e1af | ||
|
|
da318c3339 | ||
|
|
7149b6243f | ||
|
|
11f5a28c04 | ||
|
|
9cc36c7a50 | ||
|
|
861c135cc6 | ||
|
|
3b0275c411 | ||
|
|
cad1b51202 | ||
|
|
f50acd29f4 | ||
|
|
af11d595d8 | ||
|
|
44994d5b21 | ||
|
|
592fd2d846 | ||
|
|
e96be1fca2 | ||
|
|
ee44e2b5ac | ||
|
|
323bfc4d2e | ||
|
|
dca45585ca | ||
|
|
8b5918d221 | ||
|
|
9c227c1f59 | ||
|
|
2ad4499a6f | ||
|
|
33a5bf9ab3 | ||
|
|
de06d1c2d3 | ||
|
|
72855bc030 | ||
|
|
b185ea6899 | ||
|
|
1e0127e97e | ||
|
|
5bdbc98d68 | ||
|
|
e1aeb3da31 | ||
|
|
283b09e8f1 | ||
|
|
b03c96249b | ||
|
|
2971445090 | ||
|
|
55c23419cd | ||
|
|
c4b9d9503a | ||
|
|
823b73d9f0 | ||
|
|
31632d25a4 | ||
|
|
c59951a39c | ||
|
|
d9140d7b5b | ||
|
|
90865a73b5 | ||
|
|
cc45233223 | ||
|
|
5d12d601ae | ||
|
|
88f40438af | ||
|
|
0a9ec06841 | ||
|
|
a0ca6ec4b8 | ||
|
|
eb6cf96470 | ||
|
|
2ca0616771 | ||
|
|
bc85144e60 | ||
|
|
236e31c841 | ||
|
|
7a15139aa6 | ||
|
|
fb6ccfd011 | ||
|
|
ef85e2b690 | ||
|
|
bb734230aa | ||
|
|
aa31c31955 | ||
|
|
1a89822f36 | ||
|
|
fc9e0eca36 | ||
|
|
0010dcb1c6 | ||
|
|
0ab8611f29 | ||
|
|
9e02408a7e | ||
|
|
1bd0db05e6 | ||
|
|
fb438f2ca7 | ||
|
|
d4de7f2ec3 | ||
|
|
98ee1943f9 | ||
|
|
4a57c0fba3 | ||
|
|
db0698d515 | ||
|
|
712cc9ff1e | ||
|
|
501be0e4e7 | ||
|
|
19b7613eea | ||
|
|
3d9cd8f6a9 | ||
|
|
c8add22d3d | ||
|
|
69d8cc8fa0 | ||
|
|
8b8a4b3837 | ||
|
|
c45006f219 | ||
|
|
bc306a37c9 | ||
|
|
aab0487020 | ||
|
|
ca892ce188 | ||
|
|
cff5dc20e5 |
2
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
@@ -14,7 +14,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: Reviewed the documentation.
|
- label: Reviewed the documentation.
|
||||||
required: true
|
required: true
|
||||||
- label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
|
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
|
||||||
required: true
|
required: true
|
||||||
- label: Ensured I am using the latest version.
|
- label: Ensured I am using the latest version.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/3-help.yml
vendored
2
.github/ISSUE_TEMPLATE/3-help.yml
vendored
@@ -14,7 +14,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: Reviewed the documentation.
|
- label: Reviewed the documentation.
|
||||||
required: true
|
required: true
|
||||||
- label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
|
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
|
||||||
required: true
|
required: true
|
||||||
- label: Ensured I am using the latest version.
|
- label: Ensured I am using the latest version.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
21
.github/workflows/build-docker.yml
vendored
21
.github/workflows/build-docker.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
|||||||
githubHeadRef=${{ env.githubHeadRef }}
|
githubHeadRef=${{ env.githubHeadRef }}
|
||||||
latestDockerTag=""
|
latestDockerTag=""
|
||||||
versionDockerTag=""
|
versionDockerTag=""
|
||||||
|
majorVersionDockerTag=""
|
||||||
|
minorVersionDockerTag=""
|
||||||
version="0.0.1"
|
version="0.0.1"
|
||||||
|
|
||||||
if [[ "$githubRef" =~ ^"refs/tags/" ]]; then
|
if [[ "$githubRef" =~ ^"refs/tags/" ]]; then
|
||||||
@@ -36,6 +38,12 @@ jobs:
|
|||||||
latestDockerTag="latest"
|
latestDockerTag="latest"
|
||||||
versionDockerTag=${branch#v}
|
versionDockerTag=${branch#v}
|
||||||
version=${branch#v}
|
version=${branch#v}
|
||||||
|
|
||||||
|
# Extract major and minor versions for additional tags
|
||||||
|
if [[ "$versionDockerTag" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
|
||||||
|
majorVersionDockerTag="${BASH_REMATCH[1]}"
|
||||||
|
minorVersionDockerTag="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Determine if this run is for the main branch or another branch
|
# Determine if this run is for the main branch or another branch
|
||||||
if [[ -z "$githubHeadRef" ]]; then
|
if [[ -z "$githubHeadRef" ]]; then
|
||||||
@@ -53,11 +61,16 @@ jobs:
|
|||||||
githubTags=""
|
githubTags=""
|
||||||
|
|
||||||
if [ -n "$latestDockerTag" ]; then
|
if [ -n "$latestDockerTag" ]; then
|
||||||
githubTags="$githubTags,ghcr.io/cleanuparr:$latestDockerTag"
|
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$latestDockerTag"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$versionDockerTag" ]; then
|
if [ -n "$versionDockerTag" ]; then
|
||||||
githubTags="$githubTags,ghcr.io/cleanuparr:$versionDockerTag"
|
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$versionDockerTag"
|
||||||
|
fi
|
||||||
|
if [ -n "$minorVersionDockerTag" ]; then
|
||||||
|
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$minorVersionDockerTag"
|
||||||
|
fi
|
||||||
|
if [ -n "$majorVersionDockerTag" ]; then
|
||||||
|
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$majorVersionDockerTag"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# set env vars
|
# set env vars
|
||||||
@@ -113,7 +126,7 @@ jobs:
|
|||||||
version=${{ env.versionDockerTag }}
|
version=${{ env.versionDockerTag }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ env.version }}
|
VERSION=${{ env.version }}
|
||||||
PACKAGES_USERNAME=${{ env.PACKAGES_USERNAME }}
|
PACKAGES_USERNAME=${{ secrets.PACKAGES_USERNAME }}
|
||||||
PACKAGES_PAT=${{ env.PACKAGES_PAT }}
|
PACKAGES_PAT=${{ env.PACKAGES_PAT }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image
|
type=image
|
||||||
|
|||||||
42
.github/workflows/build-executable.yml
vendored
42
.github/workflows/build-executable.yml
vendored
@@ -101,28 +101,6 @@ jobs:
|
|||||||
- name: Build osx-arm64
|
- name: Build osx-arm64
|
||||||
run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime osx-arm64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
|
run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime osx-arm64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
|
||||||
|
|
||||||
- name: Create sample configuration files
|
|
||||||
run: |
|
|
||||||
# Create a sample appsettings.json for each platform
|
|
||||||
cat > sample-config.json << 'EOF'
|
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Copy to each build directory
|
|
||||||
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64/appsettings.json
|
|
||||||
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64/appsettings.json
|
|
||||||
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64/appsettings.json
|
|
||||||
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64/appsettings.json
|
|
||||||
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64/appsettings.json
|
|
||||||
|
|
||||||
- name: Zip win-x64
|
- name: Zip win-x64
|
||||||
run: |
|
run: |
|
||||||
cd ./artifacts
|
cd ./artifacts
|
||||||
@@ -156,22 +134,4 @@ jobs:
|
|||||||
./artifacts/*.zip
|
./artifacts/*.zip
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Release
|
# Removed individual release step - handled by main release workflow
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
id: release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
name: ${{ env.releaseVersion }}
|
|
||||||
tag_name: ${{ env.releaseVersion }}
|
|
||||||
repository: ${{ env.githubRepository }}
|
|
||||||
token: ${{ env.REPO_READONLY_PAT }}
|
|
||||||
make_latest: true
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
target_commitish: main
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64.zip
|
|
||||||
./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64.zip
|
|
||||||
./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64.zip
|
|
||||||
./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64.zip
|
|
||||||
./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64.zip
|
|
||||||
12
.github/workflows/build-macos-arm-installer.yml
vendored
12
.github/workflows/build-macos-arm-installer.yml
vendored
@@ -363,14 +363,4 @@ jobs:
|
|||||||
path: '${{ env.pkgName }}'
|
path: '${{ env.pkgName }}'
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Release
|
# Removed individual release step - handled by main release workflow
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
name: ${{ env.releaseVersion }}
|
|
||||||
tag_name: ${{ env.releaseVersion }}
|
|
||||||
repository: ${{ env.githubRepository }}
|
|
||||||
token: ${{ env.REPO_READONLY_PAT }}
|
|
||||||
make_latest: true
|
|
||||||
files: |
|
|
||||||
${{ env.pkgName }}
|
|
||||||
@@ -363,14 +363,4 @@ jobs:
|
|||||||
path: '${{ env.pkgName }}'
|
path: '${{ env.pkgName }}'
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Release
|
# Removed individual release step - handled by main release workflow
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
name: ${{ env.releaseVersion }}
|
|
||||||
tag_name: ${{ env.releaseVersion }}
|
|
||||||
repository: ${{ env.githubRepository }}
|
|
||||||
token: ${{ env.REPO_READONLY_PAT }}
|
|
||||||
make_latest: true
|
|
||||||
files: |
|
|
||||||
${{ env.pkgName }}
|
|
||||||
25
.github/workflows/build-windows-installer.yml
vendored
25
.github/workflows/build-windows-installer.yml
vendored
@@ -88,19 +88,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime win-x64 --self-contained -o dist /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugType=None /p:DebugSymbols=false
|
dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime win-x64 --self-contained -o dist /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugType=None /p:DebugSymbols=false
|
||||||
|
|
||||||
- name: Create sample configuration
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
# Create config directory
|
|
||||||
New-Item -ItemType Directory -Force -Path "config"
|
|
||||||
|
|
||||||
$config = @{
|
|
||||||
"HTTP_PORTS" = 11011
|
|
||||||
"BASE_PATH" = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
$config | ConvertTo-Json | Out-File -FilePath "config/cleanuparr.json" -Encoding UTF8
|
|
||||||
|
|
||||||
- name: Setup Inno Setup
|
- name: Setup Inno Setup
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
@@ -158,14 +145,4 @@ jobs:
|
|||||||
path: installer/${{ env.installerName }}
|
path: installer/${{ env.installerName }}
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Release
|
# Removed individual release step - handled by main release workflow
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
name: ${{ env.releaseVersion }}
|
|
||||||
tag_name: ${{ env.releaseVersion }}
|
|
||||||
repository: ${{ env.githubRepository }}
|
|
||||||
token: ${{ env.REPO_READONLY_PAT }}
|
|
||||||
make_latest: true
|
|
||||||
files: |
|
|
||||||
installer/${{ env.installerName }}
|
|
||||||
36
.github/workflows/cloudflare-pages-blocklists.yml
vendored
Normal file
36
.github/workflows/cloudflare-pages-blocklists.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'Cloudflare/**'
|
||||||
|
- 'blacklist'
|
||||||
|
- 'blacklist_permissive'
|
||||||
|
- 'whitelist'
|
||||||
|
- 'whitelist_with_subtitles'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Copy root static files to Cloudflare static directory
|
||||||
|
run: |
|
||||||
|
cp blacklist Cloudflare/static/
|
||||||
|
cp blacklist_permissive Cloudflare/static/
|
||||||
|
cp whitelist Cloudflare/static/
|
||||||
|
cp whitelist_with_subtitles Cloudflare/static/
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
||||||
|
workingDirectory: "Cloudflare"
|
||||||
|
command: pages deploy . --project-name=cleanuparr
|
||||||
30
.github/workflows/cloudflare-pages-status.yml
vendored
Normal file
30
.github/workflows/cloudflare-pages-status.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Create status files
|
||||||
|
run: |
|
||||||
|
mkdir -p status
|
||||||
|
echo "{ \"version\": \"${GITHUB_REF_NAME}\" }" > status/status.json
|
||||||
|
|
||||||
|
# Cache static files for 10 minutes
|
||||||
|
cat > status/_headers << 'EOF'
|
||||||
|
/*
|
||||||
|
Cache-Control: public, max-age=600, s-maxage=600
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
||||||
|
workingDirectory: "status"
|
||||||
|
command: pages deploy . --project-name=cleanuparr-status
|
||||||
7
.github/workflows/docs.yml
vendored
7
.github/workflows/docs.yml
vendored
@@ -2,9 +2,9 @@ name: Deploy Docusaurus to GitHub Pages
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
tags:
|
||||||
paths:
|
- "v*.*.*"
|
||||||
- 'docs/**'
|
workflow_dispatch: {}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: main
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
|||||||
# Build portable executables
|
# Build portable executables
|
||||||
build-executables:
|
build-executables:
|
||||||
needs: validate
|
needs: validate
|
||||||
uses: ./.github/workflows/build_executable.yml
|
uses: ./.github/workflows/build-executable.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
# Build Windows installer
|
# Build Windows installer
|
||||||
@@ -106,12 +106,12 @@ jobs:
|
|||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: Cleanuparr ${{ needs.validate.outputs.release_version }}
|
name: ${{ needs.validate.outputs.release_version }}
|
||||||
tag_name: ${{ needs.validate.outputs.release_version }}
|
tag_name: ${{ needs.validate.outputs.release_version }}
|
||||||
token: ${{ env.REPO_READONLY_PAT }}
|
token: ${{ env.REPO_READONLY_PAT }}
|
||||||
make_latest: true
|
make_latest: true
|
||||||
|
target_commitish: main
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
prerelease: ${{ contains(needs.validate.outputs.app_version, '-') }}
|
|
||||||
files: |
|
files: |
|
||||||
./artifacts/**/*.zip
|
./artifacts/**/*.zip
|
||||||
./artifacts/**/*.pkg
|
./artifacts/**/*.pkg
|
||||||
|
|||||||
3
Cloudflare/_headers
Normal file
3
Cloudflare/_headers
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Cache static files for 5 minutes
|
||||||
|
/static/*
|
||||||
|
Cache-Control: public, max-age=300, s-maxage=300
|
||||||
2
Cloudflare/static/known_malware_file_name_patterns
Normal file
2
Cloudflare/static/known_malware_file_name_patterns
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
thepirateheaven.org
|
||||||
|
RARBG.work
|
||||||
79
README.md
79
README.md
@@ -12,34 +12,71 @@ Cleanuparr was created primarily to address malicious files, such as `*.lnk` or
|
|||||||
> **Features:**
|
> **Features:**
|
||||||
> - Strike system to mark bad downloads.
|
> - Strike system to mark bad downloads.
|
||||||
> - Remove and block downloads that reached a maximum number of strikes.
|
> - Remove and block downloads that reached a maximum number of strikes.
|
||||||
> - Remove and block downloads that are **failing to be imported** by the arrs. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/import-failed)
|
> - Remove and block downloads that are **failing to be imported** by the arrs.
|
||||||
> - Remove and block downloads that are **stalled** or in **metadata downloading** state. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/stalled)
|
> - Remove and block downloads that are **stalled** or in **metadata downloading** state.
|
||||||
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/slow)
|
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**.
|
||||||
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/content-blocker/general)
|
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Malware Blocker**.
|
||||||
|
> - Remove and block known malware based on patterns found by the community.
|
||||||
> - Automatically trigger a search for downloads removed from the arrs.
|
> - Automatically trigger a search for downloads removed from the arrs.
|
||||||
> - Clean up downloads that have been **seeding** for a certain amount of time. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/seeding)
|
> - Clean up downloads that have been **seeding** for a certain amount of time.
|
||||||
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/hardlinks)
|
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support).
|
||||||
> - Notify on strike or download removal. [configuration](https://cleanuparr.github.io/cleanuparr/docs/category/notifications)
|
> - Notify on strike or download removal.
|
||||||
> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuparr.
|
> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuparr.
|
||||||
|
|
||||||
Cleanuparr 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.
|
## 🎯 Supported Applications
|
||||||
|
|
||||||
## Quick Start
|
### *Arr Applications
|
||||||
|
- **Sonarr**
|
||||||
|
- **Radarr**
|
||||||
|
- **Lidarr**
|
||||||
|
- **Readarr**
|
||||||
|
- **Whisparr**
|
||||||
|
|
||||||
> [!NOTE]
|
### Download Clients
|
||||||
>
|
- **qBittorrent**
|
||||||
> 1. **Docker (Recommended)**
|
- **Transmission**
|
||||||
> Pull the Docker image from `ghcr.io/Cleanuparr/Cleanuparr:latest`.
|
- **Deluge**
|
||||||
>
|
- **µTorrent**
|
||||||
> 2. **Unraid (for Unraid users)**
|
|
||||||
> Use the Unraid Community App.
|
|
||||||
>
|
|
||||||
> 3. **Manual Installation (if you're not using Docker)**
|
|
||||||
> Go to [Windows](#windows), [Linux](#linux) or [MacOS](#macos).
|
|
||||||
|
|
||||||
# Docs
|
### Platforms
|
||||||
|
- **Docker**
|
||||||
|
- **Windows**
|
||||||
|
- **macOS**
|
||||||
|
- **Linux**
|
||||||
|
- **Unraid**
|
||||||
|
|
||||||
Docs can be found [here](https://Cleanuparr.github.io/Cleanuparr/).
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name cleanuparr \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 11011:11011 \
|
||||||
|
-v /path/to/config:/config \
|
||||||
|
-e PORT=11011 \
|
||||||
|
-e PUID=1000 \
|
||||||
|
-e PGID=1000 \
|
||||||
|
-e TZ=Etc/UTC \
|
||||||
|
ghcr.io/cleanuparr/cleanuparr:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
For Docker Compose, health checks, and other installation methods, see the [Complete Installation Guide](https://cleanuparr.github.io/Cleanuparr/docs/installation/detailed).
|
||||||
|
|
||||||
|
### 🌐 Access the Web Interface
|
||||||
|
|
||||||
|
After installation, open your browser and navigate to:
|
||||||
|
```
|
||||||
|
http://localhost:11011
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next Steps:** Check out the [📖 Complete Documentation](https://cleanuparr.github.io/Cleanuparr/) for detailed configuration guides and setup instructions.
|
||||||
|
|
||||||
|
## 📖 Documentation & Support
|
||||||
|
|
||||||
|
- **📚 [Complete Documentation](https://cleanuparr.github.io/Cleanuparr/)** - Installation guides, configuration, and troubleshooting
|
||||||
|
- **⚙️ [Configuration Guide](https://cleanuparr.github.io/Cleanuparr/docs/category/configuration)** - Set up download clients, *arr apps, and features
|
||||||
|
- **🔧 [Setup Scenarios](https://cleanuparr.github.io/Cleanuparr/docs/category/setup-scenarios)** - Common use cases and examples
|
||||||
|
- **💬 [Discord Community](https://discord.gg/SCtMCgtsc4)** - Get help and discuss with other users
|
||||||
|
- **🔗 [GitHub Releases](https://github.com/Cleanuparr/Cleanuparr/releases)** - Download binaries and view changelog
|
||||||
|
|
||||||
# <img style="vertical-align: middle;" width="24px" src="./Logo/256.png" alt="Cleanuparr"> <span style="vertical-align: middle;">Cleanuparr</span> <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/x.svg" height="24px" width="30px" style="vertical-align: middle;"> <span style="vertical-align: middle;">Huntarr</span> <img style="vertical-align: middle;" width="24px" src="https://github.com/plexguide/Huntarr.io/blob/main/frontend/static/logo/512.png?raw=true" alt Huntarr></img>
|
# <img style="vertical-align: middle;" width="24px" src="./Logo/256.png" alt="Cleanuparr"> <span style="vertical-align: middle;">Cleanuparr</span> <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/x.svg" height="24px" width="30px" style="vertical-align: middle;"> <span style="vertical-align: middle;">Huntarr</span> <img style="vertical-align: middle;" width="24px" src="https://github.com/plexguide/Huntarr.io/blob/main/frontend/static/logo/512.png?raw=true" alt Huntarr></img>
|
||||||
|
|
||||||
|
|||||||
346
blacklist
346
blacklist
@@ -1,12 +1,27 @@
|
|||||||
*(sample).*
|
*(sample).*
|
||||||
*.0xe
|
*sample.avchd
|
||||||
|
*sample.avi
|
||||||
|
*sample.mkv
|
||||||
|
*sample.mov
|
||||||
|
*sample.mp4
|
||||||
|
*sample.webm
|
||||||
|
*sample.wmv
|
||||||
|
*.000
|
||||||
*.001
|
*.001
|
||||||
|
*.002
|
||||||
|
*.004
|
||||||
|
*.0xe
|
||||||
*.73k
|
*.73k
|
||||||
*.73p
|
*.73p
|
||||||
*.7z
|
*.7z
|
||||||
|
*.7z.001
|
||||||
|
*.7z.002
|
||||||
*.89k
|
*.89k
|
||||||
*.89z
|
*.89z
|
||||||
*.8ck
|
*.8ck
|
||||||
|
*.a00
|
||||||
|
*.a01
|
||||||
|
*.a02
|
||||||
*.a7r
|
*.a7r
|
||||||
*.ac
|
*.ac
|
||||||
*.acc
|
*.acc
|
||||||
@@ -22,8 +37,11 @@
|
|||||||
*.ahk
|
*.ahk
|
||||||
*.ai
|
*.ai
|
||||||
*.aif
|
*.aif
|
||||||
|
*.ain
|
||||||
*.air
|
*.air
|
||||||
*.alz
|
*.alz
|
||||||
|
*.ana
|
||||||
|
*.apex
|
||||||
*.api
|
*.api
|
||||||
*.apk
|
*.apk
|
||||||
*.app
|
*.app
|
||||||
@@ -31,15 +49,28 @@
|
|||||||
*.applescript
|
*.applescript
|
||||||
*.application
|
*.application
|
||||||
*.appx
|
*.appx
|
||||||
|
*.apz
|
||||||
|
*.ar
|
||||||
*.arc
|
*.arc
|
||||||
|
*.archiver
|
||||||
|
*.arduboy
|
||||||
|
*.arh
|
||||||
|
*.ari
|
||||||
*.arj
|
*.arj
|
||||||
|
*.ark
|
||||||
*.arscript
|
*.arscript
|
||||||
*.asb
|
*.asb
|
||||||
|
*.asice
|
||||||
*.asp
|
*.asp
|
||||||
*.aspx
|
*.aspx
|
||||||
*.aspx-exe
|
*.aspx-exe
|
||||||
*.atmx
|
*.atmx
|
||||||
|
*.ayt
|
||||||
*.azw2
|
*.azw2
|
||||||
|
*.b1
|
||||||
|
*.b6z
|
||||||
|
*.b64
|
||||||
|
*.ba
|
||||||
*.ba_
|
*.ba_
|
||||||
*.bak
|
*.bak
|
||||||
*.bas
|
*.bas
|
||||||
@@ -47,26 +78,48 @@
|
|||||||
*.bat
|
*.bat
|
||||||
*.bdjo
|
*.bdjo
|
||||||
*.bdmv
|
*.bdmv
|
||||||
|
*.bdoc
|
||||||
*.beam
|
*.beam
|
||||||
|
*.bh
|
||||||
*.bin
|
*.bin
|
||||||
*.bmp
|
*.bmp
|
||||||
*.bms
|
*.bms
|
||||||
|
*.bndl
|
||||||
*.bns
|
*.bns
|
||||||
|
*.boo
|
||||||
*.bsa
|
*.bsa
|
||||||
*.btm
|
*.btm
|
||||||
|
*.bundle
|
||||||
|
*.bz
|
||||||
*.bz2
|
*.bz2
|
||||||
|
*.bza
|
||||||
|
*.bzabw
|
||||||
|
*.bzip
|
||||||
|
*.bzip2
|
||||||
*.c
|
*.c
|
||||||
|
*.c00
|
||||||
|
*.c01
|
||||||
|
*.c02
|
||||||
|
*.c10
|
||||||
*.cab
|
*.cab
|
||||||
*.caction
|
*.caction
|
||||||
|
*.car
|
||||||
|
*.cb7
|
||||||
|
*.cba
|
||||||
|
*.cbr
|
||||||
|
*.cbt
|
||||||
|
*.cbz
|
||||||
*.cci
|
*.cci
|
||||||
*.cda
|
*.cda
|
||||||
*.cdb
|
*.cdb
|
||||||
|
*.cdz
|
||||||
*.cel
|
*.cel
|
||||||
*.celx
|
*.celx
|
||||||
*.cfs
|
*.cfs
|
||||||
*.cgi
|
*.cgi
|
||||||
*.cheat
|
*.cheat
|
||||||
*.chm
|
*.chm
|
||||||
|
*.cit
|
||||||
*.ckpt
|
*.ckpt
|
||||||
*.cla
|
*.cla
|
||||||
*.class
|
*.class
|
||||||
@@ -76,9 +129,15 @@
|
|||||||
*.coffee
|
*.coffee
|
||||||
*.com
|
*.com
|
||||||
*.command
|
*.command
|
||||||
|
*.comppkg.hauptwerk.rar
|
||||||
|
*.comppkg_hauptwerk_rar
|
||||||
|
*.conda
|
||||||
*.conf
|
*.conf
|
||||||
*.config
|
*.config
|
||||||
|
*.cp9
|
||||||
|
*.cpgz
|
||||||
*.cpl
|
*.cpl
|
||||||
|
*.cpt
|
||||||
*.crt
|
*.crt
|
||||||
*.cs
|
*.cs
|
||||||
*.csh
|
*.csh
|
||||||
@@ -86,17 +145,27 @@
|
|||||||
*.csproj
|
*.csproj
|
||||||
*.css
|
*.css
|
||||||
*.csv
|
*.csv
|
||||||
|
*.ctx
|
||||||
|
*.ctz
|
||||||
*.cue
|
*.cue
|
||||||
*.cur
|
*.cur
|
||||||
|
*.cxarchive
|
||||||
*.cyw
|
*.cyw
|
||||||
|
*.czip
|
||||||
*.daemon
|
*.daemon
|
||||||
|
*.daf
|
||||||
|
*.dar
|
||||||
*.dat
|
*.dat
|
||||||
*.data-00000-of-00001
|
*.data-00000-of-00001
|
||||||
*.db
|
*.db
|
||||||
|
*.dd
|
||||||
*.deamon
|
*.deamon
|
||||||
*.deb
|
*.deb
|
||||||
*.dek
|
*.dek
|
||||||
|
*.dgc
|
||||||
|
*.dist
|
||||||
*.diz
|
*.diz
|
||||||
|
*.dl_
|
||||||
*.dld
|
*.dld
|
||||||
*.dll
|
*.dll
|
||||||
*.dmc
|
*.dmc
|
||||||
@@ -113,19 +182,27 @@
|
|||||||
*.dw
|
*.dw
|
||||||
*.dword
|
*.dword
|
||||||
*.dxl
|
*.dxl
|
||||||
|
*.dz
|
||||||
*.e_e
|
*.e_e
|
||||||
*.ear
|
*.ear
|
||||||
*.ebacmd
|
*.ebacmd
|
||||||
*.ebm
|
*.ebm
|
||||||
*.ebs
|
*.ebs
|
||||||
*.ebs2
|
*.ebs2
|
||||||
|
*.ecar
|
||||||
*.ecf
|
*.ecf
|
||||||
|
*.ecs
|
||||||
|
*.ecsbx
|
||||||
|
*.edz
|
||||||
|
*.efw
|
||||||
|
*.egg
|
||||||
*.eham
|
*.eham
|
||||||
*.elf
|
*.elf
|
||||||
*.elf-so
|
*.elf-so
|
||||||
*.email
|
*.email
|
||||||
*.emu
|
*.emu
|
||||||
*.epk
|
*.epk
|
||||||
|
*.epi
|
||||||
*.es
|
*.es
|
||||||
*.esh
|
*.esh
|
||||||
*.etc
|
*.etc
|
||||||
@@ -141,36 +218,62 @@
|
|||||||
*.exz
|
*.exz
|
||||||
*.ezs
|
*.ezs
|
||||||
*.ezt
|
*.ezt
|
||||||
|
*.f
|
||||||
|
*.f3z
|
||||||
*.fas
|
*.fas
|
||||||
*.fba
|
*.fba
|
||||||
|
*.fcx
|
||||||
*.fky
|
*.fky
|
||||||
*.flac
|
*.flac
|
||||||
*.flatpak
|
*.flatpak
|
||||||
*.flv
|
*.flv
|
||||||
|
*.fp8
|
||||||
*.fpi
|
*.fpi
|
||||||
*.frs
|
*.frs
|
||||||
*.fxp
|
*.fxp
|
||||||
|
*.fzpz
|
||||||
*.gadget
|
*.gadget
|
||||||
|
*.gar
|
||||||
*.gat
|
*.gat
|
||||||
|
*.gca
|
||||||
*.gif
|
*.gif
|
||||||
*.gifv
|
*.gifv
|
||||||
*.gm9
|
*.gm9
|
||||||
|
*.gmz
|
||||||
*.gpe
|
*.gpe
|
||||||
*.gpu
|
*.gpu
|
||||||
*.gs
|
*.gs
|
||||||
*.gz
|
*.gz
|
||||||
|
*.gz2
|
||||||
|
*.gza
|
||||||
|
*.gzi
|
||||||
|
*.gzip
|
||||||
*.h5
|
*.h5
|
||||||
|
*.ha
|
||||||
*.ham
|
*.ham
|
||||||
|
*.hbc
|
||||||
|
*.hbc2
|
||||||
|
*.hbe
|
||||||
*.hex
|
*.hex
|
||||||
|
*.hki
|
||||||
|
*.hki1
|
||||||
|
*.hki2
|
||||||
|
*.hki3
|
||||||
*.hlp
|
*.hlp
|
||||||
*.hms
|
*.hms
|
||||||
*.hpf
|
*.hpf
|
||||||
|
*.hpk
|
||||||
|
*.hpkg
|
||||||
*.hta
|
*.hta
|
||||||
*.hta-psh
|
*.hta-psh
|
||||||
*.htaccess
|
*.htaccess
|
||||||
*.htm
|
*.htm
|
||||||
*.html
|
*.html
|
||||||
|
*.htmi
|
||||||
|
*.hyp
|
||||||
|
*.iadproj
|
||||||
*.icd
|
*.icd
|
||||||
|
*.ice
|
||||||
*.icns
|
*.icns
|
||||||
*.ico
|
*.ico
|
||||||
*.idx
|
*.idx
|
||||||
@@ -183,17 +286,27 @@
|
|||||||
*.ins
|
*.ins
|
||||||
*.ipa
|
*.ipa
|
||||||
*.ipf
|
*.ipf
|
||||||
|
*.ipg
|
||||||
*.ipk
|
*.ipk
|
||||||
*.ipsw
|
*.ipsw
|
||||||
*.iqylink
|
*.iqylink
|
||||||
|
*.ish
|
||||||
*.iso
|
*.iso
|
||||||
*.isp
|
*.isp
|
||||||
*.isu
|
*.isu
|
||||||
|
*.isx
|
||||||
*.ita
|
*.ita
|
||||||
|
*.ize
|
||||||
*.izh
|
*.izh
|
||||||
*.izma ace
|
*.izma ace
|
||||||
|
*.j
|
||||||
*.jar
|
*.jar
|
||||||
|
*.jar.pack
|
||||||
*.java
|
*.java
|
||||||
|
*.jex
|
||||||
|
*.jgz
|
||||||
|
*.jhh
|
||||||
|
*.jic
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.jpg
|
*.jpg
|
||||||
*.js
|
*.js
|
||||||
@@ -202,27 +315,51 @@
|
|||||||
*.jse
|
*.jse
|
||||||
*.jsf
|
*.jsf
|
||||||
*.json
|
*.json
|
||||||
|
*.jsonlz4
|
||||||
*.jsp
|
*.jsp
|
||||||
*.jsx
|
*.jsx
|
||||||
|
*.kextraction
|
||||||
|
*.kgb
|
||||||
*.kix
|
*.kix
|
||||||
*.ksh
|
*.ksh
|
||||||
|
*.ksp
|
||||||
|
*.kwgt
|
||||||
*.kx
|
*.kx
|
||||||
|
*.kz
|
||||||
|
*.layout
|
||||||
|
*.lbr
|
||||||
*.lck
|
*.lck
|
||||||
*.ldb
|
*.ldb
|
||||||
|
*.lemon
|
||||||
|
*.lha
|
||||||
|
*.lhzd
|
||||||
*.lib
|
*.lib
|
||||||
|
*.libzip
|
||||||
*.link
|
*.link
|
||||||
*.lnk
|
*.lnk
|
||||||
*.lo
|
*.lo
|
||||||
*.lock
|
*.lock
|
||||||
*.log
|
*.log
|
||||||
*.loop-vbs
|
*.loop-vbs
|
||||||
|
*.lpkg
|
||||||
|
*.lqr
|
||||||
*.ls
|
*.ls
|
||||||
|
*.lz
|
||||||
|
*.lz4
|
||||||
|
*.lzh
|
||||||
|
*.lzm
|
||||||
|
*.lzma
|
||||||
|
*.lzo
|
||||||
|
*.lzr
|
||||||
|
*.lzx
|
||||||
*.m3u
|
*.m3u
|
||||||
*.m4a
|
*.m4a
|
||||||
*.mac
|
*.mac
|
||||||
*.macho
|
*.macho
|
||||||
*.mamc
|
*.mamc
|
||||||
*.manifest
|
*.manifest
|
||||||
|
*.mar
|
||||||
|
*.mbz
|
||||||
*.mcr
|
*.mcr
|
||||||
*.md
|
*.md
|
||||||
*.mda
|
*.mda
|
||||||
@@ -233,22 +370,29 @@
|
|||||||
*.mdt
|
*.mdt
|
||||||
*.mel
|
*.mel
|
||||||
*.mem
|
*.mem
|
||||||
|
*.memo
|
||||||
*.meta
|
*.meta
|
||||||
*.mgm
|
*.mgm
|
||||||
*.mhm
|
*.mhm
|
||||||
*.mht
|
*.mht
|
||||||
*.mhtml
|
*.mhtml
|
||||||
*.mid
|
*.mid
|
||||||
|
*.mint
|
||||||
*.mio
|
*.mio
|
||||||
*.mlappinstall
|
*.mlappinstall
|
||||||
|
*.mlproj
|
||||||
*.mlx
|
*.mlx
|
||||||
*.mm
|
*.mm
|
||||||
*.mobileconfig
|
*.mobileconfig
|
||||||
*.model
|
*.model
|
||||||
*.moo
|
*.moo
|
||||||
|
*.mou
|
||||||
|
*.movpkg
|
||||||
|
*.mozlz4
|
||||||
*.mp3
|
*.mp3
|
||||||
*.mpa
|
*.mpa
|
||||||
*.mpk
|
*.mpk
|
||||||
|
*.mpkg
|
||||||
*.mpls
|
*.mpls
|
||||||
*.mrc
|
*.mrc
|
||||||
*.mrp
|
*.mrp
|
||||||
@@ -267,41 +411,79 @@
|
|||||||
*.msp
|
*.msp
|
||||||
*.mst
|
*.mst
|
||||||
*.msu
|
*.msu
|
||||||
|
*.mxc
|
||||||
*.mxe
|
*.mxe
|
||||||
|
*.mzp
|
||||||
*.n
|
*.n
|
||||||
|
*.nar
|
||||||
*.ncl
|
*.ncl
|
||||||
*.net
|
*.net
|
||||||
|
*.nex
|
||||||
*.nexe
|
*.nexe
|
||||||
*.nfo
|
*.nfo
|
||||||
|
*.npk
|
||||||
*.nrg
|
*.nrg
|
||||||
*.num
|
*.num
|
||||||
|
*.nz
|
||||||
*.nzb.bz2
|
*.nzb.bz2
|
||||||
*.nzb.gz
|
*.nzb.gz
|
||||||
*.nzbs
|
*.nzbs
|
||||||
|
*.oar
|
||||||
*.ocx
|
*.ocx
|
||||||
|
*.odlgz
|
||||||
*.odt
|
*.odt
|
||||||
|
*.opk
|
||||||
*.ore
|
*.ore
|
||||||
|
*.osf
|
||||||
*.ost
|
*.ost
|
||||||
*.osx
|
*.osx
|
||||||
*.osx-app
|
*.osx-app
|
||||||
*.otm
|
*.otm
|
||||||
*.out
|
*.out
|
||||||
*.ova
|
*.ova
|
||||||
|
*.oz
|
||||||
*.p
|
*.p
|
||||||
|
*.p01
|
||||||
|
*.p19
|
||||||
|
*.p7z
|
||||||
|
*.pa
|
||||||
|
*.pack.gz
|
||||||
|
*.package
|
||||||
|
*.pae
|
||||||
*.paf
|
*.paf
|
||||||
*.pak
|
*.pak
|
||||||
|
*.paq6
|
||||||
|
*.paq7
|
||||||
|
*.paq8
|
||||||
|
*.paq8f
|
||||||
|
*.paq8l
|
||||||
|
*.paq8p
|
||||||
|
*.par
|
||||||
|
*.par2
|
||||||
|
*.pax
|
||||||
*.pb
|
*.pb
|
||||||
|
*.pbi
|
||||||
*.pcd
|
*.pcd
|
||||||
|
*.pcv
|
||||||
*.pdb
|
*.pdb
|
||||||
*.pdf
|
*.pdf
|
||||||
*.pea
|
*.pea
|
||||||
*.perl
|
*.perl
|
||||||
|
*.pet
|
||||||
*.pex
|
*.pex
|
||||||
|
*.pf
|
||||||
*.phar
|
*.phar
|
||||||
*.php
|
*.php
|
||||||
*.php5
|
*.php5
|
||||||
*.pif
|
*.pif
|
||||||
|
*.pim
|
||||||
|
*.pima
|
||||||
|
*.pit
|
||||||
|
*.piz
|
||||||
*.pkg
|
*.pkg
|
||||||
|
*.pkg.tar.xz
|
||||||
|
*.pkg.tar.zst
|
||||||
|
*.pkz
|
||||||
*.pl
|
*.pl
|
||||||
*.plsc
|
*.plsc
|
||||||
*.plx
|
*.plx
|
||||||
@@ -319,6 +501,7 @@
|
|||||||
*.pptx
|
*.pptx
|
||||||
*.prc
|
*.prc
|
||||||
*.prg
|
*.prg
|
||||||
|
*.prs
|
||||||
*.ps
|
*.ps
|
||||||
*.ps1
|
*.ps1
|
||||||
*.ps1xml
|
*.ps1xml
|
||||||
@@ -334,9 +517,16 @@
|
|||||||
*.psh-reflection
|
*.psh-reflection
|
||||||
*.psm1
|
*.psm1
|
||||||
*.pst
|
*.pst
|
||||||
|
*.psz
|
||||||
*.pt
|
*.pt
|
||||||
|
*.pup
|
||||||
|
*.puz
|
||||||
*.pvd
|
*.pvd
|
||||||
|
*.pvmp
|
||||||
|
*.pvmz
|
||||||
|
*.pwa
|
||||||
*.pwc
|
*.pwc
|
||||||
|
*.pxl
|
||||||
*.pxo
|
*.pxo
|
||||||
*.py
|
*.py
|
||||||
*.pyc
|
*.pyc
|
||||||
@@ -344,8 +534,20 @@
|
|||||||
*.pyo
|
*.pyo
|
||||||
*.python
|
*.python
|
||||||
*.pyz
|
*.pyz
|
||||||
|
*.q
|
||||||
|
*.qda
|
||||||
*.qit
|
*.qit
|
||||||
*.qpx
|
*.qpx
|
||||||
|
*.r0
|
||||||
|
*.r00
|
||||||
|
*.r01
|
||||||
|
*.r02
|
||||||
|
*.r03
|
||||||
|
*.r04
|
||||||
|
*.r1
|
||||||
|
*.r2
|
||||||
|
*.r21
|
||||||
|
*.r30
|
||||||
*.ram
|
*.ram
|
||||||
*.rar
|
*.rar
|
||||||
*.raw
|
*.raw
|
||||||
@@ -356,22 +558,35 @@
|
|||||||
*.reg
|
*.reg
|
||||||
*.resources
|
*.resources
|
||||||
*.resx
|
*.resx
|
||||||
|
*.rev
|
||||||
*.rfs
|
*.rfs
|
||||||
*.rfu
|
*.rfu
|
||||||
*.rgs
|
*.rgs
|
||||||
|
*.rk
|
||||||
*.rm
|
*.rm
|
||||||
|
*.rnc
|
||||||
*.rox
|
*.rox
|
||||||
|
*.rp9
|
||||||
*.rpg
|
*.rpg
|
||||||
*.rpj
|
*.rpj
|
||||||
*.rpm
|
*.rpm
|
||||||
|
*.rss
|
||||||
*.ruby
|
*.ruby
|
||||||
*.run
|
*.run
|
||||||
*.rxe
|
*.rxe
|
||||||
|
*.rz
|
||||||
|
*.s00
|
||||||
|
*.s01
|
||||||
|
*.s02
|
||||||
|
*.s09
|
||||||
*.s2a
|
*.s2a
|
||||||
|
*.s7z
|
||||||
*.sample
|
*.sample
|
||||||
*.sapk
|
*.sapk
|
||||||
|
*.sar
|
||||||
*.savedmodel
|
*.savedmodel
|
||||||
*.sbs
|
*.sbs
|
||||||
|
*.sbx
|
||||||
*.sca
|
*.sca
|
||||||
*.scar
|
*.scar
|
||||||
*.scb
|
*.scb
|
||||||
@@ -381,42 +596,86 @@
|
|||||||
*.scr
|
*.scr
|
||||||
*.script
|
*.script
|
||||||
*.sct
|
*.sct
|
||||||
|
*.sdc
|
||||||
|
*.sdn
|
||||||
|
*.sdoc
|
||||||
|
*.sdocx
|
||||||
|
*.sea
|
||||||
*.seed
|
*.seed
|
||||||
|
*.sen
|
||||||
*.server
|
*.server
|
||||||
*.service
|
*.service
|
||||||
|
*.sfg
|
||||||
|
*.sfm
|
||||||
|
*.sfs
|
||||||
*.sfv
|
*.sfv
|
||||||
|
*.sfx
|
||||||
*.sh
|
*.sh
|
||||||
|
*.shar
|
||||||
*.shb
|
*.shb
|
||||||
*.shell
|
*.shell
|
||||||
|
*.shk
|
||||||
*.shortcut
|
*.shortcut
|
||||||
|
*.shr
|
||||||
*.shs
|
*.shs
|
||||||
*.shtml
|
*.shtml
|
||||||
|
*.sifz
|
||||||
|
*.sipa
|
||||||
*.sit
|
*.sit
|
||||||
*.sitx
|
*.sitx
|
||||||
*.sk
|
*.sk
|
||||||
*.sldm
|
*.sldm
|
||||||
*.sln
|
*.sln
|
||||||
*.smm
|
*.smm
|
||||||
|
*.smpf
|
||||||
*.snap
|
*.snap
|
||||||
|
*.snagitstamps
|
||||||
|
*.snappy
|
||||||
|
*.snb
|
||||||
*.snd
|
*.snd
|
||||||
|
*.snz
|
||||||
|
*.spa
|
||||||
|
*.spd
|
||||||
|
*.spl
|
||||||
|
*.spm
|
||||||
*.spr
|
*.spr
|
||||||
|
*.spt
|
||||||
*.sql
|
*.sql
|
||||||
|
*.sqf
|
||||||
*.sqx
|
*.sqx
|
||||||
|
*.sqz
|
||||||
*.srec
|
*.srec
|
||||||
|
*.srep
|
||||||
*.srt
|
*.srt
|
||||||
*.ssm
|
*.ssm
|
||||||
|
*.stg
|
||||||
|
*.stkdoodlz
|
||||||
|
*.stproj
|
||||||
*.sts
|
*.sts
|
||||||
*.sub
|
*.sub
|
||||||
*.svg
|
*.svg
|
||||||
*.swf
|
*.swf
|
||||||
|
*.sy_
|
||||||
*.sys
|
*.sys
|
||||||
*.tar
|
*.tar
|
||||||
|
*.tar.bz2
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
*.tar.gz2
|
||||||
|
*.tar.lz
|
||||||
|
*.tar.lzma
|
||||||
|
*.tar.xz
|
||||||
|
*.tar.z
|
||||||
|
*.tar.zip
|
||||||
|
*.taz
|
||||||
*.tbl
|
*.tbl
|
||||||
*.tbz
|
*.tbz
|
||||||
|
*.tbz2
|
||||||
*.tcp
|
*.tcp
|
||||||
|
*.tcx
|
||||||
*.text
|
*.text
|
||||||
*.tf
|
*.tf
|
||||||
|
*.tg
|
||||||
|
*.tgs
|
||||||
*.tgz
|
*.tgz
|
||||||
*.thm
|
*.thm
|
||||||
*.thmx
|
*.thmx
|
||||||
@@ -425,19 +684,35 @@
|
|||||||
*.tif
|
*.tif
|
||||||
*.tiff
|
*.tiff
|
||||||
*.tipa
|
*.tipa
|
||||||
|
*.tlz
|
||||||
|
*.tlzma
|
||||||
*.tmp
|
*.tmp
|
||||||
*.tms
|
*.tms
|
||||||
*.toast
|
*.toast
|
||||||
*.torrent
|
*.torrent
|
||||||
*.tpk
|
*.tpk
|
||||||
|
*.tpsr
|
||||||
|
*.trs
|
||||||
*.txt
|
*.txt
|
||||||
|
*.tx_
|
||||||
|
*.txz
|
||||||
|
*.tz
|
||||||
|
*.tzst
|
||||||
*.u3p
|
*.u3p
|
||||||
|
*.ubz
|
||||||
|
*.uc2
|
||||||
*.udf
|
*.udf
|
||||||
|
*.ufdr
|
||||||
|
*.ufs.uzip
|
||||||
|
*.uha
|
||||||
*.upk
|
*.upk
|
||||||
*.upx
|
*.upx
|
||||||
*.url
|
*.url
|
||||||
|
*.uue
|
||||||
*.uvm
|
*.uvm
|
||||||
*.uw8
|
*.uw8
|
||||||
|
*.uzed
|
||||||
|
*.uzip
|
||||||
*.vb
|
*.vb
|
||||||
*.vba
|
*.vba
|
||||||
*.vba-exe
|
*.vba-exe
|
||||||
@@ -449,26 +724,46 @@
|
|||||||
*.vbscript
|
*.vbscript
|
||||||
*.vcd
|
*.vcd
|
||||||
*.vdo
|
*.vdo
|
||||||
|
*.vem
|
||||||
*.vexe
|
*.vexe
|
||||||
|
*.vfs
|
||||||
*.vhd
|
*.vhd
|
||||||
*.vhdx
|
*.vhdx
|
||||||
|
*.vib
|
||||||
|
*.vip
|
||||||
*.vlx
|
*.vlx
|
||||||
*.vm
|
*.vm
|
||||||
|
*.vmcz
|
||||||
*.vmdk
|
*.vmdk
|
||||||
|
*.vms
|
||||||
*.vob
|
*.vob
|
||||||
*.vocab
|
*.vocab
|
||||||
|
*.voca
|
||||||
|
*.vpk
|
||||||
*.vpm
|
*.vpm
|
||||||
|
*.vrpackage
|
||||||
|
*.vsi
|
||||||
|
*.vwi
|
||||||
*.vxp
|
*.vxp
|
||||||
|
*.wa
|
||||||
|
*.wacz
|
||||||
|
*.waff
|
||||||
*.war
|
*.war
|
||||||
|
*.wastickers
|
||||||
*.wav
|
*.wav
|
||||||
*.wbk
|
*.wbk
|
||||||
*.wcm
|
*.wcm
|
||||||
|
*.wdz
|
||||||
*.webm
|
*.webm
|
||||||
|
*.whl
|
||||||
|
*.wick
|
||||||
*.widget
|
*.widget
|
||||||
*.wim
|
*.wim
|
||||||
*.wiz
|
*.wiz
|
||||||
|
*.wlb
|
||||||
*.wma
|
*.wma
|
||||||
*.workflow
|
*.workflow
|
||||||
|
*.wot
|
||||||
*.wpk
|
*.wpk
|
||||||
*.wpl
|
*.wpl
|
||||||
*.wpm
|
*.wpm
|
||||||
@@ -477,14 +772,26 @@
|
|||||||
*.wsc
|
*.wsc
|
||||||
*.wsf
|
*.wsf
|
||||||
*.wsh
|
*.wsh
|
||||||
|
*.wux
|
||||||
*.x86
|
*.x86
|
||||||
*.x86_64
|
*.x86_64
|
||||||
*.xaml
|
*.xaml
|
||||||
*.xap
|
*.xap
|
||||||
|
*.xapk
|
||||||
|
*.xar
|
||||||
*.xbap
|
*.xbap
|
||||||
*.xbe
|
*.xbe
|
||||||
|
*.xcf.bz2
|
||||||
|
*.xcf.gz
|
||||||
|
*.xcf.xz
|
||||||
|
*.xcfbz2
|
||||||
|
*.xcfgz
|
||||||
|
*.xcfxz
|
||||||
*.xex
|
*.xex
|
||||||
|
*.xez
|
||||||
|
*.xfp
|
||||||
*.xig
|
*.xig
|
||||||
|
*.xip
|
||||||
*.xla
|
*.xla
|
||||||
*.xlam
|
*.xlam
|
||||||
*.xll
|
*.xll
|
||||||
@@ -497,24 +804,47 @@
|
|||||||
*.xltb
|
*.xltb
|
||||||
*.xltm
|
*.xltm
|
||||||
*.xlw
|
*.xlw
|
||||||
|
*.xmcdz
|
||||||
*.xml
|
*.xml
|
||||||
|
*.xoj
|
||||||
|
*.xopp
|
||||||
*.xqt
|
*.xqt
|
||||||
*.xrt
|
*.xrt
|
||||||
|
*.xx
|
||||||
*.xys
|
*.xys
|
||||||
*.xz
|
*.xz
|
||||||
|
*.xzm
|
||||||
|
*.y
|
||||||
|
*.yc
|
||||||
*.ygh
|
*.ygh
|
||||||
|
*.yz1
|
||||||
*.z
|
*.z
|
||||||
|
*.z00
|
||||||
|
*.z01
|
||||||
|
*.z02
|
||||||
|
*.z03
|
||||||
|
*.z04
|
||||||
|
*.zabw
|
||||||
|
*.zap
|
||||||
|
*.zed
|
||||||
|
*.zfsendtotarget
|
||||||
|
*.zhelp
|
||||||
|
*.zi
|
||||||
|
*.zi_
|
||||||
|
*.zim
|
||||||
*.zip
|
*.zip
|
||||||
*.zipx
|
*.zipx
|
||||||
|
*.zix
|
||||||
|
*.zl
|
||||||
*.zl9
|
*.zl9
|
||||||
*.zoo
|
*.zoo
|
||||||
*sample.avchd
|
*.zpaq
|
||||||
*sample.avi
|
*.zpi
|
||||||
*sample.mkv
|
*.zsplit
|
||||||
*sample.mov
|
*.zst
|
||||||
*sample.mp4
|
*.zw
|
||||||
*sample.webm
|
*.zwi
|
||||||
*sample.wmv
|
*.zz
|
||||||
Trailer.*
|
Trailer.*
|
||||||
VOSTFR
|
VOSTFR
|
||||||
api
|
api
|
||||||
@@ -1,53 +1,410 @@
|
|||||||
|
*.000
|
||||||
*.001
|
*.001
|
||||||
|
*.002
|
||||||
|
*.004
|
||||||
|
*.7z
|
||||||
|
*.7z.001
|
||||||
|
*.7z.002
|
||||||
|
*.a00
|
||||||
|
*.a01
|
||||||
|
*.a02
|
||||||
|
*.ace
|
||||||
|
*.ain
|
||||||
|
*.alz
|
||||||
|
*.ana
|
||||||
|
*.apex
|
||||||
*.apk
|
*.apk
|
||||||
|
*.apz
|
||||||
|
*.ar
|
||||||
|
*.arc
|
||||||
|
*.archiver
|
||||||
|
*.arduboy
|
||||||
|
*.arh
|
||||||
|
*.ari
|
||||||
*.arj
|
*.arj
|
||||||
|
*.ark
|
||||||
|
*.asice
|
||||||
|
*.ayt
|
||||||
|
*.b1
|
||||||
|
*.b6z
|
||||||
|
*.b64
|
||||||
|
*.ba
|
||||||
*.bat
|
*.bat
|
||||||
|
*.bdoc
|
||||||
|
*.bh
|
||||||
*.bin
|
*.bin
|
||||||
*.bmp
|
*.bmp
|
||||||
|
*.bndl
|
||||||
|
*.boo
|
||||||
|
*.bundle
|
||||||
|
*.bz
|
||||||
|
*.bz2
|
||||||
|
*.bza
|
||||||
|
*.bzabw
|
||||||
|
*.bzip
|
||||||
|
*.bzip2
|
||||||
|
*.c00
|
||||||
|
*.c01
|
||||||
|
*.c02
|
||||||
|
*.c10
|
||||||
|
*.car
|
||||||
|
*.cb7
|
||||||
|
*.cba
|
||||||
|
*.cbr
|
||||||
|
*.cbt
|
||||||
|
*.cbz
|
||||||
|
*.cdz
|
||||||
|
*.cit
|
||||||
*.cmd
|
*.cmd
|
||||||
*.com
|
*.com
|
||||||
|
*.comppkg.hauptwerk.rar
|
||||||
|
*.comppkg_hauptwerk_rar
|
||||||
|
*.conda
|
||||||
|
*.cp9
|
||||||
|
*.cpgz
|
||||||
|
*.cpt
|
||||||
|
*.ctx
|
||||||
|
*.ctz
|
||||||
|
*.cxarchive
|
||||||
|
*.czip
|
||||||
|
*.daf
|
||||||
|
*.dar
|
||||||
*.db
|
*.db
|
||||||
|
*.dd
|
||||||
|
*.deb
|
||||||
|
*.dgc
|
||||||
|
*.dist
|
||||||
*.diz
|
*.diz
|
||||||
|
*.dl_
|
||||||
*.dll
|
*.dll
|
||||||
*.dmg
|
*.dmg
|
||||||
|
*.dz
|
||||||
|
*.ecar
|
||||||
|
*.ecs
|
||||||
|
*.ecsbx
|
||||||
|
*.edz
|
||||||
|
*.efw
|
||||||
|
*.egg
|
||||||
|
*.epi
|
||||||
*.etc
|
*.etc
|
||||||
*.exe
|
*.exe
|
||||||
|
*.f
|
||||||
|
*.f3z
|
||||||
|
*.fcx
|
||||||
|
*.fp8
|
||||||
|
*.fzpz
|
||||||
|
*.gar
|
||||||
|
*.gca
|
||||||
*.gif
|
*.gif
|
||||||
|
*.gmz
|
||||||
|
*.gz
|
||||||
|
*.gz2
|
||||||
|
*.gza
|
||||||
|
*.gzi
|
||||||
|
*.gzip
|
||||||
|
*.ha
|
||||||
|
*.hbc
|
||||||
|
*.hbc2
|
||||||
|
*.hbe
|
||||||
|
*.hki
|
||||||
|
*.hki1
|
||||||
|
*.hki2
|
||||||
|
*.hki3
|
||||||
|
*.hpk
|
||||||
|
*.hpkg
|
||||||
*.htm
|
*.htm
|
||||||
*.html
|
*.html
|
||||||
|
*.htmi
|
||||||
|
*.hyp
|
||||||
|
*.iadproj
|
||||||
|
*.ice
|
||||||
*.ico
|
*.ico
|
||||||
*.ini
|
*.ini
|
||||||
|
*.ipg
|
||||||
|
*.ipk
|
||||||
|
*.ish
|
||||||
*.iso
|
*.iso
|
||||||
|
*.isx
|
||||||
|
*.ita
|
||||||
|
*.ize
|
||||||
|
*.j
|
||||||
*.jar
|
*.jar
|
||||||
|
*.jar.pack
|
||||||
|
*.jex
|
||||||
|
*.jgz
|
||||||
|
*.jhh
|
||||||
|
*.jic
|
||||||
*.jpg
|
*.jpg
|
||||||
*.js
|
*.js
|
||||||
|
*.jsonlz4
|
||||||
|
*.kextraction
|
||||||
|
*.kgb
|
||||||
|
*.ksp
|
||||||
|
*.kwgt
|
||||||
|
*.kz
|
||||||
|
*.layout
|
||||||
|
*.lbr
|
||||||
|
*.lemon
|
||||||
|
*.lha
|
||||||
|
*.lhzd
|
||||||
|
*.libzip
|
||||||
*.link
|
*.link
|
||||||
*.lnk
|
*.lnk
|
||||||
|
*.lpkg
|
||||||
|
*.lqr
|
||||||
|
*.lz
|
||||||
|
*.lz4
|
||||||
|
*.lzh
|
||||||
|
*.lzm
|
||||||
|
*.lzma
|
||||||
|
*.lzo
|
||||||
|
*.lzr
|
||||||
|
*.lzx
|
||||||
|
*.mar
|
||||||
|
*.mbz
|
||||||
|
*.md
|
||||||
|
*.memo
|
||||||
|
*.mint
|
||||||
|
*.mlproj
|
||||||
|
*.mou
|
||||||
|
*.movpkg
|
||||||
|
*.mozlz4
|
||||||
|
*.mpkg
|
||||||
*.msi
|
*.msi
|
||||||
|
*.mxc
|
||||||
|
*.mzp
|
||||||
|
*.nar
|
||||||
|
*.nex
|
||||||
*.nfo
|
*.nfo
|
||||||
|
*.npk
|
||||||
|
*.nz
|
||||||
|
*.oar
|
||||||
|
*.odlgz
|
||||||
|
*.opk
|
||||||
|
*.osf
|
||||||
|
*.oz
|
||||||
|
*.p01
|
||||||
|
*.p19
|
||||||
|
*.p7z
|
||||||
|
*.pa
|
||||||
|
*.pack.gz
|
||||||
|
*.package
|
||||||
|
*.pae
|
||||||
|
*.pak
|
||||||
|
*.paq6
|
||||||
|
*.paq7
|
||||||
|
*.paq8
|
||||||
|
*.paq8f
|
||||||
|
*.paq8l
|
||||||
|
*.paq8p
|
||||||
|
*.par
|
||||||
|
*.par2
|
||||||
|
*.pax
|
||||||
|
*.pbi
|
||||||
|
*.pcv
|
||||||
|
*.pea
|
||||||
*.perl
|
*.perl
|
||||||
|
*.pet
|
||||||
|
*.pf
|
||||||
*.php
|
*.php
|
||||||
|
*.pim
|
||||||
|
*.pima
|
||||||
|
*.pit
|
||||||
|
*.piz
|
||||||
|
*.pkg
|
||||||
|
*.pkg.tar.xz
|
||||||
|
*.pkg.tar.zst
|
||||||
|
*.pkz
|
||||||
*.pl
|
*.pl
|
||||||
*.png
|
*.png
|
||||||
|
*.prs
|
||||||
*.ps1
|
*.ps1
|
||||||
*.psc1
|
*.psc1
|
||||||
*.psd1
|
*.psd1
|
||||||
*.psm1
|
*.psm1
|
||||||
|
*.psz
|
||||||
|
*.pup
|
||||||
|
*.puz
|
||||||
|
*.pvmp
|
||||||
|
*.pvmz
|
||||||
|
*.pwa
|
||||||
|
*.pxl
|
||||||
*.py
|
*.py
|
||||||
*.pyd
|
*.pyd
|
||||||
|
*.q
|
||||||
|
*.qda
|
||||||
|
*.r0
|
||||||
|
*.r00
|
||||||
|
*.r01
|
||||||
|
*.r02
|
||||||
|
*.r03
|
||||||
|
*.r04
|
||||||
|
*.r1
|
||||||
|
*.r2
|
||||||
|
*.r21
|
||||||
|
*.r30
|
||||||
|
*.rar
|
||||||
*.rb
|
*.rb
|
||||||
*.readme
|
*.readme
|
||||||
*.reg
|
*.reg
|
||||||
|
*.rev
|
||||||
|
*.rk
|
||||||
|
*.rnc
|
||||||
|
*.rp9
|
||||||
|
*.rpm
|
||||||
|
*.rss
|
||||||
*.run
|
*.run
|
||||||
|
*.rz
|
||||||
|
*.s00
|
||||||
|
*.s01
|
||||||
|
*.s02
|
||||||
|
*.s09
|
||||||
|
*.s7z
|
||||||
|
*.sar
|
||||||
|
*.sbx
|
||||||
*.scr
|
*.scr
|
||||||
|
*.sdc
|
||||||
|
*.sdn
|
||||||
|
*.sdoc
|
||||||
|
*.sdocx
|
||||||
|
*.sea
|
||||||
|
*.sen
|
||||||
|
*.sfg
|
||||||
|
*.sfm
|
||||||
|
*.sfs
|
||||||
|
*.sfx
|
||||||
*.sh
|
*.sh
|
||||||
|
*.shar
|
||||||
|
*.shk
|
||||||
|
*.shr
|
||||||
|
*.sifz
|
||||||
|
*.sipa
|
||||||
|
*.sit
|
||||||
|
*.sitx
|
||||||
|
*.smpf
|
||||||
|
*.snagitstamps
|
||||||
|
*.snappy
|
||||||
|
*.snb
|
||||||
|
*.snz
|
||||||
|
*.spa
|
||||||
|
*.spd
|
||||||
|
*.spl
|
||||||
|
*.spm
|
||||||
|
*.spt
|
||||||
*.sql
|
*.sql
|
||||||
|
*.sqf
|
||||||
|
*.sqx
|
||||||
|
*.sqz
|
||||||
|
*.srep
|
||||||
|
*.stg
|
||||||
|
*.stkdoodlz
|
||||||
|
*.stproj
|
||||||
|
*.sy_
|
||||||
|
*.tar.bz2
|
||||||
|
*.tar.gz
|
||||||
|
*.tar.gz2
|
||||||
|
*.tar.lz
|
||||||
|
*.tar.lzma
|
||||||
|
*.tar.xz
|
||||||
|
*.tar.z
|
||||||
|
*.tar.zip
|
||||||
|
*.taz
|
||||||
|
*.tbz
|
||||||
|
*.tbz2
|
||||||
|
*.tcx
|
||||||
*.text
|
*.text
|
||||||
|
*.tg
|
||||||
|
*.tgs
|
||||||
|
*.tgz
|
||||||
*.thumb
|
*.thumb
|
||||||
|
*.tlz
|
||||||
|
*.tlzma
|
||||||
*.torrent
|
*.torrent
|
||||||
|
*.tpsr
|
||||||
|
*.trs
|
||||||
*.txt
|
*.txt
|
||||||
|
*.tx_
|
||||||
|
*.txz
|
||||||
|
*.tz
|
||||||
|
*.tzst
|
||||||
|
*.ubz
|
||||||
|
*.uc2
|
||||||
|
*.ufdr
|
||||||
|
*.ufs.uzip
|
||||||
|
*.uha
|
||||||
*.url
|
*.url
|
||||||
|
*.uue
|
||||||
|
*.uvm
|
||||||
|
*.uzed
|
||||||
|
*.uzip
|
||||||
*.vbs
|
*.vbs
|
||||||
|
*.vem
|
||||||
|
*.vfs
|
||||||
|
*.vib
|
||||||
|
*.vip
|
||||||
|
*.vmcz
|
||||||
|
*.vms
|
||||||
|
*.voca
|
||||||
|
*.vpk
|
||||||
|
*.vrpackage
|
||||||
|
*.vsi
|
||||||
|
*.vwi
|
||||||
|
*.wa
|
||||||
|
*.wacz
|
||||||
|
*.waff
|
||||||
|
*.war
|
||||||
|
*.wastickers
|
||||||
|
*.wdz
|
||||||
|
*.whl
|
||||||
|
*.wick
|
||||||
|
*.wlb
|
||||||
|
*.wot
|
||||||
*.wsf
|
*.wsf
|
||||||
|
*.wux
|
||||||
|
*.xapk
|
||||||
|
*.xar
|
||||||
|
*.xcf.bz2
|
||||||
|
*.xcf.gz
|
||||||
|
*.xcf.xz
|
||||||
|
*.xcfbz2
|
||||||
|
*.xcfgz
|
||||||
|
*.xcfxz
|
||||||
|
*.xez
|
||||||
|
*.xfp
|
||||||
|
*.xip
|
||||||
*.xml
|
*.xml
|
||||||
*.zipx
|
*.xmcdz
|
||||||
|
*.xoj
|
||||||
|
*.xopp
|
||||||
|
*.xx
|
||||||
|
*.xz
|
||||||
|
*.xzm
|
||||||
|
*.y
|
||||||
|
*.yc
|
||||||
|
*.yz1
|
||||||
|
*.z
|
||||||
|
*.z00
|
||||||
|
*.z01
|
||||||
|
*.z02
|
||||||
|
*.z03
|
||||||
|
*.z04
|
||||||
|
*.zabw
|
||||||
|
*.zap
|
||||||
|
*.zed
|
||||||
|
*.zfsendtotarget
|
||||||
|
*.zhelp
|
||||||
|
*.zi
|
||||||
|
*.zi_
|
||||||
|
*.zim
|
||||||
|
*.zip
|
||||||
|
*.zipx
|
||||||
|
*.zix
|
||||||
|
*.zl
|
||||||
|
*.zoo
|
||||||
|
*.zpaq
|
||||||
|
*.zpi
|
||||||
|
*.zsplit
|
||||||
|
*.zst
|
||||||
|
*.zw
|
||||||
|
*.zwi
|
||||||
|
*.zz
|
||||||
@@ -45,6 +45,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
|||||||
|
|
||||||
# Install required packages for user management and timezone support
|
# Install required packages for user management and timezone support
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
gosu \
|
gosu \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -87,10 +87,6 @@ public class EventsController : ControllerBase
|
|||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
events = events
|
|
||||||
.OrderBy(e => e.Timestamp)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Return paginated result
|
// Return paginated result
|
||||||
var result = new PaginatedResult<AppEvent>
|
var result = new PaginatedResult<AppEvent>
|
||||||
{
|
{
|
||||||
|
|||||||
125
code/backend/Cleanuparr.Api/Controllers/HealthController.cs
Normal file
125
code/backend/Cleanuparr.Api/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health check endpoints for Docker and Kubernetes
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly HealthCheckService _healthCheckService;
|
||||||
|
private readonly ILogger<HealthController> _logger;
|
||||||
|
|
||||||
|
public HealthController(HealthCheckService healthCheckService, ILogger<HealthController> logger)
|
||||||
|
{
|
||||||
|
_healthCheckService = healthCheckService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Basic liveness probe - checks if the application is running
|
||||||
|
/// Used by Docker HEALTHCHECK and Kubernetes liveness probes
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("/health")]
|
||||||
|
public async Task<IActionResult> GetHealth()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _healthCheckService.CheckHealthAsync(
|
||||||
|
registration => registration.Tags.Contains("liveness"));
|
||||||
|
|
||||||
|
return result.Status == HealthStatus.Healthy
|
||||||
|
? Ok(new { status = "healthy", timestamp = DateTime.UtcNow })
|
||||||
|
: StatusCode(503, new { status = "unhealthy", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Health check failed");
|
||||||
|
return StatusCode(503, new { status = "unhealthy", error = "Health check failed", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Readiness probe - checks if the application is ready to serve traffic
|
||||||
|
/// Used by Kubernetes readiness probes
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("/health/ready")]
|
||||||
|
public async Task<IActionResult> GetReadiness()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _healthCheckService.CheckHealthAsync(
|
||||||
|
registration => registration.Tags.Contains("readiness"));
|
||||||
|
|
||||||
|
if (result.Status == HealthStatus.Healthy)
|
||||||
|
{
|
||||||
|
return Ok(new { status = "ready", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For readiness, we consider degraded as not ready
|
||||||
|
return StatusCode(503, new {
|
||||||
|
status = "not_ready",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
details = result.Entries.Where(e => e.Value.Status != HealthStatus.Healthy)
|
||||||
|
.ToDictionary(e => e.Key, e => new {
|
||||||
|
status = e.Value.Status.ToString().ToLowerInvariant(),
|
||||||
|
description = e.Value.Description
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Readiness check failed");
|
||||||
|
return StatusCode(503, new { status = "not_ready", error = "Readiness check failed", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detailed health status - for monitoring and debugging
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("/health/detailed")]
|
||||||
|
public async Task<IActionResult> GetDetailedHealth()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _healthCheckService.CheckHealthAsync();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = result.Status.ToString().ToLowerInvariant(),
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
totalDuration = result.TotalDuration.TotalMilliseconds,
|
||||||
|
entries = result.Entries.ToDictionary(
|
||||||
|
e => e.Key,
|
||||||
|
e => new
|
||||||
|
{
|
||||||
|
status = e.Value.Status.ToString().ToLowerInvariant(),
|
||||||
|
description = e.Value.Description,
|
||||||
|
duration = e.Value.Duration.TotalMilliseconds,
|
||||||
|
tags = e.Value.Tags,
|
||||||
|
data = e.Value.Data,
|
||||||
|
exception = e.Value.Exception?.Message
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return result.Status == HealthStatus.Healthy
|
||||||
|
? Ok(response)
|
||||||
|
: StatusCode(503, response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Detailed health check failed");
|
||||||
|
return StatusCode(503, new {
|
||||||
|
status = "unhealthy",
|
||||||
|
error = "Detailed health check failed",
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Cleanuparr.Api.Models;
|
using Cleanuparr.Api.Models;
|
||||||
using Cleanuparr.Infrastructure.Models;
|
using Cleanuparr.Infrastructure.Models;
|
||||||
using Infrastructure.Services.Interfaces;
|
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Cleanuparr.Api.Controllers;
|
namespace Cleanuparr.Api.Controllers;
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public class StatusController : ControllerBase
|
|||||||
.Include(x => x.Instances)
|
.Include(x => x.Instances)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstAsync(x => x.Type == InstanceType.Lidarr);
|
.FirstAsync(x => x.Type == InstanceType.Lidarr);
|
||||||
|
var readarrConfig = await _dataContext.ArrConfigs
|
||||||
|
.Include(x => x.Instances)
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstAsync(x => x.Type == InstanceType.Readarr);
|
||||||
|
|
||||||
var status = new
|
var status = new
|
||||||
{
|
{
|
||||||
@@ -80,6 +84,10 @@ public class StatusController : ControllerBase
|
|||||||
Lidarr = new
|
Lidarr = new
|
||||||
{
|
{
|
||||||
InstanceCount = lidarrConfig.Instances.Count
|
InstanceCount = lidarrConfig.Instances.Count
|
||||||
|
},
|
||||||
|
Readarr = new
|
||||||
|
{
|
||||||
|
InstanceCount = readarrConfig.Instances.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ public static class ApiDI
|
|||||||
// Add health status broadcaster
|
// Add health status broadcaster
|
||||||
services.AddHostedService<HealthStatusBroadcaster>();
|
services.AddHostedService<HealthStatusBroadcaster>();
|
||||||
|
|
||||||
// Add logging initializer service
|
|
||||||
services.AddHostedService<LoggingInitializer>();
|
|
||||||
|
|
||||||
services.AddSwaggerGen(options =>
|
services.AddSwaggerGen(options =>
|
||||||
{
|
{
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
@@ -142,6 +139,38 @@ public static class ApiDI
|
|||||||
// Map SignalR hubs
|
// Map SignalR hubs
|
||||||
app.MapHub<HealthStatusHub>("/api/hubs/health");
|
app.MapHub<HealthStatusHub>("/api/hubs/health");
|
||||||
app.MapHub<AppHub>("/api/hubs/app");
|
app.MapHub<AppHub>("/api/hubs/app");
|
||||||
|
|
||||||
|
app.MapGet("/manifest.webmanifest", (HttpContext context) =>
|
||||||
|
{
|
||||||
|
var basePath = context.Request.PathBase.HasValue
|
||||||
|
? context.Request.PathBase.Value
|
||||||
|
: "/";
|
||||||
|
|
||||||
|
var manifest = new
|
||||||
|
{
|
||||||
|
name = "Cleanuparr",
|
||||||
|
short_name = "Cleanuparr",
|
||||||
|
start_url = basePath,
|
||||||
|
display = "standalone",
|
||||||
|
background_color = "#ffffff",
|
||||||
|
theme_color = "#ffffff",
|
||||||
|
icons = new[]
|
||||||
|
{
|
||||||
|
new {
|
||||||
|
src = "icons/icon-192x192.png",
|
||||||
|
sizes = "192x192",
|
||||||
|
type = "image/png"
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
src = "icons/icon-512x512.png",
|
||||||
|
sizes = "512x512",
|
||||||
|
type = "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Results.Json(manifest, contentType: "application/manifest+json");
|
||||||
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Infrastructure.Logging;
|
||||||
using Cleanuparr.Infrastructure.Models;
|
|
||||||
using Cleanuparr.Shared.Helpers;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
|
||||||
using Serilog.Templates;
|
|
||||||
using Serilog.Templates.Themes;
|
|
||||||
|
|
||||||
namespace Cleanuparr.Api.DependencyInjection;
|
namespace Cleanuparr.Api.DependencyInjection;
|
||||||
|
|
||||||
@@ -12,82 +7,10 @@ public static class LoggingDI
|
|||||||
{
|
{
|
||||||
public static ILoggingBuilder AddLogging(this ILoggingBuilder builder)
|
public static ILoggingBuilder AddLogging(this ILoggingBuilder builder)
|
||||||
{
|
{
|
||||||
Log.Logger = GetDefaultLoggerConfiguration().CreateLogger();
|
Log.Logger = LoggingConfigManager
|
||||||
|
.CreateLoggerConfiguration()
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
return builder.ClearProviders().AddSerilog();
|
return builder.ClearProviders().AddSerilog();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LoggerConfiguration GetDefaultLoggerConfiguration()
|
|
||||||
{
|
|
||||||
LoggerConfiguration logConfig = new();
|
|
||||||
const string categoryTemplate = "{#if Category is not null} {Concat('[',Category,']'),CAT_PAD}{#end}";
|
|
||||||
const string jobNameTemplate = "{#if JobName is not null} {Concat('[',JobName,']'),JOB_PAD}{#end}";
|
|
||||||
|
|
||||||
const string consoleOutputTemplate = $"[{{@t:yyyy-MM-dd HH:mm:ss.fff}} {{@l:u3}}]{jobNameTemplate}{categoryTemplate} {{@m}}\n{{@x}}";
|
|
||||||
const string fileOutputTemplate = $"{{@t:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{@l:u3}}]{jobNameTemplate}{categoryTemplate} {{@m:lj}}\n{{@x}}";
|
|
||||||
|
|
||||||
// Determine job name padding
|
|
||||||
List<string> jobNames = [nameof(JobType.QueueCleaner), nameof(JobType.ContentBlocker), nameof(JobType.DownloadCleaner)];
|
|
||||||
int jobPadding = jobNames.Max(x => x.Length) + 2;
|
|
||||||
|
|
||||||
// Determine instance name padding
|
|
||||||
List<string> categoryNames = [
|
|
||||||
InstanceType.Sonarr.ToString(),
|
|
||||||
InstanceType.Radarr.ToString(),
|
|
||||||
InstanceType.Lidarr.ToString(),
|
|
||||||
InstanceType.Readarr.ToString(),
|
|
||||||
InstanceType.Whisparr.ToString(),
|
|
||||||
"SYSTEM"
|
|
||||||
];
|
|
||||||
int catPadding = categoryNames.Max(x => x.Length) + 2;
|
|
||||||
|
|
||||||
// Apply padding values to templates
|
|
||||||
string consoleTemplate = consoleOutputTemplate
|
|
||||||
.Replace("JOB_PAD", jobPadding.ToString())
|
|
||||||
.Replace("CAT_PAD", catPadding.ToString());
|
|
||||||
|
|
||||||
string fileTemplate = fileOutputTemplate
|
|
||||||
.Replace("JOB_PAD", jobPadding.ToString())
|
|
||||||
.Replace("CAT_PAD", catPadding.ToString());
|
|
||||||
|
|
||||||
// Configure base logger with dynamic level control
|
|
||||||
logConfig
|
|
||||||
.MinimumLevel.Is(LogEventLevel.Information)
|
|
||||||
.Enrich.FromLogContext()
|
|
||||||
.WriteTo.Console(new ExpressionTemplate(consoleTemplate, theme: TemplateTheme.Literate));
|
|
||||||
|
|
||||||
// Create the logs directory
|
|
||||||
string logsPath = Path.Combine(ConfigurationPathProvider.GetConfigPath(), "logs");
|
|
||||||
if (!Directory.Exists(logsPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(logsPath);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw new Exception($"Failed to create log directory | {logsPath}", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add main log file
|
|
||||||
logConfig.WriteTo.File(
|
|
||||||
path: Path.Combine(logsPath, "cleanuparr-.txt"),
|
|
||||||
formatter: new ExpressionTemplate(fileTemplate),
|
|
||||||
fileSizeLimitBytes: 10L * 1024 * 1024,
|
|
||||||
rollingInterval: RollingInterval.Day,
|
|
||||||
rollOnFileSizeLimit: true,
|
|
||||||
shared: true
|
|
||||||
);
|
|
||||||
|
|
||||||
logConfig
|
|
||||||
.MinimumLevel.Override("MassTransit", LogEventLevel.Warning)
|
|
||||||
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
|
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
|
||||||
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
|
|
||||||
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
|
|
||||||
.Enrich.WithProperty("ApplicationName", "Cleanuparr");
|
|
||||||
|
|
||||||
return logConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Cleanuparr.Domain.Entities.Arr;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadHunter.Consumers;
|
||||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Consumers;
|
using Cleanuparr.Infrastructure.Features.DownloadRemover.Consumers;
|
||||||
using Cleanuparr.Infrastructure.Features.Notifications.Consumers;
|
using Cleanuparr.Infrastructure.Features.Notifications.Consumers;
|
||||||
|
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||||
using Cleanuparr.Infrastructure.Health;
|
using Cleanuparr.Infrastructure.Health;
|
||||||
using Cleanuparr.Infrastructure.Http;
|
using Cleanuparr.Infrastructure.Http;
|
||||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||||
using Data.Models.Arr;
|
using Data.Models.Arr;
|
||||||
using Infrastructure.Verticals.Notifications.Models;
|
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
@@ -15,22 +17,26 @@ public static class MainDI
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||||
services
|
services
|
||||||
.AddLogging(builder => builder.ClearProviders().AddConsole())
|
|
||||||
.AddHttpClients(configuration)
|
.AddHttpClients(configuration)
|
||||||
.AddSingleton<MemoryCache>()
|
.AddSingleton<MemoryCache>()
|
||||||
.AddSingleton<IMemoryCache>(serviceProvider => serviceProvider.GetRequiredService<MemoryCache>())
|
.AddSingleton<IMemoryCache>(serviceProvider => serviceProvider.GetRequiredService<MemoryCache>())
|
||||||
.AddServices()
|
.AddServices()
|
||||||
.AddHealthServices()
|
.AddHealthServices()
|
||||||
.AddQuartzServices(configuration)
|
.AddQuartzServices(configuration)
|
||||||
.AddNotifications(configuration)
|
.AddNotifications()
|
||||||
.AddMassTransit(config =>
|
.AddMassTransit(config =>
|
||||||
{
|
{
|
||||||
|
config.DisableUsageTelemetry();
|
||||||
|
|
||||||
config.AddConsumer<DownloadRemoverConsumer<SearchItem>>();
|
config.AddConsumer<DownloadRemoverConsumer<SearchItem>>();
|
||||||
config.AddConsumer<DownloadRemoverConsumer<SonarrSearchItem>>();
|
config.AddConsumer<DownloadRemoverConsumer<SeriesSearchItem>>();
|
||||||
|
config.AddConsumer<DownloadHunterConsumer<SearchItem>>();
|
||||||
|
config.AddConsumer<DownloadHunterConsumer<SeriesSearchItem>>();
|
||||||
|
|
||||||
config.AddConsumer<NotificationConsumer<FailedImportStrikeNotification>>();
|
config.AddConsumer<NotificationConsumer<FailedImportStrikeNotification>>();
|
||||||
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
|
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
|
||||||
config.AddConsumer<NotificationConsumer<SlowStrikeNotification>>();
|
config.AddConsumer<NotificationConsumer<SlowSpeedStrikeNotification>>();
|
||||||
|
config.AddConsumer<NotificationConsumer<SlowTimeStrikeNotification>>();
|
||||||
config.AddConsumer<NotificationConsumer<QueueItemDeletedNotification>>();
|
config.AddConsumer<NotificationConsumer<QueueItemDeletedNotification>>();
|
||||||
config.AddConsumer<NotificationConsumer<DownloadCleanedNotification>>();
|
config.AddConsumer<NotificationConsumer<DownloadCleanedNotification>>();
|
||||||
config.AddConsumer<NotificationConsumer<CategoryChangedNotification>>();
|
config.AddConsumer<NotificationConsumer<CategoryChangedNotification>>();
|
||||||
@@ -48,7 +54,15 @@ public static class MainDI
|
|||||||
cfg.ReceiveEndpoint("download-remover-queue", e =>
|
cfg.ReceiveEndpoint("download-remover-queue", e =>
|
||||||
{
|
{
|
||||||
e.ConfigureConsumer<DownloadRemoverConsumer<SearchItem>>(context);
|
e.ConfigureConsumer<DownloadRemoverConsumer<SearchItem>>(context);
|
||||||
e.ConfigureConsumer<DownloadRemoverConsumer<SonarrSearchItem>>(context);
|
e.ConfigureConsumer<DownloadRemoverConsumer<SeriesSearchItem>>(context);
|
||||||
|
e.ConcurrentMessageLimit = 2;
|
||||||
|
e.PrefetchCount = 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
cfg.ReceiveEndpoint("download-hunter-queue", e =>
|
||||||
|
{
|
||||||
|
e.ConfigureConsumer<DownloadHunterConsumer<SearchItem>>(context);
|
||||||
|
e.ConfigureConsumer<DownloadHunterConsumer<SeriesSearchItem>>(context);
|
||||||
e.ConcurrentMessageLimit = 1;
|
e.ConcurrentMessageLimit = 1;
|
||||||
e.PrefetchCount = 1;
|
e.PrefetchCount = 1;
|
||||||
});
|
});
|
||||||
@@ -57,7 +71,8 @@ public static class MainDI
|
|||||||
{
|
{
|
||||||
e.ConfigureConsumer<NotificationConsumer<FailedImportStrikeNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<FailedImportStrikeNotification>>(context);
|
||||||
e.ConfigureConsumer<NotificationConsumer<StalledStrikeNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<StalledStrikeNotification>>(context);
|
||||||
e.ConfigureConsumer<NotificationConsumer<SlowStrikeNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<SlowSpeedStrikeNotification>>(context);
|
||||||
|
e.ConfigureConsumer<NotificationConsumer<SlowTimeStrikeNotification>>(context);
|
||||||
e.ConfigureConsumer<NotificationConsumer<QueueItemDeletedNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<QueueItemDeletedNotification>>(context);
|
||||||
e.ConfigureConsumer<NotificationConsumer<DownloadCleanedNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<DownloadCleanedNotification>>(context);
|
||||||
e.ConfigureConsumer<NotificationConsumer<CategoryChangedNotification>>(context);
|
e.ConfigureConsumer<NotificationConsumer<CategoryChangedNotification>>(context);
|
||||||
@@ -83,9 +98,17 @@ public static class MainDI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static IServiceCollection AddHealthServices(this IServiceCollection services) =>
|
private static IServiceCollection AddHealthServices(this IServiceCollection services) =>
|
||||||
services
|
services
|
||||||
// Register the health check service
|
// Register the existing health check service for download clients
|
||||||
.AddSingleton<IHealthCheckService, HealthCheckService>()
|
.AddSingleton<IHealthCheckService, HealthCheckService>()
|
||||||
|
|
||||||
// Register the background service for periodic health checks
|
// Register the background service for periodic health checks
|
||||||
.AddHostedService<HealthCheckBackgroundService>();
|
.AddHostedService<HealthCheckBackgroundService>()
|
||||||
|
|
||||||
|
// Add ASP.NET Core health checks
|
||||||
|
.AddHealthChecks()
|
||||||
|
.AddCheck<ApplicationHealthCheck>("application", tags: ["liveness"])
|
||||||
|
.AddCheck<DatabaseHealthCheck>("database", tags: ["readiness"])
|
||||||
|
.AddCheck<FileSystemHealthCheck>("filesystem", tags: ["readiness"])
|
||||||
|
.AddCheck<DownloadClientsHealthCheck>("download_clients", tags: ["readiness"])
|
||||||
|
.Services;
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||||
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
|
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
|
||||||
using Infrastructure.Verticals.Notifications;
|
using Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||||
|
|
||||||
namespace Cleanuparr.Api.DependencyInjection;
|
namespace Cleanuparr.Api.DependencyInjection;
|
||||||
|
|
||||||
public static class NotificationsDI
|
public static class NotificationsDI
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddNotifications(this IServiceCollection services, IConfiguration configuration) =>
|
public static IServiceCollection AddNotifications(this IServiceCollection services) =>
|
||||||
services
|
services
|
||||||
// Notification configs are now managed through ConfigManager
|
.AddScoped<INotifiarrProxy, NotifiarrProxy>()
|
||||||
.AddTransient<INotifiarrProxy, NotifiarrProxy>()
|
.AddScoped<IAppriseProxy, AppriseProxy>()
|
||||||
.AddTransient<INotificationProvider, NotifiarrProvider>()
|
.AddScoped<INtfyProxy, NtfyProxy>()
|
||||||
.AddTransient<IAppriseProxy, AppriseProxy>()
|
.AddScoped<INotificationConfigurationService, NotificationConfigurationService>()
|
||||||
.AddTransient<INotificationProvider, AppriseProvider>()
|
.AddScoped<INotificationProviderFactory, NotificationProviderFactory>()
|
||||||
.AddTransient<INotificationPublisher, NotificationPublisher>()
|
.AddScoped<NotificationProviderFactory>()
|
||||||
.AddTransient<INotificationFactory, NotificationFactory>()
|
.AddScoped<INotificationPublisher, NotificationPublisher>()
|
||||||
.AddTransient<NotificationService>();
|
.AddScoped<NotificationService>();
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
using Cleanuparr.Application.Features.ContentBlocker;
|
using Cleanuparr.Application.Features.BlacklistSync;
|
||||||
using Cleanuparr.Application.Features.DownloadCleaner;
|
using Cleanuparr.Application.Features.DownloadCleaner;
|
||||||
|
using Cleanuparr.Application.Features.DownloadClient;
|
||||||
|
using Cleanuparr.Application.Features.MalwareBlocker;
|
||||||
using Cleanuparr.Application.Features.QueueCleaner;
|
using Cleanuparr.Application.Features.QueueCleaner;
|
||||||
using Cleanuparr.Infrastructure.Events;
|
using Cleanuparr.Infrastructure.Events;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr;
|
using Cleanuparr.Infrastructure.Features.Arr;
|
||||||
using Cleanuparr.Infrastructure.Features.ContentBlocker;
|
|
||||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadHunter;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
|
||||||
using Cleanuparr.Infrastructure.Features.DownloadRemover;
|
using Cleanuparr.Infrastructure.Features.DownloadRemover;
|
||||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
||||||
using Cleanuparr.Infrastructure.Features.Files;
|
using Cleanuparr.Infrastructure.Features.Files;
|
||||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||||
|
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||||
using Cleanuparr.Infrastructure.Features.Security;
|
using Cleanuparr.Infrastructure.Features.Security;
|
||||||
|
using Cleanuparr.Infrastructure.Helpers;
|
||||||
using Cleanuparr.Infrastructure.Interceptors;
|
using Cleanuparr.Infrastructure.Interceptors;
|
||||||
using Cleanuparr.Infrastructure.Services;
|
using Cleanuparr.Infrastructure.Services;
|
||||||
|
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||||
using Cleanuparr.Persistence;
|
using Cleanuparr.Persistence;
|
||||||
using Infrastructure.Interceptors;
|
|
||||||
using Infrastructure.Services.Interfaces;
|
|
||||||
using Infrastructure.Verticals.Files;
|
using Infrastructure.Verticals.Files;
|
||||||
|
|
||||||
namespace Cleanuparr.Api.DependencyInjection;
|
namespace Cleanuparr.Api.DependencyInjection;
|
||||||
@@ -23,31 +27,34 @@ public static class ServicesDI
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||||
services
|
services
|
||||||
.AddSingleton<IEncryptionService, AesEncryptionService>()
|
.AddScoped<IEncryptionService, AesEncryptionService>()
|
||||||
.AddTransient<SensitiveDataJsonConverter>()
|
.AddScoped<SensitiveDataJsonConverter>()
|
||||||
.AddTransient<EventsContext>()
|
.AddScoped<EventsContext>()
|
||||||
.AddTransient<DataContext>()
|
.AddScoped<DataContext>()
|
||||||
.AddTransient<EventPublisher>()
|
.AddScoped<EventPublisher>()
|
||||||
.AddHostedService<EventCleanupService>()
|
.AddHostedService<EventCleanupService>()
|
||||||
// API services
|
.AddScoped<IDryRunInterceptor, DryRunInterceptor>()
|
||||||
|
.AddScoped<CertificateValidationService>()
|
||||||
|
.AddScoped<SonarrClient>()
|
||||||
|
.AddScoped<RadarrClient>()
|
||||||
|
.AddScoped<LidarrClient>()
|
||||||
|
.AddScoped<ReadarrClient>()
|
||||||
|
.AddScoped<WhisparrClient>()
|
||||||
|
.AddScoped<ArrClientFactory>()
|
||||||
|
.AddScoped<QueueCleaner>()
|
||||||
|
.AddScoped<BlacklistSynchronizer>()
|
||||||
|
.AddScoped<MalwareBlocker>()
|
||||||
|
.AddScoped<DownloadCleaner>()
|
||||||
|
.AddScoped<IQueueItemRemover, QueueItemRemover>()
|
||||||
|
.AddScoped<IDownloadHunter, DownloadHunter>()
|
||||||
|
.AddScoped<IFilenameEvaluator, FilenameEvaluator>()
|
||||||
|
.AddScoped<IHardLinkFileService, HardLinkFileService>()
|
||||||
|
.AddScoped<UnixHardLinkFileService>()
|
||||||
|
.AddScoped<WindowsHardLinkFileService>()
|
||||||
|
.AddScoped<ArrQueueIterator>()
|
||||||
|
.AddScoped<DownloadServiceFactory>()
|
||||||
|
.AddScoped<IStriker, Striker>()
|
||||||
|
.AddScoped<FileReader>()
|
||||||
.AddSingleton<IJobManagementService, JobManagementService>()
|
.AddSingleton<IJobManagementService, JobManagementService>()
|
||||||
// Core services
|
|
||||||
.AddTransient<IDryRunInterceptor, DryRunInterceptor>()
|
|
||||||
.AddTransient<CertificateValidationService>()
|
|
||||||
.AddTransient<SonarrClient>()
|
|
||||||
.AddTransient<RadarrClient>()
|
|
||||||
.AddTransient<LidarrClient>()
|
|
||||||
.AddTransient<ArrClientFactory>()
|
|
||||||
.AddTransient<QueueCleaner>()
|
|
||||||
.AddTransient<ContentBlocker>()
|
|
||||||
.AddTransient<DownloadCleaner>()
|
|
||||||
.AddTransient<IQueueItemRemover, QueueItemRemover>()
|
|
||||||
.AddTransient<IFilenameEvaluator, FilenameEvaluator>()
|
|
||||||
.AddTransient<IHardLinkFileService, HardLinkFileService>()
|
|
||||||
.AddTransient<UnixHardLinkFileService>()
|
|
||||||
.AddTransient<WindowsHardLinkFileService>()
|
|
||||||
.AddTransient<ArrQueueIterator>()
|
|
||||||
.AddTransient<DownloadServiceFactory>()
|
|
||||||
.AddTransient<IStriker, Striker>()
|
|
||||||
.AddSingleton<BlocklistProvider>();
|
.AddSingleton<BlocklistProvider>();
|
||||||
}
|
}
|
||||||
28
code/backend/Cleanuparr.Api/HealthCheckResponseWriter.cs
Normal file
28
code/backend/Cleanuparr.Api/HealthCheckResponseWriter.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom health check response writers for different formats
|
||||||
|
/// </summary>
|
||||||
|
public static class HealthCheckResponseWriter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a minimal plain text response suitable for Docker health checks
|
||||||
|
/// </summary>
|
||||||
|
public static async Task WriteMinimalPlaintext(HttpContext context, HealthReport report)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "text/plain";
|
||||||
|
|
||||||
|
var status = report.Status switch
|
||||||
|
{
|
||||||
|
HealthStatus.Healthy => "healthy",
|
||||||
|
HealthStatus.Degraded => "degraded",
|
||||||
|
HealthStatus.Unhealthy => "unhealthy",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(status, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace Cleanuparr.Api;
|
|||||||
|
|
||||||
public static class HostExtensions
|
public static class HostExtensions
|
||||||
{
|
{
|
||||||
public static async Task<IHost> Init(this WebApplication app)
|
public static async Task<IHost> InitAsync(this WebApplication app)
|
||||||
{
|
{
|
||||||
ILogger<Program> logger = app.Services.GetRequiredService<ILogger<Program>>();
|
ILogger<Program> logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
|
||||||
@@ -20,19 +20,25 @@ public static class HostExtensions
|
|||||||
|
|
||||||
logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
|
logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
|
||||||
|
|
||||||
// Apply db migrations
|
return app;
|
||||||
var eventsContext = app.Services.GetRequiredService<EventsContext>();
|
}
|
||||||
|
|
||||||
|
public static async Task<WebApplicationBuilder> InitAsync(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
// Apply events db migrations
|
||||||
|
await using var eventsContext = EventsContext.CreateStaticInstance();
|
||||||
if ((await eventsContext.Database.GetPendingMigrationsAsync()).Any())
|
if ((await eventsContext.Database.GetPendingMigrationsAsync()).Any())
|
||||||
{
|
{
|
||||||
await eventsContext.Database.MigrateAsync();
|
await eventsContext.Database.MigrateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var configContext = app.Services.GetRequiredService<DataContext>();
|
// Apply data db migrations
|
||||||
|
await using var configContext = DataContext.CreateStaticInstance();
|
||||||
if ((await configContext.Database.GetPendingMigrationsAsync()).Any())
|
if ((await configContext.Database.GetPendingMigrationsAsync()).Any())
|
||||||
{
|
{
|
||||||
await configContext.Database.MigrateAsync();
|
await configContext.Database.MigrateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
using Cleanuparr.Application.Features.ContentBlocker;
|
using Cleanuparr.Application.Features.BlacklistSync;
|
||||||
using Cleanuparr.Application.Features.DownloadCleaner;
|
using Cleanuparr.Application.Features.DownloadCleaner;
|
||||||
|
using Cleanuparr.Application.Features.DownloadClient;
|
||||||
|
using Cleanuparr.Application.Features.MalwareBlocker;
|
||||||
using Cleanuparr.Application.Features.QueueCleaner;
|
using Cleanuparr.Application.Features.QueueCleaner;
|
||||||
using Cleanuparr.Domain.Exceptions;
|
using Cleanuparr.Domain.Exceptions;
|
||||||
using Cleanuparr.Infrastructure.Features.Jobs;
|
using Cleanuparr.Infrastructure.Features.Jobs;
|
||||||
using Cleanuparr.Persistence;
|
using Cleanuparr.Persistence;
|
||||||
using Cleanuparr.Persistence.Models.Configuration;
|
using Cleanuparr.Persistence.Models.Configuration;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
|
|
||||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
|
||||||
using Cleanuparr.Shared.Helpers;
|
using Cleanuparr.Shared.Helpers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
@@ -22,18 +26,18 @@ namespace Cleanuparr.Api.Jobs;
|
|||||||
public class BackgroundJobManager : IHostedService
|
public class BackgroundJobManager : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ISchedulerFactory _schedulerFactory;
|
private readonly ISchedulerFactory _schedulerFactory;
|
||||||
private readonly DataContext _dataContext;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly ILogger<BackgroundJobManager> _logger;
|
private readonly ILogger<BackgroundJobManager> _logger;
|
||||||
private IScheduler? _scheduler;
|
private IScheduler? _scheduler;
|
||||||
|
|
||||||
public BackgroundJobManager(
|
public BackgroundJobManager(
|
||||||
ISchedulerFactory schedulerFactory,
|
ISchedulerFactory schedulerFactory,
|
||||||
DataContext dataContext,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<BackgroundJobManager> logger
|
ILogger<BackgroundJobManager> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_schedulerFactory = schedulerFactory;
|
_schedulerFactory = schedulerFactory;
|
||||||
_dataContext = dataContext;
|
_scopeFactory = scopeFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +49,12 @@ public class BackgroundJobManager : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting BackgroundJobManager");
|
_logger.LogDebug("Starting BackgroundJobManager");
|
||||||
_scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
_scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
||||||
|
|
||||||
await InitializeJobsFromConfiguration(cancellationToken);
|
await InitializeJobsFromConfiguration(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("BackgroundJobManager started");
|
_logger.LogDebug("BackgroundJobManager started");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -64,15 +68,15 @@ public class BackgroundJobManager : IHostedService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Stopping BackgroundJobManager");
|
_logger.LogDebug("Stopping BackgroundJobManager");
|
||||||
|
|
||||||
if (_scheduler != null)
|
if (_scheduler != null)
|
||||||
{
|
{
|
||||||
// Don't shutdown the scheduler as it's managed by QuartzHostedService
|
// Don't shut down the scheduler as it's managed by QuartzHostedService
|
||||||
await _scheduler.Standby(cancellationToken);
|
await _scheduler.Standby(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("BackgroundJobManager stopped");
|
_logger.LogDebug("BackgroundJobManager stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -86,21 +90,28 @@ public class BackgroundJobManager : IHostedService
|
|||||||
throw new InvalidOperationException("Scheduler not initialized");
|
throw new InvalidOperationException("Scheduler not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
|
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||||
|
|
||||||
// Get configurations from db
|
// Get configurations from db
|
||||||
QueueCleanerConfig queueCleanerConfig = await _dataContext.QueueCleanerConfigs
|
QueueCleanerConfig queueCleanerConfig = await dataContext.QueueCleanerConfigs
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstAsync(cancellationToken);
|
.FirstAsync(cancellationToken);
|
||||||
ContentBlockerConfig contentBlockerConfig = await _dataContext.ContentBlockerConfigs
|
ContentBlockerConfig malwareBlockerConfig = await dataContext.ContentBlockerConfigs
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstAsync(cancellationToken);
|
.FirstAsync(cancellationToken);
|
||||||
DownloadCleanerConfig downloadCleanerConfig = await _dataContext.DownloadCleanerConfigs
|
DownloadCleanerConfig downloadCleanerConfig = await dataContext.DownloadCleanerConfigs
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstAsync(cancellationToken);
|
||||||
|
BlacklistSyncConfig blacklistSyncConfig = await dataContext.BlacklistSyncConfigs
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstAsync(cancellationToken);
|
.FirstAsync(cancellationToken);
|
||||||
|
|
||||||
// Always register jobs, regardless of enabled status
|
// Always register jobs, regardless of enabled status
|
||||||
await RegisterQueueCleanerJob(queueCleanerConfig, cancellationToken);
|
await RegisterQueueCleanerJob(queueCleanerConfig, cancellationToken);
|
||||||
await RegisterContentBlockerJob(contentBlockerConfig, cancellationToken);
|
await RegisterMalwareBlockerJob(malwareBlockerConfig, cancellationToken);
|
||||||
await RegisterDownloadCleanerJob(downloadCleanerConfig, cancellationToken);
|
await RegisterDownloadCleanerJob(downloadCleanerConfig, cancellationToken);
|
||||||
|
await RegisterBlacklistSyncJob(blacklistSyncConfig, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,24 +127,24 @@ public class BackgroundJobManager : IHostedService
|
|||||||
// Only add triggers if the job is enabled
|
// Only add triggers if the job is enabled
|
||||||
if (config.Enabled)
|
if (config.Enabled)
|
||||||
{
|
{
|
||||||
await AddTriggersForJob<QueueCleaner>(config, config.CronExpression, cancellationToken);
|
await AddTriggersForJob<QueueCleaner>(config.CronExpression, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers the QueueCleaner job and optionally adds triggers based on configuration.
|
/// Registers the QueueCleaner job and optionally adds triggers based on configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task RegisterContentBlockerJob(
|
public async Task RegisterMalwareBlockerJob(
|
||||||
ContentBlockerConfig config,
|
ContentBlockerConfig config,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Always register the job definition
|
// Always register the job definition
|
||||||
await AddJobWithoutTrigger<ContentBlocker>(cancellationToken);
|
await AddJobWithoutTrigger<MalwareBlocker>(cancellationToken);
|
||||||
|
|
||||||
// Only add triggers if the job is enabled
|
// Only add triggers if the job is enabled
|
||||||
if (config.Enabled)
|
if (config.Enabled)
|
||||||
{
|
{
|
||||||
await AddTriggersForJob<ContentBlocker>(config, config.CronExpression, cancellationToken);
|
await AddTriggersForJob<MalwareBlocker>(config.CronExpression, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +159,21 @@ public class BackgroundJobManager : IHostedService
|
|||||||
// Only add triggers if the job is enabled
|
// Only add triggers if the job is enabled
|
||||||
if (config.Enabled)
|
if (config.Enabled)
|
||||||
{
|
{
|
||||||
await AddTriggersForJob<DownloadCleaner>(config, config.CronExpression, cancellationToken);
|
await AddTriggersForJob<DownloadCleaner>(config.CronExpression, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the BlacklistSync job and optionally adds triggers based on general configuration.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RegisterBlacklistSyncJob(BlacklistSyncConfig config, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Always register the job definition
|
||||||
|
await AddJobWithoutTrigger<BlacklistSynchronizer>(cancellationToken);
|
||||||
|
|
||||||
|
if (config.Enabled)
|
||||||
|
{
|
||||||
|
await AddTriggersForJob<BlacklistSynchronizer>(config.CronExpression, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +181,9 @@ public class BackgroundJobManager : IHostedService
|
|||||||
/// Helper method to add triggers for an existing job.
|
/// Helper method to add triggers for an existing job.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task AddTriggersForJob<T>(
|
private async Task AddTriggersForJob<T>(
|
||||||
IJobConfig config,
|
|
||||||
string cronExpression,
|
string cronExpression,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
where T : GenericHandler
|
where T : IHandler
|
||||||
{
|
{
|
||||||
if (_scheduler == null)
|
if (_scheduler == null)
|
||||||
{
|
{
|
||||||
@@ -175,7 +199,7 @@ public class BackgroundJobManager : IHostedService
|
|||||||
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
|
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
|
||||||
.WithIdentity("ValidationTrigger")
|
.WithIdentity("ValidationTrigger")
|
||||||
.StartNow()
|
.StartNow()
|
||||||
.WithCronSchedule(cronExpression)
|
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
|
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
|
||||||
@@ -186,7 +210,7 @@ public class BackgroundJobManager : IHostedService
|
|||||||
throw new ValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
|
throw new ValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(T) != typeof(ContentBlocker) && triggerValue < Constants.TriggerMinLimit)
|
if (typeof(T) != typeof(MalwareBlocker) && triggerValue < Constants.TriggerMinLimit)
|
||||||
{
|
{
|
||||||
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
|
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
|
||||||
}
|
}
|
||||||
@@ -197,26 +221,26 @@ public class BackgroundJobManager : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create cron trigger
|
// Create main cron trigger with consistent naming (matches JobManagementService)
|
||||||
var trigger = TriggerBuilder.Create()
|
var trigger = TriggerBuilder.Create()
|
||||||
.WithIdentity($"{typeName}-trigger")
|
.WithIdentity($"{typeName}-trigger")
|
||||||
.ForJob(jobKey)
|
.ForJob(jobKey)
|
||||||
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
|
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
|
||||||
.StartNow()
|
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// Create startup trigger to run immediately
|
// Schedule the main trigger
|
||||||
|
await _scheduler.ScheduleJob(trigger, cancellationToken);
|
||||||
|
|
||||||
|
// Trigger immediate execution for startup using a one-time trigger
|
||||||
var startupTrigger = TriggerBuilder.Create()
|
var startupTrigger = TriggerBuilder.Create()
|
||||||
.WithIdentity($"{typeName}-startup-trigger")
|
.WithIdentity($"{typeName}-startup-{DateTimeOffset.UtcNow.Ticks}")
|
||||||
.ForJob(jobKey)
|
.ForJob(jobKey)
|
||||||
.StartNow()
|
.StartNow()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// Schedule job with both triggers
|
|
||||||
await _scheduler.ScheduleJob(trigger, cancellationToken);
|
|
||||||
await _scheduler.ScheduleJob(startupTrigger, cancellationToken);
|
await _scheduler.ScheduleJob(startupTrigger, cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("Added triggers for job {name} with cron expression {CronExpression}",
|
_logger.LogInformation("Added trigger for job {name} with cron expression {CronExpression} and immediate startup execution",
|
||||||
typeName, cronExpression);
|
typeName, cronExpression);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +248,7 @@ public class BackgroundJobManager : IHostedService
|
|||||||
/// Helper method to add a job without a trigger (for chained jobs).
|
/// Helper method to add a job without a trigger (for chained jobs).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task AddJobWithoutTrigger<T>(CancellationToken cancellationToken = default)
|
private async Task AddJobWithoutTrigger<T>(CancellationToken cancellationToken = default)
|
||||||
where T : GenericHandler
|
where T : IHandler
|
||||||
{
|
{
|
||||||
if (_scheduler == null)
|
if (_scheduler == null)
|
||||||
{
|
{
|
||||||
@@ -250,6 +274,6 @@ public class BackgroundJobManager : IHostedService
|
|||||||
// Add job to scheduler
|
// Add job to scheduler
|
||||||
await _scheduler.AddJob(jobDetail, true, cancellationToken);
|
await _scheduler.AddJob(jobDetail, true, cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("Registered job {name} without trigger", typeName);
|
_logger.LogDebug("Registered job {name} without trigger", typeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ public sealed class GenericJob<T> : IJob
|
|||||||
where T : IHandler
|
where T : IHandler
|
||||||
{
|
{
|
||||||
private readonly ILogger<GenericJob<T>> _logger;
|
private readonly ILogger<GenericJob<T>> _logger;
|
||||||
private readonly T _handler;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
public GenericJob(ILogger<GenericJob<T>> logger, T handler)
|
public GenericJob(ILogger<GenericJob<T>> logger, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_handler = handler;
|
_scopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
@@ -23,7 +23,9 @@ public sealed class GenericJob<T> : IJob
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _handler.ExecuteAsync();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
|
var handler = scope.ServiceProvider.GetRequiredService<T>();
|
||||||
|
await handler.ExecuteAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record CreateAppriseProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Key { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Tags { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record CreateNotifiarrProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ChannelId { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public abstract record CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsEnabled { get; init; } = true;
|
||||||
|
|
||||||
|
public bool OnFailedImportStrike { get; init; }
|
||||||
|
|
||||||
|
public bool OnStalledStrike { get; init; }
|
||||||
|
|
||||||
|
public bool OnSlowStrike { get; init; }
|
||||||
|
|
||||||
|
public bool OnQueueItemDeleted { get; init; }
|
||||||
|
|
||||||
|
public bool OnDownloadCleaned { get; init; }
|
||||||
|
|
||||||
|
public bool OnCategoryChanged { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record CreateNtfyProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string ServerUrl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> Topics { get; init; } = [];
|
||||||
|
|
||||||
|
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||||
|
|
||||||
|
public string Username { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Password { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string AccessToken { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||||
|
|
||||||
|
public List<string> Tags { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record TestAppriseProviderDto
|
||||||
|
{
|
||||||
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Key { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Tags { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record TestNotifiarrProviderDto
|
||||||
|
{
|
||||||
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ChannelId { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record TestNtfyProviderDto
|
||||||
|
{
|
||||||
|
public string ServerUrl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> Topics { get; init; } = [];
|
||||||
|
|
||||||
|
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||||
|
|
||||||
|
public string Username { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Password { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string AccessToken { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||||
|
|
||||||
|
public List<string> Tags { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record UpdateAppriseProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Key { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Tags { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record UpdateNotifiarrProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ChannelId { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||||
|
|
||||||
|
public sealed record UpdateNtfyProviderDto : CreateNotificationProviderBaseDto
|
||||||
|
{
|
||||||
|
public string ServerUrl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> Topics { get; init; } = [];
|
||||||
|
|
||||||
|
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||||
|
|
||||||
|
public string Username { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Password { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string AccessToken { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||||
|
|
||||||
|
public List<string> Tags { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ public class UpdateDownloadCleanerConfigDto
|
|||||||
public string UnlinkedIgnoredRootDir { get; set; } = string.Empty;
|
public string UnlinkedIgnoredRootDir { get; set; } = string.Empty;
|
||||||
|
|
||||||
public List<string> UnlinkedCategories { get; set; } = [];
|
public List<string> UnlinkedCategories { get; set; } = [];
|
||||||
|
|
||||||
|
public List<string> IgnoredDownloads { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CleanCategoryDto
|
public class CleanCategoryDto
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Cleanuparr.Api;
|
using Cleanuparr.Api;
|
||||||
using Cleanuparr.Api.DependencyInjection;
|
using Cleanuparr.Api.DependencyInjection;
|
||||||
|
using Cleanuparr.Infrastructure.Hubs;
|
||||||
using Cleanuparr.Infrastructure.Logging;
|
using Cleanuparr.Infrastructure.Logging;
|
||||||
using Cleanuparr.Shared.Helpers;
|
using Cleanuparr.Shared.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
await builder.InitAsync();
|
||||||
|
builder.Logging.AddLogging();
|
||||||
|
|
||||||
// Fix paths for single-file deployment on macOS
|
// Fix paths for single-file deployment on macOS
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
@@ -66,14 +72,6 @@ builder.Services.AddCors(options =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register services needed for logging first
|
|
||||||
builder.Services
|
|
||||||
.AddTransient<LoggingConfigManager>()
|
|
||||||
.AddSingleton<SignalRLogSink>();
|
|
||||||
|
|
||||||
// Add logging with proper service provider
|
|
||||||
builder.Logging.AddLogging();
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
builder.Host.UseWindowsService(options =>
|
builder.Host.UseWindowsService(options =>
|
||||||
@@ -128,25 +126,27 @@ if (basePath is not null)
|
|||||||
logger.LogInformation("Server configuration: PORT={port}, BASE_PATH={basePath}", port, basePath ?? "/");
|
logger.LogInformation("Server configuration: PORT={port}, BASE_PATH={basePath}", port, basePath ?? "/");
|
||||||
|
|
||||||
// Initialize the host
|
// Initialize the host
|
||||||
await app.Init();
|
await app.InitAsync();
|
||||||
|
|
||||||
// Get LoggingConfigManager (will be created if not already registered)
|
// Configure the app hub for SignalR
|
||||||
var configManager = app.Services.GetRequiredService<LoggingConfigManager>();
|
var appHub = app.Services.GetRequiredService<IHubContext<AppHub>>();
|
||||||
|
SignalRLogSink.Instance.SetAppHubContext(appHub);
|
||||||
// Get the dynamic level switch for controlling log levels
|
|
||||||
var levelSwitch = configManager.GetLevelSwitch();
|
|
||||||
|
|
||||||
// Get the SignalRLogSink instance
|
|
||||||
var signalRSink = app.Services.GetRequiredService<SignalRLogSink>();
|
|
||||||
|
|
||||||
var logConfig = LoggingDI.GetDefaultLoggerConfiguration();
|
// Configure health check endpoints before the API configuration
|
||||||
logConfig.MinimumLevel.ControlledBy(levelSwitch);
|
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||||
|
{
|
||||||
// Add to Serilog pipeline
|
Predicate = registration => registration.Tags.Contains("liveness"),
|
||||||
logConfig.WriteTo.Sink(signalRSink);
|
ResponseWriter = HealthCheckResponseWriter.WriteMinimalPlaintext
|
||||||
|
});
|
||||||
|
|
||||||
Log.Logger = logConfig.CreateLogger();
|
app.MapHealthChecks("/health/ready", new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = registration => registration.Tags.Contains("readiness"),
|
||||||
|
ResponseWriter = HealthCheckResponseWriter.WriteMinimalPlaintext
|
||||||
|
});
|
||||||
|
|
||||||
app.ConfigureApi();
|
app.ConfigureApi();
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
|
||||||
|
await Log.CloseAndFlushAsync();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Cleanuparr.Application.Features.Arr.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for updating Readarr configuration basic settings (instances managed separately)
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateReadarrConfigDto
|
||||||
|
{
|
||||||
|
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Cleanuparr.Application.Features.Arr.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for updating Whisparr configuration basic settings (instances managed separately)
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateWhisparrConfigDto
|
||||||
|
{
|
||||||
|
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||||
|
using Cleanuparr.Infrastructure.Features.Jobs;
|
||||||
|
using Cleanuparr.Infrastructure.Helpers;
|
||||||
|
using Cleanuparr.Infrastructure.Interceptors;
|
||||||
|
using Cleanuparr.Persistence;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
|
||||||
|
using Cleanuparr.Persistence.Models.State;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Application.Features.BlacklistSync;
|
||||||
|
|
||||||
|
public sealed class BlacklistSynchronizer : IHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<BlacklistSynchronizer> _logger;
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
private readonly DownloadServiceFactory _downloadServiceFactory;
|
||||||
|
private readonly FileReader _fileReader;
|
||||||
|
private readonly IDryRunInterceptor _dryRunInterceptor;
|
||||||
|
|
||||||
|
public BlacklistSynchronizer(
|
||||||
|
ILogger<BlacklistSynchronizer> logger,
|
||||||
|
DataContext dataContext,
|
||||||
|
DownloadServiceFactory downloadServiceFactory,
|
||||||
|
FileReader fileReader,
|
||||||
|
IDryRunInterceptor dryRunInterceptor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dataContext = dataContext;
|
||||||
|
_downloadServiceFactory = downloadServiceFactory;
|
||||||
|
_fileReader = fileReader;
|
||||||
|
_dryRunInterceptor = dryRunInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
BlacklistSyncConfig config = await _dataContext.BlacklistSyncConfigs
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (!config.Enabled)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Blacklist sync is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.BlacklistPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Blacklist sync path is not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] patterns = await _fileReader.ReadContentAsync(config.BlacklistPath);
|
||||||
|
string excludedFileNames = string.Join('\n', patterns.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||||||
|
|
||||||
|
string currentHash = ComputeHash(excludedFileNames);
|
||||||
|
|
||||||
|
await _dryRunInterceptor.InterceptAsync(SyncBlacklist, currentHash, excludedFileNames);
|
||||||
|
await _dryRunInterceptor.InterceptAsync(RemoveOldSyncDataAsync, currentHash);
|
||||||
|
|
||||||
|
_logger.LogDebug("Blacklist synchronization completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SyncBlacklist(string currentHash, string excludedFileNames)
|
||||||
|
{
|
||||||
|
List<DownloadClientConfig> qBittorrentClients = await _dataContext.DownloadClients
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(c => c.Enabled && c.TypeName == DownloadClientTypeName.qBittorrent)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (qBittorrentClients.Count is 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No enabled qBittorrent clients found for blacklist sync");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Starting blacklist synchronization for {Count} qBittorrent clients", qBittorrentClients.Count);
|
||||||
|
|
||||||
|
// Pull existing sync history for this hash
|
||||||
|
var alreadySynced = await _dataContext.BlacklistSyncHistory
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(s => s.Hash == currentHash)
|
||||||
|
.Select(x => x.DownloadClientId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Only update clients not present in history for current hash
|
||||||
|
foreach (var clientConfig in qBittorrentClients)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (alreadySynced.Contains(clientConfig.Id))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Client {ClientName} already synced for current blacklist, skipping", clientConfig.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadService = _downloadServiceFactory.GetDownloadService(clientConfig);
|
||||||
|
if (downloadService is not QBitService qBitService)
|
||||||
|
{
|
||||||
|
_logger.LogError("Expected QBitService but got {ServiceType} for client {ClientName}", downloadService.GetType().Name, clientConfig.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await qBitService.LoginAsync();
|
||||||
|
await qBitService.UpdateBlacklistAsync(excludedFileNames);
|
||||||
|
|
||||||
|
_logger.LogDebug("Successfully updated blacklist for qBittorrent client {ClientName}", clientConfig.Name);
|
||||||
|
|
||||||
|
// Insert history row marking this client as synced for current hash
|
||||||
|
_dataContext.BlacklistSyncHistory.Add(new BlacklistSyncHistory
|
||||||
|
{
|
||||||
|
Hash = currentHash,
|
||||||
|
DownloadClientId = clientConfig.Id
|
||||||
|
});
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to update blacklist for qBittorrent client {ClientName}", clientConfig.Name);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
qBitService.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create download service for client {ClientName}", clientConfig.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeHash(string excludedFileNames)
|
||||||
|
{
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(excludedFileNames);
|
||||||
|
byte[] hash = sha.ComputeHash(bytes);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveOldSyncDataAsync(string currentHash)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dataContext.BlacklistSyncHistory
|
||||||
|
.Where(s => s.Hash != currentHash)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to cleanup old blacklist sync history");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Enums;
|
||||||
using Cleanuparr.Infrastructure.Events;
|
using Cleanuparr.Infrastructure.Events;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr;
|
using Cleanuparr.Infrastructure.Features.Arr;
|
||||||
@@ -10,7 +11,6 @@ using Cleanuparr.Persistence;
|
|||||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||||
using Data.Models.Arr.Queue;
|
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -59,10 +59,11 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
List<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||||
|
ignoredDownloads.AddRange(ContextProvider.Get<DownloadCleanerConfig>().IgnoredDownloads);
|
||||||
|
|
||||||
|
var downloadServiceToDownloadsMap = new Dictionary<IDownloadService, List<object>>();
|
||||||
|
|
||||||
// Process each client separately
|
|
||||||
var allDownloads = new List<object>();
|
|
||||||
foreach (var downloadService in downloadServices)
|
foreach (var downloadService in downloadServices)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -71,24 +72,24 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
var clientDownloads = await downloadService.GetSeedingDownloads();
|
var clientDownloads = await downloadService.GetSeedingDownloads();
|
||||||
if (clientDownloads?.Count > 0)
|
if (clientDownloads?.Count > 0)
|
||||||
{
|
{
|
||||||
allDownloads.AddRange(clientDownloads);
|
downloadServiceToDownloadsMap[downloadService] = clientDownloads;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to get seeding downloads from download client");
|
_logger.LogError(ex, "Failed to get seeding downloads from download client {clientName}", downloadService.ClientConfig.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allDownloads.Count == 0)
|
if (downloadServiceToDownloadsMap.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("no seeding downloads found");
|
_logger.LogDebug("no seeding downloads found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogTrace("found {count} seeding downloads", allDownloads.Count);
|
var totalDownloads = downloadServiceToDownloadsMap.Values.Sum(x => x.Count);
|
||||||
|
_logger.LogTrace("found {count} seeding downloads across {clientCount} clients", totalDownloads, downloadServiceToDownloadsMap.Count);
|
||||||
|
|
||||||
// List<object>? downloadsToChangeCategory = null;
|
|
||||||
List<Tuple<IDownloadService, List<object>>> downloadServiceWithDownloads = [];
|
List<Tuple<IDownloadService, List<object>>> downloadServiceWithDownloads = [];
|
||||||
|
|
||||||
if (isUnlinkedEnabled)
|
if (isUnlinkedEnabled)
|
||||||
@@ -102,24 +103,23 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to create category for download client");
|
_logger.LogError(ex, "Failed to create category for download client {clientName}", downloadService.ClientConfig.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get downloads to change category
|
foreach (var (downloadService, clientDownloads) in downloadServiceToDownloadsMap)
|
||||||
foreach (var downloadService in downloadServices)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var clientDownloads = downloadService.FilterDownloadsToChangeCategoryAsync(allDownloads, config.UnlinkedCategories);
|
var downloadsToChangeCategory = downloadService.FilterDownloadsToChangeCategoryAsync(clientDownloads, config.UnlinkedCategories);
|
||||||
if (clientDownloads?.Count > 0)
|
if (downloadsToChangeCategory?.Count > 0)
|
||||||
{
|
{
|
||||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, clientDownloads));
|
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, downloadsToChangeCategory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to filter downloads for category change");
|
_logger.LogError(ex, "Failed to filter downloads for category change for download client {clientName}", downloadService.ClientConfig.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,9 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr)), InstanceType.Sonarr, true);
|
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr)), InstanceType.Sonarr, true);
|
||||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr)), InstanceType.Radarr, true);
|
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr)), InstanceType.Radarr, true);
|
||||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr)), InstanceType.Lidarr, true);
|
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr)), InstanceType.Lidarr, true);
|
||||||
|
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr)), InstanceType.Readarr, true);
|
||||||
|
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr)), InstanceType.Whisparr, true);
|
||||||
|
|
||||||
if (isUnlinkedEnabled && downloadServiceWithDownloads.Count > 0)
|
if (isUnlinkedEnabled && downloadServiceWithDownloads.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Found {count} potential downloads to change category", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
_logger.LogInformation("Found {count} potential downloads to change category", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
||||||
@@ -156,16 +158,15 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get downloads to clean
|
|
||||||
downloadServiceWithDownloads = [];
|
downloadServiceWithDownloads = [];
|
||||||
foreach (var downloadService in downloadServices)
|
foreach (var (downloadService, clientDownloads) in downloadServiceToDownloadsMap)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var clientDownloads = downloadService.FilterDownloadsToBeCleanedAsync(allDownloads, config.Categories);
|
var downloadsToClean = downloadService.FilterDownloadsToBeCleanedAsync(clientDownloads, config.Categories);
|
||||||
if (clientDownloads?.Count > 0)
|
if (downloadsToClean?.Count > 0)
|
||||||
{
|
{
|
||||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, clientDownloads));
|
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, downloadsToClean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -174,9 +175,6 @@ public sealed class DownloadCleaner : GenericHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// release unused objects
|
|
||||||
allDownloads = null;
|
|
||||||
|
|
||||||
_logger.LogInformation("found {count} potential downloads to clean", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
_logger.LogInformation("found {count} potential downloads to clean", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
||||||
|
|
||||||
// Process cleaning for each client
|
// Process cleaning for each client
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
using Cleanuparr.Infrastructure.Events;
|
using Cleanuparr.Infrastructure.Events;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr;
|
using Cleanuparr.Infrastructure.Features.Arr;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||||
using Cleanuparr.Infrastructure.Features.ContentBlocker;
|
|
||||||
using Cleanuparr.Infrastructure.Features.Context;
|
using Cleanuparr.Infrastructure.Features.Context;
|
||||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||||
using Cleanuparr.Infrastructure.Features.Jobs;
|
using Cleanuparr.Infrastructure.Features.Jobs;
|
||||||
|
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||||
using Cleanuparr.Infrastructure.Helpers;
|
using Cleanuparr.Infrastructure.Helpers;
|
||||||
using Cleanuparr.Persistence;
|
using Cleanuparr.Persistence;
|
||||||
using Cleanuparr.Persistence.Models.Configuration;
|
using Cleanuparr.Persistence.Models.Configuration;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
|
|
||||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||||
using Data.Models.Arr.Queue;
|
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LogContext = Serilog.Context.LogContext;
|
using LogContext = Serilog.Context.LogContext;
|
||||||
|
|
||||||
namespace Cleanuparr.Application.Features.ContentBlocker;
|
namespace Cleanuparr.Application.Features.MalwareBlocker;
|
||||||
|
|
||||||
public sealed class ContentBlocker : GenericHandler
|
public sealed class MalwareBlocker : GenericHandler
|
||||||
{
|
{
|
||||||
private readonly BlocklistProvider _blocklistProvider;
|
private readonly BlocklistProvider _blocklistProvider;
|
||||||
|
|
||||||
public ContentBlocker(
|
public MalwareBlocker(
|
||||||
ILogger<ContentBlocker> logger,
|
ILogger<MalwareBlocker> logger,
|
||||||
DataContext dataContext,
|
DataContext dataContext,
|
||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
IBus messageBus,
|
IBus messageBus,
|
||||||
@@ -52,7 +52,7 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
|
|
||||||
var config = ContextProvider.Get<ContentBlockerConfig>();
|
var config = ContextProvider.Get<ContentBlockerConfig>();
|
||||||
|
|
||||||
if (!config.Sonarr.Enabled && !config.Radarr.Enabled && !config.Lidarr.Enabled)
|
if (!config.Sonarr.Enabled && !config.Radarr.Enabled && !config.Lidarr.Enabled && !config.Readarr.Enabled && !config.Whisparr.Enabled)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No blocklists are enabled");
|
_logger.LogWarning("No blocklists are enabled");
|
||||||
return;
|
return;
|
||||||
@@ -63,26 +63,39 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
var sonarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr));
|
var sonarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr));
|
||||||
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
||||||
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
||||||
|
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||||
|
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||||
|
|
||||||
if (config.Sonarr.Enabled)
|
if (config.Sonarr.Enabled || config.DeleteKnownMalware)
|
||||||
{
|
{
|
||||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Radarr.Enabled)
|
if (config.Radarr.Enabled || config.DeleteKnownMalware)
|
||||||
{
|
{
|
||||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Lidarr.Enabled)
|
if (config.Lidarr.Enabled || config.DeleteKnownMalware)
|
||||||
{
|
{
|
||||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.Readarr.Enabled || config.DeleteKnownMalware)
|
||||||
|
{
|
||||||
|
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Whisparr.Enabled || config.DeleteKnownMalware)
|
||||||
|
{
|
||||||
|
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
IReadOnlyList<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>().IgnoredDownloads;
|
List<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||||
|
ignoredDownloads.AddRange(ContextProvider.Get<ContentBlockerConfig>().IgnoredDownloads);
|
||||||
|
|
||||||
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
||||||
|
|
||||||
@@ -171,6 +184,10 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No torrent clients enabled");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.ShouldRemove)
|
if (!result.ShouldRemove)
|
||||||
@@ -194,7 +211,7 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
record,
|
record,
|
||||||
group.Count() > 1,
|
group.Count() > 1,
|
||||||
removeFromClient,
|
removeFromClient,
|
||||||
DeleteReason.AllFilesBlocked
|
result.DeleteReason
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Enums;
|
||||||
using Cleanuparr.Infrastructure.Events;
|
using Cleanuparr.Infrastructure.Events;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr;
|
using Cleanuparr.Infrastructure.Features.Arr;
|
||||||
@@ -11,7 +12,6 @@ using Cleanuparr.Persistence.Models.Configuration;
|
|||||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||||
using Data.Models.Arr.Queue;
|
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -42,15 +42,20 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
var sonarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr));
|
var sonarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr));
|
||||||
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
var radarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr));
|
||||||
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
||||||
|
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||||
|
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||||
|
|
||||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||||
|
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||||
|
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||||
{
|
{
|
||||||
IReadOnlyList<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>().IgnoredDownloads;
|
List<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||||
|
ignoredDownloads.AddRange(ContextProvider.Get<QueueCleanerConfig>().IgnoredDownloads);
|
||||||
|
|
||||||
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
||||||
|
|
||||||
@@ -103,7 +108,7 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
|
|
||||||
DownloadCheckResult downloadCheckResult = new();
|
DownloadCheckResult downloadCheckResult = new();
|
||||||
|
|
||||||
if (record.Protocol is "torrent")
|
if (record.Protocol.Contains("torrent", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
var torrentClients = downloadServices
|
var torrentClients = downloadServices
|
||||||
.Where(x => x.ClientConfig.Type is DownloadClientType.Torrent)
|
.Where(x => x.ClientConfig.Type is DownloadClientType.Torrent)
|
||||||
@@ -137,12 +142,16 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No torrent clients enabled");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = ContextProvider.Get<QueueCleanerConfig>();
|
var config = ContextProvider.Get<QueueCleanerConfig>();
|
||||||
|
|
||||||
// failed import check
|
// failed import check
|
||||||
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, config.FailedImport.MaxStrikes);
|
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, instance.ArrConfig.FailedImportMaxStrikes);
|
||||||
DeleteReason deleteReason = downloadCheckResult.ShouldRemove ? downloadCheckResult.DeleteReason : DeleteReason.FailedImport;
|
DeleteReason deleteReason = downloadCheckResult.ShouldRemove ? downloadCheckResult.DeleteReason : DeleteReason.FailedImport;
|
||||||
|
|
||||||
if (!shouldRemoveFromArr && !downloadCheckResult.ShouldRemove)
|
if (!shouldRemoveFromArr && !downloadCheckResult.ShouldRemove)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public record Image
|
public record Image
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public record LidarrImage
|
public record LidarrImage
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public sealed record QueueAlbum
|
public sealed record QueueAlbum
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
|
public sealed record QueueBook
|
||||||
|
{
|
||||||
|
public List<ReadarrImage> Images { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public record QueueListResponse
|
public record QueueListResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public sealed record QueueMovie
|
public sealed record QueueMovie
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public sealed record QueueRecord
|
public sealed record QueueRecord
|
||||||
{
|
{
|
||||||
// Sonarr
|
// Sonarr and Whisparr
|
||||||
public long SeriesId { get; init; }
|
public long SeriesId { get; init; }
|
||||||
public long EpisodeId { get; init; }
|
public long EpisodeId { get; init; }
|
||||||
public long SeasonNumber { get; init; }
|
public long SeasonNumber { get; init; }
|
||||||
@@ -21,6 +21,13 @@ public sealed record QueueRecord
|
|||||||
|
|
||||||
public QueueAlbum? Album { get; init; }
|
public QueueAlbum? Album { get; init; }
|
||||||
|
|
||||||
|
// Readarr
|
||||||
|
public long AuthorId { get; init; }
|
||||||
|
|
||||||
|
public long BookId { get; init; }
|
||||||
|
|
||||||
|
public QueueBook? Book { get; init; }
|
||||||
|
|
||||||
// common
|
// common
|
||||||
public required string Title { get; init; }
|
public required string Title { get; init; }
|
||||||
public string Status { get; init; }
|
public string Status { get; init; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public sealed record QueueSeries
|
public sealed record QueueSeries
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
|
public sealed record ReadarrImage
|
||||||
|
{
|
||||||
|
public required string CoverType { get; init; }
|
||||||
|
|
||||||
|
public required Uri Url { get; init; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Arr.Queue;
|
namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
|
||||||
public sealed record TrackedDownloadStatusMessage
|
public sealed record TrackedDownloadStatusMessage
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Enums;
|
||||||
|
using Data.Models.Arr;
|
||||||
|
|
||||||
namespace Data.Models.Arr;
|
namespace Cleanuparr.Domain.Entities.Arr;
|
||||||
|
|
||||||
public sealed class SonarrSearchItem : SearchItem
|
public sealed class SeriesSearchItem : SearchItem
|
||||||
{
|
{
|
||||||
public long SeriesId { get; set; }
|
public long SeriesId { get; set; }
|
||||||
|
|
||||||
public SonarrSearchType SearchType { get; set; }
|
public SeriesSearchType SearchType { get; set; }
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
if (obj is not SonarrSearchItem other)
|
if (obj is not SeriesSearchItem other)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.Readarr;
|
||||||
|
|
||||||
|
public sealed record Author
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public string AuthorName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
12
code/backend/Cleanuparr.Domain/Entities/Readarr/Book.cs
Normal file
12
code/backend/Cleanuparr.Domain/Entities/Readarr/Book.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.Readarr;
|
||||||
|
|
||||||
|
public sealed record Book
|
||||||
|
{
|
||||||
|
public required long Id { get; init; }
|
||||||
|
|
||||||
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public long AuthorId { get; set; }
|
||||||
|
|
||||||
|
public Author Author { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.Readarr;
|
||||||
|
|
||||||
|
public sealed record ReadarrCommand
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<long> BookIds { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -12,5 +12,5 @@ public sealed record SonarrCommand
|
|||||||
|
|
||||||
public List<long>? EpisodeIds { get; set; }
|
public List<long>? EpisodeIds { get; set; }
|
||||||
|
|
||||||
public SonarrSearchType SearchType { get; set; }
|
public SeriesSearchType SearchType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Request;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to the µTorrent Web UI API
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UTorrentRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The API action to perform
|
||||||
|
/// </summary>
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication token (required for CSRF protection)
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional parameters for the request
|
||||||
|
/// </summary>
|
||||||
|
public List<(string Name, string Value)> Parameters { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs the query string for the API call
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The complete query string including token and action</returns>
|
||||||
|
public string ToQueryString()
|
||||||
|
{
|
||||||
|
var queryParams = new List<string>
|
||||||
|
{
|
||||||
|
$"token={Token}",
|
||||||
|
Action
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var param in Parameters)
|
||||||
|
{
|
||||||
|
queryParams.Add($"{Uri.EscapeDataString(param.Name)}={Uri.EscapeDataString(param.Value)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("&", queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new request with the specified action
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The API action</param>
|
||||||
|
/// <param name="token">Authentication token</param>
|
||||||
|
/// <returns>A new UTorrentRequest instance</returns>
|
||||||
|
public static UTorrentRequest Create(string action, string token)
|
||||||
|
{
|
||||||
|
return new UTorrentRequest
|
||||||
|
{
|
||||||
|
Action = action,
|
||||||
|
Token = token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a parameter to the request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Parameter name</param>
|
||||||
|
/// <param name="value">Parameter value</param>
|
||||||
|
/// <returns>This instance for method chaining</returns>
|
||||||
|
public UTorrentRequest WithParameter(string key, string value)
|
||||||
|
{
|
||||||
|
Parameters.Add((key, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specific response type for file list API calls
|
||||||
|
/// Replaces the generic UTorrentResponse<T> for file listings
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FileListResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Raw file data from the API
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "files")]
|
||||||
|
public object[]? FilesRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Torrent hash for which files are listed
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed files as strongly-typed objects
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<UTorrentFile> Files { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specific response type for label list API calls
|
||||||
|
/// Replaces the generic UTorrentResponse<T> for label listings
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LabelListResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Raw label data from the API
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "label")]
|
||||||
|
public object[][]? LabelsRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed labels as string list
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<string> Labels { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specific response type for torrent properties API calls
|
||||||
|
/// Replaces the generic UTorrentResponse<T> for properties retrieval
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PropertiesResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Raw properties data from the API
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "props")]
|
||||||
|
public object[]? PropertiesRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed properties as strongly-typed object
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public UTorrentProperties Properties { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specific response type for torrent list API calls
|
||||||
|
/// Replaces the generic UTorrentResponse<T> for torrent listings
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TorrentListResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// µTorrent build number
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "build")]
|
||||||
|
public int Build { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of torrent data from the API
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "torrents")]
|
||||||
|
public object[][]? TorrentsRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label data from the API
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "label")]
|
||||||
|
public object[][]? LabelsRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed torrents as strongly-typed objects
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<UTorrentItem> Torrents { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed labels as string list
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<string> Labels { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a file within a torrent from µTorrent Web UI API
|
||||||
|
/// Based on the files array structure from the API documentation
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UTorrentFile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
public long Downloaded { get; set; }
|
||||||
|
|
||||||
|
public int Priority { get; set; }
|
||||||
|
|
||||||
|
public int Index { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a torrent from µTorrent Web UI API
|
||||||
|
/// Based on the torrent array structure from the API documentation
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UTorrentItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Torrent hash (index 0)
|
||||||
|
/// </summary>
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status bitfield (index 1)
|
||||||
|
/// </summary>
|
||||||
|
public int Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Torrent name (index 2)
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total size in bytes (index 3)
|
||||||
|
/// </summary>
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Progress in permille (1000 = 100%) (index 4)
|
||||||
|
/// </summary>
|
||||||
|
public int Progress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloaded bytes (index 5)
|
||||||
|
/// </summary>
|
||||||
|
public long Downloaded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploaded bytes (index 6)
|
||||||
|
/// </summary>
|
||||||
|
public long Uploaded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ratio * 1000 (index 7)
|
||||||
|
/// </summary>
|
||||||
|
public int RatioRaw { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload speed in bytes/sec (index 8)
|
||||||
|
/// </summary>
|
||||||
|
public int UploadSpeed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download speed in bytes/sec (index 9)
|
||||||
|
/// </summary>
|
||||||
|
public int DownloadSpeed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ETA in seconds (index 10)
|
||||||
|
/// </summary>
|
||||||
|
public int ETA { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label (index 11)
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connected peers (index 12)
|
||||||
|
/// </summary>
|
||||||
|
public int PeersConnected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Peers in swarm (index 13)
|
||||||
|
/// </summary>
|
||||||
|
public int PeersInSwarm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connected seeds (index 14)
|
||||||
|
/// </summary>
|
||||||
|
public int SeedsConnected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeds in swarm (index 15)
|
||||||
|
/// </summary>
|
||||||
|
public int SeedsInSwarm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Availability (index 16)
|
||||||
|
/// </summary>
|
||||||
|
public int Availability { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue order (index 17)
|
||||||
|
/// </summary>
|
||||||
|
public int QueueOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remaining bytes (index 18)
|
||||||
|
/// </summary>
|
||||||
|
public long Remaining { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download URL (index 19)
|
||||||
|
/// </summary>
|
||||||
|
public string DownloadUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RSS feed URL (index 20)
|
||||||
|
/// </summary>
|
||||||
|
public string RssFeedUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status message (index 21)
|
||||||
|
/// </summary>
|
||||||
|
public string StatusMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stream ID (index 22)
|
||||||
|
/// </summary>
|
||||||
|
public string StreamId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date added as Unix timestamp (index 23)
|
||||||
|
/// </summary>
|
||||||
|
public long DateAdded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date completed as Unix timestamp (index 24)
|
||||||
|
/// </summary>
|
||||||
|
public long DateCompleted { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// App update URL (index 25)
|
||||||
|
/// </summary>
|
||||||
|
public string AppUpdateUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save path (index 26)
|
||||||
|
/// </summary>
|
||||||
|
public string SavePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculated ratio value (RatioRaw / 1000.0)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double Ratio => RatioRaw / 1000.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Progress as percentage (0.0 to 1.0)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double ProgressPercent => Progress / 1000.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date completed as DateTime (or null if not completed)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime? DateCompletedDateTime =>
|
||||||
|
DateCompleted > 0 ? DateTimeOffset.FromUnixTimeSeconds(DateCompleted).DateTime : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeding time in seconds (calculated from DateCompleted to now)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public TimeSpan? SeedingTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (DateCompletedDateTime.HasValue)
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow - DateCompletedDateTime.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents torrent properties from µTorrent Web UI API getprops action
|
||||||
|
/// Based on the properties structure from the API documentation
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UTorrentProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Torrent hash
|
||||||
|
/// </summary>
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trackers list (newlines are represented by \r\n)
|
||||||
|
/// </summary>
|
||||||
|
public string Trackers { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> TrackerList => Trackers
|
||||||
|
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(x => x.Trim())
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload limit in bytes per second
|
||||||
|
/// </summary>
|
||||||
|
public int UploadLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download limit in bytes per second
|
||||||
|
/// </summary>
|
||||||
|
public int DownloadLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initial seeding / Super seeding
|
||||||
|
/// -1 = Not allowed, 0 = Disabled, 1 = Enabled
|
||||||
|
/// </summary>
|
||||||
|
public int SuperSeed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use DHT
|
||||||
|
/// -1 = Not allowed, 0 = Disabled, 1 = Enabled
|
||||||
|
/// </summary>
|
||||||
|
public int Dht { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use PEX (Peer Exchange)
|
||||||
|
/// -1 = Not allowed (indicates private torrent), 0 = Disabled, 1 = Enabled
|
||||||
|
/// </summary>
|
||||||
|
public int Pex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override queueing
|
||||||
|
/// -1 = Not allowed, 0 = Disabled, 1 = Enabled
|
||||||
|
/// </summary>
|
||||||
|
public int SeedOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed ratio in per mils (1000 = 1.0 ratio)
|
||||||
|
/// </summary>
|
||||||
|
public int SeedRatio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeding time in seconds
|
||||||
|
/// 0 = No minimum seeding time
|
||||||
|
/// </summary>
|
||||||
|
public int SeedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload slots
|
||||||
|
/// </summary>
|
||||||
|
public int UploadSlots { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this torrent is private (based on PEX value)
|
||||||
|
/// Private torrents have PEX = -1 (not allowed)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrivate => Pex == -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculated seed ratio value (SeedRatio / 1000.0)
|
||||||
|
/// </summary>
|
||||||
|
public double SeedRatioValue => SeedRatio / 1000.0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base response wrapper for µTorrent Web UI API calls
|
||||||
|
/// </summary>
|
||||||
|
public sealed record UTorrentResponse<T>
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "build")]
|
||||||
|
public int Build { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "label")]
|
||||||
|
public object[][]? Labels { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "torrents")]
|
||||||
|
public T? Torrents { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "torrentp")]
|
||||||
|
public object[]? TorrentProperties { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "files")]
|
||||||
|
public object[]? FilesDto { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<UTorrentFile>? Files
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (FilesDto is null || FilesDto.Length < 2)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = new List<UTorrentFile>();
|
||||||
|
|
||||||
|
if (FilesDto[1] is JArray jArray)
|
||||||
|
{
|
||||||
|
foreach (var jToken in jArray)
|
||||||
|
{
|
||||||
|
var fileTokenArray = (JArray)jToken;
|
||||||
|
var fileArray = fileTokenArray.ToObject<object[]>() ?? [];
|
||||||
|
files.Add(new UTorrentFile
|
||||||
|
{
|
||||||
|
Name = fileArray[0].ToString() ?? string.Empty,
|
||||||
|
Size = Convert.ToInt64(fileArray[1]),
|
||||||
|
Downloaded = Convert.ToInt64(fileArray[2]),
|
||||||
|
Priority = Convert.ToInt32(fileArray[3]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "props")]
|
||||||
|
public UTorrentProperties[]? Properties { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Domain.Entities.Whisparr;
|
||||||
|
|
||||||
|
public sealed record WhisparrCommand
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public long? SeriesId { get; set; }
|
||||||
|
|
||||||
|
public long? SeasonNumber { get; set; }
|
||||||
|
|
||||||
|
public List<long>? EpisodeIds { get; set; }
|
||||||
|
|
||||||
|
public SeriesSearchType SearchType { get; set; }
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ public enum DeleteReason
|
|||||||
AllFilesSkipped,
|
AllFilesSkipped,
|
||||||
AllFilesSkippedByQBit,
|
AllFilesSkippedByQBit,
|
||||||
AllFilesBlocked,
|
AllFilesBlocked,
|
||||||
|
MalwareFileFound,
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
public enum DownloadClientTypeName
|
public enum DownloadClientTypeName
|
||||||
{
|
{
|
||||||
QBittorrent,
|
qBittorrent,
|
||||||
Deluge,
|
Deluge,
|
||||||
Transmission,
|
Transmission,
|
||||||
}
|
uTorrent,
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
public enum NotificationEventType
|
||||||
|
{
|
||||||
|
Test,
|
||||||
|
FailedImportStrike,
|
||||||
|
StalledStrike,
|
||||||
|
SlowSpeedStrike,
|
||||||
|
SlowTimeStrike,
|
||||||
|
QueueItemDeleted,
|
||||||
|
DownloadCleaned,
|
||||||
|
CategoryChanged
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
public enum NotificationProviderType
|
||||||
|
{
|
||||||
|
Notifiarr,
|
||||||
|
Apprise,
|
||||||
|
Ntfy
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
public enum NtfyAuthenticationType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
BasicAuth,
|
||||||
|
AccessToken
|
||||||
|
}
|
||||||
10
code/backend/Cleanuparr.Domain/Enums/NtfyPriority.cs
Normal file
10
code/backend/Cleanuparr.Domain/Enums/NtfyPriority.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
|
public enum NtfyPriority
|
||||||
|
{
|
||||||
|
Min = 1,
|
||||||
|
Low = 2,
|
||||||
|
Default = 3,
|
||||||
|
High = 4,
|
||||||
|
Max = 5
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Cleanuparr.Domain.Enums;
|
namespace Cleanuparr.Domain.Enums;
|
||||||
|
|
||||||
public enum SonarrSearchType
|
public enum SeriesSearchType
|
||||||
{
|
{
|
||||||
Episode,
|
Episode,
|
||||||
Season,
|
Season,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Deluge.Exceptions;
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
public class DelugeClientException : Exception
|
public class DelugeClientException : Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Deluge.Exceptions;
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
public sealed class DelugeLoginException : DelugeClientException
|
public sealed class DelugeLoginException : DelugeClientException
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Data.Models.Deluge.Exceptions;
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
public sealed class DelugeLogoutException : DelugeClientException
|
public sealed class DelugeLogoutException : DelugeClientException
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when µTorrent authentication fails
|
||||||
|
/// </summary>
|
||||||
|
public class UTorrentAuthenticationException : UTorrentException
|
||||||
|
{
|
||||||
|
public UTorrentAuthenticationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UTorrentAuthenticationException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
|
public class UTorrentException : Exception
|
||||||
|
{
|
||||||
|
public UTorrentException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UTorrentException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Cleanuparr.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when µTorrent response parsing fails
|
||||||
|
/// </summary>
|
||||||
|
public class UTorrentParsingException : UTorrentException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The raw response that failed to parse
|
||||||
|
/// </summary>
|
||||||
|
public string RawResponse { get; }
|
||||||
|
|
||||||
|
public UTorrentParsingException(string message, string rawResponse) : base(message)
|
||||||
|
{
|
||||||
|
RawResponse = rawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UTorrentParsingException(string message, string rawResponse, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
RawResponse = rawResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||||
|
using Cleanuparr.Domain.Enums;
|
||||||
|
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||||
|
using Cleanuparr.Persistence.Models.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Moq.Protected;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Cleanuparr.Infrastructure.Tests.Verticals.DownloadClient;
|
||||||
|
|
||||||
|
public class UTorrentClientTests
|
||||||
|
{
|
||||||
|
private readonly UTorrentClient _client;
|
||||||
|
private readonly Mock<HttpMessageHandler> _mockHttpHandler;
|
||||||
|
private readonly DownloadClientConfig _config;
|
||||||
|
private readonly Mock<IUTorrentAuthenticator> _mockAuthenticator;
|
||||||
|
private readonly Mock<IUTorrentHttpService> _mockHttpService;
|
||||||
|
private readonly Mock<IUTorrentResponseParser> _mockResponseParser;
|
||||||
|
private readonly Mock<ILogger<UTorrentClient>> _mockLogger;
|
||||||
|
|
||||||
|
public UTorrentClientTests()
|
||||||
|
{
|
||||||
|
_mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
_mockAuthenticator = new Mock<IUTorrentAuthenticator>();
|
||||||
|
_mockHttpService = new Mock<IUTorrentHttpService>();
|
||||||
|
_mockResponseParser = new Mock<IUTorrentResponseParser>();
|
||||||
|
_mockLogger = new Mock<ILogger<UTorrentClient>>();
|
||||||
|
|
||||||
|
_config = new DownloadClientConfig
|
||||||
|
{
|
||||||
|
Name = "test",
|
||||||
|
Type = DownloadClientType.Torrent,
|
||||||
|
TypeName = DownloadClientTypeName.uTorrent,
|
||||||
|
Host = new Uri("http://localhost:8080"),
|
||||||
|
Username = "admin",
|
||||||
|
Password = "password"
|
||||||
|
};
|
||||||
|
|
||||||
|
_client = new UTorrentClient(
|
||||||
|
_config,
|
||||||
|
_mockAuthenticator.Object,
|
||||||
|
_mockHttpService.Object,
|
||||||
|
_mockResponseParser.Object,
|
||||||
|
_mockLogger.Object
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrentFilesAsync_ShouldDeserializeMixedArrayCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockResponse = new UTorrentResponse<object>
|
||||||
|
{
|
||||||
|
Build = 30470,
|
||||||
|
FilesDto = new object[]
|
||||||
|
{
|
||||||
|
"F0616FB199B78254474AF6D72705177E71D713ED", // Hash (string)
|
||||||
|
new object[] // File 1
|
||||||
|
{
|
||||||
|
"test name",
|
||||||
|
2604L,
|
||||||
|
0L,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
0
|
||||||
|
},
|
||||||
|
new object[] // File 2
|
||||||
|
{
|
||||||
|
"Dir1/Dir11/test11.zipx",
|
||||||
|
2604L,
|
||||||
|
0L,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
0
|
||||||
|
},
|
||||||
|
new object[] // File 3
|
||||||
|
{
|
||||||
|
"Dir1/sample.txt",
|
||||||
|
2604L,
|
||||||
|
0L,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the token request
|
||||||
|
var tokenResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("<div id='token'>test-token</div>")
|
||||||
|
};
|
||||||
|
tokenResponse.Headers.Add("Set-Cookie", "GUID=test-guid; path=/");
|
||||||
|
|
||||||
|
// Mock the files request
|
||||||
|
var filesResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(mockResponse))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock to return different responses based on URL
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("token.html")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(tokenResponse);
|
||||||
|
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("gui") && req.RequestUri.Query.Contains("action=getfiles")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(filesResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var files = await _client.GetTorrentFilesAsync("test-hash");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(files);
|
||||||
|
Assert.Equal(3, files.Count);
|
||||||
|
|
||||||
|
Assert.Equal("test name", files[0].Name);
|
||||||
|
Assert.Equal(2604L, files[0].Size);
|
||||||
|
Assert.Equal(0L, files[0].Downloaded);
|
||||||
|
Assert.Equal(2, files[0].Priority);
|
||||||
|
Assert.Equal(0, files[0].Index);
|
||||||
|
|
||||||
|
Assert.Equal("Dir1/Dir11/test11.zipx", files[1].Name);
|
||||||
|
Assert.Equal(2604L, files[1].Size);
|
||||||
|
Assert.Equal(0L, files[1].Downloaded);
|
||||||
|
Assert.Equal(2, files[1].Priority);
|
||||||
|
Assert.Equal(1, files[1].Index);
|
||||||
|
|
||||||
|
Assert.Equal("Dir1/sample.txt", files[2].Name);
|
||||||
|
Assert.Equal(2604L, files[2].Size);
|
||||||
|
Assert.Equal(0L, files[2].Downloaded);
|
||||||
|
Assert.Equal(2, files[2].Priority);
|
||||||
|
Assert.Equal(2, files[2].Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrentFilesAsync_ShouldHandleEmptyResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockResponse = new UTorrentResponse<object>
|
||||||
|
{
|
||||||
|
Build = 30470,
|
||||||
|
FilesDto = new object[]
|
||||||
|
{
|
||||||
|
"F0616FB199B78254474AF6D72705177E71D713ED" // Only hash, no files
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the token request
|
||||||
|
var tokenResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("<div id='token'>test-token</div>")
|
||||||
|
};
|
||||||
|
tokenResponse.Headers.Add("Set-Cookie", "GUID=test-guid; path=/");
|
||||||
|
|
||||||
|
// Mock the files request
|
||||||
|
var filesResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(mockResponse))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock to return different responses based on URL
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("token.html")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(tokenResponse);
|
||||||
|
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("gui") && req.RequestUri.Query.Contains("action=getfiles")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(filesResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var files = await _client.GetTorrentFilesAsync("test-hash");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(files);
|
||||||
|
Assert.Empty(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrentFilesAsync_ShouldHandleNullResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockResponse = new UTorrentResponse<object>
|
||||||
|
{
|
||||||
|
Build = 30470,
|
||||||
|
FilesDto = null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the token request
|
||||||
|
var tokenResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("<div id='token'>test-token</div>")
|
||||||
|
};
|
||||||
|
tokenResponse.Headers.Add("Set-Cookie", "GUID=test-guid; path=/");
|
||||||
|
|
||||||
|
// Mock the files request
|
||||||
|
var filesResponse = new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(mockResponse))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock to return different responses based on URL
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("token.html")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(tokenResponse);
|
||||||
|
|
||||||
|
_mockHttpHandler
|
||||||
|
.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||||
|
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.AbsolutePath.Contains("gui") && req.RequestUri.Query.Contains("action=getfiles")),
|
||||||
|
ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(filesResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var files = await _client.GetTorrentFilesAsync("test-hash");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(files);
|
||||||
|
Assert.Empty(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Cleanuparr.Infrastructure.Features.ContentBlocker;
|
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Tests.Verticals.ContentBlocker;
|
namespace Cleanuparr.Infrastructure.Tests.Verticals.MalwareBlocker;
|
||||||
|
|
||||||
public class FilenameEvaluatorFixture
|
public class FilenameEvaluatorFixture
|
||||||
{
|
{
|
||||||
@@ -4,7 +4,7 @@ using Cleanuparr.Domain.Enums;
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Tests.Verticals.ContentBlocker;
|
namespace Cleanuparr.Infrastructure.Tests.Verticals.MalwareBlocker;
|
||||||
|
|
||||||
public class FilenameEvaluatorTests : IClassFixture<FilenameEvaluatorFixture>
|
public class FilenameEvaluatorTests : IClassFixture<FilenameEvaluatorFixture>
|
||||||
{
|
{
|
||||||
@@ -7,20 +7,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Features\" />
|
<PackageReference Include="FLM.QBittorrent" Version="1.0.2" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.1" />
|
|
||||||
<PackageReference Include="FLM.Transmission" Version="1.0.3" />
|
<PackageReference Include="FLM.Transmission" Version="1.0.3" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
<PackageReference Include="MassTransit.Abstractions" Version="8.4.1" />
|
<PackageReference Include="MassTransit.Abstractions" Version="8.4.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||||
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
|
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
|
||||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||||
|
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ namespace Cleanuparr.Infrastructure.Events;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventCleanupService : BackgroundService
|
public class EventCleanupService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
private readonly ILogger<EventCleanupService> _logger;
|
private readonly ILogger<EventCleanupService> _logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(4); // Run every 4 hours
|
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(4); // Run every 4 hours
|
||||||
private readonly int _retentionDays = 30; // Keep events for 30 days
|
private readonly int _retentionDays = 30; // Keep events for 30 days
|
||||||
|
|
||||||
public EventCleanupService(IServiceProvider serviceProvider, ILogger<EventCleanupService> logger)
|
public EventCleanupService(ILogger<EventCleanupService> logger, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@@ -58,7 +58,7 @@ public class EventCleanupService : BackgroundService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var scope = _serviceProvider.CreateScope();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
var context = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
var context = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||||
|
|
||||||
var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays);
|
var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
using System.Dynamic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Enums;
|
||||||
using Cleanuparr.Infrastructure.Features.Context;
|
using Cleanuparr.Infrastructure.Features.Context;
|
||||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||||
using Cleanuparr.Infrastructure.Hubs;
|
using Cleanuparr.Infrastructure.Hubs;
|
||||||
|
using Cleanuparr.Infrastructure.Interceptors;
|
||||||
using Cleanuparr.Persistence;
|
using Cleanuparr.Persistence;
|
||||||
using Cleanuparr.Persistence.Models.Events;
|
using Cleanuparr.Persistence.Models.Events;
|
||||||
using Infrastructure.Interceptors;
|
|
||||||
using Infrastructure.Verticals.Notifications;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -47,7 +49,10 @@ public class EventPublisher
|
|||||||
EventType = eventType,
|
EventType = eventType,
|
||||||
Message = message,
|
Message = message,
|
||||||
Severity = severity,
|
Severity = severity,
|
||||||
Data = data != null ? JsonSerializer.Serialize(data) : null,
|
Data = data != null ? JsonSerializer.Serialize(data, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
}) : null,
|
||||||
TrackingId = trackingId
|
TrackingId = trackingId
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,14 +78,40 @@ public class EventPublisher
|
|||||||
StrikeType.FailedImport => EventType.FailedImportStrike,
|
StrikeType.FailedImport => EventType.FailedImportStrike,
|
||||||
StrikeType.SlowSpeed => EventType.SlowSpeedStrike,
|
StrikeType.SlowSpeed => EventType.SlowSpeedStrike,
|
||||||
StrikeType.SlowTime => EventType.SlowTimeStrike,
|
StrikeType.SlowTime => EventType.SlowTimeStrike,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(strikeType), strikeType, null)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dynamic data;
|
||||||
|
|
||||||
|
if (strikeType is StrikeType.FailedImport)
|
||||||
|
{
|
||||||
|
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||||
|
data = new
|
||||||
|
{
|
||||||
|
hash,
|
||||||
|
itemName,
|
||||||
|
strikeCount,
|
||||||
|
strikeType,
|
||||||
|
failedImportReasons = record.StatusMessages ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = new
|
||||||
|
{
|
||||||
|
hash,
|
||||||
|
itemName,
|
||||||
|
strikeCount,
|
||||||
|
strikeType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Publish the event
|
// Publish the event
|
||||||
await PublishAsync(
|
await PublishAsync(
|
||||||
eventType,
|
eventType,
|
||||||
$"Item '{itemName}' has been struck {strikeCount} times for reason '{strikeType}'",
|
$"Item '{itemName}' has been struck {strikeCount} times for reason '{strikeType}'",
|
||||||
EventSeverity.Important,
|
EventSeverity.Important,
|
||||||
data: new { hash, itemName, strikeCount, strikeType });
|
data: data);
|
||||||
|
|
||||||
// Send notification (uses ContextProvider internally)
|
// Send notification (uses ContextProvider internally)
|
||||||
await _notificationPublisher.NotifyStrike(strikeType, strikeCount);
|
await _notificationPublisher.NotifyStrike(strikeType, strikeCount);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||||
using Cleanuparr.Infrastructure.Services;
|
using Cleanuparr.Infrastructure.Services;
|
||||||
using Infrastructure.Services;
|
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Extensions;
|
namespace Cleanuparr.Infrastructure.Extensions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Cleanuparr.Infrastructure.Services;
|
using Cleanuparr.Infrastructure.Services;
|
||||||
using Infrastructure.Services;
|
|
||||||
using QBittorrent.Client;
|
using QBittorrent.Client;
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Extensions;
|
namespace Cleanuparr.Infrastructure.Extensions;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Cleanuparr.Infrastructure.Services;
|
using Cleanuparr.Infrastructure.Services;
|
||||||
using Infrastructure.Services;
|
|
||||||
using Transmission.API.RPC.Entity;
|
using Transmission.API.RPC.Entity;
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Extensions;
|
namespace Cleanuparr.Infrastructure.Extensions;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
using Cleanuparr.Domain.Enums;
|
using Cleanuparr.Domain.Enums;
|
||||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||||
using Cleanuparr.Infrastructure.Features.Context;
|
using Cleanuparr.Infrastructure.Features.Context;
|
||||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||||
|
using Cleanuparr.Infrastructure.Interceptors;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||||
using Cleanuparr.Shared.Helpers;
|
using Cleanuparr.Shared.Helpers;
|
||||||
using Data.Models.Arr;
|
using Data.Models.Arr;
|
||||||
using Data.Models.Arr.Queue;
|
|
||||||
using Infrastructure.Interceptors;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ public abstract class ArrClient : IArrClient
|
|||||||
return queueResponse;
|
return queueResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, ushort arrMaxStrikes)
|
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes)
|
||||||
{
|
{
|
||||||
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
||||||
|
|
||||||
@@ -105,6 +105,12 @@ public abstract class ArrClient : IArrClient
|
|||||||
|
|
||||||
ushort maxStrikes = arrMaxStrikes > 0 ? (ushort)arrMaxStrikes : queueCleanerConfig.FailedImport.MaxStrikes;
|
ushort maxStrikes = arrMaxStrikes > 0 ? (ushort)arrMaxStrikes : queueCleanerConfig.FailedImport.MaxStrikes;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Item {title} has failed import status with the following reason(s):\n{messages}",
|
||||||
|
record.Title,
|
||||||
|
string.Join("\n", record.StatusMessages?.Select(JsonConvert.SerializeObject) ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
return await _striker.StrikeAndCheckLimit(
|
return await _striker.StrikeAndCheckLimit(
|
||||||
record.DownloadId,
|
record.DownloadId,
|
||||||
record.Title,
|
record.Title,
|
||||||
@@ -206,7 +212,7 @@ public abstract class ArrClient : IArrClient
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasIgnoredPatterns(QueueRecord record)
|
private static bool HasIgnoredPatterns(QueueRecord record)
|
||||||
{
|
{
|
||||||
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,22 @@ public sealed class ArrClientFactory
|
|||||||
private readonly ISonarrClient _sonarrClient;
|
private readonly ISonarrClient _sonarrClient;
|
||||||
private readonly IRadarrClient _radarrClient;
|
private readonly IRadarrClient _radarrClient;
|
||||||
private readonly ILidarrClient _lidarrClient;
|
private readonly ILidarrClient _lidarrClient;
|
||||||
|
private readonly IReadarrClient _readarrClient;
|
||||||
|
private readonly IWhisparrClient _whisparrClient;
|
||||||
|
|
||||||
public ArrClientFactory(
|
public ArrClientFactory(
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
RadarrClient radarrClient,
|
RadarrClient radarrClient,
|
||||||
LidarrClient lidarrClient
|
LidarrClient lidarrClient,
|
||||||
|
ReadarrClient readarrClient,
|
||||||
|
WhisparrClient whisparrClient
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_sonarrClient = sonarrClient;
|
_sonarrClient = sonarrClient;
|
||||||
_radarrClient = radarrClient;
|
_radarrClient = radarrClient;
|
||||||
_lidarrClient = lidarrClient;
|
_lidarrClient = lidarrClient;
|
||||||
|
_readarrClient = readarrClient;
|
||||||
|
_whisparrClient = whisparrClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IArrClient GetClient(InstanceType type) =>
|
public IArrClient GetClient(InstanceType type) =>
|
||||||
@@ -26,6 +32,8 @@ public sealed class ArrClientFactory
|
|||||||
InstanceType.Sonarr => _sonarrClient,
|
InstanceType.Sonarr => _sonarrClient,
|
||||||
InstanceType.Radarr => _radarrClient,
|
InstanceType.Radarr => _radarrClient,
|
||||||
InstanceType.Lidarr => _lidarrClient,
|
InstanceType.Lidarr => _lidarrClient,
|
||||||
|
InstanceType.Readarr => _readarrClient,
|
||||||
|
InstanceType.Whisparr => _whisparrClient,
|
||||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||||
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||||
using Data.Models.Arr.Queue;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Cleanuparr.Infrastructure.Features.Arr;
|
namespace Cleanuparr.Infrastructure.Features.Arr;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user