def get_profile_list(): apikey_sonarr = settings.sonarr.apikey profiles_list = [] # Get profiles data from Sonarr if get_sonarr_info.is_legacy(): url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr else: url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr try: profiles_json = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.") return None except requests.exceptions.Timeout: logging.exception("BAZARR Error trying to get profiles from Sonarr. Timeout Error.") return None except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get profiles from Sonarr.") return None # Parsing data returned from Sonarr if get_sonarr_info.is_legacy(): for profile in profiles_json.json(): profiles_list.append([profile['id'], profile['language'].capitalize()]) else: for profile in profiles_json.json(): profiles_list.append([profile['id'], profile['name'].capitalize()]) return profiles_list
def browse_sonarr_filesystem(path='#'): if path == '#': path = '' if get_sonarr_info.is_legacy(): url_sonarr_api_filesystem = url_sonarr() + "/api/filesystem?path=" + path + \ "&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \ settings.sonarr.apikey else: url_sonarr_api_filesystem = url_sonarr() + "/api/v3/filesystem?path=" + path + \ "&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \ settings.sonarr.apikey try: r = requests.get(url_sonarr_api_filesystem, timeout=60, verify=False, headers=headers) r.raise_for_status() except requests.exceptions.HTTPError: logging.exception("BAZARR Error trying to get series from Sonarr. Http error.") return except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get series from Sonarr. Connection Error.") return except requests.exceptions.Timeout: logging.exception("BAZARR Error trying to get series from Sonarr. Timeout Error.") return except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get series from Sonarr.") return return r.json()
def start(self): if get_sonarr_info.is_legacy(): logging.warning( 'BAZARR can only sync from Sonarr v3 SignalR feed to get real-time update. You should ' 'consider upgrading your version({}).'.format( get_sonarr_info.version())) else: logging.info('BAZARR trying to connect to Sonarr SignalR feed...') self.configure() while not self.connection.started: try: self.connection.start() except ConnectionError: time.sleep(5) except json.decoder.JSONDecodeError: logging.error( "BAZARR cannot parse JSON returned by SignalR feed. This is caused by a permissions " "issue when Sonarr try to access its /config/.config directory." "Typically permissions are too permissive - only the user and group Sonarr runs as should have Read/Write permissions (e.g. files 664 / folders 775)" "You should fix permissions on that directory and restart Sonarr. Also, if you're a Docker image " "user, you should make sure you properly defined PUID/PGID environment variables. " "Otherwise, please contact Sonarr support.") else: logging.info( 'BAZARR SignalR client for Sonarr is connected and waiting for events.' ) finally: if not args.dev: scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1) scheduler.add_job(sync_episodes, kwargs={'send_event': True}, max_instances=1)
def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_series_id=None): url_sonarr_api_series = url + "/api/{0}series/{1}?apikey={2}".format( '' if get_sonarr_info.is_legacy() else 'v3/', sonarr_series_id if sonarr_series_id else "", apikey_sonarr) try: r = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as e: if e.response.status_code: raise requests.exceptions.HTTPError logging.exception( "BAZARR Error trying to get series from Sonarr. Http error.") return except requests.exceptions.ConnectionError: logging.exception( "BAZARR Error trying to get series from Sonarr. Connection Error.") return except requests.exceptions.Timeout: logging.exception( "BAZARR Error trying to get series from Sonarr. Timeout Error.") return except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get series from Sonarr.") return else: return r.json()
def get_tags(): apikey_sonarr = settings.sonarr.apikey tagsDict = [] # Get tags data from Sonarr if get_sonarr_info.is_legacy(): url_sonarr_api_series = url_sonarr( ) + "/api/tag?apikey=" + apikey_sonarr else: url_sonarr_api_series = url_sonarr( ) + "/api/v3/tag?apikey=" + apikey_sonarr try: tagsDict = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) except requests.exceptions.ConnectionError: logging.exception( "BAZARR Error trying to get tags from Sonarr. Connection Error.") return [] except requests.exceptions.Timeout: logging.exception( "BAZARR Error trying to get tags from Sonarr. Timeout Error.") return [] except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get tags from Sonarr.") return [] else: return tagsDict.json()
def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles): overview = show['overview'] if 'overview' in show else '' poster = '' fanart = '' for image in show['images']: if image['coverType'] == 'poster': poster_big = image['url'].split('?')[0] poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1] if image['coverType'] == 'fanart': fanart = image['url'].split('?')[0] alternate_titles = None if show['alternateTitles'] is not None: alternate_titles = str([item['title'] for item in show['alternateTitles']]) audio_language = [] if get_sonarr_info.is_legacy(): audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles) else: audio_language = profile_id_to_language(show['languageProfileId'], audio_profiles) tags = [d['label'] for d in tags_dict if d['id'] in show['tags']] imdbId = show['imdbId'] if 'imdbId' in show else None if action == 'update': return {'title': show["title"], 'path': show["path"], 'tvdbId': int(show["tvdbId"]), 'sonarrSeriesId': int(show["id"]), 'overview': overview, 'poster': poster, 'fanart': fanart, 'audio_language': str(audio_language), 'sortTitle': show['sortTitle'], 'year': str(show['year']), 'alternateTitles': alternate_titles, 'tags': str(tags), 'seriesType': show['seriesType'], 'imdbId': imdbId} else: return {'title': show["title"], 'path': show["path"], 'tvdbId': show["tvdbId"], 'sonarrSeriesId': show["id"], 'overview': overview, 'poster': poster, 'fanart': fanart, 'audio_language': str(audio_language), 'sortTitle': show['sortTitle'], 'year': str(show['year']), 'alternateTitles': alternate_titles, 'tags': str(tags), 'seriesType': show['seriesType'], 'imdbId': imdbId, 'profileId': serie_default_profile}
def get_series_from_sonarr_api(series_id, url, apikey_sonarr): if series_id: url_sonarr_api_series = url + "/api/{0}series/{1}?apikey={2}".format( '' if get_sonarr_info.is_legacy() else 'v3/', series_id, apikey_sonarr) else: url_sonarr_api_series = url + "/api/{0}series?apikey={1}".format( '' if get_sonarr_info.is_legacy() else 'v3/', apikey_sonarr) try: r = requests.get(url_sonarr_api_series, timeout=60, verify=False, headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as e: if e.response.status_code: raise requests.exceptions.HTTPError logging.exception( "BAZARR Error trying to get series from Sonarr. Http error.") return except requests.exceptions.ConnectionError: logging.exception( "BAZARR Error trying to get series from Sonarr. Connection Error.") return except requests.exceptions.Timeout: logging.exception( "BAZARR Error trying to get series from Sonarr. Timeout Error.") return except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get series from Sonarr.") return else: series_json = [] if series_id: series_json.append(r.json()) else: series_json = r.json() series_list = [] for series in series_json: series_list.append({ 'sonarrSeriesId': series['id'], 'title': series['title'] }) return series_list
def get_episodes_from_sonarr_api(url, apikey_sonarr, series_id=None, episode_id=None): if series_id: url_sonarr_api_episode = url + "/api/{0}episode?seriesId={1}&apikey={2}".format( '' if get_sonarr_info.is_legacy() else 'v3/', series_id, apikey_sonarr) elif episode_id: url_sonarr_api_episode = url + "/api/{0}episode/{1}?apikey={2}".format( '' if get_sonarr_info.is_legacy() else 'v3/', episode_id, apikey_sonarr) else: return try: r = requests.get(url_sonarr_api_episode, timeout=60, verify=False, headers=headers) r.raise_for_status() except requests.exceptions.HTTPError: logging.exception( "BAZARR Error trying to get episodes from Sonarr. Http error.") return except requests.exceptions.ConnectionError: logging.exception( "BAZARR Error trying to get episodes from Sonarr. Connection Error." ) return except requests.exceptions.Timeout: logging.exception( "BAZARR Error trying to get episodes from Sonarr. Timeout Error.") return except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get episodes from Sonarr.") return else: return r.json()
def series_images(url): url = url.strip("/") apikey = settings.sonarr.apikey baseUrl = settings.sonarr.base_url if get_sonarr_info.is_legacy(): url_image = (url_sonarr() + '/api/' + url.lstrip(baseUrl) + '?apikey=' + apikey).replace('poster-250', 'poster-500') else: url_image = (url_sonarr() + '/api/v3/' + url.lstrip(baseUrl) + '?apikey=' + apikey).replace('poster-250', 'poster-500') try: req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers) except: return '', 404 else: return Response(stream_with_context(req.iter_content(2048)), content_type=req.headers['content-type'])
def get_sonarr_rootfolder(): apikey_sonarr = settings.sonarr.apikey sonarr_rootfolder = [] # Get root folder data from Sonarr if get_sonarr_info.is_legacy(): url_sonarr_api_rootfolder = url_sonarr() + "/api/rootfolder?apikey=" + apikey_sonarr else: url_sonarr_api_rootfolder = url_sonarr() + "/api/v3/rootfolder?apikey=" + apikey_sonarr try: rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=60, verify=False, headers=headers) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Connection Error.") return [] except requests.exceptions.Timeout: logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Timeout Error.") return [] except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get rootfolder from Sonarr.") return [] else: sonarr_movies_paths = list(TableShows.select(TableShows.path).dicts()) for folder in rootfolder.json(): if any(item['path'].startswith(folder['path']) for item in sonarr_movies_paths): sonarr_rootfolder.append({'id': folder['id'], 'path': folder['path']}) db_rootfolder = TableShowsRootfolder.select(TableShowsRootfolder.id, TableShowsRootfolder.path).dicts() rootfolder_to_remove = [x for x in db_rootfolder if not next((item for item in sonarr_rootfolder if item['id'] == x['id']), False)] rootfolder_to_update = [x for x in sonarr_rootfolder if next((item for item in db_rootfolder if item['id'] == x['id']), False)] rootfolder_to_insert = [x for x in sonarr_rootfolder if not next((item for item in db_rootfolder if item['id'] == x['id']), False)] for item in rootfolder_to_remove: TableShowsRootfolder.delete().where(TableShowsRootfolder.id == item['id']).execute() for item in rootfolder_to_update: TableShowsRootfolder.update({TableShowsRootfolder.path: item['path']})\ .where(TableShowsRootfolder.id == item['id'])\ .execute() for item in rootfolder_to_insert: TableShowsRootfolder.insert({TableShowsRootfolder.id: item['id'], TableShowsRootfolder.path: item['path']})\ .execute()
def sync_episodes(series_id=None, send_event=True): logging.debug('BAZARR Starting episodes sync from Sonarr.') apikey_sonarr = settings.sonarr.apikey # Get current episodes id in DB current_episodes_db = TableEpisodes.select(TableEpisodes.sonarrEpisodeId, TableEpisodes.path, TableEpisodes.sonarrSeriesId)\ .where((TableEpisodes.sonarrSeriesId == series_id) if series_id else None)\ .dicts() current_episodes_db_list = [ x['sonarrEpisodeId'] for x in current_episodes_db ] current_episodes_sonarr = [] episodes_to_update = [] episodes_to_add = [] altered_episodes = [] # Get sonarrId for each series from database seriesIdList = get_series_from_sonarr_api( series_id=series_id, url=url_sonarr(), apikey_sonarr=apikey_sonarr, ) series_count = len(seriesIdList) for i, seriesId in enumerate(seriesIdList): if send_event: show_progress(id='episodes_progress', header='Syncing episodes...', name=seriesId['title'], value=i, count=series_count) # Get episodes data for a series from Sonarr episodes = get_episodes_from_sonarr_api( url=url_sonarr(), apikey_sonarr=apikey_sonarr, series_id=seriesId['sonarrSeriesId']) if not episodes: continue else: # For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results if not get_sonarr_info.is_legacy(): episodeFiles = get_episodesFiles_from_sonarr_api( url=url_sonarr(), apikey_sonarr=apikey_sonarr, series_id=seriesId['sonarrSeriesId']) for episode in episodes: if episode['hasFile']: item = [ x for x in episodeFiles if x['id'] == episode['episodeFileId'] ] if item: episode['episodeFile'] = item[0] for episode in episodes: if 'hasFile' in episode: if episode['hasFile'] is True: if 'episodeFile' in episode: if episode['episodeFile']['size'] > 20480: # Add episodes in sonarr to current episode list current_episodes_sonarr.append(episode['id']) # Parse episode data if episode['id'] in current_episodes_db_list: episodes_to_update.append( episodeParser(episode)) else: episodes_to_add.append( episodeParser(episode)) if send_event: hide_progress(id='episodes_progress') # Remove old episodes from DB removed_episodes = list( set(current_episodes_db_list) - set(current_episodes_sonarr)) for removed_episode in removed_episodes: episode_to_delete = TableEpisodes.select(TableEpisodes.sonarrSeriesId, TableEpisodes.sonarrEpisodeId)\ .where(TableEpisodes.sonarrEpisodeId == removed_episode)\ .dicts()\ .get() TableEpisodes.delete().where( TableEpisodes.sonarrEpisodeId == removed_episode).execute() if send_event: event_stream(type='episode', action='delete', payload=episode_to_delete['sonarrEpisodeId']) # Update existing episodes in DB episode_in_db_list = [] episodes_in_db = TableEpisodes.select( TableEpisodes.sonarrSeriesId, TableEpisodes.sonarrEpisodeId, TableEpisodes.title, TableEpisodes.path, TableEpisodes.season, TableEpisodes.episode, TableEpisodes.scene_name, TableEpisodes.monitored, TableEpisodes.format, TableEpisodes.resolution, TableEpisodes.video_codec, TableEpisodes.audio_codec, TableEpisodes.episode_file_id, TableEpisodes.audio_language, TableEpisodes.file_size).dicts() for item in episodes_in_db: episode_in_db_list.append(item) episodes_to_update_list = [ i for i in episodes_to_update if i not in episode_in_db_list ] for updated_episode in episodes_to_update_list: TableEpisodes.update(updated_episode).where( TableEpisodes.sonarrEpisodeId == updated_episode['sonarrEpisodeId']).execute() altered_episodes.append([ updated_episode['sonarrEpisodeId'], updated_episode['path'], updated_episode['sonarrSeriesId'] ]) # Insert new episodes in DB for added_episode in episodes_to_add: result = TableEpisodes.insert(added_episode).on_conflict( action='IGNORE').execute() if result > 0: altered_episodes.append([ added_episode['sonarrEpisodeId'], added_episode['path'], added_episode['monitored'] ]) if send_event: event_stream(type='episode', payload=added_episode['sonarrEpisodeId']) else: logging.debug( 'BAZARR unable to insert this episode into the database:{}'. format(path_mappings.path_replace(added_episode['path']))) # Store subtitles for added or modified episodes for i, altered_episode in enumerate(altered_episodes, 1): store_subtitles(altered_episode[1], path_mappings.path_replace(altered_episode[1])) logging.debug('BAZARR All episodes synced from Sonarr into database.')
def sync_one_episode(episode_id): logging.debug( 'BAZARR syncing this specific episode from Sonarr: {}'.format( episode_id)) url = url_sonarr() apikey_sonarr = settings.sonarr.apikey # Check if there's a row in database for this episode ID try: existing_episode = TableEpisodes.select(TableEpisodes.path, TableEpisodes.episode_file_id)\ .where(TableEpisodes.sonarrEpisodeId == episode_id)\ .dicts()\ .get() except DoesNotExist: existing_episode = None try: # Get episode data from sonarr api episode = None episode_data = get_episodes_from_sonarr_api( url=url, apikey_sonarr=apikey_sonarr, episode_id=episode_id) if not episode_data: return else: # For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results if not get_sonarr_info.is_legacy( ) and existing_episode and episode_data['hasFile']: episode_data['episodeFile'] = \ get_episodesFiles_from_sonarr_api(url=url, apikey_sonarr=apikey_sonarr, episode_file_id=existing_episode['episode_file_id']) episode = episodeParser(episode_data) except Exception: logging.debug( 'BAZARR cannot get episode returned by SignalR feed from Sonarr API.' ) return # Drop useless events if not episode and not existing_episode: return # Remove episode from DB if not episode and existing_episode: TableEpisodes.delete().where( TableEpisodes.sonarrEpisodeId == episode_id).execute() event_stream(type='episode', action='delete', payload=int(episode_id)) logging.debug( 'BAZARR deleted this episode from the database:{}'.format( path_mappings.path_replace(existing_episode['path']))) return # Update existing episodes in DB elif episode and existing_episode: TableEpisodes.update(episode).where( TableEpisodes.sonarrEpisodeId == episode_id).execute() event_stream(type='episode', action='update', payload=int(episode_id)) logging.debug( 'BAZARR updated this episode into the database:{}'.format( path_mappings.path_replace(episode['path']))) # Insert new episodes in DB elif episode and not existing_episode: TableEpisodes.insert(episode).on_conflict(action='IGNORE').execute() event_stream(type='episode', action='update', payload=int(episode_id)) logging.debug( 'BAZARR inserted this episode into the database:{}'.format( path_mappings.path_replace(episode['path']))) # Storing existing subtitles logging.debug('BAZARR storing subtitles for this episode: {}'.format( path_mappings.path_replace(episode['path']))) store_subtitles(episode['path'], path_mappings.path_replace(episode['path'])) # Downloading missing subtitles logging.debug( 'BAZARR downloading missing subtitles for this episode: {}'.format( path_mappings.path_replace(episode['path']))) episode_download_subtitles(episode_id)