diff --git a/CHANGELOG.md b/CHANGELOG.md index 02dd872..5c969c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +* [v3.17.0](https://github.com/newt-sc/a4kSubtitles/releases/tag/service.subtitles.a4ksubtitles%2Fservice.subtitles.a4ksubtitles-3.17.0): + * Fix: TV year being pulled incorrectly + * Fix: File name issues in both temp and media directories + * Fix: Subtitle file missing extension + * Fix: Incorrect episode selection when downloaded archive contains multiple subtitles + * Fix: Auto-download not working after the first selected episode in the playlist (A4K only works for the first media) + * Fix: Subtitle import issue due to "illegal characters" + * Improve: determination of subtitle episodes + * Improve: results parsing + * Feature: Auto-download now copies subtitles next to the video or to a custom location, based on Kodi's subtitle storage mode path + * SubSource: Now supports series in "absolute order", as used by some anime websites + * SubSource: Fixed issue of duplicated subtitle IDs with different names + * [v3.16.1](https://github.com/newt-sc/a4kSubtitles/releases/tag/service.subtitles.a4ksubtitles%2Fservice.subtitles.a4ksubtitles-3.16.1): * Fix addons.xml.crc diff --git a/a4kSubtitles/download.py b/a4kSubtitles/download.py index 8ed672c..49345ed 100644 --- a/a4kSubtitles/download.py +++ b/a4kSubtitles/download.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +subtitles_exts = ['.srt', '.sub'] +subtitles_exts_secondary = ['.smi', '.ssa', '.aqt', '.jss', '.ass', '.rt', '.txt'] +subtitles_exts_all = subtitles_exts + subtitles_exts_secondary def __download(core, filepath, request): request['stream'] = True @@ -7,6 +10,10 @@ def __download(core, filepath, request): core.shutil.copyfileobj(r.raw, f) def __extract_gzip(core, archivepath, filename): + if not any(filename.lower().endswith(ext) for ext in subtitles_exts_all): + # For now, we will use 'srt' to mark unknown file extensions as subtitles. + filename = filename + ".srt" + filepath = core.os.path.join(core.utils.temp_dir, filename) if core.utils.py2: @@ -25,8 +32,8 @@ def __extract_gzip(core, archivepath, filename): return filepath def __extract_zip(core, archivepath, filename, episodeid): - sub_exts = ['.srt', '.sub'] - sub_exts_secondary = ['.smi', '.ssa', '.aqt', '.jss', '.ass', '.rt', '.txt'] + sub_exts = subtitles_exts + sub_exts_secondary = subtitles_exts_secondary try: using_libvfs = False @@ -39,9 +46,13 @@ def __extract_zip(core, archivepath, filename, episodeid): (dirs, files) = core.kodi.xbmcvfs.listdir('archive://%s' % archivepath_) namelist = [file.decode(core.utils.default_encoding) if core.utils.py2 else file for file in files] - subfile = core.utils.find_file_in_archive(core, namelist, sub_exts, episodeid) - if not subfile: - subfile = core.utils.find_file_in_archive(core, namelist, sub_exts_secondary, episodeid) + subfile = core.utils.find_file_in_archive(core, namelist, sub_exts + sub_exts_secondary, episodeid) + + if subfile: + # Add the subtitle file extension. + subfilename_and_ext = subfile.rsplit(".", 1) + if len(subfilename_and_ext) > 1: + filename = filename + "." + subfilename_and_ext[-1] dest = core.os.path.join(core.utils.temp_dir, filename) if not subfile: @@ -67,9 +78,15 @@ def __extract_zip(core, archivepath, filename, episodeid): return dest def __insert_lang_code_in_filename(core, filename, lang_code): - filename_chunks = core.utils.strip_non_ascii_and_unprintable(filename).split('.') - filename_chunks.insert(-1, lang_code) - return '.'.join(filename_chunks) + name = core.utils.strip_non_ascii_and_unprintable(filename) + nameparts = name.rsplit(".", 1) + + # Because this can be called via "raw" subtitles where sub ext exists we will ensure it ends with the subtitle ext. + # Otherwise we will use "filename.lang_code" later the ext will be added on unzip process. + if len(nameparts) > 1 and ("." + nameparts[1] in subtitles_exts_all): + return ".".join([nameparts[0], lang_code, nameparts[1]]) + + return "{0}.{1}".format(name, lang_code) def __postprocess(core, filepath, lang_code): try: @@ -111,6 +128,24 @@ def __postprocess(core, filepath, lang_code): f.write(text.encode(core.utils.default_encoding)) except: pass +def __copy_sub_local(core, subfile): + # Copy the subfile to local. + if core.os.getenv('A4KSUBTITLES_TESTRUN') == 'true': + return + + media_name = core.os.path.splitext(core.os.path.basename(core.kodi.xbmc.getInfoLabel('Player.Filename')))[0] + sub_name, lang_code, extension = core.os.path.basename(subfile).rsplit(".", 2) + file_dest, folder_dest = None, None + if core.kodi.get_kodi_setting("subtitles.storagemode") == 0: + folder_dest = core.kodi.xbmc.getInfoLabel('Player.Folderpath') + file_dest = core.os.path.join(folder_dest, ".".join([media_name, lang_code, extension])) + elif core.kodi.get_kodi_setting("subtitles.storagemode") == 1: + folder_dest = core.kodi.get_kodi_setting("subtitles.custompath") + file_dest = core.os.path.join(folder_dest, ".".join([media_name, lang_code, extension])) + + if file_dest and core.kodi.xbmcvfs.exists(folder_dest): + core.kodi.xbmcvfs.copy(subfile, file_dest) + def download(core, params): core.logger.debug(lambda: core.json.dumps(params, indent=2)) @@ -120,6 +155,7 @@ def download(core, params): actions_args = params['action_args'] lang_code = core.utils.get_lang_id(actions_args['lang'], core.kodi.xbmc.ISO_639_2) filename = __insert_lang_code_in_filename(core, actions_args['filename'], lang_code) + filename = core.utils.slugify_filename(filename) archivepath = core.os.path.join(core.utils.temp_dir, 'sub.zip') service_name = params['service_name'] @@ -140,6 +176,7 @@ def download(core, params): __postprocess(core, filepath, lang_code) if core.api_mode_enabled: + __copy_sub_local(core, filepath) return filepath listitem = core.kodi.xbmcgui.ListItem(label=filepath, offscreen=True) diff --git a/a4kSubtitles/lib/kodi_mock.py b/a4kSubtitles/lib/kodi_mock.py index b8584f6..dff9986 100644 --- a/a4kSubtitles/lib/kodi_mock.py +++ b/a4kSubtitles/lib/kodi_mock.py @@ -28,6 +28,7 @@ xbmc.ENGLISH_NAME = 'name' __player = lambda: None +__player.isPlayingVideo = lambda: None __player.getPlayingFile = lambda: '' __player.getAvailableSubtitleStreams = lambda: [] __player.setSubtitles = lambda s: None diff --git a/a4kSubtitles/lib/utils.py b/a4kSubtitles/lib/utils.py index 908227f..89fa033 100644 --- a/a4kSubtitles/lib/utils.py +++ b/a4kSubtitles/lib/utils.py @@ -61,6 +61,9 @@ def strip_non_ascii_and_unprintable(text): result = ''.join(char for char in text if char in string.printable) return result.encode('ascii', errors='ignore').decode('ascii', errors='ignore') +def slugify_filename(text): + return re.sub(r'[\\/*?:"<>|]', '_', text) + def get_lang_id(language, lang_format): try: return get_lang_ids([language], lang_format)[0] @@ -173,15 +176,16 @@ def get_json(path, filename): with open_file_wrapper(json_path)() as json_result: return json.load(json_result) -def find_file_in_archive(core, namelist, exts, part_of_filename=''): +def find_file_in_archive(core, namelist, exts, episode_number=''): first_ext_match = None exact_file = None for file in namelist: file_lower = file.lower() if any(file_lower.endswith(ext) for ext in exts): + sub_meta = extract_season_episode(file_lower, True) if not first_ext_match: first_ext_match = file - if (part_of_filename == '' or part_of_filename in file_lower): + if (episode_number == '' or sub_meta.episode == episode_number): exact_file = file break @@ -212,3 +216,46 @@ def extract_zipfile_member(zipfile, filename, dest): except: filename = filename.encode(default_encoding).decode(py3_zip_missing_utf8_flag_fallback_encoding) return zipfile.extract(filename, dest) + +def extract_season_episode(filename, episode_fallback=False, zfill=3): + episode_pattern = r'(?:e|ep.?|episode.?)(\d{1,5})' + season_pattern = r'(?:s|season.?)(\d{1,5})' + combined_pattern = r'\b(?:s|season)(\d{1,5})\s?[x|\-|\_|\s]\s?[a-z]?(\d{1,5})\b' + range_episodes_pattern = r'\b(?:.{1,4}e|ep|eps|episodes|\s)?(\d{1,5}?)(?:v.?)?\s?[\-|\~]\s?(\d{1,5})(?:v.?)?\b' + date_pattern = r'\b(\d{2,4}-\d{1,2}-\d{2,4})\b' + + filename = re.sub(date_pattern, "", filename) + season_match = re.search(season_pattern, filename, re.IGNORECASE) + episode_match = re.search(episode_pattern, filename, re.IGNORECASE) + combined_match = re.search(combined_pattern, filename, re.IGNORECASE) + range_episodes_match = re.findall(range_episodes_pattern, filename, re.IGNORECASE) + + season = season_match.group(1) if season_match else None + episode = episode_match.group(1) if episode_match else None + episodes_range = range(0) + + if combined_match: + season = season if season else combined_match.group(1) + episode = episode if episode else combined_match.group(2) + + if range_episodes_match: + range_start, range_end = map(int, range_episodes_match[-1]) + episodes_range = range(range_start, range_end) + + if episode_fallback and not episode: + # If no matches found, attempt to capture episode-like sequences + fallback_pattern = re.compile(r'\bE?P?(\d{1,5})v?\d?\b', re.IGNORECASE) + filename = re.sub(r'[\s\.\:\;\(\)\[\]\{\}\\\/\&\€\'\`\#\@\=\$\?\!\%\+\-\_\*\^]', " ", filename) + fallback_matches = fallback_pattern.findall(filename) + + if fallback_matches: + # Assuming the last number in the fallback matches is the episode number + episode = fallback_matches[-1].lstrip("0").zfill(zfill) + + return DictAsObject( + { + "season": season.lstrip("0").zfill(zfill) if season else "", + "episode": episode.lstrip("0").zfill(zfill) if episode else "", + "episodes_range": episodes_range + } + ) diff --git a/a4kSubtitles/lib/video.py b/a4kSubtitles/lib/video.py index 744c457..4ed8f9e 100644 --- a/a4kSubtitles/lib/video.py +++ b/a4kSubtitles/lib/video.py @@ -274,7 +274,8 @@ def __update_info_from_imdb(core, meta, pagination_token=''): meta.episode = str(result['series']['episodeNumber']['episodeNumber']) else: meta.tvshow = result['titleText']['text'] - meta.tvshow_year = str(result['releaseDate']['year']) + if meta.tvshow_year == '': + meta.tvshow_year = str(result['releaseDate']['year']) episodes = result['episodes']['result']['edges'] s_number = int(meta.season) @@ -316,15 +317,11 @@ def __get_basic_info(): if regex_result: meta.imdb_id = regex_result.group(1) - if meta.season == '': - regex_result = re.search(r'.*season=(\d{1,}).*', filename_and_path, re.IGNORECASE) - if regex_result: - meta.season = regex_result.group(1) - - if meta.episode == '': - regex_result = re.search(r'.*episode=(\d{1,}).*', filename_and_path, re.IGNORECASE) - if regex_result: - meta.episode = regex_result.group(1) + if meta.season == '' or meta.episode == '': + filename_info = utils.extract_season_episode(meta.filename, zfill=0) + filename_path_info = utils.extract_season_episode(filename_and_path, zfill=0) + meta.season = meta.season or filename_path_info.season or filename_info.season + meta.episode = meta.episode or filename_path_info.episode or filename_info.episode return meta diff --git a/a4kSubtitles/search.py b/a4kSubtitles/search.py index cfccaa0..3e9bdf1 100644 --- a/a4kSubtitles/search.py +++ b/a4kSubtitles/search.py @@ -26,10 +26,11 @@ def __query_service(core, service_name, meta, request, results): core.progress_text = core.progress_text.replace(service.display_name, '') core.kodi.update_progress(core) -def __add_results(core, results): # pragma: no cover +def __add_results(core, results, meta): # pragma: no cover for item in results: listitem = core.kodi.create_listitem(item) + item['action_args'].setdefault("episodeid", meta.episode.zfill(3) if meta.episode else "") action_args = core.utils.quote_plus(core.json.dumps(item['action_args'])) core.kodi.xbmcplugin.addDirectoryItem( @@ -121,7 +122,8 @@ def __prepare_results(core, meta, results): release = [] for group in release_groups: release.extend(group) - release.extend(['avi', 'mp4', 'mkv', 'ts', 'm2ts', 'mts', 'mpeg', 'mpg', 'mov', 'wmv', 'flv', 'vob']) + media_exts = ['avi', 'mp4', 'mkv', 'ts', 'm2ts', 'mts', 'mpeg', 'mpg', 'mov', 'wmv', 'flv', 'vob'] + release.extend(media_exts) quality_groups = [ ['4k', '2160p', '2160', '4kuhd', '4kultrahd', 'ultrahd', 'uhd'], @@ -179,17 +181,17 @@ def __prepare_results(core, meta, results): extra = ['extended', 'cut', 'remastered', 'proper'] - filename = core.utils.unquote(meta.filename).lower() + filename = core.utils.unquote(meta.filename_without_ext).lower() regexsplitwords = r'[\s\.\:\;\(\)\[\]\{\}\\\/\&\€\'\`\#\@\=\$\?\!\%\+\-\_\*\^]' - nameparts = core.re.split(regexsplitwords, filename) + meta_nameparts = core.re.split(regexsplitwords, filename) - release_list = [i for i in nameparts if i in release] - quality_list = [i for i in nameparts if i in quality] - service_list = [i for i in nameparts if i in service] - codec_list = [i for i in nameparts if i in codec] - audio_list = [i for i in nameparts if i in audio] - color_list = [i for i in nameparts if i in color] - extra_list = [i for i in nameparts if i in extra] + release_list = [i for i in meta_nameparts if i in release] + quality_list = [i for i in meta_nameparts if i in quality] + service_list = [i for i in meta_nameparts if i in service] + codec_list = [i for i in meta_nameparts if i in codec] + audio_list = [i for i in meta_nameparts if i in audio] + color_list = [i for i in meta_nameparts if i in color] + extra_list = [i for i in meta_nameparts if i in extra] for item in release_list: for group in release_groups: @@ -227,22 +229,69 @@ def __prepare_results(core, meta, results): color_list = group break + def _filter_name(x): + name_diff_ignore = media_exts + quality + codec + audio + color + name_diff_ignore += ["multi", 'multiple', 'sub', 'subs', 'subtitle'] + + if x.isdigit(): + x = str(int(x)).zfill(3) + elif x.lower() in name_diff_ignore: + x = '' + return x.lower() + + def _match_numbers(a, b): + offset = 0 + for s in b: + s = core.re.sub(r'v[1-4]', "", s) + if not s.isdigit(): + continue + elif meta.episode and s.zfill(3) == meta.episode.zfill(3): + offset += 0.4 + elif s in a: + offset += 0.2 + + return offset + def sorter(x): name = x['name'].lower() nameparts = core.re.split(regexsplitwords, name) + cleaned_nameparts = list(filter(len, map(_filter_name, nameparts))) + cleaned_file_nameparts = list(filter(len, map(_filter_name, meta_nameparts))) + matching_offset = 0 + + if meta.is_tvshow: + sub_info = core.utils.extract_season_episode(name) + + is_season = sub_info.season and sub_info.season == meta.season.zfill(3) + is_episode = sub_info.episode and sub_info.episode == meta.episode.zfill(3) + + # Handle the parsed season and episode. + if is_season and not sub_info.episode: + matching_offset += 0.6 + if is_season and is_episode: + matching_offset += 0.4 + elif meta.episode and int(meta.episode) in sub_info.episodes_range: + matching_offset += 0.3 + elif sub_info.season and sub_info.episode: + matching_offset -= 0.5 + + if matching_offset == 0: + matching_offset = _match_numbers(cleaned_file_nameparts, cleaned_nameparts) + return ( not x['lang'] == meta.preferredlanguage, meta.languages.index(x['lang']), not x['sync'] == 'true', - -sum(i in nameparts for i in quality_list) * 10, + -(core.difflib.SequenceMatcher(None, cleaned_file_nameparts, cleaned_nameparts).ratio() + matching_offset), -sum(i in nameparts for i in release_list) * 10, + -sum(i in nameparts for i in quality_list) * 10, -sum(i in nameparts for i in codec_list) * 10, -sum(i in nameparts for i in service_list) * 10, -sum(i in nameparts for i in audio_list), -sum(i in nameparts for i in color_list), -sum(i in nameparts for i in extra_list), - -core.difflib.SequenceMatcher(None, name, filename).ratio(), + -core.difflib.SequenceMatcher(None, filename, name).ratio(), -x['rating'], not x['impaired'] == 'true', x['service'], @@ -275,11 +324,11 @@ def __wait_threads(core, request_threads): core.utils.wait_threads(threads) -def __complete_search(core, results): +def __complete_search(core, results, meta): if core.api_mode_enabled: return results - __add_results(core, results) # pragma: no cover + __add_results(core, results, meta) # pragma: no cover def __search(core, service_name, meta, results): service = core.services[service_name] @@ -326,7 +375,7 @@ def search(core, params): threads.append((auth_thread, search_thread)) if len(threads) == 0: - return __complete_search(core, results) + return __complete_search(core, results, meta) core.progress_text = core.progress_text[:-1] core.kodi.update_progress(core) @@ -344,7 +393,7 @@ def check_cancellation(): # pragma: no cover cancellation_token.iscanceled = True final_results = __prepare_results(core, meta, results) - ready_queue.put(__complete_search(core, final_results)) + ready_queue.put(__complete_search(core, final_results, meta)) break def wait_all_results(): @@ -353,7 +402,7 @@ def wait_all_results(): return final_results = __prepare_results(core, meta, results) __save_results(core, meta, final_results) - ready_queue.put(__complete_search(core, final_results)) + ready_queue.put(__complete_search(core, final_results, meta)) core.threading.Thread(target=check_cancellation).start() core.threading.Thread(target=wait_all_results).start() diff --git a/a4kSubtitles/service.py b/a4kSubtitles/service.py index f01718b..70ee23d 100644 --- a/a4kSubtitles/service.py +++ b/a4kSubtitles/service.py @@ -4,6 +4,7 @@ def start(api): core = api.core monitor = core.kodi.xbmc.Monitor() has_done_subs_check = False + prev_playing_filename = '' while not monitor.abortRequested(): if monitor.waitForAbort(1): @@ -12,13 +13,21 @@ def start(api): if not core.kodi.get_bool_setting('general', 'auto_search'): continue - has_video = (core.kodi.xbmc.getCondVisibility('VideoPlayer.Content(movies)') - or core.kodi.xbmc.getCondVisibility('VideoPlayer.Content(episodes)')) + has_video = core.kodi.xbmc.Player().isPlayingVideo() + if not has_video and has_done_subs_check: + prev_playing_filename = '' has_done_subs_check = False has_video_duration = core.kodi.xbmc.getCondVisibility('Player.HasDuration') + # In-case episode changed. + if has_video: + playing_filename = core.kodi.xbmc.getInfoLabel('Player.Filenameandpath') + if prev_playing_filename != playing_filename: + has_done_subs_check = False + prev_playing_filename = playing_filename + if not has_video or not has_video_duration or has_done_subs_check: continue diff --git a/a4kSubtitles/services/podnadpisi.py b/a4kSubtitles/services/podnadpisi.py index e50f7cc..0483373 100644 --- a/a4kSubtitles/services/podnadpisi.py +++ b/a4kSubtitles/services/podnadpisi.py @@ -74,7 +74,7 @@ def map_result(result): 'action_args': { 'url': '%s%s' % (__url, result['download']), 'lang': lang, - 'filename': name + 'filename': name, } } diff --git a/a4kSubtitles/services/subsource.py b/a4kSubtitles/services/subsource.py index 35a9494..f089668 100644 --- a/a4kSubtitles/services/subsource.py +++ b/a4kSubtitles/services/subsource.py @@ -7,42 +7,26 @@ __search = __api + "searchMovie" __download = __api + "downloadSub/" -def __extract_season_episode(core, text): - pattern = core.re.compile(r'(?:S(\d+)|Season\s*(\d+))[^E]*?(?:E(\d+)|Episode\s*(\d+))', core.re.IGNORECASE) - match = pattern.search(text) - - if match: - # Extract season and episode numbers from groups - season = match.group(1) or match.group(2) - episode = match.group(3) or match.group(4) - return (season, episode) - - # If no matches found, attempt to capture episode-like sequences - fallback_pattern = core.re.compile(r'\bE?P?(\d{2,5})\b', core.re.IGNORECASE) - fallback_matches = fallback_pattern.findall(text) - - if fallback_matches: - # Assuming the last number in the fallback matches is the episode number - episode_number = fallback_matches[-1] - return (None, episode_number) - - return (None, None) - def build_search_requests(core, service_name, meta): def get_movie(response): results = response.json() found = results.get("found", []) movie_name = "" + seasons = [] for res in found: - if res.get("type", "Movie") == "Movie" and meta.is_tvshow: + incorrect_type = res.get("type", "Movie") == "Movie" and meta.is_tvshow + incorrect_movie = meta.is_movie and meta.imdb_id and res.get("imdb") and res["imdb"] != meta.imdb_id + if incorrect_type or incorrect_movie: continue movie_name = res["linkName"] + seasons = res.get("seasons") break params = {"movieName": movie_name, "langs": meta.languages} if meta.is_tvshow: - params["season"] = "season-" + meta.season + season = seasons[0].get("number") if len(seasons) == 1 else meta.season + params["season"] = "season-" + str(season) return {"method": "POST", "url": __getMovie, "data": params} name = (meta.title if meta.is_movie else meta.tvshow) @@ -70,11 +54,6 @@ def parse_search_response(core, service_name, meta, response): if "subs" not in results: return [] - movie_details = results.get("movie", {}) - - # altname = movie_details.get("altName") - full_name = movie_details.get("fullName", "") - def map_result(result): name = result.get("releaseName", "") lang = result.get("lang") @@ -84,17 +63,6 @@ def map_result(result): rating = result.get("rating", 0) lang_code = core.utils.get_lang_id(lang, core.kodi.xbmc.ISO_639_1) - - if meta.is_tvshow: - subtitle_season = core.re.search(r'Season\s(\d+)', full_name) - season, episode = __extract_season_episode(core, name) - season = subtitle_season.group(1).zfill(2) if subtitle_season else season - - if season == meta.season.zfill(2) and episode == meta.episode.zfill(2): - name = meta.filename - elif not season and meta.season == "1" and episode == meta.episode.zfill(2): - name = meta.filename - return { "service_name": service_name, "service": service.display_name, @@ -106,7 +74,7 @@ def map_result(result): "impaired": "true" if result.get("hi", 0) != 0 else "false", "color": "teal", "action_args": { - "url": result["subId"], + "url": "{}#{}".format(result["subId"], name), "lang": lang, "filename": name, "full_link": result["fullLink"], diff --git a/addon.xml b/addon.xml index 881fe17..df30360 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ @@ -27,6 +27,19 @@ Supports: OpenSubtitles, BSPlayer, Podnadpisi.NET, SubDL, Addic7ed screenshot-03.png +[v3.17.0]: + * Fix: TV year being pulled incorrectly + * Fix: File name issues in both temp and media directories + * Fix: Subtitle file missing extension + * Fix: Incorrect episode selection when downloaded archive contains multiple subtitles + * Fix: Auto-download not working after the first selected episode in the playlist (A4K only works for the first media) + * Fix: Subtitle import issue due to "illegal characters" + * Improve: determination of subtitle episodes + * Improve: results parsing + * Feature: Auto-download now copies subtitles next to the video or to a custom location, based on Kodi's subtitle storage mode path + * SubSource: Now supports series in "absolute order", as used by some anime websites + * SubSource: Fixed issue of duplicated subtitle IDs with different names + [v3.16.1]: * Fix addons.xml.crc diff --git a/packages/addons.xml b/packages/addons.xml index e205c9a..a532d41 100644 --- a/packages/addons.xml +++ b/packages/addons.xml @@ -4,7 +4,7 @@ @@ -30,6 +30,19 @@ Supports: OpenSubtitles, BSPlayer, Podnadpisi.NET, SubDL, Addic7ed screenshot-03.png +[v3.17.0]: + * Fix: TV year being pulled incorrectly + * Fix: File name issues in both temp and media directories + * Fix: Subtitle file missing extension + * Fix: Incorrect episode selection when downloaded archive contains multiple subtitles + * Fix: Auto-download not working after the first selected episode in the playlist (A4K only works for the first media) + * Fix: Subtitle import issue due to "illegal characters" + * Improve: determination of subtitle episodes + * Improve: results parsing + * Feature: Auto-download now copies subtitles next to the video or to a custom location, based on Kodi's subtitle storage mode path + * SubSource: Now supports series in "absolute order", as used by some anime websites + * SubSource: Fixed issue of duplicated subtitle IDs with different names + [v3.16.1]: * Fix addons.xml.crc diff --git a/packages/addons.xml.crc b/packages/addons.xml.crc index 23758e5..f317be6 100644 --- a/packages/addons.xml.crc +++ b/packages/addons.xml.crc @@ -1 +1 @@ -5bc86b7bc15b75b88d4a87d79d11890754f439b6 \ No newline at end of file +df1db3ee71954a2943a826a98a0d1f51979f8c0b \ No newline at end of file diff --git a/tests/test_service.py b/tests/test_service.py index 2edbb33..b3be6e8 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -19,6 +19,13 @@ def restore(): api.core.kodi.xbmc.Monitor = default return restore +def __mock_is_playingvideo(api, mock_playing_state): + default = api.core.kodi.xbmc.Player().isPlayingVideo + api.core.kodi.xbmc.Player().isPlayingVideo = lambda: mock_playing_state + def restore(): + api.core.kodi.xbmc.Player().isPlayingVideo = default + return restore + def __mock_get_cond_visibility(api, mock_data): default = api.core.kodi.xbmc.getCondVisibility api.core.kodi.xbmc.getCondVisibility = lambda v: mock_data.get(v, False) @@ -56,6 +63,27 @@ def restore(): restore_settings() return restore +def test_service_start_when_video_playing(): + def test_playing_video(state): + a4ksubtitles_api = api.A4kSubtitlesApi({'kodi': True}) + + restore = __mock(a4ksubtitles_api, { + 'general.auto_search': 'True', + }) + get_infolabel_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'getInfoLabel') + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, state) + + service.start(a4ksubtitles_api) + + restore() + restore_isplayingvideo() + get_infolabel_spy.restore() + + return get_infolabel_spy.call_count + + assert test_playing_video(True) != 0 + assert test_playing_video(False) == 0 + def test_service_start_when_disabled(): a4ksubtitles_api = api.A4kSubtitlesApi({'kodi': True}) @@ -63,10 +91,12 @@ def test_service_start_when_disabled(): 'general.auto_search': 'false', }) get_cond_visibility_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'getCondVisibility') + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() get_cond_visibility_spy.restore() assert get_cond_visibility_spy.call_count == 0 @@ -78,10 +108,12 @@ def test_service_start_when_enabled(): 'general.auto_search': 'true', }) get_cond_visibility_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'getCondVisibility') + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() get_cond_visibility_spy.restore() assert get_cond_visibility_spy.call_count > 0 @@ -93,7 +125,6 @@ def test_service_when_video_does_not_have_subtitles(): 'general.auto_search': 'true', }) restore_get_cond_visibility = __mock_get_cond_visibility(a4ksubtitles_api, { - 'VideoPlayer.Content(episodes)': True, 'Player.HasDuration': True, 'VideoPlayer.HasSubtitles': False, 'VideoPlayer.SubtitlesEnabled': False, @@ -101,12 +132,14 @@ def test_service_when_video_does_not_have_subtitles(): restore_get_info_label = __mock_get_info_label(a4ksubtitles_api, { 'VideoPlayer.IMDBNumber': 'tt1234567', }) + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) executebuiltin_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'executebuiltin') service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() restore_get_cond_visibility() restore_get_info_label() executebuiltin_spy.restore() @@ -120,7 +153,6 @@ def test_service_when_video_has_disabled_subtitles(): 'general.auto_search': 'true', }) restore_get_cond_visibility = __mock_get_cond_visibility(a4ksubtitles_api, { - 'VideoPlayer.Content(movies)': True, 'Player.HasDuration': True, 'VideoPlayer.HasSubtitles': True, 'VideoPlayer.SubtitlesEnabled': False, @@ -128,12 +160,14 @@ def test_service_when_video_has_disabled_subtitles(): restore_get_info_label = __mock_get_info_label(a4ksubtitles_api, { 'VideoPlayer.IMDBNumber': 'tt1234567', }) + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) executebuiltin_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'executebuiltin') service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() restore_get_cond_visibility() restore_get_info_label() executebuiltin_spy.restore() @@ -147,7 +181,6 @@ def test_service_when_does_not_have_video_duration(): 'general.auto_search': 'true', }) restore_get_cond_visibility = __mock_get_cond_visibility(a4ksubtitles_api, { - 'VideoPlayer.Content(movies)': True, 'Player.HasDuration': False, 'VideoPlayer.HasSubtitles': False, 'VideoPlayer.SubtitlesEnabled': False, @@ -155,12 +188,14 @@ def test_service_when_does_not_have_video_duration(): restore_get_info_label = __mock_get_info_label(a4ksubtitles_api, { 'VideoPlayer.IMDBNumber': 'tt1234567', }) + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) executebuiltin_spy = utils.spy_fn(a4ksubtitles_api.core.kodi.xbmc, 'executebuiltin') service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() restore_get_cond_visibility() restore_get_info_label() executebuiltin_spy.restore() @@ -175,7 +210,6 @@ def test_service_auto_download(): 'general.auto_download': 'true', }) restore_get_cond_visibility = __mock_get_cond_visibility(a4ksubtitles_api, { - 'VideoPlayer.Content(movies)': True, 'Player.HasDuration': True, 'VideoPlayer.HasSubtitles': False, 'VideoPlayer.SubtitlesEnabled': False, @@ -183,6 +217,7 @@ def test_service_auto_download(): restore_get_info_label = __mock_get_info_label(a4ksubtitles_api, { 'VideoPlayer.IMDBNumber': 'tt1234567', }) + restore_isplayingvideo = __mock_is_playingvideo(a4ksubtitles_api, True) restore_api_search = __mock_api_search(a4ksubtitles_api) expected_download_result = 'test_download_result' restore_api_download = __mock_api_download(a4ksubtitles_api, expected_download_result) @@ -193,6 +228,7 @@ def test_service_auto_download(): service.start(a4ksubtitles_api) restore() + restore_isplayingvideo() restore_get_cond_visibility() restore_get_info_label() restore_api_search()