Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.xz
  • direct_play_nice-x86_64-apple-darwin.tar.xz
  • direct_play_nice-x86_64-unknown-linux-gnu.tar.xz
  • direct_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 (when XDG_CONFIG_HOME is 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:

  1. CLI flags (highest)
  2. --config <path>
  3. DIRECT_PLAY_NICE_CONFIG=<path>
  4. 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 or all
  • --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-output reopen the completed output and fail if expected A/V codecs, stream hygiene, or temporal checks fail (enabled by default)
  • --no-validate-output disable post-conversion output validation
  • --visual-validate-output decode sampled output frames and fail on obvious visual corruption such as repeated green-screen frames (enabled by default)
  • --no-visual-validate-output disable sampled visual validation while keeping structural validation enabled
  • --visual-quality-report log 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-input supported)
  • --servarr-output-suffix <servarr_output_suffix>
  • --servarr-language-audit run a periodic Sonarr/Radarr audit for delayed language upgrades
  • --servarr-language-audit-scope <SCOPE> choose history, inventory, or latest-missing audit 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 for D days; 0 disables this
  • --servarr-language-audit-latest-missing-no-candidate-cooldown-days <D> override the no-candidate cooldown for latest-missing audits
  • --servarr-language-audit-stale-queue-days <D> remove and blocklist zero-size torrent queue items older than D days; 0 disables this
  • --servarr-language-audit-stale-queue-max-removals <N> cap stale queue removals per audit run; 0 means no cap
  • --servarr-language-audit-episode-ids <IDS> comma-separated Sonarr episode IDs to audit instead of the full scope
  • --servarr-language-check enable pre-conversion language checks for Arr downloads
  • --required-audio-languages <LANGS> comma-separated ISO-639 tags such as eng,jpn
  • --required-subtitle-languages <LANGS> comma-separated ISO-639 tags such as eng,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-run evaluate 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: ignore drops them, fail aborts, and convert attempts 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-stream warns and disables only that subtitle stream, while fail aborts 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

  1. Runtime configuration is resolved from CLI arguments and optional config.
  2. Device profiles are intersected to choose a target container, video codec, audio codec, resolution limits, bitrate limits, and H.264 constraints.
  3. 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.
  4. 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.
  5. Video, audio, and subtitle streams are decoded, transformed, encoded, and muxed through separate stream-processing paths.
  6. Optional OCR post-processing handles bitmap subtitles and remuxes generated text subtitles into the final output.
  7. 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_ext contains metadata reads, packet allocation, buffer unref, and other narrow unsafe operations.
  • timestamp contains shared timestamp selection, rescaling, and monotonic DTS adjustment.
  • pipeline_streams owns per-packet video/audio/subtitle processing.
  • pipeline_codec owns encoder setup and rate/profile options.
  • pipeline_assessment owns 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-source
  • 360p
  • 480p
  • 720p
  • 1080p
  • 1440p
  • 2160p

These presets apply resolution caps and target bitrate ranges suitable for common direct-play scenarios.

Audio quality presets

--audio-quality supports:

  • match-source
  • 320k
  • 256k
  • 224k
  • 192k
  • 160k
  • 128k
  • 96k

Custom bitrate overrides

