Compare commits

..

4 Commits

Author SHA1 Message Date
Flaminel
6026f7f622 test 2025-06-17 11:23:01 +03:00
Flaminel
e95b50b7e4 test 2025-06-17 11:21:35 +03:00
Flaminel
b2a54c402a Update Program.cs 2025-06-17 11:19:37 +03:00
Flaminel
6dcb93a113 added trace log 2025-03-19 22:53:13 +02:00
1108 changed files with 8370 additions and 153831 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: Flaminel

View File

@@ -6,14 +6,7 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to improve Cleanuparr!
- type: checkboxes
id: duplicate-check
attributes:
label: "Duplicate check"
options:
- label: I have searched for existing issues and confirmed this is not a duplicate.
required: true
Thanks for taking the time to improve cleanuperr!
- type: checkboxes
id: init
attributes:
@@ -21,11 +14,11 @@ body:
options:
- label: Reviewed the documentation.
required: true
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
- label: Ensured I am using ghcr.io/flmorg/cleanuperr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true
- label: Enabled verbose logging.
- label: Enabled debug logging.
required: true
- type: textarea
id: what-happened

View File

@@ -6,26 +6,7 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to improve Cleanuparr!
- type: checkboxes
id: duplicate-check
attributes:
label: "Duplicate check"
options:
- label: I have searched for existing issues and confirmed this is not a duplicate.
required: true
- type: checkboxes
id: init
attributes:
label: Implementation & testing support
description: The requester should help answer questions, provide support for the implementation and test changes.
options:
- label: I understand I must be available to assist with implementation questions and to test the feature before being released.
required: true
- label: I understand that joining the Discord server may be necessary for better coordination and faster communication.
required: true
- label: I understand that failure to assist in the development process of my request will result in the request being closed.
required: true
Thanks for taking the time to improve cleanuperr!
- type: textarea
id: description
attributes:

View File

@@ -7,13 +7,6 @@ body:
attributes:
value: |
If you are experiencing unexpected behavior, please consider submitting a bug report instead.
- type: checkboxes
id: duplicate-check
attributes:
label: "Duplicate check"
options:
- label: I have searched for existing issues and confirmed this is not a duplicate.
required: true
- type: checkboxes
id: init
attributes:
@@ -21,11 +14,11 @@ body:
options:
- label: Reviewed the documentation.
required: true
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
- label: Ensured I am using ghcr.io/flmorg/cleanuperr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true
- label: Enabled verbose logging.
- label: Enabled debug logging.
required: true
- type: textarea
id: description

View File

@@ -1,8 +1,2 @@
blank_issues_enabled: false
contact_links:
- name: Discord Community
url: https://discord.gg/SCtMCgtsc4
about: Join our Discord for real-time help and discussions
- name: Documentation
url: https://cleanuparr.github.io/Cleanuparr/
about: Check the documentation for configurations and usage guidelines
contact_links: []

View File

@@ -1,24 +0,0 @@
## Description
<!-- Brief description of what this PR does -->
## Related Issue
Closes #ISSUE_NUMBER
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
<!-- Describe how you tested your changes -->
## Screenshots (if applicable)
<!-- Add screenshots here -->
## Checklist
- [ ] I have read the [Contributing Guide](../CONTRIBUTING.md)
- [ ] I have announced my intent to work on this and received approval
- [ ] My code follows the project's code standards
- [ ] I have tested my changes thoroughly
- [ ] I have updated relevant documentation

View File

@@ -1,30 +0,0 @@
name: 'Get Vault Secrets'
description: 'Retrieves secrets from HashiCorp Vault using AppRole authentication'
inputs:
vault_host:
description: 'Vault server URL'
required: true
vault_role_id:
description: 'Vault AppRole Role ID'
required: true
vault_secret_id:
description: 'Vault AppRole Secret ID'
required: true
secrets:
description: 'Secrets to retrieve (multiline string, one per line in format: path | output_name)'
required: true
default: |
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT
secrets/data/github packages_pat | PACKAGES_PAT
runs:
using: "composite"
steps:
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ inputs.vault_host }}
method: approle
roleId: ${{ inputs.vault_role_id }}
secretId: ${{ inputs.vault_secret_id }}
secrets: ${{ inputs.secrets }}

View File

