diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..82ee799 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,105 @@ +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 new file mode 100644 index 0000000..34cc3d6 --- /dev/null +++ b/.forgejo/workflows/docker-rebuild.yml @@ -0,0 +1,61 @@ +# 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 new file mode 100644 index 0000000..d757612 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,118 @@ +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 deleted file mode 100644 index e9d5ea0..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,101 +0,0 @@ -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: 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: 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: 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.11.3 - - coverage: - name: Coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - 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: - files: ./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: 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 deleted file mode 100644 index e6ba0ba..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,162 +0,0 @@ -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@v6 - 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 a9f3162..8015bab 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,12 @@ 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/.goreleaser.yml b/.goreleaser.yml index 44656cd..6bc4a51 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,6 +2,14 @@ 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 @@ -18,13 +26,27 @@ 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: - - format: tar.gz + - formats: [tar.gz] name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format_overrides: - goos: windows - format: zip + 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 checksum: name_template: "checksums.txt" @@ -37,6 +59,22 @@ 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 new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f49ea9..de1dd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,53 +5,584 @@ 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). -## [Unreleased] +## [0.9.15] - 2026-05-27 + ### Added -- 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 + +- **sentry**: enhance error handling by skipping user input errors in CaptureError ### Changed -- Renamed `internal/commands/` to `internal/cmd/` -## [0.1.0] - 2025-02-14 +- **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 + ### Added -- 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 +- **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 + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..d892572 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +unarr.torrentclaw.com \ No newline at end of file diff --git a/DOCKERHUB.md b/DOCKERHUB.md new file mode 100644 index 0000000..3df5b70 --- /dev/null +++ b/DOCKERHUB.md @@ -0,0 +1,200 @@ +# 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 ff5cdea..64ea4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,9 +16,28 @@ 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.21 +FROM alpine:3.22 -RUN apk add --no-cache ca-certificates tzdata +# 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 # 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 new file mode 100644 index 0000000..509e87a --- /dev/null +++ b/Docs/plans/library-sync.md @@ -0,0 +1,170 @@ +# 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 new file mode 100644 index 0000000..1a08e21 --- /dev/null +++ b/Docs/plans/security-stream-token.md @@ -0,0 +1,131 @@ +# 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 `