Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gssariev/6a89f81e2cc36c35f002e064261118cc to your computer and use it in GitHub Desktop.
Save gssariev/6a89f81e2cc36c35f002e064261118cc to your computer and use it in GitHub Desktop.
Modified version of Henk's 'Keep Native Lang Plus Eng' Tdarr plugin to keep only the original language track & user specified language tracks. Works with both Radarr & Sonarr.
/* eslint-disable no-await-in-loop */
module.exports.dependencies = ['axios@0.27.2', '@cospired/i18n-iso-languages'];
const details = () => ({
id: 'Tdarr_Plugin_henk_Keep_Native_Lang_Plus_Eng',
Stage: 'Pre-processing',
Name: 'Remove all langs except native and English',
Type: 'Audio',
Operation: 'Transcode',
Description: `This is a modified version made by gsariev of the original plugin. This plugin will remove all language audio tracks except the 'native' and user-specified languages.
(requires TMDB api key).
'Native' languages are the ones that are listed on TMDB. It does an API call to
Radarr, Sonarr to check if the movie/series exists and grabs the IMDb id. As a last resort, it
falls back to the IMDb id in the filename.`,
Version: '1.2',
Tags: 'pre-processing,configurable',
Inputs: [
{
name: 'user_langs',
type: 'string',
defaultValue: '',
inputUI: {
type: 'text',
},
tooltip:
'Input a comma-separated list of ISO-639-2 languages. It will still keep English and undefined tracks.'
+ '(https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes 639-2 column)'
+ '\\nExample:\\n'
+ 'ger,fre',
},
{
name: 'priority',
type: 'string',
defaultValue: 'radarr',
inputUI: {
type: 'text',
},
tooltip:
'Priority for either Radarr or Sonarr. Leaving it empty defaults to Radarr first.'
+ '\\nExample:\\n'
+ 'sonarr',
},
{
name: 'api_key',
type: 'string',
defaultValue: '',
inputUI: {
type: 'text',
},
tooltip:
'Input your TMDB api (v3) key here. (https://www.themoviedb.org/)',
},
{
name: 'radarr_api_key',
type: 'string',
defaultValue: '',
inputUI: {
type: 'text',
},
tooltip: 'Input your Radarr api key here.',
},
{
name: 'radarr_url',
type: 'string',
defaultValue: '192.168.1.2:7878',
inputUI: {
type: 'text',
},
tooltip:
'Input your Radarr url here. (Without http://). Do include the port.'
+ '\\nExample:\\n'
+ '192.168.1.2:7878',
},
{
name: 'sonarr_api_key',
type: 'string',
defaultValue: '',
inputUI: {
type: 'text',
},
tooltip: 'Input your Sonarr api key here.',
},
{
name: 'sonarr_url',
type: 'string',
defaultValue: '192.168.1.2:8989',
inputUI: {
type: 'text',
},
tooltip:
'Input your Sonarr url here. (Without http://). Do include the port.'
+ '\\nExample:\\n'
+ '192.168.1.2:8989',
},
{
name: 'commentary',
type: 'boolean',
defaultValue: false,
inputUI: {
type: 'dropdown',
options: [
'false',
'true',
],
},
tooltip: `Specify if audio tracks that contain commentary/description should be removed.
\\nExample:\\n
true
\\nExample:\\n
false`,
},
],
});
const response = {
processFile: false,
preset: ', -map 0 ',
container: '.',
handBrakeMode: false,
FFmpegMode: true,
reQueueAfter: false,
infoLog: '',
};
const languageConverter = (tmdbLanguageCode) => {
const isoLang = require('@cospired/i18n-iso-languages');
try {
// Convert TMDB language code to ISO-639-2 3-letter format
const convertedLanguageCode = isoLang.alpha2ToAlpha3B(tmdbLanguageCode);
// Log the converted language code
response.infoLog += `TMDB Language Code Return: ${convertedLanguageCode}\n`;
return convertedLanguageCode;
} catch (error) {
console.error('Error converting language code:', error.message);
response.infoLog += '☒Error converting language code.\n';
return null;
}
};
const parseArrResponse = (body, filePath, arr) => {
// eslint-disable-next-line default-case
switch (arr) {
case 'radarr':
return body.movie;
case 'sonarr':
return body.series;
}
};
const processStreams = (result, file, userLangs, isSonarr, includeCommentary) => {
const languages = require('@cospired/i18n-iso-languages');
const tracks = {
keep: [],
remove: [],
remLangs: '',
};
let streamIndex = 0;
// Convert the TMDB language code to ISO-639-2 3-letter format dynamically
const tmdbLanguageCode = result.original_language;
const convertedLanguageCode = languageConverter(tmdbLanguageCode) || tmdbLanguageCode;
response.infoLog += `Original language tag: ${convertedLanguageCode}\n`;
// Add the original language to the list of languages to keep
tracks.keep.push(streamIndex);
response.infoLog += `Keeping the original language audio track: ${languages.getName(result.original_language, 'en')}\n`;
// Flag to indicate if any audio track matches the specified languages
let matchFound = false;
for (const stream of file.ffProbeData.streams) {
if (stream.codec_type === 'audio') {
if (!stream.tags) {
response.infoLog += `☒No tags found on audio track ${streamIndex}. Removing it.\n`;
tracks.remove.push(streamIndex);
response.preset += `-map -0:a:${streamIndex} `;
} else if (stream.tags.title && isCommentaryTrack(stream.tags.title)) {
// Remove commentary tracks if includeCommentary is true
if (includeCommentary) {
response.infoLog += `☒Removing commentary audio track: ${languages.getName(stream.tags.language, 'en')} (commentary) - ${stream.tags.title}\n`;
tracks.remove.push(streamIndex);
response.preset += `-map -0:a:${streamIndex} `;
tracks.remLangs += `${languages.getName(stream.tags.language, 'en')} (commentary), `;
}
} else if (stream.tags.language) {
// Check if the language is in the user-defined languages or it's the original language
const mappedLanguage = isSonarr ? mapSonarrLanguageToTMDB(stream.tags.language) : mapRadarrLanguageToTMDB(stream.tags.language);
if (userLangs.includes(mappedLanguage) || mappedLanguage === convertedLanguageCode) {
tracks.keep.push(streamIndex);
response.infoLog += `☑Keeping audio track with language: ${languages.getName(stream.tags.language, 'en')}\n`;
matchFound = true; // At least one track matches the specified languages
} else {
response.infoLog += `☒Removing audio track with language: ${languages.getName(stream.tags.language, 'en')}\n`;
tracks.remove.push(streamIndex);
response.preset += `-map -0:a:${streamIndex} `;
tracks.remLangs += `${languages.getName(stream.tags.language, 'en')}, `;
}
} else {
response.infoLog += `☒No language tag found on audio track ${streamIndex}. Removing it.\n`;
tracks.remove.push(streamIndex);
response.preset += `-map -0:a:${streamIndex} `;
}
streamIndex += 1;
}
}
response.preset += ' -c copy -max_muxing_queue_size 9999';
// If none of the audio tracks match the specified languages, stop the plugin
if (!matchFound) {
response.infoLog += '☒Cancelling plugin because none of the audio tracks match the specified languages. \n';
response.processFile = false;
// Clear the removal tracks to prevent further deletion
tracks.remove = [];
}
return tracks;
};
const mapRadarrLanguageToTMDB = (radarrLanguage) => {
const languageMappings = {
chi: 'cn',
// Add additional mapping if needed
};
return languageMappings[radarrLanguage] || radarrLanguage;
};
const mapSonarrLanguageToTMDB = (sonarrLanguage) => {
const languageMappings = {
// Add mappings for Sonarr languages if needed
};
return languageMappings[sonarrLanguage] || sonarrLanguage;
};
const tmdbApi = async (filename, api_key, axios) => {
let fileName;
if (filename) {
if (filename.slice(0, 2) === 'tt') {
fileName = filename;
} else {
const idRegex = /(tt\d{7,8})/;
const fileMatch = filename.match(idRegex);
if (fileMatch) {
fileName = fileMatch[1];
}
}
}
if (fileName) {
try {
const result = await axios
.get(
`https://api.themoviedb.org/3/find/${fileName}?api_key=`
+ `${api_key}&language=en-US&external_source=imdb_id`,
)
.then((resp) => (resp.data.movie_results.length > 0
? resp.data.movie_results[0]
: resp.data.tv_results[0]));
console.log('TMDB API Result:', result);
if (!result) {
response.infoLog += '☒No IMDb result was found. \n';
}
if (result) {
const tmdbLanguageCode = languageConverter(result.original_language);
response.infoLog += `Converted TMDB Language Code: ${tmdbLanguageCode}\n`;
response.infoLog += `Language tag picked up by TMDB: ${tmdbLanguageCode}\n`;
} else {
response.infoLog += "☒Couldn't find the IMDb id of this file. Skipping. \n";
}
return result;
} catch (error) {
console.error('Error fetching data from TMDB API:', error.message);
response.infoLog += '☒Error fetching data from TMDB API.\n';
return null;
}
}
return null;
};
const isCommentaryTrack = (title) => {
// Check if the title includes keywords indicating a commentary track
return title.toLowerCase().includes('commentary')
|| title.toLowerCase().includes('description')
|| title.toLowerCase().includes('sdh');
};
const plugin = async (file, librarySettings, inputs, otherArguments) => {
const lib = require('../methods/lib')();
const axios = require('axios').default;
inputs = lib.loadDefaultValues(inputs, details);
response.container = `.${file.container}`;
let prio = ['radarr', 'sonarr'];
let radarrResult = null;
let sonarrResult = null;
let tmdbResult = null;
if (inputs.priority && inputs.priority === 'sonarr') {
prio = ['sonarr', 'radarr'];
}
const fileNameEncoded = encodeURIComponent(file.meta.FileName);
for (const arr of prio) {
let imdbId;
// Reset infoLog before each processing step (removes duplicated logs being displayed)
response.infoLog = '';
switch (arr) {
case 'radarr':
if (tmdbResult) break;
if (inputs.radarr_api_key) {
radarrResult = parseArrResponse(
await axios
.get(
`http://${inputs.radarr_url}/api/v3/parse?apikey=${inputs.radarr_api_key}&title=${fileNameEncoded}`,
)
.then((resp) => resp.data),
fileNameEncoded,
'radarr',
);
if (radarrResult) {
imdbId = radarrResult.imdbId;
response.infoLog += `Grabbed ID (${imdbId}) from Radarr \n`;
const radarrLanguageTag = radarrResult.originalLanguage.name;
response.infoLog += `Language tag picked up by Radarr: ${radarrLanguageTag}\n`;
tmdbResult = await tmdbApi(imdbId, inputs.api_key, axios);
if (tmdbResult) {
const tmdbLanguageTag = languageConverter(tmdbResult.original_language) || tmdbResult.original_language;
response.infoLog += `Language tag picked up by TMDB: ${tmdbLanguageTag}\n`;
}
} else {
response.infoLog += "Couldn't grab ID from Radarr \n";
imdbId = fileNameEncoded;
tmdbResult = await tmdbApi(imdbId, inputs.api_key, axios);
if (tmdbResult) {
const tmdbLanguageTag = languageConverter(tmdbResult.original_language) || tmdbResult.original_language;
response.infoLog += `Language tag picked up by TMDB: ${tmdbLanguageTag}\n`;
}
}
}
break;
case 'sonarr':
if (tmdbResult) break;
if (inputs.sonarr_api_key) {
sonarrResult = parseArrResponse(
await axios.get(
`http://${inputs.sonarr_url}/api/v3/parse?apikey=${inputs.sonarr_api_key}&title=${fileNameEncoded}`,
)
.then((resp) => resp.data),
file.meta.Directory,
'sonarr',
);
if (sonarrResult) {
imdbId = sonarrResult.imdbId;
response.infoLog += `Grabbed ID (${imdbId}) from Sonarr \n`;
tmdbResult = await tmdbApi(imdbId, inputs.api_key, axios);
if (tmdbResult) {
const sonarrTracks = processStreams(tmdbResult, file, inputs.user_langs ? inputs.user_langs.split(',') : '', true, inputs.commentary);
if (sonarrTracks.remove.length > 0) {
if (sonarrTracks.keep.length > 0) {
response.infoLog += `☑Removing tracks with languages: ${sonarrTracks.remLangs.slice(
0,
-2,
)}. \n`;
response.processFile = true;
response.infoLog += '\n';
} else {
response.infoLog
+= '☒Cancelling plugin otherwise all audio tracks would be removed. \n';
}
} else {
response.infoLog += '☒No audio tracks to be removed. \n';
}
} else {
response.infoLog += "☒Couldn't find the IMDb id of this file. Skipping. \n";
}
}
}
break;
}
}
if (tmdbResult) {
const userLanguages = inputs.user_langs ? inputs.user_langs.split(',') : [];
const originalLanguage = tmdbResult.original_language;
const originalLanguageIncluded = userLanguages.includes(originalLanguage);
const tracks = processStreams(
tmdbResult,
file,
userLanguages,
false,
inputs.commentary,
);
console.log('Tracks:', tracks);
console.log('Original Language:', originalLanguage);
console.log('User Languages:', userLanguages);
console.log('Original Language Included:', originalLanguageIncluded);
console.log('User Languages Include Removed Languages:', userLanguages.includes(tracks.remLangs));
// Check if no tracks match original or user-specified languages
const noMatchingTracks = tracks.keep.length === 0 && !originalLanguageIncluded && !userLanguages.includes(tracks.remLangs);
console.log('No Matching Tracks:', noMatchingTracks);
if (noMatchingTracks) {
response.infoLog += '☒Cancelling plugin because no audio tracks match the original language or user-specified languages. \n';
return response; // Stop execution
}
// Continue processing audio tracks
if (tracks.remove.length > 0) {
if (tracks.keep.length > 0) {
response.infoLog += `☑Removing tracks with languages: ${tracks.remLangs.slice(
0,
-2,
)}. \n`;
response.processFile = true;
response.infoLog += '\n';
} else {
response.infoLog += '☒Cancelling plugin otherwise all audio tracks would be removed. \n';
}
} else {
response.infoLog += '☒No audio tracks to be removed. \n';
}
} else {
response.infoLog += "☒Couldn't find the IMDb id of this file. Skipping. \n";
}
return response;
};
module.exports.details = details;
module.exports.plugin = plugin;
@gssariev
Copy link
Author

