Tdarr - Plugins Personnalisés¶
Plugins Tdarr optimisés pour Intel HD 530 (Quick Sync 6.0) et bibliothèque Jellyfin.
Plugin 1 : Media Optimizer - Audio Remux¶
Objectif : Convertir audio multi-canal (DTS, EAC3, TrueHD) → AAC stéréo, garder FR + Original seulement
Configuration¶
function details() {
return {
id: "Custom_MediaOptimizer_AudioRemux",
Stage: "Pre-processing",
Name: "Media Optimizer - Audio Remux FR+VO",
Type: "Video",
Operation: "Transcode",
Description: "Convertit audio 5.1/7.1 vers AAC 2.0, garde FR + Original, strip autres langues et sous-titres non FR/EN",
Version: "1.0",
Tags: "audio,french,remux,aac",
Inputs: [
{
name: "target_audio_codec",
type: "string",
defaultValue: "aac",
tooltip: "Codec audio cible (aac ou libfdk_aac si disponible)"
},
{
name: "target_audio_bitrate",
type: "string",
defaultValue: "256k",
tooltip: "Bitrate audio pour stéréo"
},
{
name: "keep_original_audio",
type: "boolean",
defaultValue: true,
tooltip: "Garder piste audio langue originale"
}
]
};
}
function plugin(file, librarySettings, inputs, otherArguments) {
const lib = require('../methods/lib')();
inputs = lib.loadDefaultValues(inputs, details);
const response = {
processFile: false,
preset: "",
container: ".mkv",
handBrakeMode: false,
FFmpegMode: true,
reQueueAfter: false,
infoLog: ""
};
// Skip si pas de streams audio
if (file.ffProbeData.streams.filter(s => s.codec_type === "audio").length === 0) {
response.infoLog += "⏭️ No audio streams, skipping\n";
return response;
}
// Checker si déjà optimisé (tous audio = AAC 2ch)
const audioStreams = file.ffProbeData.streams.filter(s => s.codec_type === "audio");
const needsProcessing = audioStreams.some(s =>
s.codec_name !== "aac" || s.channels > 2
);
if (!needsProcessing) {
response.infoLog += "✅ Audio déjà optimisé (AAC 2.0)\n";
return response;
}
// Construction FFmpeg command
let ffmpegCommand = "";
// Input
ffmpegCommand += "-i <io> ";
// Map vidéo (copy)
ffmpegCommand += "-map 0:v -c:v copy ";
// Map audio FR (priorité VFF/VOF/VF)
const frenchAudio = audioStreams.find(s =>
s.tags && (
s.tags.language === "fre" ||
s.tags.language === "fra" ||
s.tags.title?.includes("French") ||
s.tags.title?.includes("VFF") ||
s.tags.title?.includes("VOF")
)
);
if (frenchAudio) {
ffmpegCommand += `-map 0:${frenchAudio.index} `;
} else {
// Fallback première piste audio
ffmpegCommand += `-map 0:a:0 `;
}
// Map audio original si demandé et différent du français
if (inputs.keep_original_audio) {
const originalLanguage = file.meta?.OriginalLanguage || "eng";
const originalAudio = audioStreams.find(s =>
s.tags && s.tags.language === originalLanguage && s !== frenchAudio
);
if (originalAudio) {
ffmpegCommand += `-map 0:${originalAudio.index} `;
}
}
// Encoder audio en AAC stéréo avec downmix intelligent
const audioCodec = inputs.target_audio_codec;
const audioBitrate = inputs.target_audio_bitrate;
ffmpegCommand += `-c:a ${audioCodec} -ac 2 -b:a ${audioBitrate} `;
// Downmix matrix pour 5.1 → 2.0 (préserve dialogues et ambiance)
ffmpegCommand += `-af "pan=stereo|FL=FC+0.30*FL+0.30*BL+0.30*LFE|FR=FC+0.30*FR+0.30*BR+0.30*LFE" `;
// Map sous-titres FR et EN seulement
ffmpegCommand += `-map 0:s:fre? -map 0:s:eng? -c:s copy `;
// Metadata
ffmpegCommand += `-metadata:s:a:0 language=fra -metadata:s:a:0 title="Français Stéréo" `;
if (inputs.keep_original_audio) {
ffmpegCommand += `-metadata:s:a:1 language=original -metadata:s:a:1 title="Original Stéréo" `;
}
// Dispositions : default sur piste française
ffmpegCommand += `-disposition:a:0 default -disposition:s:0 0 `;
response.processFile = true;
response.preset = ffmpegCommand;
response.infoLog += `🔄 Audio remux: ${audioStreams.length} pistes → 2 pistes AAC 2.0\n`;
return response;
}
module.exports.details = details;
module.exports.plugin = plugin;
Plugin 2 : Media Optimizer - Video Transcode 4K¶
Objectif : Réencoder HEVC 10-bit → HEVC 8-bit SDR, ou H.264 high-bitrate → HEVC 8-bit
Configuration¶
function details() {
return {
id: "Custom_MediaOptimizer_VideoTranscode4K",
Stage: "Pre-processing",
Name: "Media Optimizer - Video Transcode 4K",
Type: "Video",
Operation: "Transcode",
Description: "Réencode HEVC 10-bit ou H.264 >30Mbps vers HEVC 8-bit SDR avec Intel QSV/VAAPI",
Version: "1.0",
Tags: "video,4k,hevc,vaapi,hdr",
Inputs: [
{
name: "target_codec",
type: "string",
defaultValue: "hevc_vaapi",
tooltip: "Codec vidéo cible (hevc_vaapi pour Intel QSV)"
},
{
name: "target_quality",
type: "string",
defaultValue: "23",
tooltip: "QP (constant quality) : 18-28, 23 recommandé"
},
{
name: "bitrate_threshold_mbps",
type: "number",
defaultValue: 30,
tooltip: "Réencoder si bitrate source > seuil (Mbps)"
},
{
name: "strip_hdr",
type: "boolean",
defaultValue: true,
tooltip: "Convertir HDR → SDR BT.709"
}
]
};
}
function plugin(file, librarySettings, inputs, otherArguments) {
const lib = require('../methods/lib')();
inputs = lib.loadDefaultValues(inputs, details);
const response = {
processFile: false,
preset: "",
container: ".mkv",
handBrakeMode: false,
FFmpegMode: true,
reQueueAfter: false,
infoLog: ""
};
const videoStream = file.ffProbeData.streams.find(s => s.codec_type === "video");
if (!videoStream) {
response.infoLog += "⏭️ No video stream found\n";
return response;
}
// Décision : besoin de réencodage ?
let needsTranscode = false;
let reason = "";
// Check 1 : HEVC 10-bit
if (videoStream.codec_name === "hevc" &&
(videoStream.profile === "Main 10" || videoStream.pix_fmt?.includes("10"))) {
needsTranscode = true;
reason += "HEVC 10-bit → 8-bit, ";
}
// Check 2 : Bitrate élevé H.264
const bitrateMbps = file.bit_rate / 1000000; // Convert to Mbps
if (videoStream.codec_name === "h264" && bitrateMbps > inputs.bitrate_threshold_mbps) {
needsTranscode = true;
reason += `H.264 ${bitrateMbps.toFixed(1)}Mbps > ${inputs.bitrate_threshold_mbps}Mbps, `;
}
// Check 3 : HDR présent
const isHDR = videoStream.color_transfer === "smpte2084" || // PQ (HDR10)
videoStream.color_transfer === "arib-std-b67" || // HLG
videoStream.tags?.["ENCODER_OPTIONS"]?.includes("HDR");
if (isHDR && inputs.strip_hdr) {
needsTranscode = true;
reason += "HDR → SDR, ";
}
// Skip si déjà HEVC 8-bit SDR optimal
if (videoStream.codec_name === "hevc" &&
videoStream.profile === "Main" &&
!isHDR &&
bitrateMbps <= inputs.bitrate_threshold_mbps) {
response.infoLog += "✅ Déjà optimisé (HEVC 8-bit SDR)\n";
return response;
}
if (!needsTranscode) {
response.infoLog += "✅ Vidéo ne nécessite pas de transcoding\n";
return response;
}
// Construction FFmpeg command avec hardware acceleration
let ffmpegCommand = "";
// Hardware decode
ffmpegCommand += "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 ";
// Input
ffmpegCommand += "-i <io> ";
// Video filters
let vf = "format=nv12,hwupload";
// Scale si nécessaire (conserver résolution source)
const width = videoStream.width;
const height = videoStream.height;
vf += `,scale_vaapi=w=${width}:h=${height}:format=nv12`;
ffmpegCommand += `-vf "${vf}" `;
// Video encoding avec VAAPI
ffmpegCommand += `-c:v ${inputs.target_codec} `;
ffmpegCommand += `-profile:v main `; // HEVC 8-bit Main profile
ffmpegCommand += `-qp ${inputs.target_quality} `;
ffmpegCommand += `-bf 3 `; // B-frames
// Colorspace SDR
if (inputs.strip_hdr) {
ffmpegCommand += `-pix_fmt nv12 `;
ffmpegCommand += `-colorspace bt709 `;
ffmpegCommand += `-color_primaries bt709 `;
ffmpegCommand += `-color_trc bt709 `;
}
// Copy audio et sous-titres
ffmpegCommand += `-c:a copy -c:s copy `;
// Map tous les streams
ffmpegCommand += `-map 0 `;
response.processFile = true;
response.preset = ffmpegCommand;
response.infoLog += `🔄 Video transcode: ${reason.slice(0, -2)}\n`;
response.infoLog += ` ${videoStream.width}x${videoStream.height} ${videoStream.codec_name} → ${inputs.target_codec}\n`;
return response;
}
module.exports.details = details;
module.exports.plugin = plugin;
Plugin 3 : Media Optimizer - Health Check¶
Objectif : Vérifier intégrité fichier après transcoding
Configuration¶
function details() {
return {
id: "Custom_MediaOptimizer_HealthCheck",
Stage: "Post-processing",
Name: "Media Optimizer - Health Check",
Type: "Video",
Operation: "Transcode",
Description: "Vérifie intégrité fichier : durée, taille, lisibilité",
Version: "1.0",
Tags: "healthcheck,validation",
Inputs: [
{
name: "min_file_size_ratio",
type: "number",
defaultValue: 0.3,
tooltip: "Taille minimale vs source (0.3 = 30%)"
},
{
name: "max_duration_diff_seconds",
type: "number",
defaultValue: 5,
tooltip: "Différence durée maximale autorisée (secondes)"
}
]
};
}
function plugin(file, librarySettings, inputs, otherArguments) {
const lib = require('../methods/lib')();
inputs = lib.loadDefaultValues(inputs, details);
const response = {
processFile: false,
preset: "",
container: ".mkv",
handBrakeMode: false,
FFmpegMode: false,
reQueueAfter: false,
infoLog: ""
};
// Récupérer infos fichier original (avant transcode)
const originalSize = otherArguments?.originalLibraryFile?.file_size || 0;
const originalDuration = otherArguments?.originalLibraryFile?.ffProbeData?.format?.duration || 0;
const currentSize = file.file_size;
const currentDuration = file.ffProbeData?.format?.duration || 0;
// Check 1: Taille fichier
if (originalSize > 0) {
const sizeRatio = currentSize / originalSize;
if (sizeRatio < inputs.min_file_size_ratio) {
response.infoLog += `❌ ERREUR: Fichier trop petit (${(sizeRatio*100).toFixed(1)}% vs ${inputs.min_file_size_ratio*100}% attendu)\n`;
response.infoLog += ` Original: ${(originalSize/1e9).toFixed(2)} GB, Actuel: ${(currentSize/1e9).toFixed(2)} GB\n`;
// Marquer pour requeue (retry)
response.reQueueAfter = true;
return response;
}
response.infoLog += `✅ Taille OK: ${(sizeRatio*100).toFixed(1)}% de l'original (${(currentSize/1e9).toFixed(2)} GB)\n`;
}
// Check 2: Durée
if (originalDuration > 0 && currentDuration > 0) {
const durationDiff = Math.abs(currentDuration - originalDuration);
if (durationDiff > inputs.max_duration_diff_seconds) {
response.infoLog += `❌ ERREUR: Durée invalide (diff ${durationDiff.toFixed(1)}s)\n`;
response.infoLog += ` Original: ${originalDuration.toFixed(1)}s, Actuel: ${currentDuration.toFixed(1)}s\n`;
response.reQueueAfter = true;
return response;
}
response.infoLog += `✅ Durée OK: ${currentDuration.toFixed(0)}s (diff ${durationDiff.toFixed(1)}s)\n`;
}
// Check 3: Streams vidéo/audio présents
const hasVideo = file.ffProbeData.streams.some(s => s.codec_type === "video");
const hasAudio = file.ffProbeData.streams.some(s => s.codec_type === "audio");
if (!hasVideo) {
response.infoLog += `❌ ERREUR: Aucun stream vidéo détecté\n`;
response.reQueueAfter = true;
return response;
}
if (!hasAudio) {
response.infoLog += `⚠️ WARNING: Aucun stream audio détecté\n`;
}
response.infoLog += `✅ Health check PASSED\n`;
response.infoLog += ` Video: ${hasVideo ? "✅" : "❌"}, Audio: ${hasAudio ? "✅" : "❌"}\n`;
return response;
}
module.exports.details = details;
module.exports.plugin = plugin;
Flow Tdarr Recommandé¶
Configuration Library¶
Library: Movies Optimized
Source: /media/movies
Cache: /temp/movies-cache
Transcode Flow:
Stage 1 - Pre-processing:
1. [Built-in] File Size Filter (>500 MB)
2. [Custom] Media Optimizer - Audio Remux
- Condition: Audio non-AAC OU >2 channels
- Action: Remux audio vers AAC 2.0
3. [Custom] Media Optimizer - Video Transcode 4K
- Condition: HEVC 10-bit OU H.264 >30Mbps OU HDR
- Action: Transcode vers HEVC 8-bit SDR
Stage 2 - Post-processing:
4. [Custom] Media Optimizer - Health Check
- Action: Valider intégrité
- Error: Re-queue si échec
Options:
- Replace original: ✅
- Create backup: ✅ (/media/.tdarr-originals/)
- Hold after queue: ❌ (traitement automatique)
Monitoring et Métriques¶
Dashboard Tdarr¶
Métriques clés: - Files in queue - Processing speed (avg transcodes/hour) - Worker health (% CPU, errors) - Library statistics: - % files optimized - Total space saved (GB) - Avg file size reduction (%)
Logs Utiles¶
# Logs Tdarr main
docker logs -f tdarr
# Logs transcoding spécifiques
tail -f /opt/docker/tdarr/logs/node.log
# Filtrer erreurs
docker logs tdarr 2>&1 | grep -i "error\|failed"
Commandes Debug¶
# Test plugin manuellement sur un fichier
docker exec -it tdarr node \
/app/server/Tdarr_Plugin_Custom_MediaOptimizer_AudioRemux.js \
/media/movies/test.mkv
# Vérifier VAAPI dans container
docker exec tdarr ls -la /dev/dri/
# Test hardware encode
docker exec tdarr ffmpeg -hwaccel vaapi \
-hwaccel_device /dev/dri/renderD128 \
-f lavfi -i testsrc=duration=10:size=1920x1080:rate=30 \
-c:v hevc_vaapi -qp 23 -t 5 /tmp/test.mkv
Troubleshooting¶
Erreur: "Failed to initialize VAAPI"¶
# Vérifier device présent
docker exec tdarr ls -la /dev/dri/renderD128
# Vérifier permissions
docker exec tdarr id tdarr
# Doit inclure groups 104 (render) et 44 (video)
# Recréer container avec bons groups
docker compose down tdarr
docker compose up -d tdarr
Erreur: "libva error: va_getDriverName() failed with unknown libva error,driver_name=(null)"¶
# Installer drivers dans container (si nécessaire)
docker exec -u root tdarr apt update
docker exec -u root tdarr apt install -y intel-media-va-driver libva2
docker restart tdarr
Performance Lente¶
# Vérifier charge GPU
intel_gpu_top
# Si GPU idle pendant transcode → Pas de HW accel
# Solution: Vérifier args FFmpeg dans logs plugin
docker logs tdarr | grep "ffmpeg.*vaapi"