diff --git a/.github/scripts/build_test.sh b/.github/scripts/build_test.sh old mode 100644 new mode 100755 diff --git a/.github/scripts/create_asset.sh b/.github/scripts/create_asset.sh index 304a7fe15..d699043b6 100755 --- a/.github/scripts/create_asset.sh +++ b/.github/scripts/create_asset.sh @@ -23,6 +23,6 @@ done cp VERSION $to_dist pushd __builds__/bazarr -zip -r ../bazarr.zip . -x '*.map' -b $(mktemp -d) +zip -r ../bazarr.zip . -b $(mktemp -d) popd rm -rf $to_dist \ No newline at end of file diff --git a/bazarr/analytics.py b/bazarr/analytics.py index 59947f1ec..b8df668b7 100644 --- a/bazarr/analytics.py +++ b/bazarr/analytics.py @@ -12,10 +12,10 @@ from pyga.entities import CustomVariable from get_args import args from config import settings -from utils import get_sonarr_version, get_radarr_version +from utils import get_sonarr_info, get_radarr_info -sonarr_version = get_sonarr_version() -radarr_version = get_radarr_version() +sonarr_version = get_sonarr_info.version() +radarr_version = get_radarr_info.version() def track_event(category=None, action=None, label=None): diff --git a/bazarr/api.py b/bazarr/api.py index aea8198aa..5e56f61f7 100644 --- a/bazarr/api.py +++ b/bazarr/api.py @@ -1,5 +1,7 @@ # coding=utf-8 +import sys +import os import ast from datetime import timedelta from dateutil import rrule @@ -15,6 +17,8 @@ import hashlib import apprise import gc from peewee import fn, Value +import requests +from bs4 import BeautifulSoup as bso from get_args import args from config import settings, base_url, save_settings, get_settings @@ -35,7 +39,7 @@ from notifier import send_notifications, send_notifications_movie from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, \ list_missing_subtitles, list_missing_subtitles_movies from utils import history_log, history_log_movie, blacklist_log, blacklist_delete, blacklist_delete_all, \ - blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_version, get_radarr_version, \ + blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_info, get_radarr_info, \ delete_subtitles, subtitles_apply_mods, translate_subtitles_file, check_credentials, get_health_issues from get_providers import get_providers, get_providers_auth, list_throttled_providers, reset_throttled_providers, \ get_throttled_providers, set_throttled_providers @@ -596,8 +600,8 @@ class SystemStatus(Resource): def get(self): system_status = {} system_status.update({'bazarr_version': os.environ["BAZARR_VERSION"]}) - system_status.update({'sonarr_version': get_sonarr_version()}) - system_status.update({'radarr_version': get_radarr_version()}) + system_status.update({'sonarr_version': get_sonarr_info.version()}) + system_status.update({'radarr_version': get_radarr_info.version()}) system_status.update({'operating_system': platform.platform()}) system_status.update({'python_version': platform.python_version()}) system_status.update({'bazarr_directory': os.path.dirname(os.path.dirname(__file__))}) @@ -716,6 +720,15 @@ class Series(Resource): list_missing_subtitles(no=seriesId, send_event=False) event_stream(type='series', payload=seriesId) + + episode_id_list = TableEpisodes\ + .select(TableEpisodes.sonarrEpisodeId)\ + .where(TableEpisodes.sonarrSeriesId == seriesId)\ + .dicts() + + for item in episode_id_list: + event_stream(type='episode-wanted', payload=item['sonarrEpisodeId']) + event_stream(type='badges') return '', 204 @@ -963,6 +976,7 @@ class Movies(Resource): list_missing_subtitles_movies(no=radarrId, send_event=False) event_stream(type='movie', payload=radarrId) + event_stream(type='movie-wanted', payload=radarrId) event_stream(type='badges') return '', 204 @@ -1516,7 +1530,7 @@ class MoviesHistory(Resource): if int(upgradable_movie['score']) < 120: upgradable_movies_not_perfect.append(upgradable_movie) - query_conditions = [(TableMovies is not None)] + query_conditions = [(TableMovies.title is not None)] if radarrid: query_conditions.append((TableMovies.radarrId == radarrid)) query_condition = reduce(operator.and_, query_conditions) @@ -1696,6 +1710,7 @@ class EpisodesWanted(Resource): TableShows.seriesType)\ .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ .where(wanted_condition)\ + .order_by(TableEpisodes.rowid.desc())\ .limit(length)\ .offset(start)\ .dicts() @@ -1749,7 +1764,7 @@ class MoviesWanted(Resource): TableMovies.tags, TableMovies.monitored)\ .where(wanted_condition)\ - .order_by(TableMovies.radarrId.desc())\ + .order_by(TableMovies.rowid.desc())\ .limit(length)\ .offset(start)\ .dicts() @@ -1966,6 +1981,12 @@ class Subtitles(Resource): else: subtitles_apply_mods(language, subtitles_path, [action]) + # apply chmod if required + chmod = int(settings.general.chmod, 8) if not sys.platform.startswith( + 'win') and settings.general.getboolean('chmod_enabled') else None + if chmod: + os.chmod(subtitles_path, chmod) + return '', 204 @@ -2045,6 +2066,68 @@ class BrowseRadarrFS(Resource): return jsonify(data) +class WebHooksPlex(Resource): + @authenticate + def post(self): + json_webhook = request.form.get('payload') + parsed_json_webhook = json.loads(json_webhook) + + event = parsed_json_webhook['event'] + if event not in ['media.play']: + return '', 204 + + media_type = parsed_json_webhook['Metadata']['type'] + + if media_type == 'episode': + season = parsed_json_webhook['Metadata']['parentIndex'] + episode = parsed_json_webhook['Metadata']['index'] + else: + season = episode = None + + ids = [] + for item in parsed_json_webhook['Metadata']['Guid']: + splitted_id = item['id'].split('://') + if len(splitted_id) == 2: + ids.append({splitted_id[0]: splitted_id[1]}) + if not ids: + return '', 404 + + if media_type == 'episode': + try: + episode_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0] + r = requests.get('https://imdb.com/title/{}'.format(episode_imdb_id), + headers={"User-Agent": os.environ["SZ_USER_AGENT"]}) + soup = bso(r.content, "html.parser") + series_imdb_id = soup.find('a', {'class': re.compile(r'SeriesParentLink__ParentTextLink')})['href'].split('/')[2] + except: + return '', 404 + else: + sonarrEpisodeId = TableEpisodes.select(TableEpisodes.sonarrEpisodeId) \ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \ + .where(TableShows.imdbId == series_imdb_id, + TableEpisodes.season == season, + TableEpisodes.episode == episode) \ + .dicts() \ + .get() + + if sonarrEpisodeId: + episode_download_subtitles(no=sonarrEpisodeId['sonarrEpisodeId'], send_progress=True) + else: + try: + movie_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0] + except: + return '', 404 + else: + radarrId = TableMovies.select(TableMovies.radarrId)\ + .where(TableMovies.imdbId == movie_imdb_id)\ + .dicts()\ + .get() + if radarrId: + movies_download_subtitles(no=radarrId['radarrId']) + + return '', 200 + + api.add_resource(Badges, '/badges') api.add_resource(Providers, '/providers') @@ -2086,3 +2169,5 @@ api.add_resource(HistoryStats, '/history/stats') api.add_resource(BrowseBazarrFS, '/files') api.add_resource(BrowseSonarrFS, '/files/sonarr') api.add_resource(BrowseRadarrFS, '/files/radarr') + +api.add_resource(WebHooksPlex, '/webhooks/plex') diff --git a/bazarr/check_update.py b/bazarr/check_update.py index 699342842..8cfc841ee 100644 --- a/bazarr/check_update.py +++ b/bazarr/check_update.py @@ -1,6 +1,8 @@ # coding=utf-8 import os +import shutil +import re import logging import json import requests @@ -19,13 +21,13 @@ def check_releases(): logging.debug('BAZARR getting releases from Github: {}'.format(url_releases)) r = requests.get(url_releases, allow_redirects=True) r.raise_for_status() - except requests.exceptions.HTTPError as errh: + except requests.exceptions.HTTPError: logging.exception("Error trying to get releases from Github. Http error.") - except requests.exceptions.ConnectionError as errc: + except requests.exceptions.ConnectionError: logging.exception("Error trying to get releases from Github. Connection Error.") - except requests.exceptions.Timeout as errt: + except requests.exceptions.Timeout: logging.exception("Error trying to get releases from Github. Timeout Error.") - except requests.exceptions.RequestException as err: + except requests.exceptions.RequestException: logging.exception("Error trying to get releases from Github.") else: for release in r.json(): @@ -151,6 +153,14 @@ def apply_update(): logging.exception('BAZARR unable to unzip release') else: is_updated = True + try: + logging.debug('BAZARR successfully unzipped new release and will now try to delete the leftover ' + 'files.') + update_cleaner(zipfile=bazarr_zip, bazarr_dir=bazarr_dir, config_dir=args.config_dir) + except: + logging.exception('BAZARR unable to cleanup leftover files after upgrade.') + else: + logging.debug('BAZARR successfully deleted leftover files.') finally: logging.debug('BAZARR now deleting release archive') os.remove(bazarr_zip) @@ -161,3 +171,76 @@ def apply_update(): logging.debug('BAZARR new release have been installed, now we restart') from server import webserver webserver.restart() + + +def update_cleaner(zipfile, bazarr_dir, config_dir): + with ZipFile(zipfile, 'r') as archive: + file_in_zip = archive.namelist() + logging.debug('BAZARR zip file contain {} directories and files'.format(len(file_in_zip))) + separator = os.path.sep + if os.path.sep == '\\': + logging.debug('BAZARR upgrade leftover cleaner is running on Windows. We\'ll fix the zip file separator ' + 'accordingly.') + for i, item in enumerate(file_in_zip): + file_in_zip[i] = item.replace('/', '\\') + separator += os.path.sep + else: + logging.debug('BAZARR upgrade leftover cleaner is running on something else than Windows. The zip file ' + 'separator are fine.') + + dir_to_ignore = ['^.' + separator, + '^bin' + separator, + '^venv' + separator, + '^WinPython' + separator, + separator + '__pycache__' + separator + '$'] + if os.path.abspath(bazarr_dir) in os.path.abspath(config_dir): + dir_to_ignore.append('^' + os.path.relpath(config_dir, bazarr_dir) + os.path.sep) + dir_to_ignore_regex = re.compile('(?:% s)' % '|'.join(dir_to_ignore)) + logging.debug('BAZARR upgrade leftover cleaner will ignore directories matching this regex: ' + '{}'.format(dir_to_ignore_regex)) + + file_to_ignore = ['nssm.exe', '7za.exe'] + logging.debug('BAZARR upgrade leftover cleaner will ignore those files: {}'.format(', '.join(file_to_ignore))) + extension_to_ignore = ['.pyc'] + logging.debug('BAZARR upgrade leftover cleaner will ignore files with those extensions: ' + '{}'.format(', '.join(extension_to_ignore))) + + file_on_disk = [] + folder_list = [] + for foldername, subfolders, filenames in os.walk(bazarr_dir): + relative_foldername = os.path.relpath(foldername, bazarr_dir) + os.path.sep + + if not dir_to_ignore_regex.findall(relative_foldername): + if relative_foldername not in folder_list: + folder_list.append(relative_foldername) + + for file in filenames: + if file in file_to_ignore: + continue + elif os.path.splitext(file)[1] in extension_to_ignore: + continue + elif foldername == bazarr_dir: + file_on_disk.append(file) + else: + current_dir = relative_foldername + filepath = os.path.join(current_dir, file) + if not dir_to_ignore_regex.findall(filepath): + file_on_disk.append(filepath) + logging.debug('BAZARR directory contain {} files'.format(len(file_on_disk))) + logging.debug('BAZARR directory contain {} directories'.format(len(folder_list))) + file_on_disk += folder_list + logging.debug('BAZARR directory contain {} directories and files'.format(len(file_on_disk))) + + file_to_remove = list(set(file_on_disk) - set(file_in_zip)) + logging.debug('BAZARR will delete {} directories and files'.format(len(file_to_remove))) + logging.debug('BAZARR will delete this: {}'.format(', '.join(file_to_remove))) + + for file in file_to_remove: + filepath = os.path.join(bazarr_dir, file) + try: + if os.path.isdir(filepath): + rmtree(filepath, ignore_errors=True) + else: + os.remove(filepath) + except Exception as e: + logging.debug('BAZARR upgrade leftover cleaner cannot delete {}'.format(filepath)) diff --git a/bazarr/config.py b/bazarr/config.py index e076ea3b8..1b2a3c655 100644 --- a/bazarr/config.py +++ b/bazarr/config.py @@ -139,7 +139,8 @@ defaults = { }, 'legendastv': { 'username': '', - 'password': '' + 'password': '', + 'featured_only': 'False' }, 'xsubs': { 'username': '', @@ -317,7 +318,7 @@ def save_settings(settings_items): # Make sure that text based form values aren't pass as list if isinstance(value, list) and len(value) == 1 and settings_keys[-1] not in array_keys: value = value[0] - if value in empty_values: + if value in empty_values and value != '': value = None # Make sure empty language list are stored correctly @@ -442,11 +443,17 @@ def save_settings(settings_items): if sonarr_changed: from signalr_client import sonarr_signalr_client - sonarr_signalr_client.restart() + try: + sonarr_signalr_client.restart() + except: + pass if radarr_changed: from signalr_client import radarr_signalr_client - radarr_signalr_client.restart() + try: + radarr_signalr_client.restart() + except: + pass if update_path_map: from helper import path_mappings diff --git a/bazarr/database.py b/bazarr/database.py index 8662010ce..9924f8d72 100644 --- a/bazarr/database.py +++ b/bazarr/database.py @@ -8,6 +8,7 @@ from peewee import * from playhouse.sqliteq import SqliteQueueDatabase from playhouse.shortcuts import model_to_dict from playhouse.migrate import * +from playhouse.sqlite_ext import RowIDField from helper import path_mappings from config import settings, get_array_from @@ -69,6 +70,7 @@ class TableBlacklistMovie(BaseModel): class TableEpisodes(BaseModel): + rowid = RowIDField() audio_codec = TextField(null=True) audio_language = TextField(null=True) episode = IntegerField() @@ -140,6 +142,7 @@ class TableLanguagesProfiles(BaseModel): class TableMovies(BaseModel): + rowid = RowIDField() alternativeTitles = TextField(null=True) audio_codec = TextField(null=True) audio_language = TextField(null=True) @@ -163,7 +166,7 @@ class TableMovies(BaseModel): subtitles = TextField(null=True) tags = TextField(null=True) title = TextField() - tmdbId = TextField(primary_key=True) + tmdbId = TextField(unique=True) video_codec = TextField(null=True) year = TextField(null=True) diff --git a/bazarr/filesystem.py b/bazarr/filesystem.py index 47e09c8cb..db6ccc782 100644 --- a/bazarr/filesystem.py +++ b/bazarr/filesystem.py @@ -6,7 +6,7 @@ import logging import string from config import settings, url_sonarr, url_radarr -from utils import get_sonarr_version, get_radarr_version +from utils import get_sonarr_info, get_radarr_info headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -46,10 +46,9 @@ def browse_bazarr_filesystem(path='#'): def browse_sonarr_filesystem(path='#'): - sonarr_version = get_sonarr_version() if path == '#': path = '' - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): url_sonarr_api_filesystem = url_sonarr() + "/api/filesystem?path=" + path + \ "&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \ settings.sonarr.apikey @@ -77,11 +76,10 @@ def browse_sonarr_filesystem(path='#'): def browse_radarr_filesystem(path='#'): - radarr_version = get_radarr_version() if path == '#': path = '' - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): url_radarr_api_filesystem = url_radarr() + "/api/filesystem?path=" + path + \ "&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \ settings.radarr.apikey diff --git a/bazarr/get_episodes.py b/bazarr/get_episodes.py index 4c1332882..f26c2f545 100644 --- a/bazarr/get_episodes.py +++ b/bazarr/get_episodes.py @@ -12,7 +12,7 @@ from helper import path_mappings from list_subtitles import store_subtitles, series_full_scan_subtitles from get_subtitle import episode_download_subtitles from event_handler import event_stream, show_progress, hide_progress -from utils import get_sonarr_version +from utils import get_sonarr_info headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -25,7 +25,6 @@ def update_all_episodes(): def sync_episodes(series_id=None, send_event=True): logging.debug('BAZARR Starting episodes sync from Sonarr.') apikey_sonarr = settings.sonarr.apikey - sonarr_version = get_sonarr_version() # Get current episodes id in DB current_episodes_db = TableEpisodes.select(TableEpisodes.sonarrEpisodeId, @@ -42,8 +41,7 @@ def sync_episodes(series_id=None, send_event=True): altered_episodes = [] # Get sonarrId for each series from database - seriesIdList = get_series_from_sonarr_api(series_id=series_id, url=url_sonarr(), apikey_sonarr=apikey_sonarr, - sonarr_version=sonarr_version) + seriesIdList = get_series_from_sonarr_api(series_id=series_id, url=url_sonarr(), apikey_sonarr=apikey_sonarr,) series_count = len(seriesIdList) for i, seriesId in enumerate(seriesIdList, 1): @@ -57,13 +55,12 @@ def sync_episodes(series_id=None, send_event=True): # Get episodes data for a series from Sonarr episodes = get_episodes_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr, - series_id=seriesId['sonarrSeriesId'], - sonarr_version=sonarr_version) + series_id=seriesId['sonarrSeriesId']) if not episodes: continue else: # For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results - if sonarr_version.startswith('3'): + if not get_sonarr_info.is_legacy(): episodeFiles = get_episodesFiles_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr, series_id=seriesId['sonarrSeriesId']) for episode in episodes: @@ -166,7 +163,6 @@ def sync_one_episode(episode_id): logging.debug('BAZARR syncing this specific episode from Sonarr: {}'.format(episode_id)) url = url_sonarr() apikey_sonarr = settings.sonarr.apikey - sonarr_version = get_sonarr_version() # Check if there's a row in database for this episode ID try: @@ -181,13 +177,13 @@ def sync_one_episode(episode_id): # Get episode data from sonarr api episode = None episode_data = get_episodes_from_sonarr_api(url=url, apikey_sonarr=apikey_sonarr, - episode_id=episode_id, sonarr_version=sonarr_version) + episode_id=episode_id) if not episode_data: return else: # For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results - if sonarr_version.startswith('3'): + if not get_sonarr_info.is_legacy(): episodeFile = get_episodesFiles_from_sonarr_api(url=url, apikey_sonarr=apikey_sonarr, episode_file_id=existing_episode['episode_file_id']) if episode_data['hasFile']: @@ -292,7 +288,7 @@ def episodeParser(episode): if 'name' in item: audio_language.append(item['name']) else: - audio_language = TableShows.get(TableShows == episode['seriesId']).audio_language + audio_language = TableShows.get(TableShows.sonarrSeriesId == episode['seriesId']).audio_language if 'mediaInfo' in episode['episodeFile']: if 'videoCodec' in episode['episodeFile']['mediaInfo']: @@ -336,13 +332,13 @@ def episodeParser(episode): 'file_size': episode['episodeFile']['size']} -def get_series_from_sonarr_api(series_id, url, apikey_sonarr, sonarr_version): +def get_series_from_sonarr_api(series_id, url, apikey_sonarr): if series_id: url_sonarr_api_series = url + "/api/{0}series/{1}?apikey={2}".format( - '' if sonarr_version.startswith('2') else 'v3/', series_id, apikey_sonarr) + '' if get_sonarr_info.is_legacy() else 'v3/', series_id, apikey_sonarr) else: url_sonarr_api_series = url + "/api/{0}series?apikey={1}".format( - '' if sonarr_version.startswith('2') else 'v3/', apikey_sonarr) + '' if get_sonarr_info.is_legacy() else 'v3/', apikey_sonarr) try: r = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) r.raise_for_status() @@ -372,13 +368,13 @@ def get_series_from_sonarr_api(series_id, url, apikey_sonarr, sonarr_version): return series_list -def get_episodes_from_sonarr_api(url, apikey_sonarr, sonarr_version, series_id=None, episode_id=None): +def get_episodes_from_sonarr_api(url, apikey_sonarr, series_id=None, episode_id=None): if series_id: url_sonarr_api_episode = url + "/api/{0}episode?seriesId={1}&apikey={2}".format( - '' if sonarr_version.startswith('2') else 'v3/', series_id, apikey_sonarr) + '' if get_sonarr_info.is_legacy() else 'v3/', series_id, apikey_sonarr) elif episode_id: url_sonarr_api_episode = url + "/api/{0}episode/{1}?apikey={2}".format( - '' if sonarr_version.startswith('2') else 'v3/', episode_id, apikey_sonarr) + '' if get_sonarr_info.is_legacy() else 'v3/', episode_id, apikey_sonarr) else: return diff --git a/bazarr/get_movies.py b/bazarr/get_movies.py index 1aeeb5c54..322a948a5 100644 --- a/bazarr/get_movies.py +++ b/bazarr/get_movies.py @@ -10,7 +10,7 @@ from peewee import DoesNotExist from config import settings, url_radarr from helper import path_mappings -from utils import get_radarr_version +from utils import get_radarr_info from list_subtitles import store_subtitles_movie, movies_full_scan_subtitles from get_rootfolder import check_radarr_rootfolder @@ -31,7 +31,6 @@ def update_movies(send_event=True): logging.debug('BAZARR Starting movie sync from Radarr.') apikey_radarr = settings.radarr.apikey - radarr_version = get_radarr_version() movie_default_enabled = settings.general.getboolean('movie_default_enabled') if movie_default_enabled is True: @@ -45,11 +44,10 @@ def update_movies(send_event=True): pass else: audio_profiles = get_profile_list() - tagsDict = get_tags(radarr_version=radarr_version) + tagsDict = get_tags() # Get movies data from radarr - movies = get_movies_from_radarr_api(radarr_version=radarr_version, url=url_radarr(), - apikey_radarr=apikey_radarr) + movies = get_movies_from_radarr_api(url=url_radarr(), apikey_radarr=apikey_radarr) if not movies: return else: @@ -82,13 +80,11 @@ def update_movies(send_event=True): if str(movie['tmdbId']) in current_movies_db_list: movies_to_update.append(movieParser(movie, action='update', - radarr_version=radarr_version, tags_dict=tagsDict, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)) else: movies_to_add.append(movieParser(movie, action='insert', - radarr_version=radarr_version, tags_dict=tagsDict, movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)) @@ -190,7 +186,6 @@ def update_one_movie(movie_id, action): existing_movie['path']))) return - radarr_version = get_radarr_version() movie_default_enabled = settings.general.getboolean('movie_default_enabled') if movie_default_enabled is True: @@ -201,24 +196,22 @@ def update_one_movie(movie_id, action): movie_default_profile = None audio_profiles = get_profile_list() - tagsDict = get_tags(radarr_version=radarr_version) + tagsDict = get_tags() try: # Get movie data from radarr api movie = None - movie_data = get_movies_from_radarr_api(radarr_version=radarr_version, url=url_radarr(), - apikey_radarr=settings.radarr.apikey, radarr_id=movie_id) + movie_data = get_movies_from_radarr_api(url=url_radarr(), apikey_radarr=settings.radarr.apikey, + radarr_id=movie_id) if not movie_data: return else: if action == 'updated' and existing_movie: - movie = movieParser(movie_data, action='update', radarr_version=radarr_version, - tags_dict=tagsDict, movie_default_profile=movie_default_profile, - audio_profiles=audio_profiles) + movie = movieParser(movie_data, action='update', tags_dict=tagsDict, + movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) elif action == 'updated' and not existing_movie: - movie = movieParser(movie_data, action='insert', radarr_version=radarr_version, - tags_dict=tagsDict, movie_default_profile=movie_default_profile, - audio_profiles=audio_profiles) + movie = movieParser(movie_data, action='insert', tags_dict=tagsDict, + movie_default_profile=movie_default_profile, audio_profiles=audio_profiles) except Exception: logging.debug('BAZARR cannot get movie returned by SignalR feed from Radarr API.') return @@ -262,10 +255,9 @@ def update_one_movie(movie_id, action): def get_profile_list(): apikey_radarr = settings.radarr.apikey - radarr_version = get_radarr_version() profiles_list = [] # Get profiles data from radarr - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): url_radarr_api_movies = url_radarr() + "/api/profile?apikey=" + apikey_radarr else: url_radarr_api_movies = url_radarr() + "/api/v3/qualityprofile?apikey=" + apikey_radarr @@ -280,7 +272,7 @@ def get_profile_list(): logging.exception("BAZARR Error trying to get profiles from Radarr.") else: # Parsing data returned from radarr - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): for profile in profiles_json.json(): profiles_list.append([profile['id'], profile['language'].capitalize()]) else: @@ -346,12 +338,12 @@ def RadarrFormatVideoCodec(videoFormat, videoCodecID, videoCodecLibrary): return videoFormat -def get_tags(radarr_version): +def get_tags(): apikey_radarr = settings.radarr.apikey tagsDict = [] # Get tags data from Radarr - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): url_radarr_api_series = url_radarr() + "/api/tag?apikey=" + apikey_radarr else: url_radarr_api_series = url_radarr() + "/api/v3/tag?apikey=" + apikey_radarr @@ -371,7 +363,7 @@ def get_tags(radarr_version): return tagsDict.json() -def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, audio_profiles): +def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles): if 'movieFile' in movie: # Detect file separator if movie['path'][0] == "/": @@ -399,7 +391,7 @@ def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, sceneName = None alternativeTitles = None - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): if 'alternativeTitles' in movie: alternativeTitles = str([item['title'] for item in movie['alternativeTitles']]) else: @@ -422,7 +414,7 @@ def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, if 'mediaInfo' in movie['movieFile']: videoFormat = videoCodecID = videoProfile = videoCodecLibrary = None - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): if 'videoFormat' in movie['movieFile']['mediaInfo']: videoFormat = \ movie['movieFile']['mediaInfo']['videoFormat'] else: @@ -437,7 +429,7 @@ def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, videoCodec = RadarrFormatVideoCodec(videoFormat, videoCodecID, videoCodecLibrary) audioFormat = audioCodecID = audioProfile = audioAdditionalFeatures = None - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): if 'audioFormat' in movie['movieFile']['mediaInfo']: audioFormat = \ movie['movieFile']['mediaInfo']['audioFormat'] else: @@ -456,7 +448,7 @@ def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, audioCodec = None audio_language = [] - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): if 'mediaInfo' in movie['movieFile']: if 'audioLanguages' in movie['movieFile']['mediaInfo']: audio_languages_list = movie['movieFile']['mediaInfo']['audioLanguages'].split('/') @@ -522,8 +514,8 @@ def movieParser(movie, action, radarr_version, tags_dict, movie_default_profile, 'file_size': movie['movieFile']['size']} -def get_movies_from_radarr_api(radarr_version, url, apikey_radarr, radarr_id=None): - if radarr_version.startswith('0'): +def get_movies_from_radarr_api(url, apikey_radarr, radarr_id=None): + if get_radarr_info.is_legacy(): url_radarr_api_movies = url + "/api/movie" + ("/{}".format(radarr_id) if radarr_id else "") + "?apikey=" + \ apikey_radarr else: diff --git a/bazarr/get_providers.py b/bazarr/get_providers.py index e0fd0293b..497ce4526 100644 --- a/bazarr/get_providers.py +++ b/bazarr/get_providers.py @@ -164,6 +164,9 @@ def get_providers_auth(): 'legendastv' : { 'username': settings.legendastv.username, 'password': settings.legendastv.password, + 'featured_only': settings.legendastv.getboolean( + 'featured_only' + ), }, 'xsubs' : { 'username': settings.xsubs.username, diff --git a/bazarr/get_rootfolder.py b/bazarr/get_rootfolder.py index e8529e954..fb33e0d85 100644 --- a/bazarr/get_rootfolder.py +++ b/bazarr/get_rootfolder.py @@ -7,7 +7,7 @@ import logging from config import settings, url_sonarr, url_radarr from helper import path_mappings from database import TableShowsRootfolder, TableMoviesRootfolder, TableShows, TableMovies -from utils import get_sonarr_version, get_radarr_version +from utils import get_sonarr_info, get_radarr_info headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -15,10 +15,9 @@ headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} def get_sonarr_rootfolder(): apikey_sonarr = settings.sonarr.apikey sonarr_rootfolder = [] - sonarr_version = get_sonarr_version() # Get root folder data from Sonarr - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): url_sonarr_api_rootfolder = url_sonarr() + "/api/rootfolder?apikey=" + apikey_sonarr else: url_sonarr_api_rootfolder = url_sonarr() + "/api/v3/rootfolder?apikey=" + apikey_sonarr @@ -63,8 +62,11 @@ def check_sonarr_rootfolder(): rootfolder = TableShowsRootfolder.select(TableShowsRootfolder.id, TableShowsRootfolder.path).dicts() for item in rootfolder: root_path = item['path'] - if not root_path.endswith(os.path.sep): - root_path += os.path.sep + if not root_path.endswith(('/', '\\')): + if root_path.startswith('/'): + root_path += '/' + else: + root_path += '\\' if not os.path.isdir(path_mappings.path_replace(root_path)): TableShowsRootfolder.update({TableShowsRootfolder.accessible: 0, TableShowsRootfolder.error: 'This Sonarr root directory does not seems to ' @@ -87,10 +89,9 @@ def check_sonarr_rootfolder(): def get_radarr_rootfolder(): apikey_radarr = settings.radarr.apikey radarr_rootfolder = [] - radarr_version = get_radarr_version() # Get root folder data from Radarr - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): url_radarr_api_rootfolder = url_radarr() + "/api/rootfolder?apikey=" + apikey_radarr else: url_radarr_api_rootfolder = url_radarr() + "/api/v3/rootfolder?apikey=" + apikey_radarr @@ -134,8 +135,11 @@ def check_radarr_rootfolder(): rootfolder = TableMoviesRootfolder.select(TableMoviesRootfolder.id, TableMoviesRootfolder.path).dicts() for item in rootfolder: root_path = item['path'] - if not root_path.endswith(os.path.sep): - root_path += os.path.sep + if not root_path.endswith(('/', '\\')): + if root_path.startswith('/'): + root_path += '/' + else: + root_path += '\\' if not os.path.isdir(path_mappings.path_replace_movie(root_path)): TableMoviesRootfolder.update({TableMoviesRootfolder.accessible: 0, TableMoviesRootfolder.error: 'This Radarr root directory does not seems to ' diff --git a/bazarr/get_series.py b/bazarr/get_series.py index 6dbe76292..0f3165dfa 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -11,7 +11,7 @@ from list_subtitles import list_missing_subtitles from get_rootfolder import check_sonarr_rootfolder from database import TableShows, TableEpisodes from get_episodes import sync_episodes -from utils import get_sonarr_version +from utils import get_sonarr_info from helper import path_mappings from event_handler import event_stream, show_progress, hide_progress @@ -24,7 +24,6 @@ def update_series(send_event=True): if apikey_sonarr is None: return - sonarr_version = get_sonarr_version() serie_default_enabled = settings.general.getboolean('serie_default_enabled') if serie_default_enabled is True: @@ -38,8 +37,7 @@ def update_series(send_event=True): tagsDict = get_tags() # Get shows data from Sonarr - series = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr, - sonarr_version=sonarr_version) + series = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr) if not series: return else: @@ -65,12 +63,12 @@ def update_series(send_event=True): current_shows_sonarr.append(show['id']) if show['id'] in current_shows_db_list: - series_to_update.append(seriesParser(show, action='update', sonarr_version=sonarr_version, - tags_dict=tagsDict, serie_default_profile=serie_default_profile, + series_to_update.append(seriesParser(show, action='update', tags_dict=tagsDict, + serie_default_profile=serie_default_profile, audio_profiles=audio_profiles)) else: - series_to_add.append(seriesParser(show, action='insert', sonarr_version=sonarr_version, - tags_dict=tagsDict, serie_default_profile=serie_default_profile, + series_to_add.append(seriesParser(show, action='insert', tags_dict=tagsDict, + serie_default_profile=serie_default_profile, audio_profiles=audio_profiles)) if send_event: @@ -137,7 +135,7 @@ def update_series(send_event=True): def update_one_series(series_id, action): - logging.debug('BAZARR syncing this specific series from RSonarr: {}'.format(series_id)) + logging.debug('BAZARR syncing this specific series from Sonarr: {}'.format(series_id)) # Check if there's a row in database for this series ID try: @@ -155,7 +153,6 @@ def update_one_series(series_id, action): event_stream(type='series', action='delete', payload=int(series_id)) return - sonarr_version = get_sonarr_version() serie_default_enabled = settings.general.getboolean('serie_default_enabled') if serie_default_enabled is True: @@ -173,18 +170,19 @@ def update_one_series(series_id, action): series = None series_data = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=settings.sonarr.apikey, - sonarr_series_id=int(series_id), sonarr_version=get_sonarr_version()) + sonarr_series_id=int(series_id), + sonarr_version=get_sonarr_info.version()) if not series_data: return else: if action == 'updated' and existing_series: - series = seriesParser(series_data, action='update', sonarr_version=sonarr_version, - tags_dict=tagsDict, serie_default_profile=serie_default_profile, + series = seriesParser(series_data, action='update', tags_dict=tagsDict, + serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) elif action == 'updated' and not existing_series: - series = seriesParser(series_data, action='insert', sonarr_version=sonarr_version, - tags_dict=tagsDict, serie_default_profile=serie_default_profile, + series = seriesParser(series_data, action='insert', tags_dict=tagsDict, + serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) except Exception: logging.debug('BAZARR cannot parse series returned by SignalR feed.') @@ -208,11 +206,10 @@ def update_one_series(series_id, action): def get_profile_list(): apikey_sonarr = settings.sonarr.apikey - sonarr_version = get_sonarr_version() profiles_list = [] # Get profiles data from Sonarr - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr else: url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr @@ -230,7 +227,7 @@ def get_profile_list(): return None # Parsing data returned from Sonarr - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): for profile in profiles_json.json(): profiles_list.append([profile['id'], profile['language'].capitalize()]) else: @@ -253,7 +250,7 @@ def get_tags(): tagsDict = [] # Get tags data from Sonarr - if get_sonarr_version().startswith('2'): + if get_sonarr_info.is_legacy(): url_sonarr_api_series = url_sonarr() + "/api/tag?apikey=" + apikey_sonarr else: url_sonarr_api_series = url_sonarr() + "/api/v3/tag?apikey=" + apikey_sonarr @@ -273,7 +270,7 @@ def get_tags(): return tagsDict.json() -def seriesParser(show, action, sonarr_version, tags_dict, serie_default_profile, audio_profiles): +def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles): overview = show['overview'] if 'overview' in show else '' poster = '' fanart = '' @@ -290,7 +287,7 @@ def seriesParser(show, action, sonarr_version, tags_dict, serie_default_profile, alternate_titles = str([item['title'] for item in show['alternateTitles']]) audio_language = [] - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles) else: audio_language = profile_id_to_language(show['languageProfileId'], audio_profiles) @@ -332,9 +329,9 @@ def seriesParser(show, action, sonarr_version, tags_dict, serie_default_profile, 'profileId': serie_default_profile} -def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_version, sonarr_series_id=None): +def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_series_id=None): url_sonarr_api_series = url + "/api/{0}series/{1}?apikey={2}".format( - '' if sonarr_version.startswith('2') else 'v3/', sonarr_series_id if sonarr_series_id else "", apikey_sonarr) + '' if get_sonarr_info.is_legacy() else 'v3/', sonarr_series_id if sonarr_series_id else "", apikey_sonarr) try: r = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) r.raise_for_status() diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 13011b4b1..07363606d 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -806,7 +806,7 @@ def series_download_subtitles(no): hide_progress(id='series_search_progress_{}'.format(no)) -def episode_download_subtitles(no): +def episode_download_subtitles(no, send_progress=False): conditions = [(TableEpisodes.sonarrEpisodeId == no)] conditions += get_exclusion_clause('series') episodes_details = TableEpisodes.select(TableEpisodes.path, @@ -818,7 +818,10 @@ def episode_download_subtitles(no): TableShows.title, TableShows.sonarrSeriesId, TableEpisodes.audio_language, - TableShows.seriesType)\ + TableShows.seriesType, + TableEpisodes.title.alias('episodeTitle'), + TableEpisodes.season, + TableEpisodes.episode)\ .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ .where(reduce(operator.and_, conditions))\ .dicts() @@ -831,6 +834,15 @@ def episode_download_subtitles(no): for episode in episodes_details: if providers_list: + if send_progress: + show_progress(id='episode_search_progress_{}'.format(no), + header='Searching missing subtitles...', + name='{0} - S{1:02d}E{2:02d} - {3}'.format(episode['title'], + episode['season'], + episode['episode'], + episode['episodeTitle']), + value=1, + count=1) for language in ast.literal_eval(episode['missing_subtitles']): # confirm if language is still missing or if cutoff have been reached confirmed_missing_subs = TableEpisodes.select(TableEpisodes.missing_subtitles) \ @@ -875,6 +887,8 @@ def episode_download_subtitles(no): history_log(1, episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message, path, language_code, provider, score, subs_id, subs_path) send_notifications(episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message) + if send_progress: + hide_progress(id='episode_search_progress_{}'.format(no)) else: logging.info("BAZARR All providers are throttled") break diff --git a/bazarr/init.py b/bazarr/init.py index 2ecc1902e..928c44f3d 100644 --- a/bazarr/init.py +++ b/bazarr/init.py @@ -16,7 +16,7 @@ import subliminal import datetime # set subliminal_patch user agent -os.environ["SZ_USER_AGENT"] = "Bazarr/1" +os.environ["SZ_USER_AGENT"] = "Bazarr/{}".format(os.environ["BAZARR_VERSION"]) # set anti-captcha provider and key configure_captcha_func() @@ -42,6 +42,13 @@ if not os.path.exists(os.path.join(args.config_dir, 'cache')): configure_logging(settings.general.getboolean('debug') or args.debug) import logging + +def is_virtualenv(): + # return True if Bazarr have been start from within a virtualenv or venv + base_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix + return base_prefix != sys.prefix + + # deploy requirements.txt if not args.no_update: try: @@ -57,10 +64,12 @@ if not args.no_update: else: logging.info('BAZARR installing requirements...') try: - subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--user', '-qq', - '--disable-pip-version-check', '--no-color', '-r', - os.path.join(os.path.dirname(__file__), '..', 'requirements.txt')], - stderr=subprocess.STDOUT) + pip_command = [sys.executable, '-m', 'pip', 'install', '-qq', '--disable-pip-version-check', + '-r', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'requirements.txt')] + if not is_virtualenv(): + # --user only make sense if not running under venv + pip_command.insert(4, '--user') + subprocess.check_output(pip_command, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: logging.exception('BAZARR requirements.txt installation result: {}'.format(e.stdout)) os._exit(1) diff --git a/bazarr/main.py b/bazarr/main.py index 71632d97e..c579c4979 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -2,7 +2,7 @@ # Gevent monkey patch if gevent available. If not, it will be installed on during the init process. try: - from gevent import monkey, Greenlet + from gevent import monkey, Greenlet, joinall except ImportError: pass else: @@ -43,7 +43,7 @@ from signalr_client import sonarr_signalr_client, radarr_signalr_client from check_update import apply_update, check_if_new_update, check_releases from server import app, webserver from functools import wraps -from utils import check_credentials, get_sonarr_version, get_radarr_version +from utils import check_credentials, get_sonarr_info, get_radarr_info # Install downloaded update if bazarr_version != '': @@ -131,8 +131,7 @@ def series_images(url): url = url.strip("/") apikey = settings.sonarr.apikey baseUrl = settings.sonarr.base_url - sonarr_version = get_sonarr_version() - if sonarr_version.startswith('2'): + if get_sonarr_info.is_legacy(): url_image = (url_sonarr() + '/api/' + url.lstrip(baseUrl) + '?apikey=' + apikey).replace('poster-250', 'poster-500') else: @@ -151,8 +150,7 @@ def series_images(url): def movies_images(url): apikey = settings.radarr.apikey baseUrl = settings.radarr.base_url - radarr_version = get_radarr_version() - if radarr_version.startswith('0'): + if get_radarr_info.is_legacy(): url_image = url_radarr() + '/api/' + url.lstrip(baseUrl) + '?apikey=' + apikey else: url_image = url_radarr() + '/api/v3/' + url.lstrip(baseUrl) + '?apikey=' + apikey @@ -204,10 +202,12 @@ def proxy(protocol, url): return dict(status=False, error=result.raise_for_status()) +greenlets = [] if settings.general.getboolean('use_sonarr'): - Greenlet.spawn(sonarr_signalr_client.start) + greenlets.append(Greenlet.spawn(sonarr_signalr_client.start)) if settings.general.getboolean('use_radarr'): - Greenlet.spawn(radarr_signalr_client.start) + greenlets.append(Greenlet.spawn(radarr_signalr_client.start)) +joinall(greenlets) if __name__ == "__main__": diff --git a/bazarr/signalr_client.py b/bazarr/signalr_client.py index 941d8bd9f..9968bacfb 100644 --- a/bazarr/signalr_client.py +++ b/bazarr/signalr_client.py @@ -15,7 +15,7 @@ from get_episodes import sync_episodes, sync_one_episode from get_series import update_series, update_one_series from get_movies import update_movies, update_one_movie from scheduler import scheduler -from utils import get_sonarr_version +from utils import get_sonarr_info from get_args import args @@ -33,26 +33,31 @@ class SonarrSignalrClient: self.connection = None def start(self): - if get_sonarr_version().startswith('2.'): + if get_sonarr_info.is_legacy(): logging.warning('BAZARR can only sync from Sonarr v3 SignalR feed to get real-time update. You should ' - 'consider upgrading.') - return - - logging.info('BAZARR trying to connect to Sonarr SignalR feed...') - self.configure() - while not self.connection.started: - try: - self.connection.start() - except ConnectionError: - gevent.sleep(5) - except json.decoder.JSONDecodeError: - logging.error("BAZARR cannot parse JSON returned by SignalR feed. This is a known issue when Sonarr " - "doesn't have write permission to it's /config/xdg directory.") - self.stop() - logging.info('BAZARR SignalR client for Sonarr is connected and waiting for events.') - if not args.dev: - scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1) - scheduler.add_job(sync_episodes, kwargs={'send_event': True}, max_instances=1) + 'consider upgrading your version({}).'.format(get_sonarr_info.version())) + raise gevent.GreenletExit + else: + logging.info('BAZARR trying to connect to Sonarr SignalR feed...') + self.configure() + while not self.connection.started: + try: + self.connection.start() + except ConnectionError: + gevent.sleep(5) + except json.decoder.JSONDecodeError: + logging.error("BAZARR cannot parse JSON returned by SignalR feed. This is caused by a permissions " + "issue when Sonarr try to access its /config/.config directory. You should fix " + "permissions on that directory and restart Sonarr. Also, if you're a Docker image " + "user, you should make sure you properly defined PUID/PGID environment variables. " + "Otherwise, please contact Sonarr support.") + raise gevent.GreenletExit + else: + logging.info('BAZARR SignalR client for Sonarr is connected and waiting for events.') + finally: + if not args.dev: + scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1) + scheduler.add_job(sync_episodes, kwargs={'send_event': True}, max_instances=1) def stop(self, log=True): try: diff --git a/bazarr/subsyncer.py b/bazarr/subsyncer.py index daeff09e2..4a523c18d 100644 --- a/bazarr/subsyncer.py +++ b/bazarr/subsyncer.py @@ -58,6 +58,8 @@ class SubSyncer: logging.exception('BAZARR an exception occurs during the synchronization process for this subtitles: ' '{0}'.format(self.srtin)) else: + if settings.subsync.getboolean('debug'): + return result if os.path.isfile(self.srtout): if not settings.subsync.getboolean('debug'): os.remove(self.srtin) diff --git a/bazarr/utils.py b/bazarr/utils.py index 88e55ed84..0a66b5a0c 100644 --- a/bazarr/utils.py +++ b/bazarr/utils.py @@ -236,44 +236,53 @@ def cache_maintenance(): remove_expired(fn, pack_cache_validity) -def get_sonarr_version(): - sonarr_version = '' - if settings.general.getboolean('use_sonarr'): - try: - sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey - sonarr_json = requests.get(sv, timeout=60, verify=False, headers=headers).json() - if 'version' in sonarr_json: - sonarr_version = sonarr_json['version'] - else: - sv = url_sonarr() + "/api/v3/system/status?apikey=" + settings.sonarr.apikey - sonarr_version = requests.get(sv, timeout=60, verify=False, headers=headers).json()['version'] - except Exception: - logging.debug('BAZARR cannot get Sonarr version') - sonarr_version = 'unknown' - return sonarr_version - - -def get_sonarr_platform(): - sonarr_platform = '' - if settings.general.getboolean('use_sonarr'): - try: - if get_sonarr_version().startswith('2'): +class GetSonarrInfo: + @staticmethod + def version(): + """ + Call system/status API endpoint and get the Sonarr version + @return: str + """ + sonarr_version = region.get("sonarr_version", expiration_time=datetime.timedelta(seconds=60).total_seconds()) + if sonarr_version: + region.set("sonarr_version", sonarr_version) + return sonarr_version + else: + sonarr_version = '' + if settings.general.getboolean('use_sonarr'): + try: sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey - else: - sv = url_sonarr() + "/api/v3/system/status?apikey=" + settings.sonarr.apikey - response = requests.get(sv, timeout=60, verify=False, headers=headers).json() - if response['isLinux'] or response['isOsx']: - sonarr_platform = 'posix' - elif response['isWindows']: - sonarr_platform = 'nt' - except Exception: - logging.debug('BAZARR cannot get Sonarr platform') - return sonarr_platform + sonarr_json = requests.get(sv, timeout=60, verify=False, headers=headers).json() + if 'version' in sonarr_json: + sonarr_version = sonarr_json['version'] + else: + sv = url_sonarr() + "/api/v3/system/status?apikey=" + settings.sonarr.apikey + sonarr_version = requests.get(sv, timeout=60, verify=False, headers=headers).json()['version'] + except Exception: + logging.debug('BAZARR cannot get Sonarr version') + sonarr_version = 'unknown' + logging.debug('BAZARR got this Sonarr version from its API: {}'.format(sonarr_version)) + region.set("sonarr_version", sonarr_version) + return sonarr_version + + def is_legacy(self): + """ + Call self.version() and parse the result to determine if it's a legacy version of Sonarr API + @return: bool + """ + sonarr_version = self.version() + if sonarr_version.startswith(('0.', '2.')): + return True + else: + return False + + +get_sonarr_info = GetSonarrInfo() def notify_sonarr(sonarr_series_id): try: - if get_sonarr_version().startswith('2'): + if get_sonarr_info.is_legacy(): url = url_sonarr() + "/api/command?apikey=" + settings.sonarr.apikey else: url = url_sonarr() + "/api/v3/command?apikey=" + settings.sonarr.apikey @@ -283,47 +292,56 @@ def notify_sonarr(sonarr_series_id): } requests.post(url, json=data, timeout=60, verify=False, headers=headers) except Exception as e: - logging.debug('BAZARR notify Sonarr') + logging.exception('BAZARR cannot notify Sonarr') -def get_radarr_version(): - radarr_version = '' - if settings.general.getboolean('use_radarr'): - try: - rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey - radarr_json = requests.get(rv, timeout=60, verify=False, headers=headers).json() - if 'version' in radarr_json: - radarr_version = radarr_json['version'] - else: - rv = url_radarr() + "/api/v3/system/status?apikey=" + settings.radarr.apikey - radarr_version = requests.get(rv, timeout=60, verify=False, headers=headers).json()['version'] - except Exception as e: - logging.debug('BAZARR cannot get Radarr version') - radarr_version = 'unknown' - return radarr_version - - -def get_radarr_platform(): - radarr_platform = '' - if settings.general.getboolean('use_radarr'): - try: - if get_radarr_version().startswith('0'): +class GetRadarrInfo: + @staticmethod + def version(): + """ + Call system/status API endpoint and get the Radarr version + @return: str + """ + radarr_version = region.get("radarr_version", expiration_time=datetime.timedelta(seconds=60).total_seconds()) + if radarr_version: + region.set("radarr_version", radarr_version) + return radarr_version + else: + radarr_version = '' + if settings.general.getboolean('use_radarr'): + try: rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey - else: - rv = url_radarr() + "/api/v3/system/status?apikey=" + settings.radarr.apikey - response = requests.get(rv, timeout=60, verify=False, headers=headers).json() - if response['isLinux'] or response['isOsx']: - radarr_platform = 'posix' - elif response['isWindows']: - radarr_platform = 'nt' - except Exception: - logging.debug('BAZARR cannot get Radarr platform') - return radarr_platform + radarr_json = requests.get(rv, timeout=60, verify=False, headers=headers).json() + if 'version' in radarr_json: + radarr_version = radarr_json['version'] + else: + rv = url_radarr() + "/api/v3/system/status?apikey=" + settings.radarr.apikey + radarr_version = requests.get(rv, timeout=60, verify=False, headers=headers).json()['version'] + except Exception as e: + logging.debug('BAZARR cannot get Radarr version') + radarr_version = 'unknown' + logging.debug('BAZARR got this Radarr version from its API: {}'.format(radarr_version)) + region.set("radarr_version", radarr_version) + return radarr_version + + def is_legacy(self): + """ + Call self.version() and parse the result to determine if it's a legacy version of Radarr + @return: bool + """ + radarr_version = self.version() + if radarr_version.startswith('0.'): + return True + else: + return False + + +get_radarr_info = GetRadarrInfo() def notify_radarr(radarr_id): try: - if get_radarr_version().startswith('0'): + if get_radarr_info.is_legacy(): url = url_radarr() + "/api/command?apikey=" + settings.radarr.apikey else: url = url_radarr() + "/api/v3/command?apikey=" + settings.radarr.apikey @@ -333,7 +351,7 @@ def notify_radarr(radarr_id): } requests.post(url, json=data, timeout=60, verify=False, headers=headers) except Exception as e: - logging.debug('BAZARR notify Radarr') + logging.exception('BAZARR cannot notify Radarr') def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_path, sonarr_series_id=None, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 39c1a34cb..f5c7cbb10 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "bazarr", - "version": "1", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bazarr", - "version": "1", + "version": "1.0.0", "license": "GPL-3", "dependencies": { "@fontsource/roboto": "^4.2.2", @@ -19,9 +19,8 @@ "@types/lodash": "^4", "@types/node": "^15", "@types/react": "^16", - "@types/react-dom": "^16", + "@types/react-dom": "^17", "@types/react-helmet": "^6.1", - "@types/react-redux": "^7", "@types/react-router-dom": "^5", "@types/react-select": "^4.0.3", "@types/react-table": "^7", @@ -33,9 +32,9 @@ "http-proxy-middleware": "^0.19", "lodash": "^4", "rc-slider": "^9.7", - "react": "^16", + "react": "^17", "react-bootstrap": "^1", - "react-dom": "^16", + "react-dom": "^17", "react-helmet": "^6.1", "react-redux": "^7.2", "react-router-dom": "^5.2", @@ -2969,11 +2968,11 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.12", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.12.tgz", - "integrity": "sha512-i7NPZZpPte3jtVOoW+eLB7G/jsX5OM6GqQnH+lC0nq0rqwlK0x8WcMEvYDgFWqWhWMlTltTimzdMax6wYfZssA==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", + "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", "dependencies": { - "@types/react": "^16" + "@types/react": "*" } }, "node_modules/@types/react-helmet": { @@ -2985,9 +2984,9 @@ } }, "node_modules/@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -7071,9 +7070,9 @@ } }, "node_modules/engine.io-client": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz", - "integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.2.tgz", + "integrity": "sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==", "dependencies": { "base64-arraybuffer": "0.1.4", "component-emitter": "~1.3.0", @@ -15776,13 +15775,12 @@ } }, "node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -16037,17 +16035,16 @@ } }, "node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.20.2" }, "peerDependencies": { - "react": "^16.14.0" + "react": "17.0.2" } }, "node_modules/react-error-overlay": { @@ -17711,9 +17708,9 @@ } }, "node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18223,15 +18220,15 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "node_modules/socket.io-client": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz", - "integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.3.tgz", + "integrity": "sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==", "dependencies": { "@types/component-emitter": "^1.2.10", "backo2": "~1.0.2", "component-emitter": "~1.3.0", "debug": "~4.3.1", - "engine.io-client": "~5.0.0", + "engine.io-client": "~5.1.2", "parseuri": "0.0.6", "socket.io-parser": "~4.0.4" }, @@ -24411,11 +24408,11 @@ } }, "@types/react-dom": { - "version": "16.9.12", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.12.tgz", - "integrity": "sha512-i7NPZZpPte3jtVOoW+eLB7G/jsX5OM6GqQnH+lC0nq0rqwlK0x8WcMEvYDgFWqWhWMlTltTimzdMax6wYfZssA==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", + "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", "requires": { - "@types/react": "^16" + "@types/react": "*" } }, "@types/react-helmet": { @@ -24427,9 +24424,9 @@ } }, "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", + "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==", "requires": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -27764,9 +27761,9 @@ } }, "engine.io-client": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz", - "integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.2.tgz", + "integrity": "sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==", "requires": { "base64-arraybuffer": "0.1.4", "component-emitter": "~1.3.0", @@ -34534,13 +34531,12 @@ } }, "react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" } }, "react-app-polyfill": { @@ -34742,14 +34738,13 @@ } }, "react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.20.2" } }, "react-error-overlay": { @@ -36048,9 +36043,9 @@ } }, "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -36477,15 +36472,15 @@ } }, "socket.io-client": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz", - "integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.3.tgz", + "integrity": "sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==", "requires": { "@types/component-emitter": "^1.2.10", "backo2": "~1.0.2", "component-emitter": "~1.3.0", "debug": "~4.3.1", - "engine.io-client": "~5.0.0", + "engine.io-client": "~5.1.2", "parseuri": "0.0.6", "socket.io-parser": "~4.0.4" } diff --git a/frontend/package.json b/frontend/package.json index 1b23a3647..11cda9df8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bazarr", - "version": "1", + "version": "1.0.0", "description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you.", "repository": { "type": "git", @@ -23,9 +23,8 @@ "@types/lodash": "^4", "@types/node": "^15", "@types/react": "^16", - "@types/react-dom": "^16", + "@types/react-dom": "^17", "@types/react-helmet": "^6.1", - "@types/react-redux": "^7", "@types/react-router-dom": "^5", "@types/react-select": "^4.0.3", "@types/react-table": "^7", @@ -37,9 +36,9 @@ "http-proxy-middleware": "^0.19", "lodash": "^4", "rc-slider": "^9.7", - "react": "^16", + "react": "^17", "react-bootstrap": "^1", - "react-dom": "^16", + "react-dom": "^17", "react-helmet": "^6.1", "react-redux": "^7.2", "react-router-dom": "^5.2", diff --git a/frontend/src/@redux/hooks/index.ts b/frontend/src/@redux/hooks/index.ts index 364871a4f..ac15d5b21 100644 --- a/frontend/src/@redux/hooks/index.ts +++ b/frontend/src/@redux/hooks/index.ts @@ -5,13 +5,11 @@ import { episodeDeleteItems, episodeUpdateBy, episodeUpdateById, - movieDeleteWantedItems, movieUpdateBlacklist, movieUpdateHistoryList, movieUpdateList, movieUpdateWantedList, providerUpdateList, - seriesDeleteWantedItems, seriesUpdateBlacklist, seriesUpdateHistoryList, seriesUpdateList, @@ -324,18 +322,6 @@ export function useWantedSeries() { const update = useReduxAction(seriesUpdateWantedList); const items = useReduxStore((d) => d.series.wantedEpisodesList); - const updateAction = useWrapToOptionalId(update); - const deleteAction = useReduxAction(seriesDeleteWantedItems); - const reducer = useMemo( - () => ({ - key: "episode-wanted", - update: updateAction, - delete: deleteAction, - }), - [updateAction, deleteAction] - ); - useSocketIOReducer(reducer); - return stateBuilder(items, update); } @@ -343,18 +329,6 @@ export function useWantedMovies() { const update = useReduxAction(movieUpdateWantedList); const items = useReduxStore((d) => d.movie.wantedMovieList); - const updateAction = useWrapToOptionalId(update); - const deleteAction = useReduxAction(movieDeleteWantedItems); - const reducer = useMemo( - () => ({ - key: "movie-wanted", - update: updateAction, - delete: deleteAction, - }), - [updateAction, deleteAction] - ); - useSocketIOReducer(reducer); - return stateBuilder(items, update); } diff --git a/frontend/src/@redux/utils/index.ts b/frontend/src/@redux/utils/index.ts index 61340a902..e0e063f0d 100644 --- a/frontend/src/@redux/utils/index.ts +++ b/frontend/src/@redux/utils/index.ts @@ -4,7 +4,7 @@ export function defaultAOS(): AsyncOrderState { data: { items: [], order: [], - fetched: false, + dirty: false, }, }; } diff --git a/frontend/src/@redux/utils/mapper.ts b/frontend/src/@redux/utils/mapper.ts index 32c25b8aa..772caa39a 100644 --- a/frontend/src/@redux/utils/mapper.ts +++ b/frontend/src/@redux/utils/mapper.ts @@ -36,7 +36,7 @@ export function updateOrderIdState( return { data: { ...state.data, - fetched: true, + dirty: true, }, updating: true, }; @@ -44,7 +44,7 @@ export function updateOrderIdState( return { data: { ...state.data, - fetched: true, + dirty: true, }, updating: false, error: action.payload.item as Error, @@ -107,7 +107,7 @@ export function updateOrderIdState( return { updating: false, data: { - fetched: true, + dirty: true, items: newItems, order: newOrder, }, @@ -131,7 +131,7 @@ export function deleteOrderListItemBy( return { ...state, data: { - fetched: true, + dirty: true, items: newItems, order: newOrder, }, diff --git a/frontend/src/@socketio/reducer.ts b/frontend/src/@socketio/reducer.ts index 4b39a5eb2..5ea5fba94 100644 --- a/frontend/src/@socketio/reducer.ts +++ b/frontend/src/@socketio/reducer.ts @@ -3,9 +3,13 @@ import { badgeUpdateAll, bootstrap, movieDeleteItems, + movieDeleteWantedItems, movieUpdateList, + movieUpdateWantedList, seriesDeleteItems, + seriesDeleteWantedItems, seriesUpdateList, + seriesUpdateWantedList, siteAddNotifications, siteAddProgress, siteInitializationFailed, @@ -91,6 +95,24 @@ export function createDefaultReducer(): SocketIO.Reducer[] { update: bindToReduxStore(movieUpdateList), delete: bindToReduxStore(movieDeleteItems), }, + { + key: "episode-wanted", + update: (ids: number[] | undefined) => { + if (ids) { + reduxStore.dispatch(seriesUpdateWantedList(ids) as any); + } + }, + delete: bindToReduxStore(seriesDeleteWantedItems), + }, + { + key: "movie-wanted", + update: (ids: number[] | undefined) => { + if (ids) { + reduxStore.dispatch(movieUpdateWantedList(ids) as any); + } + }, + delete: bindToReduxStore(movieDeleteWantedItems), + }, { key: "settings", any: bindToReduxStore(systemUpdateSettings), diff --git a/frontend/src/@types/basic.d.ts b/frontend/src/@types/basic.d.ts index c0fe42c3c..11ae9c5de 100644 --- a/frontend/src/@types/basic.d.ts +++ b/frontend/src/@types/basic.d.ts @@ -14,7 +14,7 @@ type StorageType = string | null; interface OrderIdState { items: IdState; order: (number | null)[]; - fetched: boolean; + dirty: boolean; } interface AsyncState { diff --git a/frontend/src/@types/settings.d.ts b/frontend/src/@types/settings.d.ts index bc93bd607..6a6e326d7 100644 --- a/frontend/src/@types/settings.d.ts +++ b/frontend/src/@types/settings.d.ts @@ -181,7 +181,9 @@ namespace Settings { skip_wrong_fps: boolean; } - interface Legendastv extends BaseProvider {} + interface Legendastv extends BaseProvider { + featured_only: boolean; + } interface XSubs extends BaseProvider {} diff --git a/frontend/src/Settings/Providers/list.ts b/frontend/src/Settings/Providers/list.ts index 2d3357687..29d359741 100644 --- a/frontend/src/Settings/Providers/list.ts +++ b/frontend/src/Settings/Providers/list.ts @@ -77,6 +77,10 @@ export const ProviderList: Readonly = [ defaultKey: { username: "", password: "", + featured_only: false, + }, + keyNameOverride: { + featured_only: "Only Download Featured", }, }, { key: "napiprojekt", description: "Polish Subtitles Provider" }, diff --git a/frontend/src/components/tables/AsyncPageTable.tsx b/frontend/src/components/tables/AsyncPageTable.tsx index ddd9469c3..54bb969c9 100644 --- a/frontend/src/components/tables/AsyncPageTable.tsx +++ b/frontend/src/components/tables/AsyncPageTable.tsx @@ -21,7 +21,7 @@ export default function AsyncPageTable(props: Props) { const { updating, - data: { order, items, fetched }, + data: { order, items, dirty }, } = aos; const allPlugins: PluginHook[] = [useDefaultSettings]; @@ -85,12 +85,12 @@ export default function AsyncPageTable(props: Props) { }, [pageIndex]); useEffect(() => { - const needInit = visibleItemIds.length === 0 && fetched === false; + const needFetch = visibleItemIds.length === 0 && dirty === false; const needRefresh = !visibleItemIds.every(isNonNullable); - if (needInit || needRefresh) { + if (needFetch || needRefresh) { loader(pageStart, pageSize); } - }, [visibleItemIds, pageStart, pageSize, loader, fetched]); + }, [visibleItemIds, pageStart, pageSize, loader, dirty]); const showLoading = useMemo( () => diff --git a/libs/subliminal_patch/providers/greeksubs.py b/libs/subliminal_patch/providers/greeksubs.py index ddd7f9d80..bec0749c6 100644 --- a/libs/subliminal_patch/providers/greeksubs.py +++ b/libs/subliminal_patch/providers/greeksubs.py @@ -75,12 +75,12 @@ class GreekSubsProvider(Provider): r = self.session.get(search_link, timeout=30) # 404 is returned if the imdb_id was not found - if r.status_code != 404: - r.raise_for_status() + if r.status_code == 404: + logger.debug('IMDB id {} not found on greeksubs'.format(imdb_id)) + return subtitles if r.status_code != 200: - logger.debug('No subtitles found') - return subtitles + r.raise_for_status() soup_page = ParserBeautifulSoup(r.content.decode('utf-8', 'ignore'), ['html.parser']) diff --git a/libs/subliminal_patch/providers/legendastv.py b/libs/subliminal_patch/providers/legendastv.py index 97a76a4b1..43db667a7 100644 --- a/libs/subliminal_patch/providers/legendastv.py +++ b/libs/subliminal_patch/providers/legendastv.py @@ -70,7 +70,7 @@ class LegendasTVProvider(_LegendasTVProvider): languages = {Language(*l) for l in language_converters['legendastv'].to_legendastv.keys()} subtitle_class = LegendasTVSubtitle - def __init__(self, username=None, password=None): + def __init__(self, username=None, password=None, featured_only=False): # Provider needs UNRAR installed. If not available raise ConfigurationError try: @@ -85,6 +85,7 @@ class LegendasTVProvider(_LegendasTVProvider): self.password = password self.logged_in = False self.session = None + self.featured_only = featured_only @staticmethod def is_valid_title(title, title_id, sanitized_title, season, year, imdb_id): @@ -209,6 +210,11 @@ class LegendasTVProvider(_LegendasTVProvider): # iterate over title's archives for a in archives: + # Check if featured + if self.featured_only and a.featured == False: + logger.info('Subtitle is not featured, skipping') + continue + # compute an expiration time based on the archive timestamp expiration_time = (datetime.utcnow().replace(tzinfo=pytz.utc) - a.timestamp).total_seconds() diff --git a/libs/subliminal_patch/providers/opensubtitlescom.py b/libs/subliminal_patch/providers/opensubtitlescom.py index fd4bb6684..7d04a6026 100644 --- a/libs/subliminal_patch/providers/opensubtitlescom.py +++ b/libs/subliminal_patch/providers/opensubtitlescom.py @@ -124,6 +124,9 @@ class OpenSubtitlesComProvider(ProviderRetryMixin, Provider): languages.update(set(Language.rebuild(l, forced=True) for l in languages)) def __init__(self, username=None, password=None, use_hash=True, api_key=None): + if not all((username, password)): + raise ConfigurationError('Username and password must be specified') + if not api_key: raise ConfigurationError('Api_key must be specified') diff --git a/libs/subliminal_patch/providers/titulky.py b/libs/subliminal_patch/providers/titulky.py index 66d6f8c7f..0639a042f 100644 --- a/libs/subliminal_patch/providers/titulky.py +++ b/libs/subliminal_patch/providers/titulky.py @@ -152,12 +152,12 @@ class TitulkyProvider(Provider): """Titulky Provider.""" languages = {Language(l) for l in ['ces', 'slk']} - server_url = 'https://premium.titulky.com' + server_url = 'https://oldpremium.titulky.com' sign_out_url = '?Logoff=true' search_url_series = '?Fulltext={}' search_url_movies = '?Searching=AdvancedResult&ARelease={}' - dn_url = 'https://premium.titulky.com' - download_url = 'https://premium.titulky.com/idown.php?titulky=' + dn_url = 'https://oldpremium.titulky.com' + download_url = 'https://oldpremium.titulky.com/idown.php?titulky=' UserAgent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'