diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index 82ee799..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - test: - name: Test - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - - - name: Run tests - run: go test -v -race -count=1 ./... - - build: - name: Build - runs-on: docker - container: - image: docker.io/library/golang:1.25 - strategy: - matrix: - goos: [linux, darwin, windows] - goarch: [amd64, arm64] - steps: - - uses: actions/checkout@v4 - - - name: Build - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: go build -o unarr ./cmd/unarr/ - - lint: - name: Lint - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - - - name: Install golangci-lint - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.11.4/install.sh \ - | sh -s -- -b /usr/local/bin v2.11.4 - - - name: Run golangci-lint - run: golangci-lint run ./... - - coverage: - name: Coverage - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - - - name: Install python3 - run: apt-get update && apt-get install -y --no-install-recommends python3 - - - name: Run tests with coverage (all packages) - run: | - go test -race -coverprofile=coverage.out -covermode=atomic \ - ./internal/engine/... \ - ./internal/agent/... \ - ./internal/cmd/... - - - name: Check coverage threshold (engine + agent) - run: | - # Threshold applies only to engine and agent — cmd contains interactive UI - # commands (config menus, daemon, auth browser) that are not unit-testable. - go test -race -coverprofile=coverage-core.out -covermode=atomic \ - ./internal/engine/... \ - ./internal/agent/... - COVERAGE=$(go tool cover -func=coverage-core.out | grep ^total | awk '{print $3}' | tr -d '%') - echo "Coverage on engine+agent: ${COVERAGE}%" - python3 -c " - coverage = float('${COVERAGE}') - threshold = 50.0 - print(f'Coverage: {coverage:.1f}% (threshold: {threshold}%)') - if coverage < threshold: - print(f'ERROR: Coverage {coverage:.1f}% is below minimum {threshold}%') - exit(1) - else: - print('OK: Coverage meets minimum threshold') - " - - vet: - name: Vet - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - - - name: Run go vet - run: go vet ./... diff --git a/.forgejo/workflows/docker-rebuild.yml b/.forgejo/workflows/docker-rebuild.yml deleted file mode 100644 index 34cc3d6..0000000 --- a/.forgejo/workflows/docker-rebuild.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Rebuilds and re-pushes the `latest` image without a version bump so newly -# *fixed* Alpine / ffmpeg / Go patches land between tagged releases. Versioned -# tags are immutable and never touched here. Runs weekly and on demand. -name: Docker rebuild - -on: - schedule: - # Mondays 04:17 UTC (off the hour to avoid the scheduler rush) - - cron: "17 4 * * 1" - workflow_dispatch: - -jobs: - rebuild: - runs-on: docker - container: - image: docker.io/library/docker:27-cli - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install build deps - run: apk add --no-cache curl git bash - - - name: Install buildx - run: | - mkdir -p ~/.docker/cli-plugins - curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ - -o ~/.docker/cli-plugins/docker-buildx - chmod +x ~/.docker/cli-plugins/docker-buildx - - - name: Set up qemu - run: docker run --rm --privileged tonistiigi/binfmt --install all - - # Stamp the binary with the most recent release tag (not "dev"). - - name: Resolve version - id: ver - run: | - v=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) - echo "version=$v" >> "$GITHUB_OUTPUT" - - - name: Login to Docker Hub - env: - DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} - DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin - - - name: Build + push (refresh latest) - env: - VERSION: ${{ steps.ver.outputs.version }} - run: | - docker buildx create --name builder --use --driver docker-container - # Refresh the floating tag only — never overwrite a versioned release. - # Force a fresh base pull so apk upgrade picks up new patches. - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg "VERSION=$VERSION" \ - --tag "torrentclaw/unarr:latest" \ - --no-cache \ - --push \ - . diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index d757612..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - contents: write - -jobs: - release: - runs-on: docker - container: - image: docker.io/library/golang:1.25 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install build deps (bash, curl, jq, ffmpeg fetch deps) - run: | - apt-get update - apt-get install -y --no-install-recommends bash curl ca-certificates jq xz-utils unzip - - - name: Install goreleaser - run: | - curl -sSfL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz \ - | tar -xz -C /usr/local/bin goreleaser - - - name: Run goreleaser - env: - # Forgejo runner auto-injects GITHUB_TOKEN (a per-job, instance-scoped - # token usable against the Forgejo REST API). goreleaser only accepts - # one token; with both GITHUB_TOKEN + GITEA_TOKEN set it errors out - # ("multiple tokens"). Unset GITHUB_TOKEN before invoking goreleaser so - # it picks the Gitea code path + the gitea_urls block in .goreleaser.yml. - GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - # Empty when RELEASE_SIGNING_PUBKEY variable is unset — goreleaser - # accepts it and the resulting binary disables signature checks - # (back-compat: pre-signing releases continue to update). Set - # RELEASE_SIGNING_PUBKEY (variable) + RELEASE_SIGNING_KEY (secret) - # to turn verification on. - RELEASE_SIGNING_PUBKEY: ${{ vars.RELEASE_SIGNING_PUBKEY }} - run: | - unset GITHUB_TOKEN - goreleaser release --clean - - - name: Sign checksums.txt with ed25519 - if: ${{ vars.RELEASE_SIGNING_PUBKEY != '' && secrets.RELEASE_SIGNING_KEY != '' }} - env: - RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} - RELEASE_TAG: ${{ github.ref_name }} - FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Tailscale IP — domain-agnostic; the runner shares the dokploy-network with - # forgejo (hostname `forgejo`), so the in-cluster hostname is fastest, but the - # Tailscale IP is the documented fallback. - FORGEJO_API: http://forgejo:3000/api/v1 - REPO: torrentclaw/unarr - run: | - set -euo pipefail - go run ./scripts/sign-checksums \ - -key "$RELEASE_SIGNING_KEY" \ - -in dist/checksums.txt \ - -out dist/checksums.txt.sig - - # Find the release ID for this tag, then upload the sig as an asset. - rel_id=$(curl -sSf "$FORGEJO_API/repos/$REPO/releases/tags/$RELEASE_TAG" \ - -H "Authorization: token $FORGEJO_TOKEN" | jq -r '.id') - curl -sSf -X POST \ - "$FORGEJO_API/repos/$REPO/releases/$rel_id/assets?name=checksums.txt.sig" \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -F "attachment=@dist/checksums.txt.sig" - - docker: - needs: release - runs-on: docker - container: - # Docker-in-Docker capable image — buildx + qemu pre-installed. - image: docker.io/library/docker:27-cli - steps: - - uses: actions/checkout@v4 - - - name: Install buildx - run: | - apk add --no-cache curl - mkdir -p ~/.docker/cli-plugins - curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ - -o ~/.docker/cli-plugins/docker-buildx - chmod +x ~/.docker/cli-plugins/docker-buildx - - - name: Login to Docker Hub - env: - DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} - DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin - - - name: Set up qemu - run: docker run --rm --privileged tonistiigi/binfmt --install all - - - name: Build + push multi-arch image - env: - VERSION: ${{ github.ref_name }} - run: | - set -euo pipefail - VERSION_SEMVER="${VERSION#v}" - MAJOR_MINOR="${VERSION_SEMVER%.*}" - docker buildx create --name builder --use --driver docker-container - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg "VERSION=$VERSION" \ - --tag "torrentclaw/unarr:$VERSION_SEMVER" \ - --tag "torrentclaw/unarr:$MAJOR_MINOR" \ - --tag "torrentclaw/unarr:latest" \ - --push \ - . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e43ed0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,131 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ["1.25", "1.26"] + steps: + - uses: actions/checkout@v6 + + - name: Checkout go-client (local replace dependency) + uses: actions/checkout@v6 + with: + repository: torrentclaw/go-client + path: ../go-client + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go-version }} + + - name: Run tests + run: go test -v -race -count=1 ./... + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v6 + + - name: Checkout go-client (local replace dependency) + uses: actions/checkout@v6 + with: + repository: torrentclaw/go-client + path: ../go-client + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: go build -o unarr ./cmd/unarr/ + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Checkout go-client (local replace dependency) + uses: actions/checkout@v6 + with: + repository: torrentclaw/go-client + path: ../go-client + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.1.6 + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Checkout go-client (local replace dependency) + uses: actions/checkout@v6 + with: + repository: torrentclaw/go-client + path: ../go-client + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Run tests with coverage + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + file: ./coverage.out + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + vet: + name: Vet + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Checkout go-client (local replace dependency) + uses: actions/checkout@v6 + with: + repository: torrentclaw/go-client + path: ../go-client + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Run go vet + run: go vet ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..229a723 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,162 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + + docker: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: torrentclaw/unarr + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} + + virustotal: + needs: release + runs-on: ubuntu-latest + if: vars.VT_ENABLED == 'true' + steps: + - name: Get release tag + id: tag + run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Download release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p assets + gh release download "${{ steps.tag.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --dir assets \ + --pattern '*.tar.gz' \ + --pattern '*.zip' \ + --pattern 'checksums.txt' + + - name: Scan assets with VirusTotal + env: + VT_API_KEY: ${{ secrets.VT_API_KEY }} + run: | + mkdir -p results + for file in assets/*; do + filename=$(basename "$file") + echo "Uploading $filename to VirusTotal..." + + response=$(curl -s --request POST \ + --url https://www.virustotal.com/api/v3/files \ + --header "x-apikey: $VT_API_KEY" \ + --form "file=@$file") + + analysis_id=$(echo "$response" | jq -r '.data.id // empty') + if [ -z "$analysis_id" ]; then + echo "::warning::Failed to upload $filename: $response" + continue + fi + + echo "$filename=$analysis_id" >> results/scans.txt + echo " Analysis ID: $analysis_id" + + # Rate limit: VT free tier allows 4 req/min + sleep 16 + done + + - name: Wait for analysis completion + env: + VT_API_KEY: ${{ secrets.VT_API_KEY }} + run: | + echo "Waiting 60s for VirusTotal analysis to complete..." + sleep 60 + + vt_report="## 🛡️ VirusTotal Scan Results\n\n" + vt_report+="| File | Result | Link |\n" + vt_report+="|------|--------|------|\n" + + while IFS='=' read -r filename analysis_id; do + result=$(curl -s --request GET \ + --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ + --header "x-apikey: $VT_API_KEY") + + malicious=$(echo "$result" | jq -r '.data.attributes.stats.malicious // 0') + undetected=$(echo "$result" | jq -r '.data.attributes.stats.undetected // 0') + sha256=$(echo "$result" | jq -r '.meta.file_info.sha256 // empty') + + if [ "$malicious" = "0" ]; then + status="✅ Clean ($undetected engines)" + else + status="⚠️ $malicious detections" + fi + + link="https://www.virustotal.com/gui/file/$sha256" + vt_report+="| \`$filename\` | $status | [View]($link) |\n" + + sleep 16 + done < results/scans.txt + + echo -e "$vt_report" > results/report.md + cat results/report.md + + - name: Append scan results to release notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + current_body=$(gh release view "${{ steps.tag.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --json body --jq '.body') + + new_body="${current_body} + + $(cat results/report.md)" + + gh release edit "${{ steps.tag.outputs.tag }}" \ + --repo "${{ github.repository }}" \ + --notes "$new_body" diff --git a/.gitignore b/.gitignore index 8015bab..a9f3162 100644 --- a/.gitignore +++ b/.gitignore @@ -36,12 +36,6 @@ Thumbs.db # GoReleaser dist/ -dist-ffbinaries/ # Docker tmp/ -config/ -dist-ffbinaries/ - -# Claude Code: keep entirely local, do not track -.claude/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 2013069..52a7fa7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,11 +1,10 @@ -version: "2" - run: timeout: 5m linters: - default: none enable: + - errcheck + - gosimple - govet - ineffassign - staticcheck @@ -15,49 +14,31 @@ linters: - copyloopvar - durationcheck - errname + - errorlint - exhaustive - settings: - gosec: - excludes: - - G104 # Unhandled errors in fire-and-forget - - G112 # Slowloris — local-only servers - - G115 # Integer overflow — CLI-safe conversions - - G204 # Subprocess with variable — intentional for player/extractor launch - - G301 # Directory perms > 0750 — standard for user dirs - - G302 # File perms > 0600 — resume files need 0644 - - G304 # File inclusion via variable — config paths are trusted - - G306 # WriteFile perms > 0600 — binaries need 0755 - - G702 # Command injection via taint — self-update uses trusted URLs - - G703 # Path traversal via taint — internal paths only - - G704 # SSRF via taint — user-configured URLs - - G706 # Log injection via taint — internal log lines - exhaustive: - default-signifies-exhaustive: true - exclusions: - paths: - - dist - rules: - # Allow misspell "cancelled" — API constant matching server - - linters: - - misspell - text: "Cancell" - # Ignore nilerr in intentional log-and-continue patterns - - linters: - - nilerr - path: "(clean|scanner|usenet|engine)" - # Ignore staticcheck style suggestions (QF/S/SA4/SA9) - - linters: - - staticcheck - text: "^(QF|S1|SA4011|SA9003)" - # Ignore gosec G101 (hardcoded creds) in tests - - linters: - - gosec - path: _test\.go - -formatters: - enable: - gofmt - goimports - exclusions: - paths: - - dist + - misspell + - nilerr + - prealloc + - unconvert + - unparam + - wastedassign + +linters-settings: + gosec: + excludes: + - G104 # Allow unhandled errors in fire-and-forget (notifications) + errcheck: + exclude-functions: + - (*os/exec.Cmd).Start # Fire-and-forget for notifications + exhaustive: + default-signifies-exhaustive: true + misspell: + locale: US + +issues: + exclude-dirs: + - dist + max-issues-per-linter: 50 + max-same-issues: 5 diff --git a/.goreleaser.yml b/.goreleaser.yml index 6bc4a51..44656cd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,14 +2,6 @@ version: 2 project_name: unarr -# Pre-build hook: fetch static ffmpeg + ffprobe per platform so each -# release tarball ships them adjacent to the unarr binary. ResolveFFmpeg / -# ResolveFFprobe pick them up via the "adjacent to executable" branch — no -# system install or runtime download needed. -before: - hooks: - - bash scripts/download-ffmpeg-static.sh - builds: - main: ./cmd/unarr/ binary: unarr @@ -26,27 +18,13 @@ builds: - -s -w - -X github.com/torrentclaw/unarr/internal/cmd.Version={{.Version}} - -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }} - # Release-signing public key — verified by the self-updater against - # checksums.txt.sig. Empty when not configured; in that case - # signature verification is skipped and a warning is logged. - - -X github.com/torrentclaw/unarr/internal/upgrade.releasePubKeyBase64={{ .Env.RELEASE_SIGNING_PUBKEY }} archives: - - formats: [tar.gz] + - format: tar.gz name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format_overrides: - goos: windows - formats: [zip] - files: - - LICENSE* - - README* - # Bundle the matching ffmpeg + ffprobe (filename includes .exe on Windows - # because download-ffmpeg-static.sh writes ffmpeg.exe / ffprobe.exe there). - - src: "dist-ffbinaries/{{ .Os }}-{{ .Arch }}/*" - dst: . - strip_parent: true - info: - mode: 0o755 + format: zip checksum: name_template: "checksums.txt" @@ -59,22 +37,6 @@ changelog: - "^test:" - "^chore:" -# Self-hosted Forgejo at git.torrentclaw.com. goreleaser detects GITEA_TOKEN + -# these URLs and publishes the release there instead of GitHub. Reachable via -# `forgejo` hostname inside the dokploy-network (the runner shares it); for -# local goreleaser runs outside the network, override via env GITEA_API_URL. -# -# In goreleaser v2 `gitea_urls` is a top-level key (was nested under `release` -# in v1). -gitea_urls: - api: http://forgejo:3000/api/v1 - download: https://git.torrentclaw.com - skip_tls_verify: false - -release: - draft: false - prerelease: auto - # Homebrew tap — requires PAT with repo scope (not GITHUB_TOKEN) # Enable when torrentclaw/homebrew-tap PAT is configured as HOMEBREW_TAP_TOKEN # brews: diff --git a/.nojekyll b/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/CHANGELOG.md b/CHANGELOG.md index de1dd6e..0f49ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,584 +5,53 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.15] - 2026-05-27 - +## [Unreleased] ### Added - -- **sentry**: enhance error handling by skipping user input errors in CaptureError +- Init wizard with daemon install step (`unarr init`, replaces `unarr setup`) +- Interactive config menu with 7 categories (`unarr config [category]`) +- Migration wizard from Sonarr/Radarr/Prowlarr (`unarr migrate`) [pre-beta] + - Auto-detect instances via Docker, config files, port scan, Prowlarr + - Import download history and blocklist to avoid re-downloading + - Detect Plex/Jellyfin/Emby media servers and library paths + - Extract debrid tokens from *arr download clients + - JSON export with `--dry-run --json` +- Media server detection in `unarr init` (suggests library paths as download directory) +- `preferred_quality` setting in config (2160p/1080p/720p) +- Clean command to remove temp files, logs, and cached data (`unarr clean`) +- Daemon mode with background download management (`unarr start`) +- One-shot download command (`unarr download`) +- Stream to media player (`unarr stream`) +- Doctor command for diagnostics (`unarr doctor`) +- Status command for daemon monitoring (`unarr status`) +- Download engine with torrent support (debrid and usenet coming soon) +- File organization (Movies/TV Shows directory structure) +- Post-download verification +- Desktop notifications (Linux, macOS) +- Docker support with multi-stage build +- Cross-platform install scripts (shell, PowerShell) +- Dependabot for automated dependency updates +- golangci-lint configuration with gosec ### Changed +- Renamed `internal/commands/` to `internal/cmd/` -- **ci**: point Forgejo URLs at torrentclaw org (post-transfer) -- **sentry**: decouple agent import via string-match, rename predicate - -### Documentation - -- **positioning**: reframe unarr around download/stream/transcode, drop misleading search-first wording - -### Fixed - -- **ci**: unset GITHUB_TOKEN so goreleaser uses GITEA_TOKEN -- **sentry**: skip "daemon not running" stop/reload errors - -### Other - -- **scripts**: harden release.sh against double-release and inline version bumps -- untrack .claude/ (private local config) -## [0.9.14] - 2026-05-27 - +## [0.1.0] - 2025-02-14 ### Added - -- **vaapi**: hybrid CPU-scale + hwupload encode path (QW2, 0.9.14) - -### CI/CD - -- port workflows from .github/ to .forgejo/ (Forgejo Actions) - -### Fixed - -- **daemon**: defensive IsClosed check in watchSessionReady poll loop -- **daemon**: use parent ctx for MarkSessionReady so cancel propagates -- **release**: move gitea_urls to top-level (goreleaser v2 schema) -## [0.9.13] - 2026-05-27 - - -### Added - -- **agent**: session-ready webhook for SSE-driven player handshake (0.9.13) -- **agent**: send full transcoder diagnostic in register payload (0.9.12) - -### Fixed - -- **daemon**: defer probeCancel so a panic mid-diagnostic still releases ctx - -### Other - -- **release**: add ship.sh end-to-end pipeline as GH Actions backup -- **skills**: add /publish slash command + allow .claude/ in git -## [0.9.11] - 2026-05-27 - - -### Added - -- **hls**: pre-segmentación delantada — 2 s segments + async session start (0.9.10) -- **hls**: faster first-start — probe cache + tighter encoder presets (0.9.9) - -### Changed - -- **hls**: critico-driven hardening of fase 3.2 - -### Fixed - -- **cors**: allow play from .to / staging / onion mirrors -- **library**: classify resolution by width + height, not height alone -- **transcode**: make preset libx264-only + restore quality opt-in - -### Other - -- **release**: 0.9.11 -## [0.9.8] - 2026-05-27 - - -### Fixed - -- **upgrade**: break auto-apply restart loop (0.9.8) -## [0.9.7] - 2026-05-26 - - -### Added - -- **hls**: persistent fMP4 segment cache + integrity + stats (0.9.7) -## [0.9.6] - 2026-05-26 - - -### Added - -- **daemon**: auto-apply upgrades when server signals (0.9.6) -## [0.9.5] - 2026-05-26 - - -### Added - -- **funnel**: cloudflare quick tunnel embedded subprocess (0.9.5) -## [0.9.4] - 2026-05-26 - - -### Added - -- **stream**: retire WebRTC, HLS-only, bump 0.9.4 (**BREAKING**) -## [0.9.3] - 2026-05-26 - - -### Added - -- **usenet**: warn at startup when par2 or extractor is missing - -### Fixed - -- **engine**: truncate errorMessage before reporting status -- **hls**: clamp ffmpeg bitrate to the level we derive from outputHeight -## [0.9.2] - 2026-05-22 - - -### Added - -- **vpn**: unarr vpn command + report/arbitrate the WireGuard slot -## [0.9.1] - 2026-05-21 - - -### Added - -- **mirror**: update fallback URLs to use IPFS and remove GitHub Pages - -### Fixed - -- **security**: bump golang.org/x deps and add container CVE scan gate - -### Other - -- **release**: 0.9.1 -## [0.9.0] - 2026-05-21 - - -### Added - -- **agent**: add mirror failover, agent client refactor, status 401 detection -- **vpn**: local config_file for self-hosted/personal VPN testing -- **vpn**: split-tunnel torrent traffic through managed WireGuard - -### CI/CD - -- deploy install scripts to GitHub Pages - -### Documentation - -- **docker**: refresh Docker Hub README + sync description in CI - -### Fixed - -- **security**: CORS allowlist, URL scheme guard, state perms, ZIP slip, mirror docs -- **security**: UPnP opt-in, bounded SSE reader, signed self-update -- **security**: harden HLS session IDs, /health disclosure, archive password handling -- **upgrade**: fetch releases from TorrentClaw app, not GitHub - -### Other - -- **pages**: add .nojekyll to disable Jekyll processing -- **pages**: set custom domain unarr.torrentclaw.com -- **release**: 0.9.0 -## [0.8.1] - 2026-05-08 - - -### Added - -- **config**: set default values for WebRTC and transcoding in minimal TOML config -- **transcode**: dynamic H.264 level + HW probe + capability reporting - -### Changed - -- **streaming**: improve signal handling and remove unused components - -### Fixed - -- **self-update**: auto-restart live daemon after upgrade -- **streaming**: allow HLS sessions when webrtc disabled - -### Other - -- **gitignore**: add dist-ffbinaries to ignored files -- **release**: 0.8.1 -## [0.8.0] - 2026-05-08 - - -### Added - -- **mediainfo**: ResolveFFmpeg + DownloadFFmpeg mirroring ffprobe pattern -- **release**: bundle ffmpeg + ffprobe in tarballs and Docker image -- **seed-file**: unarr-side handler for browser-on-demand seeding (Fase 4.7.c) -- **stream**: per-session quality cap from web -- **stream**: real-time transcoding for non-browser-decodable codecs -- **stream**: pion-based WebRTC byte streamer for browser playback -- **streaming**: seek-restart, single-session, idle sweeper, probe.json -- **streaming**: add HLS transport pipeline (daemon side) -- **streaming**: ffmpeg transcoding pipeline (direct play / fMP4 / HW accel) -- **torrent**: act as WebTorrent peer for browser ↔ unarr P2P streaming -- **wstracker-probe**: -seed FILE mode for browser ↔ unarr e2e validation - -### Fixed - -- **streaming**: bounded ffmpeg auto-restart + tmpdir gc + probe/stderr safety -- **transcoder**: force aac stereo 48khz + frag_duration for mse compat -- **transcoder**: force main profile + setparams Rec.709 + serveRange wait -- **transcoder**: correct scale filter + always force yuv420p - -### Other - -- **release**: 0.8.0 -- **streaming**: post-review fixes — race lock, dead branch, stderr cap -- **torrent**: bump anacrolix log level Critical → Warning for visibility -## [0.7.0] - 2026-04-10 - - -### Added - -- **daemon**: enhance service management with start, stop, restart, and status commands for Windows - -### Other - -- **release**: 0.7.0 -## [0.6.8] - 2026-04-10 - - -### Added - -- **library**: add server-driven file deletion with allow_delete config - -### Other - -- **release**: 0.6.8 -## [0.6.7] - 2026-04-10 - - -### Added - -- **scan**: always scan downloads + organize dirs, deduplicate child paths - -### Other - -- **release**: 0.6.7 -## [0.6.6] - 2026-04-09 - - -### Fixed - -- **docker**: switch ffprobe download from johnvansickle.com to BtbN/FFmpeg-Builds -- **stream**: fix black screen on remote/Tailscale streaming - -### Other - -- **release**: 0.6.6 -## [0.6.5] - 2026-04-09 - - -### Fixed - -- **upgrade**: retry download on transient network errors with user feedback - -### Other - -- **release**: 0.6.5 -## [0.6.4] - 2026-04-09 - - -### Fixed - -- **daemon**: report error status when stream path is rejected - -### Other - -- **release**: 0.6.4 -## [0.6.3] - 2026-04-09 - - -### Fixed - -- **library**: use native arm64 ffprobe on Apple Silicon (osx-arm-64) - -### Other - -- **release**: 0.6.3 -## [0.6.2] - 2026-04-09 - - -### Added - -- **library**: resilient scan for large libraries and better ffprobe errors - -### Other - -- **release**: 0.6.2 -- ignore local config/ directory -## [0.6.1] - 2026-04-08 - - -### Added - -- **wake**: long-poll wake listener for instant CLI sync - -### Fixed - -- resolve deadlock, data races and path traversal vulnerabilities -## [0.6.0] - 2026-04-08 - - -### Added - -- **sync**: replace WS+DO transport with unified HTTP sync - -### Fixed - -- **ws**: add ping/pong keepalive and read deadline to detect zombie connections - -### Other - -- **release**: 0.6.0 -## [0.5.5] - 2026-04-07 - - -### Added - -- **agent**: send stream port and IPs in register request -- **stream**: report duration and position in watch progress -- **stream**: trackingReader with byte-based progress and rate limiting - -### Fixed - -- **daemon**: cancel watch reporter on stream switch and re-notify ready - -### Other - -- **release**: 0.5.5 -## [0.5.4] - 2026-04-07 - - -### Fixed - -- **stream**: use platform-specific socket options for Windows cross-compilation - -### Other - -- **release**: 0.5.4 -## [0.5.3] - 2026-04-07 - - -### Added - -- **stream**: persistent stream server with file swapping - -### Other - -- **release**: 0.5.3 -## [0.5.2] - 2026-04-07 - - -### Added - -- **stream**: report multi-network URLs for smart resolution - -### Other - -- **release**: 0.5.2 -## [0.5.1] - 2026-04-07 - - -### Added - -- **daemon**: add on-demand library scan via heartbeat and WebSocket - -### Fixed - -- **agent**: add retry with backoff and WebSocket connect for daemon registration -- **daemon**: report failed status on stream request errors -- **daemon**: use correct systemd user target and isolate test cache -- **stream**: prevent duplicate events from killing active stream server - -### Other - -- **release**: 0.5.1 -## [0.5.0] - 2026-04-06 - - -### Added - -- **organize**: use server metadata for file organization and subtitle handling -- **stream**: add NAT-PMP port mapping for remote downloads - -### Other - -- **release**: 0.5.0 -- **release**: add changelog generation and release automation -## [0.4.1] - 2026-04-01 - - -### Added - -- **cli**: add login command and refactor shared helpers -- **stream**: report watch progress to API via HTTP Range tracking - -### Fixed - -- **ci**: fix lint errors and pin CI to Go 1.25 -- **lint**: remove unused newStubCmd function - -### Other - -- **cli**: remove moreseed stub command -- **cli**: remove redundant stub commands (monitor, open, add, compare) -## [0.4.0] - 2026-03-31 - - -### Added - -- **cli**: upgrade command, rich status, and version cache - -### Fixed - -- **progress**: always report status transitions and poll for control signals -## [0.3.7] - 2026-03-31 - - -### CI/CD - -- **docker**: remove dockerhub-description sync step -## [0.3.6] - 2026-03-31 - - -### CI/CD - -- **deps**: bump docker/metadata-action from 5 to 6 -- **deps**: bump docker/setup-qemu-action from 3 to 4 -- **deps**: bump docker/login-action from 3 to 4 -- **deps**: bump docker/build-push-action from 6 to 7 -- **deps**: bump codecov/codecov-action from 5 to 6 -- **docker**: add Docker Hub description sync and DOCKERHUB.md - -### Fixed - -- **ci**: upgrade golangci-lint to v2.11.3 for Go 1.25 support -- **docker**: upgrade alpine packages to patch CVE-2025-60876 and CVE-2026-27171 -- **lint**: use default:none to disable errcheck, fix all gofmt and exhaustive -- **lint**: disable errcheck, tune gosec/exclusions for codebase state -- **lint**: configure linters for codebase maturity, fix gofmt and ineffassign -- **lint**: exclude common fire-and-forget patterns from errcheck -- **lint**: resolve errcheck and bodyclose warnings for golangci-lint v2 -## [0.3.5] - 2026-03-30 - - -### Changed - -- migrate lint config to v2, remove daemon auto-upgrade, add trust badges -## [0.3.3] - 2026-03-30 - - -### Fixed - -- **ci**: remove go-client checkout steps -## [0.3.2] - 2026-03-30 - - -### Added - -- **init**: add 60s countdown, skip key, and cancel detection to browser auth - -### CI/CD - -- **release**: add Docker Hub publish and VirusTotal scan jobs - -### Documentation - -- add beta notice, fix install URLs to get.torrentclaw.com - -### Fixed - -- **ci**: fix virustotal job condition syntax -- **docker**: simplify Dockerfile for CI builds (no local go-client) -- **release**: disable homebrew tap (needs PAT, not GITHUB_TOKEN) - -### Other - -- re-enable homebrew tap in goreleaser -## [0.3.1] - 2026-03-30 - - -### Fixed - -- **build**: unused variable in Windows process check -- **release**: disable homebrew tap until repo is created - -### Other - -- rename module from torrentclaw-cli to unarr - -### Build - -- remove UPX compression (antivirus false positives, startup penalty) -## [0.3.0] - 2026-03-29 - - -### Added - -- **agent**: add WebSocket transport with HTTP fallback -- **auth**: browser-based CLI authentication (like Claude Code) -- **daemon**: add auto-scan, force start, and stall timeout default -- **debrid**: add HTTPS downloader for debrid direct URLs -- **stream**: UPnP port forwarding for remote video playback -- **usenet**: implement full NNTP download pipeline -- add migrate command, media server detection, and debrid auto-config -- replace setup with init wizard + interactive config menu -- add clean command to remove temp files, logs, and cached data -- add Sentry error reporting -- improve daemon resilience, streaming, and usenet downloads -- initial commit — unarr CLI - -### Changed - -- extract BuildSyncItems to library package, remove duplication - -### Documentation - -- improve CLI help, shell completion, and README - -### Fixed - -- **torrent**: expand tracker list, add DHT persistence and configurable timeouts -- force-start tasks bypass HasCapacity check in dispatch loop -- add panic recovery to auto-scan, cap DHT nodes at 200 -- harden usenet/debrid downloaders from critico review - -### Build - -- add -s -w -trimpath to Makefile, add build-small target with UPX -[0.9.15]: https://github.com/torrentclaw/unarr/compare/v0.9.14...v0.9.15 -[0.9.14]: https://github.com/torrentclaw/unarr/compare/v0.9.13...v0.9.14 -[0.9.13]: https://github.com/torrentclaw/unarr/compare/v0.9.11...v0.9.13 -[0.9.11]: https://github.com/torrentclaw/unarr/compare/v0.9.8...v0.9.11 -[0.9.8]: https://github.com/torrentclaw/unarr/compare/v0.9.7...v0.9.8 -[0.9.7]: https://github.com/torrentclaw/unarr/compare/v0.9.6...v0.9.7 -[0.9.6]: https://github.com/torrentclaw/unarr/compare/v0.9.5...v0.9.6 -[0.9.5]: https://github.com/torrentclaw/unarr/compare/v0.9.4...v0.9.5 -[0.9.4]: https://github.com/torrentclaw/unarr/compare/v0.9.3...v0.9.4 -[0.9.3]: https://github.com/torrentclaw/unarr/compare/v0.9.2...v0.9.3 -[0.9.2]: https://github.com/torrentclaw/unarr/compare/v0.9.1...v0.9.2 -[0.9.1]: https://github.com/torrentclaw/unarr/compare/v0.9.0...v0.9.1 -[0.9.0]: https://github.com/torrentclaw/unarr/compare/v0.8.1...v0.9.0 -[0.8.1]: https://github.com/torrentclaw/unarr/compare/v0.8.0...v0.8.1 -[0.8.0]: https://github.com/torrentclaw/unarr/compare/v0.7.0...v0.8.0 -[0.7.0]: https://github.com/torrentclaw/unarr/compare/v0.6.8...v0.7.0 -[0.6.8]: https://github.com/torrentclaw/unarr/compare/v0.6.7...v0.6.8 -[0.6.7]: https://github.com/torrentclaw/unarr/compare/v0.6.6...v0.6.7 -[0.6.6]: https://github.com/torrentclaw/unarr/compare/v0.6.5...v0.6.6 -[0.6.5]: https://github.com/torrentclaw/unarr/compare/v0.6.4...v0.6.5 -[0.6.4]: https://github.com/torrentclaw/unarr/compare/v0.6.3...v0.6.4 -[0.6.3]: https://github.com/torrentclaw/unarr/compare/v0.6.2...v0.6.3 -[0.6.2]: https://github.com/torrentclaw/unarr/compare/v0.6.1...v0.6.2 -[0.6.1]: https://github.com/torrentclaw/unarr/compare/v0.6.0...v0.6.1 -[0.6.0]: https://github.com/torrentclaw/unarr/compare/v0.5.5...v0.6.0 -[0.5.5]: https://github.com/torrentclaw/unarr/compare/v0.5.4...v0.5.5 -[0.5.4]: https://github.com/torrentclaw/unarr/compare/v0.5.3...v0.5.4 -[0.5.3]: https://github.com/torrentclaw/unarr/compare/v0.5.2...v0.5.3 -[0.5.2]: https://github.com/torrentclaw/unarr/compare/v0.5.1...v0.5.2 -[0.5.1]: https://github.com/torrentclaw/unarr/compare/v0.5.0...v0.5.1 -[0.5.0]: https://github.com/torrentclaw/unarr/compare/v0.4.1...v0.5.0 -[0.4.1]: https://github.com/torrentclaw/unarr/compare/v0.4.0...v0.4.1 -[0.4.0]: https://github.com/torrentclaw/unarr/compare/v0.3.7...v0.4.0 -[0.3.7]: https://github.com/torrentclaw/unarr/compare/v0.3.6...v0.3.7 -[0.3.6]: https://github.com/torrentclaw/unarr/compare/v0.3.5...v0.3.6 -[0.3.5]: https://github.com/torrentclaw/unarr/compare/v0.3.3...v0.3.5 -[0.3.3]: https://github.com/torrentclaw/unarr/compare/v0.3.2...v0.3.3 -[0.3.2]: https://github.com/torrentclaw/unarr/compare/v0.3.1...v0.3.2 -[0.3.1]: https://github.com/torrentclaw/unarr/compare/v0.3.0...v0.3.1 -[0.3.0]: https://github.com/torrentclaw/unarr/releases/tag/v0.3.0 - +- Initial release +- Search across 30+ torrent sources with advanced filters +- TrueSpec torrent inspection (quality, codec, seeds, score) +- Watch command (streaming providers + torrent alternatives) +- Popular and recent content browsing +- System statistics +- Interactive configuration +- JSON output mode (`--json`) for scripting +- Colored terminal output with `--no-color` support +- Homebrew tap distribution +- GoReleaser with UPX compression +- CI pipeline (test, build, lint, vet) +- Lefthook git hooks (gofmt, go vet, conventional commits) + +[Unreleased]: https://github.com/torrentclaw/unarr/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/torrentclaw/unarr/releases/tag/v0.1.0 diff --git a/CNAME b/CNAME deleted file mode 100644 index d892572..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -unarr.torrentclaw.com \ No newline at end of file diff --git a/DOCKERHUB.md b/DOCKERHUB.md deleted file mode 100644 index 3df5b70..0000000 --- a/DOCKERHUB.md +++ /dev/null @@ -1,200 +0,0 @@ -# unarr - -**The single binary that replaces your whole *arr stack.** Built-in torrent, -debrid, and usenet engines. Stream, transcode, and organize your library from -one terminal — or run it as a headless daemon with a web dashboard, WireGuard -split-tunnel, and Cloudflare Funnel remote access. - -**[Website & docs](https://torrentclaw.com/unarr)** · **[Install guide](https://torrentclaw.com/cli)** · **[Get an API key](https://torrentclaw.com)** - -> Powered by [TorrentClaw](https://torrentclaw.com) — an aggregator that unifies -> YTS, EZTV, Knaben, Torrentio, Bitmagnet and more, enriched with TMDB metadata -> and a 0–100 quality score per release. - ---- - -## Quick start - -### 1. First-time setup (interactive wizard) - -```bash -docker run -it --rm \ - -v ~/.config/unarr:/config \ - torrentclaw/unarr setup -``` - -The wizard asks for your TorrentClaw API key (free at -[torrentclaw.com](https://torrentclaw.com)) and your download directory. - -### 2. Run the daemon - -```bash -docker run -d --name unarr \ - --restart unless-stopped \ - --network host \ - --read-only --memory 512m \ - -v ~/.config/unarr:/config \ - -v ~/Media:/downloads \ - torrentclaw/unarr -``` - -That's it — `unarr` now runs headless, watching for jobs and managing downloads. - ---- - -## Docker Compose - -```yaml -services: - unarr: - image: torrentclaw/unarr:latest - container_name: unarr - restart: unless-stopped - user: "1000:1000" - read_only: true - tmpfs: - - /tmp:size=64m,mode=1777 - volumes: - - ./config:/config - - ~/Media:/downloads - - unarr-data:/data - environment: - - TZ=UTC - # - UNARR_API_KEY=tc_your_key_here - network_mode: host # recommended for full P2P performance - deploy: - resources: - limits: - memory: 512M - cpus: "2.0" - -volumes: - unarr-data: -``` - -```bash -docker compose run --rm unarr setup # one-time wizard -docker compose up -d # start the daemon -``` - ---- - -## Volumes - -| Path | Purpose | -|--------------|--------------------------------------------------| -| `/config` | Configuration file (`config.toml`) | -| `/downloads` | Finished media downloads | -| `/data` | Internal state: torrent metadata, cache | - -## Environment variables - -| Variable | Description | Default | -|------------------------|--------------------------------------|---------------------------| -| `UNARR_API_KEY` | TorrentClaw API key | from config | -| `UNARR_API_URL` | API endpoint | `https://torrentclaw.com` | -| `UNARR_DOWNLOAD_DIR` | Download directory | `/downloads` | -| `UNARR_CONFIG_DIR` | Config directory | `/config` | -| `UNARR_COUNTRY` | Country code (ISO 3166) | `US` | -| `TZ` | Timezone | `UTC` | - -Any config value can be overridden by its matching `UNARR_*` environment variable. - -## Networking - -**Host mode (recommended)** — full P2P performance, no port mapping: - -```yaml -network_mode: host -``` - -**Bridge mode** — more isolated, but you must expose the BitTorrent ports: - -```yaml -ports: - - "6881-6889:6881-6889/tcp" - - "6881-6889:6881-6889/udp" -``` - -## Running commands - -Use `docker exec` for one-off commands while the daemon is running: - -```bash -docker exec unarr unarr search "inception" --quality 1080p -docker exec unarr unarr popular --limit 10 -docker exec unarr unarr status -docker exec unarr unarr doctor # diagnose config / connectivity -``` - ---- - -## Tags - -| Tag | Description | -|----------|--------------------------------------------------| -| `latest` | Latest stable release | -| `X.Y.Z` | Exact version (e.g. `0.9.0`) | -| `X.Y` | Latest patch within a minor (e.g. `0.9`) | - -Pin a tag in production (`torrentclaw/unarr:0.9.0`) for reproducible deploys. - -## Supported architectures - -Multi-arch image — Docker pulls the right one automatically: - -- `linux/amd64` -- `linux/arm64` (Apple Silicon, Raspberry Pi 4/5, ARM servers) - -## Image details - -- **Base:** Alpine 3.22 (minimal, regularly patched) -- **User:** `unarr` (UID 1000, GID 1000) — runs as **non-root** -- **Entrypoint:** `unarr start` (daemon mode) -- **Read-only rootfs** — only mounted volumes are writable -- **Bundled `ffmpeg` / `ffprobe`** for media inspection — nothing else to install -- **Self-contained updates** — binaries are served from TorrentClaw's own - infrastructure, no third-party registry dependency - ---- - -## Other install methods - -Not using Docker? Install the native binary instead: - -```bash -# Linux / macOS -curl -fsSL https://torrentclaw.com/install.sh | sh - -# Windows (PowerShell) -irm https://torrentclaw.com/install.ps1 | iex - -# Go toolchain -go install github.com/torrentclaw/unarr/cmd/unarr@latest -``` - -## Mirrors - -The installer and release binaries are served from every TorrentClaw mirror, so -you can install even if one domain is blocked in your region. Each mirror is -self-contained (it serves its own binaries — no cross-domain dependency): - -| Mirror | Install command | -|--------|-----------------| -| `torrentclaw.com` (primary) | `curl -fsSL https://torrentclaw.com/install.sh \| sh` | -| `torrentclaw.to` | `curl -fsSL https://torrentclaw.to/install.sh \| sh` | -| Tor (`.onion`) | `torsocks sh -c "$(curl http://torrentf3aifidcsaaanmnmuhv2s53r6hqsl3zkmfidiaxainkeqk5id.onion/install.sh)"` | - -The Tor address routes everything (install script + binaries) through the hidden -service, so no clearnet exit is needed. - -## Links - -- **Website & docs:** https://torrentclaw.com/unarr -- **CLI install guide:** https://torrentclaw.com/cli -- **API & account:** https://torrentclaw.com -- **Mirror status:** https://torrentclaw.com/mirrors - -## License - -MIT. diff --git a/Dockerfile b/Dockerfile index 64ea4e2..ff5cdea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,28 +16,9 @@ ARG VERSION=dev RUN CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/torrentclaw/unarr/internal/cmd.Version=${VERSION}" -trimpath -o /unarr ./cmd/unarr/ # ---- Runtime stage ---- -FROM alpine:3.22 +FROM alpine:3.21 -# Use Alpine's native musl ffmpeg + ffprobe instead of the johnvansickle / -# BtbN static glibc builds — those need a glibc shim on Alpine and the -# vector-math symbols the GPL builds reference are not satisfiable by -# gcompat. Alpine ships ffmpeg ~7.x which is fine for the HLS transcoding -# pipeline (libx264 + libfdk-aac alternatives included). -RUN apk upgrade --no-cache && \ - apk add --no-cache ca-certificates tzdata ffmpeg wget - -# Bundle cloudflared so `unarr funnel on` (default: on, see config defaults) -# Just Works on a headless container with no first-run network round-trip. -# TARGETARCH is set automatically by Docker buildx during cross-builds. -ARG TARGETARCH=amd64 -RUN case "$TARGETARCH" in \ - amd64) CF_ARCH=amd64 ;; \ - arm64) CF_ARCH=arm64 ;; \ - arm) CF_ARCH=armhf ;; \ - *) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \ - esac && \ - wget -qO /usr/local/bin/cloudflared "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-$CF_ARCH" && \ - chmod +x /usr/local/bin/cloudflared +RUN apk add --no-cache ca-certificates tzdata # Non-root user (UID 1000 matches typical host user for volume permissions) RUN addgroup -g 1000 unarr && adduser -u 1000 -G unarr -D -h /home/unarr unarr diff --git a/Docs/plans/library-sync.md b/Docs/plans/library-sync.md deleted file mode 100644 index 509e87a..0000000 --- a/Docs/plans/library-sync.md +++ /dev/null @@ -1,170 +0,0 @@ -# Plan: Sincronización bidireccional de biblioteca (CLI ↔ Web) - -## Context -La biblioteca web solo muestra descargas completadas (download_task + debrid). El `unarr scan` escanea ficheros con ffprobe y los sube al servidor, pero solo soporta un path, no detecta borrados del disco, y no permite borrar ficheros desde la web. El usuario quiere una biblioteca unificada que refleje el estado real de su colección y se sincronice en ambas direcciones. - -## Protocolo de sincronización - -### Forward Sync (Disco → Web) -1. CLI escanea todos los `ScanPaths` configurados -2. Para cada path: descubre ficheros, compara con cache (skip ffprobe si no cambió), sube a `/library-sync` -3. En `isLastBatch=true`: el servidor elimina items con ese `scanPath` que no estén en el batch (ficheros borrados del disco desaparecen de la web) - -### Reverse Sync (Web → Disco) -1. CLI llama a `GET /agent/library-deletions` — items que el usuario soft-deleted desde la web -2. Si `AutoDelete=true` o `--yes`: borra ficheros del disco -3. Si no: muestra lista y pide confirmación interactiva -4. Llama a `POST /agent/library-deletions/confirm` con los IDs confirmados → hard-delete en DB - -### Resolución de conflictos -- Fichero en disco pero no en web → forward sync lo añade -- Fichero en web pero no en disco → forward sync lo elimina (isLastBatch) -- Soft-deleted en web, aún en disco → reverse sync lo borra del disco y confirma -- Soft-deleted en web, ya borrado del disco → reverse sync confirma directamente -- Race condition (user borra en web mientras CLI escanea) → forward sync skippea rows con `deleted_at IS NOT NULL` - ---- - -## Fase 1: Multi-path + Forward Sync mejorado - -### 1.1 CLI — Config multi-path -**Archivo:** `torrentclaw-cli/internal/config/config.go` -- Añadir `ScanPaths []string` a `LibraryConfig` -- Migrar `ScanPath` → `ScanPaths[0]` en `Load()` si `ScanPaths` está vacío -- Añadir `AutoDelete bool` (default false) - -### 1.2 CLI — Cache v2 -**Archivo:** `torrentclaw-cli/internal/library/types.go` -- Cambiar `LibraryCache` a version 2: `Paths map[string][]LibraryItem` -- Migración v1→v2: `Path`+items → `Paths[Path]` - -**Archivo:** `torrentclaw-cli/internal/library/cache.go` -- `LoadCache` detecta versión y migra -- `SaveCache` siempre guarda v2 - -### 1.3 CLI — Scan multi-path -**Archivo:** `torrentclaw-cli/internal/cmd/scan.go` -- `unarr scan` sin args → escanea todos los `ScanPaths` -- `unarr scan /path/a /path/b` → escanea paths específicos y los recuerda en config -- Loop: para cada path, scan + sync con su `scanPath` - -### 1.4 CLI — Nuevo comando `unarr sync` -**Archivo nuevo:** `torrentclaw-cli/internal/cmd/sync.go` -- Forward sync: scan ligero (sin ffprobe para ficheros sin cambios) + upload -- Sin reverse sync todavía (Fase 3) -- Flags: `--dry-run`, `--paths` - -### 1.5 Web — Columna `scan_path` en `library_item` -**Archivo:** `torrentclaw-web/src/lib/db/schema.ts` -- Añadir `scanPath: varchar(2048)` a tabla `libraryItem` -- Generar migración con `pnpm db:generate` - -**Archivo:** `torrentclaw-web/src/lib/services/library-upgrade.ts` -- `syncLibraryItems()`: persistir `scanPath` en cada row al hacer upsert - -### 1.6 CLI — Daemon multi-path -**Archivo:** `torrentclaw-cli/internal/cmd/daemon.go` -- `runAutoScan()` itera sobre todos los `ScanPaths` - ---- - -## Fase 2: Reverse Sync (Web → Disco) - -### 2.1 Web — Soft-delete -**Archivo:** `torrentclaw-web/src/lib/db/schema.ts` -- Añadir `deletedAt: timestamp` a tabla `libraryItem` -- Generar migración - -### 2.2 Web — Endpoints de borrado -**Archivo nuevo:** `torrentclaw-web/src/app/api/internal/library/items/route.ts` -- `DELETE` — session auth, recibe `{itemIds: number[]}`, hace soft-delete (`deletedAt = NOW()`) - -**Archivo nuevo:** `torrentclaw-web/src/app/api/internal/agent/library-deletions/route.ts` -- `GET` — agent auth, devuelve items con `deletedAt IS NOT NULL` para ese usuario -- `POST` — agent auth, recibe `{confirmedIds: number[]}`, hard-delete los rows - -### 2.3 Web — Heartbeat con pendingDeletions -**Archivo:** endpoint de heartbeat del agente -- Añadir `pendingDeletions: number` al response (count de items con `deletedAt IS NOT NULL`) - -### 2.4 Web — Forward sync respeta soft-deletes -**Archivo:** `torrentclaw-web/src/lib/services/library-upgrade.ts` -- `syncLibraryItems()` en `isLastBatch`: la query de DELETE excluye rows con `deletedAt IS NOT NULL` - -### 2.5 CLI — Agent client nuevos métodos -**Archivo:** `torrentclaw-cli/internal/agent/client.go` -- `GetLibraryDeletions(ctx) → []DeletionItem` -- `ConfirmLibraryDeletions(ctx, ids []int) → error` - -**Archivo:** `torrentclaw-cli/internal/agent/types.go` -- `DeletionItem {ID int, FilePath string, DeletedAt string}` - -### 2.6 CLI — Sync reverse -**Archivo:** `torrentclaw-cli/internal/cmd/sync.go` -- Después del forward sync: llama a `GetLibraryDeletions()` -- Valida que cada fichero está dentro de un `ScanPaths` conocido (seguridad) -- Si `AutoDelete` o `--yes`: borra y confirma -- Si no: muestra lista interactiva, pide confirmación -- Flag `--no-delete` para skip reverse sync -- Si `BackupDir` configurado: mover a backup en vez de borrar - -### 2.7 CLI — Daemon auto-delete -**Archivo:** `torrentclaw-cli/internal/cmd/daemon.go` -- Al final de `runAutoSync()`: si `AutoDelete=true`, procesa deletions automáticamente -- Si no: log warning "N files pending deletion, run `unarr sync`" - ---- - -## Fase 3: Web UI (brief) - -- Botón "Eliminar" en items de biblioteca → llama `DELETE /library/items` -- Badge "Pendiente de borrar" en items soft-deleted -- Posibilidad de cancelar el borrado (clear `deletedAt`) -- Vista unificada: scanned items + downloaded items en la misma vista - ---- - -## Archivos clave - -### CLI (Go) -| Archivo | Cambio | -|---------|--------| -| `internal/config/config.go` | ScanPaths, AutoDelete, migración | -| `internal/library/types.go` | Cache v2 con Paths map | -| `internal/library/cache.go` | Load/Save v2, migración v1 | -| `internal/library/sync.go` | BuildSyncItems (sin cambios) | -| `internal/cmd/scan.go` | Multi-path loop | -| `internal/cmd/sync.go` | **Nuevo** — comando sync bidireccional | -| `internal/cmd/daemon.go` | runAutoSync multi-path + reverse | -| `internal/agent/client.go` | GetLibraryDeletions, ConfirmLibraryDeletions | -| `internal/agent/types.go` | DeletionItem type | - -### Web (TypeScript) -| Archivo | Cambio | -|---------|--------| -| `src/lib/db/schema.ts` | scanPath + deletedAt en library_item | -| `src/lib/services/library-upgrade.ts` | persistir scanPath, respetar soft-deletes | -| `src/app/api/internal/agent/library-deletions/route.ts` | **Nuevo** — GET + POST | -| `src/app/api/internal/library/items/route.ts` | **Nuevo** — DELETE soft-delete | -| Endpoint heartbeat del agente | pendingDeletions en response | - ---- - -## Verificación - -### Fase 1 -1. `go build ./cmd/unarr/ && go test ./...` -2. Configurar 2 scan paths en config.toml, ejecutar `unarr scan` → ambos se escanean -3. Borrar un fichero del disco, ejecutar `unarr scan` → desaparece de la web -4. `pnpm build` en torrentclaw-web para verificar tipos - -### Fase 2 -1. Desde la web: borrar un item de la biblioteca -2. Ejecutar `unarr sync` → muestra el fichero pendiente de borrar, pedir confirmación -3. Confirmar → fichero se borra del disco y desaparece de la web -4. `unarr sync --dry-run` → muestra lo que haría sin hacer nada -5. Con `auto_delete = true` en config: el daemon borra automáticamente - -### Fase 3 -1. Verificar visualmente en Chrome DevTools la UI de borrado -2. Verificar que el badge "pendiente" aparece y desaparece correctamente diff --git a/Docs/plans/security-stream-token.md b/Docs/plans/security-stream-token.md deleted file mode 100644 index 1a08e21..0000000 --- a/Docs/plans/security-stream-token.md +++ /dev/null @@ -1,131 +0,0 @@ -# Phase 2.2 — Per-task stream token (deferred) - -Status: deferred. Requires coordinated change in the web app -(`torrentclaw-web`) and the CLI daemon. Pulled out of the Phase 2 -security pass because the CLI-only fixes (UPnP opt-in, SSE caps, -signed self-update) ship without web-side work; the stream-token -work cannot. - -## Problem - -`/stream`, `/playlist.m3u` and `/hls//...` on the daemon -HTTP server have no authentication. Today, anyone who can reach the -listener and guesses (or learns) the `taskID` (for `/stream`) or -`sessionID` (for `/hls`) can fetch the active file. - -Mitigations already in place after Phase 1+2: - -- `sessionID` is restricted to a safe regex and is a server-issued - UUID v4 (122-bit entropy, not enumerable in practice). -- `/health` no longer leaks the active filename, taskID prefix or - client IP to remote callers (loopback diagnostics preserved). -- UPnP is opt-in, so by default the daemon is not exposed to the - public internet. -- The web client probes `/health` to pick LAN vs Tailscale. - -Residual risk: - -- On a shared LAN (open Wi-Fi, office network, dorm) any device can - reach the listener and brute-force `?id=` against - `/stream`. taskIDs are also UUIDs, so this is high entropy, but - the URL may leak through browser history, sharing, screen capture - or a passive logger and there is no second factor. -- A user who explicitly opts into UPnP exposes the same surface to - the entire internet. - -A per-task secret carried in the URL closes this without breaking -the `