Format conversion pipeline (Tartube parity 1.7)

Post-download ffmpeg pass with three modes, global + per-channel.
Closes the last real Tartube parity gap.

## Modes
- remux-mp4: fast container change mkv→mp4 (stream-copy video, aac audio)
- h264-mp4: re-encode to H.264/AAC at a configurable CRF + x264 preset
- audio: extract to mp3 / m4a / opus / flac

## Design (separate ffmpeg pass)
The main download adds `--print after_move:CONVERT_PATH:%(filepath)s`
when conversion is active. poll() scans freshly-Done download jobs for
those lines and enqueues a distinct ffmpeg transcode Job per finished
file — so the UI shows transcoding as its own phase. On success the
source is renamed to <name>.original.<ext> (keep_original) or deleted;
the converted file lands next to it. Transcodes don't auto-retry and
don't themselves convert (no infinite loop).

## Config
New [convert] section (mode / crf / preset / audio_format /
keep_original) + per-channel DownloadOptions.convert_mode override
(None = defer to global, "off" = force-disable for that channel).
ConvertSpec::resolve merges them; CRF 0→23, empty preset→medium, empty
audio→mp3 defaults.

## UI
Desktop + web: a "Format conversion (global defaults)" Settings section
with mode-dependent rows (CRF/preset for h264, format for audio, keep-
original toggle), and a per-channel Convert dropdown in the channel-
options dialog.

Verified: all three ffmpeg modes produce valid output on a real library
file (remux instant, h264 CRF23 ~12s for a 3.4min clip, mp3 extraction);
settings round-trip (global → config.toml [convert] → echo; per-channel
override saves + reads back). 4 resolver tests; 95 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-03 23:20:06 -07:00
parent b58d393d98
commit fd9063b5cb
6 changed files with 534 additions and 9 deletions

View file

@ -92,6 +92,14 @@ pub struct DownloadOptions {
#[serde(default)]
pub youtube_player_clients: Option<String>,
/// Per-channel post-download conversion mode override. `None` defers
/// to the global `[convert]` config; `Some("off")` forces no
/// conversion for this channel even when the global default is on.
/// Other values: "remux-mp4" / "h264-mp4" / "audio". CRF / preset /
/// audio-format always come from the global config.
#[serde(default)]
pub convert_mode: Option<String>,
/// Raw passthrough — every entry is appended as a separate argument to
/// yt-dlp. Lets users access any flag we haven't exposed yet. Equivalent
/// to Tartube's `extra_cmd_string`.