Added condition to prevent the deletion of single audio tracks in the case of a mismatch of original language tag (TMDB)

@BaileySri
Copy link

BaileySri commented Apr 29, 2024

Please let me know if I should mention this elsewhere.

I ran into some errors using this plugin which I think comes from line 387 and line 418 both running.
Specifically I end up with this command:

Args: -i "/mnt/data/tv.mkv" -map 0 -map -0:a:2 -c copy -max_muxing_queue_size 9999-map -0:a:2 -c copy -max_muxing_queue_size 9999 "/temp/tv-TdarrCacheFile-xMNz98Abz.mkv"

And this error:

Unrecognized option '0:a:2'.
Error splitting the argument list: Option not found

@gssariev
Copy link
Author

Please let me know if I should mention this elsewhere.

I ran into some errors using this plugin which I think comes from line 387 and line 418 both running. Specifically I end up with this command:

Args: -i "/mnt/data/tv.mkv" -map 0 -map -0:a:2 -c copy -max_muxing_queue_size 9999-map -0:a:2 -c copy -max_muxing_queue_size 9999 "/temp/tv-TdarrCacheFile-xMNz98Abz.mkv"

And this error:

Unrecognized option '0:a:2'. Error splitting the argument list: Option not found

Hey, thanks for sharing feedback! Would you mind sharing how you have the plugin set up as it could help narrow down the issue as I tried replicating the error, which I initially assumed was due to the naming of the file as this plugin works in combination with Radarr/Sonarr, but was unable to do so.

