Source code for remote_compression.settings

"""Encoding settings: the immutable description of *what* to do."""

from dataclasses import dataclass, field

#: Extensions scanned by default (always compared lowercase).
DEFAULT_EXTENSIONS = frozenset(
    {
        ".avi", ".mp4", ".flv", ".wmv", ".mkv", ".ts", ".webm", ".m4v",
        ".mov", ".mpg", ".mpeg", ".m2ts", ".mts", ".3gp",
    }
)

#: Encoder name (ffmpeg ``-c:v`` value) -> codec name (as reported by ffprobe).
ENCODER_TO_CODEC = {
    "libx265": "hevc",
    "libx264": "h264",
    "libsvtav1": "av1",
    "libaom-av1": "av1",
    "librav1e": "av1",
    "libvpx-vp9": "vp9",
    "libvpx": "vp8",
    "mpeg4": "mpeg4",
}

#: Hardware/vendor encoder suffixes stripped by the heuristic of :func:`derive_codec_name`.
_HW_SUFFIXES = ("_nvenc", "_qsv", "_amf", "_videotoolbox", "_vaapi", "_v4l2m2m", "_mf")

#: Subtitle codecs that matroska accepts as-is (``-c:s copy``).
MKV_COPY_SUBS = frozenset(
    {
        "subrip",
        "srt",
        "ass",
        "ssa",
        "webvtt",
        "hdmv_pgs_subtitle",
        "dvd_subtitle",
        "dvb_subtitle",
    }
)

#: Text subtitle codecs that must be converted for matroska (``-c:s srt``).
TEXT_CONVERT_SUBS = frozenset({"mov_text", "text"})

#: ffprobe ``field_order`` values meaning the source is interlaced.
INTERLACED_FIELD_ORDERS = frozenset({"tt", "bb", "tb", "bt"})


[docs] def derive_codec_name(encoder): """ Derive the ffprobe codec name matching an encoder name. Parameters ---------- encoder: :class:`str` ffmpeg encoder (``-c:v`` value), e.g. ``libx265``, ``hevc_nvenc``. Returns ------- :class:`str` or None Codec name as reported by ffprobe (e.g. ``hevc``), or None if it cannot be derived. Callers must then require an explicit ``codec_name``. Examples -------- >>> derive_codec_name('libx265') 'hevc' >>> derive_codec_name('hevc_nvenc') 'hevc' >>> derive_codec_name('libsvtav1') 'av1' >>> derive_codec_name('mystery_encoder') is None True """ if encoder in ENCODER_TO_CODEC: return ENCODER_TO_CODEC[encoder] name = encoder for suffix in _HW_SUFFIXES: name = name.removesuffix(suffix) if name != encoder: return {"x265": "hevc", "x264": "h264"}.get(name, name) stripped = name.removeprefix("lib") if stripped in {"x265", "hevc"}: return "hevc" if stripped in {"x264", "h264"}: return "h264" if stripped != name and stripped in {"av1", "vp9", "vp8", "aom-av1"}: return stripped.removeprefix("aom-") return None
[docs] @dataclass(frozen=True) class Settings: """ Resolved encoding settings (immutable). Attributes ---------- codec: :class:`str` ffmpeg encoder used for transcoding (free choice: ``libx264``, ``libsvtav1``, ``hevc_nvenc``...). codec_name: :class:`str`, optional ffprobe codec name considered *compliant* (no transcode needed). Derived from `codec` when None; an explicit value is required when the derivation fails. crf: :class:`int`, optional Constant rate factor. None keeps the encoder default. ffmpeg_preset: :class:`str`, optional ffmpeg ``-preset`` (``medium``, ``slow``...). None keeps the encoder default. height: :class:`int`, optional Maximal definition, understood as the short side of the *display* frame (rotation-aware): a 1920x1080 landscape and a 1080x1920 portrait are both "1080p" and both get downscaled to 720p when this is 720. None disables resizing (the CLI/TOML sentinel for None is ``0``). container: :class:`str` Output container (extension without dot). Everything is muxed to mkv by default: it accepts about any codec/subtitle combination. map_streams: :class:`bool` Explicitly map video/audio/subtitle streams (``-map 0:V -map 0:a? -map 0:s?``). When False, keep ffmpeg default stream selection. replace: :class:`bool` When True, the original file is deleted after a successful compression. When False, it is kept next to the output, renamed ``ori_<name>``. hostname: :class:`str` SSH alias of the compression server (resolved through ``~/.ssh/config``), or ``local`` to compress on this machine. remote_ffmpeg: :class:`str`, optional Explicit path of the ffmpeg binary on the remote server, for hosts whose non-interactive PATH misses it (typical on Synology). Must be a space-free ASCII path; forward slashes work on Windows servers too. remote_workdir: :class:`str`, optional Workspace directory on the server as seen by the *shell* running ffmpeg (default: ``.rcomp``, relative to the login home). Space-free ASCII. remote_workdir_sftp: :class:`str`, optional The same workspace as seen by the *SFTP* channel, when the two views differ. Synology chroots SFTP into a virtual share tree: its ``/home`` is the login home, so ``remote_workdir_sftp = "/home/.rcomp"`` (with the default `remote_workdir`) points both channels at the same physical directory. Defaults to `remote_workdir`. extensions: :class:`frozenset` of :class:`str` File extensions (lowercase, with dot) considered as videos when scanning. auto_local: :class:`bool` When True, a hostname that resolves to this very machine switches to local mode automatically. auto_purge_days: :class:`float`, optional Age threshold for the opportunistic purge of the remote workspace at connection time. None disables it (the TOML sentinel for None is ``0``). """ codec: str = "libx265" codec_name: str | None = None crf: int | None = None ffmpeg_preset: str | None = None height: int | None = 720 container: str = "mkv" map_streams: bool = True replace: bool = False hostname: str = "remote_host" remote_ffmpeg: str | None = None remote_workdir: str | None = None remote_workdir_sftp: str | None = None extensions: frozenset = field(default=DEFAULT_EXTENSIONS) auto_local: bool = True auto_purge_days: float | None = 7.0 @property def effective_codec_name(self): """ :class:`str`: ffprobe codec name considered compliant. Raises ------ ValueError If `codec_name` is not set and cannot be derived from `codec`. """ if self.codec_name is not None: return self.codec_name derived = derive_codec_name(self.codec) if derived is None: msg = ( f"Cannot derive the probe codec name from encoder '{self.codec}'. " "Set codec_name explicitly (--codec-name on the command line, " "or codec_name in the configuration file)." ) raise ValueError(msg) return derived