From 8b38986690470f15547c47d37cf0f69d5f0d1b80 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 16 May 2026 20:20:05 +0300 Subject: [PATCH] chore: bring MANUAL.md in line with current encoder behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §6 step 2: drop the codec-name allowlist, describe the bounded idet run and the Multi-frame summary parse. §6 step 6/7: per-stream audio..wav / audio..opus now. §6 step 8/9: zscale is conditional and width rounds to mod-2; -vf is omitted entirely when no filter applies. Audio language tags are indexed by output position. TITLE/COLLECTION values are unquoted. §6 step 11: series filename no longer requires an IMDb mapping. §7: added a token example and notes about what the token does and does not influence in the output filename. §8: noted the known log-rotation limitation (filename frozen at start; restart to rotate; structured.json does not rotate). --- MANUAL.md | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/MANUAL.md b/MANUAL.md index ac68d76..ce3178b 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -170,33 +170,33 @@ For every `.mkv` found in `paths.input`, the program runs these steps in order. 1. **Parse filename** → determines whether this is a movie or series, and what IDs to use. 2. **Probe video** with `ffprobe`: - - Picks the first video stream whose codec is `mpeg2video`, `h264`, or `hevc`. + - Picks the first stream with `codec_type=video`, regardless of codec name. Errors out if there isn't one. - Records width, height, and sample aspect ratio (SAR). - - Runs `ffmpeg -vf idet` to detect interlacing (presence of `TFF`/`BFF` in stderr). + - Detects interlacing by running `ffmpeg -vf idet -frames:v 400 -an -sn -f null -` and parsing the `Multi frame detection: TFF: a BFF: b Progressive: c Undetermined: d` summary line. The source is treated as interlaced only when `a+b > c`; undetermined frames are ignored, and a missing summary line defaults to progressive. 3. **Probe stream languages** with `ffprobe` — collects `language` tags for every audio/subtitle stream so they can be re-applied after encoding (FFmpeg's `-map_metadata -1` strips them otherwise). 4. **Detect media type**: - First, check the filename for a `dvd` / `bluray` / `webdl` / `tvrip` token (case-insensitive). If present, that wins. - Otherwise, fall back to pixel count: `width × height < 600,000` → DVD, else Blu-ray. The chosen profile selects which `encoding..crf` / `encoding..preset` pair from the config to use, and is written into the `ORIGINAL_MEDIA_TYPE` metadata tag. 5. **Fetch metadata** from OMDb or TVmaze depending on the parsed filename. Failures here are logged but do not abort the encode — the file is just encoded without metadata. -6. **Extract audio** to `audio.wav` (PCM s16le, 48 kHz) in the input directory. -7. **Encode audio** with `opusenc --bitrate 128k` → `audio.opus`. -8. **Calculate display width** from SAR. If SAR ≠ `1:1`, the width is rescaled so the output has square pixels, using a `zscale` filter (`spline36`). Height is preserved. +6. **Extract audio** — one PCM wav per source audio stream, written to the input directory as `audio.0.wav`, `audio.1.wav`, … in source order (PCM s16le, 48 kHz). Errors out if the source has no audio streams. +7. **Encode audio** — each wav is converted with `opusenc --bitrate 128k` to a matching `audio..opus`. +8. **Calculate display width** from SAR. The width is rescaled so the output has square pixels only when there's actually work to do — `zscale` is skipped entirely for square-pixel sources (SAR `1:1`, `N/A`, empty, `0:N`), and for any SAR whose calculated width rounds to the source width. When rescaling, the width is rounded to the nearest even number (mod-2, preferred by AV1). 9. **Encode video** with FFmpeg: - - Video filter chain: `bwdif=mode=0:par=-1:-1,zscale=w=W:h=H:filter=spline36` if interlaced, else just the `zscale` step. + - Video filter chain is built conditionally. `bwdif=mode=0:par=-1:-1` is prepended when the source is interlaced; the `zscale` step is appended only when a rescale is actually needed (see step 8). If neither applies, `-vf` is omitted entirely. - Codec: `libsvtav1`, `-pix_fmt yuv420p10le`. - - `-crf` and `-preset` from the selected profile. + - `-crf` and `-preset` from the selected profile (step 4). - `-svtav1-params film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:keyint=10s`. - - Streams mapped: video from input, subtitles from input (`0:s?` — optional), audio from the re-encoded Opus files. - - Audio copied (`-c:a copy`), subtitles copied (`-c:s copy`). - - `-map_metadata -1` strips global metadata; per-stream language tags are then re-applied from the ffprobe pass. - - Container metadata written: `TITLE`, `DATE_RELEASED`, `IMDBID`, `ORIGINAL_MEDIA_TYPE`. For series: also `COLLECTION`, `SEASON`, `EPISODE`, `TVMAZE_ID`. + - Streams mapped: video from input (`0:v`), subtitles from input (`0:s?` — optional), and one audio stream per Opus file (`1:a`, `2:a`, …). + - Audio re-muxed (`-c:a copy` — copies the already-Opus-encoded streams), subtitles copied (`-c:s copy`). + - `-map_metadata -1` strips global metadata; per-stream language tags are then re-applied. Audio language tags are indexed by **output position** (the position in the opus-file list), not by counting source streams, so missing-language streams don't shift the index. + - Container metadata written: `TITLE`, `DATE_RELEASED`, `IMDBID`, `ORIGINAL_MEDIA_TYPE`. For series: also `COLLECTION`, `SEASON`, `EPISODE`, `TVMAZE_ID`. Values are unquoted (literal value, no wrapping `"…"`). - Output written to `output.mkv` in the input directory. -10. **Clean up** intermediate `.wav` and `.opus` files. +10. **Clean up** intermediate `audio.*.wav` and `audio.*.opus` files. 11. **Rename and move** `output.mkv` to `paths.output` with a final name: - - Series → `.SE.mkv` - - Movie → `..mkv` - - No metadata → `<8-hex-chars>.nometadata.mkv` + - Series with a known show name → `.SE.mkv` (the series no longer needs a populated IMDb mapping — a TVmaze show with no external IMDb link still gets a useful filename). + - Movie with a known title and IMDb ID → `..mkv`. + - Anything else → `<8-hex-chars>.nometadata.mkv`. Sanitization keeps `a–z A–Z 0–9 - ä ö Ä Ö` only. 12. **Dispose of the source**: - `-d` flag set → delete the original `.mkv`. @@ -206,13 +206,18 @@ For every `.mkv` found in `paths.input`, the program runs these steps in order. ## 7. Output naming examples -| Input filename | Result in `paths.output` | -|---|---| -| `Heat.tt0113277.mkv` | `Heat.tt0113277.mkv` | -| `Breaking.Bad.tvm169.S01E01.mkv` | `BreakingBad.S01E01.mkv` | -| `unrecognized-rip.mkv` | `a1b2c3d4.nometadata.mkv` | +| Input filename | Result in `paths.output` | Notes | +|---|---|---| +| `Heat.tt0113277.mkv` | `Heat.tt0113277.mkv` | Pixel-count fallback → Blu-ray profile + tag | +| `Heat.tt0113277.bluray.mkv` | `Heat.tt0113277.mkv` | Same output filename; muxed `ORIGINAL_MEDIA_TYPE=Blu-ray` is now from the token, not the guess | +| `Breaking.Bad.tvm169.S01E01.webdl.mkv` | `BreakingBad.S01E01.mkv` | WebDL profile + tag | +| `unrecognized-rip.mkv` | `a1b2c3d4.nometadata.mkv` | No IDs at all | -The IMDb ID returned by the API is used, not the one from the filename, so a typo in the filename would surface there. +A few things worth knowing: + +- The media-type token affects the muxed `ORIGINAL_MEDIA_TYPE` tag and the CRF/preset profile, but **not** the output filename. +- The IMDb ID written into the filename is the one returned by the API, not the one in the input filename, so a typo in the source filename will surface in the output name. +- A TVmaze show with no IMDb mapping still gets a `.SE.mkv` filename (only the `IMDBID` metadata tag is left empty). --- @@ -228,6 +233,8 @@ Run the program from the directory where you want the logs to land. Note: a number of `DEBUG` lines are printed to stdout/stderr (ffprobe output, calculated zscale width, the full ffmpeg command, etc.). These are intentional but not written to the log files. +**Known limitation:** the date in `info_*.log` / `error_*.log` filenames is computed when the daemon starts and does not roll over at midnight. If the daemon is left running across days, all writes continue into the start-day's file. Restart the daemon to rotate. `structured.json` does not rotate at all. + --- ## 9. Failure handling