diff --git a/.github/ISSUE_TEMPLATE/1-bug.yml b/.github/ISSUE_TEMPLATE/1-bug.yml
index ee50073b..731cc971 100644
--- a/.github/ISSUE_TEMPLATE/1-bug.yml
+++ b/.github/ISSUE_TEMPLATE/1-bug.yml
@@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
- Thanks for taking the time to improve cleanuperr!
+ Thanks for taking the time to improve Cleanuparr!
- type: checkboxes
id: init
attributes:
@@ -14,7 +14,7 @@ body:
options:
- label: Reviewed the documentation.
required: true
- - label: Ensured I am using ghcr.io/flmorg/cleanuperr docker repository.
+ - label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true
diff --git a/.github/ISSUE_TEMPLATE/2-feature.yml b/.github/ISSUE_TEMPLATE/2-feature.yml
index 2496f7ff..1d6bcd95 100644
--- a/.github/ISSUE_TEMPLATE/2-feature.yml
+++ b/.github/ISSUE_TEMPLATE/2-feature.yml
@@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
- Thanks for taking the time to improve cleanuperr!
+ Thanks for taking the time to improve Cleanuparr!
- type: textarea
id: description
attributes:
diff --git a/.github/ISSUE_TEMPLATE/3-help.yml b/.github/ISSUE_TEMPLATE/3-help.yml
index 2e7828c4..5dc31004 100644
--- a/.github/ISSUE_TEMPLATE/3-help.yml
+++ b/.github/ISSUE_TEMPLATE/3-help.yml
@@ -14,7 +14,7 @@ body:
options:
- label: Reviewed the documentation.
required: true
- - label: Ensured I am using ghcr.io/flmorg/cleanuperr docker repository.
+ - label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true
diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml
new file mode 100644
index 00000000..0816a111
--- /dev/null
+++ b/.github/workflows/build-docker.yml
@@ -0,0 +1,125 @@
+name: Build Docker Images
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ pull_request:
+ paths:
+ - 'code/**'
+ workflow_dispatch:
+ workflow_call:
+
+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 }}
+ latestDockerTag=""
+ versionDockerTag=""
+ version="0.0.1"
+
+ if [[ "$githubRef" =~ ^"refs/tags/" ]]; then
+ branch=${githubRef##*/}
+ latestDockerTag="latest"
+ versionDockerTag=${branch#v}
+ version=${branch#v}
+ 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:$latestDockerTag"
+ fi
+
+ if [ -n "$versionDockerTag" ]; then
+ githubTags="$githubTags,ghcr.io/cleanuparr:$versionDockerTag"
+ 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
+ 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=${{ env.PACKAGES_USERNAME }}
+ PACKAGES_PAT=${{ env.PACKAGES_PAT }}
+ outputs: |
+ type=image
+ platforms: |
+ linux/amd64
+ linux/arm64
+ push: true
+ tags: |
+ ${{ env.githubTags }}
\ No newline at end of file
diff --git a/.github/workflows/build-executable.yml b/.github/workflows/build-executable.yml
new file mode 100644
index 00000000..ed26f570
--- /dev/null
+++ b/.github/workflows/build-executable.yml
@@ -0,0 +1,177 @@
+name: Build Executables
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Gate
+ if: ${{ !startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch' }}
+ run: |
+ echo "This workflow only runs on tag events or manual dispatch. Pipeline finished."
+ exit 0
+
+ - name: Set variables
+ run: |
+ repoFullName=${{ github.repository }}
+ ref=${{ github.ref }}
+
+ # Handle both tag events and manual dispatch
+ if [[ "$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
+
+ echo "githubRepository=${{ github.repository }}" >> $GITHUB_ENV
+ echo "githubRepositoryName=${repoFullName#*/}" >> $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 Node.js for frontend build
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: code/frontend/package-lock.json
+
+ - name: Build frontend
+ run: |
+ cd code/frontend
+ npm ci
+ npm run build
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+
+ - name: Install dependencies and restore
+ run: |
+ dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.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 win-x64
+ run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime win-x64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
+
+ - name: Build linux-x64
+ run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime linux-x64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
+
+ - name: Build linux-arm64
+ run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime linux-arm64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
+
+ - name: Build osx-x64
+ run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime osx-x64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
+
+ - name: Build osx-arm64
+ run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime osx-arm64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
+
+ - name: Create sample configuration files
+ run: |
+ # Create a sample appsettings.json for each platform
+ cat > sample-config.json << 'EOF'
+ {
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+ }
+ EOF
+
+ # Copy to each build directory
+ cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64/appsettings.json
+ cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64/appsettings.json
+ cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64/appsettings.json
+ cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64/appsettings.json
+ cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64/appsettings.json
+
+ - name: Zip win-x64
+ run: |
+ cd ./artifacts
+ zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64/
+
+ - name: Zip linux-x64
+ run: |
+ cd ./artifacts
+ zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64/
+
+ - name: Zip linux-arm64
+ run: |
+ cd ./artifacts
+ zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64/
+
+ - name: Zip osx-x64
+ run: |
+ cd ./artifacts
+ zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64/
+
+ - name: Zip osx-arm64
+ run: |
+ cd ./artifacts
+ zip -r ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64.zip ./${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64/
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: cleanuparr-executables
+ path: |
+ ./artifacts/*.zip
+ retention-days: 30
+
+ - name: Release
+ if: startsWith(github.ref, 'refs/tags/')
+ id: release
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ${{ env.releaseVersion }}
+ tag_name: ${{ env.releaseVersion }}
+ repository: ${{ env.githubRepository }}
+ token: ${{ env.REPO_READONLY_PAT }}
+ make_latest: true
+ fail_on_unmatched_files: true
+ target_commitish: main
+ generate_release_notes: true
+ files: |
+ ./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64.zip
+ ./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64.zip
+ ./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64.zip
+ ./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64.zip
+ ./artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64.zip
\ No newline at end of file
diff --git a/.github/workflows/build-macos-arm-installer.yml b/.github/workflows/build-macos-arm-installer.yml
new file mode 100644
index 00000000..fbf9ed6a
--- /dev/null
+++ b/.github/workflows/build-macos-arm-installer.yml
@@ -0,0 +1,376 @@
+name: Build macOS ARM Installer
+
+permissions:
+ contents: write
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ build-macos-arm-installer:
+ name: Build macOS ARM Installer
+ runs-on: macos-14 # ARM runner for Apple Silicon
+
+ steps:
+ - name: Set variables
+ run: |
+ repoFullName=${{ github.repository }}
+ ref=${{ github.ref }}
+
+ # Handle both tag events and manual dispatch
+ if [[ "$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: Setup Node.js for frontend build
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: code/frontend/package-lock.json
+
+ - name: Build frontend
+ run: |
+ cd code/frontend
+ npm ci
+ npm run build
+
+ - 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 ARM 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 osx-arm64 \
+ --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
+
+
+
+
+ Label
+ com.cleanuparr.daemon
+ ProgramArguments
+
+ /Applications/Cleanuparr.app/Contents/MacOS/Cleanuparr
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /var/log/cleanuparr.log
+ StandardErrorPath
+ /var/log/cleanuparr.error.log
+ WorkingDirectory
+ /Applications/Cleanuparr.app/Contents/MacOS
+ EnvironmentVariables
+
+ HTTP_PORTS
+ 11011
+
+
+
+ EOF
+
+ # Create Info.plist with proper configuration
+ cat > dist/Cleanuparr.app/Contents/Info.plist << EOF
+
+
+
+
+ CFBundleExecutable
+ Cleanuparr
+ CFBundleIdentifier
+ com.Cleanuparr
+ CFBundleName
+ Cleanuparr
+ CFBundleDisplayName
+ Cleanuparr
+ CFBundleVersion
+ ${{ env.appVersion }}
+ CFBundleShortVersionString
+ ${{ env.appVersion }}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ CLNR
+ CFBundleIconFile
+ Cleanuparr
+ NSHighResolutionCapable
+
+ NSRequiresAquaSystemAppearance
+
+ LSMinimumSystemVersion
+ 11.0
+ LSApplicationCategoryType
+ public.app-category.productivity
+ NSSupportsAutomaticTermination
+
+ NSSupportsSuddenTermination
+
+ LSBackgroundOnly
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+
+
+ 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 [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
+ pkg_name="Cleanuparr-${{ env.appVersion }}-macos-arm64.pkg"
+ else
+ pkg_name="Cleanuparr-${{ env.appVersion }}-macos-arm64-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-arm64-installer
+ path: '${{ env.pkgName }}'
+ retention-days: 30
+
+ - name: Release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ${{ env.releaseVersion }}
+ tag_name: ${{ env.releaseVersion }}
+ repository: ${{ env.githubRepository }}
+ token: ${{ env.REPO_READONLY_PAT }}
+ make_latest: true
+ files: |
+ ${{ env.pkgName }}
\ No newline at end of file
diff --git a/.github/workflows/build-macos-intel-installer.yml b/.github/workflows/build-macos-intel-installer.yml
new file mode 100644
index 00000000..8cad0e35
--- /dev/null
+++ b/.github/workflows/build-macos-intel-installer.yml
@@ -0,0 +1,376 @@
+name: Build macOS Intel Installer
+
+permissions:
+ contents: write
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ build-macos-intel-installer:
+ name: Build macOS Intel Installer
+ runs-on: macos-13 # Intel runner
+
+ steps:
+ - name: Set variables
+ run: |
+ repoFullName=${{ github.repository }}
+ ref=${{ github.ref }}
+
+ # Handle both tag events and manual dispatch
+ if [[ "$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: Setup Node.js for frontend build
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: code/frontend/package-lock.json
+
+ - name: Build frontend
+ run: |
+ cd code/frontend
+ npm ci
+ npm run build
+
+ - 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 Intel 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 osx-x64 \
+ --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
+
+
+
+
+ Label
+ com.cleanuparr.daemon
+ ProgramArguments
+
+ /Applications/Cleanuparr.app/Contents/MacOS/Cleanuparr
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /var/log/cleanuparr.log
+ StandardErrorPath
+ /var/log/cleanuparr.error.log
+ WorkingDirectory
+ /Applications/Cleanuparr.app/Contents/MacOS
+ EnvironmentVariables
+
+ HTTP_PORTS
+ 11011
+
+
+
+ EOF
+
+ # Create Info.plist with proper configuration
+ cat > dist/Cleanuparr.app/Contents/Info.plist << EOF
+
+
+
+
+ CFBundleExecutable
+ Cleanuparr
+ CFBundleIdentifier
+ com.Cleanuparr
+ CFBundleName
+ Cleanuparr
+ CFBundleDisplayName
+ Cleanuparr
+ CFBundleVersion
+ ${{ env.appVersion }}
+ CFBundleShortVersionString
+ ${{ env.appVersion }}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ CLNR
+ CFBundleIconFile
+ Cleanuparr
+ NSHighResolutionCapable
+
+ NSRequiresAquaSystemAppearance
+
+ LSMinimumSystemVersion
+ 10.15
+ LSApplicationCategoryType
+ public.app-category.productivity
+ NSSupportsAutomaticTermination
+
+ NSSupportsSuddenTermination
+
+ LSBackgroundOnly
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+
+
+ 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 [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
+ pkg_name="Cleanuparr-${{ env.appVersion }}-macos-intel.pkg"
+ else
+ pkg_name="Cleanuparr-${{ env.appVersion }}-macos-intel-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-intel-installer
+ path: '${{ env.pkgName }}'
+ retention-days: 30
+
+ - name: Release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ${{ env.releaseVersion }}
+ tag_name: ${{ env.releaseVersion }}
+ repository: ${{ env.githubRepository }}
+ token: ${{ env.REPO_READONLY_PAT }}
+ make_latest: true
+ files: |
+ ${{ env.pkgName }}
\ No newline at end of file
diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml
new file mode 100644
index 00000000..4843b15b
--- /dev/null
+++ b/.github/workflows/build-windows-installer.yml
@@ -0,0 +1,171 @@
+name: Build Windows Installer
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ build-windows-installer:
+ runs-on: windows-latest
+
+ steps:
+ - name: Set variables
+ shell: pwsh
+ run: |
+ $repoFullName = "${{ github.repository }}"
+ $ref = "${{ github.ref }}"
+
+ # Handle both tag events and manual dispatch
+ if ($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 "executableName=Cleanuparr.Api" >> $env:GITHUB_ENV
+ echo "APP_VERSION=$appVersion" >> $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: Setup Node.js for frontend build
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: code/frontend/package-lock.json
+
+ - name: Build frontend
+ run: |
+ cd code/frontend
+ npm ci
+ npm run build
+
+ - 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: Create sample configuration
+ shell: pwsh
+ run: |
+ # Create config directory
+ New-Item -ItemType Directory -Force -Path "config"
+
+ $config = @{
+ "HTTP_PORTS" = 11011
+ "BASE_PATH" = "/"
+ }
+
+ $config | ConvertTo-Json | Out-File -FilePath "config/cleanuparr.json" -Encoding UTF8
+
+ - name: Setup Inno Setup
+ 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
+
+ - name: Release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ${{ env.releaseVersion }}
+ tag_name: ${{ env.releaseVersion }}
+ repository: ${{ env.githubRepository }}
+ token: ${{ env.REPO_READONLY_PAT }}
+ make_latest: true
+ files: |
+ installer/${{ env.installerName }}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 6a5e4aad..00000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-on:
- workflow_dispatch:
- workflow_call:
-
-jobs:
- build:
- uses: flmorg/universal-workflows-testing/.github/workflows/dotnet.build.app.yml@main
- with:
- dockerRepository: flaminel/cleanuperr
- githubContext: ${{ toJSON(github) }}
- outputName: cleanuperr
- selfContained: false
- baseImage: 9.0-bookworm-slim
- secrets: inherit
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index 2c632ff2..00000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-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
\ No newline at end of file
diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
deleted file mode 100644
index 640e9382..00000000
--- a/.github/workflows/pipeline.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-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
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 37b246d3..d287b856 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,11 +1,164 @@
+name: Release Build
+
on:
push:
tags:
- "v*.*.*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to release (e.g., 1.0.0)'
+ required: false
+ default: ''
jobs:
- release:
- uses: flmorg/universal-workflows/.github/workflows/dotnet.release.yml@main
- with:
- githubContext: ${{ toJSON(github) }}
- secrets: inherit
\ No newline at end of file
+ # 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
+ elif [[ -n "${{ github.event.inputs.version }}" ]]; then
+ # Manual workflow with version
+ app_version="${{ github.event.inputs.version }}"
+ release_version="v$app_version"
+ is_tag=false
+ else
+ # Manual workflow without version
+ 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 "๐ท๏ธ Release Version: $release_version"
+ echo "๐ฑ App Version: $app_version"
+ echo "๐ Is Tag: $is_tag"
+
+ # Build portable executables
+ build-executables:
+ needs: validate
+ uses: ./.github/workflows/build_executable.yml
+ secrets: inherit
+
+ # Build Windows installer
+ build-windows-installer:
+ needs: validate
+ uses: ./.github/workflows/build-windows-installer.yml
+ secrets: inherit
+
+ # Build macOS Intel installer
+ build-macos-intel:
+ needs: validate
+ uses: ./.github/workflows/build-macos-intel-installer.yml
+ secrets: inherit
+
+ # Build macOS ARM installer
+ build-macos-arm:
+ needs: validate
+ uses: ./.github/workflows/build-macos-arm-installer.yml
+ secrets: inherit
+
+ # Create GitHub release
+ create-release:
+ needs: [validate, build-executables, build-windows-installer, build-macos-intel, build-macos-arm]
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+
+ 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 all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: ./artifacts
+
+ - name: List downloaded artifacts
+ run: |
+ echo "๐ฆ Downloaded artifacts:"
+ find ./artifacts -type f -name "*.zip" -o -name "*.pkg" -o -name "*.exe" | sort
+
+ - name: Create release
+ uses: softprops/action-gh-release@v2
+ with:
+ name: Cleanuparr ${{ needs.validate.outputs.release_version }}
+ tag_name: ${{ needs.validate.outputs.release_version }}
+ token: ${{ env.REPO_READONLY_PAT }}
+ make_latest: true
+ generate_release_notes: true
+ prerelease: ${{ contains(needs.validate.outputs.app_version, '-') }}
+ files: |
+ ./artifacts/**/*.zip
+ ./artifacts/**/*.pkg
+ ./artifacts/**/*.exe
+
+ # Summary job
+ summary:
+ needs: [validate, build-executables, build-windows-installer, build-macos-intel, build-macos-arm]
+ runs-on: ubuntu-latest
+ if: always()
+
+ steps:
+ - name: Build Summary
+ run: |
+ 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 "" >> $GITHUB_STEP_SUMMARY
+ echo "### Build Results" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Check job results
+ if [[ "${{ needs.build-executables.result }}" == "success" ]]; then
+ echo "โ
**Portable Executables**: Success" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "โ **Portable Executables**: ${{ needs.build-executables.result }}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [[ "${{ needs.build-windows-installer.result }}" == "success" ]]; then
+ echo "โ
**Windows Installer**: Success" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "โ **Windows Installer**: ${{ needs.build-windows-installer.result }}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [[ "${{ needs.build-macos-intel.result }}" == "success" ]]; then
+ echo "โ
**macOS Intel Installer**: Success" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "โ **macOS Intel Installer**: ${{ needs.build-macos-intel.result }}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [[ "${{ needs.build-macos-arm.result }}" == "success" ]]; then
+ echo "โ
**macOS ARM Installer**: Success" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "โ **macOS ARM Installer**: ${{ needs.build-macos-arm.result }}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "๐ **Build completed!**" >> $GITHUB_STEP_SUMMARY
\ No newline at end of file
diff --git a/Logo/favicon.ico b/Logo/favicon.ico
new file mode 100644
index 00000000..c82f0265
Binary files /dev/null and b/Logo/favicon.ico differ
diff --git a/Logo/cleanuperr.svg b/Logo/logo.svg
similarity index 100%
rename from Logo/cleanuperr.svg
rename to Logo/logo.svg
diff --git a/README.md b/README.md
index 74ddd25e..6c3df31e 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,35 @@
_Love this project? Give it a โญ๏ธ and let others know!_
-#
Cleanuperr
+#
Cleanuparr
[](https://discord.gg/SCtMCgtsc4)
-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.
+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.
-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/).
+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/).
> [!IMPORTANT]
> **Features:**
> - Strike system to mark bad downloads.
> - Remove and block downloads that reached a maximum number of strikes.
-> - Remove and block downloads that are **failing to be imported** by the arrs. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/import-failed)
-> - Remove and block downloads that are **stalled** or in **metadata downloading** state. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/stalled)
-> - Remove and block downloads that have a **low download speed** or **high estimated completion time**. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/slow)
-> - Remove and block downloads blocked by qBittorrent or by Cleanuperr's **Content Blocker**. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/content-blocker/general)
+> - Remove and block downloads that are **failing to be imported** by the arrs. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/import-failed)
+> - Remove and block downloads that are **stalled** or in **metadata downloading** state. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/stalled)
+> - Remove and block downloads that have a **low download speed** or **high estimated completion time**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/slow)
+> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/content-blocker/general)
> - Automatically trigger a search for downloads removed from the arrs.
-> - Clean up downloads that have been **seeding** for a certain amount of time. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/download-cleaner/seeding)
-> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/download-cleaner/hardlinks)
-> - Notify on strike or download removal. [configuration](https://flmorg.github.io/cleanuperr/docs/category/notifications)
-> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuperr.
+> - Clean up downloads that have been **seeding** for a certain amount of time. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/seeding)
+> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/hardlinks)
+> - Notify on strike or download removal. [configuration](https://cleanuparr.github.io/cleanuparr/docs/category/notifications)
+> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuparr.
-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.
+Cleanuparr supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
## Quick Start
> [!NOTE]
>
> 1. **Docker (Recommended)**
-> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
+> Pull the Docker image from `ghcr.io/Cleanuparr/Cleanuparr:latest`.
>
> 2. **Unraid (for Unraid users)**
> Use the Unraid Community App.
@@ -39,13 +39,13 @@ Cleanuperr supports both qBittorrent's built-in exclusion features and its own b
# Docs
-Docs can be found [here](https://flmorg.github.io/cleanuperr/).
+Docs can be found [here](https://Cleanuparr.github.io/Cleanuparr/).
-#
Cleanuperr
Huntarr
+#
Cleanuparr
Huntarr
-Think of **Cleanuperr** 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
+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
-While **Huntarr** fills in the blanks and improves what you already have, **Cleanuperr** makes sure that only clean downloads get through. If you're aiming for a reliable and self-sufficient setup, **Cleanuperr** and **Huntarr** will take your automated media stack to another level.
+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.
โก๏ธ [**Huntarr**](https://github.com/plexguide/Huntarr.io) 
diff --git a/chart/values.yaml b/chart/values.yaml
deleted file mode 100644
index f6835a79..00000000
--- a/chart/values.yaml
+++ /dev/null
@@ -1,187 +0,0 @@
-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: Verbose
- - 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: "true"
- - name: DOWNLOADCLEANER__DELETE_PRIVATE
- value: "true"
- - name: DOWNLOADCLEANER__CATEGORIES__0__NAME
- value: unlinked
- - name: DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME
- value: "240"
-
- - name: DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY
- value: unlinked
- - name: DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR
- value: /downloads
- - name: DOWNLOADCLEANER__UNLINKED_CATEGORIES__0
- value: sonarr-low
- - name: DOWNLOADCLEANER__UNLINKED_CATEGORIES__1
- value: sonarr-high
- - name: DOWNLOADCLEANER__UNLINKED_CATEGORIES__2
- value: radarr-low
- - name: DOWNLOADCLEANER__UNLINKED_CATEGORIES__3
- value: radarr-high
-
- - 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
- - name: storage
- mountPath: /downloads/general2
- subPath: media/downloads/general2
- - name: storage
- mountPath: /downloads/cross-seed
- subPath: media/downloads/cross-seed
- 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 %}"
\ No newline at end of file
diff --git a/code/.dockerignore b/code/.dockerignore
new file mode 100644
index 00000000..f95c9d74
--- /dev/null
+++ b/code/.dockerignore
@@ -0,0 +1,41 @@
+# 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/
\ No newline at end of file
diff --git a/code/Common/Attributes/DryRunSafeguardAttribute.cs b/code/Common/Attributes/DryRunSafeguardAttribute.cs
deleted file mode 100644
index 2f5fa091..00000000
--- a/code/Common/Attributes/DryRunSafeguardAttribute.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Attributes;
-
-[AttributeUsage(AttributeTargets.Method, Inherited = true)]
-public class DryRunSafeguardAttribute : Attribute
-{
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Arr/ArrConfig.cs b/code/Common/Configuration/Arr/ArrConfig.cs
deleted file mode 100644
index 6863084c..00000000
--- a/code/Common/Configuration/Arr/ArrConfig.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Common.Configuration.ContentBlocker;
-using Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.Arr;
-
-public abstract record ArrConfig
-{
- public required bool Enabled { get; init; }
-
- public Block Block { get; init; } = new();
-
- [ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
- public short ImportFailedMaxStrikes { get; init; } = -1;
-
- public required List Instances { get; init; }
-}
-
-public readonly record struct Block
-{
- public BlocklistType Type { get; init; }
-
- public string? Path { get; init; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Arr/ArrInstance.cs b/code/Common/Configuration/Arr/ArrInstance.cs
deleted file mode 100644
index eff7e62b..00000000
--- a/code/Common/Configuration/Arr/ArrInstance.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-๏ปฟnamespace Common.Configuration.Arr;
-
-public sealed class ArrInstance
-{
- public required Uri Url { get; set; }
-
- public required string ApiKey { get; set; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Arr/LidarrConfig.cs b/code/Common/Configuration/Arr/LidarrConfig.cs
deleted file mode 100644
index 33be0ec0..00000000
--- a/code/Common/Configuration/Arr/LidarrConfig.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Configuration.Arr;
-
-public sealed record LidarrConfig : ArrConfig
-{
- public const string SectionName = "Lidarr";
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Arr/RadarrConfig.cs b/code/Common/Configuration/Arr/RadarrConfig.cs
deleted file mode 100644
index 6f24c570..00000000
--- a/code/Common/Configuration/Arr/RadarrConfig.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Configuration.Arr;
-
-public sealed record RadarrConfig : ArrConfig
-{
- public const string SectionName = "Radarr";
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Arr/SonarrConfig.cs b/code/Common/Configuration/Arr/SonarrConfig.cs
deleted file mode 100644
index 97518cfb..00000000
--- a/code/Common/Configuration/Arr/SonarrConfig.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-๏ปฟnamespace Common.Configuration.Arr;
-
-public sealed record SonarrConfig : ArrConfig
-{
- public const string SectionName = "Sonarr";
-
- public SonarrSearchType SearchType { get; init; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs b/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs
deleted file mode 100644
index 4b071eff..00000000
--- a/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-๏ปฟ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()
- {
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadCleaner/CleanCategory.cs b/code/Common/Configuration/DownloadCleaner/CleanCategory.cs
deleted file mode 100644
index 48574cfe..00000000
--- a/code/Common/Configuration/DownloadCleaner/CleanCategory.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-๏ปฟusing Common.Exceptions;
-using Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.DownloadCleaner;
-
-public sealed record CleanCategory : IConfig
-{
- public required string Name { get; init; }
-
- ///
- /// Max ratio before removing a download.
- ///
- [ConfigurationKeyName("MAX_RATIO")]
- public required double MaxRatio { get; init; } = -1;
-
- ///
- /// Min number of hours to seed before removing a download, if the ratio has been met.
- ///
- [ConfigurationKeyName("MIN_SEED_TIME")]
- public required double MinSeedTime { get; init; } = 0;
-
- ///
- /// Number of hours to seed before removing a download.
- ///
- [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");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs b/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs
deleted file mode 100644
index 4bfd4100..00000000
--- a/code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-๏ปฟ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? Categories { get; init; }
-
- [ConfigurationKeyName("DELETE_PRIVATE")]
- public bool DeletePrivate { get; init; }
-
- [ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
- public string? IgnoredDownloadsPath { get; init; }
-
- [ConfigurationKeyName("UNLINKED_TARGET_CATEGORY")]
- public string UnlinkedTargetCategory { get; init; } = "cleanuperr-unlinked";
-
- [ConfigurationKeyName("UNLINKED_USE_TAG")]
- public bool UnlinkedUseTag { get; init; }
-
- [ConfigurationKeyName("UNLINKED_IGNORED_ROOT_DIR")]
- public string UnlinkedIgnoredRootDir { get; init; } = string.Empty;
-
- [ConfigurationKeyName("UNLINKED_CATEGORIES")]
- public List? UnlinkedCategories { get; init; }
-
- public void Validate()
- {
- if (!Enabled)
- {
- return;
- }
-
- if (Categories?.GroupBy(x => x.Name).Any(x => x.Count() > 1) is true)
- {
- throw new ValidationException("duplicated clean categories found");
- }
-
- Categories?.ForEach(x => x.Validate());
-
- if (string.IsNullOrEmpty(UnlinkedTargetCategory))
- {
- return;
- }
-
- if (UnlinkedCategories?.Count is null or 0)
- {
- throw new ValidationException("no unlinked categories configured");
- }
-
- if (UnlinkedCategories.Contains(UnlinkedTargetCategory))
- {
- throw new ValidationException($"{SectionName.ToUpperInvariant()}__UNLINKED_TARGET_CATEGORY should not be present in {SectionName.ToUpperInvariant()}__UNLINKED_CATEGORIES");
- }
-
- if (UnlinkedCategories.Any(string.IsNullOrEmpty))
- {
- throw new ValidationException("empty unlinked category filter found");
- }
-
- if (!string.IsNullOrEmpty(UnlinkedIgnoredRootDir) && !Directory.Exists(UnlinkedIgnoredRootDir))
- {
- throw new ValidationException($"{UnlinkedIgnoredRootDir} root directory does not exist");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadClient/DelugeConfig.cs b/code/Common/Configuration/DownloadClient/DelugeConfig.cs
deleted file mode 100644
index 59033a28..00000000
--- a/code/Common/Configuration/DownloadClient/DelugeConfig.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Common.Exceptions;
-using Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.DownloadClient;
-
-public sealed record DelugeConfig : IConfig
-{
- public const string SectionName = "Deluge";
-
- public Uri? Url { get; init; }
-
- [ConfigurationKeyName("URL_BASE")]
- public string UrlBase { get; init; } = string.Empty;
-
- public string? Password { get; init; }
-
- public void Validate()
- {
- if (Url is null)
- {
- throw new ValidationException($"{nameof(Url)} is empty");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs b/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs
deleted file mode 100644
index 73b17087..00000000
--- a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-๏ปฟusing Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.DownloadClient;
-
-public sealed record DownloadClientConfig
-{
- [ConfigurationKeyName("DOWNLOAD_CLIENT")]
- public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.None;
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadClient/QBitConfig.cs b/code/Common/Configuration/DownloadClient/QBitConfig.cs
deleted file mode 100644
index 7de0fafb..00000000
--- a/code/Common/Configuration/DownloadClient/QBitConfig.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Common.Exceptions;
-using Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.DownloadClient;
-
-public sealed class QBitConfig : IConfig
-{
- public const string SectionName = "qBittorrent";
-
- public Uri? Url { get; init; }
-
- [ConfigurationKeyName("URL_BASE")]
- public string UrlBase { get; init; } = string.Empty;
-
- public string? Username { get; init; }
-
- public string? Password { get; init; }
-
- public void Validate()
- {
- if (Url is null)
- {
- throw new ValidationException($"{nameof(Url)} is empty");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/DownloadClient/TransmissionConfig.cs b/code/Common/Configuration/DownloadClient/TransmissionConfig.cs
deleted file mode 100644
index 4d30b626..00000000
--- a/code/Common/Configuration/DownloadClient/TransmissionConfig.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Common.Exceptions;
-using Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.DownloadClient;
-
-public record TransmissionConfig : IConfig
-{
- public const string SectionName = "Transmission";
-
- public Uri? Url { get; init; }
-
- [ConfigurationKeyName("URL_BASE")]
- public string UrlBase { get; init; } = "transmission";
-
- public string? Username { get; init; }
-
- public string? Password { get; init; }
-
- public void Validate()
- {
- if (Url is null)
- {
- throw new ValidationException($"{nameof(Url)} is empty");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/General/DryRunConfig.cs b/code/Common/Configuration/General/DryRunConfig.cs
deleted file mode 100644
index 22b9c419..00000000
--- a/code/Common/Configuration/General/DryRunConfig.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-๏ปฟusing Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.General;
-
-public sealed record DryRunConfig
-{
- [ConfigurationKeyName("DRY_RUN")]
- public bool IsDryRun { get; init; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/General/HttpConfig.cs b/code/Common/Configuration/General/HttpConfig.cs
deleted file mode 100644
index 60c3f05b..00000000
--- a/code/Common/Configuration/General/HttpConfig.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-๏ปฟusing Common.Enums;
-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;
-
- [ConfigurationKeyName("HTTP_VALIDATE_CERT")]
- public CertificateValidationType CertificateValidation { get; init; } = CertificateValidationType.Enabled;
-
- public void Validate()
- {
- if (Timeout is 0)
- {
- throw new ValidationException("HTTP_TIMEOUT must be greater than 0");
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/General/SearchConfig.cs b/code/Common/Configuration/General/SearchConfig.cs
deleted file mode 100644
index 4439b187..00000000
--- a/code/Common/Configuration/General/SearchConfig.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-๏ปฟusing Microsoft.Extensions.Configuration;
-
-namespace Common.Configuration.General;
-
-public sealed record SearchConfig
-{
- [ConfigurationKeyName("SEARCH_ENABLED")]
- public bool SearchEnabled { get; init; } = true;
-
- [ConfigurationKeyName("SEARCH_DELAY")]
- public ushort SearchDelay { get; init; } = 30;
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/General/TriggersConfig.cs b/code/Common/Configuration/General/TriggersConfig.cs
deleted file mode 100644
index 77aee797..00000000
--- a/code/Common/Configuration/General/TriggersConfig.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-๏ปฟ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; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/IConfig.cs b/code/Common/Configuration/IConfig.cs
deleted file mode 100644
index b653cb3d..00000000
--- a/code/Common/Configuration/IConfig.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Configuration;
-
-public interface IConfig
-{
- void Validate();
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/IIgnoredDownloadsConfig.cs b/code/Common/Configuration/IIgnoredDownloadsConfig.cs
deleted file mode 100644
index f08e445a..00000000
--- a/code/Common/Configuration/IIgnoredDownloadsConfig.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Configuration;
-
-public interface IIgnoredDownloadsConfig
-{
- string? IgnoredDownloadsPath { get; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/IJobConfig.cs b/code/Common/Configuration/IJobConfig.cs
deleted file mode 100644
index 1ef7a8f1..00000000
--- a/code/Common/Configuration/IJobConfig.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Common.Configuration;
-
-public interface IJobConfig : IConfig
-{
- bool Enabled { get; init; }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Logging/FileLogConfig.cs b/code/Common/Configuration/Logging/FileLogConfig.cs
deleted file mode 100644
index 17d3ed77..00000000
--- a/code/Common/Configuration/Logging/FileLogConfig.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-๏ปฟnamespace Common.Configuration.Logging;
-
-public class FileLogConfig : IConfig
-{
- public bool Enabled { get; set; }
-
- public string Path { get; set; } = string.Empty;
-
- public void Validate()
- {
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/Logging/LoggingConfig.cs b/code/Common/Configuration/Logging/LoggingConfig.cs
deleted file mode 100644
index 187a3911..00000000
--- a/code/Common/Configuration/Logging/LoggingConfig.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-๏ปฟ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()
- {
- }
-}
\ No newline at end of file
diff --git a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
deleted file mode 100644
index b8f746b8..00000000
--- a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-๏ปฟusing Common.CustomDataTypes;
-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 IReadOnlyList? 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; }
-
- [ConfigurationKeyName("DOWNLOADING_METADATA_MAX_STRIKES")]
- public ushort DownloadingMetadataMaxStrikes { get; init; }
-
- [ConfigurationKeyName("SLOW_MAX_STRIKES")]
- public ushort SlowMaxStrikes { get; init; }
-
- [ConfigurationKeyName("SLOW_RESET_STRIKES_ON_PROGRESS")]
- public bool SlowResetStrikesOnProgress { get; init; }
-
- [ConfigurationKeyName("SLOW_IGNORE_PRIVATE")]
- public bool SlowIgnorePrivate { get; init; }
-
- [ConfigurationKeyName("SLOW_DELETE_PRIVATE")]
- public bool SlowDeletePrivate { get; init; }
-
- [ConfigurationKeyName("SLOW_MIN_SPEED")]
- public string SlowMinSpeed { get; init; } = string.Empty;
-
- public ByteSize SlowMinSpeedByteSize => string.IsNullOrEmpty(SlowMinSpeed) ? new ByteSize(0) : ByteSize.Parse(SlowMinSpeed);
-
- [ConfigurationKeyName("SLOW_MAX_TIME")]
- public double SlowMaxTime { get; init; }
-
- [ConfigurationKeyName("SLOW_IGNORE_ABOVE_SIZE")]
- public string SlowIgnoreAboveSize { get; init; } = string.Empty;
-
- public ByteSize? SlowIgnoreAboveSizeByteSize => string.IsNullOrEmpty(SlowIgnoreAboveSize) ? null : ByteSize.Parse(SlowIgnoreAboveSize);
-
- public void Validate()
- {
- if (ImportFailedMaxStrikes is > 0 and < 3)
- {
- throw new ValidationException($"the minimum value for {SectionName.ToUpperInvariant()}__IMPORT_FAILED_MAX_STRIKES must be 3");
- }
-
- if (StalledMaxStrikes is > 0 and < 3)
- {
- throw new ValidationException($"the minimum value for {SectionName.ToUpperInvariant()}__STALLED_MAX_STRIKES must be 3");
- }
-
- if (DownloadingMetadataMaxStrikes is > 0 and < 3)
- {
- throw new ValidationException($"the minimum value for {SectionName.ToUpperInvariant()}__DOWNLOADING_METADATA_MAX_STRIKES must be 3");
- }
-
- if (SlowMaxStrikes is > 0 and < 3)
- {
- throw new ValidationException($"the minimum value for {SectionName.ToUpperInvariant()}__SLOW_MAX_STRIKES must be 3");
- }
-
- if (SlowMaxStrikes > 0)
- {
- bool isSlowSpeedSet = !string.IsNullOrEmpty(SlowMinSpeed);
-
- if (isSlowSpeedSet && ByteSize.TryParse(SlowMinSpeed, out _) is false)
- {
- throw new ValidationException($"invalid value for {SectionName.ToUpperInvariant()}__SLOW_MIN_SPEED");
- }
-
- if (SlowMaxTime < 0)
- {
- throw new ValidationException($"invalid value for {SectionName.ToUpperInvariant()}__SLOW_MAX_TIME");
- }
-
- if (!isSlowSpeedSet && SlowMaxTime is 0)
- {
- throw new ValidationException($"either {SectionName.ToUpperInvariant()}__SLOW_MIN_SPEED or {SectionName.ToUpperInvariant()}__SLOW_MAX_STRIKES must be set");
- }
-
- bool isSlowIgnoreAboveSizeSet = !string.IsNullOrEmpty(SlowIgnoreAboveSize);
-
- if (isSlowIgnoreAboveSizeSet && ByteSize.TryParse(SlowIgnoreAboveSize, out _) is false)
- {
- throw new ValidationException($"invalid value for {SectionName.ToUpperInvariant()}__SLOW_IGNORE_ABOVE_SIZE");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/code/Common/Enums/CertificateValidationType.cs b/code/Common/Enums/CertificateValidationType.cs
deleted file mode 100644
index bc935987..00000000
--- a/code/Common/Enums/CertificateValidationType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-๏ปฟnamespace Common.Enums;
-
-public enum CertificateValidationType
-{
- Enabled = 0,
- DisabledForLocalAddresses = 1,
- Disabled = 2
-}
\ No newline at end of file
diff --git a/code/Common/Enums/DownloadClient.cs b/code/Common/Enums/DownloadClient.cs
deleted file mode 100644
index 7c215064..00000000
--- a/code/Common/Enums/DownloadClient.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-๏ปฟnamespace Common.Enums;
-
-public enum DownloadClient
-{
- QBittorrent,
- Deluge,
- Transmission,
- None,
- Disabled
-}
\ No newline at end of file
diff --git a/code/Dockerfile b/code/Dockerfile
new file mode 100644
index 00000000..6b19ed15
--- /dev/null
+++ b/code/Dockerfile
@@ -0,0 +1,72 @@
+# Build Angular frontend
+FROM --platform=$BUILDPLATFORM node:18-alpine AS frontend-build
+WORKDIR /app
+
+# Copy package files first for better layer caching
+COPY frontend/package*.json ./
+RUN 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 solution and project files first for better layer caching
+# COPY backend/*.sln ./backend/
+# COPY backend/*/*.csproj ./backend/*/
+
+# Copy source code
+COPY backend/ ./backend/
+
+# Restore dependencies
+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
+
+# Build and publish
+RUN dotnet publish ./backend/Cleanuparr.Api/Cleanuparr.Api.csproj \
+ -a $TARGETARCH \
+ -c Release \
+ -o /app/publish \
+ /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 and timezone support
+RUN apt-get update && apt-get install -y \
+ tzdata \
+ gosu \
+ && rm -rf /var/lib/apt/lists/*
+
+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"]
\ No newline at end of file
diff --git a/code/Domain/Enums/InstanceType.cs b/code/Domain/Enums/InstanceType.cs
deleted file mode 100644
index 4ec96a4c..00000000
--- a/code/Domain/Enums/InstanceType.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-๏ปฟnamespace Domain.Enums;
-
-public enum InstanceType
-{
- Sonarr,
- Radarr,
- Lidarr,
- Readarr
-}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Blocking/BlockedItem.cs b/code/Domain/Models/Arr/Blocking/BlockedItem.cs
deleted file mode 100644
index b7d94687..00000000
--- a/code/Domain/Models/Arr/Blocking/BlockedItem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-๏ปฟnamespace Domain.Models.Arr.Blocking;
-
-public record BlockedItem
-{
- public required string Hash { get; init; }
-
- public required Uri InstanceUrl { get; init; }
-}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Blocking/LidarrBlockedItem.cs b/code/Domain/Models/Arr/Blocking/LidarrBlockedItem.cs
deleted file mode 100644
index 6a2b59af..00000000
--- a/code/Domain/Models/Arr/Blocking/LidarrBlockedItem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-๏ปฟnamespace Domain.Models.Arr.Blocking;
-
-public sealed record LidarrBlockedItem : BlockedItem
-{
- public required long AlbumId { get; init; }
-
- public required long ArtistId { get; init; }
-}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Blocking/RadarrBlockedItem.cs b/code/Domain/Models/Arr/Blocking/RadarrBlockedItem.cs
deleted file mode 100644
index 16532dab..00000000
--- a/code/Domain/Models/Arr/Blocking/RadarrBlockedItem.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-๏ปฟnamespace Domain.Models.Arr.Blocking;
-
-public sealed record RadarrBlockedItem : BlockedItem
-{
- public required long MovieId { get; init; }
-}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Blocking/SonarrBlockedItem.cs b/code/Domain/Models/Arr/Blocking/SonarrBlockedItem.cs
deleted file mode 100644
index a42b0acb..00000000
--- a/code/Domain/Models/Arr/Blocking/SonarrBlockedItem.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-๏ปฟ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; }
-}
\ No newline at end of file
diff --git a/code/Executable/DependencyInjection/ConfigurationDI.cs b/code/Executable/DependencyInjection/ConfigurationDI.cs
deleted file mode 100644
index ee5cacb4..00000000
--- a/code/Executable/DependencyInjection/ConfigurationDI.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-๏ปฟusing Common.Configuration.Arr;
-using Common.Configuration.ContentBlocker;
-using Common.Configuration.DownloadCleaner;
-using Common.Configuration.DownloadClient;
-using Common.Configuration.General;
-using Common.Configuration.Logging;
-using Common.Configuration.QueueCleaner;
-
-namespace Executable.DependencyInjection;
-
-public static class ConfigurationDI
-{
- public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
- services
- .Configure(configuration)
- .Configure(configuration)
- .Configure(configuration.GetSection(QueueCleanerConfig.SectionName))
- .Configure(configuration.GetSection(ContentBlockerConfig.SectionName))
- .Configure(configuration.GetSection(DownloadCleanerConfig.SectionName))
- .Configure(configuration)
- .Configure(configuration.GetSection(QBitConfig.SectionName))
- .Configure(configuration.GetSection(DelugeConfig.SectionName))
- .Configure(configuration.GetSection(TransmissionConfig.SectionName))
- .Configure(configuration.GetSection(SonarrConfig.SectionName))
- .Configure(configuration.GetSection(RadarrConfig.SectionName))
- .Configure(configuration.GetSection(LidarrConfig.SectionName))
- .Configure(configuration.GetSection(LoggingConfig.SectionName));
-}
\ No newline at end of file
diff --git a/code/Executable/DependencyInjection/LoggingDI.cs b/code/Executable/DependencyInjection/LoggingDI.cs
deleted file mode 100644
index eb1e4ad2..00000000
--- a/code/Executable/DependencyInjection/LoggingDI.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-๏ปฟusing Common.Configuration.Logging;
-using Domain.Enums;
-using Infrastructure.Verticals.ContentBlocker;
-using Infrastructure.Verticals.DownloadCleaner;
-using Infrastructure.Verticals.QueueCleaner;
-using Serilog;
-using Serilog.Events;
-using Serilog.Templates;
-using Serilog.Templates.Themes;
-
-namespace Executable.DependencyInjection;
-
-public static class LoggingDI
-{
- public static ILoggingBuilder AddLogging(this ILoggingBuilder builder, IConfiguration configuration)
- {
- LoggingConfig? config = configuration.GetSection(LoggingConfig.SectionName).Get();
-
- if (!string.IsNullOrEmpty(config?.File?.Path) && !Directory.Exists(config.File.Path))
- {
- try
- {
- Directory.CreateDirectory(config.File.Path);
- }
- catch (Exception exception)
- {
- throw new Exception($"log file path is not a valid directory | {config.File.Path}", exception);
- }
- }
-
- LoggerConfiguration logConfig = new();
- const string jobNameTemplate = "{#if JobName is not null} {Concat('[',JobName,']'),JOB_PAD}{#end}";
- const string instanceNameTemplate = "{#if InstanceName is not null} {Concat('[',InstanceName,']'),ARR_PAD}{#end}";
- const string consoleOutputTemplate = $"[{{@t:yyyy-MM-dd HH:mm:ss.fff}} {{@l:u3}}]{jobNameTemplate}{instanceNameTemplate} {{@m}}\n{{@x}}";
- const string fileOutputTemplate = $"{{@t:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{@l:u3}}]{jobNameTemplate}{instanceNameTemplate} {{@m:lj}}\n{{@x}}";
- LogEventLevel level = LogEventLevel.Information;
- List names = [nameof(ContentBlocker), nameof(QueueCleaner), nameof(DownloadCleaner)];
- int jobPadding = names.Max(x => x.Length) + 2;
- names = [InstanceType.Sonarr.ToString(), InstanceType.Radarr.ToString(), InstanceType.Lidarr.ToString()];
- int arrPadding = names.Max(x => x.Length) + 2;
-
- string consoleTemplate = consoleOutputTemplate
- .Replace("JOB_PAD", jobPadding.ToString())
- .Replace("ARR_PAD", arrPadding.ToString());
- string fileTemplate = fileOutputTemplate
- .Replace("JOB_PAD", jobPadding.ToString())
- .Replace("ARR_PAD", arrPadding.ToString());
-
- if (config is not null)
- {
- level = config.LogLevel;
-
- if (config.File?.Enabled is true)
- {
- logConfig.WriteTo.File(
- path: Path.Combine(config.File.Path, "cleanuperr-.txt"),
- formatter: new ExpressionTemplate(fileTemplate),
- fileSizeLimitBytes: 10L * 1024 * 1024,
- rollingInterval: RollingInterval.Day,
- rollOnFileSizeLimit: true
- );
- }
- }
-
- Log.Logger = logConfig
- .MinimumLevel.Is(level)
- .MinimumLevel.Override("MassTransit", LogEventLevel.Warning)
- .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
- .MinimumLevel.Override("Microsoft.Extensions.Http", LogEventLevel.Warning)
- .MinimumLevel.Override("Quartz", LogEventLevel.Warning)
- .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
- .WriteTo.Console(new ExpressionTemplate(consoleTemplate))
- .Enrich.FromLogContext()
- .Enrich.WithProperty("ApplicationName", "cleanuperr")
- .CreateLogger();
-
- return builder
- .ClearProviders()
- .AddSerilog();
- }
-}
\ No newline at end of file
diff --git a/code/Executable/DependencyInjection/QuartzDI.cs b/code/Executable/DependencyInjection/QuartzDI.cs
deleted file mode 100644
index 31b1ea40..00000000
--- a/code/Executable/DependencyInjection/QuartzDI.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-๏ปฟusing Common.Configuration;
-using Common.Configuration.ContentBlocker;
-using Common.Configuration.DownloadCleaner;
-using Common.Configuration.General;
-using Common.Configuration.QueueCleaner;
-using Common.Helpers;
-using Executable.Jobs;
-using Infrastructure.Verticals.ContentBlocker;
-using Infrastructure.Verticals.DownloadCleaner;
-using Infrastructure.Verticals.Jobs;
-using Infrastructure.Verticals.QueueCleaner;
-using Quartz;
-using Quartz.Spi;
-
-namespace Executable.DependencyInjection;
-
-public static class QuartzDI
-{
- public static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
- services
- .AddQuartz(q =>
- {
- TriggersConfig? config = configuration
- .GetRequiredSection(TriggersConfig.SectionName)
- .Get();
-
- if (config is null)
- {
- throw new NullReferenceException("triggers configuration is null");
- }
-
- q.AddJobs(configuration, config);
- })
- .AddQuartzHostedService(opt =>
- {
- opt.WaitForJobsToComplete = true;
- });
-
- private static void AddJobs(
- this IServiceCollectionQuartzConfigurator q,
- IConfiguration configuration,
- TriggersConfig triggersConfig
- )
- {
- ContentBlockerConfig? contentBlockerConfig = configuration
- .GetRequiredSection(ContentBlockerConfig.SectionName)
- .Get();
-
- q.AddJob(contentBlockerConfig, triggersConfig.ContentBlocker);
-
- QueueCleanerConfig? queueCleanerConfig = configuration
- .GetRequiredSection(QueueCleanerConfig.SectionName)
- .Get();
-
- if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true })
- {
- q.AddJob(queueCleanerConfig, string.Empty);
- q.AddJobListener(new JobChainingListener(nameof(ContentBlocker), nameof(QueueCleaner)));
- }
- else
- {
- q.AddJob(queueCleanerConfig, triggersConfig.QueueCleaner);
- }
-
- DownloadCleanerConfig? downloadCleanerConfig = configuration
- .GetRequiredSection(DownloadCleanerConfig.SectionName)
- .Get();
-
- q.AddJob(downloadCleanerConfig, triggersConfig.DownloadCleaner);
- }
-
- private static void AddJob(
- this IServiceCollectionQuartzConfigurator q,
- IJobConfig? config,
- string trigger
- ) where T: GenericHandler
- {
- string typeName = typeof(T).Name;
-
- if (config is null)
- {
- throw new NullReferenceException($"{typeName} configuration is null");
- }
-
- if (!config.Enabled)
- {
- return;
- }
-
- bool hasTrigger = trigger.Length > 0;
-
- q.AddJob>(opts =>
- {
- opts.WithIdentity(typeName);
-
- if (!hasTrigger)
- {
- // jobs with no triggers need to be stored durably
- opts.StoreDurably();
- }
- });
-
- // skip empty triggers
- if (!hasTrigger)
- {
- return;
- }
-
- IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
- .WithIdentity("ExampleTrigger")
- .StartNow()
- .WithCronSchedule(trigger)
- .Build();
-
- IReadOnlyList nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
- TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0];
-
- if (triggerValue > Constants.TriggerMaxLimit)
- {
- throw new Exception($"{trigger} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
- }
-
- if (triggerValue > StaticConfiguration.TriggerValue)
- {
- StaticConfiguration.TriggerValue = triggerValue;
- }
-
- q.AddTrigger(opts =>
- {
- opts.ForJob(typeName)
- .WithIdentity($"{typeName}-trigger")
- .WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing())
- .StartNow();
- });
-
- // Startup trigger
- q.AddTrigger(opts =>
- {
- opts.ForJob(typeName)
- .WithIdentity($"{typeName}-startup-trigger")
- .StartNow();
- });
- }
-}
\ No newline at end of file
diff --git a/code/Executable/DependencyInjection/ServicesDI.cs b/code/Executable/DependencyInjection/ServicesDI.cs
deleted file mode 100644
index e2e557de..00000000
--- a/code/Executable/DependencyInjection/ServicesDI.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-๏ปฟusing Common.Configuration.ContentBlocker;
-using Common.Configuration.DownloadCleaner;
-using Common.Configuration.QueueCleaner;
-using Infrastructure.Interceptors;
-using Infrastructure.Providers;
-using Infrastructure.Services;
-using Infrastructure.Verticals.Arr;
-using Infrastructure.Verticals.ContentBlocker;
-using Infrastructure.Verticals.DownloadCleaner;
-using Infrastructure.Verticals.DownloadClient;
-using Infrastructure.Verticals.DownloadClient.Deluge;
-using Infrastructure.Verticals.DownloadClient.QBittorrent;
-using Infrastructure.Verticals.DownloadClient.Transmission;
-using Infrastructure.Verticals.DownloadRemover;
-using Infrastructure.Verticals.DownloadRemover.Interfaces;
-using Infrastructure.Verticals.Files;
-using Infrastructure.Verticals.ItemStriker;
-using Infrastructure.Verticals.QueueCleaner;
-
-namespace Executable.DependencyInjection;
-
-public static class ServicesDI
-{
- public static IServiceCollection AddServices(this IServiceCollection services) =>
- services
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddTransient()
- .AddSingleton()
- .AddSingleton>()
- .AddSingleton>()
- .AddSingleton>();
-}
\ No newline at end of file
diff --git a/code/Executable/Executable.csproj b/code/Executable/Executable.csproj
deleted file mode 100644
index 79c6b329..00000000
--- a/code/Executable/Executable.csproj
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- cleanuperr
- net9.0
- 0.0.1
- enable
- enable
- dotnet-Executable-6108b2ba-f035-47bc-addf-aaf5e20da4b8
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/code/Executable/HostExtensions.cs b/code/Executable/HostExtensions.cs
deleted file mode 100644
index 0aa8d795..00000000
--- a/code/Executable/HostExtensions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-๏ปฟusing System.Reflection;
-
-namespace Executable;
-
-public static class HostExtensions
-{
- public static IHost Init(this IHost host)
- {
- ILogger logger = host.Services.GetRequiredService>();
-
- Version? version = Assembly.GetExecutingAssembly().GetName().Version;
-
- logger.LogInformation(
- version is null
- ? "cleanuperr version not detected"
- : $"cleanuperr v{version.Major}.{version.Minor}.{version.Build}"
- );
-
- logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
-
- return host;
- }
-}
\ No newline at end of file
diff --git a/code/Executable/Program.cs b/code/Executable/Program.cs
deleted file mode 100644
index 5623667f..00000000
--- a/code/Executable/Program.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Executable;
-using Executable.DependencyInjection;
-
-var builder = Host.CreateApplicationBuilder(args);
-
-builder.Services.AddInfrastructure(builder.Configuration);
-builder.Logging.AddLogging(builder.Configuration);
-
-var host = builder.Build();
-host.Init();
-
-host.Run();
\ No newline at end of file
diff --git a/code/Executable/appsettings.Development.json b/code/Executable/appsettings.Development.json
deleted file mode 100644
index 536b1ced..00000000
--- a/code/Executable/appsettings.Development.json
+++ /dev/null
@@ -1,151 +0,0 @@
-{
- "DRY_RUN": true,
- "HTTP_MAX_RETRIES": 0,
- "HTTP_TIMEOUT": 100,
- "HTTP_VALIDATE_CERT": "enabled",
- "Logging": {
- "LogLevel": "Verbose",
- "Enhanced": true,
- "File": {
- "Enabled": false,
- "Path": ""
- }
- },
- "SEARCH_ENABLED": true,
- "SEARCH_DELAY": 5,
- "Triggers": {
- "QueueCleaner": "0/10 * * * * ?",
- "ContentBlocker": "0/10 * * * * ?",
- "DownloadCleaner": "0/10 * * * * ?"
- },
- "ContentBlocker": {
- "Enabled": true,
- "IGNORE_PRIVATE": true,
- "DELETE_PRIVATE": false,
- "IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
- },
- "QueueCleaner": {
- "Enabled": true,
- "RunSequentially": true,
- "IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads",
- "IMPORT_FAILED_MAX_STRIKES": 3,
- "IMPORT_FAILED_IGNORE_PRIVATE": true,
- "IMPORT_FAILED_DELETE_PRIVATE": false,
- "IMPORT_FAILED_IGNORE_PATTERNS": [
- "file is a sample"
- ],
- "STALLED_MAX_STRIKES": 3,
- "STALLED_RESET_STRIKES_ON_PROGRESS": true,
- "STALLED_IGNORE_PRIVATE": true,
- "STALLED_DELETE_PRIVATE": false,
- "DOWNLOADING_METADATA_MAX_STRIKES": 3,
- "SLOW_MAX_STRIKES": 5,
- "SLOW_RESET_STRIKES_ON_PROGRESS": true,
- "SLOW_IGNORE_PRIVATE": false,
- "SLOW_DELETE_PRIVATE": false,
- "SLOW_MIN_SPEED": "1MB",
- "SLOW_MAX_TIME": 20,
- "SLOW_IGNORE_ABOVE_SIZE": "4GB"
- },
- "DownloadCleaner": {
- "Enabled": false,
- "DELETE_PRIVATE": false,
- "CATEGORIES": [
- {
- "Name": "tv-sonarr",
- "MAX_RATIO": -1,
- "MIN_SEED_TIME": 0,
- "MAX_SEED_TIME": 240
- }
- ],
- "UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
- "UNLINKED_USE_TAG": false,
- "UNLINKED_IGNORED_ROOT_DIR": "",
- "UNLINKED_CATEGORIES": [
- "tv-sonarr",
- "radarr"
- ],
- "IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
- },
- "DOWNLOAD_CLIENT": "qbittorrent",
- "qBittorrent": {
- "Url": "http://localhost:8080",
- "URL_BASE": "",
- "Username": "test",
- "Password": "testing"
- },
- "Deluge": {
- "Url": "http://localhost:8112",
- "URL_BASE": "",
- "Password": "testing"
- },
- "Transmission": {
- "Url": "http://localhost:9091",
- "URL_BASE": "transmission",
- "Username": "test",
- "Password": "testing"
- },
- "Sonarr": {
- "Enabled": true,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "SearchType": "Episode",
- "Block": {
- "Type": "blacklist",
- "Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
- },
- "Instances": [
- {
- "Url": "http://localhost:8989",
- "ApiKey": "425d1e713f0c405cbbf359ac0502c1f4"
- }
- ]
- },
- "Radarr": {
- "Enabled": true,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "Block": {
- "Type": "blacklist",
- "Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
- },
- "Instances": [
- {
- "Url": "http://localhost:7878",
- "ApiKey": "8b7454f668e54c5b8f44f56f93969761"
- }
- ]
- },
- "Lidarr": {
- "Enabled": true,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "Block": {
- "Type": "blacklist",
- "Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
- },
- "Instances": [
- {
- "Url": "http://localhost:8686",
- "ApiKey": "7f677cfdc074414397af53dd633860c5"
- }
- ]
- },
- "Notifiarr": {
- "ON_IMPORT_FAILED_STRIKE": true,
- "ON_STALLED_STRIKE": true,
- "ON_SLOW_STRIKE": true,
- "ON_QUEUE_ITEM_DELETED": true,
- "ON_DOWNLOAD_CLEANED": true,
- "ON_CATEGORY_CHANGED": true,
- "API_KEY": "",
- "CHANNEL_ID": ""
- },
- "Apprise": {
- "ON_IMPORT_FAILED_STRIKE": true,
- "ON_STALLED_STRIKE": true,
- "ON_SLOW_STRIKE": true,
- "ON_QUEUE_ITEM_DELETED": true,
- "ON_DOWNLOAD_CLEANED": true,
- "ON_CATEGORY_CHANGED": true,
- "URL": "http://localhost:8000",
- "KEY": ""
- }
-}
diff --git a/code/Executable/appsettings.json b/code/Executable/appsettings.json
deleted file mode 100644
index 35bb7679..00000000
--- a/code/Executable/appsettings.json
+++ /dev/null
@@ -1,138 +0,0 @@
-{
- "DRY_RUN": false,
- "HTTP_MAX_RETRIES": 0,
- "HTTP_TIMEOUT": 100,
- "HTTP_VALIDATE_CERT": "enabled",
- "Logging": {
- "LogLevel": "Information",
- "Enhanced": true,
- "File": {
- "Enabled": false,
- "Path": ""
- }
- },
- "SEARCH_ENABLED": true,
- "SEARCH_DELAY": 30,
- "Triggers": {
- "QueueCleaner": "0 0/5 * * * ?",
- "ContentBlocker": "0 0/5 * * * ?",
- "DownloadCleaner": "0 0 * * * ?"
- },
- "ContentBlocker": {
- "Enabled": false,
- "IGNORE_PRIVATE": false,
- "IGNORED_DOWNLOADS_PATH": ""
- },
- "QueueCleaner": {
- "Enabled": false,
- "RunSequentially": true,
- "IGNORED_DOWNLOADS_PATH": "",
- "IMPORT_FAILED_MAX_STRIKES": 0,
- "IMPORT_FAILED_IGNORE_PRIVATE": false,
- "IMPORT_FAILED_DELETE_PRIVATE": false,
- "IMPORT_FAILED_IGNORE_PATTERNS": [],
- "STALLED_MAX_STRIKES": 0,
- "STALLED_RESET_STRIKES_ON_PROGRESS": false,
- "STALLED_IGNORE_PRIVATE": false,
- "STALLED_DELETE_PRIVATE": false,
- "DOWNLOADING_METADATA_MAX_STRIKES": 0,
- "SLOW_MAX_STRIKES": 0,
- "SLOW_RESET_STRIKES_ON_PROGRESS": true,
- "SLOW_IGNORE_PRIVATE": false,
- "SLOW_DELETE_PRIVATE": false,
- "SLOW_MIN_SPEED": "",
- "SLOW_MAX_TIME": 0,
- "SLOW_IGNORE_ABOVE_SIZE": ""
- },
- "DownloadCleaner": {
- "Enabled": false,
- "DELETE_PRIVATE": false,
- "CATEGORIES": [],
- "UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
- "UNLINKED_USE_TAG": false,
- "UNLINKED_IGNORED_ROOT_DIR": "",
- "UNLINKED_CATEGORIES": [],
- "IGNORED_DOWNLOADS_PATH": ""
- },
- "DOWNLOAD_CLIENT": "none",
- "qBittorrent": {
- "Url": "http://localhost:8080",
- "URL_BASE": "",
- "Username": "",
- "Password": ""
- },
- "Deluge": {
- "Url": "http://localhost:8112",
- "URL_BASE": "",
- "Password": "testing"
- },
- "Transmission": {
- "Url": "http://localhost:9091",
- "URL_BASE": "transmission",
- "Username": "test",
- "Password": "testing"
- },
- "Sonarr": {
- "Enabled": false,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "SearchType": "Episode",
- "Block": {
- "Type": "blacklist",
- "Path": ""
- },
- "Instances": [
- {
- "Url": "http://localhost:8989",
- "ApiKey": ""
- }
- ]
- },
- "Radarr": {
- "Enabled": false,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "Block": {
- "Type": "blacklist",
- "Path": ""
- },
- "Instances": [
- {
- "Url": "http://localhost:7878",
- "ApiKey": ""
- }
- ]
- },
- "Lidarr": {
- "Enabled": false,
- "IMPORT_FAILED_MAX_STRIKES": -1,
- "Block": {
- "Type": "blacklist",
- "Path": ""
- },
- "Instances": [
- {
- "Url": "http://localhost:8686",
- "ApiKey": ""
- }
- ]
- },
- "Notifiarr": {
- "ON_IMPORT_FAILED_STRIKE": false,
- "ON_STALLED_STRIKE": false,
- "ON_SLOW_STRIKE": false,
- "ON_QUEUE_ITEM_DELETED": false,
- "ON_DOWNLOAD_CLEANED": false,
- "ON_CATEGORY_CHANGED": false,
- "API_KEY": "",
- "CHANNEL_ID": ""
- },
- "Apprise": {
- "ON_IMPORT_FAILED_STRIKE": false,
- "ON_STALLED_STRIKE": false,
- "ON_SLOW_STRIKE": false,
- "ON_QUEUE_ITEM_DELETED": false,
- "ON_DOWNLOAD_CLEANED": false,
- "ON_CATEGORY_CHANGED": false,
- "URL": "",
- "KEY": ""
- }
-}
diff --git a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceFixture.cs b/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceFixture.cs
deleted file mode 100644
index 5b89934a..00000000
--- a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceFixture.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using Common.Configuration.ContentBlocker;
-using Common.Configuration.DownloadCleaner;
-using Common.Configuration.QueueCleaner;
-using Infrastructure.Interceptors;
-using Infrastructure.Verticals.ContentBlocker;
-using Infrastructure.Verticals.DownloadClient;
-using Infrastructure.Verticals.Files;
-using Infrastructure.Verticals.ItemStriker;
-using Infrastructure.Verticals.Notifications;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using NSubstitute;
-
-namespace Infrastructure.Tests.Verticals.DownloadClient;
-
-public class DownloadServiceFixture : IDisposable
-{
- public ILogger Logger { get; set; }
- public IMemoryCache Cache { get; set; }
- public IStriker Striker { get; set; }
-
- public DownloadServiceFixture()
- {
- Logger = Substitute.For>();
- Cache = Substitute.For();
- Striker = Substitute.For();
- }
-
- public TestDownloadService CreateSut(
- QueueCleanerConfig? queueCleanerConfig = null,
- ContentBlockerConfig? contentBlockerConfig = null
- )
- {
- queueCleanerConfig ??= new QueueCleanerConfig
- {
- Enabled = true,
- RunSequentially = true,
- StalledResetStrikesOnProgress = true,
- StalledMaxStrikes = 3
- };
-
- var queueCleanerOptions = Substitute.For>();
- queueCleanerOptions.Value.Returns(queueCleanerConfig);
-
- contentBlockerConfig ??= new ContentBlockerConfig
- {
- Enabled = true
- };
-
- var contentBlockerOptions = Substitute.For>();
- contentBlockerOptions.Value.Returns(contentBlockerConfig);
-
- var downloadCleanerOptions = Substitute.For>();
- downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig());
-
- var filenameEvaluator = Substitute.For();
- var notifier = Substitute.For();
- var dryRunInterceptor = Substitute.For();
- var hardlinkFileService = Substitute.For();
-
- return new TestDownloadService(
- Logger,
- queueCleanerOptions,
- contentBlockerOptions,
- downloadCleanerOptions,
- Cache,
- filenameEvaluator,
- Striker,
- notifier,
- dryRunInterceptor,
- hardlinkFileService
- );
- }
-
- public void Dispose()
- {
- // Cleanup if needed
- }
-}
\ No newline at end of file
diff --git a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs b/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs
deleted file mode 100644
index e69c21c2..00000000
--- a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs
+++ /dev/null
@@ -1,214 +0,0 @@
-๏ปฟusing Common.Configuration.DownloadCleaner;
-using Domain.Enums;
-using Domain.Models.Cache;
-using Infrastructure.Helpers;
-using Infrastructure.Verticals.Context;
-using Infrastructure.Verticals.DownloadClient;
-using NSubstitute;
-using NSubstitute.ClearExtensions;
-using Shouldly;
-
-namespace Infrastructure.Tests.Verticals.DownloadClient;
-
-public class DownloadServiceTests : IClassFixture
-{
- private readonly DownloadServiceFixture _fixture;
-
- public DownloadServiceTests(DownloadServiceFixture fixture)
- {
- _fixture = fixture;
- _fixture.Cache.ClearSubstitute();
- _fixture.Striker.ClearSubstitute();
- }
-
- public class ResetStrikesOnProgressTests : DownloadServiceTests
- {
- public ResetStrikesOnProgressTests(DownloadServiceFixture fixture) : base(fixture)
- {
- }
-
- [Fact]
- public void WhenStalledStrikeDisabled_ShouldNotResetStrikes()
- {
- // Arrange
- TestDownloadService sut = _fixture.CreateSut(queueCleanerConfig: new()
- {
- Enabled = true,
- RunSequentially = true,
- StalledResetStrikesOnProgress = false,
- });
-
- // Act
- sut.ResetStalledStrikesOnProgress("test-hash", 100);
-
- // Assert
- _fixture.Cache.ReceivedCalls().ShouldBeEmpty();
- }
-
- [Fact]
- public void WhenProgressMade_ShouldResetStrikes()
- {
- // Arrange
- const string hash = "test-hash";
- StalledCacheItem stalledCacheItem = new StalledCacheItem { Downloaded = 100 };
-
- _fixture.Cache.TryGetValue(Arg.Any