@@ -1,166 +0,0 @@
name: Build Docker Images
on:
pull_request:
paths:
- 'code/**'
workflow_call:
inputs:
push_docker:
description: 'Push Docker image to registry'
type: boolean
required: false
default: true
app_version:
description: 'Application version'
type: string
required: false
default: ''
# Cancel in-progress runs for the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build_app:
runs-on: ubuntu-latest
steps:
- name: Set github context
timeout-minutes: 1
run: |
echo 'githubRepository=${{ github.repository }}' >> $GITHUB_ENV
echo 'githubSha=${{ github.sha }}' >> $GITHUB_ENV
echo 'githubRef=${{ github.ref }}' >> $GITHUB_ENV
echo 'githubHeadRef=${{ github.head_ref }}' >> $GITHUB_ENV
- name: Initialize build info
timeout-minutes: 1
run: |
githubHeadRef=${{ env.githubHeadRef }}
inputVersion="${{ inputs.app_version }}"
latestDockerTag=""
versionDockerTag=""
majorVersionDockerTag=""
minorVersionDockerTag=""
version="0.0.1"
if [[ -n "$inputVersion" ]]; then
# Version provided via input (manual release)
branch="main"
latestDockerTag="latest"
versionDockerTag="$inputVersion"
version="$inputVersion"
# 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
elif [[ "$githubRef" =~ ^"refs/tags/" ]]; then
# Tag push
branch=${githubRef##*/}
latestDockerTag="latest"
versionDockerTag=${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
# Determine if this run is for the main branch or another branch
if [[ -z "$githubHeadRef" ]]; then
# Main branch
githubRef=${{ env.githubRef }}
branch=${githubRef##*/}
versionDockerTag="$branch"
else
# Pull request
branch=$githubHeadRef
versionDockerTag="$branch"
fi
fi
githubTags=""
if [ -n "$latestDockerTag" ]; then
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$latestDockerTag"
fi
if [ -n "$versionDockerTag" ]; then
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
# set env vars
echo "branch=$branch" >> $GITHUB_ENV
echo "githubTags=$githubTags" >> $GITHUB_ENV
echo "versionDockerTag=$versionDockerTag" >> $GITHUB_ENV
echo "version=$version" >> $GITHUB_ENV
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/docker username | DOCKER_USERNAME;
secrets/data/docker password | DOCKER_PASSWORD;
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT;
secrets/data/github packages_pat | PACKAGES_PAT
- name: Checkout target repository
uses: actions/checkout@v4
timeout-minutes: 1
with:
repository: ${{ env.githubRepository }}
ref: ${{ env.branch }}
token: ${{ env.REPO_READONLY_PAT }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
timeout-minutes: 5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push docker image
id: docker-build
timeout-minutes: 15
uses: docker/build-push-action@v6
with:
context: ${{ github.workspace }}/code
file: ${{ github.workspace }}/code/Dockerfile
provenance: false
labels: |
commit=sha-${{ env.githubSha }}
version=${{ env.versionDockerTag }}
build-args: |
VERSION=${{ env.version }}
PACKAGES_USERNAME=${{ secrets.PACKAGES_USERNAME }}
PACKAGES_PAT=${{ env.PACKAGES_PAT }}
platforms: |
linux/amd64
linux/arm64
push: ${{ github.event_name == 'pull_request' || inputs.push_docker == true }}
tags: |
${{ env.githubTags }}
# Enable BuildKit cache for faster builds
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,126 +0,0 @@
name: Build Executables
on:
workflow_call:
inputs:
app_version:
description: 'Application version'
type: string
required: false
default: ''
jobs:
# Build for each platform in parallel using matrix strategy
build-platform:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
include:
- runtime: win-x64
platform: win-amd64
- runtime: linux-x64
platform: linux-amd64
- runtime: linux-arm64
platform: linux-arm64
- runtime: osx-x64
platform: osx-amd64
- runtime: osx-arm64
platform: osx-arm64
steps:
- name: Set variables
run: |
ref=${{ github.ref }}
# Use input version if provided, otherwise determine from ref
if [[ -n "${{ inputs.app_version }}" ]]; then
appVersion="${{ inputs.app_version }}"
releaseVersion="v$appVersion"
elif [[ "$ref" =~ ^refs/tags/ ]]; then
releaseVersion=${ref##refs/tags/}
appVersion=${releaseVersion#v}
else
releaseVersion="dev-$(date +%Y%m%d-%H%M%S)"
appVersion="0.0.1-dev"
fi
repoFullName=${{ github.repository }}
repositoryName=${repoFullName#*/}
echo "githubRepository=${{ github.repository }}" >> $GITHUB_ENV
echo "githubRepositoryName=$repositoryName" >> $GITHUB_ENV
echo "releaseVersion=$releaseVersion" >> $GITHUB_ENV
echo "appVersion=$appVersion" >> $GITHUB_ENV
echo "executableName=Cleanuparr.Api" >> $GITHUB_ENV
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT;
secrets/data/github packages_pat | PACKAGES_PAT
- name: Checkout target repository
uses: actions/checkout@v4
timeout-minutes: 1
with:
repository: ${{ env.githubRepository }}
ref: ${{ github.ref_name }}
token: ${{ env.REPO_READONLY_PAT }}
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: frontend-dist
path: code/frontend/dist/ui/browser
- name: Install dependencies and restore
run: |
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ env.PACKAGES_PAT }} --store-password-in-clear-text --name Cleanuparr https://nuget.pkg.github.com/Cleanuparr/index.json
dotnet restore code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj
- name: Copy frontend to backend wwwroot
run: |
mkdir -p code/backend/${{ env.executableName }}/wwwroot
cp -r code/frontend/dist/ui/browser/* code/backend/${{ env.executableName }}/wwwroot/
- name: Build ${{ matrix.platform }}
run: |
dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj \
-c Release \
--runtime ${{ matrix.runtime }} \
--self-contained \
-o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-${{ matrix.platform }} \
/p:PublishSingleFile=true \
/p:Version=${{ env.appVersion }} \
/p:DebugSymbols=false
- name: Zip artifact
run: |
cd ./artifacts
zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-${{ matrix.platform }}.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-${{ matrix.platform }}/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: executable-${{ matrix.platform }}
path: ./artifacts/*.zip
retention-days: 30

View File

@@ -1,46 +0,0 @@
name: Build Frontend
on:
workflow_call:
jobs:
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT
- name: Checkout repository
uses: actions/checkout@v4
timeout-minutes: 1
with:
repository: ${{ github.repository }}
ref: ${{ github.ref_name }}
token: ${{ env.REPO_READONLY_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: code/frontend/package-lock.json
- name: Build frontend
run: |
cd code/frontend
npm ci
npm run build
- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: code/frontend/dist/ui/browser
retention-days: 1

View File

@@ -1,376 +0,0 @@
name: Build macOS Installers
permissions:
contents: write
on:
workflow_call:
inputs:
app_version:
description: 'Application version'
type: string
required: false
default: ''
jobs:
build-macos-installer:
name: Build macOS ${{ matrix.arch }} Installer
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: Intel
runner: macos-13
runtime: osx-x64
min_os_version: "10.15"
artifact_suffix: intel
- arch: ARM
runner: macos-14
runtime: osx-arm64
min_os_version: "11.0"
artifact_suffix: arm64
steps:
- name: Set variables
run: |
repoFullName=${{ github.repository }}
ref=${{ github.ref }}
# Use input version if provided, otherwise determine from ref
if [[ -n "${{ inputs.app_version }}" ]]; then
appVersion="${{ inputs.app_version }}"
releaseVersion="v$appVersion"
elif [[ "$ref" =~ ^refs/tags/ ]]; then
releaseVersion=${ref##refs/tags/}
appVersion=${releaseVersion#v}
else
# For manual dispatch, use a default version
releaseVersion="dev-$(date +%Y%m%d-%H%M%S)"
appVersion="0.0.1-dev"
fi
repositoryName=${repoFullName#*/}
echo "githubRepository=${{ github.repository }}" >> $GITHUB_ENV
echo "githubRepositoryName=$repositoryName" >> $GITHUB_ENV
echo "releaseVersion=$releaseVersion" >> $GITHUB_ENV
echo "appVersion=$appVersion" >> $GITHUB_ENV
echo "executableName=Cleanuparr.Api" >> $GITHUB_ENV
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT;
secrets/data/github packages_pat | PACKAGES_PAT
- name: Checkout repository
uses: actions/checkout@v4
with:
repository: ${{ env.githubRepository }}
ref: ${{ github.ref_name }}
token: ${{ env.REPO_READONLY_PAT }}
fetch-depth: 0
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: frontend-dist
path: code/frontend/dist/ui/browser
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore .NET dependencies
run: |
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ env.PACKAGES_PAT }} --store-password-in-clear-text --name Cleanuparr https://nuget.pkg.github.com/Cleanuparr/index.json
dotnet restore code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj
- name: Build macOS ${{ matrix.arch }} executable
run: |
# Clean any existing output directory
rm -rf dist
mkdir -p dist/temp
# Build to a temporary location
dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj \
-c Release \
--runtime ${{ matrix.runtime }} \
--self-contained true \
-o dist/temp \
/p:PublishSingleFile=true \
/p:Version=${{ env.appVersion }} \
/p:DebugType=None \
/p:DebugSymbols=false \
/p:UseAppHost=true \
/p:EnableMacOSCodeSign=false \
/p:CodeSignOnCopy=false \
/p:_CodeSignDuringBuild=false \
/p:PublishTrimmed=false \
/p:TrimMode=link
# Create proper app bundle structure
mkdir -p dist/Cleanuparr.app/Contents/MacOS
# Copy the built executable (note: AssemblyName is "Cleanuparr" not "Cleanuparr.Api")
cp dist/temp/Cleanuparr dist/Cleanuparr.app/Contents/MacOS/Cleanuparr
# Copy frontend directly to where it belongs in the app bundle
mkdir -p dist/Cleanuparr.app/Contents/MacOS/wwwroot
cp -r code/frontend/dist/ui/browser/* dist/Cleanuparr.app/Contents/MacOS/wwwroot/
# Copy any additional runtime files if they exist
if [ -d "dist/temp" ]; then
find dist/temp -name "*.dylib" -exec cp {} dist/Cleanuparr.app/Contents/MacOS/ \; 2>/dev/null || true
find dist/temp -name "createdump" -exec cp {} dist/Cleanuparr.app/Contents/MacOS/ \; 2>/dev/null || true
fi
- name: Post-build setup
run: |
# Make sure the executable is actually executable
chmod +x dist/Cleanuparr.app/Contents/MacOS/Cleanuparr
# Remove any .pdb files that might have been created
find dist/Cleanuparr.app/Contents/MacOS -name "*.pdb" -delete 2>/dev/null || true
echo "Checking architecture of built binary:"
file dist/Cleanuparr.app/Contents/MacOS/Cleanuparr
if command -v lipo >/dev/null 2>&1; then
lipo -info dist/Cleanuparr.app/Contents/MacOS/Cleanuparr
fi
echo "Files in MacOS directory:"
ls -la dist/Cleanuparr.app/Contents/MacOS/
- name: Create macOS app bundle structure
run: |
# Create proper app bundle structure
mkdir -p dist/Cleanuparr.app/Contents/{MacOS,Resources,Frameworks}
# Convert ICO to ICNS for macOS app bundle
if command -v iconutil >/dev/null 2>&1; then
# Create iconset directory structure
mkdir -p Cleanuparr.iconset
# Use existing PNG files from Logo directory for different sizes
cp Logo/16.png Cleanuparr.iconset/icon_16x16.png
cp Logo/32.png Cleanuparr.iconset/icon_16x16@2x.png
cp Logo/32.png Cleanuparr.iconset/icon_32x32.png
cp Logo/64.png Cleanuparr.iconset/icon_32x32@2x.png
cp Logo/128.png Cleanuparr.iconset/icon_128x128.png
cp Logo/256.png Cleanuparr.iconset/icon_128x128@2x.png
cp Logo/256.png Cleanuparr.iconset/icon_256x256.png
cp Logo/512.png Cleanuparr.iconset/icon_256x256@2x.png
cp Logo/512.png Cleanuparr.iconset/icon_512x512.png
cp Logo/1024.png Cleanuparr.iconset/icon_512x512@2x.png
# Create ICNS file
iconutil -c icns Cleanuparr.iconset -o dist/Cleanuparr.app/Contents/Resources/Cleanuparr.icns
# Clean up iconset directory
rm -rf Cleanuparr.iconset
fi
# Create Launch Daemon plist
cat > dist/Cleanuparr.app/Contents/Resources/com.cleanuparr.daemon.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cleanuparr.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Cleanuparr.app/Contents/MacOS/Cleanuparr</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/cleanuparr.log</string>
<key>StandardErrorPath</key>
<string>/var/log/cleanuparr.error.log</string>
<key>WorkingDirectory</key>
<string>/Applications/Cleanuparr.app/Contents/MacOS</string>
<key>EnvironmentVariables</key>
<dict>
<key>HTTP_PORTS</key>
<string>11011</string>
</dict>
</dict>
</plist>
EOF
# Create Info.plist with proper configuration
cat > dist/Cleanuparr.app/Contents/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>Cleanuparr</string>
<key>CFBundleIdentifier</key>
<string>com.Cleanuparr</string>
<key>CFBundleName</key>
<string>Cleanuparr</string>
<key>CFBundleDisplayName</key>
<string>Cleanuparr</string>
<key>CFBundleVersion</key>
<string>${{ env.appVersion }}</string>
<key>CFBundleShortVersionString</key>
<string>${{ env.appVersion }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>CLNR</string>
<key>CFBundleIconFile</key>
<string>Cleanuparr</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>${{ matrix.min_os_version }}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>NSSupportsAutomaticTermination</key>
<false/>
<key>NSSupportsSuddenTermination</key>
<false/>
<key>LSBackgroundOnly</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
EOF
# Clean up temp directory
rm -rf dist/temp
- name: Create PKG installer
run: |
# Create preinstall script to handle existing installations
mkdir -p scripts
cat > scripts/preinstall << 'EOF'
#!/bin/bash
# Stop and unload existing launch daemon if it exists
if launchctl list | grep -q "com.cleanuparr.daemon"; then
launchctl stop com.cleanuparr.daemon 2>/dev/null || true
launchctl unload /Library/LaunchDaemons/com.cleanuparr.daemon.plist 2>/dev/null || true
fi
# Stop any running instances of Cleanuparr
pkill -f "Cleanuparr" || true
sleep 2
# Remove old installation if it exists
if [[ -d "/Applications/Cleanuparr.app" ]]; then
rm -rf "/Applications/Cleanuparr.app"
fi
# Remove old launch daemon plist if it exists
if [[ -f "/Library/LaunchDaemons/com.cleanuparr.daemon.plist" ]]; then
rm -f "/Library/LaunchDaemons/com.cleanuparr.daemon.plist"
fi
exit 0
EOF
chmod +x scripts/preinstall
# Create postinstall script
cat > scripts/postinstall << 'EOF'
#!/bin/bash
# Set proper permissions for the app bundle
chmod -R 755 /Applications/Cleanuparr.app
chmod +x /Applications/Cleanuparr.app/Contents/MacOS/Cleanuparr
# Install the launch daemon
cp /Applications/Cleanuparr.app/Contents/Resources/com.cleanuparr.daemon.plist /Library/LaunchDaemons/
chown root:wheel /Library/LaunchDaemons/com.cleanuparr.daemon.plist
chmod 644 /Library/LaunchDaemons/com.cleanuparr.daemon.plist
# Load and start the service
launchctl load /Library/LaunchDaemons/com.cleanuparr.daemon.plist
launchctl start com.cleanuparr.daemon
# Wait a moment for service to start
sleep 3
# Display as system notification
osascript -e 'display notification "Cleanuparr service started! Visit http://localhost:11011 in your browser." with title "Installation Complete"' 2>/dev/null || true
exit 0
EOF
chmod +x scripts/postinstall
# Create uninstall script (optional, for user reference)
cat > scripts/uninstall_cleanuparr.sh << 'EOF'
#!/bin/bash
# Cleanuparr Uninstall Script
# Run this script with sudo to completely remove Cleanuparr
echo "Stopping Cleanuparr service..."
launchctl stop com.cleanuparr.daemon 2>/dev/null || true
launchctl unload /Library/LaunchDaemons/com.cleanuparr.daemon.plist 2>/dev/null || true
echo "Removing service files..."
rm -f /Library/LaunchDaemons/com.cleanuparr.daemon.plist
echo "Removing application..."
rm -rf /Applications/Cleanuparr.app
echo "Removing logs..."
rm -f /var/log/cleanuparr.log
rm -f /var/log/cleanuparr.error.log
echo "Cleanuparr has been completely removed."
echo "Note: Configuration files in /Applications/Cleanuparr.app/Contents/MacOS/config/ have been removed with the app."
EOF
chmod +x scripts/uninstall_cleanuparr.sh
# Copy uninstall script to app bundle for user access
cp scripts/uninstall_cleanuparr.sh dist/Cleanuparr.app/Contents/Resources/
# Determine package name - if app_version input was provided, it's a release build
if [[ -n "${{ inputs.app_version }}" ]] || [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
pkg_name="Cleanuparr-${{ env.appVersion }}-macos-${{ matrix.artifact_suffix }}.pkg"
else
pkg_name="Cleanuparr-${{ env.appVersion }}-macos-${{ matrix.artifact_suffix }}-dev.pkg"
fi
# Create PKG installer with better metadata
pkgbuild --root dist/ \
--scripts scripts/ \
--identifier com.Cleanuparr \
--version ${{ env.appVersion }} \
--install-location /Applications \
--ownership preserve \
${pkg_name}
echo "pkgName=${pkg_name}" >> $GITHUB_ENV
- name: Upload installer as artifact
uses: actions/upload-artifact@v4
with:
name: Cleanuparr-macos-${{ matrix.artifact_suffix }}-installer
path: '${{ env.pkgName }}'
retention-days: 30

View File

@@ -1,147 +0,0 @@
name: Build Windows Installer
on:
workflow_call:
inputs:
app_version:
description: 'Application version'
type: string
required: false
default: ''
jobs:
build-windows-installer:
runs-on: windows-latest
steps:
- name: Set variables
shell: pwsh
run: |
$repoFullName = "${{ github.repository }}"
$ref = "${{ github.ref }}"
$inputVersion = "${{ inputs.app_version }}"
# Use input version if provided, otherwise determine from ref
if ($inputVersion -ne "") {
$appVersion = $inputVersion
$releaseVersion = "v$appVersion"
} elseif ($ref -match "^refs/tags/") {
$releaseVersion = $ref -replace "refs/tags/", ""
$appVersion = $releaseVersion -replace "^v", ""
} else {
# For manual dispatch, use a default version
$releaseVersion = "dev-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
$appVersion = "0.0.1-dev"
}
$repositoryName = $repoFullName.Split("/")[1]
echo "githubRepository=${{ github.repository }}" >> $env:GITHUB_ENV
echo "githubRepositoryName=$repositoryName" >> $env:GITHUB_ENV
echo "releaseVersion=$releaseVersion" >> $env:GITHUB_ENV
echo "appVersion=$appVersion" >> $env:GITHUB_ENV
echo "APP_VERSION=$appVersion" >> $env:GITHUB_ENV
echo "executableName=Cleanuparr.Api" >> $env:GITHUB_ENV
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT;
secrets/data/github packages_pat | PACKAGES_PAT
- name: Checkout repository
uses: actions/checkout@v4
with:
repository: ${{ env.githubRepository }}
ref: ${{ github.ref_name }}
token: ${{ env.REPO_READONLY_PAT }}
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: frontend-dist
path: code/frontend/dist/ui/browser
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Restore .NET dependencies
run: |
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ env.PACKAGES_PAT }} --store-password-in-clear-text --name Cleanuparr https://nuget.pkg.github.com/Cleanuparr/index.json
dotnet restore code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj
- name: Copy frontend to backend wwwroot
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "code/backend/${{ env.executableName }}/wwwroot"
Copy-Item -Path "code/frontend/dist/ui/browser/*" -Destination "code/backend/${{ env.executableName }}/wwwroot/" -Recurse -Force
- name: Build Windows executable
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
- name: Setup Inno Setup
shell: pwsh
run: |
# Download and install Inno Setup
$url = "https://jrsoftware.org/download.php/is.exe"
$output = "innosetup-installer.exe"
Invoke-WebRequest -Uri $url -OutFile $output
Start-Process -FilePath $output -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART" -Wait
# Add Inno Setup to PATH
$innoPath = "C:\Program Files (x86)\Inno Setup 6"
echo "$innoPath" >> $env:GITHUB_PATH
- name: Verify LICENSE file exists
shell: pwsh
run: |
if (-not (Test-Path "LICENSE")) {
Write-Error "LICENSE file not found in repository root"
exit 1
}
Write-Host "LICENSE file found successfully"
- name: Build Windows installer
shell: pwsh
run: |
# Copy installer script to root
Copy-Item "installers/windows/cleanuparr-installer.iss" -Destination "cleanuparr-installer.iss"
# The installer script has been pre-updated with proper icon and config paths
# No dynamic modifications needed as the base script now includes correct references
# Run Inno Setup compiler
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "cleanuparr-installer.iss"
# Check if installer was created
if (Test-Path "installer/Cleanuparr_Setup.exe") {
Write-Host "Installer created successfully"
} else {
Write-Error "Installer creation failed"
exit 1
}
- name: Rename installer with version
shell: pwsh
run: |
$installerName = "Cleanuparr-${{ env.appVersion }}-Setup.exe"
Move-Item "installer/Cleanuparr_Setup.exe" "installer/$installerName"
echo "installerName=$installerName" >> $env:GITHUB_ENV
- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: Cleanuparr-windows-installer
path: installer/${{ env.installerName }}
retention-days: 30
# Removed individual release step - handled by main release workflow

12
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
on:
workflow_dispatch:
workflow_call:
jobs:
build:
uses: flmorg/universal-workflows/.github/workflows/dotnet.build.app.yml@main
with:
dockerRepository: flaminel/cleanuperr
githubContext: ${{ toJSON(github) }}
outputName: cleanuperr
secrets: inherit

View File

@@ -1,36 +0,0 @@
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

View File

@@ -1,30 +0,0 @@
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

View File

@@ -1,45 +0,0 @@
name: Dependency Review
on:
pull_request:
branches:
- main
# Cancel in-progress runs for the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
# Fail on critical and high severity vulnerabilities
fail-on-severity: high
# Warn on moderate vulnerabilities
warn-on-severity: moderate
# Allow licenses
# allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD
# Comment summarizes the vulnerabilities found
comment-summary-in-pr: on-failure
# Show dependency changes in PR
show-openssf-scorecard: true
vulnerability-check: true
- name: Upload dependency review results
uses: actions/upload-artifact@v4
with:
name: dependency-review-results
path: dependency-review-*.json
if-no-files-found: ignore
retention-days: 30

19
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
on:
workflow_call:
workflow_dispatch:
push:
paths:
- 'chart/**'
branches: [ main ]
jobs:
deploy:
uses: flmorg/universal-workflows/.github/workflows/chart.install.yml@main
with:
githubContext: ${{ toJSON(github) }}
chartRepo: oci://ghcr.io/flmorg
chartName: universal-chart
version: ^1.0.0
valuesPath: chart/values.yaml
releaseName: main
secrets: inherit

View File

@@ -1,50 +0,0 @@
name: Deploy Docusaurus to GitHub Pages
on:
push:
tags:
- "v*.*.*"
workflow_dispatch: {}
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: yarn
cache-dependency-path: docs/yarn.lock
- name: Install dependencies
working-directory: docs
run: yarn install --frozen-lockfile
- name: Build Docusaurus
working-directory: docs
run: yarn build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/build
retention-days: 1
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

20
.github/workflows/pipeline.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
on:
push:
tags:
- "v*.*.*"
# paths:
# - 'code/**'
# branches: [ main ]
pull_request:
paths:
- 'code/**'
jobs:
build:
uses: flmorg/cleanuperr/.github/workflows/build.yml@main
secrets: inherit
# deploy:
# needs: [ build ]
# uses: flmorg/cleanuperr/.github/workflows/deploy.yml@main
# secrets: inherit

View File

@@ -1,287 +1,11 @@
name: Release Build
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 1.0.0)'
required: true
runTests:
description: 'Run test suite'
type: boolean
required: false
default: true
buildDocker:
description: 'Build Docker image'
type: boolean
required: false
default: true
pushDocker:
description: 'Push Docker image to registry'
type: boolean
required: false
default: false
buildBinaries:
description: 'Build executables and installers'
type: boolean
required: false
default: true
createRelease:
description: 'Create GitHub release'
type: boolean
required: false
default: false
jobs:
# Validate release
validate:
runs-on: ubuntu-latest
outputs:
app_version: ${{ steps.version.outputs.app_version }}
release_version: ${{ steps.version.outputs.release_version }}
is_tag: ${{ steps.version.outputs.is_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version info
id: version
run: |
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag event
release_version=${GITHUB_REF##refs/tags/}
app_version=${release_version#v}
is_tag=true
else
# Manual workflow with version
app_version="${{ github.event.inputs.version }}"
# Validate version format (x.x.x)
if ! [[ "$app_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format x.x.x (e.g., 1.0.0)"
echo "Provided version: $app_version"
exit 1
fi
release_version="v$app_version"
is_tag=false
fi
echo "app_version=$app_version" >> $GITHUB_OUTPUT
echo "release_version=$release_version" >> $GITHUB_OUTPUT
echo "is_tag=$is_tag" >> $GITHUB_OUTPUT
echo "Release Version: $release_version"
echo "App Version: $app_version"
echo "Is Tag: $is_tag"
- name: Check if release already exists
run: |
if gh release view "${{ steps.version.outputs.release_version }}" &>/dev/null; then
echo "❌ Release ${{ steps.version.outputs.release_version }} already exists. Stopping workflow."
exit 1
fi
echo "✅ Release ${{ steps.version.outputs.release_version }} does not exist. Proceeding."
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Run tests
test:
needs: validate
if: ${{ needs.validate.outputs.is_tag == 'true' || github.event.inputs.runTests == 'true' }}
uses: ./.github/workflows/test.yml
secrets: inherit
# Build frontend once for all build jobs and cache it
build-frontend:
needs: [validate, test]
if: |
always() &&
needs.validate.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.validate.outputs.is_tag == 'true' || github.event.inputs.buildBinaries == 'true')
uses: ./.github/workflows/build-frontend.yml
secrets: inherit
# Build portable executables
build-executables:
needs: [validate, test, build-frontend]
if: |
always() &&
needs.validate.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
needs.build-frontend.result == 'success' &&
(needs.validate.outputs.is_tag == 'true' || github.event.inputs.buildBinaries == 'true')
uses: ./.github/workflows/build-executable.yml
release:
uses: flmorg/universal-workflows/.github/workflows/dotnet.release.yml@main
with:
app_version: ${{ needs.validate.outputs.app_version }}
secrets: inherit
# Build Windows installer
build-windows-installer:
needs: [validate, test, build-frontend]
if: |
always() &&
needs.validate.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
needs.build-frontend.result == 'success' &&
(needs.validate.outputs.is_tag == 'true' || github.event.inputs.buildBinaries == 'true')
uses: ./.github/workflows/build-windows-installer.yml
with:
app_version: ${{ needs.validate.outputs.app_version }}
secrets: inherit
# Build macOS installers (Intel and ARM)
build-macos:
needs: [validate, test, build-frontend]
if: |
always() &&
needs.validate.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
needs.build-frontend.result == 'success' &&
(needs.validate.outputs.is_tag == 'true' || github.event.inputs.buildBinaries == 'true')
uses: ./.github/workflows/build-macos-installer.yml
with:
app_version: ${{ needs.validate.outputs.app_version }}
secrets: inherit
# Build and push Docker image(s)
build-docker:
needs: [validate, test]
if: |
always() &&
needs.validate.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.validate.outputs.is_tag == 'true' || github.event.inputs.buildDocker == 'true')
uses: ./.github/workflows/build-docker.yml
with:
push_docker: ${{ needs.validate.outputs.is_tag == 'true' || github.event.inputs.pushDocker == 'true' }}
app_version: ${{ needs.validate.outputs.app_version }}
secrets: inherit
# Create GitHub release
create-release:
needs: [validate, build-executables, build-windows-installer, build-macos]
runs-on: ubuntu-latest
if: |
always() &&
needs.validate.result == 'success' &&
needs.build-executables.result == 'success' &&
needs.build-windows-installer.result == 'success' &&
needs.build-macos.result == 'success' &&
(
needs.validate.outputs.is_tag == 'true' ||
(github.event.inputs.createRelease == 'true' && github.event.inputs.buildBinaries == 'true')
)
steps:
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github repo_readonly_pat | REPO_READONLY_PAT
- name: Download executable artifacts
uses: actions/download-artifact@v4
with:
pattern: executable-*
path: ./artifacts
merge-multiple: true
- name: Download Windows installer
uses: actions/download-artifact@v4
with:
name: Cleanuparr-windows-installer
path: ./artifacts
- name: Download macOS installers
uses: actions/download-artifact@v4
with:
pattern: Cleanuparr-macos-*-installer
path: ./artifacts
merge-multiple: true
- name: List downloaded artifacts
run: |
echo "Downloaded artifacts:"
find ./artifacts -type f \( -name "*.zip" -o -name "*.pkg" -o -name "*.exe" \) | sort
echo ""
echo "Total files: $(find ./artifacts -type f \( -name "*.zip" -o -name "*.pkg" -o -name "*.exe" \) | wc -l)"
- name: Create release
uses: softprops/action-gh-release@v2
with:
name: ${{ needs.validate.outputs.release_version }}
tag_name: ${{ needs.validate.outputs.release_version }}
token: ${{ env.REPO_READONLY_PAT }}
make_latest: true
target_commitish: main
generate_release_notes: true
files: |
./artifacts/*.zip
./artifacts/*.pkg
./artifacts/*.exe
# Summary job
summary:
needs: [validate, test, build-frontend, build-executables, build-windows-installer, build-macos, build-docker]
runs-on: ubuntu-latest
if: always()
steps:
- name: Record workflow start time
id: workflow-start
run: |
# Get workflow start time from GitHub API
workflow_start=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }} --jq '.run_started_at')
start_epoch=$(date -d "$workflow_start" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$workflow_start" +%s)
echo "start=$start_epoch" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Summary
run: |
# Calculate total workflow duration
start_time=${{ steps.workflow-start.outputs.start }}
end_time=$(date +%s)
duration=$((end_time - start_time))
minutes=$((duration / 60))
seconds=$((duration % 60))
echo "## 🏗️ Cleanuparr Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version**: ${{ needs.validate.outputs.release_version }}" >> $GITHUB_STEP_SUMMARY
echo "**App Version**: ${{ needs.validate.outputs.app_version }}" >> $GITHUB_STEP_SUMMARY
echo "**Is Tag**: ${{ needs.validate.outputs.is_tag }}" >> $GITHUB_STEP_SUMMARY
echo "**Total Duration**: ${minutes}m ${seconds}s" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Build Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Helper function to print job result
print_result() {
local name="$1"
local result="$2"
case "$result" in
success) echo "✅ **$name**: Success" >> $GITHUB_STEP_SUMMARY ;;
skipped) echo "⏭️ **$name**: Skipped" >> $GITHUB_STEP_SUMMARY ;;
*) echo "❌ **$name**: $result" >> $GITHUB_STEP_SUMMARY ;;
esac
}
print_result "Tests" "${{ needs.test.result }}"
print_result "Frontend Build" "${{ needs.build-frontend.result }}"
print_result "Portable Executables" "${{ needs.build-executables.result }}"
print_result "Windows Installer" "${{ needs.build-windows-installer.result }}"
print_result "macOS Installers (Intel & ARM)" "${{ needs.build-macos.result }}"
print_result "Docker Image Build" "${{ needs.build-docker.result }}"
echo "" >> $GITHUB_STEP_SUMMARY
echo "🎉 **Build completed!**" >> $GITHUB_STEP_SUMMARY
githubContext: ${{ toJSON(github) }}
secrets: inherit

View File

@@ -1,99 +0,0 @@
name: Tests
on:
push:
branches:
- main
paths:
- 'code/backend/**'
- '.github/workflows/test.yml'
pull_request:
paths:
- 'code/backend/**'
- '.github/workflows/test.yml'
workflow_call:
# Cancel in-progress runs for the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
timeout-minutes: 1
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Get vault secrets
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_HOST }}
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets:
secrets/data/github packages_pat | PACKAGES_PAT
- name: Restore dependencies
run: |
dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ env.PACKAGES_PAT }} --store-password-in-clear-text --name Cleanuparr https://nuget.pkg.github.com/Cleanuparr/index.json
dotnet restore code/backend/cleanuparr.sln
- name: Build solution
run: dotnet build code/backend/cleanuparr.sln --configuration Release --no-restore
- name: Run tests
id: run-tests
run: dotnet test code/backend/cleanuparr.sln --configuration Release --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" --settings code/backend/coverage.runsettings --results-directory ./coverage
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: ./coverage/*.trx
retention-days: 30
- name: Upload coverage reports
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: ./coverage/**/coverage.cobertura.xml
retention-days: 30
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage/**/coverage.cobertura.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
flags: backend
name: backend-coverage
- name: Test Summary
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.run-tests.outcome }}" == "success" ]; then
echo "✅ All tests passed!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Tests failed or were cancelled. Status: ${{ steps.run-tests.outcome }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Test artifacts have been uploaded for detailed analysis." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,66 +0,0 @@
name: Get Version Info
on:
workflow_call:
inputs:
manual_version:
description: 'Manual version override (e.g., 1.0.0)'
required: false
type: string
default: ''
outputs:
app_version:
description: 'Application version (without v prefix)'
value: ${{ jobs.version.outputs.app_version }}
release_version:
description: 'Release version (with v prefix)'
value: ${{ jobs.version.outputs.release_version }}
is_tag:
description: 'Whether this is a tag event'
value: ${{ jobs.version.outputs.is_tag }}
repository_name:
description: 'Repository name without owner'
value: ${{ jobs.version.outputs.repository_name }}
jobs:
version:
runs-on: ubuntu-latest
outputs:
app_version: ${{ steps.version.outputs.app_version }}
release_version: ${{ steps.version.outputs.release_version }}
is_tag: ${{ steps.version.outputs.is_tag }}
repository_name: ${{ steps.version.outputs.repository_name }}
steps:
- name: Calculate version info
id: version
run: |
repoFullName="${{ github.repository }}"
repositoryName="${repoFullName#*/}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag event
release_version="${GITHUB_REF##refs/tags/}"
app_version="${release_version#v}"
is_tag="true"
elif [[ -n "${{ inputs.manual_version }}" ]]; then
# Manual workflow with version
app_version="${{ inputs.manual_version }}"
release_version="v${app_version}"
is_tag="false"
else
# Development build
app_version="0.0.1-dev-$(date +%Y%m%d-%H%M%S)"
release_version="v${app_version}"
is_tag="false"
fi
echo "app_version=${app_version}" >> $GITHUB_OUTPUT
echo "release_version=${release_version}" >> $GITHUB_OUTPUT
echo "is_tag=${is_tag}" >> $GITHUB_OUTPUT
echo "repository_name=${repositoryName}" >> $GITHUB_OUTPUT
echo "📦 Repository: ${repositoryName}"
echo "🏷️ Release Version: ${release_version}"
echo "📱 App Version: ${app_version}"
echo "🔖 Is Tag: ${is_tag}"