Use these for explicit control:

  • --max-video-bitrate <RATE> (for example 4800k, 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)
  • bilinear
  • bicubic
  • lanczos (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 CUDA scale_cuda when 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 CUDA scale_cuda and 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 skip disable subtitle processing
  • --sub-mode force force subtitle processing
  • --ocr-engine pp-ocr-v4 force PP-OCR v4 pipeline
  • --ocr-engine pp-ocr-v3 fallback for older GPU/runtime combinations
  • --ocr-format ass request ASS (may be downgraded in MP4)
  • --ocr-write-srt-sidecar write .srt sidecars 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=1
  • DPN_OCR_FORCE_CPU=1

ONNX engines:

  • --ocr-engine pp-ocr-v4 for modern GPU/runtime stacks
  • --ocr-engine pp-ocr-v3 for legacy/older GPU compatibility cases

Linux runtime notes:

  • Ensure CUDA/cuDNN and ONNX Runtime are version-compatible.
  • ORT_DYLIB_PATH=/path/to/libonnxruntime.so can be used if ONNX Runtime is not discoverable on default library paths.
  • For older NVIDIA stacks, --ocr-engine pp-ocr-v3 can be more stable than pp-ocr-v4.
  • Use scripts/ocr-tools/check_gpu_env.sh to 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.onnx
  • japan_PP-OCRv4_rec_mobile.onnx
  • korean_PP-OCRv4_rec_mobile.onnx
  • chinese_cht_PP-OCRv3_rec_mobile.onnx

Override paths for these optional profiles with:

  • DPN_OCR_REC_LATIN_MODEL
  • DPN_OCR_REC_MULTILINGUAL_MODEL
  • DPN_OCR_REC_JAPANESE_MODEL
  • DPN_OCR_REC_KOREAN_MODEL
  • DPN_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_OVERRIDES Example: spa=latin,rus=multilingual,sr-Latn=latin Script tags are also recognized automatically (for example zh-Hant, sr-Cyrl, sr-Latn).
  • DPN_OCR_LANGUAGE_SCRIPT_HINTS Example: rus=Cyrl,ara=Arab,srp=Cyrl
  • DPN_OCR_ROUTING_MANIFEST Path to custom TOML routing manifest (default: config/ocr-routing.toml in 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

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-smi reports compute capability major <= 5 (Maxwell-class and older), --ocr-engine auto prefers pp-ocr-v3 and 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 auto and verify provider availability with probe logs; if providers are unavailable, OCR falls back to CPU/Tesseract path

OCR performance and validation artifacts


  1. NVIDIA FFmpeg acceleration guide: https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/ffmpeg-with-nvidia-gpu/index.html. ↩2

  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

  3. FFmpeg h264_qsv / hevc_qsv encoder options: https://ffmpeg.org/ffmpeg-codecs.html#QSV-Encoders. ↩2

  4. FFmpeg h264_vaapi / hevc_vaapi encoder options: https://ffmpeg.org/ffmpeg-codecs.html#VAAPI-encoders. ↩2

  5. Apple VideoToolbox framework: https://developer.apple.com/documentation/videotoolbox. ↩2 ↩3

  6. AMD AMF SDK: https://github.com/GPUOpen-LibrariesAndSDKs/AMF. ↩2 ↩3

  7. NVIDIA Video encode/decode support matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new.

  8. Intel oneVPL supported hardware: https://www.intel.com/content/www/us/en/docs/onevpl/upgrade-from-msdk/2021-3/supported-hardware.html.

  9. ONNX Runtime CUDA execution provider: https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html.

  10. ONNX Runtime DirectML execution provider: https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html.

  11. ONNX Runtime CoreML execution provider: https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html.

  12. 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=true
  • DIRECT_PLAY_NICE_PLEX_URL=http://127.0.0.1:32400
  • DIRECT_PLAY_NICE_PLEX_TOKEN=... (or PLEX_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

  1. Arr service imports media.
  2. Custom script invokes direct_play_nice.
  3. Successful conversion output replaces source according to configured behavior.

Event behavior

The binary auto-detects Sonarr/Radarr custom-script invocations:

  • sonarr_eventtype=Download and radarr_eventtype=Download trigger conversion.
  • Non-download events (for example Test, Grab, Rename) exit cleanly.

Naming and replacement notes

  • Use --servarr-output-extension and --servarr-output-suffix to control output naming.
  • --delete-source applies to direct CLI usage.
  • In Sonarr/Radarr mode, replacement/rollback logic is handled by integration flow.
  • --servarr-output-extension match-input keeps 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 as Anime-multi-audio or anime-multi-sub.
  • custom-format-or-title: custom formats plus strong title tokens such as Dual-Audio, Multi-Audio, Multi-Subs, or MSubs.
  • 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:

  1. 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
    
  2. Use latest-missing dry-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
    
  3. 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
    
  4. 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, or Multi-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.

  5. Apply only a focused batch. Use Sonarr episode IDs from the dry-run logs to constrain the destructive run. Keep --servarr-language-audit-max-searches at 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
    
  6. Watch Arr’s queue and history. A successful Sonarr language replacement usually goes grabbeddownloadFolderImported; 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.

  7. Prioritize recent items when needed. Prefer latest-missing for a built-in newest-missing priority lane. DPN can also target a caller-supplied list of Sonarr episode IDs with --servarr-language-audit-episode-ids for fully custom Sonarr batches.

  8. 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_JOBS
  • DIRECT_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_PATH when libonnxruntime.so is in a non-standard location
  • set DPN_OCR_REQUIRE_GPU=1 for 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 none as 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=1 to fail fast instead of silently falling back
  • try --ocr-engine pp-ocr-v3 on 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

  1. Pick the next version.

    Prerelease versions use SemVer suffixes such as 0.1.0-beta.3, 0.1.0-alpha.12, or 0.1.0-rc.1. GitHub and crates.io treat those as prereleases. Stable releases do not have a suffix, for example 0.1.0.

  2. Update Cargo.toml.

    version = "0.1.0-beta.3"
    
  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.
    
  4. 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.md already contains concrete notes for the exact Cargo.toml version.

Merge and Verify

  1. Merge the release PR to main.

  2. Wait for the merge pipelines to pass:

    • Continuous Deployment
    • Benchmarks (Post-Merge)
  3. 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.

  1. Open GitHub Actions.

  2. Run the Release workflow manually.

  3. Enter the exact tag, including the leading v.

    v0.1.0-beta.3
    
  4. Wait for all release jobs to pass.

    Expected platform archives:

    • direct_play_nice-aarch64-apple-darwin.tar.xz
    • direct_play_nice-x86_64-apple-darwin.tar.xz
    • direct_play_nice-x86_64-unknown-linux-gnu.tar.xz
    • direct_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:

  • isDraft is false.
  • isPrerelease matches the version suffix.
  • The release body contains the matching CHANGELOG.md section.
  • 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 Release workflow 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 on main to pass before publishing or repairing binaries.