direct-play-nice Manual
This manual is the operational guide for the direct_play_nice CLI.
Use it for:
- installation and setup
- first-run walkthroughs
- full command/flag reference
- Sonarr/Radarr automation
- troubleshooting hardware, codec, and OCR issues
If you only need the quick install and a one-command example, use the project
README.md.
Installation
Prebuilt binaries
Each GitHub release publishes platform archives and checksums built by
cargo-dist:
direct_play_nice-aarch64-apple-darwin.tar.xzdirect_play_nice-x86_64-apple-darwin.tar.xzdirect_play_nice-x86_64-unknown-linux-gnu.tar.xzdirect_play_nice-x86_64-pc-windows-msvc.zip
The release also includes shell and PowerShell installers.
From crates.io
cargo install direct_play_nice
From source
git clone https://github.com/ns-mkusper/direct-play-nice.git
cd direct-play-nice
cargo build --release
Binary path:
target/release/direct_play_nice
Getting Started
First conversion
direct_play_nice input.mkv output.mp4
Set up a default config file
If you run this tool often, create a config once and keep day-to-day commands short.
By default, direct-play-nice reads:
$XDG_CONFIG_HOME/direct-play-nice/config.toml(whenXDG_CONFIG_HOMEis set)~/.config/direct-play-nice/config.toml
Create the file:
mkdir -p ~/.config/direct-play-nice
cat > ~/.config/direct-play-nice/config.toml <<'EOF'
streaming_devices = "all"
video_quality = "match-source"
video_codec = "auto"
audio_quality = "192k"
hw_accel = "auto"
unsupported_video_policy = "ignore"
sub_mode = "auto"
ocr_default_language = "eng"
servarr_output_extension = "mp4"
servarr_output_suffix = ".fixed"
[plex]
refresh = false
EOF
Why these defaults are sane
streaming_devices = "all"keeps output compatible across all built-in device profiles.video_quality = "match-source"avoids unnecessary downscaling by default.video_codec = "auto"lets the tool pick the safest codec intersection.audio_quality = "192k"is a practical bitrate for broad AAC compatibility.hw_accel = "auto"uses hardware encoding when available and falls back to software when not.unsupported_video_policy = "ignore"skips extra video streams that can break muxing in common container/player paths.sub_mode = "auto"only OCRs bitmap subtitles when needed.ocr_default_language = "eng"gives OCR a stable fallback language.servarr_output_extension = "mp4"targets the most portable container for direct play.servarr_output_suffix = ".fixed"makes replaced files easy to identify during rollout.
Override order
When the same option appears in multiple places, priority is:
- CLI flags (highest)
--config <path>DIRECT_PLAY_NICE_CONFIG=<path>- Default config location above
Device targeting
Use --device to narrow compatibility constraints:
direct_play_nice --device chromecast input.mkv output.mp4
direct_play_nice --device chromecast,roku input.mkv output.mp4
--device all (or omitting --device) computes a profile compatible across all
built-in device definitions.
Inspect an input before converting
direct_play_nice --probe-streams input.mkv
direct_play_nice --probe-streams --output json input.mkv
Command Reference
Synopsis
direct_play_nice [OPTIONS] [INPUT_FILE] [OUTPUT_FILE]
Positional arguments
[INPUT_FILE]video file to convert (required unless probing)[OUTPUT_FILE]output media file (required unless probing)
Device and quality options
-d, --device <DEVICE>target family/model orall--video-quality <video_quality>quality preset--video-codec <video_codec>auto|h264|hevc--audio-quality <audio_quality>quality preset--max-video-bitrate <max_video_bitrate>explicit video cap--max-audio-bitrate <max_audio_bitrate>explicit audio cap--resize-quality <resize_quality>fast-bilinear|bilinear|bicubic|lanczos|spline--resize-backend <resize_backend>auto|software|cuda
Stream and compatibility controls
--unsupported-video-policy <unsupported_video_policy>convert|ignore|fail--primary-video-stream-index <primary_video_stream_index>--primary-video-criteria <primary_video_criteria>resolution|bitrate|fps--skip-codec-check--validate-outputreopen the completed output and fail if expected A/V codecs, stream hygiene, or temporal checks fail (enabled by default)--no-validate-outputdisable post-conversion output validation--visual-validate-outputdecode sampled output frames and fail on obvious visual corruption such as repeated green-screen frames (enabled by default)--no-visual-validate-outputdisable sampled visual validation while keeping structural validation enabled--visual-quality-reportlog sampled luma/chroma statistics for troubleshooting scaling or visual-corruption issues--visual-scan-frames <N>set how many decoded video frames visual validation scans before deciding the output is safe enough to promote--visual-sample-interval <N>inspect every Nth decoded frame during visual validation; default is 15--visual-failure-ratio <R>set the sampled-frame fraction that must look corrupt before visual validation fails; default is 0.60
Hardware controls
--hw-accel <hw_accel>auto|none|nvenc|vaapi|qsv|videotoolbox|amf
Probe modes
--probe-streams--probe-hw--probe-codecs--probe-ocr-fixtures <PATH>evaluate OCR accuracy against fixture PNG+JSON pairs--only-video--only-hw--probe-json--output <OUTPUT>text|json--streams-filter <STREAMS_FILTER>all|video|audio|subtitle
Sonarr/Radarr options
--servarr-output-extension <EXTENSION>(match-inputsupported)--servarr-output-suffix <servarr_output_suffix>--servarr-language-auditrun a periodic Sonarr/Radarr audit for delayed language upgrades--servarr-language-audit-scope <SCOPE>choosehistory,inventory, orlatest-missingaudit source--servarr-language-audit-lookback-days <DAYS>recent import window for history audit mode--servarr-language-audit-max-searches <N>cap release searches per audit run--servarr-language-audit-no-candidate-cooldown-days <D>skip items with a recent no-candidate audit result forDdays;0disables this--servarr-language-audit-latest-missing-no-candidate-cooldown-days <D>override the no-candidate cooldown forlatest-missingaudits--servarr-language-audit-stale-queue-days <D>remove and blocklist zero-size torrent queue items older thanDdays;0disables this--servarr-language-audit-stale-queue-max-removals <N>cap stale queue removals per audit run;0means no cap--servarr-language-audit-episode-ids <IDS>comma-separated Sonarr episode IDs to audit instead of the full scope--servarr-language-checkenable pre-conversion language checks for Arr downloads--required-audio-languages <LANGS>comma-separated ISO-639 tags such aseng,jpn--required-subtitle-languages <LANGS>comma-separated ISO-639 tags such aseng,spa--servarr-api-url <URL>Sonarr/Radarr base URL for mismatch replacement checks--servarr-api-key <KEY>Sonarr/Radarr API key for mismatch replacement checks--servarr-language-dry-runevaluate candidates without grabbing or blocklisting--servarr-untagged-audio-language <LANG>opt-in language tag to apply to untagged audio streams before redownload decisions--servarr-untagged-subtitle-language <LANG>opt-in language tag to apply to untagged subtitle streams before redownload decisions--servarr-language-candidate-policy <POLICY>strict|custom-format|custom-format-or-title|title-guess--delete-source [<BOOL>]
Subtitle OCR options
--sub-mode <sub_mode>auto|force|skip--subtitle-failure-policy <subtitle_failure_policy>skip-stream|fail--ocr-default-language <ocr_default_language>--ocr-engine <ocr_engine>auto|tesseract|pp-ocr-v3|pp-ocr-v4|external--ocr-format <ocr_format>srt|ass--ocr-write-srt-sidecar
Failure policy
- Extra video streams follow
--unsupported-video-policy:ignoredrops them,failaborts, andconvertattempts to include them when the output container supports it. - Attachments, data streams, and attached pictures are treated as metadata and skipped for direct-play outputs.
- Bitmap subtitles are handled by the OCR side pass unless
--sub-mode=skip; text subtitles are converted to MP4-compatible timed text when included. - Subtitle decode/encode timestamp failures follow
--subtitle-failure-policy:skip-streamwarns and disables only that subtitle stream, whilefailaborts conversion. - Audio conversion setup is fail-fast: if FFmpeg cannot initialize the required resampler, conversion aborts rather than writing suspect audio.
- Hardware encoder/profile failures may retry with a safer software encoder path; decoder bitstream failures remain hard failures.
Plex options
--plex-refresh--plex-url <PLEX_URL>--plex-token <PLEX_TOKEN>
Full generated help
For the exact, version-specific clap output:
direct_play_nice --help
Architecture
direct-play-nice converts media by separating policy decisions from FFmpeg’s
packet loop.
Conversion Model
- Runtime configuration is resolved from CLI arguments and optional config.
- Device profiles are intersected to choose a target container, video codec, audio codec, resolution limits, bitrate limits, and H.264 constraints.
- The input is probed for direct-play compatibility. FFmpeg’s detected demuxer is the primary container signal; filename extension is used only to disambiguate MOV-family containers or as a fallback.
- Conversion creates an explicit input-to-output stream map. Input stream indexes are used only for demuxed packets; output stream indexes are used only for encoded packets and muxer metadata.
- Video, audio, and subtitle streams are decoded, transformed, encoded, and muxed through separate stream-processing paths.
- Optional OCR post-processing handles bitmap subtitles and remuxes generated text subtitles into the final output.
- Post-write verification checks H.264 constraints. Output validation is enabled by default: the final media file is reopened to verify expected stream codecs, stream hygiene, temporal consistency, and sampled visual statistics for obvious corruption such as repeated green-screen frames. The visual layer exists because a file can decode cleanly and still look dangerously wrong to users. Operators can tune how many frames are scanned, how often samples are inspected, and what sampled corruption ratio fails.
FFmpeg Boundaries
Most FFmpeg operations use rsmpeg wrappers. Raw pointer access is isolated in small helpers where possible:
ffmpeg_extcontains metadata reads, packet allocation, buffer unref, and other narrow unsafe operations.timestampcontains shared timestamp selection, rescaling, and monotonic DTS adjustment.pipeline_streamsowns per-packet video/audio/subtitle processing.pipeline_codecowns encoder setup and rate/profile options.pipeline_assessmentowns direct-play compatibility explanations.
Content Policy
Playable A/V streams are preserved by conversion. Attachments, data streams,
and attached pictures are metadata for direct-play targets and are skipped.
Extra video streams are governed by --unsupported-video-policy. Text
subtitles are converted when included; bitmap subtitles are deferred to OCR
unless subtitle processing is skipped.
Failures are intentionally policy-driven. Audio setup failures abort because
they would risk invalid audio. Subtitle stream failures follow
--subtitle-failure-policy: the default isolates the bad subtitle stream so
A/V conversion can still complete, while strict mode aborts. Hardware encoder
failures may retry with software when a safe fallback is available.
Quality Controls
By default, the CLI preserves source quality (match-source) for both video
and audio.
Video quality presets
--video-quality supports:
match-source360p480p720p1080p1440p2160p
These presets apply resolution caps and target bitrate ranges suitable for common direct-play scenarios.
Audio quality presets
--audio-quality supports:
match-source320k256k224k192k160k128k96k
Custom bitrate overrides
Use these for explicit control:
--max-video-bitrate <RATE>(for example4800k,6M,12.5mbps)--max-audio-bitrate <RATE>
When overrides are provided, they constrain the selected quality profile.
Resizing
When a source exceeds a selected device or quality cap, the video is resized down while preserving aspect ratio and keeping encoder-friendly dimensions. The tool does not enlarge source dimensions as part of this deterministic FFmpeg resizing path.
--resize-quality supports:
fast-bilinear(fastest)bilinearbicubiclanczos(default, highest-quality deterministic default)spline
Higher-quality resize kernels generally cost more CPU time. They remain deterministic FFmpeg scalers and do not add AI model dependencies.
--resize-backend controls where resizing runs:
auto(default): use CUDAscale_cudawhen CUDA decode, NVENC encode, filter support, and selected quality make a zero-copy GPU resize path possible; otherwise use software.software: always use libswscale on CPU.cuda: require CUDAscale_cudaand fail if the GPU resize path cannot be used.
The CUDA backend currently supports bilinear, bicubic, and lanczos
interpolation. fast-bilinear and spline fall back to software in auto mode
because scale_cuda does not expose matching kernels.
The resize benchmark harness compares downscale speed and full-reference quality against a deterministic 360p reference:
scripts/resize-tools/run_resize_benchmark.sh
The benchmark writes a CSV report with elapsed time, FPS, realtime factor,
output size, and full-reference quality metrics. It reports VMAF when FFmpeg has
libvmaf, PSNR component/average/min/max scores in dB, and SSIM
component/aggregate scores including the SSIM dB transform.
Set DPN_RESIZE_REF_VIDEO=/path/to/reference.mkv to use a real source clip.
If DPN_RESIZE_REF_VIDEO is unset, the harness also honors the benchmark-runner
BENCHMARK_SOURCE_PATH environment variable. Set DPN_RESIZE_BENCH_SS=00:01:30
to start from a representative section. Set DPN_RESIZE_CUDA=1 to add a
lanczos,cuda,nvenc candidate alongside the software scaler candidates.
Subtitle OCR
Bitmap subtitle formats (PGS/VobSub/DVD) are not directly compatible with MP4
Direct Play in many client stacks. direct_play_nice can OCR bitmap subtitles
into text tracks using AI OCR backends (PP-OCR/Tesseract). This path is meant
for bitmap subtitle streams; text subtitles are muxed directly when compatible.
For official GPU architecture/provider references and compatibility links, see Hardware Acceleration.
Defaults
--sub-mode auto--ocr-engine auto--ocr-format srt
Common overrides
--sub-mode skipdisable subtitle processing--sub-mode forceforce subtitle processing--ocr-engine pp-ocr-v4force PP-OCR v4 pipeline--ocr-engine pp-ocr-v3fallback for older GPU/runtime combinations--ocr-format assrequest ASS (may be downgraded in MP4)--ocr-write-srt-sidecarwrite.srtsidecars in addition to embedded output
OCR flow
Bitmap subtitle OCR runs as a side pass after the main media streams are planned. The default path is PP-OCR first; Tesseract is only used as a quality fallback when enabled and when the PP-OCR result fails generic quality checks.
flowchart TD
A[Input container] --> B{Subtitle stream type}
B -->|Text subtitle| C[Copy or remux text subtitle]
B -->|Bitmap subtitle| D[Decode subtitle packets]
D --> E[Rasterize subtitle rectangles to OCR images]
E --> F[Select OCR engine and runtime provider]
F --> G[Run PP-OCR detector and recognizer]
G --> H[Prune impossible geometry]
H --> I[Generic text normalization]
I --> J{Spacing or quality issue?}
J -->|No| N[Emit OCR cue]
J -->|Yes| K[Try PP-OCR word/phrase recovery]
K --> L{Recovered output improves quality?}
L -->|Yes| N
L -->|No| M{Safety fallback enabled and useful?}
M -->|Yes| N
M -->|No| N
N --> O[Write SRT/ASS track]
O --> P[Mux output and optional sidecar]
Quality checks include low spacing density, long glued tokens, mixed-case glue, low-information garbage fragments, and impossible bounding boxes. The goal is to prefer the best AI OCR result first, then use the fallback only for residual cue-level failures.
GPU behavior
The OCR runtime attempts provider fallback when available (for example CUDA, DirectML, CoreML, then CPU). You can force behavior with:
DPN_OCR_REQUIRE_GPU=1DPN_OCR_FORCE_CPU=1
ONNX engines:
--ocr-engine pp-ocr-v4for modern GPU/runtime stacks--ocr-engine pp-ocr-v3for legacy/older GPU compatibility cases
Linux runtime notes:
- Ensure CUDA/cuDNN and ONNX Runtime are version-compatible.
ORT_DYLIB_PATH=/path/to/libonnxruntime.socan be used if ONNX Runtime is not discoverable on default library paths.- For older NVIDIA stacks,
--ocr-engine pp-ocr-v3can be more stable thanpp-ocr-v4. - Use
scripts/ocr-tools/check_gpu_env.shto inspect runtime/library setup. - Containerized workloads may need NVIDIA Container Toolkit and exposed runtime libraries.
Model location
Models are downloaded to a default model directory unless DPN_OCR_MODEL_DIR
is set.
Default model filenames:
- v4:
ch_PP-OCRv4_det_infer.onnx,ch_ppocr_mobile_v2.0_cls_infer.onnx,en_PP-OCRv4_rec_infer.onnx - v3:
ch_PP-OCRv3_det_infer.onnx,ch_ppocr_mobile_v2.0_cls_train.onnx,en_PP-OCRv3_rec_infer.onnx
Optional profile rec models are also auto-provisioned (downloaded on first use if missing in the model directory):
latin_PP-OCRv3_rec_mobile.onnxjapan_PP-OCRv4_rec_mobile.onnxkorean_PP-OCRv4_rec_mobile.onnxchinese_cht_PP-OCRv3_rec_mobile.onnx
Override paths for these optional profiles with:
DPN_OCR_REC_LATIN_MODELDPN_OCR_REC_MULTILINGUAL_MODELDPN_OCR_REC_JAPANESE_MODELDPN_OCR_REC_KOREAN_MODELDPN_OCR_REC_CJK_MODEL
DPN_OCR_REC_MULTILINGUAL_MODEL is local-first: if unset, OCR auto-detects a
compatible multilingual recognizer already present in the model directory
(for example multilingual_PP-OCRv4_rec_infer.onnx) and uses it when script
routing targets multilingual coverage. Unlike latin/japanese/korean/cjk
profiles, this profile is not downloaded automatically.
Override recognition profile routing (language -> profile) with:
DPN_OCR_REC_PROFILE_OVERRIDESExample:spa=latin,rus=multilingual,sr-Latn=latinScript tags are also recognized automatically (for examplezh-Hant,sr-Cyrl,sr-Latn).DPN_OCR_LANGUAGE_SCRIPT_HINTSExample:rus=Cyrl,ara=Arab,srp=CyrlDPN_OCR_ROUTING_MANIFESTPath to custom TOML routing manifest (default:config/ocr-routing.tomlin the repo source tree).
Config-file example
sub_mode = "auto" # auto | force | skip
ocr_default_language = "eng"
ocr_engine = "auto" # auto | tesseract | pp-ocr-v3 | pp-ocr-v4 | external
ocr_format = "srt" # srt | ass
ocr_write_srt_sidecar = false
ocr_external_command = "python3 /opt/ocr/run.py"
Hardware Acceleration
This chapter covers GPU acceleration in direct_play_nice for:
- H.264/HEVC hardware transcoding via FFmpeg encoders
- AI OCR for bitmap subtitle streams (PGS/VobSub/DVD)
Transcoding acceleration
direct_play_nice hardware encoder selection is currently targeted at H.264
and HEVC output.
Codec and hardware mapping implemented by the CLI
- H.264 hardware encoders:
h264_nvenc,h264_qsv,h264_vaapi,h264_videotoolbox,h264_amf1 2 3 4 5 6 - HEVC hardware encoders:
hevc_nvenc,hevc_qsv,hevc_vaapi,hevc_videotoolbox,hevc_amf1 2 3 4 5 6 - Backend availability is OS/build dependent and discovered at runtime7 8 6 5
You can inspect your current host/build support with:
direct_play_nice --probe-hw --probe-codecs --only-video --only-hw --probe-json
Transcoding performance and validation artifacts
- NVENC end-to-end matrix test validates profile/level/bitrate/device behavior:
- NVENC regression tests:
OCR acceleration (bitmap subtitles)
direct_play_nice uses ONNX Runtime providers for PP-OCR and has explicit
legacy-NVIDIA logic in auto mode.
What is supported in this project
- NVIDIA CUDA path for PP-OCRv3/PP-OCRv4 (primary validated path)
- Legacy NVIDIA behavior: if
nvidia-smireports compute capability major<= 5(Maxwell-class and older),--ocr-engine autopreferspp-ocr-v3and disables classifier for stability - Windows DirectML and Apple CoreML provider paths are wired and can be used when runtimes are installed9 10 11
- CPU fallback is available (or forced with
DPN_OCR_FORCE_CPU=1)
OCR workload guidance by hardware class
- Older NVIDIA families (Maxwell/Pascal-era systems): prefer
--ocr-engine pp-ocr-v312 - Newer NVIDIA families (Turing/Ampere/Ada): start with
--ocr-engine pp-ocr-v4 - Non-NVIDIA GPUs: use
autoand verify provider availability with probe logs; if providers are unavailable, OCR falls back to CPU/Tesseract path
OCR performance and validation artifacts
- Full-movie OCR benchmark (self-hosted Linux, PP-OCRv3 GPU-required profile):
87.62 FPS,3.65xrealtime - OCR AI/GPU paths are covered in integration tests:
-
NVIDIA FFmpeg acceleration guide: https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/ffmpeg-with-nvidia-gpu/index.html. ↩ ↩2
-
NVIDIA NVENC programming guide: https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/nvenc-video-encoder-api-prog-guide/index.html. ↩ ↩2
-
FFmpeg
h264_qsv/hevc_qsvencoder options: https://ffmpeg.org/ffmpeg-codecs.html#QSV-Encoders. ↩ ↩2 -
FFmpeg
h264_vaapi/hevc_vaapiencoder options: https://ffmpeg.org/ffmpeg-codecs.html#VAAPI-encoders. ↩ ↩2 -
Apple VideoToolbox framework: https://developer.apple.com/documentation/videotoolbox. ↩ ↩2 ↩3
-
AMD AMF SDK: https://github.com/GPUOpen-LibrariesAndSDKs/AMF. ↩ ↩2 ↩3
-
NVIDIA Video encode/decode support matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new. ↩
-
Intel oneVPL supported hardware: https://www.intel.com/content/www/us/en/docs/onevpl/upgrade-from-msdk/2021-3/supported-hardware.html. ↩
-
ONNX Runtime CUDA execution provider: https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html. ↩
-
ONNX Runtime DirectML execution provider: https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html. ↩
-
ONNX Runtime CoreML execution provider: https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html. ↩
-
NVIDIA CUDA GPU compute capability list: https://developer.nvidia.com/cuda-gpus/. ↩
Plex Refresh
To avoid the “Plex dance” (manual library rescans after conversion),
direct_play_nice can trigger a targeted Plex refresh automatically.
CLI options
--plex-refresh--plex-url <PLEX_URL>(default:http://127.0.0.1:32400)--plex-token <PLEX_TOKEN>
Environment variable equivalents:
DIRECT_PLAY_NICE_PLEX_REFRESH=trueDIRECT_PLAY_NICE_PLEX_URL=http://127.0.0.1:32400DIRECT_PLAY_NICE_PLEX_TOKEN=...(orPLEX_TOKEN)
Example:
direct_play_nice \
--plex-refresh \
--plex-url http://127.0.0.1:32400 \
--plex-token "$PLEX_TOKEN" \
input.mkv output.mp4
Config file equivalent
[plex]
refresh = true
url = "http://127.0.0.1:32400"
token = "YOUR_TOKEN"
Need a token? See Plex support: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
Sonarr/Radarr Integration
direct-play-nice can be wired as a custom script in Sonarr/Radarr pipelines.
High-level flow
- Arr service imports media.
- Custom script invokes
direct_play_nice. - Successful conversion output replaces source according to configured behavior.
Event behavior
The binary auto-detects Sonarr/Radarr custom-script invocations:
sonarr_eventtype=Downloadandradarr_eventtype=Downloadtrigger conversion.- Non-download events (for example
Test,Grab,Rename) exit cleanly.
Naming and replacement notes
- Use
--servarr-output-extensionand--servarr-output-suffixto control output naming. --delete-sourceapplies to direct CLI usage.- In Sonarr/Radarr mode, replacement/rollback logic is handled by integration flow.
--servarr-output-extension match-inputkeeps the source container.
Example command in Sonarr custom script:
/path/to/direct_play_nice --config-file /path/to/direct-play-nice-sonarr.toml
Optional language mismatch replacement
Language checks are off by default. When enabled on a Sonarr/Radarr Download
event, direct-play-nice inspects the imported file before conversion. If any
configured audio or subtitle language is missing, conversion is skipped and DPN
asks Sonarr/Radarr for manual-search release results. It only grabs a specific
replacement release when the returned release metadata or configured candidate
policy indicates the required languages. Existing-file/cutoff rejections may be
overridden for language upgrades, but unrelated rejection reasons are still
honored. If no verified candidate is available, DPN leaves the current file
untouched and does not request a blind redownload.
CLI examples
Dry-run first while tuning your matching rules. This inspects the imported file, queries Arr for replacement candidates when requirements are missing, and reports what would happen without grabbing or blocklisting anything:
/path/to/direct_play_nice \
--servarr-language-check \
--servarr-language-dry-run \
--servarr-language-candidate-policy custom-format-or-title \
--required-audio-languages eng \
--servarr-api-url http://127.0.0.1:8989 \
--servarr-api-key "$SONARR_API_KEY"
After dry-run output looks correct, remove --servarr-language-dry-run to allow
DPN to grab the selected replacement and blocklist the old history item:
/path/to/direct_play_nice \
--servarr-language-check \
--servarr-language-candidate-policy custom-format-or-title \
--required-audio-languages eng \
--servarr-api-url http://127.0.0.1:8989 \
--servarr-api-key "$SONARR_API_KEY"
Config-file example
In a Sonarr/Radarr custom script, prefer keeping the command short and putting policy in the DPN config file:
/path/to/direct_play_nice --config-file /path/to/direct-play-nice-sonarr.toml
servarr_language_check = true
servarr_language_audit = true
servarr_language_audit_scope = "history"
servarr_language_audit_lookback_days = 30
servarr_language_audit_max_searches = 20
# Optional: prevent persistent no-candidate items from consuming every audit run.
servarr_language_audit_no_candidate_cooldown_days = 14
# Optional: limit a Sonarr audit to specific episode IDs.
# servarr_language_audit_episode_ids = "123,456"
required_audio_languages = "eng"
# Leave empty unless subtitle completeness is a goal.
required_subtitle_languages = ""
# Optional: for trusted English-native libraries, retag untagged audio before
# deciding the file is missing English audio.
servarr_untagged_audio_language = "eng"
servarr_api_url = "http://127.0.0.1:8989"
servarr_api_key = "..."
# Recommended while tuning rules. Logs the selected candidate but does not grab
# or blocklist anything.
servarr_language_dry_run = true
# strict only trusts explicit Arr language/subtitle metadata. custom-format-or-title
# also trusts matching custom formats and strong tokens like Dual-Audio/Multi-Subs.
servarr_language_candidate_policy = "custom-format-or-title"
For Radarr, use the Radarr URL/key instead:
servarr_api_url = "http://127.0.0.1:7878"
servarr_api_key = "..."
servarr_api_url and servarr_api_key can also be supplied with CLI flags or
environment variables. Supported env names include SONARR_URL,
SONARR_API_KEY, RADARR_URL, and RADARR_API_KEY.
Before grabbing a verified replacement, DPN marks the current Arr history item as
failed so the old release is blocklisted. It resolves that history item from the
Arr download id when available, or from DIRECT_PLAY_NICE_SONARR_HISTORY_ID /
DIRECT_PLAY_NICE_RADARR_HISTORY_ID if your wrapper provides it.
For Sonarr/Radarr, a language-better file can be stuck as a completed queue item when Arr considers it a quality downgrade. Audit mode checks those pending imports; when the pending file satisfies DPN’s language policy, apply mode deletes the old episode/movie-file entry, posts manual import, and removes the completed queue item without deleting the downloaded replacement.
The default examples above require English audio only. Extra audio languages are
allowed; a file with English plus Japanese audio still satisfies
required_audio_languages = "eng". Use stricter audio requirements such as
eng,jpn only for libraries where preserving the original language alongside the
dub is required. Leave required_subtitle_languages empty if missing subtitles
are acceptable.
Untagged streams are handled conservatively. By default, und/empty language
metadata does not satisfy a required language. If a library is known to be
English-native, set servarr_untagged_audio_language = "eng" or pass
--servarr-untagged-audio-language eng to remux unknown audio streams with an
English tag before DPN searches for replacement releases. The remux uses stream
copy, writes through a temporary file, and never overwrites an explicit
non-unknown language tag. servarr_language_dry_run = true only reports the
retag action. Use servarr_untagged_subtitle_language only for trusted subtitle
streams; DPN does not run speech recognition or globally infer und = eng.
Candidate policies control how much DPN infers before a replacement is grabbed:
strict: only explicit Arr language/subtitle metadata.custom-format: explicit metadata plus matching Arr custom format names such asAnime-multi-audiooranime-multi-sub.custom-format-or-title: custom formats plus strong title tokens such asDual-Audio,Multi-Audio,Multi-Subs, orMSubs.title-guess: looser title matching. Use dry-run first.
DPN always treats the already-imported file differently from candidate releases:
actual file compliance is based on FFmpeg stream metadata, then cached by DPN.
The best-effort cache defaults to
$XDG_CACHE_HOME/direct-play-nice/servarr-language-cache.json or
~/.cache/direct-play-nice/servarr-language-cache.json; override it with
DIRECT_PLAY_NICE_LANGUAGE_CACHE.
Periodic language audit
The Download-event hook only runs when Arr imports a file. To catch delayed dubs or subtitles that appear days later, run an audit from cron/systemd/launchd with no Arr custom-script environment variables.
Language upgrade process
The periodic audit is language-first. It treats queue remediation, delayed dub/sub searches, and conservative torrent cleanup as separate stages so a bad or blocked queue item does not permanently prevent a better language candidate from importing.
flowchart TD
A[Start periodic Servarr language audit] --> B[Fetch current Arr queue]
B --> C[Log Sonarr queue health before cleanup]
C --> D{Bad queue item?}
D -->|Invalid/wrong episode or multi-episode pack| E[Remove and blocklist]
D -->|Completed importPending/importBlocked language upgrade| F[Manual import if candidate satisfies language policy]
D -->|Stale zero-size torrent older than configured days| G{Within stale cleanup cap?}
G -->|Yes| H[Remove, blocklist, and search replacement]
G -->|No| I[Leave queued torrent for later run]
D -->|No| I
E --> J[Fetch queue again]
F --> J
H --> J
I --> J
J --> K[Log queue health after cleanup/import]
K --> L{Audit scope}
L -->|latest-missing| M[Scan inventory, filter non-compliant items, newest first]
L -->|inventory| N[Scan inventory backlog]
L -->|history| O[Scan recent import history]
M --> P{Required languages present?}
N --> P
O --> P
P -->|Yes| Q[Cache compliant language report]
P -->|No and cooldown active| R[Skip search this run]
P -->|No and cooldown expired| S[Search Arr releases]
S --> T{Verified language candidate?}
T -->|Yes| U[Grab replacement and blocklist old history item]
T -->|No| V[Record no-candidate cooldown]
U --> W[Next run imports or force-imports pending replacement]
Q --> X[Log audit summary]
R --> X
V --> X
W --> X
Queue health is logged at the start of Sonarr audits and after cleanup/import stages. The log includes total queue items, completed import-pending/import-blocked items, stale zero-size torrents, queued nonzero torrents, active downloads, and invalid/mismatch warning counts. Use those counters to distinguish genuine bad queue entries from a saturated torrent client. For example, many old zero-size queued torrents may indicate Deluge’s active download queue is too small rather than a bad release.
Run an audit with:
/path/to/direct_play_nice \
--config-file /path/to/direct-play-nice-sonarr.toml \
--servarr-language-audit \
--servarr-language-audit-lookback-days 30 \
--servarr-language-audit-max-searches 20 \
--servarr-language-dry-run
By default, audit mode uses --servarr-language-audit-scope history: it queries
recent Sonarr/Radarr imports, inspects the actual imported file language
metadata, updates DPN’s cache, and release-searches only missing-language items
up to --servarr-language-audit-max-searches. Use
--servarr-language-audit-scope inventory with Sonarr to inspect the current
library inventory instead of only recent import history. Inventory scope walks
series episode files, checks current media metadata, then uses each missing
item’s latest import history entry before any apply-mode grab/blocklist action.
Use --servarr-language-audit-scope latest-missing to inspect inventory, keep
only currently non-compliant files, sort those by newest air/release date first,
and spend the search budget on recent delayed dubs/subs before older backlog.
If early inventory items repeatedly return no approved replacement, set
--servarr-language-audit-no-candidate-cooldown-days (or
servarr_language_audit_no_candidate_cooldown_days) so those no-candidate items
are not release-searched again until the cooldown expires. For weekly delayed
sub/dub drops, use
--servarr-language-audit-latest-missing-no-candidate-cooldown-days (or
servarr_language_audit_latest_missing_no_candidate_cooldown_days) to give
latest-missing a shorter retry window than broad backlog sweeps. DPN stores
this best-effort state in
$XDG_CACHE_HOME/direct-play-nice/servarr-language-no-candidate-cache.json or
~/.cache/direct-play-nice/servarr-language-no-candidate-cache.json; override it
with DIRECT_PLAY_NICE_LANGUAGE_NO_CANDIDATE_CACHE. This lets later inventory
items get searched on subsequent capped audit runs instead of reprocessing the
same no-candidate edge every day.
For focused Sonarr batches, pass --servarr-language-audit-episode-ids with a
comma-separated episode ID list.
DPN also checks completed pending imports that Sonarr/Radarr refused for quality hierarchy reasons. When deciding whether to force-import a pending language upgrade, DPN inspects the existing media file’s actual stream metadata instead of trusting Arr’s language label. This lets a lower-quality pending file replace a higher-quality current file only when the current file is missing a required language and the pending file satisfies the language policy.
Sonarr auto-grabs are limited to releases that map back to the single requested
episode; multi-episode and season-pack results are skipped even when their titles
contain strong language hints. During audits, DPN can also clean up hopeless
queue entries: completed warning items with invalid season/episode or wrong
multi-episode mappings are removed and blocklisted, while quality-only warnings
are left for possible force-import. Set
--servarr-language-audit-stale-queue-days to also remove and blocklist
zero-size torrent queue items that have not acquired metadata after the given
number of days. DPN immediately searches for a replacement after stale cleanup,
preferring non-torrent candidates when possible; use
--servarr-language-audit-stale-queue-max-removals to cap churn per run. Keep
dry-run enabled while reviewing reports; remove
--servarr-language-dry-run only when you want DPN to grab selected
language-upgrade candidates, blocklist the old history item, force-import
eligible pending replacements, and clean bad queue entries.
This feature assumes DPN is the authority for language upgrades. To avoid Arr and DPN fighting each other, keep ordinary Arr quality-only upgrades conservative or disabled for libraries where DPN should make language-first replacement decisions.
Safe language upgrade runbook
Language replacement is intentionally opt-in and should be rolled out in small, observable batches. A safe operator workflow is:
-
Start with a dry-run history audit. This catches delayed dubs/subs for recent imports without scanning the whole library:
/path/to/direct_play_nice \ --config-file /path/to/direct-play-nice-sonarr.toml \ --servarr-language-audit \ --servarr-language-audit-scope history \ --servarr-language-audit-lookback-days 30 \ --servarr-language-audit-max-searches 20 \ --servarr-language-dry-run -
Use
latest-missingdry-run for delayed dub/sub follow-up. This prioritizes newest aired/released non-compliant files before older backlog:/path/to/direct_play_nice \ --config-file /path/to/direct-play-nice-sonarr.toml \ --servarr-language-audit \ --servarr-language-audit-scope latest-missing \ --servarr-language-audit-max-searches 25 \ --servarr-language-audit-latest-missing-no-candidate-cooldown-days 2 \ --servarr-language-dry-run -
Use inventory dry-run for backlog discovery. This can be slow on large libraries, so keep the search cap low while tuning:
/path/to/direct_play_nice \ --config-file /path/to/direct-play-nice-sonarr.toml \ --servarr-language-audit \ --servarr-language-audit-scope inventory \ --servarr-language-audit-max-searches 25 \ --servarr-language-dry-run -
Review candidate evidence before apply mode. Prefer candidates with explicit Arr language metadata or strong custom-format/title evidence such as
Anime-multi-audio,anime-multi-sub,Dual-Audio,Multi-Audio, orMulti-Subs. Treat unrelated rejections such as blocked indexers, unknown series, or seed/availability failures as blockers. Do not add subtitle requirements unless missing subtitles should trigger redownloads. -
Apply only a focused batch. Use Sonarr episode IDs from the dry-run logs to constrain the destructive run. Keep
--servarr-language-audit-max-searchesat or below the number of intended items:/path/to/direct_play_nice \ --config-file /path/to/direct-play-nice-sonarr.toml \ --servarr-language-audit \ --servarr-language-audit-scope inventory \ --servarr-language-audit-episode-ids "12345,12346,12347" \ --servarr-language-audit-max-searches 3 -
Watch Arr’s queue and history. A successful Sonarr language replacement usually goes
grabbed→downloadFolderImported; the imported file should then show the required audio/subtitle languages in Arr media info. Some candidates may remain queued/downloading for a while, and failed alternates in history do not necessarily mean the current queued candidate failed. -
Prioritize recent items when needed. Prefer
latest-missingfor a built-in newest-missing priority lane. DPN can also target a caller-supplied list of Sonarr episode IDs with--servarr-language-audit-episode-idsfor fully custom Sonarr batches. -
Verify and repeat. Re-run the same command with
--servarr-language-dry-run. Completed items should no longer be searched; remaining missing items should either show a queued candidate, no candidate, or a rejection reason to fix before another apply batch.
For Sonarr/Radarr, keep the same dry-run-first workflow. In apply mode, DPN may force-import completed pending replacements that satisfy the language policy by deleting the old episode/movie-file entry, posting manual import, and removing the stale completed queue item.
Practical wrapper pattern
For GPU OCR environments, keep a stable wrapper script as the command Sonarr or Radarr calls. This keeps runtime paths and OCR flags centralized.
Example wrapper:
#!/usr/bin/env bash
set -euo pipefail
export ORT_DYLIB_PATH=\"/opt/onnxruntime/lib/libonnxruntime.so\"
export LD_LIBRARY_PATH=\"/opt/onnxruntime/lib:${LD_LIBRARY_PATH:-}\"
exec /path/to/direct_play_nice --config-file /path/to/direct-play-nice-sonarr.toml
This avoids drift between manual shell runs and Arr-triggered runs.
For service-specific script setup, see the Servarr docs:
Probe and Debug
Hardware probe
direct_play_nice --probe-hw
direct_play_nice --probe-hw --probe-json
Codec inventory
direct_play_nice --probe-codecs --only-video
direct_play_nice --probe-codecs --only-video --only-hw --probe-json
Input stream probe
direct_play_nice --probe-streams input.mkv
direct_play_nice --probe-streams --output json input.mkv
Concurrency controls
DIRECT_PLAY_NICE_MAX_JOBSDIRECT_PLAY_NICE_JOBS_PER_GPU
Use these to tune parallel conversion throughput per machine.
For GPU architecture guidance and official vendor/runtime references (ONNX, CUDA, DirectML, Video Codec SDK, oneVPL, AMF, VideoToolbox), see Hardware Acceleration.
OCR runtime diagnostics (Linux)
- verify ONNX Runtime linkage if OCR provider loading fails
- set
ORT_DYLIB_PATHwhenlibonnxruntime.sois in a non-standard location - set
DPN_OCR_REQUIRE_GPU=1for fail-fast behavior when GPU OCR is mandatory
Troubleshooting
--probe-hw shows no usable hardware encoders
- confirm FFmpeg build includes your target encoder (
h264_nvenc,h264_qsv, etc.) - run
--probe-codecs --only-video --only-hw - set
--hw-accel noneas a fallback path while debugging
OCR falls back to CPU unexpectedly
- verify runtime libraries for your platform/provider are installed
- use
DPN_OCR_REQUIRE_GPU=1to fail fast instead of silently falling back - try
--ocr-engine pp-ocr-v3on older GPUs/runtime stacks
Output not directly playable on a target client
- verify target selection (
--device) - inspect stream details with
--probe-streams - set explicit bitrate/quality limits to match endpoint constraints
- compare against SUPPORTED_DEVICES.md
Build and Test
Build from source
cargo install cargo-vcpkg
cargo vcpkg build
cargo build
If your vcpkg checkout is in a non-default location, set VCPKG_ROOT.
External vcpkg host notes
Some long-lived Linux hosts keep a shared vcpkg checkout outside the repo, for
example under /home/$USER/vcpkg. On those hosts, use that checkout explicitly:
source "$HOME/.cargo/env"
export VCPKG_ROOT=/home/$USER/vcpkg
export VCPKGRS_TRIPLET=x64-linux
export LD_LIBRARY_PATH="$VCPKG_ROOT/installed/x64-linux/lib:${LD_LIBRARY_PATH:-}"
cargo build --release
If rsmpeg fails with missing FFmpeg struct fields such as AVFormatContext.pb,
AVFormatContext.streams, or AVBitStreamFilter.name, bindgen likely generated
opaque FFmpeg structs for the local headers. Reuse the bundled FFmpeg 8 bindings
from rusty_ffmpeg while still linking against the host vcpkg libraries:
export FFMPEG_BINDING_PATH="$(
find "$HOME/.cargo/registry/src" \
-path '*/rusty_ffmpeg-0.16.7+ffmpeg.8/src/binding.rs' \
-print -quit
)"
cargo build --release
This keeps source builds reproducible on hosts whose clang/bindgen combination does not expose FFmpeg internals consistently.
Run tests
cargo test
Run integration tests requiring ffmpeg CLI:
VCPKG_ROOT=/opt/vcpkg cargo test --features ffmpeg-cli-tests
Optional NVENC regression suite
ENABLE_NVENC_TESTS=1 cargo test nvenc_matrix -- --test-threads=1
Optional direnv setup
export VCPKG_ROOT=/opt/vcpkg
export RUST_LOG=${RUST_LOG:-WARN}
Quality gates
Run the same strict checks used in CI before opening a PR:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items
cargo test --no-run
Release Process
Use this process when cutting a new source and binary release.
Prepare the Release PR
-
Pick the next version.
Prerelease versions use SemVer suffixes such as
0.1.0-beta.3,0.1.0-alpha.12, or0.1.0-rc.1. GitHub and crates.io treat those as prereleases. Stable releases do not have a suffix, for example0.1.0. -
Update
Cargo.toml.version = "0.1.0-beta.3" -
Add a concrete section to
CHANGELOG.md.Do not leave release notes only under
[Unreleased]. The binary release workflow extracts the section that matches the tag.## [0.1.0-beta.3] - 2026-04-29 ### Highlights - Added or changed something user-visible. - Fixed something release-worthy. -
Open a PR with the version bump and changelog entry.
The release-readiness workflow checks whether release metadata will be updated on merge and verifies that
CHANGELOG.mdalready contains concrete notes for the exactCargo.tomlversion.
Merge and Verify
-
Merge the release PR to
main. -
Wait for the merge pipelines to pass:
Continuous DeploymentBenchmarks (Post-Merge)
-
Confirm the release tag exists.
git fetch --tags origin git rev-parse v0.1.0-beta.3
Publish or Rerun Binaries
Use the Release workflow when binaries need to be built or repaired.
-
Open GitHub Actions.
-
Run the
Releaseworkflow manually. -
Enter the exact tag, including the leading
v.v0.1.0-beta.3 -
Wait for all release jobs to pass.
Expected platform archives:
direct_play_nice-aarch64-apple-darwin.tar.xzdirect_play_nice-x86_64-apple-darwin.tar.xzdirect_play_nice-x86_64-unknown-linux-gnu.tar.xzdirect_play_nice-x86_64-pc-windows-msvc.zip
The workflow also publishes checksums, installers,
dist-manifest.json, and the source archive.
Verify the Published Release
Check the GitHub release:
gh release view v0.1.0-beta.3 --json tagName,isDraft,isPrerelease,publishedAt,assets
Verify:
isDraftisfalse.isPrereleasematches the version suffix.- The release body contains the matching
CHANGELOG.mdsection. - All expected binary archives and checksum files are present.
Check crates.io:
curl -sS https://crates.io/api/v1/crates/direct_play_nice/0.1.0-beta.3
Notes
- Manual
Releaseworkflow reruns update an existing GitHub release and replace assets with the same names. - The workflow resolves the GitHub release target from the requested tag, not from the branch used to dispatch the workflow.
- Release-workflow binary publishing does not rerun benchmarks. It waits for
the matching
Benchmarks (Post-Merge)run onmainto pass before publishing or repairing binaries.