Commit graph

136 commits

Author SHA1 Message Date
dependabot[bot]
06cd6f58b6 chore(deps): bump golang.org/x/term from 0.41.0 to 0.43.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.41.0 to 0.43.0.
- [Commits](https://github.com/golang/term/compare/v0.41.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 05:46:32 +00:00
Deivid Soto
e89b647dfa chore(release): 0.8.1
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.8.1
- Update CHANGELOG.md
2026-05-08 17:23:19 +02:00
Deivid Soto
26814ff6f7 feat(config): set default values for WebRTC and transcoding in minimal TOML config 2026-05-08 17:21:53 +02:00
Deivid Soto
209ea38ecf feat(transcode): dynamic H.264 level + HW probe + capability reporting
Three related fixes around 4K-source transcoding that left the web
player stuck on "preparing session" with no useful diagnostics:

1. Dynamic -level:v derived from output height (hls.go, transcoder.go).
   The previous fixed "4.0" silently rejected anything taller than 1080p
   inside libx264 — "frame MB size > level limit", "DPB size > level
   limit" — and emitted unplayable segments. Helper H264LevelForHeight()
   now picks 4.0 / 5.0 / 5.1 / 6.0 from the actual encode height.

2. New `unarr probe-hwaccel` diagnostic command. Lists the HW encoders
   compiled into ffmpeg, the device files / drivers present, and the
   backend the daemon would actually pick today. Surfaces the canonical
   gotcha: a host with an RTX 3090 + nvidia-smi but a Homebrew ffmpeg
   built without --enable-nvenc still falls back to libx264 software.

3. Register payload now includes hwAccel + maxTranscodeHeight so the web
   side can suggest a smaller alternate quality before the user even
   tries to play a 4K source on a software-only host. Software-only =
   1080p cap, any HW backend = 2160p cap.
2026-05-08 15:57:02 +02:00
Deivid Soto
01941ed2e4 fix(streaming): allow HLS sessions when webrtc disabled
OnWebRTCSession gated cfg.Download.WebRTC.Enabled before the
transport=="hls" branch, so HLS sessions were rejected even though
they only need ffmpeg + StreamServer (no WebRTC peer).

Reorder: path validation first, then HLS branch, then WebRTC.Enabled
gate (only for DataChannel transport). HLS now works without enabling
[downloads.webrtc].
2026-05-08 12:44:06 +02:00
Deivid Soto
6ce743c39d fix(self-update): auto-restart live daemon after upgrade
Old isRunningAsDaemon() only matched "start" in argv — never true for
`unarr self-update`, so the daemon kept running the old binary in memory
and heartbeat reported the stale version (web gated features wrong).

Now: detect live daemon via state file + isDaemonAlive (PID alive +
heartbeat fresh), call runDaemonSvcRestart through the system service
manager. On failure show clear manual recovery command instead of
leaving the daemon dead. No-op when daemon is not running.
2026-05-08 12:43:59 +02:00
Deivid Soto
75df0e4308 refactor(streaming): improve signal handling and remove unused components 2026-05-08 12:39:07 +02:00
Deivid Soto
c5d4c4f3e3 chore(gitignore): add dist-ffbinaries to ignored files 2026-05-08 11:29:54 +02:00
Deivid Soto
36bd9edbeb chore(release): 0.8.0
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.8.0
- Update CHANGELOG.md
2026-05-08 11:27:47 +02:00
Deivid Soto
4ed95f5f4c chore(streaming): post-review fixes — race lock, dead branch, stderr cap
Follow-ups from /critico review on commits eb2548f + 40e7977. No
functional change.

- engine/hls.go restartFromSegment now reads `s.exited` under
  `readyMu`. The field is documented as readyMu-protected (see field
  declaration) and writers in waitFFmpeg / pollSegments hold the lock
  consistently; the previous direct read produced a `go test -race`
  warning under concurrent restart paths.
- engine/hls.go renderMasterPlaylist drops the `defaultIdx := -1`
  branch that was unreachable (no rendition was ever flagged DEFAULT
  or AUTOSELECT). Output is unchanged; the source is just shorter.
- engine/hls.go subtitle "(forzados)" suffix → "(forced)". Daemon
  convention is English; the web client localises if needed.
- engine/hls.go hlsStderrCapture now also caps single-write payloads
  larger than maxStderrBuf (was only capping the cumulative buffer).
- engine/hls.go waitFFmpeg restart-window reset drops the redundant
  `!IsZero` guard — a zero time is far enough in the past that the
  `> restartWindow` branch covers it.
2026-05-08 09:27:08 +02:00
Deivid Soto
40e7977cf5 fix(streaming): bounded ffmpeg auto-restart + tmpdir gc + probe/stderr safety
Reliability hardening pass for the HLS daemon. None of these change the
public API, all reduce the chances of an end-user seeing a broken
session in production.

- engine/hls.go waitFFmpeg now supervises ffmpeg: on a non-graceful
  exit while the session is still in use, restart from the last good
  segment up to 3 times within a 60 s window. Beyond that we give up
  and log the file as broken — better than a perpetually black player
  with no error.
- engine/hls.go CleanupHLSOrphanDirs() removes tmpdirs older than 1 h
  at startup; cmd/daemon.go calls it before streamSrv.Listen so a
  daemon crash + restart doesn't leak gigabytes of segment files.
- engine/hls.go StartHLSSession wraps ffprobe in a 15 s timeout. A
  hung probe on a slow remote fs would otherwise block the goroutine
  forever and the player would stay on "Preparando sesion".
- engine/hls.go hlsStderrCapture buffer is capped at 64 KiB; a
  misbehaving ffmpeg that emits megabytes without newlines used to
  grow daemon memory unbounded.
2026-05-08 08:51:19 +02:00
Deivid Soto
eb2548f9a6 feat(streaming): seek-restart, single-session, idle sweeper, probe.json
Follow-ups on the daemon HLS pipeline (0fc0e1c):

- engine/hls.go HLSSession.Register now closes every other active
  session in the registry. Modeled as "one viewer == one transcode" so
  repeated quality switches or page reloads don't leave orphan ffmpegs
  saturating the CPU until the idle sweeper reaps them 30 min later.
- engine/hls.go restartFromSegment kills + respawns ffmpeg with
  -ss / -output_ts_offset / -start_number when the browser asks for a
  segment far ahead of the writer head. Segments already on disk stay
  cached. Without this, a user dragging the scrubber to minute 30 of a
  fresh stream blocks until the encoder reaches minute 30 in real time.
- engine/hls.go subtitle disambiguation: never set DEFAULT=YES on any
  rendition (anime forced "signs only" tracks were autoselected and
  rendered nothing during opening dialogue, looking broken). Names get
  numeric suffixes when language is duplicated; FORCED tracks get a
  "(forzados)" suffix.
- engine/hls.go ProbeInfo() exposes codec / audio / subtitle metadata
  to the new GET /hls/<id>/probe.json endpoint for the player's info
  badge + bandwidth logic.
- engine/hls.go scale chain fix: chains a trunc(iw/2)*2 scale after
  the height cap so libx264 stops rejecting odd widths (853x480 etc.).
- engine/hls.go HW encoder tuning: NVENC -preset p4 -rc vbr -tune hq,
  QSV -preset medium.
- engine/stream_server.go routes /hls/<id>/probe.json to the session.
- cmd/daemon.go runs an idle sweeper goroutine every 5 min, reaping
  sessions whose last segment fetch was >30 min ago.
2026-05-07 23:55:05 +02:00
Deivid Soto
0fc0e1c21a feat(streaming): add HLS transport pipeline (daemon side)
Introduces an HLS-over-HTTP path as Plan B for in-browser streaming. The
WebRTC + MSE pipeline keeps working untouched; the new path is selected
when the backend sets transport="hls" on a streaming session.

Daemon scope:
- engine/hls.go: HLSSession + HLSSessionRegistry. Spawns ffmpeg with
  -f hls -hls_segment_type fmp4 + force_key_frames aligned with 4 s
  segments. Pre-renders master + media playlists from the probe duration
  so the browser knows the total timeline before any segment exists,
  fixing seek/duration/pause/multi-track issues seen with the live fMP4
  pipe.
- engine/probe.go: enumerate every audio + subtitle track instead of
  collapsing to a single default audio track.
- engine/stream_server.go: route /hls/<id>/{master.m3u8,video/...,
  subs/...} to the matching session. Emit a synthesised single-VTT
  subtitle playlist per text track; bitmap subs (PGS/DVB) skip silently.
- cmd/daemon.go: branch on WebRTCSession.Transport == "hls" to register
  an HLS session instead of running the legacy DataChannel pump.
- agent/types.go: WebRTCSession.Transport + AudioIndex fields.

Backend + web sides land in a follow-up commit.
2026-05-07 16:10:22 +02:00
Deivid Soto
81abc4acca fix(transcoder): force aac stereo 48khz + frag_duration for mse compat
Two transcoder fixes for browser MediaSource Extensions parsing:

1. -ar 48000 -ac 2 on the audio output. Source 5.1 / 7.1 streams produced
   a moov atom Chrome CHUNK_DEMUXER refuses to parse, even when the video
   metadata is fine and a non-MSE video element accepts the same file.
   Forcing AAC-LC stereo 48 kHz makes the moov shape MSE-compatible.

2. -frag_duration 1000000 (1 second) so each moof+mdat fragment caps at
   ~1s of media. Without it, ffmpeg only splits at keyframes and high-
   bitrate 1080p produces 8 MiB+ mdat boxes — MSE waits for the whole
   mdat before parsing the first fragment, so playback never starts.

3. -movflags +negative_cts_offsets so b-frames carry the right pts/dts
   offsets and the playhead doesn't reset every fragment.

4. New range_req debug log to make sizing bugs greppable.
2026-05-07 14:59:43 +02:00
Deivid Soto
27fe84f2a0 fix(transcoder): force main profile + setparams Rec.709 + serveRange wait
1. -profile:v main + -level:v 4.0 to avoid Chrome's HW decoder path that
   failed with "VaapiWrapper: failed initializing for h264 high" on Linux.
2. setparams to rewrite HDR HEVC color metadata to SDR Rec.709 so browsers
   don't reject wide-gamut output.
3. serveRange caps `want` by estimated final size (not current). ReadAt
   blocks until ffmpeg catches up — that's the right behaviour. Returning
   RangeEnd inmediato was making the browser abort with "Format error".
4. Debug log on every range_req.
2026-05-07 13:48:45 +02:00
Deivid Soto
457d6e1f7c fix(transcoder): correct scale filter + always force yuv420p
The previous scale expression `min(iw,iw*H/ih)':'min(ih,H)` produced odd
widths (e.g. 1425×720 for a 16:9 source capped at 720p) which libx264
refuses with `width not divisible by 2`, killing the encoder before a
single byte was written.

Switch to `scale=-2:H:force_original_aspect_ratio=decrease`, which
derives a width that preserves aspect ratio AND is rounded to a multiple
of 2. Always set `-pix_fmt yuv420p` so 10-bit HEVC sources are downcast
to the 8-bit format browser <video> elements actually decode.

Also add `-y`, guard nil pipe in Close(), and the related transcode
plumbing for browser-decided per-session quality.
2026-05-07 11:52:28 +02:00
Deivid Soto
70f7337226 feat(stream): per-session quality cap from web
Adds WebRTCSession.Quality to the sync payload so the daemon can pick a
MaxHeight + bitrate per session instead of using the global config cap.

resolveQualityCap() maps the label to a (height, b:v) pair and
buildStreamSource() promotes a passthrough decision to ActionTranscodeVideo
when the source resolution exceeds the cap (4K source on a phone client
with quality="720p" must transcode, not pass-through).

Also lands the transcode-on-by-default fix for legacy configs without a
[downloads.transcode] section so existing installs pick up h264+aac
fallback for HEVC/AC3 content without re-running setup.
2026-05-07 10:13:45 +02:00
Deivid Soto
66ac79664b feat(stream): real-time transcoding for non-browser-decodable codecs
Source files in HEVC, AV1, AC3, DTS, EAC3, etc. now transcode through ffmpeg
to fragmented MP4 (h264 + aac) on-the-fly when the browser would otherwise
play silent black. Decision matrix lives in engine.DecideAction:
passthrough → remux → audio-transcode → full video-transcode.

Architecture — temp file + growing-size source:
- engine.streamSource interface abstracts byte source. Two impls:
  * diskFileSource: passthrough when codecs are already browser-friendly.
  * transcodeSource: spawns ffmpeg writing to a /tmp/tc-stream-*.mp4 file.
    A ticker polls file size and wakes blocked ReadAt callers as ffmpeg
    produces output. Estimate of final size (bitrate × duration) is
    announced over the wire so the browser's scrubber has something to
    anchor on.
- dataChannelPump now reads from streamSource instead of *os.File. HELLO
  carries Transcoding=true + an estimated total size; Seekable=true (we
  read random-access from the temp file even while writing).
- Transcoder runtime resolved per session by buildTranscodeRuntime in
  cmd/daemon: ffmpeg/ffprobe path lookup + HWAccel auto-detection
  (NVENC/QSV/VAAPI/VideoToolbox).
- New [downloads.transcode] TOML section: enabled (default true), hw_accel
  (auto), preset (veryfast), video_bitrate (5M), audio_bitrate (192k),
  max_height (optional downscale), max_concurrent (safety cap).

Falls back to passthrough if ffprobe is missing, fails, or codecs are
already browser-friendly. tmp file is cleaned up on session shutdown.
2026-05-07 09:26:05 +02:00
Deivid Soto
4314c06c5c feat(stream): pion-based WebRTC byte streamer for browser playback
Replaces the broken anacrolix WebTorrent path with a custom WebRTC peer
that the browser drives directly. Architecture matches plan/clever-
weaving-dove.md (Fase 2 + 3 + 6 of the streaming pivot).

- engine/wire: shared 12-byte binary frame format (Hello / RangeReq /
  RangeData / RangeEnd / Cancel / Ping / Pong / SeekHint). Roundtrip +
  oversized-frame rejection tests.
- agent/signal_client: SSE consumer + POST sender for SDP/ICE relay
  through /api/internal/stream/signal/<id>; auto-reconnects.
- engine/webrtc_stream: pion v4 PeerConnection + DataChannel pump.
  Reads file via os.ReadAt, chunks RangeData at 16 KiB, honours app-
  level backpressure with SetBufferedAmountLowThreshold.
- cmd/daemon dispatcher learns mode webrtc_stream + new
  webrtcSessionRegistry tracks per-session cancel funcs for clean
  shutdown.
- engine/probe + hwaccel + transcoder: foundation for Fase 2.5
  (codec detection, NVENC/QSV/VAAPI/VideoToolbox autodetection,
  ffmpeg pipe wrapper to fragmented MP4). Integration into
  webrtc_stream still pending.
- pion/webrtc/v4 promoted from indirect to direct dep.

End-to-end against unarr-dev confirms a 122 MB 1080p H.264 / AAC MP4
plays in Chrome with the new pipeline.
2026-05-06 23:12:38 +02:00
Deivid Soto
4c52d9b039 chore(torrent): bump anacrolix log level Critical → Warning for visibility
Surfaces tracker-announce + WebRTC peer events that were previously
swallowed by the Critical filter. Required for diagnosing the browser
↔ Go piece-transfer issue uncovered during the e2e smoke (peers
connect, signalling brokers, WebRTC handshake completes, but
anacrolix's outbound seeding to webtorrent.js browsers — known
upstream weak spot, issues #402/#752/#805 — produces zero pieces).

No behaviour change in normal operation; only changes what gets
logged.
2026-05-06 21:17:11 +02:00
Deivid Soto
e50dd17a00 feat(seed-file): unarr-side handler for browser-on-demand seeding (Fase 4.7.c)
Closes the agent half of in-browser playback for arbitrary files. When
the web app inserts a download_task with mode="seed_file", the daemon
now wraps the on-disk file as a single-file torrent, adds it to the
existing WebRTC-enabled torrent client, and reports the generated
info_hash back so the browser can target /stream/<hash>.

Pieces:

- internal/agent/types.go: Task.FilePath (received from claim) +
  StatusUpdate.InfoHash (sent back). Both serialise compatibly with
  the matching Zod schemas in the Next.js sync route.

- internal/engine/seed_file.go: SeedFile(client, filePath, trackers)
  builds the metainfo via metainfo.Info.BuildFromFilePath +
  bencode.Marshal, then AddTorrent + DownloadAll() so anacrolix
  hashes the file and flips pieces to "have" as it goes. The
  libtorrent piece-size ladder is mirrored from wstracker-probe so
  generated torrents are interoperable with mainstream clients.
  SeedFileOnDownloader is the daemon-facing convenience wrapper —
  bails loud when [downloads.webrtc].enabled = false instead of
  silently producing a torrent no browser can find.

- internal/cmd/seed_file_handler.go: handleSeedFileTask invoked from
  the existing OnTasksClaimed dispatcher in daemon.go for mode=
  seed_file. Validates filePath, calls the engine helper, and pushes
  the resulting info_hash via Client.ReportStatus. Failures (missing
  file, WebRTC disabled, ffmpeg-style oddities) report status="failed"
  + errorMessage so the browser's WatchInBrowserButton can show the
  reason instead of timing out at 60 s.

- internal/cmd/daemon.go: dispatcher learns the seed_file branch in
  the same shape as the existing stream branch.

Tests (6 unit, all green):
- SeedFile rejects missing files + directories.
- SeedFile yields a deterministic info_hash for the same payload across
  fresh clients (web client polls expecting this).
- SeedFileOnDownloader errors when WebRTC is disabled.
- chooseSeedPieceLength matches the ladder breakpoints.
- makeAnnounceList handles nil/empty/partial inputs.

Web side compatible: mode=seed_file is already accepted by the sync
schema; agent.Task.filePath + StatusUpdate.infoHash now propagate
through the existing claim/report endpoints. End-to-end browser ↔
unarr smoke is the next concrete verification step (needs a running
unarr-dev daemon plus library scan + a file with no source torrent).
2026-05-06 16:28:01 +02:00
Deivid Soto
2aeabe6b50 feat(wstracker-probe): -seed FILE mode for browser ↔ unarr e2e validation
Extends the probe binary so it can do more than verify tracker reach:
when given a real file, it builds a single-file torrent in memory,
seeds it via the WebTorrent peer wire, and prints the magnet URI
(with the WSS tracker injected). Useful for proving the end-to-end
streaming path before any actual unarr daemon work lands.

Internally uses anacrolix/torrent's metainfo.Info.BuildFromFilePath
+ bencode.Marshal to mint InfoBytes, then AddTorrent → seed loop.
Piece length picked from a libtorrent-like ladder (16 KiB → 4 MiB)
so the resulting torrent is interoperable with mainstream clients.

Validation: synthesised a 5 s 320×240 H.264+AAC mp4 with ffmpeg
(`testsrc + sine`), seeded it via this binary against the production
wss://tracker.torrentclaw.com endpoint, opened the in-browser player
at /stream/<info_hash>. Browser reported `downloaded: 105 KB / 105 KB`
and rendered a working <video> with controls. Seeder reported
`uploaded=107452 bytes`. Pieces flowed P2P over WebRTC data channel,
zero relay through TorrentClaw infrastructure.

Closes the manual half of Fase 4.4 of the streaming plan; the
companion player change (file.blob() in place of the v2-removed
file.renderTo) is committed separately on the web side.
2026-05-06 14:46:38 +02:00
Deivid Soto
c2e9925162 test(streaming): integration tests with real ffmpeg (skipped without it)
Three end-to-end checks that the transcoder actually produces playable
output, not just plausible argv. Skip cleanly on hosts without ffmpeg
on PATH so unit-test CI keeps working.

- TestTranscoder_DirectPlayProducesH264 — synth h264+aac MP4 via
  `ffmpeg -f lavfi testsrc/sine`, run Analyze (expect direct play),
  Stream to disk, ffprobe the result, assert codecs are still h264+aac.
- TestTranscoder_TranscodeHEVCToH264 — synth hevc+ac3 MKV, expect
  transcode decision, Stream to memory, ffprobe-verify the output is
  h264+aac. Skipped if libx265 isn't compiled in.
- TestTranscoder_AnalyzeReportsRealMediaInfo — sanity check that
  Analyze returns a usable mediainfo (320x240, ~2s duration) the API
  handler can show to the player.

Verified locally:
  PASS: TestTranscoder_DirectPlayProducesH264 (0.09s)
  PASS: TestTranscoder_TranscodeHEVCToH264   (0.22s)
  PASS: TestTranscoder_AnalyzeReportsRealMediaInfo (0.06s)
2026-05-06 11:35:52 +02:00
Deivid Soto
75dcc0f1cb feat(streaming): ffmpeg transcoding pipeline (direct play / fMP4 / HW accel)
The browser-side WebRTC reproductor needs MP4 / H.264 / AAC / yuv420p to
keep MSE happy. This package decides per request whether to:

  • direct-play  — input already MSE-compatible, just remux to fMP4
  • transcode    — re-encode video (libx264 / NVENC / QSV / VAAPI /
                   VideoToolbox) + audio (AAC), fragment to fMP4

Pieces:

- internal/streaming/transcoder.go — AnalyzeCompatibility decides the
  recipe from a parsed mediainfo. CompatibilityReport carries the reasons
  so the player UI can show "transcoding video: HEVC → H.264".

- internal/streaming/ffmpeg_args.go — BuildFFmpegArgs assembles the argv
  for ffmpeg. Direct play uses `-c copy`; transcode uses libx264 or the
  selected HW encoder. Output is always fragmented MP4 piped to stdout
  (-movflags frag_keyframe+empty_moov+default_base_moof) so the HTTP
  handler can stream straight to the browser without disk I/O.

  Quality ladder: 480p (1.5Mb), 720p (3.5Mb), 1080p (6Mb), 2160p (25Mb).
  Default 1080p when unset / unknown. -ss seek for resume / scrubbing.

- internal/streaming/hwaccel.go — DetectHWAccel runs `ffmpeg -encoders`
  once per process and caches the best available. Order: NVENC → QSV →
  VAAPI → VideoToolbox → libx264. VAAPI is the only family that wires up
  HW decode too (`-hwaccel vaapi`); the others software-decode and HW-
  encode (works fine and avoids /dev/dri permission rabbit holes).

- internal/streaming/stream.go — Transcoder facade wires Analyze + Stream
  together for the API handler in Fase 4. Captures the last 8 KiB of
  ffmpeg stderr for diagnosable errors without unbounded memory.

Tests (20 unit, all green):
- AnalyzeCompatibility: h264+aac direct, video-only direct, HEVC →
  transcode, 10-bit HDR → transcode, EAC3 audio → transcode, nil guards
- ResolveQuality: empty + unknown fallback to 1080p, 4-step ladder
- BuildFFmpegArgs: direct play -c copy, transcode libx264 + bitrate +
  scale, NVENC swaps encoder & drops preset, VAAPI injects -hwaccel +
  scale_vaapi, -ss timestamp formatting
- HWAccel: encoder-name table, VAAPI is the only one with HW decode
- formatDuration: zero, sub-second, HH:MM:SS, negative-clamped
- cappedBuffer: tail retention through multi-write and large-write paths
- NewTranscoder: rejects empty paths
2026-05-06 11:34:57 +02:00
Deivid Soto
e68b127acc feat(release): bundle ffmpeg + ffprobe in tarballs and Docker image
Operators no longer have to install ffmpeg manually. Both the release
tarballs (5 platforms × 2 binaries) and the Docker image now ship a
working ffmpeg + ffprobe pair adjacent to the unarr binary;
ResolveFFmpeg / ResolveFFprobe pick them up via the "adjacent to
executable" branch with zero configuration.

Tarball bundle (scripts/download-ffmpeg-static.sh + .goreleaser.yml):
- ffbinaries.com (johnvansickle / Zeranoe-style static GPL builds) for
  linux-amd64, linux-arm64, darwin-amd64, windows-amd64
- evermeet.cx universal Mach-O for darwin-arm64 (ffbinaries lacks it)
- BtbN/FFmpeg-Builds for windows-arm64 (ffbinaries lacks it)
- Idempotent fetch with curl --retry 5 so transient github.com SSL
  errors don't fail the goreleaser before-hook
- New `before.hooks` runs the script automatically per release; archive
  files glob `dist-ffbinaries/{{ .Os }}-{{ .Arch }}/*` + strip_parent
- Migrated to non-deprecated `formats: [tar.gz]` / `formats: [zip]`
- Verified via `goreleaser release --snapshot --clean --skip=publish` —
  6 archives all carry ffmpeg + ffprobe (~60-130MB each)

Docker image (Dockerfile):
- Replaced the failing BtbN static glibc binaries with Alpine's native
  musl `apk add ffmpeg`. The static GPL builds need glibc + libmvec /
  libgcc_s; gcompat alone is not enough (vector-math symbols unresolved).
  Alpine ships ffmpeg 6.1.2 which is fine for the WebRTC transcoder.
- Image size 174MB, built + ffmpeg/ffprobe/unarr smoke OK.

Targets the v0.8 unarr release (per user direction — new feature, not
a patch). dist-ffbinaries/ added to .gitignore.
2026-05-06 11:26:01 +02:00
Deivid Soto
727ab19468 feat(mediainfo): ResolveFFmpeg + DownloadFFmpeg mirroring ffprobe pattern
Adds the ffmpeg-binary half of the resolution stack so the upcoming
WebRTC streaming transcoder (Fase 3.3) has a single point of entry.

Search order matches ResolveFFprobe so operators don't need to learn a
second mental model:
  1. Explicit path  (--ffmpeg flag / library.ffmpeg_path config)
  2. FFMPEG_PATH env var
  3. "ffmpeg" on PATH (system install)
  4. Adjacent to the unarr executable (release tarball bundles it here —
     this is the preferred path; see Fase 3.2 goreleaser changes)
  5. Cache dir (sibling of the cached ffprobe binary)
  6. Auto-download from ffbinaries.com (~70MB) as last resort

Includes:
- internal/library/mediainfo/ffmpeg.go         — ResolveFFmpeg + actionable
  Docker / non-Docker error messages
- internal/library/mediainfo/ffmpeg_download.go — DownloadFFmpeg, reuses
  ffprobePlatformKey + ffprobeAPIClient + ffprobeDLClient + extractFromZip
  helpers; bumps maxZipSize to 200MB (ffmpeg static is ~70-100MB)
- internal/config: LibraryConfig.FFmpegPath toml field for explicit paths
- 4 unit tests: explicit OK, explicit missing, env var, sibling cache path

Tarball bundling and the actual transcoding pipeline land in the next
two commits.
2026-05-06 09:49:32 +02:00
Deivid Soto
aa291320f5 test(wstracker-probe): standalone Go binary to verify WSS tracker reachability
Tiny `go run ./cmd/wstracker-probe` that spins up an anacrolix/torrent
Client with WebRTC enabled, advertises a random info_hash to the given
WSS tracker, and reports via Callbacks.StatusUpdated whether the
announce round-trip succeeded.

Used as the production smoke for unarr ↔ wss://tracker.torrentclaw.com:

  $ /tmp/wstracker-probe -tracker wss://tracker.torrentclaw.com -timeout 30s
  [probe] tracker=wss://tracker.torrentclaw.com info_hash=e978df8d... timeout=30s
  [probe] tracker connected: wss://tracker.torrentclaw.com
  [probe] tracker announce OK: wss://tracker.torrentclaw.com ih=e978df8d...
  [probe] OK — tracker announce succeeded

Disables TCP/uTP/DHT/IPv6/UPnP — only the WS tracker path matters here.
Exit codes: 0 success, 1 announce error, 2 timeout.
2026-05-06 09:40:37 +02:00
Deivid Soto
f6117ddeb9 feat(torrent): act as WebTorrent peer for browser ↔ unarr P2P streaming
Wires anacrolix/torrent's built-in webtorrent package so a browser
running webtorrent.js can fetch pieces from this CLI via WebRTC data
channels. The daemon stays the seeder; we never relay bytes through
TorrentClaw infrastructure — same legal posture as today.

Changes:
- internal/config: new [downloads.webrtc] section
  (enabled/trackers/stun_servers/turn_servers/turn_user/turn_pass).
  Disabled by default, opt-in via config.toml. When enabled but
  trackers / STUN slices are empty, defaults are reapplied on Load() so
  users get a working setup with a single `enabled = true`.
- internal/engine: TorrentConfig gains WebRTCEnabled / WebRTCTrackers
  / ICEServers; NewTorrentDownloader populates ClientConfig.ICEServerList
  and forces NoUpload=false when WebRTC is on (browsers can't pull
  otherwise). buildMagnet now accepts variadic extra trackers and the
  downloader method prepends WSS trackers so anacrolix's
  webtorrent.TrackerClient picks them up first.
- internal/engine/webrtc.go: BuildICEServers helper converts the TOML
  WebRTCConfig into []webrtc.ICEServer with shared TURN credentials.
- internal/cmd/daemon.go + download.go: pass WebRTC config through to
  the engine.

Tests (8 new, all green; full suite 0 lint issues, 0 vet):
- buildMagnet free function: defaults-only, with extras, trim+empty-skip
- downloader method: WebRTC disabled keeps WSS out, enabled prepends them
- BuildICEServers: nil when disabled, STUN-only path, TURN+credentials
- NewTorrentDownloader: full WebRTC-enabled construction (logs WebRTC
  peer enabled, magnet contains wss://tracker.torrentclaw.com)

End-to-end smoke (browser ↔ unarr peer transfer) is deferred to a
manual test once tracker.torrentclaw.com WSS is live.
2026-05-06 08:59:58 +02:00
Deivid Soto
6955b6144b chore(release): 0.7.0
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.7.0
- Update CHANGELOG.md
2026-04-10 19:18:38 +02:00
Deivid Soto
37fcb9fad9 feat(daemon): enhance service management with start, stop, restart, and status commands for Windows 2026-04-10 19:18:13 +02:00
Deivid Soto
debf77005f chore(release): 0.6.8
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.8
- Update CHANGELOG.md
2026-04-10 16:36:27 +02:00
Deivid Soto
f699b26fa6 feat(library): add server-driven file deletion with allow_delete config 2026-04-10 16:35:12 +02:00
Deivid Soto
8ad8a5ea47 chore(release): 0.6.7
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.7
- Update CHANGELOG.md
2026-04-10 11:47:58 +02:00
Deivid Soto
db316726fd feat(scan): always scan downloads + organize dirs, deduplicate child paths
ResolveScanPaths() collects downloads.dir, organize.movies_dir,
organize.tv_shows_dir, and library.scan_path (if set), then removes
paths that are subdirectories of a parent already in the list.

This ensures the daemon and CLI scan all configured dirs without
relying solely on scan_path being set.
2026-04-10 11:46:20 +02:00
Deivid Soto
b2ed81ee74 fix(docker): switch ffprobe download from johnvansickle.com to BtbN/FFmpeg-Builds
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
johnvansickle.com was unreachable from GitHub Actions runners (2 failed releases),
switching to BtbN static builds on GitHub CDN which are more reliable.
2026-04-09 19:25:28 +02:00
Deivid Soto
b3f2b3e64d chore(release): 0.6.6
- Bump version to 0.6.6
- Update CHANGELOG.md
2026-04-09 18:37:56 +02:00
Deivid Soto
f1b4f2e327 fix(stream): fix black screen on remote/Tailscale streaming
Three root-cause fixes for VLC showing a black screen when opening a
stream from a different network or via Tailscale:

1. PrioritizeTail: when VLC opens an MKV/MP4 stream it immediately seeks
   to the end of the file to read the container index (seekhead/moov
   atom). For active torrents those end-pieces aren't downloaded yet, so
   the reader blocks indefinitely. PrioritizeTail() opens a background
   reader positioned at the last 5 MB, keeping those pieces at high
   priority until ctx is cancelled or they finish downloading.

2. /health endpoint: GET /health returns a lightweight JSON response
   {"status":"ok","streaming":bool,...} so connectivity can be tested
   with a simple curl from any device before involving VLC.

3. Per-request logging: every incoming /stream request now logs the
   client IP and Range header, making it trivial to confirm whether
   remote/Tailscale clients are reaching the server at all.
2026-04-09 16:15:41 +02:00
Deivid Soto
7eaf357680 chore(release): 0.6.5
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.5
- Update CHANGELOG.md
2026-04-09 14:16:02 +02:00
Deivid Soto
db3e74a736 fix(upgrade): retry download on transient network errors with user feedback
Add downloadWithRetry with up to 3 attempts and quadratic backoff (5s, 20s)
to handle TLS timeouts and transient failures. Progress messages inform the
user of each failure and wait time before retrying.
2026-04-09 14:15:32 +02:00
Deivid Soto
29f4886a53 chore(release): 0.6.4
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.4
- Update CHANGELOG.md
2026-04-09 10:54:42 +02:00
Deivid Soto
8fae119903 fix(daemon): report error status when stream path is rejected 2026-04-09 10:54:14 +02:00
Deivid Soto
d7fa0af504 chore(release): 0.6.3
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.3
- Update CHANGELOG.md
2026-04-09 09:26:17 +02:00
Deivid Soto
fad53a5d84 fix(library): use native arm64 ffprobe on Apple Silicon (osx-arm-64) 2026-04-09 09:26:10 +02:00
Deivid Soto
bea73335a8 chore(release): 0.6.2
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.2
- Update CHANGELOG.md
2026-04-09 09:21:00 +02:00
Deivid Soto
db6d78d50a chore: ignore local config/ directory 2026-04-09 09:18:14 +02:00
Deivid Soto
228564eb7f feat(library): resilient scan for large libraries and better ffprobe errors
- Use a dedicated 10-minute HTTP client for library-sync so libraries
  with hundreds or thousands of items no longer time out
- Show actionable ffprobe-not-found error: detects Docker and suggests
  FFPROBE_PATH env var, config.toml setting, or package install
- Include static ffprobe binary in Docker image (johnvansickle.com)
- Bump version to 0.6.2
2026-04-09 09:13:38 +02:00
Deivid Soto
3fd19f1406 feat(wake): long-poll wake listener for instant CLI sync
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
CLI now holds a GET /api/internal/agent/wake connection open.
When the server calls triggerWake(userId) — on stream request,
download queue, pause, cancel, resume, scan, etc. — the CLI
receives the signal immediately and fires a sync cycle in <100ms
instead of waiting up to 10s for the next scheduled interval.

- Add WaitForWake(ctx) to Client using a no-timeout HTTP client
- Add runWakeListener goroutine to SyncClient (auto-reconnects)
- Start wake listener from SyncClient.Run()

Closes: sub-second stream latency from the web UI
2026-04-09 00:01:24 +02:00
Deivid Soto
ef4f38d324 fix: resolve deadlock, data races and path traversal vulnerabilities
- task.go: fix deadlock in ToStatusUpdate() — calling Percent() (which
  RLocks) while already holding RLock caused deadlock when a writer was
  waiting; compute percent inline instead
- usenet.go: fix data race in Cancel() — tracker and taskDir were read
  without the mutex while Download() writes them under it; read all
  fields under the same lock
- upnp.go: fix UPnP Remove() blocking shutdown — run cleanup in goroutine
  with 10s deadline (removeNATPMP worst case is 3s dial + 5s deadline)
- daemon.go: add path traversal protection for stream requests — validate
  sr.FilePath is within configured directories before os.Stat; defends
  against compromised API server sending arbitrary paths
- client.go: add wakeClient without timeout for long-poll wake endpoint
  where context controls cancellation
- sync.go: trigger immediate sync when entering watching mode so stream
  requests are picked up without waiting for the next scheduled interval
2026-04-08 23:36:18 +02:00
Deivid Soto
78c16c295e test: add comprehensive test suite for engine, agent and cmd packages
- Refactor download.go and stream.go with downloadDeps/streamDeps structs
  for dependency injection, enabling unit testing without real I/O
- download_test.go: 15 tests — input validation, mock downloaders, method
  selection, cobra Args, deadlock detection
- stream_test.go: input validation, noOpen flag, engine error handling
- client_test.go: context cancellation, timeout, full Sync roundtrip,
  watch-progress and HTTP error unwrapping
- sync_test.go: TriggerSync on watching transition, adjustInterval
- torrent_test.go: TorrentDownloader lifecycle without network
- stream_server_test.go: HTTP server lifecycle, SetFile/ClearFile,
  concurrent requests, Shutdown releases port, content-type
- manager_integration_test.go: full pipeline — success, torrent→debrid
  fallback, all-fail, multi-concurrent, ForceStart, OnTaskDone,
  recent-finished drain, cancel mid-download, organize
- usenet_test.go: Cancel/Pause race regression test (run with -race)
- daemon_test.go: isAllowedStreamPath table tests
- CI: split coverage gate to engine+agent only (50% threshold); cmd
  coverage still reported but not gated (interactive UI commands)
- lefthook: add pre-push hook with go test -race -count=1 -timeout=120s
2026-04-08 23:36:00 +02:00
Deivid Soto
b14ab98580 chore(release): 0.6.0
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s
- Bump version to 0.6.0
- Update CHANGELOG.md
2026-04-08 18:57:36 +02:00