Do you still get the error if you add response.preset = '';

before const languages = require('@cospired/i18n-iso-languages'); in processStreams (line 156) ?

@BaileySri
Copy link

BaileySri commented Apr 29, 2024

Still new to Tdarr so I'm not sure how to force rerun a file from the queue.

The change I did make was copying the condition block from Radarr (Line 352) to replace Sonarr's (Line 386).

I re-queued the media but if you happen to know how to start processing a file immediately from the Transcode Queue or manually run a plugin on a file I could test my change and then your suggestion.

EDIT:
Forgot to add plugin options
user_langs: tha,eng
priority: sonarr
All of the URLs and API Keys are filled in.
commentary: false

@gssariev
Copy link
Author

Still new to Tdarr so I'm not sure how to force rerun a file from the queue.

The change I did make was copying the condition block from Radarr (Line 352) to replace Sonarr's (Line 386).

I re-queued the media but if you happen to know how to start processing a file immediately from the Transcode Queue or manually run a plugin on a file I could test my change and then your suggestion.

To get Tdarr to automatically detect and place files in the transcode queue, you need to enable Folder Watch in the library you've created in Tdarr that's pointing to your media. If you have configured a schedule for when scans and transcodes to be run, you can go to the home page of your Tdarr instance and check the ignore schedules checkbox in the Status window at the bottom.

