{"id":47860804,"url":"https://github.com/binhex/trimarr","last_synced_at":"2026-05-26T16:01:17.021Z","repository":{"id":340962276,"uuid":"1168353693","full_name":"binhex/trimarr","owner":"binhex","description":"Removes (trims) unwanted audio and subtitles from matroska container format video files.","archived":false,"fork":false,"pushed_at":"2026-05-20T12:06:57.000Z","size":1718,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-20T14:58:34.239Z","etag":null,"topics":["audio","cli","language","matroska","mkvmerge","mkvtoolnix","python","remove","strip","subtitles","video"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/binhex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-27T09:40:16.000Z","updated_at":"2026-05-20T12:06:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/binhex/trimarr","commit_stats":null,"previous_names":["binhex/trimarr"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/binhex/trimarr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binhex%2Ftrimarr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binhex%2Ftrimarr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binhex%2Ftrimarr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binhex%2Ftrimarr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/binhex","download_url":"https://codeload.github.com/binhex/trimarr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binhex%2Ftrimarr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33528089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"ssl_error","status_checked_at":"2026-05-26T15:22:15.568Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","cli","language","matroska","mkvmerge","mkvtoolnix","python","remove","strip","subtitles","video"],"created_at":"2026-04-03T23:29:24.792Z","updated_at":"2026-05-26T16:01:17.015Z","avatar_url":"https://github.com/binhex.png","language":"Python","funding_links":["https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif","https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=MM5E27UX6AUU4"],"categories":[],"sub_categories":[],"readme":"# Trimarr\n\nRemoves (trims) unwanted audio and subtitles from matroska container format video files.\n\n## Features\n\n- **Recursive scan** — finds all `.mkv` files under one or more specified directory trees. Pass a pipe-separated list\nto `--media-path` to process multiple roots in a single run; duplicate files (from overlapping paths or symlinks) are\nautomatically deduplicated.\n- **Smart skip** — tracks processed files in SQLite using a fingerprint (size + mtime + partial\n  hash); only reprocesses a file if its content has changed.\n- **Commentary track safety** — if the default audio or subtitle track is a commentary track,\n  trimarr demotes it and promotes the first non-commentary track to be the new default\n  (whenever tracks of that type are being removed).\n- **Multi-language support** — keep tracks in any combination of languages with a single\n  comma-separated value, e.g. `--language eng,fre` retains both English and French.\n- **Language safety fallbacks** — if *no* audio (or subtitle) tracks match the target language(s),\n  all tracks of that type are kept to prevent accidentally silencing a file. Additionally, if all\n  language-matching audio tracks are commentary (e.g. Director's Commentary on a foreign-language\n  film), audio filtering is also skipped. A warning is logged in both cases.\n- **Native language preservation** — with `--keep-native-audio`, trimarr identifies the film's\n  original spoken language(s) via IMDb (or TMDb as fallback) and preserves those audio tracks\n  even if they don't match your `--language` preference. Ideal for dubbed films (e.g., keeping\n  original Chinese audio alongside an English dub). Results are cached in the database so each\n  file is looked up only once.\n- **Auto-managed mkvmerge** — downloads the mkvmerge binary from MKVToolNix GitHub releases on\n  first run and keeps it up to date automatically (disable with `--no-update-check`).\n- **Space savings summary** — reports bytes reclaimed at the end of each run and a cumulative\n  all-time total across all sessions.\n- **Graceful interrupt** — Ctrl+C shows a partial summary before exiting with code 130.\n- **Safe file replacement** — output is written to a temp file first, then atomically renamed over\n  the original so a failed remux never corrupts the source.\n- **Corrupt output safeguard** — before replacing the original, trimarr probes the output with\n  `mkvmerge -J` to confirm it is a structurally valid MKV and rejects any output smaller than 50 %\n  of the source. If either check fails, all processing halts immediately, the temp file is preserved\n  for inspection, and the original file is left untouched. The size guard can be bypassed with\n  `--skip-size-check` when legitimate remuxes are expected to produce significantly smaller output\n  (e.g. files with very large audio or subtitle payloads).\n- **BCP-47 / ISO 639-1 language tag support** — files using IETF `language_ietf` tags (e.g. `en`,\n  `en-US`, `fr`) are automatically mapped to ISO 639-2 codes before matching, so `--language eng`\n  correctly matches tracks tagged as either `eng` or `en`.\n- **Failure report** — after the run summary, a consolidated list of every file that could not be\n  processed is printed with a concise reason for each failure.\n\n## Prerequisites\n\n- [Python 3.12+](https://www.python.org/downloads/)\n- [Astral uv](https://github.com/astral-sh/uv#installation) (optional)\n\n## Quick start\n\n### Installation using uv (recommended)\n\n```bash\ngit clone https://github.com/binhex/trimarr\ncd trimarr\nuv venv --quiet\nuv sync\n```\n\n### Installation using pip\n\n```bash\ngit clone https://github.com/binhex/trimarr\ncd trimarr\npython -m venv .venv\nsource .venv/bin/activate\npip install .\n```\n\n### Usage\n\n```bash\ntrimarr --help\n```\n\n## Options\n\n| Option | Description | Default | Example | Type |\n| ------ | ----------- | ------- | ------- | ---- |\n| `--language` ✱ | One or more ISO 639-2 language codes (comma-separated) for the audio/subtitle tracks to keep. See [ISO 639-2 codes](http://en.wikipedia.org/wiki/List_of_ISO_639-2_codes). | — | `eng` or `eng,fre` | `string` |\n| `--media-path` ✱ | Path(s) to the directory/directories containing media files to process (scanned recursively). Accepts a single path or a pipe-separated list of paths (using the pipe character as delimiter, safe even when directory names contain commas). | — | `/mnt/user/Movies` or `/mnt/user/Movies\\|/mnt/media/tv` | `path` |\n| `--mkvmerge-path` | Path to the mkvmerge executable. When omitted, trimarr manages its own binary and auto-updates it. | Linux: `~/.local/share/trimarr/bin/mkvmerge`\u003cbr\u003eWindows: `%LOCALAPPDATA%\\trimarr\\bin\\mkvmerge.exe` | `/usr/bin/mkvmerge` | `path` |\n| `--database-path` | Path to the SQLite database file used for tracking processed files. | Linux: `~/.local/share/trimarr/db/trimarr.db`\u003cbr\u003eWindows: `%LOCALAPPDATA%\\trimarr\\db\\trimarr.db` | `/var/lib/trimarr/trimarr.db` | `path` |\n| `--log-path` | Path to the log file for tracking application events. | Linux: `~/.local/share/trimarr/logs/trimarr.log`\u003cbr\u003eWindows: `%LOCALAPPDATA%\\trimarr\\logs\\trimarr.log` | `/var/log/trimarr.log` | `path` |\n| `--log-level` | Logging level for console output. Choices: `DEBUG`, `INFO`, `SUCCESS`, `WARNING`, `ERROR`. | `INFO` | `DEBUG` | `choice` |\n| `--edit-metadata-title` | Update the container title metadata of each file to match its filename stem. Mutually exclusive with `--delete-metadata-title`. | `false` | — | `flag` |\n| `--delete-metadata-title` | Remove the container title metadata from each file. Mutually exclusive with `--edit-metadata-title`. | `false` | — | `flag` |\n| `--keep-subtitles` | Keep all subtitle tracks regardless of language. | `false` | — | `flag` |\n| `--keep-audio` | Keep all audio tracks regardless of language. | `false` | — | `flag` |\n| `--keep-native-audio` | Identify the film's native/original spoken language(s) via IMDb (or TMDb as fallback) and keep all audio tracks in those languages alongside your `--language` preference. Ignored when `--keep-audio` is set. First-time lookups require an internet connection; results are cached in the database. | `false` | — | `flag` |\n| `--tmdb-api-key` | TMDb API key used as fallback when IMDbPie cannot identify a film's native language. Optional — lookups that fail on IMDbPie silently fall back to standard behaviour. | — | `\u003ckey\u003e` | `string` |\n| `--no-backup` | Delete the original file after successful processing instead of renaming it to `\u003cname\u003e.bak`. By default a backup is always created. | `false` | — | `flag` |\n| `--no-update-check` | Skip the automatic check for a newer mkvmerge version. Has no effect when `--mkvmerge-path` is supplied (user-managed binaries are never auto-updated). | `false` | — | `flag` |\n| `--strip-lower-channels` | After language filtering, drop any audio tracks whose channel count is strictly below the highest channel count among the surviving audio tracks. For example, given English tracks at 8ch, 8ch, and 2ch, the 2ch track is removed. Tracks with an unknown channel count are always kept. Has no effect when `--keep-audio` is set. **Disabled by default** — enable only when you are confident lower-channel duplicates are not needed. | `false` | — | `flag` |\n| `--strip-commentary` | If specified, audio and subtitle tracks whose name contains \"commentary\" (case-insensitive) will be removed after language filtering. **Audio final gate:** if stripping would leave zero audio tracks, all audio is retained and a warning is logged — a silent file is never acceptable. Subtitles have no such gate and are stripped unconditionally. Has no effect on audio when `--keep-audio` is set, or on subtitles when `--keep-subtitles` is set. **Disabled by default.** | `false` | — | `flag` |\n| `--strip-subtitle-regex` | One or more Python regex patterns. Any subtitle track whose name matches any pattern is removed after language filtering, regardless of language. Runs after `--strip-commentary`. Specify multiple times for multiple patterns (e.g. `--strip-subtitle-regex '(?i)songs.*signs' --strip-subtitle-regex '(?i)signs.*songs'`). Has no effect when `--keep-subtitles` is set. **Disabled by default.** | `()` | `'(?i)songs.*signs'` | `string (repeatable)` |\n| `--dry-run` | Log planned changes without modifying any files. Processed files are not recorded to the database in this mode. | `false` | — | `flag` |\n| `--schedule` | Run on a cron schedule (instead of once). Standard 5-field POSIX cron expression: minute hour day month weekday. Also accepts `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`. Examples: `'0 2 * * *'` (daily at 2am), `'*/30 * * * *'` (every 30 min), `'@daily'` (once per day). When omitted, trimarr runs once and exits. | — | — | `string` |\n| `--run-on-start` | When used with `--schedule`, execute an immediate run before the first scheduled cron fire. Without `--schedule` this flag is an error. | `false` | — | `flag` |\n| `--skip-size-check` | Bypass the output size guard that rejects mkvmerge results smaller than 50 % of the source file. Use when legitimate remuxes are expected to produce significantly smaller output (e.g. files with very large audio or subtitle payloads). The structural validity check (`mkvmerge -J`) is never bypassed. | `false` | — | `flag` |\n| `--pre-process` | Shell command to run **before** processing files in each directory. The placeholders `{leaf}` (directory basename) and `{dir}` (full directory path) are substituted. Only fires for directories where at least one file needs processing. Failures are logged as warnings and do not abort the run. May be used independently of `--schedule`. | — | `'no_ransom.sh --unlock yes {leaf}'` | `string` |\n| `--post-process` | Shell command to run **after** processing files in each directory. Same placeholder behaviour as `--pre-process`. Fires even if some files in the directory failed. | — | `'no_ransom.sh --unlock no {leaf}'` | `string` |\n| `--command-timeout-mins` | Maximum time in minutes each pre/post process command is allowed to run before being killed. Set to `0` to disable the timeout entirely. | `5` | `10` | `int` |\n\n✱ Required.\n\n\u003e **Note:** Default paths are platform-aware. On Linux, paths respect `XDG_DATA_HOME` (if set to\n\u003e an absolute path, trimarr uses `$XDG_DATA_HOME/trimarr/`). On Windows, `%LOCALAPPDATA%` is used\n\u003e (falling back to `%APPDATA%`).\n\n## How it works\n\nTrimarr evaluates audio and subtitle tracks independently for each file.\n\n### Audio tracks\n\n```mermaid\nflowchart TD\n    A([Start]) --\u003e B{--keep-audio?}\n    B -- Yes --\u003e Z([Keep all audio])\n    B -- No --\u003e C[Filter by --language]\n    C --\u003e D{Any track\\nmatches language?}\n    D -- No --\u003e E([⚠️ Keep all\\nno language match])\n    D -- Yes --\u003e F{All matches\\nare commentary?}\n    F -- Yes --\u003e G([⚠️ Keep all\\ncommentary-only audio])\n    F -- No --\u003e H[Drop non-matching tracks]\n    H --\u003e SC{--strip-commentary?}\n    SC -- No --\u003e L\n    SC -- Yes --\u003e SC2{Stripping would\\nleave zero audio?}\n    SC2 -- Yes --\u003e SC3[⚠️ Keep all audio\\nsilent-file gate]\n    SC3 --\u003e L\n    SC2 -- No --\u003e SC4[Drop audio\\ncommentary tracks]\n    SC4 --\u003e L\n    L{--strip-lower-channels?}\n    L -- No --\u003e I\n    L -- Yes --\u003e M{All surviving tracks\\nsame channel count?}\n    M -- Yes --\u003e I\n    M -- No --\u003e N[Drop tracks below\\nmax channel count]\n    N --\u003e I\n    I{Commentary track\\nholds default flag?}\n    I -- No --\u003e J([✅ Apply changes])\n    I -- Yes --\u003e K[Promote non-commentary\\nto default · demote commentary]\n    K --\u003e J\n```\n\n### Subtitle tracks\n\n```mermaid\nflowchart TD\n    A([Start]) --\u003e B{--keep-subtitles?}\n    B -- Yes --\u003e Z([Keep all subtitles])\n    B -- No --\u003e C[Filter by --language]\n    C --\u003e D{Any track\\nmatches language?}\n    D -- No --\u003e E([⚠️ Keep all\\nno language match])\n    D -- Yes --\u003e H[Drop non-matching tracks]\n    H --\u003e SC{--strip-commentary?}\n    SC -- No --\u003e SR\n    SC -- Yes --\u003e SC4[Drop subtitle\\ncommentary tracks]\n    SC4 --\u003e SR\n    SR{--strip-subtitle-regex?}\n    SR -- No --\u003e I\n    SR -- Yes --\u003e SR2[Drop subtitle tracks\\nmatching regex]\n    SR2 --\u003e I\n    I{Commentary subtitle\\nholds default flag?}\n    I -- No --\u003e J([✅ Apply changes])\n    I -- Yes --\u003e K[Promote non-commentary\\nto default · demote commentary]\n    K --\u003e J\n```\n\n\u003e If a file needs no changes (all tracks already match, no metadata to edit), it is marked as\n\u003e processed in the database and skipped on all future runs — unless the file content or processing\n\u003e profile changes.\n\n\u003e **Native audio preservation:** When `--keep-native-audio` is enabled, trimarr identifies the\n\u003e film's original spoken language(s) via IMDb (or TMDb as fallback) and merges them into the\n\u003e effective language list *before* the `Filter by --language` step. This means the native language\n\u003e is treated identically to a user-supplied language — all existing safeguards (audio fallback,\n\u003e commentary stripping, channel stripping) apply uniformly. The lookup result is cached in the\n\u003e database so each unique file is resolved only once.\n\n## Scheduler\n\nBy default trimarr runs once and exits. Pass `--schedule` with a cron expression to run on a\ntimed schedule.\n\n```bash\n# Run daily at 2 AM\ntrimarr --language eng --media-path /mnt/media --schedule \"0 2 * * *\"\n\n# Run every 30 minutes, with an immediate run on startup\ntrimarr --language eng --media-path /mnt/media --schedule \"*/30 * * * *\" --run-on-start\n\n# Using a special keyword\ntrimarr --language eng --media-path /mnt/media --schedule \"@daily\"\n```\n\n### Schedule format\n\n`--schedule` accepts a standard 5-field POSIX cron expression:\n\n| Field | Values     | Description                |\n|-------|------------|----------------------------|\n| `min` | 0–59       | Minute of the hour         |\n| `hour` | 0–23       | Hour of the day            |\n| `day` | 1–31       | Day of the month           |\n| `mon` | 1–12       | Month (1 = January)        |\n| `wday` | 0–7 (0,7 = Sunday) | Day of the week      |\n\nEach field supports **wildcards** (`*`), **ranges** (`9-17`), **lists** (`9,18`), and **steps** (`*/30`, `0-30/10`).\n\nSpecial keyword shortcuts (via croniter):\n\n| Keyword     | Equivalent         |\n|-------------|--------------------|\n| `@hourly`   | `0 * * * *`        |\n| `@daily`    | `0 0 * * *`        |\n| `@weekly`   | `0 0 * * 0`        |\n| `@monthly`  | `0 0 1 * *`        |\n| `@yearly`   | `0 0 1 1 *`        |\n\n### Pre/Post process hooks\n\nUse `--pre-process` and `--post-process` to run shell commands before and after trimarr\nprocesses files in a directory. This is useful for external operations such as unlocking\nfiles before processing and re-locking them afterwards.\n\nThe hooks fire **once per directory** that has files needing processing, not per file.\n`{leaf}` and `{dir}` placeholders in the command template are replaced with the directory\nbasename and full path respectively.\n\n```bash\n# Run a script before and after processing each directory\ntrimarr --language eng --media-path /mnt/media \\\\\n  --pre-process 'no_ransom.sh --unlock yes {leaf}' \\\\\n  --post-process 'no_ransom.sh --unlock no {leaf}' \\\\\n  --schedule \"0 2 * * *\"\n\n# Only pre-process (no post-process), with a 10-minute timeout\ntrimarr --language eng --media-path /mnt/media \\\\\n  --pre-process 'prepare_dir.sh {dir}' \\\\\n  --command-timeout-mins 10\n```\n\n**Notes:**\n\n- Hooks are independent — use one, both, or neither.\n- A non-zero exit code from a hook logs a warning but does **not** stop processing.\n- If a hook times out (per `--command-timeout-mins`), the process is killed and a warning is logged.\n- Set `--command-timeout-mins 0` to disable the timeout entirely.\n\n### Cron-driven scheduling\n\nThe scheduler stays running and recomputes the next fire time from the cron expression after\neach run. If a run takes longer than the gap to the next scheduled fire, the missed fire\nis skipped automatically and a warning is logged. This approach naturally handles overruns\nwithout drift accumulation.\n\n### Stopping the scheduler\n\nPress **Ctrl+C** at any time. The scheduler logs `Scheduler stopped.` and exits with code 0.\n\n## Development\n\n```bash\ngit clone https://github.com/binhex/trimarr\ncd trimarr\nuv venv --quiet\nuv sync --extra dev\n```\n\nIf you wish to perform linting on all files before committing (PR will not be\naccepted if it does not pass all linting) then run `pre-commit run --all-files`.\n\n## FAQ\n\nWIP\n\n___\nIf you appreciate my work, then please consider buying me a beer  :D\n\n[![PayPal donation](https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=MM5E27UX6AUU4)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinhex%2Ftrimarr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinhex%2Ftrimarr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinhex%2Ftrimarr/lists"}