From 766cbee310fecf9db24ba7e2783cfc640dc1980f Mon Sep 17 00:00:00 2001 From: Lachie Date: Wed, 27 Nov 2024 17:29:41 +1000 Subject: [PATCH] [reorder_audio_streams_by_language] v1.1.11 add radarr/sonarr support --- .../changelog.md | 2 + .../info.json | 4 +- .../plugin.py | 250 ++++++++++++++---- .../requirements.txt | 2 + 4 files changed, 205 insertions(+), 53 deletions(-) diff --git a/source/reorder_audio_streams_by_language/changelog.md b/source/reorder_audio_streams_by_language/changelog.md index 6e032e2dd..c165258e6 100644 --- a/source/reorder_audio_streams_by_language/changelog.md +++ b/source/reorder_audio_streams_by_language/changelog.md @@ -1,3 +1,5 @@ +**1.2.0** +- Add support for grabbing original language from Radarr or Sonarr **1.1.10** - 2nd fix stream order test comparing new order with original order diff --git a/source/reorder_audio_streams_by_language/info.json b/source/reorder_audio_streams_by_language/info.json index 217cecf76..347752002 100644 --- a/source/reorder_audio_streams_by_language/info.json +++ b/source/reorder_audio_streams_by_language/info.json @@ -4,7 +4,7 @@ 1, 2 ], - "description": "Specify a language tag for Unmanic to try and put as 1st audio track.", + "description": "Manually specify language tag, or use \"original language\" from Radarr/Sonarr, for Unmanic to set as the default audio track.", "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.reorder_audio_streams_by_language/master/icon.png", "id": "reorder_audio_streams_by_language", "name": "Re-order audio streams by language", @@ -16,5 +16,5 @@ "on_worker_process": 2 }, "tags": "audio,ffmpeg,library file test", - "version": "1.1.10" + "version": "1.2.0" } diff --git a/source/reorder_audio_streams_by_language/plugin.py b/source/reorder_audio_streams_by_language/plugin.py index 4eab97d34..fb8a6a781 100644 --- a/source/reorder_audio_streams_by_language/plugin.py +++ b/source/reorder_audio_streams_by_language/plugin.py @@ -4,7 +4,7 @@ """ Written by: Josh.5 Date: 23 March 2021, (8:06 PM) - + Copyright: Copyright (C) 2021 Josh Sunnex @@ -19,12 +19,16 @@ If not, see . """ + import logging import os +import pycountry +from pyarr import RadarrAPI, SonarrAPI + from unmanic.libs.unplugins.settings import PluginSettings -from reorder_audio_streams_by_language.lib.ffmpeg import StreamMapper, Probe, Parser +from reorder_audio_streams_by_language.lib.ffmpeg import Parser, Probe, StreamMapper # Configure plugin logger logger = logging.getLogger("Unmanic.Plugin.reorder_audio_streams_by_language") @@ -33,25 +37,89 @@ class Settings(PluginSettings): settings = { "Search String": "eng", + "use_radarr": False, + "radarr_url": "http://localhost:7878", + "radarr_api_key": "", + "use_sonarr": False, + "sonarr_url": "http://localhost:8989", + "sonarr_api_key": "", } def __init__(self, *args, **kwargs): super(Settings, self).__init__(*args, **kwargs) self.form_settings = { "Search String": { - "label": "Enter language code to move to 1st audio stream (likely a 3 letter code)" - } + "label": "Enter language code to move to 1st audio stream (likely a 3 letter code). Will be ignored if radarr or sonarr are enabled." + }, + "use_radarr": {"label": "Attempt to use original language from Radarr"}, + "radarr_url": self._radarr_url(), + "radarr_api_key": self._radarr_api_key(), + "use_sonarr": {"label": "Attempt to use original language from Sonarr"}, + "sonarr_url": self._sonarr_url(), + "sonarr_api_key": self._sonarr_api_key(), } + def _radarr_url(self): + values = { + "label": "Radarr URL", + "sub_setting": True, + } + + if not self.get_setting("use_radarr"): + values["display"] = "hidden" + + return values + + def _radarr_api_key(self): + values = { + "label": "Radarr API Key", + "tooltip": "Ensure the address starts with 'http'", + "sub_setting": True, + } + + if not self.get_setting("use_radarr"): + values["display"] = "hidden" + + return values + + def _sonarr_url(self): + values = { + "label": "Sonarr URL", + "sub_setting": True, + } + + if not self.get_setting("use_sonarr"): + values["display"] = "hidden" + + return values + + def _sonarr_api_key(self): + values = { + "label": "Sonarr API Key", + "tooltip": "Ensure the address starts with 'http'", + "sub_setting": True, + } + + if not self.get_setting("use_sonarr"): + values["display"] = "hidden" + + return values + class PluginStreamMapper(StreamMapper): - def __init__(self): + def __init__(self, abspath): # Check all streams (only the desired stream type will matter when tested) - super(PluginStreamMapper, self).__init__(logger, ['video', 'audio', 'subtitle', 'data', 'attachment']) + super(PluginStreamMapper, self).__init__( + logger, ["video", "audio", "subtitle", "data", "attachment"] + ) self.settings = None + self.search_string = None + + self.abspath = abspath + # The stream type we are considering as streams of interest - self.stream_type = 'audio' + self.stream_type = "audio" # Flag to say if a search string has matched a stream of interest self.found_search_string_streams = False @@ -69,14 +137,68 @@ def __init__(self): def set_settings(self, settings): self.settings = settings + def set_langcode(self): + original_langcode = '' + + if self.settings.get_setting("use_radarr"): + original_langcode = self.get_language_from_radarr() + + if self.settings.get_setting("use_sonarr") and not original_langcode: + original_langcode = self.get_language_from_sonarr() + + self.search_string = original_langcode or self.settings.get_setting( + "Search String" + ) + + def get_language_from_radarr(self): + radarr_url = self.settings.get_setting("radarr_url") + radarr_api_key = self.settings.get_setting("radarr_api_key") + + if not radarr_url or not radarr_api_key: + return None + + api = RadarrAPI(radarr_url, radarr_api_key) + + movie_data = api.lookup_movie(self.abspath) + + original_language = movie_data[0].get("originalLanguage") + + if not original_language: + return None + + lang = pycountry.languages.get(name=original_language["name"]) + + return lang.alpha_3 if lang else None + + def get_language_from_sonarr(self): + sonarr_url = self.settings.get_setting("sonarr_url") + sonarr_api_key = self.settings.get_setting("sonarr_api_key") + + if not sonarr_url or not sonarr_api_key: + return None + + api = SonarrAPI(sonarr_url, sonarr_api_key) + + series_data = api.lookup_series(self.abspath) + + original_language = series_data[0].get("originalLanguage") + + if not original_language: + return None + + lang = pycountry.languages.get(name=original_language["name"]) + + return lang.alpha_3 if lang else None + def test_tags_for_search_string(self, stream_tags): - if stream_tags and True in list(k.lower() in ['title', 'language'] for k in stream_tags): - search_string = self.settings.get_setting('Search String') + if stream_tags and True in list( + k.lower() in ["title", "language"] for k in stream_tags + ): # Check if tag matches the "Search String" - if search_string.lower() in stream_tags.get('language', '').lower(): + if self.search_string.lower() in stream_tags.get("language", "").lower(): # Found a matching stream in language tag return True - elif search_string in stream_tags.get('title', '').lower(): + elif self.search_string in stream_tags.get("title", "").lower(): # Found a matching stream in title tag return True return False @@ -88,36 +210,50 @@ def test_stream_needs_processing(self, stream_info: dict): def custom_stream_mapping(self, stream_info: dict, stream_id: int): ident = { - 'video': 'v', - 'audio': 'a', - 'subtitle': 's', - 'data': 'd', - 'attachment': 't' + "video": "v", + "audio": "a", + "subtitle": "s", + "data": "d", + "attachment": "t", } - codec_type = stream_info.get('codec_type').lower() + codec_type = stream_info.get("codec_type").lower() if codec_type == self.stream_type: # Process streams of interest - if self.test_tags_for_search_string(stream_info.get('tags')): + if self.test_tags_for_search_string(stream_info.get("tags")): self.found_search_string_streams = True if len(self.search_string_stream_mapping) == 0: - self.search_string_stream_mapping += ['-map', '0:{}:{}'.format(ident.get(codec_type), stream_id), '-disposition:{}:{}'.format(ident.get(codec_type), 0), 'default'] + self.search_string_stream_mapping += [ + "-map", + "0:{}:{}".format(ident.get(codec_type), stream_id), + "-disposition:{}:{}".format(ident.get(codec_type), 0), + "default", + ] else: - self.search_string_stream_mapping += ['-map', '0:{}:{}'.format(ident.get(codec_type), stream_id)] + self.search_string_stream_mapping += [ + "-map", + "0:{}:{}".format(ident.get(codec_type), stream_id), + ] else: - self.unmatched_stream_mapping += ['-map', '0:{}:{}'.format(ident.get(codec_type), stream_id)] + self.unmatched_stream_mapping += [ + "-map", + "0:{}:{}".format(ident.get(codec_type), stream_id), + ] else: # Process streams not of interest if not self.found_search_string_streams: - self.first_stream_mapping += ['-map', '0:{}:{}'.format(ident.get(codec_type), stream_id)] + self.first_stream_mapping += [ + "-map", + "0:{}:{}".format(ident.get(codec_type), stream_id), + ] else: - self.last_stream_mapping += ['-map', '0:{}:{}'.format(ident.get(codec_type), stream_id)] + self.last_stream_mapping += [ + "-map", + "0:{}:{}".format(ident.get(codec_type), stream_id), + ] # Do not map any streams using the default method - return { - 'stream_mapping': [], - 'stream_encoding': [] - } + return {"stream_mapping": [], "stream_encoding": []} def streams_to_be_reordered(self): result = False @@ -130,12 +266,16 @@ def streams_to_be_reordered(self): logger.info("Streams were found matching the search string") # Test if the mapping is already in the correct order counter = 0 - for item in self.search_string_stream_mapping + self.unmatched_stream_mapping: - if '-map' in item or '-disposition' in item or 'default' in item: + for item in ( + self.search_string_stream_mapping + self.unmatched_stream_mapping + ): + if "-map" in item or "-disposition" in item or "default" in item: continue - original_position = item.split(':')[-1] + original_position = item.split(":")[-1] if int(original_position) != int(counter): - logger.info("The new order for the mapped streams will differ from the source file") + logger.info( + "The new order for the mapped streams will differ from the source file" + ) result = True break counter += 1 @@ -143,7 +283,7 @@ def streams_to_be_reordered(self): return result def order_stream_mapping(self): - args = ['-c', 'copy', '-disposition:a', '-default'] + args = ["-c", "copy", "-disposition:a", "-default"] args += self.first_stream_mapping args += self.search_string_stream_mapping args += self.unmatched_stream_mapping @@ -165,31 +305,38 @@ def on_library_management_file_test(data): """ # Get the path to the file - abspath = data.get('path') + abspath = data.get("path") # Get file probe - probe = Probe(logger, allowed_mimetypes=['video']) + probe = Probe(logger, allowed_mimetypes=["video"]) if not probe.file(abspath): # File probe failed, skip the rest of this test return data # Configure settings object (maintain compatibility with v1 plugins) - if data.get('library_id'): - settings = Settings(library_id=data.get('library_id')) + if data.get("library_id"): + settings = Settings(library_id=data.get("library_id")) else: settings = Settings() # Get stream mapper - mapper = PluginStreamMapper() + mapper = PluginStreamMapper(abspath) mapper.set_settings(settings) + mapper.set_langcode() mapper.set_probe(probe) if mapper.streams_to_be_reordered(): # Mark this file to be added to the pending tasks - data['add_file_to_pending_tasks'] = True - logger.debug("File '{}' should be added to task list. Probe found streams require processing.".format(abspath)) + data["add_file_to_pending_tasks"] = True + logger.debug( + "File '{}' should be added to task list. Probe found streams require processing.".format( + abspath + ) + ) else: - logger.debug("File '{}' does not contain streams require processing.".format(abspath)) + logger.debug( + "File '{}' does not contain streams require processing.".format(abspath) + ) return data @@ -211,27 +358,28 @@ def on_worker_process(data): """ # Default to no FFMPEG command required. This prevents the FFMPEG command from running if it is not required - data['exec_command'] = [] - data['repeat'] = False + data["exec_command"] = [] + data["repeat"] = False # Get the path to the file - abspath = data.get('file_in') + abspath = data.get("file_in") # Get file probe - probe = Probe(logger, allowed_mimetypes=['video']) + probe = Probe(logger, allowed_mimetypes=["video"]) if not probe.file(abspath): # File probe failed, skip the rest of this test return data # Configure settings object (maintain compatibility with v1 plugins) - if data.get('library_id'): - settings = Settings(library_id=data.get('library_id')) + if data.get("library_id"): + settings = Settings(library_id=data.get("library_id")) else: settings = Settings() # Get stream mapper - mapper = PluginStreamMapper() + mapper = PluginStreamMapper(abspath) mapper.set_settings(settings) + mapper.set_langcode() mapper.set_probe(probe) if mapper.streams_to_be_reordered(): @@ -240,7 +388,7 @@ def on_worker_process(data): # Set the output file # Do not remux the file. Keep the file out in the same container - mapper.set_output_file(data.get('file_out')) + mapper.set_output_file(data.get("file_out")) # Set the custom mapping order with the advanced options mapper.order_stream_mapping() @@ -250,12 +398,12 @@ def on_worker_process(data): logger.debug("ffmpeg_args: '{}'".format(ffmpeg_args)) # Apply ffmpeg args to command - data['exec_command'] = ['ffmpeg'] - data['exec_command'] += ffmpeg_args + data["exec_command"] = ["ffmpeg"] + data["exec_command"] += ffmpeg_args # Set the parser parser = Parser(logger) parser.set_probe(probe) - data['command_progress_parser'] = parser.parse_progress + data["command_progress_parser"] = parser.parse_progress return data diff --git a/source/reorder_audio_streams_by_language/requirements.txt b/source/reorder_audio_streams_by_language/requirements.txt index e69de29bb..2e84e7fe9 100644 --- a/source/reorder_audio_streams_by_language/requirements.txt +++ b/source/reorder_audio_streams_by_language/requirements.txt @@ -0,0 +1,2 @@ +pyarr==5.2.0 +pycountry==24.6.1 \ No newline at end of file