View File

@@ -1,325 +0,0 @@
# Contributing to Cleanuparr
Thanks for your interest in contributing to Cleanuparr! This guide will help you get started with development.
## Before You Start
### Announce Your Intent
Before starting any work, please let us know what you want to contribute:
- For existing issues: Comment on the issue stating you'd like to work on it
- For new features/changes: Create a new issue first and mention that you want to work on it
This helps us avoid redundant work, git conflicts, and contributions that may not align with the project's direction.
**Wait for approval from the maintainers before proceeding with your contribution.**
## Development Setup
### Prerequisites
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
- [Node.js 18+](https://nodejs.org/)
- [Git](https://git-scm.com/)
- (Optional) [Make](https://www.gnu.org/software/make/) for database migrations
- (Optional) IDE: [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/)
### Repository Setup
1. Fork the repository on GitHub
2. Clone your fork locally:
```bash
git clone https://github.com/YOUR_USERNAME/Cleanuparr.git
cd Cleanuparr
```
3. Add the upstream repository:
```bash
git remote add upstream https://github.com/Cleanuparr/Cleanuparr.git
```
## Backend Development
### Initial Setup
#### 1. Create a GitHub Personal Access Token (PAT)
Cleanuparr uses GitHub Packages for NuGet dependencies. You'll need a PAT with `read:packages` permission:
1. Go to [GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic)](https://github.com/settings/tokens)
2. Click "Generate new token" → "Generate new token (classic)"
3. Give it a descriptive name (e.g., "Cleanuparr NuGet Access")
4. Set an expiration (recommend 90 days or longer for development)
5. Select only the `read:packages` scope
6. Click "Generate token" and copy it
#### 2. Configure NuGet Source
Add the Cleanuparr NuGet repository:
```bash
dotnet nuget add source \
--username YOUR_GITHUB_USERNAME \
--password YOUR_GITHUB_PAT \
--store-password-in-clear-text \
--name Cleanuparr \
https://nuget.pkg.github.com/Cleanuparr/index.json
```
Replace `YOUR_GITHUB_USERNAME` and `YOUR_GITHUB_PAT` with your GitHub username and the PAT you created.
### Running the Backend
#### Option 1: Using .NET CLI
Navigate to the backend directory:
```bash
cd code/backend
```
Build the application:
```bash
dotnet build Cleanuparr.Api/Cleanuparr.Api.csproj
```
Run the application:
```bash
dotnet run --project Cleanuparr.Api/Cleanuparr.Api.csproj
```
Run tests:
```bash
dotnet test
```
The API will be available at http://localhost:5000
#### Option 2: Using an IDE
For JetBrains Rider or Visual Studio:
1. Open the solution file: `code/backend/cleanuparr.sln`
2. Set `Cleanuparr.Api` as the startup project
3. Press `F5` to start the application
### Database Migrations
Cleanuparr uses two separate database contexts: `DataContext` and `EventsContext`.
#### Prerequisites
Install Make if not already installed:
- Windows: Install via [Chocolatey](https://chocolatey.org/) (`choco install make`) or use [WSL](https://docs.microsoft.com/windows/wsl/)
- macOS: Install via Homebrew (`brew install make`)
- Linux: Usually pre-installed, or install via package manager (`apt install make`, `yum install make`, etc.)
#### Creating Migrations
From the `code` directory:
For data migrations (DataContext):
```bash
make migrate-data name=YourMigrationName
```
For events migrations (EventsContext):
```bash
make migrate-events name=YourMigrationName
```
Example:
```bash
make migrate-data name=AddUserPreferences
make migrate-events name=AddAuditLogEvents
```
## Frontend Development
### Setup
1. Navigate to the frontend directory:
```bash
cd code/frontend
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm start
```
The UI will be available at http://localhost:4200
## Documentation Development
### Setup
1. Navigate to the docs directory:
```bash
cd docs
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm start
```
The documentation site will be available at http://localhost:3000
## Building with Docker
### Building a Local Docker Image
To build the Docker image locally for testing:
1. Navigate to the `code` directory:
```bash
cd code
```
2. Build the image:
```bash
docker build \
--build-arg PACKAGES_USERNAME=YOUR_GITHUB_USERNAME \
--build-arg PACKAGES_PAT=YOUR_GITHUB_PAT \
-t cleanuparr:local \
-f Dockerfile .
```
Replace `YOUR_GITHUB_USERNAME` and `YOUR_GITHUB_PAT` with your credentials.
3. Run the container:
```bash
docker run -d \
--name cleanuparr-dev \
-p 11011:11011 \
-v /path/to/config:/config \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Etc/UTC \
cleanuparr:local
```
4. Access the application at http://localhost:11011
### Building for Multiple Architectures
Use Docker Buildx for multi-platform builds:
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg PACKAGES_USERNAME=YOUR_GITHUB_USERNAME \
--build-arg PACKAGES_PAT=YOUR_GITHUB_PAT \
-t cleanuparr:local \
-f Dockerfile .
```
## Code Standards
### Backend (.NET/C#)
- Follow existing conventions and [Microsoft C# Coding Conventions](https://docs.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions)
- Use meaningful variable and method names
- Add XML documentation comments for public APIs
- Write unit tests whenever possible
### Frontend (Angular/TypeScript)
- Follow existing conventions and the [Angular Style Guide](https://angular.io/guide/styleguide)
- Use TypeScript strict mode
- Write unit tests whenever possible
### Documentation
- Use clear, concise language
- Include code examples where appropriate
- Update relevant documentation when adding/changing features
- Check for spelling and grammar
## Submitting Your Contribution
### 1. Create a Feature Branch
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix-name
```
### 2. Make Your Changes
- Write clean, well-documented code
- Follow the code standards outlined above
- **Test your changes thoroughly!**
### 3. Commit Your Changes
Write clear, descriptive commit messages:
```bash
git add .
git commit -m "Add feature: brief description of your changes"
```
### 4. Keep Your Branch Updated
```bash
git fetch upstream
git rebase upstream/main
```
### 5. Push to Your Fork
```bash
git push origin feature/your-feature-name
```
### 6. Create a Pull Request
1. Go to the [Cleanuparr repository](https://github.com/Cleanuparr/Cleanuparr)
2. Click "New Pull Request"
3. Select your fork and branch
4. Fill out the PR template with:
- A descriptive title (e.g., "Add support for Prowlarr integration" or "Fix memory leak in download client polling")
- Description of changes
- Related issue number
- Testing performed
- Screenshots (if applicable)
### 7. Code Review Process
- Maintainers will review your PR
- Address any feedback or requested changes
- Once approved, your PR will be merged
## Other Ways to Contribute
### Help Test New Features
We're always looking for testers to help validate new features before they are released. If you'd like to help test upcoming changes:
1. Join our [Discord community](https://discord.gg/SCtMCgtsc4)
2. Let us know you're interested in testing
3. We'll provide you with pre-release builds and testing instructions
Your feedback helps us catch issues early and deliver better releases.
## Getting Help
- Discord: Join our [Discord community](https://discord.gg/SCtMCgtsc4) for real-time help
- Issues: Check existing [GitHub issues](https://github.com/Cleanuparr/Cleanuparr/issues) or create a new one
- Documentation: Review the [complete documentation](https://cleanuparr.github.io/Cleanuparr/)
## License
By contributing to Cleanuparr, you agree that your contributions will be licensed under the same license as the project.
---
Thanks for contributing to Cleanuparr!

View File

@@ -1,3 +0,0 @@
# Cache static files for 5 minutes
/static/*
Cache-Control: public, max-age=300, s-maxage=300

View File

@@ -1,2 +0,0 @@
thepirateheaven.org
RARBG.work

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

391
README.md
View File

@@ -1,110 +1,353 @@
_Love this project? Give it a ⭐️ and let others know!_
# <img width="24px" src="./Logo/256.png" alt="Cleanuparr"></img> Cleanuparr
# <img width="24px" src="./Logo/256.png" alt="cleanuperr"></img> cleanuperr
![Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcleanuparr-status.pages.dev%2Fstatus.json&query=%24.version&logo=git&label=version&color=blue)
![Total Downloads](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fghcr-badge.elias.eu.org%2Fapi%2FCleanuparr%2FCleanuparr%2Fcleanuparr&query=%24.downloadCount&style=flat&logo=docker&label=Total%20Downloads&color=blue)
[![Tests](https://github.com/Cleanuparr/Cleanuparr/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/Cleanuparr/Cleanuparr/actions/workflows/test.yml)
[![Discord](https://img.shields.io/discord/1306721212587573389?color=7289DA&label=Discord&style=for-the-badge&logo=discord)](https://discord.gg/sWggpnmGNY)
cleanuperr is a tool for automating the cleanup of unwanted or blocked files in Sonarr, Radarr, and supported download clients like qBittorrent. It removes incomplete or blocked downloads, updates queues, and enforces blacklists or whitelists to manage file selection. After removing blocked content, cleanuperr can also trigger a search to replace the deleted shows/movies.
[![Discord](https://img.shields.io/discord/1306721212587573389?color=7289DA&label=Discord&style=for-the-badge&logo=discord)](https://discord.gg/SCtMCgtsc4)
Cleanuparr is a tool for automating the cleanup of unwanted or blocked files in Sonarr, Radarr, and supported download clients like qBittorrent. It removes incomplete or blocked downloads, updates queues, and enforces blacklists or whitelists to manage file selection. After removing blocked content, Cleanuparr can also trigger a search to replace the deleted shows/movies.
Cleanuparr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made Cleanuparr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
> [!IMPORTANT]
> **Features:**
> - Strike system to mark bad downloads.
> - Strike system to mark stalled or downloads stuck in metadata downloading.
> - Remove and block downloads that reached a maximum number of strikes.
> - 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.
> - 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 **Malware Blocker**.
> - Remove and block known malware based on patterns found by the community.
> - Automatically trigger a search for downloads removed from the arrs.
> - 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).
> - Remove downloads blocked by qBittorrent or by cleanuperr's **content blocker**.
> - Trigger a search for downloads removed from the *arrs.
> - Clean up downloads that have been seeding for a certain amount of time.
> - 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 processing.
## Screenshots
cleanuperr 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.
https://cleanuparr.github.io/Cleanuparr/docs/screenshots
> [!WARNING]
> This tool is actively developed and still a work in progress, so using the `latest` Docker tag may result in breaking changes. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
>
> https://discord.gg/sWggpnmGNY
## 🎯 Supported Applications
## Table of contents:
- [Naming choice](#naming-choice)
- [Quick Start](#quick-start)
- [How it works](#how-it-works)
- [Content blocker](#1-content-blocker-will)
- [Queue cleaner](#2-queue-cleaner-will)
- [Download cleaner](#3-download-cleaner-will)
- [Setup](#setup-examples)
- [Usage](#usage)
- [Docker](#docker)
- [Environment Variables](#environment-variables)
- [Docker Compose](#docker-compose-example)
- [Windows](#windows)
- [Linux](#linux)
- [MacOS](#macos)
- [FreeBSD](#freebsd)
- [Credits](#credits)
### *Arr Applications
- **Sonarr**
- **Radarr**
- **Lidarr**
- **Readarr**
- **Whisparr**
## Naming choice
### Download Clients
- **qBittorrent**
- **Transmission**
- **Deluge**
- **µTorrent**
I've had people asking why it's `cleanuperr` and not `cleanuparr` and that I should change it. This name was intentional.
### Platforms
- **Docker**
- **Windows**
- **macOS**
- **Linux**
- **Unraid**
I've seen a few discussions on this type of naming and I've decided that I didn't deserve the `arr` moniker since `cleanuperr` is not a fork of `NZB.Drone` and it does not have any affiliation with the arrs. I still wanted to keep the naming style close enough though, to suggest a correlation between them.
## 🚀 Quick Start
## 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
> [!NOTE]
>
> 1. **Docker (Recommended)**
> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
>
> 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).
> [!TIP]
> Refer to the [Environment variables](#environment-variables) section for detailed configuration instructions and the [Setup examples](#setup-examples) section for an in-depth explanation of the cleanup process.
> [!IMPORTANT]
> Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version:
> - qBittorrent
> - Deluge
> - Transmission
> - Sonarr
> - Radarr
> - Lidarr
# How it works
#### 1. **Content blocker** will:
- Run every 5 minutes (or configured cron).
- Process all items in the *arr queue.
- Find the corresponding item from the download client for each queue item.
- Mark the files that were found in the queue as **unwanted/skipped** if:
- They **are listed in the blacklist**, or
- They **are not included in the whitelist**.
- If **all files** of a download **are unwanted**:
- It will be removed from the *arr's queue and blocked.
- It will be deleted from the download client.
- A new search will be triggered for the *arr item.
#### 2. **Queue cleaner** will:
- Run every 5 minutes (or configured cron, or right after `content blocker`).
- Process all items in the *arr queue.
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in metadata downloading** or **failed to be imported**.
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
- Check each queue item if it meets one of the following condition in the download client:
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **content blocker**).
- All associated files of are marked as **unwanted/skipped**.
- If the item **DOES NOT** match the above criteria, it will be skipped.
- If the item **DOES** match the criteria or has received the **maximum number of strikes**:
- It will be removed from the *arr's queue and blocked.
- It will be deleted from the download client.
- A new search will be triggered for the *arr item.
#### 3. **Download cleaner** will:
- Run every hour (or configured cron).
- Automatically clean up downloads that have been seeding for a certain amount of time.
# Setup examples
## Using qBittorrent's built-in feature (works only with qBittorrent)
1. Go to qBittorrent -> Options -> Downloads -> make sure `Excluded file names` is checked -> Paste an exclusion list that you have copied.
- [blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), or
- [permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive), or
- create your own
2. qBittorrent will block files from being downloaded. In the case of malicious content, **nothing is downloaded and the torrent is marked as complete**.
3. Start **cleanuperr** with `QUEUECLEANER__ENABLED` set to `true`.
4. The **queue cleaner** will perform a cleanup process as described in the [How it works](#how-it-works) section.
## Using cleanuperr's blocklist (works with all supported download clients)
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER__ENABLED` to `true` in your environment variables.
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Arr variables](variables.md#Arr-settings) section.
3. Once configured, cleanuperr will perform the following tasks:
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
## Using cleanuperr just for failed *arr imports (works for Usenet users as well)
1. Set `QUEUECLEANER__ENABLED` to `true`.
2. Set `QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES` to a desired value.
3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`.
4. Set `DOWNLOAD_CLIENT` to `none`.
> [!WARNING]
> When `DOWNLOAD_CLIENT=none`, no other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).
## Usage
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/docker.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Docker</span>
### **Environment variables**
**Jump to:**
- [General settings](variables.md#general-settings)
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
- [Content Blocker settings](variables.md#content-blocker-settings)
- [Download Cleaner settings](variables.md#download-cleaner-settings)
- [Download Client settings](variables.md#download-client-settings)
- [Arr settings](variables.md#arr-settings)
- [Notification settings](variables.md#notification-settings)
- [Advanced settings](variables.md#advanced-settings)
### Docker compose example
> [!NOTE]
>
> This example contains all settings and should be modified to fit your needs.
```
version: "3.3"
services:
cleanuperr:
image: ghcr.io/flmorg/cleanuperr:latest
restart: unless-stopped
volumes:
- ./cleanuperr/logs:/var/logs
- ./cleanuperr/ignored.txt:/ignored.txt
environment:
- TZ=America/New_York
- DRY_RUN=false
- LOGGING__LOGLEVEL=Information
- LOGGING__FILE__ENABLED=false
- LOGGING__FILE__PATH=/var/logs/
- LOGGING__ENHANCED=true
- TRIGGERS__QUEUECLEANER=0 0/5 * * * ?
- TRIGGERS__CONTENTBLOCKER=0 0/5 * * * ?
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?
- QUEUECLEANER__ENABLED=true
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS=false
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- CONTENTBLOCKER__IGNORE_PRIVATE=false
- CONTENTBLOCKER__DELETE_PRIVATE=false
- DOWNLOADCLEANER__ENABLED=true
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- DOWNLOADCLEANER__DELETE_PRIVATE=false
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME=240
- DOWNLOADCLEANER__CATEGORIES__1__NAME=radarr
- DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=240
- DOWNLOAD_CLIENT=none
# OR
# - DOWNLOAD_CLIENT=qBittorrent
# - QBITTORRENT__URL=http://localhost:8080
# - QBITTORRENT__USERNAME=user
# - QBITTORRENT__PASSWORD=pass
# OR
# - DOWNLOAD_CLIENT=deluge
# - DELUGE__URL=http://localhost:8112
# - DELUGE__PASSWORD=testing
# OR
# - DOWNLOAD_CLIENT=transmission
# - TRANSMISSION__URL=http://localhost:9091
# - TRANSMISSION__USERNAME=test
# - TRANSMISSION__PASSWORD=testing
- SONARR__ENABLED=true
- SONARR__SEARCHTYPE=Episode
- SONARR__BLOCK__TYPE=blacklist
- SONARR__BLOCK__PATH=https://example.com/path/to/file.txt
- SONARR__INSTANCES__0__URL=http://localhost:8989
- SONARR__INSTANCES__0__APIKEY=secret1
- SONARR__INSTANCES__1__URL=http://localhost:8990
- SONARR__INSTANCES__1__APIKEY=secret2
- RADARR__ENABLED=true
- RADARR__BLOCK__TYPE=blacklist
- RADARR__BLOCK__PATH=https://example.com/path/to/file.txt
- RADARR__INSTANCES__0__URL=http://localhost:7878
- RADARR__INSTANCES__0__APIKEY=secret3
- RADARR__INSTANCES__1__URL=http://localhost:7879
- RADARR__INSTANCES__1__APIKEY=secret4
- LIDARR__ENABLED=true
- LIDARR__BLOCK__TYPE=blacklist
- LIDARR__BLOCK__PATH=https://example.com/path/to/file.txt
- LIDARR__INSTANCES__0__URL=http://radarr:8686
- LIDARR__INSTANCES__0__APIKEY=secret5
- LIDARR__INSTANCES__1__URL=http://radarr:8687
- LIDARR__INSTANCES__1__APIKEY=secret6
- NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
- NOTIFIARR__ON_STALLED_STRIKE=true
- NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
- NOTIFIARR__ON_DOWNLOAD_CLEANED=true
- NOTIFIARR__API_KEY=notifiarr_secret
- NOTIFIARR__CHANNEL_ID=discord_channel_id
```
For Docker Compose, health checks, and other installation methods, see the [Complete Installation Guide](https://cleanuparr.github.io/Cleanuparr/docs/installation/detailed), but not before reading the [Prerequisites](https://cleanuparr.github.io/Cleanuparr/docs/installation/).
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/windows.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Windows</span>
### 🌐 Access the Web Interface
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
2. Extract the zip file into `C:\example\directory`.
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
4. Execute `cleanuperr.exe`.
After installation, open your browser and navigate to:
```
http://localhost:11011
```
> [!TIP]
> ### Run as a Windows Service
> Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
**Next Steps:** Check out the [📖 Complete Documentation](https://cleanuparr.github.io/Cleanuparr/) for detailed configuration guides and setup instructions.
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/linux.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Linux</span>
## 📖 Documentation & Support
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
2. Extract the zip file into `/example/directory`.
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
4. Open a terminal and execute these commands:
```
cd /example/directory
chmod +x cleanuperr
./cleanuperr
```
- **📚 [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 src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/apple.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">MacOS</span>
## 🤝 Contributing
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
2. Extract the zip file into `/example/directory`.
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
4. Open a terminal and execute these commands:
```
cd /example/directory
chmod +x cleanuperr
./cleanuperr
```
We welcome contributions from the community! Whether it's bug fixes, new features, documentation improvements, or testing, your help is appreciated.
> [!IMPORTANT]
> Some people have experienced problems when trying to execute cleanuperr on MacOS because the system actively blocked the file for not being signed.
> As per [this](), you may need to also execute this command:
> ```
> codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime /example/directory/cleanuperr
> ```
**Before contributing:** Please read our [Contributing Guide](CONTRIBUTING.md) and announce your intent to work on an issue before starting.
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/freebsd.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">FreeBSD</span>
- **[Contributing Guide](CONTRIBUTING.md)** - Learn how to set up your development environment and submit contributions
- **[Report Issues](https://github.com/Cleanuparr/Cleanuparr/issues/new/choose)** - Found a bug? Let us know!
- **[Feature Requests](https://github.com/Cleanuparr/Cleanuparr/issues/new/choose)** - Share your ideas for new features
- **[Help Test Features](https://discord.gg/SCtMCgtsc4)** - Join Discord to test pre-release features and provide feedback
1. Installation:
```
# install dependencies
pkg install -y git icu libinotify libunwind wget
# <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>
# set up the dotnet SDK
cd ~
wget -q https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/dotnet-sdk-9.0.104-freebsd-x64.tar.gz
export DOTNET_ROOT=$(pwd)/.dotnet
mkdir -p "$DOTNET_ROOT" && tar zxf dotnet-sdk-9.0.104-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools
Think of **Cleanuparr** as the janitor of your server; it keeps your download queue spotless, removes clutter, and blocks malicious files. Now imagine combining that with **Huntarr**, the compulsive librarian who finds missing and upgradable media to complete your collection
# download NuGet dependencies
mkdir -p /tmp/nuget
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.AspNetCore.App.Runtime.freebsd-x64.9.0.3.nupkg
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.NETCore.App.Host.freebsd-x64.9.0.3.nupkg
wget -q -P /tmp/nuget/ https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v9.0.104-amd64-freebsd-14/Microsoft.NETCore.App.Runtime.freebsd-x64.9.0.3.nupkg
While **Huntarr** fills in the blanks and improves what you already have, **Cleanuparr** makes sure that only clean downloads get through. If you're aiming for a reliable and self-sufficient setup, **Cleanuparr** and **Huntarr** will take your automated media stack to another level.
# add NuGet source
dotnet nuget add source /tmp/nuget --name tmp
<span style="font-size:24px"> ➡️ [**Huntarr**](https://github.com/plexguide/Huntarr.io) <span style="vertical-align: middle">![Huntarr](https://img.shields.io/github/stars/plexguide/Huntarr.io?style=social)</span></span>
# add GitHub NuGet source
# a PAT (Personal Access Token) can be generated here https://github.com/settings/tokens
dotnet nuget add source --username <YOUR_USERNAME> --password <YOUR_PERSONAL_ACCESS_TOKEN> --store-password-in-clear-text --name flmorg https://nuget.pkg.github.com/flmorg/index.json
```
2. Building:
```
# clone the project
git clone https://github.com/flmorg/cleanuperr.git
cd cleanuperr
# build and publish the app
dotnet publish code/Executable/Executable.csproj -c Release --self-contained -o artifacts /p:PublishSingleFile=true
# move the files to permanent destination
mv artifacts/cleanuperr /example/directory/
mv artifacts/appsettings.json /example/directory/
```
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
4. Run the app:
```
cd /example/directory
chmod +x cleanuperr
./cleanuperr
```
# Credits
Special thanks for inspiration go to:

345
blacklist
View File

@@ -1,27 +1,11 @@
*(sample).*
*sample.avchd
*sample.avi
*sample.mkv
*sample.mov
*sample.mp4
*sample.webm
*sample.wmv
*.000
*.001
*.002
*.004
*.0xe
*.73k
*.73p
*.7z
*.7z.001
*.7z.002
*.89k
*.89z
*.8ck
*.a00
*.a01
*.a02
*.a7r
*.ac
*.acc
@@ -37,11 +21,8 @@
*.ahk
*.ai
*.aif
*.ain
*.air
*.alz
*.ana
*.apex
*.api
*.apk
*.app
@@ -49,28 +30,15 @@
*.applescript
*.application
*.appx
*.apz
*.ar
*.arc
*.archiver
*.arduboy
*.arh
*.ari
*.arj
*.ark
*.arscript
*.asb
*.asice
*.asp
*.aspx
*.aspx-exe
*.atmx
*.ayt
*.azw2
*.b1
*.b6z
*.b64
*.ba
*.ba_
*.bak
*.bas
@@ -78,48 +46,26 @@
*.bat
*.bdjo
*.bdmv
*.bdoc
*.beam
*.bh
*.bin
*.bmp
*.bms
*.bndl
*.bns
*.boo
*.bsa
*.btm
*.bundle
*.bz
*.bz2
*.bza
*.bzabw
*.bzip
*.bzip2
*.c
*.c00
*.c01
*.c02
*.c10
*.cab
*.caction
*.car
*.cb7
*.cba
*.cbr
*.cbt
*.cbz
*.cci
*.cda
*.cdb
*.cdz
*.cel
*.celx
*.cfs
*.cgi
*.cheat
*.chm
*.cit
*.ckpt
*.cla
*.class
@@ -129,15 +75,9 @@
*.coffee
*.com
*.command
*.comppkg.hauptwerk.rar
*.comppkg_hauptwerk_rar
*.conda
*.conf
*.config
*.cp9
*.cpgz
*.cpl
*.cpt
*.crt
*.cs
*.csh
@@ -145,27 +85,17 @@
*.csproj
*.css
*.csv
*.ctx
*.ctz
*.cue
*.cur
*.cxarchive
*.cyw
*.czip
*.daemon
*.daf
*.dar
*.dat
*.data-00000-of-00001
*.db
*.dd
*.deamon
*.deb
*.dek
*.dgc
*.dist
*.diz
*.dl_
*.dld
*.dll
*.dmc
@@ -182,27 +112,19 @@
*.dw
*.dword
*.dxl
*.dz
*.e_e
*.ear
*.ebacmd
*.ebm
*.ebs
*.ebs2
*.ecar
*.ecf
*.ecs
*.ecsbx
*.edz
*.efw
*.egg
*.eham
*.elf
*.elf-so
*.email
*.emu
*.epk
*.epi
*.es
*.esh
*.etc
@@ -218,62 +140,36 @@
*.exz
*.ezs
*.ezt
*.f
*.f3z
*.fas
*.fba
*.fcx
*.fky
*.flac
*.flatpak
*.flv
*.fp8
*.fpi
*.frs
*.fxp
*.fzpz
*.gadget
*.gar
*.gat
*.gca
*.gif
*.gifv
*.gm9
*.gmz
*.gpe
*.gpu
*.gs
*.gz
*.gz2
*.gza
*.gzi
*.gzip
*.h5
*.ha
*.ham
*.hbc
*.hbc2
*.hbe
*.hex
*.hki
*.hki1
*.hki2
*.hki3
*.hlp
*.hms
*.hpf
*.hpk
*.hpkg
*.hta
*.hta-psh
*.htaccess
*.htm
*.html
*.htmi
*.hyp
*.iadproj
*.icd
*.ice
*.icns
*.ico
*.idx
@@ -286,27 +182,17 @@
*.ins
*.ipa
*.ipf
*.ipg
*.ipk
*.ipsw
*.iqylink
*.ish
*.iso
*.isp
*.isu
*.isx
*.ita
*.ize
*.izh
*.izma ace
*.j
*.jar
*.jar.pack
*.java
*.jex
*.jgz
*.jhh
*.jic
*.jpeg
*.jpg
*.js
@@ -315,51 +201,27 @@
*.jse
*.jsf
*.json
*.jsonlz4
*.jsp
*.jsx
*.kextraction
*.kgb
*.kix
*.ksh
*.ksp
*.kwgt
*.kx
*.kz
*.layout
*.lbr
*.lck
*.ldb
*.lemon
*.lha
*.lhzd
*.lib
*.libzip
*.link
*.lnk
*.lo
*.lock
*.log
*.loop-vbs
*.lpkg
*.lqr
*.ls
*.lz
*.lz4
*.lzh
*.lzm
*.lzma
*.lzo
*.lzr
*.lzx
*.m3u
*.m4a
*.mac
*.macho
*.mamc
*.manifest
*.mar
*.mbz
*.mcr
*.md
*.mda
@@ -370,29 +232,22 @@
*.mdt
*.mel
*.mem
*.memo
*.meta
*.mgm
*.mhm
*.mht
*.mhtml
*.mid
*.mint
*.mio
*.mlappinstall
*.mlproj
*.mlx
*.mm
*.mobileconfig
*.model
*.moo
*.mou
*.movpkg
*.mozlz4
*.mp3
*.mpa
*.mpk
*.mpkg
*.mpls
*.mrc
*.mrp
@@ -411,79 +266,41 @@
*.msp
*.mst
*.msu
*.mxc
*.mxe
*.mzp
*.n
*.nar
*.ncl
*.net
*.nex
*.nexe
*.nfo
*.npk
*.nrg
*.num
*.nz
*.nzb.bz2
*.nzb.gz
*.nzbs
*.oar
*.ocx
*.odlgz
*.odt
*.opk
*.ore
*.osf
*.ost
*.osx
*.osx-app
*.otm
*.out
*.ova
*.oz
*.p
*.p01
*.p19
*.p7z
*.pa
*.pack.gz
*.package
*.pae
*.paf
*.pak
*.paq6
*.paq7
*.paq8
*.paq8f
*.paq8l
*.paq8p
*.par
*.par2
*.pax
*.pb
*.pbi
*.pcd
*.pcv
*.pdb
*.pdf
*.pea
*.perl
*.pet
*.pex
*.pf
*.phar
*.php
*.php5
*.pif
*.pim
*.pima
*.pit
*.piz
*.pkg
*.pkg.tar.xz
*.pkg.tar.zst
*.pkz
*.pl
*.plsc
*.plx
@@ -501,7 +318,6 @@
*.pptx
*.prc
*.prg
*.prs
*.ps
*.ps1
*.ps1xml
@@ -517,16 +333,9 @@
*.psh-reflection
*.psm1
*.pst
*.psz
*.pt
*.pup
*.puz
*.pvd
*.pvmp
*.pvmz
*.pwa
*.pwc
*.pxl
*.pxo
*.py
*.pyc
@@ -534,20 +343,8 @@
*.pyo
*.python
*.pyz
*.q
*.qda
*.qit
*.qpx
*.r0
*.r00
*.r01
*.r02
*.r03
*.r04
*.r1
*.r2
*.r21
*.r30
*.ram
*.rar
*.raw
@@ -558,35 +355,22 @@
*.reg
*.resources
*.resx
*.rev
*.rfs
*.rfu
*.rgs
*.rk
*.rm
*.rnc
*.rox
*.rp9
*.rpg
*.rpj
*.rpm
*.rss
*.ruby
*.run
*.rxe
*.rz
*.s00
*.s01
*.s02
*.s09
*.s2a
*.s7z
*.sample
*.sapk
*.sar
*.savedmodel
*.sbs
*.sbx
*.sca
*.scar
*.scb
@@ -596,86 +380,42 @@
*.scr
*.script
*.sct
*.sdc
*.sdn
*.sdoc
*.sdocx
*.sea
*.seed
*.sen
*.server
*.service
*.sfg
*.sfm
*.sfs
*.sfv
*.sfx
*.sh
*.shar
*.shb
*.shell
*.shk
*.shortcut
*.shr
*.shs
*.shtml
*.sifz
*.sipa
*.sit
*.sitx
*.sk
*.sldm
*.sln
*.smm
*.smpf
*.snap
*.snagitstamps
*.snappy
*.snb
*.snd
*.snz
*.spa
*.spd
*.spl
*.spm
*.spr
*.spt
*.sql
*.sqf
*.sqx
*.sqz
*.srec
*.srep
*.srt
*.ssm
*.stg
*.stkdoodlz
*.stproj
*.sts
*.sub
*.svg
*.swf
*.sy_
*.sys
*.tar
*.tar.bz2
*.tar.gz
*.tar.gz2
*.tar.lz
*.tar.lzma
*.tar.xz
*.tar.z
*.tar.zip
*.taz
*.tbl
*.tbz
*.tbz2
*.tcp
*.tcx
*.text
*.tf
*.tg
*.tgs
*.tgz
*.thm
*.thmx
@@ -684,35 +424,19 @@
*.tif
*.tiff
*.tipa
*.tlz
*.tlzma
*.tmp
*.tms
*.toast
*.torrent
*.tpk
*.tpsr
*.trs
*.txt
*.tx_
*.txz
*.tz
*.tzst
*.u3p
*.ubz
*.uc2
*.udf
*.ufdr
*.ufs.uzip
*.uha
*.upk
*.upx
*.url
*.uue
*.uvm
*.uw8
*.uzed
*.uzip
*.vb
*.vba
*.vba-exe
@@ -724,46 +448,26 @@
*.vbscript
*.vcd
*.vdo
*.vem
*.vexe
*.vfs
*.vhd
*.vhdx
*.vib
*.vip
*.vlx
*.vm
*.vmcz
*.vmdk
*.vms
*.vob
*.vocab
*.voca
*.vpk
*.vpm
*.vrpackage
*.vsi
*.vwi
*.vxp
*.wa
*.wacz
*.waff
*.war
*.wastickers
*.wav
*.wbk
*.wcm
*.wdz
*.webm
*.whl
*.wick
*.widget
*.wim
*.wiz
*.wlb
*.wma
*.workflow
*.wot
*.wpk
*.wpl
*.wpm
@@ -772,26 +476,14 @@
*.wsc
*.wsf
*.wsh
*.wux
*.x86
*.x86_64
*.xaml
*.xap
*.xapk
*.xar
*.xbap
*.xbe
*.xcf.bz2
*.xcf.gz
*.xcf.xz
*.xcfbz2
*.xcfgz
*.xcfxz
*.xex
*.xez
*.xfp
*.xig
*.xip
*.xla
*.xlam
*.xll
@@ -804,47 +496,24 @@
*.xltb
*.xltm
*.xlw
*.xmcdz
*.xml
*.xoj
*.xopp
*.xqt
*.xrt
*.xx
*.xys
*.xz
*.xzm
*.y
*.yc
*.ygh
*.yz1
*.z
*.z00
*.z01
*.z02
*.z03
*.z04
*.zabw
*.zap
*.zed
*.zfsendtotarget
*.zhelp
*.zi
*.zi_
*.zim
*.zip
*.zipx
*.zix
*.zl
*.zl9
*.zoo
*.zpaq
*.zpi
*.zsplit
*.zst
*.zw
*.zwi
*.zz
*sample.avchd
*sample.avi
*sample.mkv
*sample.mov
*sample.mp4
*sample.webm
*sample.wmv
Trailer.*
VOSTFR
api

View File

@@ -1,410 +1,52 @@
*.000
*.001
*.002
*.004
*.7z
*.7z.001
*.7z.002
*.a00
*.a01
*.a02
*.ace
*.ain
*.alz
*.ana
*.apex
*.apk
*.apz
*.ar
*.arc
*.archiver
*.arduboy
*.arh
*.ari
*.arj
*.ark
*.asice
*.ayt
*.b1
*.b6z
*.b64
*.ba
*.bat
*.bdoc
*.bh
*.bin
*.bmp
*.bndl
*.boo
*.bundle
*.bz
*.bz2
*.bza
*.bzabw
*.bzip
*.bzip2
*.c00
*.c01
*.c02
*.c10
*.car
*.cb7
*.cba
*.cbr
*.cbt
*.cbz
*.cdz
*.cit
*.cmd
*.com
*.comppkg.hauptwerk.rar
*.comppkg_hauptwerk_rar
*.conda
*.cp9
*.cpgz
*.cpt
*.ctx
*.ctz
*.cxarchive
*.czip
*.daf
*.dar
*.db
*.dd
*.deb
*.dgc
*.dist
*.diz
*.dl_
*.dll
*.dmg
*.dz
*.ecar
*.ecs
*.ecsbx
*.edz
*.efw
*.egg
*.epi
*.etc
*.exe
*.f
*.f3z
*.fcx
*.fp8
*.fzpz
*.gar
*.gca
*.gif
*.gmz
*.gz
*.gz2
*.gza
*.gzi
*.gzip
*.ha
*.hbc
*.hbc2
*.hbe
*.hki
*.hki1
*.hki2
*.hki3
*.hpk
*.hpkg
*.htm
*.html
*.htmi
*.hyp
*.iadproj
*.ice
*.ico
*.ini
*.ipg
*.ipk
*.ish
*.iso
*.isx
*.ita
*.ize
*.j
*.jar
*.jar.pack
*.jex
*.jgz
*.jhh
*.jic
*.jpg
*.js
*.jsonlz4
*.kextraction
*.kgb
*.ksp
*.kwgt
*.kz
*.layout
*.lbr
*.lemon
*.lha
*.lhzd
*.libzip
*.link
*.lnk
*.lpkg
*.lqr
*.lz
*.lz4
*.lzh
*.lzm
*.lzma
*.lzo
*.lzr
*.lzx
*.mar
*.mbz
*.md
*.memo
*.mint
*.mlproj
*.mou
*.movpkg
*.mozlz4
*.mpkg
*.msi
*.mxc
*.mzp
*.nar
*.nex
*.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
*.pet
*.pf
*.php
*.pim
*.pima
*.pit
*.piz
*.pkg
*.pkg.tar.xz
*.pkg.tar.zst
*.pkz
*.pl
*.png
*.prs
*.ps1
*.psc1
*.psd1
*.psm1
*.psz
*.pup
*.puz
*.pvmp
*.pvmz
*.pwa
*.pxl
*.py
*.pyd
*.q
*.qda
*.r0
*.r00
*.r01
*.r02
*.r03
*.r04
*.r1
*.r2
*.r21
*.r30
*.rar
*.rb
*.readme
*.reg
*.rev
*.rk
*.rnc
*.rp9
*.rpm
*.rss
*.run
*.rz
*.s00
*.s01
*.s02
*.s09
*.s7z
*.sar
*.sbx
*.scr
*.sdc
*.sdn
*.sdoc
*.sdocx
*.sea
*.sen
*.sfg
*.sfm
*.sfs
*.sfx
*.sh
*.shar
*.shk
*.shr
*.sifz
*.sipa
*.sit
*.sitx
*.smpf
*.snagitstamps
*.snappy
*.snb
*.snz
*.spa
*.spd
*.spl
*.spm
*.spt
*.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
*.tg
*.tgs
*.tgz
*.thumb
*.tlz
*.tlzma
*.torrent
*.tpsr
*.trs
*.txt
*.tx_
*.txz
*.tz
*.tzst
*.ubz
*.uc2
*.ufdr
*.ufs.uzip
*.uha
*.url
*.uue
*.uvm
*.uzed
*.uzip
*.vbs
*.vem
*.vfs
*.vib
*.vip
*.vmcz
*.vms
*.voca
*.vpk
*.vrpackage
*.vsi
*.vwi
*.wa
*.wacz
*.waff
*.war
*.wastickers
*.wdz
*.whl
*.wick
*.wlb
*.wot
*.wsf
*.wux
*.xapk
*.xar
*.xcf.bz2
*.xcf.gz
*.xcf.xz
*.xcfbz2
*.xcfgz
*.xcfxz
*.xez
*.xfp
*.xip
*.xml
*.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
*.zipx

162
chart/values.yaml Normal file
View File

@@ -0,0 +1,162 @@
deployment:
replicas: 1
strategy:
type: RollingUpdate
maxSurge: 1
maxUnavailable: 0
containers:
- name: qbit
image:
repository: ghcr.io/flmorg/cleanuperr
tag: latest
env:
- name: DRY_RUN
value: "false"
- name: LOGGING__LOGLEVEL
value: Debug
- name: LOGGING__FILE__ENABLED
value: "true"
- name: LOGGING__FILE__PATH
value: /var/logs
- name: LOGGING__ENHANCED
value: "true"
- name: TRIGGERS__QUEUECLEANER
value: 0 0/5 * * * ?
- name: TRIGGERS__CONTENTBLOCKER
value: 0 0/5 * * * ?
- name: QUEUECLEANER__ENABLED
value: "true"
- name: QUEUECLEANER__RUNSEQUENTIALLY
value: "true"
- name: QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES
value: "3"
- name: QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE
value: "false"
- name: QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE
value: "false"
- name: QUEUECLEANER__STALLED_MAX_STRIKES
value: "3"
- name: QUEUECLEANER__STALLED_IGNORE_PRIVATE
value: "false"
- name: QUEUECLEANER__STALLED_DELETE_PRIVATE
value: "false"
- name: CONTENTBLOCKER__ENABLED
value: "true"
- name: CONTENTBLOCKER__IGNORE_PRIVATE
value: "true"
- name: CONTENTBLOCKER__DELETE_PRIVATE
value: "false"
- name: DOWNLOADCLEANER__ENABLED
value: "false"
- name: DOWNLOAD_CLIENT
value: qbittorrent
- name: QBITTORRENT__URL
value: http://service.qbittorrent-videos.svc.cluster.local
- name: SONARR__ENABLED
value: "true"
- name: SONARR__SEARCHTYPE
value: Episode
- name: SONARR__BLOCK__TYPE
value: blacklist
- name: SONARR__BLOCK__PATH
value: https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
- name: SONARR__INSTANCES__0__URL
value: http://service.sonarr-low-res.svc.cluster.local
- name: SONARR__INSTANCES__1__URL
value: http://service.sonarr-high-res.svc.cluster.local
- name: RADARR__ENABLED
value: "true"
- name: RADARR__BLOCK__TYPE
value: blacklist
- name: RADARR__BLOCK__PATH
value: https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
- name: RADARR__INSTANCES__0__URL
value: http://service.radarr-low-res.svc.cluster.local
- name: RADARR__INSTANCES__1__URL
value: http://service.radarr-high-res.svc.cluster.local
- name: NOTIFIARR__ON_IMPORT_FAILED_STRIKE
value: "true"
- name: NOTIFIARR__ON_STALLED_STRIKE
value: "true"
- name: NOTIFIARR__ON_QUEUE_ITEM_DELETED
value: "true"
- name: NOTIFIARR__ON_DOWNLOAD_CLEANED
value: "true"
- name: NOTIFIARR__CHANNEL_ID
value: "1340708411259748413"
envFromSecret:
- secretName: qbit-auth
envs:
- name: QBITTORRENT__USERNAME
key: QBIT_USER
- name: QBITTORRENT__PASSWORD
key: QBIT_PASS
- secretName: sonarr-auth
envs:
- name: SONARR__INSTANCES__0__APIKEY
key: SNRL_API_KEY
- name: SONARR__INSTANCES__1__APIKEY
key: SNRH_API_KEY
- secretName: radarr-auth
envs:
- name: RADARR__INSTANCES__0__APIKEY
key: RDRL_API_KEY
- name: RADARR__INSTANCES__1__APIKEY
key: RDRH_API_KEY
- secretName: notifiarr-auth
envs:
- name: NOTIFIARR__API_KEY
key: API_KEY
resources:
requests:
cpu: 0m
memory: 0Mi
limits:
cpu: 1000m
memory: 1000Mi
volumeMounts:
- name: storage
mountPath: /var/logs
subPath: cleanuperr/logs
volumes:
- name: storage
type: pvc
typeName: storage-pvc
pvcs:
- name: storage-pvc
storageClassName: local-path-persistent
accessModes:
- ReadWriteOnce
size: 1Gi
volumeMode: Filesystem
vaultSecrets:
- name: qbit-auth
path: secrets/qbittorrent
templates:
QBIT_USER: "{% .Secrets.username %}"
QBIT_PASS: "{% .Secrets.password %}"
- name: radarr-auth
path: secrets/radarr
templates:
RDRL_API_KEY: "{% .Secrets.low_api_key %}"
RDRH_API_KEY: "{% .Secrets.high_api_key %}"
- name: sonarr-auth
path: secrets/sonarr
templates:
SNRL_API_KEY: "{% .Secrets.low_api_key %}"
SNRH_API_KEY: "{% .Secrets.high_api_key %}"
- name: notifiarr-auth
path: secrets/notifiarr
templates:
API_KEY: "{% .Secrets.passthrough_api_key %}"

View File

@@ -1,41 +0,0 @@
# Documentation
*.md
docs/
# Version control
.git/
.gitignore
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Node.js
frontend/node_modules/
frontend/dist/
frontend/.angular/
# .NET
backend/bin/
backend/obj/
backend/*/bin/
backend/*/obj/
backend/.vs/
# Build artifacts
artifacts/
dist/
# Test files
backend/**/*Tests/
backend/**/Tests/
# Development files
docker-compose*.yml
test/

View File

@@ -0,0 +1,6 @@
namespace Common.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class DryRunSafeguardAttribute : Attribute
{
}

View File

@@ -7,7 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
<PackageReference Include="Serilog" Version="4.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
using Common.Configuration.ContentBlocker;
namespace Common.Configuration.Arr;
public abstract record ArrConfig
{
public required bool Enabled { get; init; }
public Block Block { get; init; } = new();
public required List<ArrInstance> Instances { get; init; }
}
public readonly record struct Block
{
public BlocklistType Type { get; init; }
public string? Path { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.Arr;
public sealed class ArrInstance
{
public required Uri Url { get; set; }
public required string ApiKey { get; set; }
}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.Arr;
public sealed record SonarrConfig : ArrConfig
{
public const string SectionName = "Sonarr";
public SonarrSearchType SearchType { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.Arr;
public enum SonarrSearchType
{
Episode,
Season,
Series
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Enums;
namespace Common.Configuration.ContentBlocker;
public enum BlocklistType
{

View File

@@ -0,0 +1,23 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.ContentBlocker;
public sealed record ContentBlockerConfig : IJobConfig, IIgnoredDownloadsConfig
{
public const string SectionName = "ContentBlocker";
public required bool Enabled { get; init; }
[ConfigurationKeyName("IGNORE_PRIVATE")]
public bool IgnorePrivate { get; init; }
[ConfigurationKeyName("DELETE_PRIVATE")]
public bool DeletePrivate { get; init; }
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
public string? IgnoredDownloadsPath { get; init; }
public void Validate()
{
}
}

View File

@@ -0,0 +1,45 @@
using Common.Exceptions;
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.DownloadCleaner;
public sealed record Category : IConfig
{
public required string Name { get; init; }
/// <summary>
/// Max ratio before removing a download.
/// </summary>
[ConfigurationKeyName("MAX_RATIO")]
public required double MaxRatio { get; init; } = -1;
/// <summary>
/// Min number of hours to seed before removing a download, if the ratio has been met.
/// </summary>
[ConfigurationKeyName("MIN_SEED_TIME")]
public required double MinSeedTime { get; init; } = 0;
/// <summary>
/// Number of hours to seed before removing a download.
/// </summary>
[ConfigurationKeyName("MAX_SEED_TIME")]
public required double MaxSeedTime { get; init; } = -1;
public void Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
throw new ValidationException($"{nameof(Name)} can not be empty");
}
if (MaxRatio < 0 && MaxSeedTime < 0)
{
throw new ValidationException($"both {nameof(MaxRatio)} and {nameof(MaxSeedTime)} are disabled");
}
if (MinSeedTime < 0)
{
throw new ValidationException($"{nameof(MinSeedTime)} can not be negative");
}
}
}

View File

@@ -0,0 +1,39 @@
using Common.Exceptions;
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.DownloadCleaner;
public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
{
public const string SectionName = "DownloadCleaner";
public bool Enabled { get; init; }
public List<Category>? Categories { get; init; }
[ConfigurationKeyName("DELETE_PRIVATE")]
public bool DeletePrivate { get; init; }
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
public string? IgnoredDownloadsPath { get; init; }
public void Validate()
{
if (!Enabled)
{
return;
}
if (Categories?.Count is null or 0)
{
throw new ValidationException("no categories configured");
}
if (Categories?.GroupBy(x => x.Name).Any(x => x.Count() > 1) is true)
{
throw new ValidationException("duplicated categories found");
}
Categories?.ForEach(x => x.Validate());
}
}

View File

@@ -0,0 +1,20 @@
using Common.Exceptions;
namespace Common.Configuration.DownloadClient;
public sealed record DelugeConfig : IConfig
{
public const string SectionName = "Deluge";
public Uri? Url { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (Url is null)
{
throw new ValidationException($"{nameof(Url)} is empty");
}
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.DownloadClient;
public sealed record DownloadClientConfig
{
[ConfigurationKeyName("DOWNLOAD_CLIENT")]
public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.None;
}

View File

@@ -0,0 +1,22 @@
using Common.Exceptions;
namespace Common.Configuration.DownloadClient;
public sealed class QBitConfig : IConfig
{
public const string SectionName = "qBittorrent";
public Uri? Url { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (Url is null)
{
throw new ValidationException($"{nameof(Url)} is empty");
}
}
}

View File

@@ -0,0 +1,22 @@
using Common.Exceptions;
namespace Common.Configuration.DownloadClient;
public record TransmissionConfig : IConfig
{
public const string SectionName = "Transmission";
public Uri? Url { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (Url is null)
{
throw new ValidationException($"{nameof(Url)} is empty");
}
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.General;
public sealed record DryRunConfig
{
[ConfigurationKeyName("DRY_RUN")]
public bool IsDryRun { get; init; }
}

View File

@@ -0,0 +1,21 @@
using Common.Exceptions;
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.General;
public sealed record HttpConfig : IConfig
{
[ConfigurationKeyName("HTTP_MAX_RETRIES")]
public ushort MaxRetries { get; init; }
[ConfigurationKeyName("HTTP_TIMEOUT")]
public ushort Timeout { get; init; } = 100;
public void Validate()
{
if (Timeout is 0)
{
throw new ValidationException("HTTP_TIMEOUT must be greater than 0");
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Common.Configuration.General;
public sealed class TriggersConfig
{
public const string SectionName = "Triggers";
public required string QueueCleaner { get; init; }
public required string ContentBlocker { get; init; }
public required string DownloadCleaner { get; init; }
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities;
namespace Common.Configuration;
public interface IConfig
{

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public interface IIgnoredDownloadsConfig
{
string? IgnoredDownloadsPath { get; }
}

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public interface IJobConfig : IConfig
{
bool Enabled { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace Common.Configuration.Logging;
public class FileLogConfig : IConfig
{
public bool Enabled { get; set; }
public string Path { get; set; } = string.Empty;
public void Validate()
{
}
}

View File

@@ -0,0 +1,18 @@
using Serilog.Events;
namespace Common.Configuration.Logging;
public class LoggingConfig : IConfig
{
public const string SectionName = "Logging";
public LogEventLevel LogLevel { get; set; }
public bool Enhanced { get; set; }
public FileLogConfig? File { get; set; }
public void Validate()
{
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.Notification;
public abstract record NotificationConfig
{
[ConfigurationKeyName("ON_IMPORT_FAILED_STRIKE")]
public bool OnImportFailedStrike { get; init; }
[ConfigurationKeyName("ON_STALLED_STRIKE")]
public bool OnStalledStrike { get; init; }
[ConfigurationKeyName("ON_QUEUE_ITEM_DELETED")]
public bool OnQueueItemDeleted { get; init; }
[ConfigurationKeyName("ON_DOWNLOAD_CLEANED")]
public bool OnDownloadCleaned { get; init; }
public bool IsEnabled => OnImportFailedStrike || OnStalledStrike || OnQueueItemDeleted || OnDownloadCleaned;
public abstract bool IsValid();
}

View File

@@ -0,0 +1,53 @@
using Common.Exceptions;
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.QueueCleaner;
public sealed record QueueCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
{
public const string SectionName = "QueueCleaner";
public required bool Enabled { get; init; }
public required bool RunSequentially { get; init; }
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
public string? IgnoredDownloadsPath { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
public ushort ImportFailedMaxStrikes { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
public bool ImportFailedIgnorePrivate { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_DELETE_PRIVATE")]
public bool ImportFailedDeletePrivate { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
public List<string>? ImportFailedIgnorePatterns { get; init; }
[ConfigurationKeyName("STALLED_MAX_STRIKES")]
public ushort StalledMaxStrikes { get; init; }
[ConfigurationKeyName("STALLED_RESET_STRIKES_ON_PROGRESS")]
public bool StalledResetStrikesOnProgress { get; init; }
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
public bool StalledIgnorePrivate { get; init; }
[ConfigurationKeyName("STALLED_DELETE_PRIVATE")]
public bool StalledDeletePrivate { get; init; }
public void Validate()
{
if (ImportFailedMaxStrikes is > 0 and < 3)
{
throw new ValidationException("the minimum value for IMPORT_FAILED_MAX_STRIKES must be 3");
}
if (StalledMaxStrikes is > 0 and < 3)
{
throw new ValidationException("the minimum value for STALLED_MAX_STRIKES must be 3");
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Common.Enums;
public enum DownloadClient
{
QBittorrent,
Deluge,
Transmission,
None,
Disabled
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Exceptions;
namespace Common.Exceptions;
public sealed class ValidationException : Exception
{

View File

@@ -0,0 +1,9 @@
namespace Common.Helpers;
public static class Constants
{
public static readonly TimeSpan TriggerMaxLimit = TimeSpan.FromHours(6);
public static readonly TimeSpan CacheLimitBuffer = TimeSpan.FromHours(2);
public const string HttpClientWithRetryName = "retry";
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Shared.Helpers;
namespace Common.Helpers;
public static class StaticConfiguration
{

View File

@@ -1,82 +0,0 @@
# Build Angular frontend
FROM --platform=$BUILDPLATFORM node:24-alpine AS frontend-build
WORKDIR /app
# Copy package files first for better layer caching
COPY frontend/package*.json ./
# Use cache mount for npm to speed up builds
RUN --mount=type=cache,target=/root/.npm \
npm ci && npm install -g @angular/cli
# Copy source code
COPY frontend/ .
# Build with appropriate base-href and deploy-url
RUN npm run build
# Build .NET backend
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build
ARG TARGETARCH
ARG VERSION=0.0.1
ARG PACKAGES_USERNAME
ARG PACKAGES_PAT
WORKDIR /app
EXPOSE 11011
# Copy source code
COPY backend/ ./backend/
# Add NuGet source
RUN dotnet nuget add source --username ${PACKAGES_USERNAME} --password ${PACKAGES_PAT} --store-password-in-clear-text --name Cleanuparr https://nuget.pkg.github.com/Cleanuparr/index.json
# Restore and publish with cache mount
RUN --mount=type=cache,target=/root/.nuget/packages,sharing=locked \
dotnet restore ./backend/Cleanuparr.Api/Cleanuparr.Api.csproj -a $TARGETARCH && \
dotnet publish ./backend/Cleanuparr.Api/Cleanuparr.Api.csproj \
-a $TARGETARCH \
-c Release \
-o /app/publish \
--no-restore \
/p:Version=${VERSION} \
/p:PublishSingleFile=true \
/p:DebugSymbols=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
# Install required packages for user management, timezone support, and Python for Apprise CLI
RUN apt-get update && apt-get install -y \
curl \
tzdata \
gosu \
python3 \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment and install Apprise CLI
ENV VIRTUAL_ENV=/opt/apprise-venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip install --no-cache-dir apprise==1.9.6
ENV PUID=1000 \
PGID=1000 \
UMASK=022 \
TZ=Etc/UTC \
HTTP_PORTS=11011
# Fix FileSystemWatcher in Docker: https://github.com/dotnet/dotnet-docker/issues/3546
ENV DOTNET_USE_POLLING_FILE_WATCHER=true
WORKDIR /app
# Copy backend
COPY --from=build /app/publish .
# Copy frontend to wwwroot
COPY --from=frontend-build /app/dist/ui/browser ./wwwroot
# Copy entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["./Cleanuparr"]

View File

@@ -7,7 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Enums;
namespace Domain.Enums;
public enum CleanReason
{

View File

@@ -0,0 +1,8 @@
namespace Domain.Enums;
public enum DeleteReason
{
Stalled,
ImportFailed,
AllFilesBlocked
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace Domain.Enums;
public enum StrikeType
{
Stalled,
ImportFailed
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Arr.Blocking;
public record BlockedItem
{
public required string Hash { get; init; }
public required Uri InstanceUrl { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Arr.Blocking;
public sealed record LidarrBlockedItem : BlockedItem
{
public required long AlbumId { get; init; }
public required long ArtistId { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace Domain.Models.Arr.Blocking;
public sealed record RadarrBlockedItem : BlockedItem
{
public required long MovieId { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Domain.Models.Arr.Blocking;
public sealed record SonarrBlockedItem : BlockedItem
{
public required long EpisodeId { get; init; }
public required long SeasonNumber { get; init; }
public required long SeriesId { get; init; }
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public record Image
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public record LidarrImage
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public sealed record QueueAlbum
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public record QueueListResponse
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public sealed record QueueMovie
{

View File

@@ -1,8 +1,8 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public sealed record QueueRecord
{
// Sonarr and Whisparr
// Sonarr
public long SeriesId { get; init; }
public long EpisodeId { get; init; }
public long SeasonNumber { get; init; }
@@ -21,13 +21,6 @@ public sealed record QueueRecord
public QueueAlbum? Album { get; init; }
// Readarr
public long AuthorId { get; init; }
public long BookId { get; init; }
public QueueBook? Book { get; init; }
// common
public required string Title { get; init; }
public string Status { get; init; }

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public sealed record QueueSeries
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public sealed record TrackedDownloadStatusMessage
{

View File

@@ -1,4 +1,4 @@
namespace Data.Models.Arr;
namespace Domain.Models.Arr;
public class SearchItem
{

View File

@@ -1,17 +1,16 @@
using Cleanuparr.Domain.Enums;
using Data.Models.Arr;
using Common.Configuration.Arr;
namespace Cleanuparr.Domain.Entities.Arr;
namespace Domain.Models.Arr;
public sealed class SeriesSearchItem : SearchItem
public sealed class SonarrSearchItem : SearchItem
{
public long SeriesId { get; set; }
public SeriesSearchType SearchType { get; set; }
public SonarrSearchType SearchType { get; set; }
public override bool Equals(object? obj)
{
if (obj is not SeriesSearchItem other)
if (obj is not SonarrSearchItem other)
{
return false;
}

View File

@@ -1,6 +1,6 @@
namespace Cleanuparr.Domain.Entities.Cache;
namespace Domain.Models.Cache;
public sealed record StalledCacheItem
public sealed record CacheItem
{
/// <summary>
/// The amount of bytes that have been downloaded.

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Exceptions;
namespace Domain.Models.Deluge.Exceptions;
public class DelugeClientException : Exception
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Exceptions;
namespace Domain.Models.Deluge.Exceptions;
public sealed class DelugeLoginException : DelugeClientException
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Exceptions;
namespace Domain.Models.Deluge.Exceptions;
public sealed class DelugeLogoutException : DelugeClientException
{

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Request;
namespace Domain.Models.Deluge.Request;
public class DelugeRequest
{
@@ -8,19 +8,19 @@ public class DelugeRequest
public int RequestId { get; set; }
[JsonProperty(PropertyName = "method")]
public string Method { get; set; }
public String Method { get; set; }
[JsonProperty(PropertyName = "params")]
public List<object> Params { get; set; }
public List<Object> Params { get; set; }
[JsonIgnore]
public NullValueHandling NullValueHandling { get; set; }
public DelugeRequest(int requestId, string method, params object[]? parameters)
public DelugeRequest(int requestId, String method, params object[] parameters)
{
RequestId = requestId;
Method = method;
Params = [];
Params = new List<Object>();
if (parameters != null)
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public sealed record DelugeContents
{

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public sealed record DelugeError
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public class DelugeFileOrDirectory
{

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public sealed record DelugeResponse<T>
{

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public record DelugeTorrent
{

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public sealed record DelugeTorrentExtended : DelugeTorrent
{

View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json;
namespace Cleanuparr.Domain.Entities.Deluge.Response;
namespace Domain.Models.Deluge.Response;
public sealed record DownloadStatus
public sealed record TorrentStatus
{
public string? Hash { get; init; }
@@ -12,18 +12,12 @@ public sealed record DownloadStatus
public ulong Eta { get; init; }
[JsonProperty("download_payload_rate")]
public long DownloadSpeed { get; init; }
public bool Private { get; init; }
[JsonProperty("total_size")]
public long Size { get; init; }
[JsonProperty("total_done")]
public long TotalDone { get; init; }
public string? Label { get; set; }
public string? Label { get; init; }
[JsonProperty("seeding_time")]
public long SeedingTime { get; init; }
@@ -31,12 +25,9 @@ public sealed record DownloadStatus
public float Ratio { get; init; }
public required IReadOnlyList<Tracker> Trackers { get; init; }
[JsonProperty("download_location")]
public required string DownloadLocation { get; init; }
}
public sealed record Tracker
{
public required string Url { get; init; }
public required Uri Url { get; init; }
}

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Lidarr;
namespace Domain.Models.Lidarr;
public sealed record Album
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Lidarr;
namespace Domain.Models.Lidarr;
public sealed record Artist
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Lidarr;
namespace Domain.Models.Lidarr;
public sealed record LidarrCommand
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Radarr;
namespace Domain.Models.Radarr;
public sealed record Movie
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Radarr;
namespace Domain.Models.Radarr;
public sealed record RadarrCommand
{

View File

@@ -1,4 +1,4 @@
namespace Cleanuparr.Domain.Entities.Sonarr;
namespace Domain.Models.Sonarr;
public sealed record Episode
{

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