I am not sure if there is a correct way to test plugins in Tdarr, but what I do is:

  1. Create a test Tdarr library
  2. Point it to a folder with media that I would like to test
  3. Keep only the plugin(s) I want to test in the stack of that same library

If you want to re-queue a already transcoded file you can do so by going to Transcode: Success/Not required tab in Status and then either clicking on Requeue, which will put all files back in the queue or click on the requeue arrow symbol for the specific file. Same applies for the Transcode: Error/Cancelled tab.

Cancelled transcoded would also appear in the Staging Section window where you can click on the gear icon next to the file and either add it back to the queue or retry.

Also, do remember to click on Sync plugins once you've made any changes to a Local plugin in order for the changes to be transferred over to your stack/flow.

I assume you've found this plugin through the guide I made in the Tdarr subreddit? If not, here is a link to it that goes into a bit more detail - https://www.reddit.com/r/Tdarr/comments/1cba135/guideplex_optimize_media_for_direct_play_by/

@BaileySri
Copy link

BaileySri commented Apr 29, 2024

Thought I'd follow up. The change I made, replacing the condition in the Sonarr case with the Radarr one, ended up running with no error and produced the following command:

Args: -i "/mnt/data/tv.mkv" -map 0 -map -0:a:2 -c copy -max_muxing_queue_size 9999 "/temp/tv-TdarrCacheFile-mplgky3ut.mkv"

If I were to guess your suggested change of setting the preset to an empty string, '', would work as well but you would still end up running processStreams() twice which may be inefficient unless the processing in the Sonarr case is necessary.

EDIT:
I did find the plugin from the guide you made in the Tdarr subreddit. Thanks again for that post.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment