HaveAGitGat / Tdarr_Plugins

Tdarr Plugins
GNU General Public License v3.0
141 stars 164 forks source link

[request] Reorder (or Delete) Default Subtitle Track Based off Description (Sign & Song) #416

Open pchang388 opened 1 year ago

pchang388 commented 1 year ago

Is your feature request related to a problem? Please describe. I often have anime that comes with Sign & Songs subtitles as default. From what I can tell, this subtitle track only contains subtitles for the songs in the episode and do not provide dialogue subtitles. This is pretty useless to me personally as I don't really care about what the intro/outro songs are saying.

What makes it undesirable, is that based off user/system settings in Plex, it may choose the sign|song as the default subtitle stream even if only Japanese/Foreign audio exists - causing the end user to have to change the subtitle track for each episode manually/script if they have subtitles always on/enabled.

Example:

Screenshot 2023-05-23 at 11 28 19 AM

Describe the solution you'd like I would like a way to do the following:

Describe alternatives you've considered So far from what I can tell, there is no community plugin to do this feature after my few hours of searching on google/online. Closest I found was this which is similar but opposite of what I want: https://www.reddit.com/r/Tdarr/comments/109yg5s/set_subtitles_as_forced_if_named_signs_songs/

Additional context Using Latest Docker Image of Tdarr, version: 2.00.20

pchang388 commented 1 year ago

For now, I ended up adding a few lines to the existing Migz4CleanSubs plugin.

Below is nothing fancy, just quick for my use case. It will remove the subtitle stream if it has sign|sing (sign or sing, some name them sing|song instead of sign|song) and song in the title/description of the subtitle stream. Does not do any safety checks to see if it is the only subtitle stream remaining, so some caution.

This will work if you have your subtitle stream(s) properly labeled with sign and song like from my screenshot in the original post.

/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */
const details = () => ({
    id: 'Tdarr_Plugin_MC93_Migz4CleanSubs_SIGN_SONG',
    Stage: 'Pre-processing',
    Name: 'Custom SignSong version of Migz-Clean subtitle streams',
    Type: 'Subtitle',
    Operation: 'Transcode',
    Description: 'This plugin keeps only specified language tracks & can tag tracks with an unknown language. \n\n',
    Version: '2.4',
    Tags: 'pre-processing,ffmpeg,subtitle only,configurable',
    Inputs: [{
      name: 'language',
      type: 'string',
      defaultValue: 'eng',
      inputUI: {
        type: 'text',
      },
      tooltip: `Specify language tag/s here for the subtitle tracks you'd like to keep.
                      \\nMust follow ISO-639-2 3 letter format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
                 \\nExample:\\n
                 eng

                 \\nExample:\\n
                 eng,jpn`,
    },
    {
      name: 'commentary',
      type: 'boolean',
      defaultValue: false,
      inputUI: {
        type: 'dropdown',
        options: [
          'false',
          'true',
        ],
      },
      tooltip: `Specify if subtitle tracks that contain commentary/description should be removed.
                 \\nExample:\\n
                 true

                 \\nExample:\\n
                 false`,
    },
    {
      name: 'signsong',
      type: 'boolean',
      defaultValue: false,
      inputUI: {
        type: 'dropdown',
        options: [
          'false',
          'true',
        ],
      },
      tooltip: `Specify if subtitle tracks containing sign/song subtitles should be removed.
                 \\nExample:\\n
                 true

                 \\nExample:\\n
                 false`,
    },
    {
      name: 'tag_language',
      type: 'string',
      defaultValue: '',
      inputUI: {
        type: 'text',
      },
      tooltip: `Specify a single language for subtitle tracks with no language or unknown language to be tagged with.
                      \\nMust follow ISO-639-2 3 letter format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
                      \\nLeave empty to disable.
                 \\nExample:\\n
                 eng

                 \\nExample:\\n
                 por`,
    },
    ],
  });

  // eslint-disable-next-line no-unused-vars
  const plugin = (file, librarySettings, inputs, otherArguments) => {
    const lib = require('../methods/lib')();
    // eslint-disable-next-line no-unused-vars,no-param-reassign
    inputs = lib.loadDefaultValues(inputs, details);
    const response = {
      processFile: false,
      preset: '',
      container: `.${file.container}`,
      handBrakeMode: false,
      FFmpegMode: true,
      reQueueAfter: false,
      infoLog: '',
    };

    // Check if file is a video. If it isn't then exit plugin.
    if (file.fileMedium !== 'video') {
      // eslint-disable-next-line no-console
      console.log('File is not video');
      response.infoLog += '☒File is not video \n';
      response.processFile = false;
      return response;
    }

    // Check if inputs.language has been configured. If it hasn't then exit plugin.
    if (inputs.language === '') {
      response.infoLog
        += '☒Language/s to keep have not been configured, '
        + 'please configure required options. Skipping this plugin.  \n';
      response.processFile = false;
      return response;
    }

    // Set up required variables.
    const language = inputs.language.split(',');
    let ffmpegCommandInsert = '';
    let subtitleIdx = 0;
    let convert = false;

    // look for both to be sure
    const signRegex = /(sign|sing)/gi;
    const songRegex = /song/gi

    // Go through each stream in the file.
    for (let i = 0; i < file.ffProbeData.streams.length; i++) {
      // Catch error here incase the language metadata is completely missing.
      try {
        // Check if stream is subtitle
        // AND checks if the tracks language code does not match any of the languages entered in inputs.language.
        if (
          file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle'
          && language.indexOf(
            file.ffProbeData.streams[i].tags.language.toLowerCase(),
          ) === -1
        ) {
          ffmpegCommandInsert += `-map -0:s:${subtitleIdx} `;
          response.infoLog += `☒Subtitle stream 0:s:${subtitleIdx} has unwanted language tag ${file.ffProbeData.streams[
            i
          ].tags.language.toLowerCase()}, removing. \n`;
          convert = true;
        }
      } catch (err) {
        // Error
      }

      // Catch error here incase the title metadata is completely missing.
      try {
        // Check if inputs.commentary is set to true
        // AND if stream is subtitle
        // AND then checks for stream titles with the following "commentary, description, sdh".
        // Removing any streams that are applicable.
        if (
          inputs.commentary === true
          && file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle'
          && (file.ffProbeData.streams[i].tags.title
            .toLowerCase()
            .includes('commentary')
            || file.ffProbeData.streams[i].tags.title
              .toLowerCase()
              .includes('description')
            || file.ffProbeData.streams[i].tags.title.toLowerCase().includes('sdh'))
        ) {
          ffmpegCommandInsert += `-map -0:s:${subtitleIdx} `;
          response.infoLog += `☒Subtitle stream 0:s:${subtitleIdx} detected as being descriptive, removing. \n`;
          convert = true;
        }
      } catch (err) {
        // Error
      }

      // Catch error here incase the title metadata is completely missing.
      try {
        // Check if inputs.singsong is set to true
        // AND if stream is subtitle
        // AND then checks for sing|song
        // Removing any streams that are applicable.
        if (
          inputs.signsong === true
          && file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle'
          && signRegex.test(file.ffProbeData.streams[i].tags.title.toLowerCase())
          && songRegex.test(file.ffProbeData.streams[i].tags.title.toLowerCase())
        ) {
          ffmpegCommandInsert += `-map -0:s:${subtitleIdx} `;
          response.infoLog += `☒Subtitle stream 0:s:${subtitleIdx} detected as being sign|song subtitle, removing. \n`;
          convert = true;
        }
      } catch (err) {
        // Error
      }

      // Check if inputs.tag_language has something entered.
      // (Entered means user actually wants something to happen, empty would disable this)
      // AND checks that stream is subtitle.
      if (
        inputs.tag_language !== ''
        && file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle'
      ) {
        // Catch error here incase the metadata is completely missing.
        try {
          // Look for subtitle with "und" as metadata language.
          if (
            file.ffProbeData.streams[i].tags.language
              .toLowerCase()
              .includes('und')
          ) {
            ffmpegCommandInsert += `-metadata:s:s:${subtitleIdx} language=${inputs.tag_language} `;
            response.infoLog
              += `☒Subtitle stream 0:s:${subtitleIdx} has no language, tagging as ${inputs.tag_language}. \n`;
            convert = true;
          }
        } catch (err) {
          // Error
        }

        // Checks if the tags metadata is completely missing.
        //  If so this would cause playback to show language as "undefined".
        // No catch error here otherwise it would never detect the metadata as missing.
        if (typeof file.ffProbeData.streams[i].tags === 'undefined') {
          ffmpegCommandInsert += `-metadata:s:s:${subtitleIdx} language=${inputs.tag_language} `;
          response.infoLog
            += `☒Subtitle stream 0:s:${subtitleIdx} has no language, tagging as ${inputs.tag_language}. \n`;
          convert = true;
        } else if (typeof file.ffProbeData.streams[i].tags.language === 'undefined') {
        // Checks if the tags.language metadata is completely missing.
        // If so this would cause playback to show language as "undefined".
        // No catch error here otherwise it would never detect the metadata as missing
          ffmpegCommandInsert += `-metadata:s:s:${subtitleIdx} language=${inputs.tag_language} `;
          response.infoLog
            += `☒Subtitle stream 0:s:${subtitleIdx} has no language, tagging as ${inputs.tag_language}. \n`;
          convert = true;
        }
      }

      // Check if stream type is subtitle and increment subtitleIdx if true.
      if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle') {
        subtitleIdx += 1;
      }
    }

    // Convert file if convert variable is set to true.
    if (convert === true) {
      response.processFile = true;
      response.preset = `, -map 0 ${ffmpegCommandInsert} -c copy -max_muxing_queue_size 9999`;
      response.container = `.${file.container}`;
      response.reQueueAfter = true;
    } else {
      response.processFile = false;
      response.infoLog += "☑File doesn't contain subtitle tracks which are unwanted or that require tagging.\n";
    }
    return response;
  };
  module.exports.details = details;
  module.exports.plugin = plugin;
tws101 commented 1 year ago

Okay assuming plex is reading the disposition of the sub stream

They look like this

"disposition":{ "default":1 "dub":0 "original":0 "comment":0 "lyrics":0 "karaoke":0 "forced":0 "hearing_impaired":0 "visual_impaired":0 "clean_effects":0 "attached_pic":0 "timed_thumbnails":0 "captions":0 "descriptions":0 "metadata":0 "dependent":0 "still_image":0

I assume the streams offending you are marked default or forced or both and your plex setting is choosing them and playing them....

Lets assume you look at the probe data and see (EXAMPLE) Sub Stream 0 eng Sub Stream 1 eng forced/default signs and songs in the title

does this work in ffmepg? THREE VARIBALES

  1. input.mkv
  2. output.mkv
  3. iD this is in "-disposition:s:iD" <- Last part meaning in the example it would be this -> "-i input.mkv -map 0 -c copy -disposition:s:1 0 output.mkv" COMMAND in FFMPEG "-i input.mkv -map 0 -c copy -disposition:s:iD 0 output.mkv"

If this fixes your issue with ONE file let me know and I will produce a plugin that will do it for you

The substream dispositions should all be unset in the output file after that command is run AND you need to make sure Plex also does not auto play it... It could be that plex auto plays those in if the title they says signs which would be another issue

The theoretical plugin I would make would loop through the Subtitle Streams and look for this if (stream.disposition.lyrics || (title.includes('signs')) || (title.includes('songs')))

If a stream had the above we would run that ffmpeg command on it with its iD going in that field.

If I forget about this thread you could edit migz plugin with if (stream.disposition.lyrics || (title.includes('signs')) || (title.includes('songs'))) { ffmpegCommandInsert += -disposition:s:${subtitleIdx} 0; }

The above if syntax would need to fit is loop style so its not exact

tws101 commented 1 year ago

Tdarr_Plugin_remove_disposition_sign_songs.zip

Was working on something similar so just changed it quick for you..... All this should do is remove the disposition forced and default from a sub track that is signs and songs.... This will leave the sign and songs track with NO DISPOSITION AT ALL, but the title tag remains in place

Simple 79 lines total, lines 50 through 73 handle everything

****This is a Prototype and is largely untested, use at your own risk

tws101 commented 1 year ago

I missed that present needs a comma preset: '',
to preset: ' , ', <- like this

gabehf commented 1 day ago

I modified @abelfodil's Tdarr_Plugin_bldl_Clean_Subtitle_Streams to accomplish this. The only thing that I changed is a fix to the code so that it operates on the correct subtitle track, and I made it so that the plugin only removes the disposition of the subtitle track instead of removing it entirely (sometimes I like to watch my anime dubbed).

Here is the file:

/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */
const details = () => ({
    id: 'Tdarr_Plugin_bldl_Clean_Subtitle_Streams',
    Stage: 'Pre-processing',
    Name: 'bldl Clean Subtitle Streams',
    Type: 'Subtitle',
    Operation: 'Transcode',
    Description: 'This plugin remove disposition of specified subtitle tracks. \n\n',
    Version: '1.0',
    Tags: 'pre-processing,ffmpeg,subtitle only,configurable',
    Inputs: [{
      name: 'titles_to_clean',
      type: 'string',
      defaultValue: 'Signs / Songs',
      inputUI: {
        type: 'text',
      },
      tooltip: 'Specify titles to clean, separated by a comma. Defaults to "Signs / Songs".',
    }],
  });

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const plugin = (file, librarySettings, inputs, otherArguments) => {
    const lib = require('../methods/lib')();
    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign
    inputs = lib.loadDefaultValues(inputs, details);
    const response = {
      processFile: false,
      preset: '',
      container: `.${file.container}`,
      handBrakeMode: false,
      FFmpegMode: true,
      reQueueAfter: false,
      infoLog: '',
    };

    if (inputs.titles_to_clean === undefined) {
      response.processFile = false;
      response.infoLog += '☒ Inputs not entered! \n';
      return response;
    }

    // Check if file is a video. If it isn't then exit plugin.
    if (file.fileMedium !== 'video') {
      // eslint-disable-next-line no-console
      console.log('File is not video');
      response.infoLog += '☒File is not video \n';
      response.processFile = false;
      return response;
    }

    // Set up required variables.
    let ffmpegCommandInsert = '';
    let subtitleIdx = 0;
    let convert = false;

    const titles_to_clean = inputs.titles_to_clean.split(',').map((s) => s.toLowerCase());

    // Go through each stream in the file.
    for (let i = 0; i < file.ffProbeData.streams.length; i++) {
      // Catch error here incase the title metadata is completely missing.
      try {
        // Check if stream is subtitle
        // AND then checks for stream titles with the specified titles.
        // Removing any streams that are applicable.
        if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'subtitle') {
          if (titles_to_clean.some((s) => file.ffProbeData.streams[i].tags.title.toLowerCase().includes(s))) {
          ffmpegCommandInsert += `-disposition:s:${subtitleIdx} 0 `;
          response.infoLog += `☒Subtitle stream 0:s:${subtitleIdx} detected as being ${titles_to_clean}; removing disposition. \n`;
          convert = true;
          }
          subtitleIdx++; // increment current subtitle id
        }
      } catch (err) {
        // Error
      }
    }

    // Convert file if convert variable is set to true.
    if (convert === true) {
      response.processFile = true;
      response.preset = `, -map 0 ${ffmpegCommandInsert} -c copy -max_muxing_queue_size 9999`;
      response.container = `.${file.container}`;
      response.reQueueAfter = true;
    } else {
      response.processFile = false;
      response.infoLog += "☑File doesn't contain subtitle tracks which are unwanted or that require tagging.\n";
    }
    return response;
  };
  module.exports.details = details;
  module.exports.plugin = plugin;

Do note that I did only very minimal testing so make sure to test it yourself to make sure it works.

abelfodil commented 1 day ago

@gabehf Thanks for the fix! I updated the PR to fix the bug :)

I also recently created https://github.com/HaveAGitGat/Tdarr_Plugins/pull/717, and I use it in conjunction with stream reordering plugins.

It might be interesting to create a subtitles reordering plugin based on titles.