From 204a1c3f3192dcac6688e4a48ea8dd814ffccefe Mon Sep 17 00:00:00 2001 From: morpheus65535 Date: Fri, 3 Dec 2021 22:49:20 -0500 Subject: [PATCH] Cleaned up and split API to make it easier to maintain. --- bazarr/api.py | 2192 -------------------- bazarr/api/__init__.py | 25 + bazarr/api/badges/__init__.py | 12 + bazarr/api/badges/badges.py | 47 + bazarr/api/episodes/__init__.py | 20 + bazarr/api/episodes/blacklist.py | 92 + bazarr/api/episodes/episodes.py | 30 + bazarr/api/episodes/episodes_subtitles.py | 177 ++ bazarr/api/episodes/history.py | 133 ++ bazarr/api/episodes/wanted.py | 74 + bazarr/api/files/__init__.py | 16 + bazarr/api/files/files.py | 24 + bazarr/api/files/files_radarr.py | 24 + bazarr/api/files/files_sonarr.py | 24 + bazarr/api/history/__init__.py | 12 + bazarr/api/history/stats.py | 85 + bazarr/api/movies/__init__.py | 20 + bazarr/api/movies/blacklist.py | 86 + bazarr/api/movies/history.py | 129 ++ bazarr/api/movies/movies.py | 80 + bazarr/api/movies/movies_subtitles.py | 175 ++ bazarr/api/movies/wanted.py | 62 + bazarr/api/providers/__init__.py | 16 + bazarr/api/providers/providers.py | 52 + bazarr/api/providers/providers_episodes.py | 103 + bazarr/api/providers/providers_movies.py | 102 + bazarr/api/series/__init__.py | 12 + bazarr/api/series/series.py | 114 + bazarr/api/subtitles/__init__.py | 14 + bazarr/api/subtitles/subtitles.py | 72 + bazarr/api/subtitles/subtitles_info.py | 41 + bazarr/api/system/__init__.py | 33 + bazarr/api/system/account.py | 29 + bazarr/api/system/health.py | 13 + bazarr/api/system/languages.py | 54 + bazarr/api/system/languages_profiles.py | 13 + bazarr/api/system/logs.py | 41 + bazarr/api/system/notifications.py | 27 + bazarr/api/system/releases.py | 47 + bazarr/api/system/searches.py | 40 + bazarr/api/system/settings.py | 102 + bazarr/api/system/status.py | 27 + bazarr/api/system/system.py | 18 + bazarr/api/system/tasks.py | 31 + bazarr/api/utils.py | 239 +++ bazarr/api/webhooks/__init__.py | 12 + bazarr/api/webhooks/plex.py | 76 + bazarr/server.py | 5 +- 48 files changed, 2678 insertions(+), 2194 deletions(-) delete mode 100644 bazarr/api.py create mode 100644 bazarr/api/__init__.py create mode 100644 bazarr/api/badges/__init__.py create mode 100644 bazarr/api/badges/badges.py create mode 100644 bazarr/api/episodes/__init__.py create mode 100644 bazarr/api/episodes/blacklist.py create mode 100644 bazarr/api/episodes/episodes.py create mode 100644 bazarr/api/episodes/episodes_subtitles.py create mode 100644 bazarr/api/episodes/history.py create mode 100644 bazarr/api/episodes/wanted.py create mode 100644 bazarr/api/files/__init__.py create mode 100644 bazarr/api/files/files.py create mode 100644 bazarr/api/files/files_radarr.py create mode 100644 bazarr/api/files/files_sonarr.py create mode 100644 bazarr/api/history/__init__.py create mode 100644 bazarr/api/history/stats.py create mode 100644 bazarr/api/movies/__init__.py create mode 100644 bazarr/api/movies/blacklist.py create mode 100644 bazarr/api/movies/history.py create mode 100644 bazarr/api/movies/movies.py create mode 100644 bazarr/api/movies/movies_subtitles.py create mode 100644 bazarr/api/movies/wanted.py create mode 100644 bazarr/api/providers/__init__.py create mode 100644 bazarr/api/providers/providers.py create mode 100644 bazarr/api/providers/providers_episodes.py create mode 100644 bazarr/api/providers/providers_movies.py create mode 100644 bazarr/api/series/__init__.py create mode 100644 bazarr/api/series/series.py create mode 100644 bazarr/api/subtitles/__init__.py create mode 100644 bazarr/api/subtitles/subtitles.py create mode 100644 bazarr/api/subtitles/subtitles_info.py create mode 100644 bazarr/api/system/__init__.py create mode 100644 bazarr/api/system/account.py create mode 100644 bazarr/api/system/health.py create mode 100644 bazarr/api/system/languages.py create mode 100644 bazarr/api/system/languages_profiles.py create mode 100644 bazarr/api/system/logs.py create mode 100644 bazarr/api/system/notifications.py create mode 100644 bazarr/api/system/releases.py create mode 100644 bazarr/api/system/searches.py create mode 100644 bazarr/api/system/settings.py create mode 100644 bazarr/api/system/status.py create mode 100644 bazarr/api/system/system.py create mode 100644 bazarr/api/system/tasks.py create mode 100644 bazarr/api/utils.py create mode 100644 bazarr/api/webhooks/__init__.py create mode 100644 bazarr/api/webhooks/plex.py diff --git a/bazarr/api.py b/bazarr/api.py deleted file mode 100644 index d5522a8d0..000000000 --- a/bazarr/api.py +++ /dev/null @@ -1,2192 +0,0 @@ -# coding=utf-8 - -import sys -import os -import ast -from datetime import timedelta -from dateutil import rrule -import pretty -import time -import operator -from operator import itemgetter -from functools import reduce -import platform -import re -import json -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 -from logger import empty_log -from init import startTime - -from init import * -import logging -from database import get_exclusion_clause, get_profiles_list, get_desired_languages, get_profile_id_name, \ - get_audio_profile_languages, update_profile_id_list, convert_list_to_clause, TableEpisodes, TableShows, \ - TableMovies, TableSettingsLanguages, TableSettingsNotifier, TableLanguagesProfiles, TableHistory, \ - TableHistoryMovie, TableBlacklist, TableBlacklistMovie -from helper import path_mappings -from get_languages import language_from_alpha2, language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2 -from get_subtitle import download_subtitle, series_download_subtitles, manual_search, manual_download_subtitle, \ - manual_upload_subtitle, wanted_search_missing_subtitles_series, wanted_search_missing_subtitles_movies, \ - episode_download_subtitles, movies_download_subtitles -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_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 -from event_handler import event_stream -from scheduler import scheduler -from subsyncer import subsync -from filesystem import browse_bazarr_filesystem, browse_sonarr_filesystem, browse_radarr_filesystem - -from subliminal_patch.core import SUBTITLE_EXTENSIONS, guessit - -from flask import Flask, jsonify, request, Response, Blueprint, url_for, make_response, session - -from flask_restful import Resource, Api, abort -from functools import wraps - -api_bp = Blueprint('api', __name__, url_prefix=base_url.rstrip('/') + '/api') -api = Api(api_bp) - -None_Keys = ['null', 'undefined', '', None] - -False_Keys = ['False', 'false', '0'] - - -def authenticate(actual_method): - @wraps(actual_method) - def wrapper(*args, **kwargs): - apikey_settings = settings.auth.apikey - apikey_get = request.args.get('apikey') - apikey_post = request.form.get('apikey') - apikey_header = None - if 'X-API-KEY' in request.headers: - apikey_header = request.headers['X-API-KEY'] - - if apikey_settings in [apikey_get, apikey_post, apikey_header]: - return actual_method(*args, **kwargs) - - return abort(401) - - return wrapper - - -def postprocess(item): - # Remove ffprobe_cache - if 'ffprobe_cache' in item: - del (item['ffprobe_cache']) - - # Parse tags - if 'tags' in item: - if item['tags'] is None: - item['tags'] = [] - else: - item['tags'] = ast.literal_eval(item['tags']) - - if 'monitored' in item: - if item['monitored'] is None: - item['monitored'] = False - else: - item['monitored'] = item['monitored'] == 'True' - - if 'hearing_impaired' in item and item['hearing_impaired'] is not None: - if item['hearing_impaired'] is None: - item['hearing_impaired'] = False - else: - item['hearing_impaired'] = item['hearing_impaired'] == 'True' - - if 'language' in item: - if item['language'] == 'None': - item['language'] = None - elif item['language'] is not None: - splitted_language = item['language'].split(':') - item['language'] = {"name": language_from_alpha2(splitted_language[0]), - "code2": splitted_language[0], - "code3": alpha3_from_alpha2(splitted_language[0]), - "forced": True if item['language'].endswith(':forced') else False, - "hi": True if item['language'].endswith(':hi') else False} - - -def postprocessSeries(item): - postprocess(item) - # Parse audio language - if 'audio_language' in item and item['audio_language'] is not None: - item['audio_language'] = get_audio_profile_languages(series_id=item['sonarrSeriesId']) - - if 'alternateTitles' in item: - if item['alternateTitles'] is None: - item['alternativeTitles'] = [] - else: - item['alternativeTitles'] = ast.literal_eval(item['alternateTitles']) - del item["alternateTitles"] - - # Parse seriesType - if 'seriesType' in item and item['seriesType'] is not None: - item['seriesType'] = item['seriesType'].capitalize() - - if 'path' in item: - item['path'] = path_mappings.path_replace(item['path']) - - # map poster and fanart to server proxy - if 'poster' in item: - poster = item['poster'] - item['poster'] = f"{base_url}/images/series{poster}" if poster else None - - if 'fanart' in item: - fanart = item['fanart'] - item['fanart'] = f"{base_url}/images/series{fanart}" if fanart else None - - -def postprocessEpisode(item): - postprocess(item) - if 'audio_language' in item and item['audio_language'] is not None: - item['audio_language'] = get_audio_profile_languages(episode_id=item['sonarrEpisodeId']) - - if 'subtitles' in item: - if item['subtitles'] is None: - raw_subtitles = [] - else: - raw_subtitles = ast.literal_eval(item['subtitles']) - subtitles = [] - - for subs in raw_subtitles: - subtitle = subs[0].split(':') - sub = {"name": language_from_alpha2(subtitle[0]), - "code2": subtitle[0], - "code3": alpha3_from_alpha2(subtitle[0]), - "path": path_mappings.path_replace(subs[1]), - "forced": False, - "hi": False} - if len(subtitle) > 1: - sub["forced"] = True if subtitle[1] == 'forced' else False - sub["hi"] = True if subtitle[1] == 'hi' else False - - subtitles.append(sub) - - item.update({"subtitles": subtitles}) - - # Parse missing subtitles - if 'missing_subtitles' in item: - if item['missing_subtitles'] is None: - item['missing_subtitles'] = [] - else: - item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles']) - for i, subs in enumerate(item['missing_subtitles']): - subtitle = subs.split(':') - item['missing_subtitles'][i] = {"name": language_from_alpha2(subtitle[0]), - "code2": subtitle[0], - "code3": alpha3_from_alpha2(subtitle[0]), - "forced": False, - "hi": False} - if len(subtitle) > 1: - item['missing_subtitles'][i].update({ - "forced": True if subtitle[1] == 'forced' else False, - "hi": True if subtitle[1] == 'hi' else False - }) - - if 'scene_name' in item: - item["sceneName"] = item["scene_name"] - del item["scene_name"] - - if 'path' in item and item['path']: - # Provide mapped path - item['path'] = path_mappings.path_replace(item['path']) - - -# TODO: Move -def postprocessMovie(item): - postprocess(item) - # Parse audio language - if 'audio_language' in item and item['audio_language'] is not None: - item['audio_language'] = get_audio_profile_languages(movie_id=item['radarrId']) - - # Parse alternate titles - if 'alternativeTitles' in item: - if item['alternativeTitles'] is None: - item['alternativeTitles'] = [] - else: - item['alternativeTitles'] = ast.literal_eval(item['alternativeTitles']) - - # Parse failed attempts - if 'failedAttempts' in item: - if item['failedAttempts']: - item['failedAttempts'] = ast.literal_eval(item['failedAttempts']) - - # Parse subtitles - if 'subtitles' in item: - if item['subtitles'] is None: - item['subtitles'] = [] - else: - item['subtitles'] = ast.literal_eval(item['subtitles']) - for i, subs in enumerate(item['subtitles']): - language = subs[0].split(':') - item['subtitles'][i] = {"path": path_mappings.path_replace_movie(subs[1]), - "name": language_from_alpha2(language[0]), - "code2": language[0], - "code3": alpha3_from_alpha2(language[0]), - "forced": False, - "hi": False} - if len(language) > 1: - item['subtitles'][i].update({ - "forced": True if language[1] == 'forced' else False, - "hi": True if language[1] == 'hi' else False - }) - - if settings.general.getboolean('embedded_subs_show_desired'): - desired_lang_list = get_desired_languages(item['profileId']) - item['subtitles'] = [x for x in item['subtitles'] if x['code2'] in desired_lang_list or x['path']] - - item['subtitles'] = sorted(item['subtitles'], key=itemgetter('name', 'forced')) - - # Parse missing subtitles - if 'missing_subtitles' in item: - if item['missing_subtitles'] is None: - item['missing_subtitles'] = [] - else: - item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles']) - for i, subs in enumerate(item['missing_subtitles']): - language = subs.split(':') - item['missing_subtitles'][i] = {"name": language_from_alpha2(language[0]), - "code2": language[0], - "code3": alpha3_from_alpha2(language[0]), - "forced": False, - "hi": False} - if len(language) > 1: - item['missing_subtitles'][i].update({ - "forced": True if language[1] == 'forced' else False, - "hi": True if language[1] == 'hi' else False - }) - - # Provide mapped path - if 'path' in item: - if item['path']: - item['path'] = path_mappings.path_replace_movie(item['path']) - - if 'subtitles_path' in item: - # Provide mapped subtitles path - item['subtitles_path'] = path_mappings.path_replace_movie(item['subtitles_path']) - - # map poster and fanart to server proxy - if 'poster' in item: - poster = item['poster'] - item['poster'] = f"{base_url}/images/movies{poster}" if poster else None - - if 'fanart' in item: - fanart = item['fanart'] - item['fanart'] = f"{base_url}/images/movies{fanart}" if fanart else None - - -class SystemAccount(Resource): - def post(self): - if settings.auth.type != 'form': - return '', 405 - - action = request.args.get('action') - if action == 'login': - username = request.form.get('username') - password = request.form.get('password') - if check_credentials(username, password): - session['logged_in'] = True - return '', 204 - elif action == 'logout': - session.clear() - gc.collect() - return '', 204 - - return '', 401 - - -class System(Resource): - @authenticate - def post(self): - from server import webserver - action = request.args.get('action') - if action == "shutdown": - webserver.shutdown() - elif action == "restart": - webserver.restart() - return '', 204 - - -class Badges(Resource): - @authenticate - def get(self): - episodes_conditions = [(TableEpisodes.missing_subtitles is not None), - (TableEpisodes.missing_subtitles != '[]')] - episodes_conditions += get_exclusion_clause('series') - missing_episodes = TableEpisodes.select(TableShows.tags, - TableShows.seriesType, - TableEpisodes.monitored)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(reduce(operator.and_, episodes_conditions))\ - .count() - - movies_conditions = [(TableMovies.missing_subtitles is not None), - (TableMovies.missing_subtitles != '[]')] - movies_conditions += get_exclusion_clause('movie') - missing_movies = TableMovies.select(TableMovies.tags, - TableMovies.monitored)\ - .where(reduce(operator.and_, movies_conditions))\ - .count() - - throttled_providers = len(eval(str(get_throttled_providers()))) - - health_issues = len(get_health_issues()) - - result = { - "episodes": missing_episodes, - "movies": missing_movies, - "providers": throttled_providers, - "status": health_issues - } - return jsonify(result) - - -class Languages(Resource): - @authenticate - def get(self): - history = request.args.get('history') - if history and history not in False_Keys: - languages = list(TableHistory.select(TableHistory.language) - .where(TableHistory.language != None) - .dicts()) - languages += list(TableHistoryMovie.select(TableHistoryMovie.language) - .where(TableHistoryMovie.language != None) - .dicts()) - languages_list = list(set([l['language'].split(':')[0] for l in languages])) - languages_dicts = [] - for language in languages_list: - code2 = None - if len(language) == 2: - code2 = language - elif len(language) == 3: - code2 = alpha2_from_alpha3(language) - else: - continue - - if not any(x['code2'] == code2 for x in languages_dicts): - try: - languages_dicts.append({ - 'code2': code2, - 'name': language_from_alpha2(code2), - # Compatibility: Use false temporarily - 'enabled': False - }) - except: - continue - return jsonify(sorted(languages_dicts, key=itemgetter('name'))) - - result = TableSettingsLanguages.select(TableSettingsLanguages.name, - TableSettingsLanguages.code2, - TableSettingsLanguages.enabled)\ - .order_by(TableSettingsLanguages.name).dicts() - result = list(result) - for item in result: - item['enabled'] = item['enabled'] == 1 - return jsonify(result) - - -class LanguagesProfiles(Resource): - @authenticate - def get(self): - return jsonify(get_profiles_list()) - - -class Notifications(Resource): - @authenticate - def patch(self): - url = request.form.get("url") - - asset = apprise.AppriseAsset(async_mode=False) - - apobj = apprise.Apprise(asset=asset) - - apobj.add(url) - - apobj.notify( - title='Bazarr test notification', - body='Test notification' - ) - - return '', 204 - - -class Searches(Resource): - @authenticate - def get(self): - query = request.args.get('query') - search_list = [] - - if query: - if settings.general.getboolean('use_sonarr'): - # Get matching series - series = TableShows.select(TableShows.title, - TableShows.sonarrSeriesId, - TableShows.year)\ - .where(TableShows.title.contains(query))\ - .order_by(TableShows.title)\ - .dicts() - series = list(series) - search_list += series - - if settings.general.getboolean('use_radarr'): - # Get matching movies - movies = TableMovies.select(TableMovies.title, - TableMovies.radarrId, - TableMovies.year) \ - .where(TableMovies.title.contains(query)) \ - .order_by(TableMovies.title) \ - .dicts() - movies = list(movies) - search_list += movies - - return jsonify(search_list) - - -class SystemSettings(Resource): - @authenticate - def get(self): - data = get_settings() - - notifications = TableSettingsNotifier.select().order_by(TableSettingsNotifier.name).dicts() - notifications = list(notifications) - for i, item in enumerate(notifications): - item["enabled"] = item["enabled"] == 1 - notifications[i] = item - - data['notifications'] = dict() - data['notifications']['providers'] = notifications - - return jsonify(data) - - @authenticate - def post(self): - enabled_languages = request.form.getlist('languages-enabled') - if len(enabled_languages) != 0: - TableSettingsLanguages.update({ - TableSettingsLanguages.enabled: 0 - }).execute() - for code in enabled_languages: - TableSettingsLanguages.update({ - TableSettingsLanguages.enabled: 1 - })\ - .where(TableSettingsLanguages.code2 == code)\ - .execute() - event_stream("languages") - - languages_profiles = request.form.get('languages-profiles') - if languages_profiles: - existing_ids = TableLanguagesProfiles.select(TableLanguagesProfiles.profileId).dicts() - existing_ids = list(existing_ids) - existing = [x['profileId'] for x in existing_ids] - for item in json.loads(languages_profiles): - if item['profileId'] in existing: - # Update existing profiles - TableLanguagesProfiles.update({ - TableLanguagesProfiles.name: item['name'], - TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None, - TableLanguagesProfiles.items: json.dumps(item['items']) - })\ - .where(TableLanguagesProfiles.profileId == item['profileId'])\ - .execute() - existing.remove(item['profileId']) - else: - # Add new profiles - TableLanguagesProfiles.insert({ - TableLanguagesProfiles.profileId: item['profileId'], - TableLanguagesProfiles.name: item['name'], - TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None, - TableLanguagesProfiles.items: json.dumps(item['items']) - }).execute() - for profileId in existing: - # Unassign this profileId from series and movies - TableShows.update({ - TableShows.profileId: None - }).where(TableShows.profileId == profileId).execute() - TableMovies.update({ - TableMovies.profileId: None - }).where(TableMovies.profileId == profileId).execute() - # Remove deleted profiles - TableLanguagesProfiles.delete().where(TableLanguagesProfiles.profileId == profileId).execute() - - update_profile_id_list() - event_stream("languages") - - if settings.general.getboolean('use_sonarr'): - scheduler.add_job(list_missing_subtitles, kwargs={'send_event': False}) - if settings.general.getboolean('use_radarr'): - scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': False}) - - # Update Notification - notifications = request.form.getlist('notifications-providers') - for item in notifications: - item = json.loads(item) - TableSettingsNotifier.update({ - TableSettingsNotifier.enabled: item['enabled'], - TableSettingsNotifier.url: item['url'] - }).where(TableSettingsNotifier.name == item['name']).execute() - - save_settings(zip(request.form.keys(), request.form.listvalues())) - event_stream("settings") - return '', 204 - - -class SystemTasks(Resource): - @authenticate - def get(self): - taskid = request.args.get('taskid') - - task_list = scheduler.get_task_list() - - if taskid: - for item in task_list: - if item['job_id'] == taskid: - task_list = [item] - continue - - return jsonify(data=task_list) - - @authenticate - def post(self): - taskid = request.form.get('taskid') - - scheduler.execute_job_now(taskid) - - return '', 204 - - -class SystemLogs(Resource): - @authenticate - def get(self): - logs = [] - with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file: - raw_lines = file.read() - lines = raw_lines.split('|\n') - for line in lines: - if line == '': - continue - raw_message = line.split('|') - raw_message_len = len(raw_message) - if raw_message_len > 3: - log = dict() - log["timestamp"] = raw_message[0] - log["type"] = raw_message[1].rstrip() - log["message"] = raw_message[3] - if raw_message_len > 4 and raw_message[4] != '\n': - log['exception'] = raw_message[4].strip('\'').replace(' ', '\u2003\u2003') - logs.append(log) - - logs.reverse() - return jsonify(data=logs) - - @authenticate - def delete(self): - empty_log() - return '', 204 - - -class SystemStatus(Resource): - @authenticate - def get(self): - system_status = {} - system_status.update({'bazarr_version': os.environ["BAZARR_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__))}) - system_status.update({'bazarr_config_directory': args.config_dir}) - system_status.update({'start_time': startTime}) - return jsonify(data=system_status) - - -class SystemHealth(Resource): - @authenticate - def get(self): - return jsonify(data=get_health_issues()) - - -class SystemReleases(Resource): - @authenticate - def get(self): - filtered_releases = [] - try: - with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f: - releases = json.loads(f.read()) - - for release in releases: - if settings.general.branch == 'master' and not release['prerelease']: - filtered_releases.append(release) - elif settings.general.branch != 'master' and any(not x['prerelease'] for x in filtered_releases): - continue - elif settings.general.branch != 'master': - filtered_releases.append(release) - if settings.general.branch == 'master': - filtered_releases = filtered_releases[:5] - - current_version = os.environ["BAZARR_VERSION"] - - for i, release in enumerate(filtered_releases): - body = release['body'].replace('- ', '').split('\n')[1:] - filtered_releases[i] = {"body": body, - "name": release['name'], - "date": release['date'][:10], - "prerelease": release['prerelease'], - "current": release['name'].lstrip('v') == current_version} - - except Exception as e: - logging.exception( - 'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt')) - return jsonify(data=filtered_releases) - - -class Series(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - seriesId = request.args.getlist('seriesid[]') - - count = TableShows.select().count() - - if len(seriesId) != 0: - result = TableShows.select()\ - .where(TableShows.sonarrSeriesId.in_(seriesId))\ - .order_by(TableShows.sortTitle).dicts() - else: - result = TableShows.select().order_by(TableShows.sortTitle).limit(length).offset(start).dicts() - - result = list(result) - - for item in result: - postprocessSeries(item) - - # Add missing subtitles episode count - episodes_missing_conditions = [(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId']), - (TableEpisodes.missing_subtitles != '[]')] - episodes_missing_conditions += get_exclusion_clause('series') - - episodeMissingCount = TableEpisodes.select(TableShows.tags, - TableEpisodes.monitored, - TableShows.seriesType)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(reduce(operator.and_, episodes_missing_conditions))\ - .count() - item.update({"episodeMissingCount": episodeMissingCount}) - - # Add episode count - episodeFileCount = TableEpisodes.select(TableShows.tags, - TableEpisodes.monitored, - TableShows.seriesType)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId'])\ - .count() - item.update({"episodeFileCount": episodeFileCount}) - - return jsonify(data=result, total=count) - - @authenticate - def post(self): - seriesIdList = request.form.getlist('seriesid') - profileIdList = request.form.getlist('profileid') - - for idx in range(len(seriesIdList)): - seriesId = seriesIdList[idx] - profileId = profileIdList[idx] - - if profileId in None_Keys: - profileId = None - else: - try: - profileId = int(profileId) - except Exception: - return '', 400 - - TableShows.update({ - TableShows.profileId: profileId - })\ - .where(TableShows.sonarrSeriesId == seriesId)\ - .execute() - - 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 - - @authenticate - def patch(self): - seriesid = request.form.get('seriesid') - action = request.form.get('action') - if action == "scan-disk": - series_scan_subtitles(seriesid) - return '', 204 - elif action == "search-missing": - series_download_subtitles(seriesid) - return '', 204 - elif action == "search-wanted": - wanted_search_missing_subtitles_series() - return '', 204 - - return '', 400 - - -class Episodes(Resource): - @authenticate - def get(self): - seriesId = request.args.getlist('seriesid[]') - episodeId = request.args.getlist('episodeid[]') - - if len(episodeId) > 0: - result = TableEpisodes.select().where(TableEpisodes.sonarrEpisodeId.in_(episodeId)).dicts() - elif len(seriesId) > 0: - result = TableEpisodes.select()\ - .where(TableEpisodes.sonarrSeriesId.in_(seriesId))\ - .order_by(TableEpisodes.season.desc(), TableEpisodes.episode.desc())\ - .dicts() - else: - return "Series or Episode ID not provided", 400 - - result = list(result) - for item in result: - postprocessEpisode(item) - - return jsonify(data=result) - - -# PATCH: Download Subtitles -# POST: Upload Subtitles -# DELETE: Delete Subtitles -class EpisodesSubtitles(Resource): - @authenticate - def patch(self): - sonarrSeriesId = request.args.get('seriesid') - sonarrEpisodeId = request.args.get('episodeid') - episodeInfo = TableEpisodes.select(TableEpisodes.title, - TableEpisodes.path, - TableEpisodes.scene_name, - TableEpisodes.audio_language)\ - .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ - .dicts()\ - .get() - - title = episodeInfo['title'] - episodePath = path_mappings.path_replace(episodeInfo['path']) - sceneName = episodeInfo['scene_name'] - audio_language = episodeInfo['audio_language'] - if sceneName is None: sceneName = "None" - - language = request.form.get('language') - hi = request.form.get('hi').capitalize() - forced = request.form.get('forced').capitalize() - - providers_list = get_providers() - providers_auth = get_providers_auth() - - audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId) - if len(audio_language_list) > 0: - audio_language = audio_language_list[0]['name'] - else: - audio_language = None - - try: - result = download_subtitle(episodePath, language, audio_language, hi, forced, providers_list, - providers_auth, sceneName, title, 'series') - if result is not None: - message = result[0] - path = result[1] - forced = result[5] - if result[8]: - language_code = result[2] + ":hi" - elif forced: - language_code = result[2] + ":forced" - else: - language_code = result[2] - provider = result[3] - score = result[4] - subs_id = result[6] - subs_path = result[7] - history_log(1, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id, - subs_path) - send_notifications(sonarrSeriesId, sonarrEpisodeId, message) - store_subtitles(path, episodePath) - else: - event_stream(type='episode', payload=sonarrEpisodeId) - - except OSError: - pass - - return '', 204 - - @authenticate - def post(self): - sonarrSeriesId = request.args.get('seriesid') - sonarrEpisodeId = request.args.get('episodeid') - episodeInfo = TableEpisodes.select(TableEpisodes.title, - TableEpisodes.path, - TableEpisodes.scene_name, - TableEpisodes.audio_language)\ - .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ - .dicts()\ - .get() - - title = episodeInfo['title'] - episodePath = path_mappings.path_replace(episodeInfo['path']) - sceneName = episodeInfo['scene_name'] - audio_language = episodeInfo['audio_language'] - if sceneName is None: sceneName = "None" - - language = request.form.get('language') - forced = True if request.form.get('forced') == 'true' else False - hi = True if request.form.get('hi') == 'true' else False - subFile = request.files.get('file') - - _, ext = os.path.splitext(subFile.filename) - - if ext not in SUBTITLE_EXTENSIONS: - raise ValueError('A subtitle of an invalid format was uploaded.') - - try: - result = manual_upload_subtitle(path=episodePath, - language=language, - forced=forced, - hi=hi, - title=title, - scene_name=sceneName, - media_type='series', - subtitle=subFile, - audio_language=audio_language) - - if result is not None: - message = result[0] - path = result[1] - subs_path = result[2] - if hi: - language_code = language + ":hi" - elif forced: - language_code = language + ":forced" - else: - language_code = language - provider = "manual" - score = 360 - history_log(4, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, - subtitles_path=subs_path) - if not settings.general.getboolean('dont_notify_manual_actions'): - send_notifications(sonarrSeriesId, sonarrEpisodeId, message) - store_subtitles(path, episodePath) - - except OSError: - pass - - return '', 204 - - @authenticate - def delete(self): - sonarrSeriesId = request.args.get('seriesid') - sonarrEpisodeId = request.args.get('episodeid') - episodeInfo = TableEpisodes.select(TableEpisodes.title, - TableEpisodes.path, - TableEpisodes.scene_name, - TableEpisodes.audio_language)\ - .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ - .dicts()\ - .get() - - episodePath = path_mappings.path_replace(episodeInfo['path']) - - language = request.form.get('language') - forced = request.form.get('forced') - hi = request.form.get('hi') - subtitlesPath = request.form.get('path') - - subtitlesPath = path_mappings.path_replace_reverse(subtitlesPath) - - result = delete_subtitles(media_type='series', - language=language, - forced=forced, - hi=hi, - media_path=episodePath, - subtitles_path=subtitlesPath, - sonarr_series_id=sonarrSeriesId, - sonarr_episode_id=sonarrEpisodeId) - - return '', 204 - - -class Movies(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - radarrId = request.args.getlist('radarrid[]') - - count = TableMovies.select().count() - - if len(radarrId) != 0: - result = TableMovies.select()\ - .where(TableMovies.radarrId.in_(radarrId))\ - .order_by(TableMovies.sortTitle)\ - .dicts() - else: - result = TableMovies.select().order_by(TableMovies.sortTitle).limit(length).offset(start).dicts() - result = list(result) - for item in result: - postprocessMovie(item) - - return jsonify(data=result, total=count) - - @authenticate - def post(self): - radarrIdList = request.form.getlist('radarrid') - profileIdList = request.form.getlist('profileid') - - for idx in range(len(radarrIdList)): - radarrId = radarrIdList[idx] - profileId = profileIdList[idx] - - if profileId in None_Keys: - profileId = None - else: - try: - profileId = int(profileId) - except Exception: - return '', 400 - - TableMovies.update({ - TableMovies.profileId: profileId - })\ - .where(TableMovies.radarrId == radarrId)\ - .execute() - - 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 - - @authenticate - def patch(self): - radarrid = request.form.get('radarrid') - action = request.form.get('action') - if action == "scan-disk": - movies_scan_subtitles(radarrid) - return '', 204 - elif action == "search-missing": - movies_download_subtitles(radarrid) - return '', 204 - elif action == "search-wanted": - wanted_search_missing_subtitles_movies() - return '', 204 - - return '', 400 - - -""" -:param language: Alpha2 language code -""" - - -class MoviesSubtitles(Resource): - @authenticate - def patch(self): - # Download - radarrId = request.args.get('radarrid') - - movieInfo = TableMovies.select(TableMovies.title, - TableMovies.path, - TableMovies.sceneName, - TableMovies.audio_language)\ - .where(TableMovies.radarrId == radarrId)\ - .dicts()\ - .get() - - moviePath = path_mappings.path_replace_movie(movieInfo['path']) - sceneName = movieInfo['sceneName'] - if sceneName is None: sceneName = 'None' - - title = movieInfo['title'] - audio_language = movieInfo['audio_language'] - - language = request.form.get('language') - hi = request.form.get('hi').capitalize() - forced = request.form.get('forced').capitalize() - - providers_list = get_providers() - providers_auth = get_providers_auth() - - audio_language_list = get_audio_profile_languages(movie_id=radarrId) - if len(audio_language_list) > 0: - audio_language = audio_language_list[0]['name'] - else: - audio_language = None - - try: - result = download_subtitle(moviePath, language, audio_language, hi, forced, providers_list, - providers_auth, sceneName, title, 'movie') - if result is not None: - message = result[0] - path = result[1] - forced = result[5] - if result[8]: - language_code = result[2] + ":hi" - elif forced: - language_code = result[2] + ":forced" - else: - language_code = result[2] - provider = result[3] - score = result[4] - subs_id = result[6] - subs_path = result[7] - history_log_movie(1, radarrId, message, path, language_code, provider, score, subs_id, subs_path) - send_notifications_movie(radarrId, message) - store_subtitles_movie(path, moviePath) - else: - event_stream(type='movie', payload=radarrId) - except OSError: - pass - - return '', 204 - - @authenticate - def post(self): - # Upload - # TODO: Support Multiply Upload - radarrId = request.args.get('radarrid') - movieInfo = TableMovies.select(TableMovies.title, - TableMovies.path, - TableMovies.sceneName, - TableMovies.audio_language) \ - .where(TableMovies.radarrId == radarrId) \ - .dicts() \ - .get() - - moviePath = path_mappings.path_replace_movie(movieInfo['path']) - sceneName = movieInfo['sceneName'] - if sceneName is None: sceneName = 'None' - - title = movieInfo['title'] - audioLanguage = movieInfo['audio_language'] - - language = request.form.get('language') - forced = True if request.form.get('forced') == 'true' else False - hi = True if request.form.get('hi') == 'true' else False - subFile = request.files.get('file') - - _, ext = os.path.splitext(subFile.filename) - - if ext not in SUBTITLE_EXTENSIONS: - raise ValueError('A subtitle of an invalid format was uploaded.') - - try: - result = manual_upload_subtitle(path=moviePath, - language=language, - forced=forced, - hi=hi, - title=title, - scene_name=sceneName, - media_type='movie', - subtitle=subFile, - audio_language=audioLanguage) - - if result is not None: - message = result[0] - path = result[1] - subs_path = result[2] - if hi: - language_code = language + ":hi" - elif forced: - language_code = language + ":forced" - else: - language_code = language - provider = "manual" - score = 120 - history_log_movie(4, radarrId, message, path, language_code, provider, score, subtitles_path=subs_path) - if not settings.general.getboolean('dont_notify_manual_actions'): - send_notifications_movie(radarrId, message) - store_subtitles_movie(path, moviePath) - except OSError: - pass - - return '', 204 - - @authenticate - def delete(self): - # Delete - radarrId = request.args.get('radarrid') - movieInfo = TableMovies.select(TableMovies.path) \ - .where(TableMovies.radarrId == radarrId) \ - .dicts() \ - .get() - - moviePath = path_mappings.path_replace_movie(movieInfo['path']) - - language = request.form.get('language') - forced = request.form.get('forced') - hi = request.form.get('hi') - subtitlesPath = request.form.get('path') - - subtitlesPath = path_mappings.path_replace_reverse_movie(subtitlesPath) - - result = delete_subtitles(media_type='movie', - language=language, - forced=forced, - hi=hi, - media_path=moviePath, - subtitles_path=subtitlesPath, - radarr_id=radarrId) - if result: - return '', 202 - else: - return '', 204 - - -class Providers(Resource): - @authenticate - def get(self): - history = request.args.get('history') - if history and history not in False_Keys: - providers = list(TableHistory.select(TableHistory.provider) - .where(TableHistory.provider != None and TableHistory.provider != "manual") - .dicts()) - providers += list(TableHistoryMovie.select(TableHistoryMovie.provider) - .where(TableHistoryMovie.provider != None and TableHistoryMovie.provider != "manual") - .dicts()) - providers_list = list(set([x['provider'] for x in providers])) - providers_dicts = [] - for provider in providers_list: - providers_dicts.append({ - 'name': provider, - 'status': 'History', - 'retry': '-' - }) - return jsonify(data=sorted(providers_dicts, key=itemgetter('name'))) - - throttled_providers = list_throttled_providers() - - providers = list() - for provider in throttled_providers: - providers.append({ - "name": provider[0], - "status": provider[1] if provider[1] is not None else "Good", - "retry": provider[2] if provider[2] != "now" else "-" - }) - return jsonify(data=providers) - - @authenticate - def post(self): - action = request.form.get('action') - - if action == 'reset': - reset_throttled_providers() - return '', 204 - - return '', 400 - - -class ProviderMovies(Resource): - @authenticate - def get(self): - # Manual Search - radarrId = request.args.get('radarrid') - movieInfo = TableMovies.select(TableMovies.title, - TableMovies.path, - TableMovies.sceneName, - TableMovies.profileId) \ - .where(TableMovies.radarrId == radarrId) \ - .dicts() \ - .get() - - title = movieInfo['title'] - moviePath = path_mappings.path_replace_movie(movieInfo['path']) - sceneName = movieInfo['sceneName'] - profileId = movieInfo['profileId'] - if sceneName is None: sceneName = "None" - - providers_list = get_providers() - providers_auth = get_providers_auth() - - data = manual_search(moviePath, profileId, providers_list, providers_auth, sceneName, title, - 'movie') - if not data: - data = [] - return jsonify(data=data) - - @authenticate - def post(self): - # Manual Download - radarrId = request.args.get('radarrid') - movieInfo = TableMovies.select(TableMovies.title, - TableMovies.path, - TableMovies.sceneName, - TableMovies.audio_language) \ - .where(TableMovies.radarrId == radarrId) \ - .dicts() \ - .get() - - title = movieInfo['title'] - moviePath = path_mappings.path_replace_movie(movieInfo['path']) - sceneName = movieInfo['sceneName'] - if sceneName is None: sceneName = "None" - audio_language = movieInfo['audio_language'] - - language = request.form.get('language') - hi = request.form.get('hi').capitalize() - forced = request.form.get('forced').capitalize() - selected_provider = request.form.get('provider') - subtitle = request.form.get('subtitle') - - providers_auth = get_providers_auth() - - audio_language_list = get_audio_profile_languages(movie_id=radarrId) - if len(audio_language_list) > 0: - audio_language = audio_language_list[0]['name'] - else: - audio_language = 'None' - - try: - result = manual_download_subtitle(moviePath, language, audio_language, hi, forced, subtitle, - selected_provider, providers_auth, sceneName, title, 'movie') - if result is not None: - message = result[0] - path = result[1] - forced = result[5] - if result[8]: - language_code = result[2] + ":hi" - elif forced: - language_code = result[2] + ":forced" - else: - language_code = result[2] - provider = result[3] - score = result[4] - subs_id = result[6] - subs_path = result[7] - history_log_movie(2, radarrId, message, path, language_code, provider, score, subs_id, subs_path) - if not settings.general.getboolean('dont_notify_manual_actions'): - send_notifications_movie(radarrId, message) - store_subtitles_movie(path, moviePath) - except OSError: - pass - - return '', 204 - - -class ProviderEpisodes(Resource): - @authenticate - def get(self): - # Manual Search - sonarrEpisodeId = request.args.get('episodeid') - episodeInfo = TableEpisodes.select(TableEpisodes.title, - TableEpisodes.path, - TableEpisodes.scene_name, - TableShows.profileId) \ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \ - .dicts() \ - .get() - - title = episodeInfo['title'] - episodePath = path_mappings.path_replace(episodeInfo['path']) - sceneName = episodeInfo['scene_name'] - profileId = episodeInfo['profileId'] - if sceneName is None: sceneName = "None" - - providers_list = get_providers() - providers_auth = get_providers_auth() - - data = manual_search(episodePath, profileId, providers_list, providers_auth, sceneName, title, - 'series') - if not data: - data = [] - return jsonify(data=data) - - @authenticate - def post(self): - # Manual Download - sonarrSeriesId = request.args.get('seriesid') - sonarrEpisodeId = request.args.get('episodeid') - episodeInfo = TableEpisodes.select(TableEpisodes.title, - TableEpisodes.path, - TableEpisodes.scene_name) \ - .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \ - .dicts() \ - .get() - - title = episodeInfo['title'] - episodePath = path_mappings.path_replace(episodeInfo['path']) - sceneName = episodeInfo['scene_name'] - if sceneName is None: sceneName = "None" - - language = request.form.get('language') - hi = request.form.get('hi').capitalize() - forced = request.form.get('forced').capitalize() - selected_provider = request.form.get('provider') - subtitle = request.form.get('subtitle') - providers_auth = get_providers_auth() - - audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId) - if len(audio_language_list) > 0: - audio_language = audio_language_list[0]['name'] - else: - audio_language = 'None' - - try: - result = manual_download_subtitle(episodePath, language, audio_language, hi, forced, subtitle, - selected_provider, providers_auth, sceneName, title, 'series') - if result is not None: - message = result[0] - path = result[1] - forced = result[5] - if result[8]: - language_code = result[2] + ":hi" - elif forced: - language_code = result[2] + ":forced" - else: - language_code = result[2] - provider = result[3] - score = result[4] - subs_id = result[6] - subs_path = result[7] - history_log(2, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id, - subs_path) - if not settings.general.getboolean('dont_notify_manual_actions'): - send_notifications(sonarrSeriesId, sonarrEpisodeId, message) - store_subtitles(path, episodePath) - return result, 201 - except OSError: - pass - - return '', 204 - - -class EpisodesHistory(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - episodeid = request.args.get('episodeid') - - upgradable_episodes_not_perfect = [] - if settings.general.getboolean('upgrade_subs'): - days_to_upgrade_subs = settings.general.days_to_upgrade_subs - minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) - - datetime.datetime(1970, 1, 1)).total_seconds() - - if settings.general.getboolean('upgrade_manual'): - query_actions = [1, 2, 3, 6] - else: - query_actions = [1, 3] - - upgradable_episodes_conditions = [(TableHistory.action.in_(query_actions)), - (TableHistory.timestamp > minimum_timestamp), - (TableHistory.score is not None)] - upgradable_episodes_conditions += get_exclusion_clause('series') - upgradable_episodes = TableHistory.select(TableHistory.video_path, - fn.MAX(TableHistory.timestamp).alias('timestamp'), - TableHistory.score, - TableShows.tags, - TableEpisodes.monitored, - TableShows.seriesType)\ - .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ - .join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(reduce(operator.and_, upgradable_episodes_conditions))\ - .group_by(TableHistory.video_path)\ - .dicts() - upgradable_episodes = list(upgradable_episodes) - for upgradable_episode in upgradable_episodes: - if upgradable_episode['timestamp'] > minimum_timestamp: - try: - int(upgradable_episode['score']) - except ValueError: - pass - else: - if int(upgradable_episode['score']) < 360: - upgradable_episodes_not_perfect.append(upgradable_episode) - - query_conditions = [(TableEpisodes.title is not None)] - if episodeid: - query_conditions.append((TableEpisodes.sonarrEpisodeId == episodeid)) - query_condition = reduce(operator.and_, query_conditions) - episode_history = TableHistory.select(TableHistory.id, - TableShows.title.alias('seriesTitle'), - TableEpisodes.monitored, - TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), - TableEpisodes.title.alias('episodeTitle'), - TableHistory.timestamp, - TableHistory.subs_id, - TableHistory.description, - TableHistory.sonarrSeriesId, - TableEpisodes.path, - TableHistory.language, - TableHistory.score, - TableShows.tags, - TableHistory.action, - TableHistory.subtitles_path, - TableHistory.sonarrEpisodeId, - TableHistory.provider, - TableShows.seriesType)\ - .join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ - .where(query_condition)\ - .order_by(TableHistory.timestamp.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - episode_history = list(episode_history) - - blacklist_db = TableBlacklist.select(TableBlacklist.provider, TableBlacklist.subs_id).dicts() - blacklist_db = list(blacklist_db) - - for item in episode_history: - # Mark episode as upgradable or not - item.update({"upgradable": False}) - if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']), - "tags": str(item['tags']), "monitored": str(item['monitored']), - "seriesType": str(item['seriesType'])} in upgradable_episodes_not_perfect: - if os.path.isfile(path_mappings.path_replace(item['subtitles_path'])): - item.update({"upgradable": True}) - - del item['path'] - - postprocessEpisode(item) - - if item['score']: - item['score'] = str(round((int(item['score']) * 100 / 360), 2)) + "%" - - # Make timestamp pretty - if item['timestamp']: - item["raw_timestamp"] = int(item['timestamp']) - item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') - item['timestamp'] = pretty.date(item["raw_timestamp"]) - - # Check if subtitles is blacklisted - item.update({"blacklisted": False}) - if item['action'] not in [0, 4, 5]: - for blacklisted_item in blacklist_db: - if blacklisted_item['provider'] == item['provider'] and \ - blacklisted_item['subs_id'] == item['subs_id']: - item.update({"blacklisted": True}) - break - - count = TableHistory.select()\ - .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ - .where(TableEpisodes.title is not None).count() - - return jsonify(data=episode_history, total=count) - - -class MoviesHistory(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - radarrid = request.args.get('radarrid') - - upgradable_movies = [] - upgradable_movies_not_perfect = [] - if settings.general.getboolean('upgrade_subs'): - days_to_upgrade_subs = settings.general.days_to_upgrade_subs - minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) - - datetime.datetime(1970, 1, 1)).total_seconds() - - if settings.general.getboolean('upgrade_manual'): - query_actions = [1, 2, 3, 6] - else: - query_actions = [1, 3] - - upgradable_movies_conditions = [(TableHistoryMovie.action.in_(query_actions)), - (TableHistoryMovie.timestamp > minimum_timestamp), - (TableHistoryMovie.score is not None)] - upgradable_movies_conditions += get_exclusion_clause('movie') - upgradable_movies = TableHistoryMovie.select(TableHistoryMovie.video_path, - fn.MAX(TableHistoryMovie.timestamp).alias('timestamp'), - TableHistoryMovie.score, - TableMovies.tags, - TableMovies.monitored)\ - .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ - .where(reduce(operator.and_, upgradable_movies_conditions))\ - .group_by(TableHistoryMovie.video_path)\ - .dicts() - upgradable_movies = list(upgradable_movies) - - for upgradable_movie in upgradable_movies: - if upgradable_movie['timestamp'] > minimum_timestamp: - try: - int(upgradable_movie['score']) - except ValueError: - pass - else: - if int(upgradable_movie['score']) < 120: - upgradable_movies_not_perfect.append(upgradable_movie) - - query_conditions = [(TableMovies.title is not None)] - if radarrid: - query_conditions.append((TableMovies.radarrId == radarrid)) - query_condition = reduce(operator.and_, query_conditions) - - movie_history = TableHistoryMovie.select(TableHistoryMovie.id, - TableHistoryMovie.action, - TableMovies.title, - TableHistoryMovie.timestamp, - TableHistoryMovie.description, - TableHistoryMovie.radarrId, - TableMovies.monitored, - TableHistoryMovie.video_path.alias('path'), - TableHistoryMovie.language, - TableMovies.tags, - TableHistoryMovie.score, - TableHistoryMovie.subs_id, - TableHistoryMovie.provider, - TableHistoryMovie.subtitles_path)\ - .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ - .where(query_condition)\ - .order_by(TableHistoryMovie.timestamp.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - movie_history = list(movie_history) - - blacklist_db = TableBlacklistMovie.select(TableBlacklistMovie.provider, TableBlacklistMovie.subs_id).dicts() - blacklist_db = list(blacklist_db) - - for item in movie_history: - # Mark movies as upgradable or not - item.update({"upgradable": False}) - if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']), - "tags": str(item['tags']), "monitored": str(item['monitored'])} in upgradable_movies_not_perfect: - if os.path.isfile(path_mappings.path_replace_movie(item['subtitles_path'])): - item.update({"upgradable": True}) - - del item['path'] - - postprocessMovie(item) - - if item['score']: - item['score'] = str(round((int(item['score']) * 100 / 120), 2)) + "%" - - # Make timestamp pretty - if item['timestamp']: - item["raw_timestamp"] = int(item['timestamp']) - item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') - item['timestamp'] = pretty.date(item["raw_timestamp"]) - - # Check if subtitles is blacklisted - item.update({"blacklisted": False}) - if item['action'] not in [0, 4, 5]: - for blacklisted_item in blacklist_db: - if blacklisted_item['provider'] == item['provider'] and blacklisted_item['subs_id'] == item[ - 'subs_id']: - item.update({"blacklisted": True}) - break - - count = TableHistoryMovie.select()\ - .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ - .where(TableMovies.title is not None)\ - .count() - - return jsonify(data=movie_history, total=count) - - -class HistoryStats(Resource): - @authenticate - def get(self): - timeframe = request.args.get('timeframe') or 'month' - action = request.args.get('action') or 'All' - provider = request.args.get('provider') or 'All' - language = request.args.get('language') or 'All' - - # timeframe must be in ['week', 'month', 'trimester', 'year'] - if timeframe == 'year': - delay = 364 * 24 * 60 * 60 - elif timeframe == 'trimester': - delay = 90 * 24 * 60 * 60 - elif timeframe == 'month': - delay = 30 * 24 * 60 * 60 - elif timeframe == 'week': - delay = 6 * 24 * 60 * 60 - - now = time.time() - past = now - delay - - history_where_clauses = [(TableHistory.timestamp.between(past, now))] - history_where_clauses_movie = [(TableHistoryMovie.timestamp.between(past, now))] - - if action != 'All': - history_where_clauses.append((TableHistory.action == action)) - history_where_clauses_movie.append((TableHistoryMovie.action == action)) - else: - history_where_clauses.append((TableHistory.action.in_([1, 2, 3]))) - history_where_clauses_movie.append((TableHistoryMovie.action.in_([1, 2, 3]))) - - if provider != 'All': - history_where_clauses.append((TableHistory.provider == provider)) - history_where_clauses_movie.append((TableHistoryMovie.provider == provider)) - - if language != 'All': - history_where_clauses.append((TableHistory.language == language)) - history_where_clauses_movie.append((TableHistoryMovie.language == language)) - - history_where_clause = reduce(operator.and_, history_where_clauses) - history_where_clause_movie = reduce(operator.and_, history_where_clauses_movie) - - data_series = TableHistory.select(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch').alias('date'), - fn.COUNT(TableHistory.id).alias('count'))\ - .where(history_where_clause) \ - .group_by(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch'))\ - .dicts() - data_series = list(data_series) - - data_movies = TableHistoryMovie.select(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch').alias('date'), - fn.COUNT(TableHistoryMovie.id).alias('count')) \ - .where(history_where_clause_movie) \ - .group_by(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch')) \ - .dicts() - data_movies = list(data_movies) - - for dt in rrule.rrule(rrule.DAILY, - dtstart=datetime.datetime.now() - datetime.timedelta(seconds=delay), - until=datetime.datetime.now()): - if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_series): - data_series.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0}) - if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_movies): - data_movies.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0}) - - sorted_data_series = sorted(data_series, key=lambda i: i['date']) - sorted_data_movies = sorted(data_movies, key=lambda i: i['date']) - - return jsonify(series=sorted_data_series, movies=sorted_data_movies) - - -# GET: Get Wanted Episodes -class EpisodesWanted(Resource): - @authenticate - def get(self): - episodeid = request.args.getlist('episodeid[]') - - wanted_conditions = [(TableEpisodes.missing_subtitles != '[]')] - if len(episodeid) > 0: - wanted_conditions.append((TableEpisodes.sonarrEpisodeId in episodeid)) - wanted_conditions += get_exclusion_clause('series') - wanted_condition = reduce(operator.and_, wanted_conditions) - - if len(episodeid) > 0: - data = TableEpisodes.select(TableShows.title.alias('seriesTitle'), - TableEpisodes.monitored, - TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), - TableEpisodes.title.alias('episodeTitle'), - TableEpisodes.missing_subtitles, - TableEpisodes.sonarrSeriesId, - TableEpisodes.sonarrEpisodeId, - TableEpisodes.scene_name.alias('sceneName'), - TableShows.tags, - TableEpisodes.failedAttempts, - TableShows.seriesType)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(wanted_condition)\ - .dicts() - else: - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - data = TableEpisodes.select(TableShows.title.alias('seriesTitle'), - TableEpisodes.monitored, - TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), - TableEpisodes.title.alias('episodeTitle'), - TableEpisodes.missing_subtitles, - TableEpisodes.sonarrSeriesId, - TableEpisodes.sonarrEpisodeId, - TableEpisodes.scene_name.alias('sceneName'), - TableShows.tags, - TableEpisodes.failedAttempts, - TableShows.seriesType)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(wanted_condition)\ - .order_by(TableEpisodes.rowid.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - data = list(data) - - for item in data: - postprocessEpisode(item) - - count_conditions = [(TableEpisodes.missing_subtitles != '[]')] - count_conditions += get_exclusion_clause('series') - count = TableEpisodes.select(TableShows.tags, - TableShows.seriesType, - TableEpisodes.monitored)\ - .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ - .where(reduce(operator.and_, count_conditions))\ - .count() - - return jsonify(data=data, total=count) - - -# GET: Get Wanted Movies -class MoviesWanted(Resource): - @authenticate - def get(self): - radarrid = request.args.getlist("radarrid[]") - - wanted_conditions = [(TableMovies.missing_subtitles != '[]')] - if len(radarrid) > 0: - wanted_conditions.append((TableMovies.radarrId.in_(radarrid))) - wanted_conditions += get_exclusion_clause('movie') - wanted_condition = reduce(operator.and_, wanted_conditions) - - if len(radarrid) > 0: - result = TableMovies.select(TableMovies.title, - TableMovies.missing_subtitles, - TableMovies.radarrId, - TableMovies.sceneName, - TableMovies.failedAttempts, - TableMovies.tags, - TableMovies.monitored)\ - .where(wanted_condition)\ - .dicts() - else: - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - result = TableMovies.select(TableMovies.title, - TableMovies.missing_subtitles, - TableMovies.radarrId, - TableMovies.sceneName, - TableMovies.failedAttempts, - TableMovies.tags, - TableMovies.monitored)\ - .where(wanted_condition)\ - .order_by(TableMovies.rowid.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - result = list(result) - - for item in result: - postprocessMovie(item) - - count_conditions = [(TableMovies.missing_subtitles != '[]')] - count_conditions += get_exclusion_clause('movie') - count = TableMovies.select(TableMovies.monitored, - TableMovies.tags)\ - .where(reduce(operator.and_, count_conditions))\ - .count() - - return jsonify(data=result, total=count) - - -# GET: get blacklist -# POST: add blacklist -# DELETE: remove blacklist -class EpisodesBlacklist(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - - data = TableBlacklist.select(TableShows.title.alias('seriesTitle'), - TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), - TableEpisodes.title.alias('episodeTitle'), - TableEpisodes.sonarrSeriesId, - TableBlacklist.provider, - TableBlacklist.subs_id, - TableBlacklist.language, - TableBlacklist.timestamp)\ - .join(TableEpisodes, on=(TableBlacklist.sonarr_episode_id == TableEpisodes.sonarrEpisodeId))\ - .join(TableShows, on=(TableBlacklist.sonarr_series_id == TableShows.sonarrSeriesId))\ - .order_by(TableBlacklist.timestamp.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - data = list(data) - - for item in data: - # Make timestamp pretty - item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') - item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))}) - - postprocessEpisode(item) - - return jsonify(data=data) - - @authenticate - def post(self): - sonarr_series_id = int(request.args.get('seriesid')) - sonarr_episode_id = int(request.args.get('episodeid')) - provider = request.form.get('provider') - subs_id = request.form.get('subs_id') - language = request.form.get('language') - - episodeInfo = TableEpisodes.select(TableEpisodes.path)\ - .where(TableEpisodes.sonarrEpisodeId == sonarr_episode_id)\ - .dicts()\ - .get() - - media_path = episodeInfo['path'] - subtitles_path = request.form.get('subtitles_path') - - blacklist_log(sonarr_series_id=sonarr_series_id, - sonarr_episode_id=sonarr_episode_id, - provider=provider, - subs_id=subs_id, - language=language) - delete_subtitles(media_type='series', - language=language, - forced=False, - hi=False, - media_path=path_mappings.path_replace(media_path), - subtitles_path=subtitles_path, - sonarr_series_id=sonarr_series_id, - sonarr_episode_id=sonarr_episode_id) - episode_download_subtitles(sonarr_episode_id) - event_stream(type='episode-history') - return '', 200 - - @authenticate - def delete(self): - if request.args.get("all") == "true": - blacklist_delete_all() - else: - provider = request.form.get('provider') - subs_id = request.form.get('subs_id') - blacklist_delete(provider=provider, subs_id=subs_id) - return '', 204 - - -# GET: get blacklist -# POST: add blacklist -# DELETE: remove blacklist -class MoviesBlacklist(Resource): - @authenticate - def get(self): - start = request.args.get('start') or 0 - length = request.args.get('length') or -1 - - data = TableBlacklistMovie.select(TableMovies.title, - TableMovies.radarrId, - TableBlacklistMovie.provider, - TableBlacklistMovie.subs_id, - TableBlacklistMovie.language, - TableBlacklistMovie.timestamp)\ - .join(TableMovies, on=(TableBlacklistMovie.radarr_id == TableMovies.radarrId))\ - .order_by(TableBlacklistMovie.timestamp.desc())\ - .limit(length)\ - .offset(start)\ - .dicts() - data = list(data) - - for item in data: - postprocessMovie(item) - - # Make timestamp pretty - item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') - item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))}) - - return jsonify(data=data) - - @authenticate - def post(self): - radarr_id = int(request.args.get('radarrid')) - provider = request.form.get('provider') - subs_id = request.form.get('subs_id') - language = request.form.get('language') - # TODO - forced = False - hi = False - - data = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == radarr_id).dicts().get() - - media_path = data['path'] - subtitles_path = request.form.get('subtitles_path') - - blacklist_log_movie(radarr_id=radarr_id, - provider=provider, - subs_id=subs_id, - language=language) - delete_subtitles(media_type='movie', - language=language, - forced=forced, - hi=hi, - media_path=path_mappings.path_replace_movie(media_path), - subtitles_path=subtitles_path, - radarr_id=radarr_id) - movies_download_subtitles(radarr_id) - event_stream(type='movie-history') - return '', 200 - - @authenticate - def delete(self): - if request.args.get("all") == "true": - blacklist_delete_all_movie() - else: - provider = request.form.get('provider') - subs_id = request.form.get('subs_id') - blacklist_delete_movie(provider=provider, subs_id=subs_id) - return '', 200 - - -class Subtitles(Resource): - @authenticate - def patch(self): - action = request.args.get('action') - - language = request.form.get('language') - subtitles_path = request.form.get('path') - media_type = request.form.get('type') - id = request.form.get('id') - - if media_type == 'episode': - subtitles_path = path_mappings.path_replace(subtitles_path) - metadata = TableEpisodes.select(TableEpisodes.path, TableEpisodes.sonarrSeriesId)\ - .where(TableEpisodes.sonarrEpisodeId == id)\ - .dicts()\ - .get() - video_path = path_mappings.path_replace(metadata['path']) - else: - subtitles_path = path_mappings.path_replace_movie(subtitles_path) - metadata = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == id).dicts().get() - video_path = path_mappings.path_replace_movie(metadata['path']) - - if action == 'sync': - if media_type == 'episode': - subsync.sync(video_path=video_path, srt_path=subtitles_path, - srt_lang=language, media_type='series', sonarr_series_id=metadata['sonarrSeriesId'], - sonarr_episode_id=int(id)) - else: - subsync.sync(video_path=video_path, srt_path=subtitles_path, - srt_lang=language, media_type='movies', radarr_id=id) - elif action == 'translate': - dest_language = language - forced = True if request.form.get('forced') == 'true' else False - hi = True if request.form.get('hi') == 'true' else False - result = translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path, - to_lang=dest_language, - forced=forced, hi=hi) - if result: - if media_type == 'episode': - store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) - else: - store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) - return '', 200 - else: - return '', 404 - 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 - - -class SubtitleNameInfo(Resource): - @authenticate - def get(self): - names = request.args.getlist('filenames[]') - results = [] - for name in names: - opts = dict() - opts['type'] = 'episode' - guessit_result = guessit(name, options=opts) - result = {} - result['filename'] = name - if 'subtitle_language' in guessit_result: - result['subtitle_language'] = str(guessit_result['subtitle_language']) - - result['episode'] = 0 - if 'episode' in guessit_result: - if isinstance(guessit_result['episode'], list): - # for multiple episodes file, choose the first episode number - if len(guessit_result['episode']): - # make sure that guessit returned a list of more than 0 items - result['episode'] = int(guessit_result['episode'][0]) - elif isinstance(guessit_result['episode'], (str, int)): - # if single episode (should be int but just in case we cast it to int) - result['episode'] = int(guessit_result['episode']) - - if 'season' in guessit_result: - result['season'] = int(guessit_result['season']) - else: - result['season'] = 0 - - results.append(result) - - return jsonify(data=results) - - -class BrowseBazarrFS(Resource): - @authenticate - def get(self): - path = request.args.get('path') or '' - data = [] - try: - result = browse_bazarr_filesystem(path) - if result is None: - raise ValueError - except Exception: - return jsonify([]) - for item in result['directories']: - data.append({'name': item['name'], 'children': True, 'path': item['path']}) - return jsonify(data) - - -class BrowseSonarrFS(Resource): - @authenticate - def get(self): - path = request.args.get('path') or '' - data = [] - try: - result = browse_sonarr_filesystem(path) - if result is None: - raise ValueError - except Exception: - return jsonify([]) - for item in result['directories']: - data.append({'name': item['name'], 'children': True, 'path': item['path']}) - return jsonify(data) - - -class BrowseRadarrFS(Resource): - @authenticate - def get(self): - path = request.args.get('path') or '' - data = [] - try: - result = browse_radarr_filesystem(path) - if result is None: - raise ValueError - except Exception: - return jsonify([]) - for item in result['directories']: - data.append({'name': item['name'], 'children': True, 'path': item['path']}) - 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') -api.add_resource(ProviderMovies, '/providers/movies') -api.add_resource(ProviderEpisodes, '/providers/episodes') - -api.add_resource(System, '/system') -api.add_resource(Searches, "/system/searches") -api.add_resource(SystemAccount, '/system/account') -api.add_resource(SystemTasks, '/system/tasks') -api.add_resource(SystemLogs, '/system/logs') -api.add_resource(SystemStatus, '/system/status') -api.add_resource(SystemHealth, '/system/health') -api.add_resource(SystemReleases, '/system/releases') -api.add_resource(SystemSettings, '/system/settings') -api.add_resource(Languages, '/system/languages') -api.add_resource(LanguagesProfiles, '/system/languages/profiles') -api.add_resource(Notifications, '/system/notifications') - -api.add_resource(Subtitles, '/subtitles') -api.add_resource(SubtitleNameInfo, '/subtitles/info') - -api.add_resource(Series, '/series') - -api.add_resource(Episodes, '/episodes') -api.add_resource(EpisodesWanted, '/episodes/wanted') -api.add_resource(EpisodesSubtitles, '/episodes/subtitles') -api.add_resource(EpisodesHistory, '/episodes/history') -api.add_resource(EpisodesBlacklist, '/episodes/blacklist') - -api.add_resource(Movies, '/movies') -api.add_resource(MoviesWanted, '/movies/wanted') -api.add_resource(MoviesSubtitles, '/movies/subtitles') -api.add_resource(MoviesHistory, '/movies/history') -api.add_resource(MoviesBlacklist, '/movies/blacklist') - -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/api/__init__.py b/bazarr/api/__init__.py new file mode 100644 index 000000000..85d075435 --- /dev/null +++ b/bazarr/api/__init__.py @@ -0,0 +1,25 @@ +# coding=utf-8 + +from .badges import api_bp_badges +from .system import api_bp_system +from .series import api_bp_series +from .episodes import api_bp_episodes +from .providers import api_bp_providers +from .subtitles import api_bp_subtitles +from .webhooks import api_bp_webhooks +from .history import api_bp_history +from .files import api_bp_files +from .movies import api_bp_movies + +api_bp_list = [ + api_bp_badges, + api_bp_system, + api_bp_series, + api_bp_episodes, + api_bp_providers, + api_bp_subtitles, + api_bp_webhooks, + api_bp_history, + api_bp_files, + api_bp_movies +] diff --git a/bazarr/api/badges/__init__.py b/bazarr/api/badges/__init__.py new file mode 100644 index 000000000..e5254c9b9 --- /dev/null +++ b/bazarr/api/badges/__init__.py @@ -0,0 +1,12 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .badges import Badges + + +api_bp_badges = Blueprint('api_badges', __name__) +api = Api(api_bp_badges) + +api.add_resource(Badges, '/badges') diff --git a/bazarr/api/badges/badges.py b/bazarr/api/badges/badges.py new file mode 100644 index 000000000..4d1b6ddf2 --- /dev/null +++ b/bazarr/api/badges/badges.py @@ -0,0 +1,47 @@ +# coding=utf-8 + +from flask import jsonify +from flask_restful import Resource + +import operator +from functools import reduce + +from database import get_exclusion_clause, TableEpisodes, TableShows, TableMovies +from get_providers import get_throttled_providers +from utils import get_health_issues + +from ..utils import authenticate + + +class Badges(Resource): + @authenticate + def get(self): + episodes_conditions = [(TableEpisodes.missing_subtitles is not None), + (TableEpisodes.missing_subtitles != '[]')] + episodes_conditions += get_exclusion_clause('series') + missing_episodes = TableEpisodes.select(TableShows.tags, + TableShows.seriesType, + TableEpisodes.monitored)\ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(reduce(operator.and_, episodes_conditions))\ + .count() + + movies_conditions = [(TableMovies.missing_subtitles is not None), + (TableMovies.missing_subtitles != '[]')] + movies_conditions += get_exclusion_clause('movie') + missing_movies = TableMovies.select(TableMovies.tags, + TableMovies.monitored)\ + .where(reduce(operator.and_, movies_conditions))\ + .count() + + throttled_providers = len(eval(str(get_throttled_providers()))) + + health_issues = len(get_health_issues()) + + result = { + "episodes": missing_episodes, + "movies": missing_movies, + "providers": throttled_providers, + "status": health_issues + } + return jsonify(result) diff --git a/bazarr/api/episodes/__init__.py b/bazarr/api/episodes/__init__.py new file mode 100644 index 000000000..2a5680ae7 --- /dev/null +++ b/bazarr/api/episodes/__init__.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .episodes import Episodes +from .episodes_subtitles import EpisodesSubtitles +from .history import EpisodesHistory +from .wanted import EpisodesWanted +from .blacklist import EpisodesBlacklist + + +api_bp_episodes = Blueprint('api_episodes', __name__) +api = Api(api_bp_episodes) + +api.add_resource(Episodes, '/episodes') +api.add_resource(EpisodesWanted, '/episodes/wanted') +api.add_resource(EpisodesSubtitles, '/episodes/subtitles') +api.add_resource(EpisodesHistory, '/episodes/history') +api.add_resource(EpisodesBlacklist, '/episodes/blacklist') diff --git a/bazarr/api/episodes/blacklist.py b/bazarr/api/episodes/blacklist.py new file mode 100644 index 000000000..800ad4ed8 --- /dev/null +++ b/bazarr/api/episodes/blacklist.py @@ -0,0 +1,92 @@ +# coding=utf-8 + +import datetime +import pretty + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableEpisodes, TableShows, TableBlacklist +from ..utils import authenticate, postprocessEpisode +from utils import blacklist_log, delete_subtitles, blacklist_delete_all, blacklist_delete +from helper import path_mappings +from get_subtitle import episode_download_subtitles +from event_handler import event_stream + + +# GET: get blacklist +# POST: add blacklist +# DELETE: remove blacklist +class EpisodesBlacklist(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + + data = TableBlacklist.select(TableShows.title.alias('seriesTitle'), + TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), + TableEpisodes.title.alias('episodeTitle'), + TableEpisodes.sonarrSeriesId, + TableBlacklist.provider, + TableBlacklist.subs_id, + TableBlacklist.language, + TableBlacklist.timestamp)\ + .join(TableEpisodes, on=(TableBlacklist.sonarr_episode_id == TableEpisodes.sonarrEpisodeId))\ + .join(TableShows, on=(TableBlacklist.sonarr_series_id == TableShows.sonarrSeriesId))\ + .order_by(TableBlacklist.timestamp.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + data = list(data) + + for item in data: + # Make timestamp pretty + item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') + item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))}) + + postprocessEpisode(item) + + return jsonify(data=data) + + @authenticate + def post(self): + sonarr_series_id = int(request.args.get('seriesid')) + sonarr_episode_id = int(request.args.get('episodeid')) + provider = request.form.get('provider') + subs_id = request.form.get('subs_id') + language = request.form.get('language') + + episodeInfo = TableEpisodes.select(TableEpisodes.path)\ + .where(TableEpisodes.sonarrEpisodeId == sonarr_episode_id)\ + .dicts()\ + .get() + + media_path = episodeInfo['path'] + subtitles_path = request.form.get('subtitles_path') + + blacklist_log(sonarr_series_id=sonarr_series_id, + sonarr_episode_id=sonarr_episode_id, + provider=provider, + subs_id=subs_id, + language=language) + delete_subtitles(media_type='series', + language=language, + forced=False, + hi=False, + media_path=path_mappings.path_replace(media_path), + subtitles_path=subtitles_path, + sonarr_series_id=sonarr_series_id, + sonarr_episode_id=sonarr_episode_id) + episode_download_subtitles(sonarr_episode_id) + event_stream(type='episode-history') + return '', 200 + + @authenticate + def delete(self): + if request.args.get("all") == "true": + blacklist_delete_all() + else: + provider = request.form.get('provider') + subs_id = request.form.get('subs_id') + blacklist_delete(provider=provider, subs_id=subs_id) + return '', 204 diff --git a/bazarr/api/episodes/episodes.py b/bazarr/api/episodes/episodes.py new file mode 100644 index 000000000..1ce718c48 --- /dev/null +++ b/bazarr/api/episodes/episodes.py @@ -0,0 +1,30 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableEpisodes +from ..utils import authenticate, postprocessEpisode + + +class Episodes(Resource): + @authenticate + def get(self): + seriesId = request.args.getlist('seriesid[]') + episodeId = request.args.getlist('episodeid[]') + + if len(episodeId) > 0: + result = TableEpisodes.select().where(TableEpisodes.sonarrEpisodeId.in_(episodeId)).dicts() + elif len(seriesId) > 0: + result = TableEpisodes.select()\ + .where(TableEpisodes.sonarrSeriesId.in_(seriesId))\ + .order_by(TableEpisodes.season.desc(), TableEpisodes.episode.desc())\ + .dicts() + else: + return "Series or Episode ID not provided", 400 + + result = list(result) + for item in result: + postprocessEpisode(item) + + return jsonify(data=result) diff --git a/bazarr/api/episodes/episodes_subtitles.py b/bazarr/api/episodes/episodes_subtitles.py new file mode 100644 index 000000000..86f8b0024 --- /dev/null +++ b/bazarr/api/episodes/episodes_subtitles.py @@ -0,0 +1,177 @@ +# coding=utf-8 + +import os + +from flask import request +from flask_restful import Resource +from subliminal_patch.core import SUBTITLE_EXTENSIONS + +from database import TableEpisodes, get_audio_profile_languages +from ..utils import authenticate +from helper import path_mappings +from get_providers import get_providers, get_providers_auth +from get_subtitle import download_subtitle, manual_upload_subtitle +from utils import history_log, delete_subtitles +from notifier import send_notifications +from list_subtitles import store_subtitles +from event_handler import event_stream +from config import settings + + +# PATCH: Download Subtitles +# POST: Upload Subtitles +# DELETE: Delete Subtitles +class EpisodesSubtitles(Resource): + @authenticate + def patch(self): + sonarrSeriesId = request.args.get('seriesid') + sonarrEpisodeId = request.args.get('episodeid') + episodeInfo = TableEpisodes.select(TableEpisodes.title, + TableEpisodes.path, + TableEpisodes.scene_name, + TableEpisodes.audio_language)\ + .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ + .dicts()\ + .get() + + title = episodeInfo['title'] + episodePath = path_mappings.path_replace(episodeInfo['path']) + sceneName = episodeInfo['scene_name'] + audio_language = episodeInfo['audio_language'] + if sceneName is None: sceneName = "None" + + language = request.form.get('language') + hi = request.form.get('hi').capitalize() + forced = request.form.get('forced').capitalize() + + providers_list = get_providers() + providers_auth = get_providers_auth() + + audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId) + if len(audio_language_list) > 0: + audio_language = audio_language_list[0]['name'] + else: + audio_language = None + + try: + result = download_subtitle(episodePath, language, audio_language, hi, forced, providers_list, + providers_auth, sceneName, title, 'series') + if result is not None: + message = result[0] + path = result[1] + forced = result[5] + if result[8]: + language_code = result[2] + ":hi" + elif forced: + language_code = result[2] + ":forced" + else: + language_code = result[2] + provider = result[3] + score = result[4] + subs_id = result[6] + subs_path = result[7] + history_log(1, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id, + subs_path) + send_notifications(sonarrSeriesId, sonarrEpisodeId, message) + store_subtitles(path, episodePath) + else: + event_stream(type='episode', payload=sonarrEpisodeId) + + except OSError: + pass + + return '', 204 + + @authenticate + def post(self): + sonarrSeriesId = request.args.get('seriesid') + sonarrEpisodeId = request.args.get('episodeid') + episodeInfo = TableEpisodes.select(TableEpisodes.title, + TableEpisodes.path, + TableEpisodes.scene_name, + TableEpisodes.audio_language)\ + .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ + .dicts()\ + .get() + + title = episodeInfo['title'] + episodePath = path_mappings.path_replace(episodeInfo['path']) + sceneName = episodeInfo['scene_name'] + audio_language = episodeInfo['audio_language'] + if sceneName is None: sceneName = "None" + + language = request.form.get('language') + forced = True if request.form.get('forced') == 'true' else False + hi = True if request.form.get('hi') == 'true' else False + subFile = request.files.get('file') + + _, ext = os.path.splitext(subFile.filename) + + if ext not in SUBTITLE_EXTENSIONS: + raise ValueError('A subtitle of an invalid format was uploaded.') + + try: + result = manual_upload_subtitle(path=episodePath, + language=language, + forced=forced, + hi=hi, + title=title, + scene_name=sceneName, + media_type='series', + subtitle=subFile, + audio_language=audio_language) + + if result is not None: + message = result[0] + path = result[1] + subs_path = result[2] + if hi: + language_code = language + ":hi" + elif forced: + language_code = language + ":forced" + else: + language_code = language + provider = "manual" + score = 360 + history_log(4, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, + subtitles_path=subs_path) + if not settings.general.getboolean('dont_notify_manual_actions'): + send_notifications(sonarrSeriesId, sonarrEpisodeId, message) + store_subtitles(path, episodePath) + + except OSError: + pass + + return '', 204 + + @authenticate + def delete(self): + sonarrSeriesId = request.args.get('seriesid') + sonarrEpisodeId = request.args.get('episodeid') + episodeInfo = TableEpisodes.select(TableEpisodes.title, + TableEpisodes.path, + TableEpisodes.scene_name, + TableEpisodes.audio_language)\ + .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\ + .dicts()\ + .get() + + episodePath = path_mappings.path_replace(episodeInfo['path']) + + language = request.form.get('language') + forced = request.form.get('forced') + hi = request.form.get('hi') + subtitlesPath = request.form.get('path') + + subtitlesPath = path_mappings.path_replace_reverse(subtitlesPath) + + delete_subtitles(media_type='series', + language=language, + forced=forced, + hi=hi, + media_path=episodePath, + subtitles_path=subtitlesPath, + sonarr_series_id=sonarrSeriesId, + sonarr_episode_id=sonarrEpisodeId) + + return '', 204 diff --git a/bazarr/api/episodes/history.py b/bazarr/api/episodes/history.py new file mode 100644 index 000000000..c3b4cab7c --- /dev/null +++ b/bazarr/api/episodes/history.py @@ -0,0 +1,133 @@ +# coding=utf-8 + +import datetime +import os +import operator +import pretty + +from flask import request, jsonify +from flask_restful import Resource +from functools import reduce +from peewee import fn +from datetime import timedelta + +from database import get_exclusion_clause, TableEpisodes, TableShows, TableHistory, TableBlacklist +from ..utils import authenticate, postprocessEpisode +from config import settings +from helper import path_mappings + + +class EpisodesHistory(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + episodeid = request.args.get('episodeid') + + upgradable_episodes_not_perfect = [] + if settings.general.getboolean('upgrade_subs'): + days_to_upgrade_subs = settings.general.days_to_upgrade_subs + minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) - + datetime.datetime(1970, 1, 1)).total_seconds() + + if settings.general.getboolean('upgrade_manual'): + query_actions = [1, 2, 3, 6] + else: + query_actions = [1, 3] + + upgradable_episodes_conditions = [(TableHistory.action.in_(query_actions)), + (TableHistory.timestamp > minimum_timestamp), + (TableHistory.score is not None)] + upgradable_episodes_conditions += get_exclusion_clause('series') + upgradable_episodes = TableHistory.select(TableHistory.video_path, + fn.MAX(TableHistory.timestamp).alias('timestamp'), + TableHistory.score, + TableShows.tags, + TableEpisodes.monitored, + TableShows.seriesType)\ + .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ + .join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(reduce(operator.and_, upgradable_episodes_conditions))\ + .group_by(TableHistory.video_path)\ + .dicts() + upgradable_episodes = list(upgradable_episodes) + for upgradable_episode in upgradable_episodes: + if upgradable_episode['timestamp'] > minimum_timestamp: + try: + int(upgradable_episode['score']) + except ValueError: + pass + else: + if int(upgradable_episode['score']) < 360: + upgradable_episodes_not_perfect.append(upgradable_episode) + + query_conditions = [(TableEpisodes.title is not None)] + if episodeid: + query_conditions.append((TableEpisodes.sonarrEpisodeId == episodeid)) + query_condition = reduce(operator.and_, query_conditions) + episode_history = TableHistory.select(TableHistory.id, + TableShows.title.alias('seriesTitle'), + TableEpisodes.monitored, + TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), + TableEpisodes.title.alias('episodeTitle'), + TableHistory.timestamp, + TableHistory.subs_id, + TableHistory.description, + TableHistory.sonarrSeriesId, + TableEpisodes.path, + TableHistory.language, + TableHistory.score, + TableShows.tags, + TableHistory.action, + TableHistory.subtitles_path, + TableHistory.sonarrEpisodeId, + TableHistory.provider, + TableShows.seriesType)\ + .join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ + .where(query_condition)\ + .order_by(TableHistory.timestamp.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + episode_history = list(episode_history) + + blacklist_db = TableBlacklist.select(TableBlacklist.provider, TableBlacklist.subs_id).dicts() + blacklist_db = list(blacklist_db) + + for item in episode_history: + # Mark episode as upgradable or not + item.update({"upgradable": False}) + if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']), + "tags": str(item['tags']), "monitored": str(item['monitored']), + "seriesType": str(item['seriesType'])} in upgradable_episodes_not_perfect: + if os.path.isfile(path_mappings.path_replace(item['subtitles_path'])): + item.update({"upgradable": True}) + + del item['path'] + + postprocessEpisode(item) + + if item['score']: + item['score'] = str(round((int(item['score']) * 100 / 360), 2)) + "%" + + # Make timestamp pretty + if item['timestamp']: + item["raw_timestamp"] = int(item['timestamp']) + item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') + item['timestamp'] = pretty.date(item["raw_timestamp"]) + + # Check if subtitles is blacklisted + item.update({"blacklisted": False}) + if item['action'] not in [0, 4, 5]: + for blacklisted_item in blacklist_db: + if blacklisted_item['provider'] == item['provider'] and \ + blacklisted_item['subs_id'] == item['subs_id']: + item.update({"blacklisted": True}) + break + + count = TableHistory.select()\ + .join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\ + .where(TableEpisodes.title is not None).count() + + return jsonify(data=episode_history, total=count) diff --git a/bazarr/api/episodes/wanted.py b/bazarr/api/episodes/wanted.py new file mode 100644 index 000000000..3634a69b3 --- /dev/null +++ b/bazarr/api/episodes/wanted.py @@ -0,0 +1,74 @@ +# coding=utf-8 + +import operator + +from flask import request, jsonify +from flask_restful import Resource +from functools import reduce + +from database import get_exclusion_clause, TableEpisodes, TableShows +from ..utils import authenticate, postprocessEpisode + + +# GET: Get Wanted Episodes +class EpisodesWanted(Resource): + @authenticate + def get(self): + episodeid = request.args.getlist('episodeid[]') + + wanted_conditions = [(TableEpisodes.missing_subtitles != '[]')] + if len(episodeid) > 0: + wanted_conditions.append((TableEpisodes.sonarrEpisodeId in episodeid)) + wanted_conditions += get_exclusion_clause('series') + wanted_condition = reduce(operator.and_, wanted_conditions) + + if len(episodeid) > 0: + data = TableEpisodes.select(TableShows.title.alias('seriesTitle'), + TableEpisodes.monitored, + TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), + TableEpisodes.title.alias('episodeTitle'), + TableEpisodes.missing_subtitles, + TableEpisodes.sonarrSeriesId, + TableEpisodes.sonarrEpisodeId, + TableEpisodes.scene_name.alias('sceneName'), + TableShows.tags, + TableEpisodes.failedAttempts, + TableShows.seriesType)\ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(wanted_condition)\ + .dicts() + else: + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + data = TableEpisodes.select(TableShows.title.alias('seriesTitle'), + TableEpisodes.monitored, + TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'), + TableEpisodes.title.alias('episodeTitle'), + TableEpisodes.missing_subtitles, + TableEpisodes.sonarrSeriesId, + TableEpisodes.sonarrEpisodeId, + TableEpisodes.scene_name.alias('sceneName'), + TableShows.tags, + TableEpisodes.failedAttempts, + TableShows.seriesType)\ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(wanted_condition)\ + .order_by(TableEpisodes.rowid.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + data = list(data) + + for item in data: + postprocessEpisode(item) + + count_conditions = [(TableEpisodes.missing_subtitles != '[]')] + count_conditions += get_exclusion_clause('series') + count = TableEpisodes.select(TableShows.tags, + TableShows.seriesType, + TableEpisodes.monitored)\ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(reduce(operator.and_, count_conditions))\ + .count() + + return jsonify(data=data, total=count) diff --git a/bazarr/api/files/__init__.py b/bazarr/api/files/__init__.py new file mode 100644 index 000000000..7c78565c9 --- /dev/null +++ b/bazarr/api/files/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .files import BrowseBazarrFS +from .files_sonarr import BrowseSonarrFS +from .files_radarr import BrowseRadarrFS + + +api_bp_files = Blueprint('api_files', __name__) +api = Api(api_bp_files) + +api.add_resource(BrowseBazarrFS, '/files') +api.add_resource(BrowseSonarrFS, '/files/sonarr') +api.add_resource(BrowseRadarrFS, '/files/radarr') diff --git a/bazarr/api/files/files.py b/bazarr/api/files/files.py new file mode 100644 index 000000000..bcec93b75 --- /dev/null +++ b/bazarr/api/files/files.py @@ -0,0 +1,24 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from filesystem import browse_bazarr_filesystem + +from ..utils import authenticate + + +class BrowseBazarrFS(Resource): + @authenticate + def get(self): + path = request.args.get('path') or '' + data = [] + try: + result = browse_bazarr_filesystem(path) + if result is None: + raise ValueError + except Exception: + return jsonify([]) + for item in result['directories']: + data.append({'name': item['name'], 'children': True, 'path': item['path']}) + return jsonify(data) diff --git a/bazarr/api/files/files_radarr.py b/bazarr/api/files/files_radarr.py new file mode 100644 index 000000000..156d73d33 --- /dev/null +++ b/bazarr/api/files/files_radarr.py @@ -0,0 +1,24 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from filesystem import browse_radarr_filesystem + +from ..utils import authenticate + + +class BrowseRadarrFS(Resource): + @authenticate + def get(self): + path = request.args.get('path') or '' + data = [] + try: + result = browse_radarr_filesystem(path) + if result is None: + raise ValueError + except Exception: + return jsonify([]) + for item in result['directories']: + data.append({'name': item['name'], 'children': True, 'path': item['path']}) + return jsonify(data) diff --git a/bazarr/api/files/files_sonarr.py b/bazarr/api/files/files_sonarr.py new file mode 100644 index 000000000..0007157dc --- /dev/null +++ b/bazarr/api/files/files_sonarr.py @@ -0,0 +1,24 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from filesystem import browse_sonarr_filesystem + +from ..utils import authenticate + + +class BrowseSonarrFS(Resource): + @authenticate + def get(self): + path = request.args.get('path') or '' + data = [] + try: + result = browse_sonarr_filesystem(path) + if result is None: + raise ValueError + except Exception: + return jsonify([]) + for item in result['directories']: + data.append({'name': item['name'], 'children': True, 'path': item['path']}) + return jsonify(data) diff --git a/bazarr/api/history/__init__.py b/bazarr/api/history/__init__.py new file mode 100644 index 000000000..2783a1233 --- /dev/null +++ b/bazarr/api/history/__init__.py @@ -0,0 +1,12 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .stats import HistoryStats + + +api_bp_history = Blueprint('api_history', __name__) +api = Api(api_bp_history) + +api.add_resource(HistoryStats, '/history/stats') diff --git a/bazarr/api/history/stats.py b/bazarr/api/history/stats.py new file mode 100644 index 000000000..b9651399d --- /dev/null +++ b/bazarr/api/history/stats.py @@ -0,0 +1,85 @@ +# coding=utf-8 + +import time +import datetime +import operator + +from dateutil import rrule +from flask import request, jsonify +from flask_restful import Resource +from functools import reduce +from peewee import fn + +from database import TableHistory, TableHistoryMovie + +from ..utils import authenticate + + +class HistoryStats(Resource): + @authenticate + def get(self): + timeframe = request.args.get('timeframe') or 'month' + action = request.args.get('action') or 'All' + provider = request.args.get('provider') or 'All' + language = request.args.get('language') or 'All' + + # timeframe must be in ['week', 'month', 'trimester', 'year'] + if timeframe == 'year': + delay = 364 * 24 * 60 * 60 + elif timeframe == 'trimester': + delay = 90 * 24 * 60 * 60 + elif timeframe == 'month': + delay = 30 * 24 * 60 * 60 + elif timeframe == 'week': + delay = 6 * 24 * 60 * 60 + + now = time.time() + past = now - delay + + history_where_clauses = [(TableHistory.timestamp.between(past, now))] + history_where_clauses_movie = [(TableHistoryMovie.timestamp.between(past, now))] + + if action != 'All': + history_where_clauses.append((TableHistory.action == action)) + history_where_clauses_movie.append((TableHistoryMovie.action == action)) + else: + history_where_clauses.append((TableHistory.action.in_([1, 2, 3]))) + history_where_clauses_movie.append((TableHistoryMovie.action.in_([1, 2, 3]))) + + if provider != 'All': + history_where_clauses.append((TableHistory.provider == provider)) + history_where_clauses_movie.append((TableHistoryMovie.provider == provider)) + + if language != 'All': + history_where_clauses.append((TableHistory.language == language)) + history_where_clauses_movie.append((TableHistoryMovie.language == language)) + + history_where_clause = reduce(operator.and_, history_where_clauses) + history_where_clause_movie = reduce(operator.and_, history_where_clauses_movie) + + data_series = TableHistory.select(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch').alias('date'), + fn.COUNT(TableHistory.id).alias('count'))\ + .where(history_where_clause) \ + .group_by(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch'))\ + .dicts() + data_series = list(data_series) + + data_movies = TableHistoryMovie.select(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch').alias('date'), + fn.COUNT(TableHistoryMovie.id).alias('count')) \ + .where(history_where_clause_movie) \ + .group_by(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch')) \ + .dicts() + data_movies = list(data_movies) + + for dt in rrule.rrule(rrule.DAILY, + dtstart=datetime.datetime.now() - datetime.timedelta(seconds=delay), + until=datetime.datetime.now()): + if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_series): + data_series.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0}) + if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_movies): + data_movies.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0}) + + sorted_data_series = sorted(data_series, key=lambda i: i['date']) + sorted_data_movies = sorted(data_movies, key=lambda i: i['date']) + + return jsonify(series=sorted_data_series, movies=sorted_data_movies) diff --git a/bazarr/api/movies/__init__.py b/bazarr/api/movies/__init__.py new file mode 100644 index 000000000..eb691f529 --- /dev/null +++ b/bazarr/api/movies/__init__.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .movies import Movies +from .movies_subtitles import MoviesSubtitles +from .history import MoviesHistory +from .wanted import MoviesWanted +from .blacklist import MoviesBlacklist + + +api_bp_movies = Blueprint('api_movies', __name__) +api = Api(api_bp_movies) + +api.add_resource(Movies, '/movies') +api.add_resource(MoviesWanted, '/movies/wanted') +api.add_resource(MoviesSubtitles, '/movies/subtitles') +api.add_resource(MoviesHistory, '/movies/history') +api.add_resource(MoviesBlacklist, '/movies/blacklist') diff --git a/bazarr/api/movies/blacklist.py b/bazarr/api/movies/blacklist.py new file mode 100644 index 000000000..cda24f348 --- /dev/null +++ b/bazarr/api/movies/blacklist.py @@ -0,0 +1,86 @@ +# coding=utf-8 + +import datetime +import pretty + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableMovies, TableBlacklistMovie +from ..utils import authenticate, postprocessMovie +from utils import blacklist_log_movie, delete_subtitles, blacklist_delete_all_movie, blacklist_delete_movie +from helper import path_mappings +from get_subtitle import movies_download_subtitles +from event_handler import event_stream + + +# GET: get blacklist +# POST: add blacklist +# DELETE: remove blacklist +class MoviesBlacklist(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + + data = TableBlacklistMovie.select(TableMovies.title, + TableMovies.radarrId, + TableBlacklistMovie.provider, + TableBlacklistMovie.subs_id, + TableBlacklistMovie.language, + TableBlacklistMovie.timestamp)\ + .join(TableMovies, on=(TableBlacklistMovie.radarr_id == TableMovies.radarrId))\ + .order_by(TableBlacklistMovie.timestamp.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + data = list(data) + + for item in data: + postprocessMovie(item) + + # Make timestamp pretty + item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') + item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))}) + + return jsonify(data=data) + + @authenticate + def post(self): + radarr_id = int(request.args.get('radarrid')) + provider = request.form.get('provider') + subs_id = request.form.get('subs_id') + language = request.form.get('language') + # TODO + forced = False + hi = False + + data = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == radarr_id).dicts().get() + + media_path = data['path'] + subtitles_path = request.form.get('subtitles_path') + + blacklist_log_movie(radarr_id=radarr_id, + provider=provider, + subs_id=subs_id, + language=language) + delete_subtitles(media_type='movie', + language=language, + forced=forced, + hi=hi, + media_path=path_mappings.path_replace_movie(media_path), + subtitles_path=subtitles_path, + radarr_id=radarr_id) + movies_download_subtitles(radarr_id) + event_stream(type='movie-history') + return '', 200 + + @authenticate + def delete(self): + if request.args.get("all") == "true": + blacklist_delete_all_movie() + else: + provider = request.form.get('provider') + subs_id = request.form.get('subs_id') + blacklist_delete_movie(provider=provider, subs_id=subs_id) + return '', 200 diff --git a/bazarr/api/movies/history.py b/bazarr/api/movies/history.py new file mode 100644 index 000000000..13e3c6d88 --- /dev/null +++ b/bazarr/api/movies/history.py @@ -0,0 +1,129 @@ +# coding=utf-8 + +import datetime +import os +import operator +import pretty + +from flask import request, jsonify +from flask_restful import Resource +from functools import reduce +from peewee import fn +from datetime import timedelta + +from database import get_exclusion_clause, TableMovies, TableHistoryMovie, TableBlacklistMovie +from ..utils import authenticate, postprocessMovie +from config import settings +from helper import path_mappings + + +class MoviesHistory(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + radarrid = request.args.get('radarrid') + + upgradable_movies = [] + upgradable_movies_not_perfect = [] + if settings.general.getboolean('upgrade_subs'): + days_to_upgrade_subs = settings.general.days_to_upgrade_subs + minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) - + datetime.datetime(1970, 1, 1)).total_seconds() + + if settings.general.getboolean('upgrade_manual'): + query_actions = [1, 2, 3, 6] + else: + query_actions = [1, 3] + + upgradable_movies_conditions = [(TableHistoryMovie.action.in_(query_actions)), + (TableHistoryMovie.timestamp > minimum_timestamp), + (TableHistoryMovie.score is not None)] + upgradable_movies_conditions += get_exclusion_clause('movie') + upgradable_movies = TableHistoryMovie.select(TableHistoryMovie.video_path, + fn.MAX(TableHistoryMovie.timestamp).alias('timestamp'), + TableHistoryMovie.score, + TableMovies.tags, + TableMovies.monitored)\ + .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ + .where(reduce(operator.and_, upgradable_movies_conditions))\ + .group_by(TableHistoryMovie.video_path)\ + .dicts() + upgradable_movies = list(upgradable_movies) + + for upgradable_movie in upgradable_movies: + if upgradable_movie['timestamp'] > minimum_timestamp: + try: + int(upgradable_movie['score']) + except ValueError: + pass + else: + if int(upgradable_movie['score']) < 120: + upgradable_movies_not_perfect.append(upgradable_movie) + + query_conditions = [(TableMovies.title is not None)] + if radarrid: + query_conditions.append((TableMovies.radarrId == radarrid)) + query_condition = reduce(operator.and_, query_conditions) + + movie_history = TableHistoryMovie.select(TableHistoryMovie.id, + TableHistoryMovie.action, + TableMovies.title, + TableHistoryMovie.timestamp, + TableHistoryMovie.description, + TableHistoryMovie.radarrId, + TableMovies.monitored, + TableHistoryMovie.video_path.alias('path'), + TableHistoryMovie.language, + TableMovies.tags, + TableHistoryMovie.score, + TableHistoryMovie.subs_id, + TableHistoryMovie.provider, + TableHistoryMovie.subtitles_path)\ + .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ + .where(query_condition)\ + .order_by(TableHistoryMovie.timestamp.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + movie_history = list(movie_history) + + blacklist_db = TableBlacklistMovie.select(TableBlacklistMovie.provider, TableBlacklistMovie.subs_id).dicts() + blacklist_db = list(blacklist_db) + + for item in movie_history: + # Mark movies as upgradable or not + item.update({"upgradable": False}) + if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']), + "tags": str(item['tags']), "monitored": str(item['monitored'])} in upgradable_movies_not_perfect: + if os.path.isfile(path_mappings.path_replace_movie(item['subtitles_path'])): + item.update({"upgradable": True}) + + del item['path'] + + postprocessMovie(item) + + if item['score']: + item['score'] = str(round((int(item['score']) * 100 / 120), 2)) + "%" + + # Make timestamp pretty + if item['timestamp']: + item["raw_timestamp"] = int(item['timestamp']) + item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X') + item['timestamp'] = pretty.date(item["raw_timestamp"]) + + # Check if subtitles is blacklisted + item.update({"blacklisted": False}) + if item['action'] not in [0, 4, 5]: + for blacklisted_item in blacklist_db: + if blacklisted_item['provider'] == item['provider'] and blacklisted_item['subs_id'] == item[ + 'subs_id']: + item.update({"blacklisted": True}) + break + + count = TableHistoryMovie.select()\ + .join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\ + .where(TableMovies.title is not None)\ + .count() + + return jsonify(data=movie_history, total=count) diff --git a/bazarr/api/movies/movies.py b/bazarr/api/movies/movies.py new file mode 100644 index 000000000..d088a71a5 --- /dev/null +++ b/bazarr/api/movies/movies.py @@ -0,0 +1,80 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableMovies +from ..utils import authenticate, postprocessMovie, None_Keys +from list_subtitles import list_missing_subtitles_movies, movies_scan_subtitles +from event_handler import event_stream +from get_subtitle import movies_download_subtitles, wanted_search_missing_subtitles_movies + + +class Movies(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + radarrId = request.args.getlist('radarrid[]') + + count = TableMovies.select().count() + + if len(radarrId) != 0: + result = TableMovies.select()\ + .where(TableMovies.radarrId.in_(radarrId))\ + .order_by(TableMovies.sortTitle)\ + .dicts() + else: + result = TableMovies.select().order_by(TableMovies.sortTitle).limit(length).offset(start).dicts() + result = list(result) + for item in result: + postprocessMovie(item) + + return jsonify(data=result, total=count) + + @authenticate + def post(self): + radarrIdList = request.form.getlist('radarrid') + profileIdList = request.form.getlist('profileid') + + for idx in range(len(radarrIdList)): + radarrId = radarrIdList[idx] + profileId = profileIdList[idx] + + if profileId in None_Keys: + profileId = None + else: + try: + profileId = int(profileId) + except Exception: + return '', 400 + + TableMovies.update({ + TableMovies.profileId: profileId + })\ + .where(TableMovies.radarrId == radarrId)\ + .execute() + + 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 + + @authenticate + def patch(self): + radarrid = request.form.get('radarrid') + action = request.form.get('action') + if action == "scan-disk": + movies_scan_subtitles(radarrid) + return '', 204 + elif action == "search-missing": + movies_download_subtitles(radarrid) + return '', 204 + elif action == "search-wanted": + wanted_search_missing_subtitles_movies() + return '', 204 + + return '', 400 diff --git a/bazarr/api/movies/movies_subtitles.py b/bazarr/api/movies/movies_subtitles.py new file mode 100644 index 000000000..cc2a07e75 --- /dev/null +++ b/bazarr/api/movies/movies_subtitles.py @@ -0,0 +1,175 @@ +# coding=utf-8 + +import os + +from flask import request +from flask_restful import Resource +from subliminal_patch.core import SUBTITLE_EXTENSIONS + +from database import TableMovies, get_audio_profile_languages +from ..utils import authenticate +from helper import path_mappings +from get_providers import get_providers, get_providers_auth +from get_subtitle import download_subtitle, manual_upload_subtitle +from utils import history_log_movie, delete_subtitles +from notifier import send_notifications_movie +from list_subtitles import store_subtitles_movie +from event_handler import event_stream +from config import settings + + +# PATCH: Download Subtitles +# POST: Upload Subtitles +# DELETE: Delete Subtitles +class MoviesSubtitles(Resource): + @authenticate + def patch(self): + # Download + radarrId = request.args.get('radarrid') + + movieInfo = TableMovies.select(TableMovies.title, + TableMovies.path, + TableMovies.sceneName, + TableMovies.audio_language)\ + .where(TableMovies.radarrId == radarrId)\ + .dicts()\ + .get() + + moviePath = path_mappings.path_replace_movie(movieInfo['path']) + sceneName = movieInfo['sceneName'] + if sceneName is None: sceneName = 'None' + + title = movieInfo['title'] + audio_language = movieInfo['audio_language'] + + language = request.form.get('language') + hi = request.form.get('hi').capitalize() + forced = request.form.get('forced').capitalize() + + providers_list = get_providers() + providers_auth = get_providers_auth() + + audio_language_list = get_audio_profile_languages(movie_id=radarrId) + if len(audio_language_list) > 0: + audio_language = audio_language_list[0]['name'] + else: + audio_language = None + + try: + result = download_subtitle(moviePath, language, audio_language, hi, forced, providers_list, + providers_auth, sceneName, title, 'movie') + if result is not None: + message = result[0] + path = result[1] + forced = result[5] + if result[8]: + language_code = result[2] + ":hi" + elif forced: + language_code = result[2] + ":forced" + else: + language_code = result[2] + provider = result[3] + score = result[4] + subs_id = result[6] + subs_path = result[7] + history_log_movie(1, radarrId, message, path, language_code, provider, score, subs_id, subs_path) + send_notifications_movie(radarrId, message) + store_subtitles_movie(path, moviePath) + else: + event_stream(type='movie', payload=radarrId) + except OSError: + pass + + return '', 204 + + @authenticate + def post(self): + # Upload + # TODO: Support Multiply Upload + radarrId = request.args.get('radarrid') + movieInfo = TableMovies.select(TableMovies.title, + TableMovies.path, + TableMovies.sceneName, + TableMovies.audio_language) \ + .where(TableMovies.radarrId == radarrId) \ + .dicts() \ + .get() + + moviePath = path_mappings.path_replace_movie(movieInfo['path']) + sceneName = movieInfo['sceneName'] + if sceneName is None: sceneName = 'None' + + title = movieInfo['title'] + audioLanguage = movieInfo['audio_language'] + + language = request.form.get('language') + forced = True if request.form.get('forced') == 'true' else False + hi = True if request.form.get('hi') == 'true' else False + subFile = request.files.get('file') + + _, ext = os.path.splitext(subFile.filename) + + if ext not in SUBTITLE_EXTENSIONS: + raise ValueError('A subtitle of an invalid format was uploaded.') + + try: + result = manual_upload_subtitle(path=moviePath, + language=language, + forced=forced, + hi=hi, + title=title, + scene_name=sceneName, + media_type='movie', + subtitle=subFile, + audio_language=audioLanguage) + + if result is not None: + message = result[0] + path = result[1] + subs_path = result[2] + if hi: + language_code = language + ":hi" + elif forced: + language_code = language + ":forced" + else: + language_code = language + provider = "manual" + score = 120 + history_log_movie(4, radarrId, message, path, language_code, provider, score, subtitles_path=subs_path) + if not settings.general.getboolean('dont_notify_manual_actions'): + send_notifications_movie(radarrId, message) + store_subtitles_movie(path, moviePath) + except OSError: + pass + + return '', 204 + + @authenticate + def delete(self): + # Delete + radarrId = request.args.get('radarrid') + movieInfo = TableMovies.select(TableMovies.path) \ + .where(TableMovies.radarrId == radarrId) \ + .dicts() \ + .get() + + moviePath = path_mappings.path_replace_movie(movieInfo['path']) + + language = request.form.get('language') + forced = request.form.get('forced') + hi = request.form.get('hi') + subtitlesPath = request.form.get('path') + + subtitlesPath = path_mappings.path_replace_reverse_movie(subtitlesPath) + + result = delete_subtitles(media_type='movie', + language=language, + forced=forced, + hi=hi, + media_path=moviePath, + subtitles_path=subtitlesPath, + radarr_id=radarrId) + if result: + return '', 202 + else: + return '', 204 diff --git a/bazarr/api/movies/wanted.py b/bazarr/api/movies/wanted.py new file mode 100644 index 000000000..45511f0ab --- /dev/null +++ b/bazarr/api/movies/wanted.py @@ -0,0 +1,62 @@ +# coding=utf-8 + +import operator + +from flask import request, jsonify +from flask_restful import Resource +from functools import reduce + +from database import get_exclusion_clause, TableMovies +from ..utils import authenticate, postprocessMovie + + +# GET: Get Wanted Movies +class MoviesWanted(Resource): + @authenticate + def get(self): + radarrid = request.args.getlist("radarrid[]") + + wanted_conditions = [(TableMovies.missing_subtitles != '[]')] + if len(radarrid) > 0: + wanted_conditions.append((TableMovies.radarrId.in_(radarrid))) + wanted_conditions += get_exclusion_clause('movie') + wanted_condition = reduce(operator.and_, wanted_conditions) + + if len(radarrid) > 0: + result = TableMovies.select(TableMovies.title, + TableMovies.missing_subtitles, + TableMovies.radarrId, + TableMovies.sceneName, + TableMovies.failedAttempts, + TableMovies.tags, + TableMovies.monitored)\ + .where(wanted_condition)\ + .dicts() + else: + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + result = TableMovies.select(TableMovies.title, + TableMovies.missing_subtitles, + TableMovies.radarrId, + TableMovies.sceneName, + TableMovies.failedAttempts, + TableMovies.tags, + TableMovies.monitored)\ + .where(wanted_condition)\ + .order_by(TableMovies.rowid.desc())\ + .limit(length)\ + .offset(start)\ + .dicts() + result = list(result) + + for item in result: + postprocessMovie(item) + + count_conditions = [(TableMovies.missing_subtitles != '[]')] + count_conditions += get_exclusion_clause('movie') + count = TableMovies.select(TableMovies.monitored, + TableMovies.tags)\ + .where(reduce(operator.and_, count_conditions))\ + .count() + + return jsonify(data=result, total=count) diff --git a/bazarr/api/providers/__init__.py b/bazarr/api/providers/__init__.py new file mode 100644 index 000000000..dc3793d70 --- /dev/null +++ b/bazarr/api/providers/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .providers import Providers +from .providers_episodes import ProviderEpisodes +from .providers_movies import ProviderMovies + + +api_bp_providers = Blueprint('api_providers', __name__) +api = Api(api_bp_providers) + +api.add_resource(Providers, '/providers') +api.add_resource(ProviderMovies, '/providers/movies') +api.add_resource(ProviderEpisodes, '/providers/episodes') diff --git a/bazarr/api/providers/providers.py b/bazarr/api/providers/providers.py new file mode 100644 index 000000000..0bcfb45a7 --- /dev/null +++ b/bazarr/api/providers/providers.py @@ -0,0 +1,52 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource +from operator import itemgetter + +from database import TableHistory, TableHistoryMovie +from get_providers import list_throttled_providers, reset_throttled_providers +from ..utils import authenticate, False_Keys + + +class Providers(Resource): + @authenticate + def get(self): + history = request.args.get('history') + if history and history not in False_Keys: + providers = list(TableHistory.select(TableHistory.provider) + .where(TableHistory.provider != None and TableHistory.provider != "manual") + .dicts()) + providers += list(TableHistoryMovie.select(TableHistoryMovie.provider) + .where(TableHistoryMovie.provider != None and TableHistoryMovie.provider != "manual") + .dicts()) + providers_list = list(set([x['provider'] for x in providers])) + providers_dicts = [] + for provider in providers_list: + providers_dicts.append({ + 'name': provider, + 'status': 'History', + 'retry': '-' + }) + return jsonify(data=sorted(providers_dicts, key=itemgetter('name'))) + + throttled_providers = list_throttled_providers() + + providers = list() + for provider in throttled_providers: + providers.append({ + "name": provider[0], + "status": provider[1] if provider[1] is not None else "Good", + "retry": provider[2] if provider[2] != "now" else "-" + }) + return jsonify(data=providers) + + @authenticate + def post(self): + action = request.form.get('action') + + if action == 'reset': + reset_throttled_providers() + return '', 204 + + return '', 400 diff --git a/bazarr/api/providers/providers_episodes.py b/bazarr/api/providers/providers_episodes.py new file mode 100644 index 000000000..feedc0134 --- /dev/null +++ b/bazarr/api/providers/providers_episodes.py @@ -0,0 +1,103 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableEpisodes, TableShows, get_audio_profile_languages +from helper import path_mappings +from get_providers import get_providers, get_providers_auth +from get_subtitle import manual_search, manual_download_subtitle +from utils import history_log +from config import settings +from notifier import send_notifications +from list_subtitles import store_subtitles + +from ..utils import authenticate + + +class ProviderEpisodes(Resource): + @authenticate + def get(self): + # Manual Search + sonarrEpisodeId = request.args.get('episodeid') + episodeInfo = TableEpisodes.select(TableEpisodes.title, + TableEpisodes.path, + TableEpisodes.scene_name, + TableShows.profileId) \ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\ + .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \ + .dicts() \ + .get() + + title = episodeInfo['title'] + episodePath = path_mappings.path_replace(episodeInfo['path']) + sceneName = episodeInfo['scene_name'] + profileId = episodeInfo['profileId'] + if sceneName is None: sceneName = "None" + + providers_list = get_providers() + providers_auth = get_providers_auth() + + data = manual_search(episodePath, profileId, providers_list, providers_auth, sceneName, title, + 'series') + if not data: + data = [] + return jsonify(data=data) + + @authenticate + def post(self): + # Manual Download + sonarrSeriesId = request.args.get('seriesid') + sonarrEpisodeId = request.args.get('episodeid') + episodeInfo = TableEpisodes.select(TableEpisodes.title, + TableEpisodes.path, + TableEpisodes.scene_name) \ + .where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \ + .dicts() \ + .get() + + title = episodeInfo['title'] + episodePath = path_mappings.path_replace(episodeInfo['path']) + sceneName = episodeInfo['scene_name'] + if sceneName is None: sceneName = "None" + + language = request.form.get('language') + hi = request.form.get('hi').capitalize() + forced = request.form.get('forced').capitalize() + selected_provider = request.form.get('provider') + subtitle = request.form.get('subtitle') + providers_auth = get_providers_auth() + + audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId) + if len(audio_language_list) > 0: + audio_language = audio_language_list[0]['name'] + else: + audio_language = 'None' + + try: + result = manual_download_subtitle(episodePath, language, audio_language, hi, forced, subtitle, + selected_provider, providers_auth, sceneName, title, 'series') + if result is not None: + message = result[0] + path = result[1] + forced = result[5] + if result[8]: + language_code = result[2] + ":hi" + elif forced: + language_code = result[2] + ":forced" + else: + language_code = result[2] + provider = result[3] + score = result[4] + subs_id = result[6] + subs_path = result[7] + history_log(2, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id, + subs_path) + if not settings.general.getboolean('dont_notify_manual_actions'): + send_notifications(sonarrSeriesId, sonarrEpisodeId, message) + store_subtitles(path, episodePath) + return result, 201 + except OSError: + pass + + return '', 204 diff --git a/bazarr/api/providers/providers_movies.py b/bazarr/api/providers/providers_movies.py new file mode 100644 index 000000000..c189ab2c4 --- /dev/null +++ b/bazarr/api/providers/providers_movies.py @@ -0,0 +1,102 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from database import TableMovies, get_audio_profile_languages +from helper import path_mappings +from get_providers import get_providers, get_providers_auth +from get_subtitle import manual_search, manual_download_subtitle +from utils import history_log_movie +from config import settings +from notifier import send_notifications_movie +from list_subtitles import store_subtitles_movie + +from ..utils import authenticate + + +class ProviderMovies(Resource): + @authenticate + def get(self): + # Manual Search + radarrId = request.args.get('radarrid') + movieInfo = TableMovies.select(TableMovies.title, + TableMovies.path, + TableMovies.sceneName, + TableMovies.profileId) \ + .where(TableMovies.radarrId == radarrId) \ + .dicts() \ + .get() + + title = movieInfo['title'] + moviePath = path_mappings.path_replace_movie(movieInfo['path']) + sceneName = movieInfo['sceneName'] + profileId = movieInfo['profileId'] + if sceneName is None: sceneName = "None" + + providers_list = get_providers() + providers_auth = get_providers_auth() + + data = manual_search(moviePath, profileId, providers_list, providers_auth, sceneName, title, + 'movie') + if not data: + data = [] + return jsonify(data=data) + + @authenticate + def post(self): + # Manual Download + radarrId = request.args.get('radarrid') + movieInfo = TableMovies.select(TableMovies.title, + TableMovies.path, + TableMovies.sceneName, + TableMovies.audio_language) \ + .where(TableMovies.radarrId == radarrId) \ + .dicts() \ + .get() + + title = movieInfo['title'] + moviePath = path_mappings.path_replace_movie(movieInfo['path']) + sceneName = movieInfo['sceneName'] + if sceneName is None: sceneName = "None" + audio_language = movieInfo['audio_language'] + + language = request.form.get('language') + hi = request.form.get('hi').capitalize() + forced = request.form.get('forced').capitalize() + selected_provider = request.form.get('provider') + subtitle = request.form.get('subtitle') + + providers_auth = get_providers_auth() + + audio_language_list = get_audio_profile_languages(movie_id=radarrId) + if len(audio_language_list) > 0: + audio_language = audio_language_list[0]['name'] + else: + audio_language = 'None' + + try: + result = manual_download_subtitle(moviePath, language, audio_language, hi, forced, subtitle, + selected_provider, providers_auth, sceneName, title, 'movie') + if result is not None: + message = result[0] + path = result[1] + forced = result[5] + if result[8]: + language_code = result[2] + ":hi" + elif forced: + language_code = result[2] + ":forced" + else: + language_code = result[2] + provider = result[3] + score = result[4] + subs_id = result[6] + subs_path = result[7] + history_log_movie(2, radarrId, message, path, language_code, provider, score, subs_id, subs_path) + if not settings.general.getboolean('dont_notify_manual_actions'): + send_notifications_movie(radarrId, message) + store_subtitles_movie(path, moviePath) + except OSError: + pass + + return '', 204 diff --git a/bazarr/api/series/__init__.py b/bazarr/api/series/__init__.py new file mode 100644 index 000000000..68f437ba4 --- /dev/null +++ b/bazarr/api/series/__init__.py @@ -0,0 +1,12 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .series import Series + + +api_bp_series = Blueprint('api_series', __name__) +api = Api(api_bp_series) + +api.add_resource(Series, '/series') diff --git a/bazarr/api/series/series.py b/bazarr/api/series/series.py new file mode 100644 index 000000000..057462f11 --- /dev/null +++ b/bazarr/api/series/series.py @@ -0,0 +1,114 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +import operator +from functools import reduce + +from database import get_exclusion_clause, TableEpisodes, TableShows +from list_subtitles import list_missing_subtitles, series_scan_subtitles +from get_subtitle import series_download_subtitles, wanted_search_missing_subtitles_series +from ..utils import authenticate, postprocessSeries, None_Keys +from event_handler import event_stream + + +class Series(Resource): + @authenticate + def get(self): + start = request.args.get('start') or 0 + length = request.args.get('length') or -1 + seriesId = request.args.getlist('seriesid[]') + + count = TableShows.select().count() + + if len(seriesId) != 0: + result = TableShows.select() \ + .where(TableShows.sonarrSeriesId.in_(seriesId)) \ + .order_by(TableShows.sortTitle).dicts() + else: + result = TableShows.select().order_by(TableShows.sortTitle).limit(length).offset(start).dicts() + + result = list(result) + + for item in result: + postprocessSeries(item) + + # Add missing subtitles episode count + episodes_missing_conditions = [(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId']), + (TableEpisodes.missing_subtitles != '[]')] + episodes_missing_conditions += get_exclusion_clause('series') + + episodeMissingCount = TableEpisodes.select(TableShows.tags, + TableEpisodes.monitored, + TableShows.seriesType) \ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \ + .where(reduce(operator.and_, episodes_missing_conditions)) \ + .count() + item.update({"episodeMissingCount": episodeMissingCount}) + + # Add episode count + episodeFileCount = TableEpisodes.select(TableShows.tags, + TableEpisodes.monitored, + TableShows.seriesType) \ + .join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \ + .where(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId']) \ + .count() + item.update({"episodeFileCount": episodeFileCount}) + + return jsonify(data=result, total=count) + + @authenticate + def post(self): + seriesIdList = request.form.getlist('seriesid') + profileIdList = request.form.getlist('profileid') + + for idx in range(len(seriesIdList)): + seriesId = seriesIdList[idx] + profileId = profileIdList[idx] + + if profileId in None_Keys: + profileId = None + else: + try: + profileId = int(profileId) + except Exception: + return '', 400 + + TableShows.update({ + TableShows.profileId: profileId + }) \ + .where(TableShows.sonarrSeriesId == seriesId) \ + .execute() + + 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 + + @authenticate + def patch(self): + seriesid = request.form.get('seriesid') + action = request.form.get('action') + if action == "scan-disk": + series_scan_subtitles(seriesid) + return '', 204 + elif action == "search-missing": + series_download_subtitles(seriesid) + return '', 204 + elif action == "search-wanted": + wanted_search_missing_subtitles_series() + return '', 204 + + return '', 400 diff --git a/bazarr/api/subtitles/__init__.py b/bazarr/api/subtitles/__init__.py new file mode 100644 index 000000000..7cb7d9a11 --- /dev/null +++ b/bazarr/api/subtitles/__init__.py @@ -0,0 +1,14 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .subtitles import Subtitles +from .subtitles_info import SubtitleNameInfo + + +api_bp_subtitles = Blueprint('api_subtitles', __name__) +api = Api(api_bp_subtitles) + +api.add_resource(Subtitles, '/subtitles') +api.add_resource(SubtitleNameInfo, '/subtitles/info') diff --git a/bazarr/api/subtitles/subtitles.py b/bazarr/api/subtitles/subtitles.py new file mode 100644 index 000000000..7799a9679 --- /dev/null +++ b/bazarr/api/subtitles/subtitles.py @@ -0,0 +1,72 @@ +# coding=utf-8 + +import os +import sys + +from flask import request +from flask_restful import Resource + +from database import TableEpisodes, TableMovies +from helper import path_mappings +from ..utils import authenticate +from subsyncer import subsync +from utils import translate_subtitles_file, subtitles_apply_mods +from get_subtitle import store_subtitles, store_subtitles_movie +from config import settings + + +class Subtitles(Resource): + @authenticate + def patch(self): + action = request.args.get('action') + + language = request.form.get('language') + subtitles_path = request.form.get('path') + media_type = request.form.get('type') + id = request.form.get('id') + + if media_type == 'episode': + subtitles_path = path_mappings.path_replace(subtitles_path) + metadata = TableEpisodes.select(TableEpisodes.path, TableEpisodes.sonarrSeriesId)\ + .where(TableEpisodes.sonarrEpisodeId == id)\ + .dicts()\ + .get() + video_path = path_mappings.path_replace(metadata['path']) + else: + subtitles_path = path_mappings.path_replace_movie(subtitles_path) + metadata = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == id).dicts().get() + video_path = path_mappings.path_replace_movie(metadata['path']) + + if action == 'sync': + if media_type == 'episode': + subsync.sync(video_path=video_path, srt_path=subtitles_path, + srt_lang=language, media_type='series', sonarr_series_id=metadata['sonarrSeriesId'], + sonarr_episode_id=int(id)) + else: + subsync.sync(video_path=video_path, srt_path=subtitles_path, + srt_lang=language, media_type='movies', radarr_id=id) + elif action == 'translate': + dest_language = language + forced = True if request.form.get('forced') == 'true' else False + hi = True if request.form.get('hi') == 'true' else False + result = translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path, + to_lang=dest_language, + forced=forced, hi=hi) + if result: + if media_type == 'episode': + store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) + else: + store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) + return '', 200 + else: + return '', 404 + 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 diff --git a/bazarr/api/subtitles/subtitles_info.py b/bazarr/api/subtitles/subtitles_info.py new file mode 100644 index 000000000..9de38ac55 --- /dev/null +++ b/bazarr/api/subtitles/subtitles_info.py @@ -0,0 +1,41 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource +from subliminal_patch.core import guessit +from ..utils import authenticate + + +class SubtitleNameInfo(Resource): + @authenticate + def get(self): + names = request.args.getlist('filenames[]') + results = [] + for name in names: + opts = dict() + opts['type'] = 'episode' + guessit_result = guessit(name, options=opts) + result = {} + result['filename'] = name + if 'subtitle_language' in guessit_result: + result['subtitle_language'] = str(guessit_result['subtitle_language']) + + result['episode'] = 0 + if 'episode' in guessit_result: + if isinstance(guessit_result['episode'], list): + # for multiple episodes file, choose the first episode number + if len(guessit_result['episode']): + # make sure that guessit returned a list of more than 0 items + result['episode'] = int(guessit_result['episode'][0]) + elif isinstance(guessit_result['episode'], (str, int)): + # if single episode (should be int but just in case we cast it to int) + result['episode'] = int(guessit_result['episode']) + + if 'season' in guessit_result: + result['season'] = int(guessit_result['season']) + else: + result['season'] = 0 + + results.append(result) + + return jsonify(data=results) diff --git a/bazarr/api/system/__init__.py b/bazarr/api/system/__init__.py new file mode 100644 index 000000000..cbe54a13b --- /dev/null +++ b/bazarr/api/system/__init__.py @@ -0,0 +1,33 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .system import System +from .searches import Searches +from .account import SystemAccount +from .tasks import SystemTasks +from .logs import SystemLogs +from .status import SystemStatus +from .health import SystemHealth +from .releases import SystemReleases +from .settings import SystemSettings +from .languages import Languages +from .languages_profiles import LanguagesProfiles +from .notifications import Notifications + +api_bp_system = Blueprint('api_system', __name__) +api = Api(api_bp_system) + +api.add_resource(System, '/system') +api.add_resource(Searches, '/system/searches') +api.add_resource(SystemAccount, '/system/account') +api.add_resource(SystemTasks, '/system/tasks') +api.add_resource(SystemLogs, '/system/logs') +api.add_resource(SystemStatus, '/system/status') +api.add_resource(SystemHealth, '/system/health') +api.add_resource(SystemReleases, '/system/releases') +api.add_resource(SystemSettings, '/system/settings') +api.add_resource(Languages, '/system/languages') +api.add_resource(LanguagesProfiles, '/system/languages/profiles') +api.add_resource(Notifications, '/system/notifications') diff --git a/bazarr/api/system/account.py b/bazarr/api/system/account.py new file mode 100644 index 000000000..a46e2b331 --- /dev/null +++ b/bazarr/api/system/account.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +import gc + +from flask import request, session +from flask_restful import Resource + +from config import settings +from utils import check_credentials + + +class SystemAccount(Resource): + def post(self): + if settings.auth.type != 'form': + return '', 405 + + action = request.args.get('action') + if action == 'login': + username = request.form.get('username') + password = request.form.get('password') + if check_credentials(username, password): + session['logged_in'] = True + return '', 204 + elif action == 'logout': + session.clear() + gc.collect() + return '', 204 + + return '', 401 diff --git a/bazarr/api/system/health.py b/bazarr/api/system/health.py new file mode 100644 index 000000000..3db5374a1 --- /dev/null +++ b/bazarr/api/system/health.py @@ -0,0 +1,13 @@ +# coding=utf-8 + +from flask import jsonify +from flask_restful import Resource + +from ..utils import authenticate +from utils import get_health_issues + + +class SystemHealth(Resource): + @authenticate + def get(self): + return jsonify(data=get_health_issues()) diff --git a/bazarr/api/system/languages.py b/bazarr/api/system/languages.py new file mode 100644 index 000000000..0f69a8eea --- /dev/null +++ b/bazarr/api/system/languages.py @@ -0,0 +1,54 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from operator import itemgetter + +from ..utils import authenticate, False_Keys +from database import TableHistory, TableHistoryMovie, TableSettingsLanguages +from get_languages import alpha2_from_alpha3, language_from_alpha2 + + +class Languages(Resource): + @authenticate + def get(self): + history = request.args.get('history') + if history and history not in False_Keys: + languages = list(TableHistory.select(TableHistory.language) + .where(TableHistory.language != None) + .dicts()) + languages += list(TableHistoryMovie.select(TableHistoryMovie.language) + .where(TableHistoryMovie.language != None) + .dicts()) + languages_list = list(set([l['language'].split(':')[0] for l in languages])) + languages_dicts = [] + for language in languages_list: + code2 = None + if len(language) == 2: + code2 = language + elif len(language) == 3: + code2 = alpha2_from_alpha3(language) + else: + continue + + if not any(x['code2'] == code2 for x in languages_dicts): + try: + languages_dicts.append({ + 'code2': code2, + 'name': language_from_alpha2(code2), + # Compatibility: Use false temporarily + 'enabled': False + }) + except: + continue + return jsonify(sorted(languages_dicts, key=itemgetter('name'))) + + result = TableSettingsLanguages.select(TableSettingsLanguages.name, + TableSettingsLanguages.code2, + TableSettingsLanguages.enabled)\ + .order_by(TableSettingsLanguages.name).dicts() + result = list(result) + for item in result: + item['enabled'] = item['enabled'] == 1 + return jsonify(result) diff --git a/bazarr/api/system/languages_profiles.py b/bazarr/api/system/languages_profiles.py new file mode 100644 index 000000000..15e8f3cb7 --- /dev/null +++ b/bazarr/api/system/languages_profiles.py @@ -0,0 +1,13 @@ +# coding=utf-8 + +from flask import jsonify +from flask_restful import Resource + +from ..utils import authenticate +from database import get_profiles_list + + +class LanguagesProfiles(Resource): + @authenticate + def get(self): + return jsonify(get_profiles_list()) diff --git a/bazarr/api/system/logs.py b/bazarr/api/system/logs.py new file mode 100644 index 000000000..83b8546c2 --- /dev/null +++ b/bazarr/api/system/logs.py @@ -0,0 +1,41 @@ +# coding=utf-8 + +import io +import os + +from flask import jsonify +from flask_restful import Resource + +from ..utils import authenticate +from logger import empty_log +from get_args import args + + +class SystemLogs(Resource): + @authenticate + def get(self): + logs = [] + with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file: + raw_lines = file.read() + lines = raw_lines.split('|\n') + for line in lines: + if line == '': + continue + raw_message = line.split('|') + raw_message_len = len(raw_message) + if raw_message_len > 3: + log = dict() + log["timestamp"] = raw_message[0] + log["type"] = raw_message[1].rstrip() + log["message"] = raw_message[3] + if raw_message_len > 4 and raw_message[4] != '\n': + log['exception'] = raw_message[4].strip('\'').replace(' ', '\u2003\u2003') + logs.append(log) + + logs.reverse() + return jsonify(data=logs) + + @authenticate + def delete(self): + empty_log() + return '', 204 diff --git a/bazarr/api/system/notifications.py b/bazarr/api/system/notifications.py new file mode 100644 index 000000000..4cb8e24a1 --- /dev/null +++ b/bazarr/api/system/notifications.py @@ -0,0 +1,27 @@ +# coding=utf-8 + +import apprise + +from flask import request +from flask_restful import Resource + +from ..utils import authenticate + + +class Notifications(Resource): + @authenticate + def patch(self): + url = request.form.get("url") + + asset = apprise.AppriseAsset(async_mode=False) + + apobj = apprise.Apprise(asset=asset) + + apobj.add(url) + + apobj.notify( + title='Bazarr test notification', + body='Test notification' + ) + + return '', 204 diff --git a/bazarr/api/system/releases.py b/bazarr/api/system/releases.py new file mode 100644 index 000000000..150cb083f --- /dev/null +++ b/bazarr/api/system/releases.py @@ -0,0 +1,47 @@ +# coding=utf-8 + +import io +import json +import os +import logging + +from flask import jsonify +from flask_restful import Resource + +from ..utils import authenticate +from config import settings +from get_args import args + + +class SystemReleases(Resource): + @authenticate + def get(self): + filtered_releases = [] + try: + with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f: + releases = json.loads(f.read()) + + for release in releases: + if settings.general.branch == 'master' and not release['prerelease']: + filtered_releases.append(release) + elif settings.general.branch != 'master' and any(not x['prerelease'] for x in filtered_releases): + continue + elif settings.general.branch != 'master': + filtered_releases.append(release) + if settings.general.branch == 'master': + filtered_releases = filtered_releases[:5] + + current_version = os.environ["BAZARR_VERSION"] + + for i, release in enumerate(filtered_releases): + body = release['body'].replace('- ', '').split('\n')[1:] + filtered_releases[i] = {"body": body, + "name": release['name'], + "date": release['date'][:10], + "prerelease": release['prerelease'], + "current": release['name'].lstrip('v') == current_version} + + except Exception: + logging.exception( + 'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt')) + return jsonify(data=filtered_releases) diff --git a/bazarr/api/system/searches.py b/bazarr/api/system/searches.py new file mode 100644 index 000000000..5386be226 --- /dev/null +++ b/bazarr/api/system/searches.py @@ -0,0 +1,40 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from ..utils import authenticate +from config import settings +from database import TableShows, TableMovies + + +class Searches(Resource): + @authenticate + def get(self): + query = request.args.get('query') + search_list = [] + + if query: + if settings.general.getboolean('use_sonarr'): + # Get matching series + series = TableShows.select(TableShows.title, + TableShows.sonarrSeriesId, + TableShows.year)\ + .where(TableShows.title.contains(query))\ + .order_by(TableShows.title)\ + .dicts() + series = list(series) + search_list += series + + if settings.general.getboolean('use_radarr'): + # Get matching movies + movies = TableMovies.select(TableMovies.title, + TableMovies.radarrId, + TableMovies.year) \ + .where(TableMovies.title.contains(query)) \ + .order_by(TableMovies.title) \ + .dicts() + movies = list(movies) + search_list += movies + + return jsonify(search_list) diff --git a/bazarr/api/system/settings.py b/bazarr/api/system/settings.py new file mode 100644 index 000000000..c8d9574fc --- /dev/null +++ b/bazarr/api/system/settings.py @@ -0,0 +1,102 @@ +# coding=utf-8 + +import json + +from flask import request, jsonify +from flask_restful import Resource + +from ..utils import authenticate +from database import TableLanguagesProfiles, TableSettingsLanguages, TableShows, TableMovies, TableSettingsNotifier, \ + update_profile_id_list +from event_handler import event_stream +from config import settings, save_settings, get_settings +from scheduler import scheduler +from list_subtitles import list_missing_subtitles, list_missing_subtitles_movies + + +class SystemSettings(Resource): + @authenticate + def get(self): + data = get_settings() + + notifications = TableSettingsNotifier.select().order_by(TableSettingsNotifier.name).dicts() + notifications = list(notifications) + for i, item in enumerate(notifications): + item["enabled"] = item["enabled"] == 1 + notifications[i] = item + + data['notifications'] = dict() + data['notifications']['providers'] = notifications + + return jsonify(data) + + @authenticate + def post(self): + enabled_languages = request.form.getlist('languages-enabled') + if len(enabled_languages) != 0: + TableSettingsLanguages.update({ + TableSettingsLanguages.enabled: 0 + }).execute() + for code in enabled_languages: + TableSettingsLanguages.update({ + TableSettingsLanguages.enabled: 1 + })\ + .where(TableSettingsLanguages.code2 == code)\ + .execute() + event_stream("languages") + + languages_profiles = request.form.get('languages-profiles') + if languages_profiles: + existing_ids = TableLanguagesProfiles.select(TableLanguagesProfiles.profileId).dicts() + existing_ids = list(existing_ids) + existing = [x['profileId'] for x in existing_ids] + for item in json.loads(languages_profiles): + if item['profileId'] in existing: + # Update existing profiles + TableLanguagesProfiles.update({ + TableLanguagesProfiles.name: item['name'], + TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None, + TableLanguagesProfiles.items: json.dumps(item['items']) + })\ + .where(TableLanguagesProfiles.profileId == item['profileId'])\ + .execute() + existing.remove(item['profileId']) + else: + # Add new profiles + TableLanguagesProfiles.insert({ + TableLanguagesProfiles.profileId: item['profileId'], + TableLanguagesProfiles.name: item['name'], + TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None, + TableLanguagesProfiles.items: json.dumps(item['items']) + }).execute() + for profileId in existing: + # Unassign this profileId from series and movies + TableShows.update({ + TableShows.profileId: None + }).where(TableShows.profileId == profileId).execute() + TableMovies.update({ + TableMovies.profileId: None + }).where(TableMovies.profileId == profileId).execute() + # Remove deleted profiles + TableLanguagesProfiles.delete().where(TableLanguagesProfiles.profileId == profileId).execute() + + update_profile_id_list() + event_stream("languages") + + if settings.general.getboolean('use_sonarr'): + scheduler.add_job(list_missing_subtitles, kwargs={'send_event': False}) + if settings.general.getboolean('use_radarr'): + scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': False}) + + # Update Notification + notifications = request.form.getlist('notifications-providers') + for item in notifications: + item = json.loads(item) + TableSettingsNotifier.update({ + TableSettingsNotifier.enabled: item['enabled'], + TableSettingsNotifier.url: item['url'] + }).where(TableSettingsNotifier.name == item['name']).execute() + + save_settings(zip(request.form.keys(), request.form.listvalues())) + event_stream("settings") + return '', 204 diff --git a/bazarr/api/system/status.py b/bazarr/api/system/status.py new file mode 100644 index 000000000..4a8b69a16 --- /dev/null +++ b/bazarr/api/system/status.py @@ -0,0 +1,27 @@ +# coding=utf-8 + +import os +import platform + +from flask import jsonify +from flask_restful import Resource + +from ..utils import authenticate +from utils import get_sonarr_info, get_radarr_info +from get_args import args +from init import startTime + + +class SystemStatus(Resource): + @authenticate + def get(self): + system_status = {} + system_status.update({'bazarr_version': os.environ["BAZARR_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__))}) + system_status.update({'bazarr_config_directory': args.config_dir}) + system_status.update({'start_time': startTime}) + return jsonify(data=system_status) diff --git a/bazarr/api/system/system.py b/bazarr/api/system/system.py new file mode 100644 index 000000000..98b614523 --- /dev/null +++ b/bazarr/api/system/system.py @@ -0,0 +1,18 @@ +# coding=utf-8 + +from flask import request +from flask_restful import Resource + +from ..utils import authenticate + + +class System(Resource): + @authenticate + def post(self): + from server import webserver + action = request.args.get('action') + if action == "shutdown": + webserver.shutdown() + elif action == "restart": + webserver.restart() + return '', 204 diff --git a/bazarr/api/system/tasks.py b/bazarr/api/system/tasks.py new file mode 100644 index 000000000..0bdfed429 --- /dev/null +++ b/bazarr/api/system/tasks.py @@ -0,0 +1,31 @@ +# coding=utf-8 + +from flask import request, jsonify +from flask_restful import Resource + +from ..utils import authenticate +from scheduler import scheduler + + +class SystemTasks(Resource): + @authenticate + def get(self): + taskid = request.args.get('taskid') + + task_list = scheduler.get_task_list() + + if taskid: + for item in task_list: + if item['job_id'] == taskid: + task_list = [item] + continue + + return jsonify(data=task_list) + + @authenticate + def post(self): + taskid = request.form.get('taskid') + + scheduler.execute_job_now(taskid) + + return '', 204 diff --git a/bazarr/api/utils.py b/bazarr/api/utils.py new file mode 100644 index 000000000..61e19c947 --- /dev/null +++ b/bazarr/api/utils.py @@ -0,0 +1,239 @@ +# coding=utf-8 + +import ast + +from functools import wraps +from flask import request, abort +from operator import itemgetter + +from config import settings, base_url +from get_languages import language_from_alpha2, alpha3_from_alpha2 +from database import get_audio_profile_languages, get_desired_languages +from helper import path_mappings + +None_Keys = ['null', 'undefined', '', None] + +False_Keys = ['False', 'false', '0'] + + +def authenticate(actual_method): + @wraps(actual_method) + def wrapper(*args, **kwargs): + apikey_settings = settings.auth.apikey + apikey_get = request.args.get('apikey') + apikey_post = request.form.get('apikey') + apikey_header = None + if 'X-API-KEY' in request.headers: + apikey_header = request.headers['X-API-KEY'] + + if apikey_settings in [apikey_get, apikey_post, apikey_header]: + return actual_method(*args, **kwargs) + + return abort(401) + + return wrapper + + +def postprocess(item): + # Remove ffprobe_cache + if 'ffprobe_cache' in item: + del (item['ffprobe_cache']) + + # Parse tags + if 'tags' in item: + if item['tags'] is None: + item['tags'] = [] + else: + item['tags'] = ast.literal_eval(item['tags']) + + if 'monitored' in item: + if item['monitored'] is None: + item['monitored'] = False + else: + item['monitored'] = item['monitored'] == 'True' + + if 'hearing_impaired' in item and item['hearing_impaired'] is not None: + if item['hearing_impaired'] is None: + item['hearing_impaired'] = False + else: + item['hearing_impaired'] = item['hearing_impaired'] == 'True' + + if 'language' in item: + if item['language'] == 'None': + item['language'] = None + elif item['language'] is not None: + splitted_language = item['language'].split(':') + item['language'] = {"name": language_from_alpha2(splitted_language[0]), + "code2": splitted_language[0], + "code3": alpha3_from_alpha2(splitted_language[0]), + "forced": True if item['language'].endswith(':forced') else False, + "hi": True if item['language'].endswith(':hi') else False} + + +def postprocessSeries(item): + postprocess(item) + # Parse audio language + if 'audio_language' in item and item['audio_language'] is not None: + item['audio_language'] = get_audio_profile_languages(series_id=item['sonarrSeriesId']) + + if 'alternateTitles' in item: + if item['alternateTitles'] is None: + item['alternativeTitles'] = [] + else: + item['alternativeTitles'] = ast.literal_eval(item['alternateTitles']) + del item["alternateTitles"] + + # Parse seriesType + if 'seriesType' in item and item['seriesType'] is not None: + item['seriesType'] = item['seriesType'].capitalize() + + if 'path' in item: + item['path'] = path_mappings.path_replace(item['path']) + + # map poster and fanart to server proxy + if 'poster' in item: + poster = item['poster'] + item['poster'] = f"{base_url}/images/series{poster}" if poster else None + + if 'fanart' in item: + fanart = item['fanart'] + item['fanart'] = f"{base_url}/images/series{fanart}" if fanart else None + + +def postprocessEpisode(item): + postprocess(item) + if 'audio_language' in item and item['audio_language'] is not None: + item['audio_language'] = get_audio_profile_languages(episode_id=item['sonarrEpisodeId']) + + if 'subtitles' in item: + if item['subtitles'] is None: + raw_subtitles = [] + else: + raw_subtitles = ast.literal_eval(item['subtitles']) + subtitles = [] + + for subs in raw_subtitles: + subtitle = subs[0].split(':') + sub = {"name": language_from_alpha2(subtitle[0]), + "code2": subtitle[0], + "code3": alpha3_from_alpha2(subtitle[0]), + "path": path_mappings.path_replace(subs[1]), + "forced": False, + "hi": False} + if len(subtitle) > 1: + sub["forced"] = True if subtitle[1] == 'forced' else False + sub["hi"] = True if subtitle[1] == 'hi' else False + + subtitles.append(sub) + + item.update({"subtitles": subtitles}) + + # Parse missing subtitles + if 'missing_subtitles' in item: + if item['missing_subtitles'] is None: + item['missing_subtitles'] = [] + else: + item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles']) + for i, subs in enumerate(item['missing_subtitles']): + subtitle = subs.split(':') + item['missing_subtitles'][i] = {"name": language_from_alpha2(subtitle[0]), + "code2": subtitle[0], + "code3": alpha3_from_alpha2(subtitle[0]), + "forced": False, + "hi": False} + if len(subtitle) > 1: + item['missing_subtitles'][i].update({ + "forced": True if subtitle[1] == 'forced' else False, + "hi": True if subtitle[1] == 'hi' else False + }) + + if 'scene_name' in item: + item["sceneName"] = item["scene_name"] + del item["scene_name"] + + if 'path' in item and item['path']: + # Provide mapped path + item['path'] = path_mappings.path_replace(item['path']) + + +# TODO: Move +def postprocessMovie(item): + postprocess(item) + # Parse audio language + if 'audio_language' in item and item['audio_language'] is not None: + item['audio_language'] = get_audio_profile_languages(movie_id=item['radarrId']) + + # Parse alternate titles + if 'alternativeTitles' in item: + if item['alternativeTitles'] is None: + item['alternativeTitles'] = [] + else: + item['alternativeTitles'] = ast.literal_eval(item['alternativeTitles']) + + # Parse failed attempts + if 'failedAttempts' in item: + if item['failedAttempts']: + item['failedAttempts'] = ast.literal_eval(item['failedAttempts']) + + # Parse subtitles + if 'subtitles' in item: + if item['subtitles'] is None: + item['subtitles'] = [] + else: + item['subtitles'] = ast.literal_eval(item['subtitles']) + for i, subs in enumerate(item['subtitles']): + language = subs[0].split(':') + item['subtitles'][i] = {"path": path_mappings.path_replace_movie(subs[1]), + "name": language_from_alpha2(language[0]), + "code2": language[0], + "code3": alpha3_from_alpha2(language[0]), + "forced": False, + "hi": False} + if len(language) > 1: + item['subtitles'][i].update({ + "forced": True if language[1] == 'forced' else False, + "hi": True if language[1] == 'hi' else False + }) + + if settings.general.getboolean('embedded_subs_show_desired'): + desired_lang_list = get_desired_languages(item['profileId']) + item['subtitles'] = [x for x in item['subtitles'] if x['code2'] in desired_lang_list or x['path']] + + item['subtitles'] = sorted(item['subtitles'], key=itemgetter('name', 'forced')) + + # Parse missing subtitles + if 'missing_subtitles' in item: + if item['missing_subtitles'] is None: + item['missing_subtitles'] = [] + else: + item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles']) + for i, subs in enumerate(item['missing_subtitles']): + language = subs.split(':') + item['missing_subtitles'][i] = {"name": language_from_alpha2(language[0]), + "code2": language[0], + "code3": alpha3_from_alpha2(language[0]), + "forced": False, + "hi": False} + if len(language) > 1: + item['missing_subtitles'][i].update({ + "forced": True if language[1] == 'forced' else False, + "hi": True if language[1] == 'hi' else False + }) + + # Provide mapped path + if 'path' in item: + if item['path']: + item['path'] = path_mappings.path_replace_movie(item['path']) + + if 'subtitles_path' in item: + # Provide mapped subtitles path + item['subtitles_path'] = path_mappings.path_replace_movie(item['subtitles_path']) + + # map poster and fanart to server proxy + if 'poster' in item: + poster = item['poster'] + item['poster'] = f"{base_url}/images/movies{poster}" if poster else None + + if 'fanart' in item: + fanart = item['fanart'] + item['fanart'] = f"{base_url}/images/movies{fanart}" if fanart else None diff --git a/bazarr/api/webhooks/__init__.py b/bazarr/api/webhooks/__init__.py new file mode 100644 index 000000000..76b8d62ff --- /dev/null +++ b/bazarr/api/webhooks/__init__.py @@ -0,0 +1,12 @@ +# coding=utf-8 + +from flask import Blueprint +from flask_restful import Api + +from .plex import WebHooksPlex + + +api_bp_webhooks = Blueprint('api_webhooks', __name__) +api = Api(api_bp_webhooks) + +api.add_resource(WebHooksPlex, '/webhooks/plex') diff --git a/bazarr/api/webhooks/plex.py b/bazarr/api/webhooks/plex.py new file mode 100644 index 000000000..6cc6e9da8 --- /dev/null +++ b/bazarr/api/webhooks/plex.py @@ -0,0 +1,76 @@ +# coding=utf-8 + +import json +import requests +import os +import re + +from flask import request +from flask_restful import Resource +from bs4 import BeautifulSoup as bso + +from database import TableEpisodes, TableShows, TableMovies +from get_subtitle import episode_download_subtitles, movies_download_subtitles +from ..utils import authenticate + + +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 diff --git a/bazarr/server.py b/bazarr/server.py index b414d8ee6..0b024d1ec 100644 --- a/bazarr/server.py +++ b/bazarr/server.py @@ -13,8 +13,9 @@ from database import database from app import create_app app = create_app() -from api import api_bp -app.register_blueprint(api_bp) +from api import api_bp_list +for item in api_bp_list: + app.register_blueprint(item, url_prefix=base_url.rstrip('/') + '/api') class Server: