You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/bazarr/sonarr/sync/episodes.py

265 lines
12 KiB

# coding=utf-8
import os
import logging
from constants import MINIMUM_VIDEO_SIZE
from sqlalchemy.exc import IntegrityError
from app.database import database, TableShows, TableEpisodes, delete, update, insert, select
from app.config import settings
from utilities.path_mappings import path_mappings
from subtitles.indexer.series import store_subtitles, series_full_scan_subtitles
from subtitles.mass_download import episode_download_subtitles
from app.event_handler import event_stream
from sonarr.info import get_sonarr_info, url_sonarr
from .parser import episodeParser
from .utils import get_episodes_from_sonarr_api, get_episodesFiles_from_sonarr_api
# map between booleans and strings in DB
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_EPISODES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def get_episodes_monitored_table(series_id):
episodes_monitored = database.execute(
select(TableEpisodes.episode_file_id, TableEpisodes.monitored)
.where(TableEpisodes.sonarrSeriesId == series_id))\
.all()
episode_dict = dict((x, y) for x, y in episodes_monitored)
return episode_dict
def update_all_episodes():
series_full_scan_subtitles()
logging.info('BAZARR All existing episode subtitles indexed from disk.')
def sync_episodes(series_id, send_event=True):
logging.debug(f'BAZARR Starting episodes sync from Sonarr for series ID {series_id}.')
apikey_sonarr = settings.sonarr.apikey
# Get current episodes id in DB
if series_id:
current_episodes_id_db_list = [row.sonarrEpisodeId for row in
database.execute(
select(TableEpisodes.sonarrEpisodeId,
TableEpisodes.path,
TableEpisodes.sonarrSeriesId)
.where(TableEpisodes.sonarrSeriesId == series_id)).all()]
current_episodes_db_kv = [x.items() for x in [y._asdict()['TableEpisodes'].__dict__ for y in
database.execute(
select(TableEpisodes)
.where(TableEpisodes.sonarrSeriesId == series_id))
.all()]]
else:
return
current_episodes_sonarr = []
episodes_to_update = []
episodes_to_add = []
# Get episodes data for a series from Sonarr
episodes = get_episodes_from_sonarr_api(apikey_sonarr=apikey_sonarr, series_id=series_id)
if episodes:
# 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(apikey_sonarr=apikey_sonarr, series_id=series_id)
for episode in episodes:
if episodeFiles and episode['hasFile']:
item = [x for x in episodeFiles if x['id'] == episode['episodeFileId']]
if item:
episode['episodeFile'] = item[0]
sync_monitored = settings.sonarr.sync_only_monitored_series and settings.sonarr.sync_only_monitored_episodes
if sync_monitored:
episodes_monitored = get_episodes_monitored_table(series_id)
skipped_count = 0
for episode in episodes:
if 'hasFile' in episode:
if episode['hasFile'] is True:
if 'episodeFile' in episode:
# monitored_status_db = get_episodes_monitored_status(episode['episodeFileId'])
if sync_monitored:
try:
monitored_status_db = bool_map[episodes_monitored[episode['episodeFileId']]]
except KeyError:
monitored_status_db = None
if monitored_status_db is None:
# not in db, might need to add, if we have a file on disk
pass
elif monitored_status_db != episode['monitored']:
# monitored status changed and we don't know about it until now
trace(f"(Monitor Status Mismatch) {episode['title']}")
# pass
elif not episode['monitored']:
# Add unmonitored episode in sonarr to current episode list, otherwise it will be deleted from db
current_episodes_sonarr.append(episode['id'])
skipped_count += 1
continue
try:
bazarr_file_size = \
os.path.getsize(path_mappings.path_replace(episode['episodeFile']['path']))
except OSError:
bazarr_file_size = 0
if episode['episodeFile']['size'] > MINIMUM_VIDEO_SIZE or bazarr_file_size > MINIMUM_VIDEO_SIZE:
# Add episodes in sonarr to current episode list
current_episodes_sonarr.append(episode['id'])
# Parse episode data
if episode['id'] in current_episodes_id_db_list:
parsed_episode = episodeParser(episode)
if not any([parsed_episode.items() <= x for x in current_episodes_db_kv]):
episodes_to_update.append(parsed_episode)
else:
episodes_to_add.append(episodeParser(episode))
else:
return
if sync_monitored:
# try to avoid unnecessary database calls
if settings.general.debug:
series_title = database.execute(select(TableShows.title).where(TableShows.sonarrSeriesId == series_id)).first()[0]
trace(f"Skipped {skipped_count} unmonitored episodes out of {len(episodes)} for {series_title}")
# Remove old episodes from DB
episodes_to_delete = list(set(current_episodes_id_db_list) - set(current_episodes_sonarr))
if len(episodes_to_delete):
try:
database.execute(delete(TableEpisodes).where(TableEpisodes.sonarrEpisodeId.in_(episodes_to_delete)))
except IntegrityError as e:
logging.error(f"BAZARR cannot delete episodes because of {e}")
else:
for removed_episode in episodes_to_delete:
if send_event:
event_stream(type='episode', action='delete', payload=removed_episode)
# Update existing episodes in DB
if len(episodes_to_update):
for updated_episode in episodes_to_update:
try:
database.execute(update(TableEpisodes)
.values(updated_episode)
.where(TableEpisodes.sonarrEpisodeId == updated_episode['sonarrEpisodeId']))
except IntegrityError as e:
logging.error(f"BAZARR cannot update episodes because of {e}")
else:
store_subtitles(updated_episode['path'], path_mappings.path_replace(updated_episode['path']))
if send_event:
event_stream(type='episode', action='update', payload=updated_episode['sonarrEpisodeId'])
# Insert new episodes in DB
if len(episodes_to_add):
for added_episode in episodes_to_add:
try:
database.execute(insert(TableEpisodes).values(added_episode))
except IntegrityError as e:
logging.error(f"BAZARR cannot insert episodes because of {e}")
else:
store_subtitles(added_episode['path'], path_mappings.path_replace(added_episode['path']))
if send_event:
event_stream(type='episode', payload=added_episode['sonarrEpisodeId'])
logging.debug(f'BAZARR All episodes from series ID {series_id} synced from Sonarr into database.')
def sync_one_episode(episode_id, defer_search=False):
logging.debug(f'BAZARR syncing this specific episode from Sonarr: {episode_id}')
url = url_sonarr()
apikey_sonarr = settings.sonarr.apikey
# Check if there's a row in database for this episode ID
existing_episode = database.execute(
select(TableEpisodes.path, TableEpisodes.episode_file_id)
.where(TableEpisodes.sonarrEpisodeId == episode_id)) \
.first()
try:
# Get episode data from sonarr api
episode = None
episode_data = get_episodes_from_sonarr_api(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(apikey_sonarr=apikey_sonarr,
episode_file_id=episode_data['episodeFileId'])
episode = episodeParser(episode_data)
except Exception:
logging.exception('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:
try:
database.execute(
delete(TableEpisodes)
.where(TableEpisodes.sonarrEpisodeId == episode_id))
except IntegrityError as e:
logging.error(f"BAZARR cannot delete episode {existing_episode.path} because of {e}")
else:
event_stream(type='episode', action='delete', payload=int(episode_id))
logging.debug(
f'BAZARR deleted this episode from the database:{path_mappings.path_replace(existing_episode["path"])}')
return
# Update existing episodes in DB
elif episode and existing_episode:
try:
database.execute(
update(TableEpisodes)
.values(episode)
.where(TableEpisodes.sonarrEpisodeId == episode_id))
except IntegrityError as e:
logging.error(f"BAZARR cannot update episode {episode['path']} because of {e}")
else:
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
event_stream(type='episode', action='update', payload=int(episode_id))
logging.debug(
f'BAZARR updated this episode into the database:{path_mappings.path_replace(episode["path"])}')
# Insert new episodes in DB
elif episode and not existing_episode:
try:
database.execute(
insert(TableEpisodes)
.values(episode))
except IntegrityError as e:
logging.error(f"BAZARR cannot insert episode {episode['path']} because of {e}")
else:
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
event_stream(type='episode', action='update', payload=int(episode_id))
logging.debug(
f'BAZARR inserted this episode into the database:{path_mappings.path_replace(episode["path"])}')
# Storing existing subtitles
logging.debug(f'BAZARR storing subtitles for this episode: {path_mappings.path_replace(episode["path"])}')
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
# Downloading missing subtitles
if defer_search:
logging.debug(
f'BAZARR searching for missing subtitles is deferred until scheduled task execution for this episode: '
f'{path_mappings.path_replace(episode["path"])}')
else:
logging.debug(
f'BAZARR downloading missing subtitles for this episode: {path_mappings.path_replace(episode["path"])}')
episode_download_subtitles(episode_id)