Merge pull request #2 from morpheus65535/development

Development
pull/2794/head
destpstrzy 4 months ago committed by GitHub
commit 87f32dc0fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -25,8 +25,8 @@ def check_python_version():
print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.")
exit_program(EXIT_PYTHON_UPGRADE_NEEDED)
elif int(python_version[0]) == 3 and int(python_version[1]) > 11:
print("Python version greater than 3.11.x is unsupported. Current version is " + platform.python_version() +
elif int(python_version[0]) == 3 and int(python_version[1]) > 12:
print("Python version greater than 3.12.x is unsupported. Current version is " + platform.python_version() +
". Keep in mind that even if it works, you're on your own.")
elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \
(int(python_version[0]) != minimum_py3_tuple[0]):

@ -36,7 +36,7 @@ class Badges(Resource):
def get(self):
"""Get badges count to update the UI"""
episodes_conditions = [(TableEpisodes.missing_subtitles.is_not(None)),
(TableEpisodes.missing_subtitles.is_not('[]'))]
(TableEpisodes.missing_subtitles != '[]')]
episodes_conditions += get_exclusion_clause('series')
missing_episodes = database.execute(
select(TableEpisodes.missing_subtitles)
@ -49,7 +49,7 @@ class Badges(Resource):
missing_episodes_count += len(ast.literal_eval(episode.missing_subtitles))
movies_conditions = [(TableMovies.missing_subtitles.is_not(None)),
(TableMovies.missing_subtitles.is_not('[]'))]
(TableMovies.missing_subtitles != '[]')]
movies_conditions += get_exclusion_clause('movie')
missing_movies = database.execute(
select(TableMovies.missing_subtitles)

@ -49,7 +49,7 @@ class EpisodesWanted(Resource):
episodeid = args.get('episodeid[]')
wanted_conditions = [(TableEpisodes.missing_subtitles.is_not(None)),
(TableEpisodes.missing_subtitles.is_not('[]'))]
(TableEpisodes.missing_subtitles != '[]')]
if len(episodeid) > 0:
wanted_conditions.append((TableEpisodes.sonarrEpisodeId in episodeid))
start = 0

@ -46,7 +46,7 @@ class MoviesWanted(Resource):
radarrid = args.get("radarrid[]")
wanted_conditions = [(TableMovies.missing_subtitles.is_not(None)),
(TableMovies.missing_subtitles.is_not('[]'))]
(TableMovies.missing_subtitles != '[]')]
if len(radarrid) > 0:
wanted_conditions.append((TableMovies.radarrId.in_(radarrid)))
start = 0

@ -34,9 +34,11 @@ class Series(Resource):
'alternativeTitles': fields.List(fields.String),
'audio_language': fields.Nested(get_audio_language_model),
'episodeFileCount': fields.Integer(default=0),
'ended': fields.Boolean(),
'episodeMissingCount': fields.Integer(default=0),
'fanart': fields.String(),
'imdbId': fields.String(),
'lastAired': fields.String(),
'monitored': fields.Boolean(),
'overview': fields.String(),
'path': fields.String(),
@ -74,7 +76,7 @@ class Series(Resource):
.subquery()
episodes_missing_conditions = [(TableEpisodes.missing_subtitles.is_not(None)),
(TableEpisodes.missing_subtitles.is_not('[]'))]
(TableEpisodes.missing_subtitles != '[]')]
episodes_missing_conditions += get_exclusion_clause('series')
episodeMissingCount = select(TableShows.sonarrSeriesId,
@ -100,6 +102,8 @@ class Series(Resource):
TableShows.tags,
TableShows.title,
TableShows.year,
TableShows.ended,
TableShows.lastAired,
episodeFileCount.c.episodeFileCount,
episodeMissingCount.c.episodeMissingCount) \
.select_from(TableShows) \
@ -128,6 +132,8 @@ class Series(Resource):
'tags': x.tags,
'title': x.title,
'year': x.year,
'ended': x.ended,
'lastAired': x.lastAired,
'episodeFileCount': x.episodeFileCount,
'episodeMissingCount': x.episodeMissingCount,
}) for x in database.execute(stmt).all()]

@ -276,8 +276,10 @@ class TableShows(Base):
alternativeTitles = mapped_column(Text)
audio_language = mapped_column(Text)
created_at_timestamp = mapped_column(DateTime)
ended = mapped_column(Text)
fanart = mapped_column(Text)
imdbId = mapped_column(Text)
lastAired = mapped_column(Text)
monitored = mapped_column(Text)
overview = mapped_column(Text)
path = mapped_column(Text, nullable=False, unique=True)

@ -206,7 +206,11 @@ def update_movies(send_event=True):
files_missing += 1
if send_event:
hide_progress(id='movies_progress')
show_progress(id='movies_progress',
header='Syncing movies...',
name='',
value=movies_count,
count=movies_count)
trace(f"Skipped {files_missing} file missing movies out of {movies_count}")
if sync_monitored:

@ -2,6 +2,8 @@
import os
from dateutil import parser
from app.config import settings
from app.database import TableShows, database, select
from constants import MINIMUM_VIDEO_SIZE
@ -45,6 +47,10 @@ def seriesParser(show, action, tags_dict, language_profiles, serie_default_profi
imdbId = show['imdbId'] if 'imdbId' in show else None
ended = 'True' if 'ended' in show and show['ended'] else 'False'
lastAired = parser.parse(show['lastAired']).strftime("%Y-%m-%d") if 'lastAired' in show and show['lastAired'] else None
audio_language = []
if not settings.general.parse_embedded_audio_track:
if get_sonarr_info.is_legacy():
@ -56,22 +62,24 @@ def seriesParser(show, action, tags_dict, language_profiles, serie_default_profi
audio_language = []
parsed_series = {
'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']),
'alternativeTitles': str(alternate_titles),
'tags': str(tags),
'seriesType': show['seriesType'],
'imdbId': imdbId,
'monitored': str(bool(show['monitored']))
}
'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']),
'alternativeTitles': str(alternate_titles),
'tags': str(tags),
'seriesType': show['seriesType'],
'imdbId': imdbId,
'monitored': str(bool(show['monitored'])),
'ended': ended,
'lastAired': lastAired,
}
if action == 'insert':
parsed_series['profileId'] = serie_default_profile

@ -178,7 +178,11 @@ def update_series(send_event=True):
event_stream(type='series', action='delete', payload=series)
if send_event:
hide_progress(id='series_progress')
show_progress(id='series_progress',
header='Syncing series...',
name='',
value=series_count,
count=series_count)
if sync_monitored:
trace(f"skipped {skipped_count} unmonitored series out of {i}")

@ -292,7 +292,11 @@ def movies_full_scan_subtitles(use_cache=None):
count=count_movies)
store_subtitles_movie(movie.path, path_mappings.path_replace_movie(movie.path), use_cache=use_cache)
hide_progress(id='movies_disk_scan')
show_progress(id='movies_disk_scan',
header='Full disk scan...',
name='Movies subtitles',
value=count_movies,
count=count_movies)
gc.collect()

@ -294,7 +294,11 @@ def series_full_scan_subtitles(use_cache=None):
count=count_episodes)
store_subtitles(episode.path, path_mappings.path_replace(episode.path), use_cache=use_cache)
hide_progress(id='episodes_disk_scan')
show_progress(id='episodes_disk_scan',
header='Full disk scan...',
name='Episodes subtitles',
value=count_episodes,
count=count_episodes)
gc.collect()

@ -20,7 +20,7 @@ from ..download import generate_subtitles
def movies_download_subtitles(no):
conditions = [(TableMovies.radarrId.is_(no))]
conditions = [(TableMovies.radarrId == no)]
conditions += get_exclusion_clause('movie')
stmt = select(TableMovies.path,
TableMovies.missing_subtitles,
@ -99,4 +99,8 @@ def movies_download_subtitles(no):
history_log_movie(1, no, result)
send_notifications_movie(no, result.message)
hide_progress(id=f'movie_search_progress_{no}')
show_progress(id=f'movie_search_progress_{no}',
header='Searching missing subtitles...',
name=movie.title,
value=count_movie,
count=count_movie)

@ -29,7 +29,7 @@ def series_download_subtitles(no):
raise OSError
conditions = [(TableEpisodes.sonarrSeriesId == no),
(TableEpisodes.missing_subtitles.is_not('[]'))]
(TableEpisodes.missing_subtitles != '[]')]
conditions += get_exclusion_clause('series')
episodes_details = database.execute(
select(TableEpisodes.sonarrEpisodeId,
@ -64,7 +64,11 @@ def series_download_subtitles(no):
logging.info("BAZARR All providers are throttled")
break
hide_progress(id=f'series_search_progress_{no}')
show_progress(id=f'series_search_progress_{no}',
header='Searching missing subtitles...',
name='',
value=count_episodes_details,
count=count_episodes_details)
def episode_download_subtitles(no, send_progress=False, providers_list=None):
@ -145,6 +149,10 @@ def episode_download_subtitles(no, send_progress=False, providers_list=None):
send_notifications(episode.sonarrSeriesId, episode.sonarrEpisodeId, result.message)
if send_progress:
hide_progress(id=f'episode_search_progress_{no}')
show_progress(id=f'episode_search_progress_{no}',
header='Searching missing subtitles...',
name=f'{episode.title} - S{episode.season:02d}E{episode.episode:02d} - {episode.episodeTitle}',
value=1,
count=1)
else:
logging.info("BAZARR All providers are throttled")

@ -111,7 +111,7 @@ class AniDBClient(object):
mapping_list = anime.find('mapping-list')
# Handle mapping list for Specials
if mapping_list:
if mapping_list is not None:
for mapping in mapping_list.findall("mapping"):
if mapping.text is None:
continue
@ -176,7 +176,7 @@ class AniDBClient(object):
episode_elements = xml_root.find('episodes')
if not episode_elements:
if episode_elements is None:
raise ValueError
return etree.tostring(episode_elements, encoding='utf8', method='xml')

@ -40,10 +40,8 @@ def refine_from_db(path, video):
if data:
video.series = _TITLE_RE.sub('', data.seriesTitle)
if not video.season and data.season:
video.season = int(data.season)
if not video.episode and data.episode:
video.episode = int(data.episode)
video.season = int(data.season)
video.episode = int(data.episode)
video.title = data.episodeTitle
# Only refine year as a fallback

@ -3,8 +3,10 @@
import logging
import gc
import os
from app.config import settings
from app.event_handler import show_progress, hide_progress
from subtitles.tools.subsyncer import SubSyncer
@ -40,7 +42,22 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, hi, percent_score, so
'sonarr_episode_id': sonarr_episode_id,
'radarr_id': radarr_id,
}
subsync.sync(**sync_kwargs)
subtitles_filename = os.path.basename(srt_path)
show_progress(id=f'subsync_{subtitles_filename}',
header='Syncing Subtitle',
name=srt_path,
value=0,
count=1)
try:
subsync.sync(**sync_kwargs)
except Exception:
hide_progress(id=f'subsync_{subtitles_filename}')
else:
show_progress(id=f'subsync_{subtitles_filename}',
header='Syncing Subtitle',
name=srt_path,
value=1,
count=1)
del subsync
gc.collect()
return True

@ -94,7 +94,11 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
for i, line in enumerate(translated_lines):
lines_list[line['id']] = line['line']
hide_progress(id=f'translate_progress_{dest_srt_file}')
show_progress(id=f'translate_progress_{dest_srt_file}',
header=f'Translating subtitles lines to {language_from_alpha3(to_lang)}...',
name='',
value=lines_list_len,
count=lines_list_len)
logging.debug(f'BAZARR saving translated subtitles to {dest_srt_file}')
for i, line in enumerate(subs):

@ -133,7 +133,11 @@ def upgrade_subtitles():
upgraded_from_id=episode['original_id'])
send_notifications(episode['sonarrSeriesId'], episode['sonarrEpisodeId'], result.message)
hide_progress(id='upgrade_episodes_progress')
show_progress(id='upgrade_episodes_progress',
header='Upgrading episodes subtitles...',
name='',
value=count_episode_to_upgrade,
count=count_episode_to_upgrade)
if use_radarr:
movies_to_upgrade = get_upgradable_movies_subtitles()
@ -231,7 +235,11 @@ def upgrade_subtitles():
history_log_movie(3, movie['radarrId'], result, upgraded_from_id=movie['original_id'])
send_notifications_movie(movie['radarrId'], result.message)
hide_progress(id='upgrade_movies_progress')
show_progress(id='upgrade_movies_progress',
header='Upgrading movies subtitles...',
name='',
value=count_movie_to_upgrade,
count=count_movie_to_upgrade)
logging.info('BAZARR Finished searching for Subtitles to upgrade. Check History for more information.')

@ -39,9 +39,8 @@ def get_video(path, title, sceneName, providers=None, media_type="movie"):
logging.debug(f'BAZARR guessing video object using scene name: {scenename_with_extension}')
scenename_video = parse_video(scenename_with_extension, hints=hints, dry_run=True)
refine_video_with_scenename(initial_video=video, scenename_video=scenename_video)
video.original_name = os.path.basename(path)
video.original_path = path
logging.debug('BAZARR resulting video object once refined using scene name: %s',
json.dumps(vars(video), cls=GuessitEncoder, indent=4, ensure_ascii=False))
for key, refiner in registered_refiners.items():
logging.debug("Running refiner: %s", key)
@ -105,6 +104,6 @@ def _set_forced_providers(pool, also_forced=False, forced_required=False):
def refine_video_with_scenename(initial_video, scenename_video):
for key, value in vars(scenename_video).items():
if value:
if value and getattr(initial_video, key) in [None, (), {}, []]:
setattr(initial_video, key, value)
return initial_video

@ -97,7 +97,7 @@ def wanted_download_subtitles_movie(radarr_id):
def wanted_search_missing_subtitles_movies():
conditions = [(TableMovies.missing_subtitles.is_not(None)),
(TableMovies.missing_subtitles.is_not('[]'))]
(TableMovies.missing_subtitles != '[]')]
conditions += get_exclusion_clause('movie')
movies = database.execute(
select(TableMovies.radarrId,
@ -122,6 +122,10 @@ def wanted_search_missing_subtitles_movies():
logging.info("BAZARR All providers are throttled")
break
hide_progress(id='wanted_movies_progress')
show_progress(id='wanted_movies_progress',
header='Searching subtitles...',
name="",
value=count_movies,
count=count_movies)
logging.info('BAZARR Finished searching for missing Movies Subtitles. Check History for more information.')

@ -102,7 +102,7 @@ def wanted_download_subtitles(sonarr_episode_id):
def wanted_search_missing_subtitles_series():
conditions = [(TableEpisodes.missing_subtitles.is_not(None)),
(TableEpisodes.missing_subtitles.is_not('[]'))]
(TableEpisodes.missing_subtitles != '[]')]
conditions += get_exclusion_clause('series')
episodes = database.execute(
select(TableEpisodes.sonarrSeriesId,
@ -134,6 +134,10 @@ def wanted_search_missing_subtitles_series():
logging.info("BAZARR All providers are throttled")
break
hide_progress(id='wanted_episodes_progress')
show_progress(id='wanted_episodes_progress',
header='Searching subtitles...',
name='',
value=count_episodes,
count=count_episodes)
logging.info('BAZARR Finished searching for missing Series Subtitles. Check History for more information.')

@ -255,8 +255,6 @@ class EmbeddedSubtitlesProvider(Provider):
class _MemoizedFFprobeVideoContainer(FFprobeVideoContainer):
# 128 is the default value for maxsize since Python 3.8. We ste it here for previous versions.
@functools.lru_cache(maxsize=128)
def get_subtitles(self, *args, **kwargs):
return super().get_subtitles(*args, **kwargs)

@ -209,7 +209,7 @@ class PodnapisiProvider(_PodnapisiProvider, ProviderSubtitleArchiveMixin):
break
# exit if no results
if (not xml.find('pagination/results') or not xml.find('pagination/results').text or not
if (xml.find('pagination/results') is None or not xml.find('pagination/results').text or not
int(xml.find('pagination/results').text)):
logger.debug('No subtitles found')
break

@ -9,12 +9,12 @@
"version": "1.0.0",
"license": "GPL-3",
"dependencies": {
"@mantine/core": "^7.13.5",
"@mantine/dropzone": "^7.13.5",
"@mantine/form": "^7.13.5",
"@mantine/hooks": "^7.13.5",
"@mantine/modals": "^7.13.5",
"@mantine/notifications": "^7.13.5",
"@mantine/core": "^7.14.3",
"@mantine/dropzone": "^7.14.3",
"@mantine/form": "^7.14.3",
"@mantine/hooks": "^7.14.3",
"@mantine/modals": "^7.14.3",
"@mantine/notifications": "^7.14.3",
"@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.7.4",
@ -2390,9 +2390,9 @@
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.27",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.27.tgz",
"integrity": "sha512-jLP72x0Kr2CgY6eTYi/ra3VA9LOkTo4C+DUTrbFgFOExKy3omYVmwMjNKqxAHdsnyLS96BIDLcO2SlnsNf8KUQ==",
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
@ -2639,27 +2639,27 @@
}
},
"node_modules/@mantine/core": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.13.5.tgz",
"integrity": "sha512-1m0C0qH9eIWJZy19M06kKNWbbSLZhsTDvHPqTxMgvFg6JuSN7a6r3v6fqCbvaI1kTQiK51NMe+9vMNVnw4zOsA==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.14.3.tgz",
"integrity": "sha512-niAi+ZYBr4KrG+X2Mx+muvEzUOOHc/Rx0vsbIGYeNe7urwHSm/xNEGsaapmCqeRC0CSL4KI6TJOq8QhnSuQZcw==",
"dependencies": {
"@floating-ui/react": "^0.26.27",
"@floating-ui/react": "^0.26.28",
"clsx": "^2.1.1",
"react-number-format": "^5.4.2",
"react-remove-scroll": "^2.6.0",
"react-textarea-autosize": "8.5.4",
"type-fest": "^4.26.1"
"react-textarea-autosize": "8.5.5",
"type-fest": "^4.27.0"
},
"peerDependencies": {
"@mantine/hooks": "7.13.5",
"@mantine/hooks": "7.14.3",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/core/node_modules/type-fest": {
"version": "4.26.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
"version": "4.30.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz",
"integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==",
"engines": {
"node": ">=16"
},
@ -2668,23 +2668,23 @@
}
},
"node_modules/@mantine/dropzone": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.13.5.tgz",
"integrity": "sha512-+CNC0hV7bxoiK4M1BXYHEM8J8mIZbpFr7RYUfoSC2dmZ49FYYWV1t59tUHDfygEOCHTUY1U+RB0hDGGBwoa0SA==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.14.3.tgz",
"integrity": "sha512-9ExiWRod5/gBHBd4hsUnPk7Rles0BiJr5FE2Kuq7lqeEXbtYfuSognJD/f5atMgu/5mMEkkyK/Bq5XesZBumBQ==",
"dependencies": {
"react-dropzone-esm": "15.0.1"
"react-dropzone-esm": "15.2.0"
},
"peerDependencies": {
"@mantine/core": "7.13.5",
"@mantine/hooks": "7.13.5",
"@mantine/core": "7.14.3",
"@mantine/hooks": "7.14.3",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/form": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.13.5.tgz",
"integrity": "sha512-BnHbGFPRlIfzpetg+igjn81MUuI38qEcLhiC3s7grolaJncAnvcxSEVUTiwUJP2KS6mqxtNHKOcaqEs7rH8Umg==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.14.3.tgz",
"integrity": "sha512-NquXVQz3IRCT5WTWCEdQjQzThMj7FpX/u0PDD+8XydiMPB7zJGPM9IdV88mWDI2ghT9vS6rBn22XWjTYsKa8+A==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"klona": "^2.0.6"
@ -2694,43 +2694,43 @@
}
},
"node_modules/@mantine/hooks": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.13.5.tgz",
"integrity": "sha512-hxFOQn6NeN7fP37VXZh7z5KxwqA9HYmydivIay0jyQTYA4Falc8Pb4ozSxnyFbXyxzUWcFIQL4xayHRvedgE+Q==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.14.3.tgz",
"integrity": "sha512-cU3R9b8GLs6aCvpsVC56ZOOJCUIoDqX3RcLWkcfpA5a47LjWa/rzegP4YWfNW6/E9vodPJT4AEbYXVffYlyNwA==",
"peerDependencies": {
"react": "^18.x || ^19.x"
}
},
"node_modules/@mantine/modals": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.13.5.tgz",
"integrity": "sha512-baYeRR7VB2GpdydJGYlsAvorToN021hWCJArLY0PqjDRNXMQK1luDPDHpKEJQWkMqr67axow0LIiSPUDkk7F1A==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.14.3.tgz",
"integrity": "sha512-wn2eMSROG7bPbeSH2OnTp8iVv1wH9L9tLeBt88mTEXLg3vIPfQtWD9g/kFrjhoCjygYYtyJeqMQFYPUkHQMXDw==",
"peerDependencies": {
"@mantine/core": "7.13.5",
"@mantine/hooks": "7.13.5",
"@mantine/core": "7.14.3",
"@mantine/hooks": "7.14.3",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/notifications": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.13.5.tgz",
"integrity": "sha512-LGxTnqBtZRfACCv8qjL+RGVFwpXXjwIekC1V9JalcNt8/PxZ9H50V/H7lOgTvw5wpnA0W6Jzcdph/gRoZfhwug==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.14.3.tgz",
"integrity": "sha512-7N9u4upi1On8TL94UvrUNhpDGxp1sAkbcgiNcu6zhvy4j29TPFapoXB5CRE9zzjAf3CYq3AigE96bXlCDm9xuQ==",
"dependencies": {
"@mantine/store": "7.13.5",
"@mantine/store": "7.14.3",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@mantine/core": "7.13.5",
"@mantine/hooks": "7.13.5",
"@mantine/core": "7.14.3",
"@mantine/hooks": "7.14.3",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/store": {
"version": "7.13.5",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.13.5.tgz",
"integrity": "sha512-+enhEaZpVKn0x3PLN3Txlk/06eIuq2wQhlFQnBe4dnD+C9VZZhXdff/IYCtXwB4XojwJl3rln7BSL4Ih4rSGmw==",
"version": "7.14.3",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.14.3.tgz",
"integrity": "sha512-o15vbTUNlLqD/yLOtEClnc4fY2ONDaCZiaL9REUy0xhCDbVTeeqnu9BV604yuym50ZH5mhMHix1TX3K9vGsWvA==",
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@ -5293,10 +5293,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -8938,9 +8939,9 @@
}
},
"node_modules/react-dropzone-esm": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.0.1.tgz",
"integrity": "sha512-RdeGpqwHnoV/IlDFpQji7t7pTtlC2O1i/Br0LWkRZ9hYtLyce814S71h5NolnCZXsIN5wrZId6+8eQj2EBnEzg==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.2.0.tgz",
"integrity": "sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==",
"dependencies": {
"prop-types": "^15.8.1"
},
@ -9087,9 +9088,9 @@
}
},
"node_modules/react-textarea-autosize": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.4.tgz",
"integrity": "sha512-eSSjVtRLcLfFwFcariT77t9hcbVJHQV76b51QjQGarQIHml2+gM2lms0n3XrhnDmgK5B+/Z7TmQk5OHNzqYm/A==",
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.5.tgz",
"integrity": "sha512-CVA94zmfp8m4bSHtWwmANaBR8EPsKy2aZ7KwqhoS4Ftib87F9Kvi7XQhOixypPLMc6kVYgOXvKFuuzZDpHGRPg==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",

@ -13,12 +13,12 @@
},
"private": true,
"dependencies": {
"@mantine/core": "^7.13.5",
"@mantine/dropzone": "^7.13.5",
"@mantine/form": "^7.13.5",
"@mantine/hooks": "^7.13.5",
"@mantine/modals": "^7.13.5",
"@mantine/notifications": "^7.13.5",
"@mantine/core": "^7.14.3",
"@mantine/dropzone": "^7.14.3",
"@mantine/form": "^7.14.3",
"@mantine/hooks": "^7.14.3",
"@mantine/modals": "^7.14.3",
"@mantine/notifications": "^7.14.3",
"@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.7.4",

@ -9,7 +9,7 @@ import {
Stack,
Text,
} from "@mantine/core";
import { useHover, useMediaQuery } from "@mantine/hooks";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import {
faCheckCircle,
faExclamationCircle,
@ -33,7 +33,7 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
}) => {
const hasIssues = dont.length > 0;
const { hovered, ref } = useHover();
const [opened, { close, open }] = useDisclosure(false);
const isMobile = useMediaQuery(`(max-width: ${em(750)})`);
@ -52,9 +52,9 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
};
return (
<Popover opened={hovered} position="top" width={360} withArrow withinPortal>
<Popover position="left" opened={opened} width={360} withArrow withinPortal>
<Popover.Target>
<Text ref={ref}>
<Text onMouseEnter={open} onMouseLeave={close}>
<PopoverTarget />
</Text>
</Popover.Target>

@ -1,6 +1,5 @@
import { FunctionComponent, ReactElement } from "react";
import { Tooltip, TooltipProps } from "@mantine/core";
import { useHover } from "@mantine/hooks";
import { isNull, isUndefined } from "lodash";
interface TextPopoverProps {
@ -14,20 +13,18 @@ const TextPopover: FunctionComponent<TextPopoverProps> = ({
text,
tooltip,
}) => {
const { hovered, ref } = useHover();
if (isNull(text) || isUndefined(text)) {
return children;
}
return (
<Tooltip
opened={hovered}
label={text}
{...tooltip}
style={{ textWrap: "wrap" }}
events={{ hover: true, focus: false, touch: true }}
>
<div ref={ref}>{children}</div>
<div>{children}</div>
</Tooltip>
);
};

@ -65,7 +65,12 @@ const HistoryIcon: FunctionComponent<{
if (icon) {
return (
<Tooltip label={label} openDelay={500} position="right">
<Tooltip
label={label}
openDelay={500}
position="right"
events={{ hover: true, focus: false, touch: true }}
>
<FontAwesomeIcon
aria-label={label}
title={title}

@ -107,27 +107,32 @@ class TaskDispatcher {
public updateProgress(items: Site.Progress[]) {
items.forEach((item) => {
// TODO: FIX ME!
item.value += 1;
if (item.value >= item.count && this.progress[item.id]) {
updateNotification(notification.progress.end(item.id, item.header));
delete this.progress[item.id];
} else if (item.value > 1 && this.progress[item.id]) {
updateNotification(
notification.progress.update(
item.id,
item.header,
item.name,
item.value,
item.count,
),
);
} else if (item.value > 1 && this.progress[item.id] === undefined) {
if (this.progress[item.id] === undefined) {
showNotification(notification.progress.pending(item.id, item.header));
this.progress[item.id] = true;
setTimeout(() => this.updateProgress([item]), 1000);
return;
}
if (item.value >= item.count) {
updateNotification(notification.progress.end(item.id, item.header));
delete this.progress[item.id];
return;
}
item.value += 1;
updateNotification(
notification.progress.update(
item.id,
item.header,
item.name,
item.value,
item.count,
),
);
});
}

@ -13,12 +13,16 @@ import { showNotification } from "@mantine/notifications";
import {
faAdjust,
faBriefcase,
faCalendar,
faCircleChevronDown,
faCircleChevronRight,
faCloudUploadAlt,
faHdd,
faPlay,
faSearch,
faStop,
faSync,
faTriangleExclamation,
faWrench,
} from "@fortawesome/free-solid-svg-icons";
import { Table as TableInstance } from "@tanstack/table-core/build/lib/types";
@ -62,6 +66,18 @@ const SeriesEpisodesView: FunctionComponent = () => {
icon: faHdd,
text: `${series?.episodeFileCount} files`,
},
{
icon: faTriangleExclamation,
text: `${series?.episodeMissingCount} missing subtitles`,
},
{
icon: series?.ended ? faStop : faPlay,
text: series?.ended ? "Ended" : "Continuing",
},
{
icon: faCalendar,
text: `Last ${series?.ended ? "aired on" : "known airdate"}: ${series?.lastAired}`,
},
{
icon: faAdjust,
text: series?.seriesType ?? "",

@ -1,9 +1,14 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
import { Anchor, Container, Progress } from "@mantine/core";
import { Anchor, Container, Group, Progress } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import {
faBookmark,
faPlay,
faStop,
faWrench,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { useSeriesModification, useSeriesPagination } from "@/apis/hooks";
@ -23,16 +28,19 @@ const SeriesView: FunctionComponent = () => {
const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [
{
id: "monitored",
cell: ({
row: {
original: { monitored },
},
}) => (
<FontAwesomeIcon
title={monitored ? "monitored" : "unmonitored"}
icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon>
id: "status",
cell: ({ row: { original } }) => (
<Group gap="xs" wrap="nowrap">
<FontAwesomeIcon
title={original.monitored ? "monitored" : "unmonitored"}
icon={original.monitored ? faBookmark : farBookmark}
></FontAwesomeIcon>
<FontAwesomeIcon
title={original.ended ? "Ended" : "Continuing"}
icon={original.ended ? faStop : faPlay}
></FontAwesomeIcon>
</Group>
),
},
{

@ -161,7 +161,7 @@ const SettingsLanguagesView: FunctionComponent = () => {
empty if you don't want Bazarr to remove language profiles.
</Message>
</Section>
<Section header="Default Settings">
<Section header="Default Language Profiles For Newly Added Shows">
<Check
label="Series"
settingKey="settings-general-serie_default_enabled"

@ -1,4 +1,5 @@
import {
Fragment,
FunctionComponent,
useCallback,
useMemo,
@ -42,7 +43,7 @@ import {
} from "@/pages/Settings/utilities/SettingsProvider";
import { BuildKey, useSelectorOptions } from "@/utilities";
import { ASSERT } from "@/utilities/console";
import { ProviderInfo } from "./list";
import { ProviderInfo, ProviderList } from "./list";
type SettingsKey =
| "settings-general-enabled_providers"
@ -151,6 +152,27 @@ const SelectItem: AutocompleteProps["renderOption"] = ({ option }) => {
);
};
const validation = ProviderList.map((provider) => {
return provider.inputs
?.map((input) => {
if (input.validation === undefined) {
return null;
}
return {
[`settings-${provider.key}-${input.key}`]: input.validation?.rule,
};
})
.filter((input) => input && Object.keys(input).length > 0)
.reduce((acc, curr) => {
return { ...acc, ...curr };
}, {});
})
.filter((provider) => provider && Object.keys(provider).length > 0)
.reduce((acc, item) => {
return { ...acc, ...item };
}, {});
const ProviderTool: FunctionComponent<ProviderToolProps> = ({
payload,
enabledProviders,
@ -172,6 +194,9 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
settings: staged,
hooks: {},
},
validate: {
settings: validation!,
},
});
const deletePayload = useCallback(() => {
@ -188,6 +213,12 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
const submit = useCallback(
(values: FormValues) => {
const result = form.validate();
if (result.hasErrors) {
return;
}
if (info && enabledProviders) {
const changes = { ...values.settings };
const hooks = values.hooks;
@ -204,7 +235,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
modals.closeAll();
}
},
[info, enabledProviders, modals, settingsKey],
[info, enabledProviders, modals, settingsKey, form],
);
const canSave = info !== null;
@ -249,43 +280,57 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
const label = value.name ?? capitalize(value.key);
const options = value.options ?? [];
const error = form.errors[`settings.settings-${itemKey}-${key}`] ? (
<MantineText c="red" component="span" size="xs">
{form.errors[`settings.settings-${itemKey}-${key}`]}
</MantineText>
) : null;
switch (value.type) {
case "text":
elements.push(
<Text
key={BuildKey(itemKey, key)}
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Text>,
<Fragment key={BuildKey(itemKey, key)}>
<Text
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Text>
{error}
</Fragment>,
);
return;
case "password":
elements.push(
<Password
key={BuildKey(itemKey, key)}
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Password>,
<Fragment key={BuildKey(itemKey, key)}>
<Password
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Password>
{error}
</Fragment>,
);
return;
case "switch":
elements.push(
<Check
key={key}
inline
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Check>,
<Fragment key={BuildKey(itemKey, key)}>
<Check
inline
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Check>
{error}
</Fragment>,
);
return;
case "select":
elements.push(
<GlobalSelector
key={key}
label={label}
settingKey={`settings-${itemKey}-${key}`}
options={options}
></GlobalSelector>,
<Fragment key={BuildKey(itemKey, key)}>
<GlobalSelector
label={label}
settingKey={`settings-${itemKey}-${key}`}
options={options}
></GlobalSelector>
{error}
</Fragment>,
);
return;
case "testbutton":
@ -295,11 +340,13 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
return;
case "chips":
elements.push(
<Chips
key={key}
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Chips>,
<Fragment key={BuildKey(itemKey, key)}>
<Chips
label={label}
settingKey={`settings-${itemKey}-${key}`}
></Chips>
{error}
</Fragment>,
);
return;
default:
@ -308,7 +355,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
});
return <Stack gap="xs">{elements}</Stack>;
}, [info]);
}, [info, form]);
return (
<SettingsProvider value={settings}>

@ -1,6 +1,7 @@
import { ReactText } from "react";
import { SelectorOption } from "@/components";
type Text = string | number;
type Input<T, N> = {
type: N;
key: string;
@ -8,15 +9,18 @@ type Input<T, N> = {
name?: string;
description?: string;
options?: SelectorOption<string>[];
validation?: {
rule: (value: string) => string | null;
};
};
type AvailableInput =
| Input<ReactText, "text">
| Input<Text, "text">
| Input<string, "password">
| Input<boolean, "switch">
| Input<string, "select">
| Input<string, "testbutton">
| Input<ReactText[], "chips">;
| Input<Text[], "chips">;
export interface ProviderInfo {
key: string;
@ -151,7 +155,8 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
{
key: "embeddedsubtitles",
name: "Embedded Subtitles",
description: "Embedded Subtitles from your Media Files",
description:
"This provider extracts embedded subtitles from your media files. You must disable 'Treat Embedded Subtitles as Downloaded' in Settings -> Subtitles for this provider to work.",
inputs: [
{
type: "chips",
@ -390,6 +395,12 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
{
type: "text",
key: "username",
validation: {
rule: (value: string) =>
/^.\S+@\S+$/.test(value)
? "Invalid Username. Do not use your e-mail."
: null,
},
},
{
type: "password",

@ -48,7 +48,7 @@ const SettingsRadarrView: FunctionComponent = () => {
</Section>
<Section header="Options">
<Slider
label="Minimum Score"
label="Minimum Score For Movies"
settingKey="settings-general-minimum_score_movie"
></Slider>
<Chips

@ -97,7 +97,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Weekly"}
>
<Selector
label="Day of The Week"
label="Day of Week"
settingKey="settings-sonarr-full_update_day"
options={dayOptions}
></Selector>
@ -107,7 +107,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Daily" || k === "Weekly"}
>
<Selector
label="Time of The Day"
label="Time of Day"
settingKey="settings-sonarr-full_update_hour"
options={timeOptions}
></Selector>
@ -134,7 +134,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Weekly"}
>
<Selector
label="Day of The Week"
label="Day of Week"
settingKey="settings-radarr-full_update_day"
options={dayOptions}
></Selector>
@ -144,7 +144,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Daily" || k === "Weekly"}
>
<Selector
label="Time of The Day"
label="Time of Day"
settingKey="settings-radarr-full_update_hour"
options={timeOptions}
></Selector>
@ -190,7 +190,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Weekly"}
>
<Selector
label="Day of The Week"
label="Day of Week"
settingKey="settings-backup-day"
options={dayOptions}
></Selector>
@ -200,7 +200,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
on={(k) => k === "Daily" || k === "Weekly"}
>
<Selector
label="Time of The Day"
label="Time of Day"
settingKey="settings-backup-hour"
options={timeOptions}
></Selector>

@ -50,7 +50,7 @@ const SettingsSonarrView: FunctionComponent = () => {
</Section>
<Section header="Options">
<Slider
label="Minimum Score"
label="Minimum Score For Episodes"
settingKey="settings-general-minimum_score"
></Slider>
<Chips

@ -129,7 +129,7 @@ const commandOptionElements: React.JSX.Element[] = commandOptions.map(
const SettingsSubtitlesView: FunctionComponent = () => {
return (
<Layout name="Subtitles">
<Section header="Basic Options">
<Section header="Subtitle File Options">
<Selector
label="Subtitle Folder"
options={folderOptions}
@ -156,14 +156,33 @@ const SettingsSubtitlesView: FunctionComponent = () => {
What file extension to use when saving hearing-impaired subtitles to
disk (e.g., video.en.sdh.srt).
</Message>
<Check
label="Encode Subtitles To UTF-8"
settingKey="settings-general-utf8_encode"
></Check>
<Message>
Re-encode downloaded subtitles to UTF-8. Should be left enabled in
most cases.
</Message>
<Check
label="Change Subtitle File Permission After Download (chmod)"
settingKey="settings-general-chmod_enabled"
></Check>
<CollapseBox indent settingKey="settings-general-chmod_enabled">
<Text placeholder="0777" settingKey="settings-general-chmod"></Text>
<Message>
Must be a 4 digit octal number. Only for non-Windows systems.
</Message>
</CollapseBox>
</Section>
<Section header="Embedded Subtitles">
<Section header="Embedded Subtitles Handling">
<Check
label="Use Embedded Subtitles"
label="Treat Embedded Subtitles as Downloaded"
settingKey="settings-general-use_embedded_subs"
></Check>
<Message>
Use embedded subtitles in media files when determining missing ones.
Treat embedded subtitles in media files as already downloaded when
determining missing ones.
</Message>
<CollapseBox indent settingKey="settings-general-use_embedded_subs">
<Selector
@ -179,21 +198,21 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-general-ignore_pgs_subs"
></Check>
<Message>
Ignores PGS Subtitles in Embedded Subtitles detection.
Ignore PGS Subtitles when detecting embedded subtitles.
</Message>
<Check
label="Ignore Embedded VobSub Subtitles"
settingKey="settings-general-ignore_vobsub_subs"
></Check>
<Message>
Ignores VobSub Subtitles in Embedded Subtitles detection.
Ignore VobSub Subtitles when detecting embedded subtitles.
</Message>
<Check
label="Ignore Embedded ASS Subtitles"
settingKey="settings-general-ignore_ass_subs"
></Check>
<Message>
Ignores ASS Subtitles in Embedded Subtitles detection.
Ignore ASS Subtitles when detecting embedded subtitles.
</Message>
<Check
label="Show Only Desired Languages"
@ -232,28 +251,6 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message>
</CollapseBox>
</Section>
<Section header="Encoding">
<Check
label="Encode Subtitles To UTF-8"
settingKey="settings-general-utf8_encode"
></Check>
<Message>
Re-encode downloaded subtitles to UTF-8. Should be left enabled in
most cases.
</Message>
</Section>
<Section header="Permissions">
<Check
label="Change Subtitle File Permission (chmod)"
settingKey="settings-general-chmod_enabled"
></Check>
<CollapseBox indent settingKey="settings-general-chmod_enabled">
<Text placeholder="0777" settingKey="settings-general-chmod"></Text>
<Message>
Must be a 4 digit octal number. Only for non-Windows systems.
</Message>
</CollapseBox>
</Section>
<Section header="Performance / Optimization">
<Check
label="Adaptive Searching"
@ -303,7 +300,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
results scores.
</Message>
</Section>
<Section header="Sub-Zero Modifications">
<Section header="Sub-Zero Subtitle Content Modifications">
<Message>
After downloaded, content of the subtitles will be modified based on
options selected below.
</Message>
<Check
label="Hearing Impaired"
settingOptions={{ onLoaded: SubzeroModification("remove_HI") }}
@ -372,7 +373,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
playback devices.
</Message>
</Section>
<Section header="Synchronization / Alignment">
<Section header="Audio Synchronization / Alignment">
<Check
label="Always use Audio Track as Reference for Syncing"
settingKey="settings-subsync-force_audio"
@ -382,7 +383,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
subtitle.
</Message>
<Check
label="No Fix Framerate"
label="Do Not Fix Framerate Mismatch"
settingKey="settings-subsync-no_fix_framerate"
></Check>
<Message>
@ -407,11 +408,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
The max allowed offset seconds for any subtitle segment.
</Message>
<Check
label="Automatic Subtitles Synchronization"
label="Automatic Subtitles Audio Synchronization"
settingKey="settings-subsync-use_subsync"
></Check>
<Message>
Enable automatic synchronization after downloading subtitles.
Enable automatic audio synchronization after downloading subtitles.
</Message>
<CollapseBox indent settingKey="settings-subsync-use_subsync">
<MultiSelector
@ -428,7 +429,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
the media file.
</Message>
<Check
label="Series Score Threshold"
label="Series Score Threshold For Audio Sync"
settingKey="settings-subsync-use_subsync_threshold"
></Check>
<CollapseBox
@ -446,7 +447,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message>
</CollapseBox>
<Check
label="Movies Score Threshold"
label="Movies Score Threshold For Audio Sync"
settingKey="settings-subsync-use_subsync_movie_threshold"
></Check>
<CollapseBox
@ -477,7 +478,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<CollapseBox indent settingKey="settings-general-use_postprocessing">
<Check
settingKey="settings-general-use_postprocessing_threshold"
label="Series Score Threshold"
label="Series Score Threshold For Post-Processing"
></Check>
<CollapseBox
indent
@ -495,7 +496,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</CollapseBox>
<Check
settingKey="settings-general-use_postprocessing_threshold_movie"
label="Movies Score Threshold"
label="Movies Score Threshold For Post-Processing"
></Check>
<CollapseBox
indent

@ -49,11 +49,20 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
if (item) {
badges.push(
<ItemBadge key="file-path" icon={faFolder} title="File Path">
<ItemBadge
key="file-path"
icon={faFolder}
title="File Path"
styles={{
root: { overflow: "unset" },
label: { overflow: "hidden" },
}}
>
<Tooltip
label={item.path}
multiline
style={{ overflowWrap: "anywhere" }}
events={{ hover: true, focus: false, touch: true }}
>
<span>{item.path}</span>
</Tooltip>

@ -151,6 +151,8 @@ declare namespace Item {
SeriesIdType & {
episodeFileCount: number;
episodeMissingCount: number;
ended: boolean;
lastAired: string;
seriesType: SonarrSeriesType;
tvdbId: number;
};

@ -0,0 +1,38 @@
"""empty message
Revision ID: 4274a5dfc4ad
Revises: 8baf97427327
Create Date: 2024-12-15 21:19:19.406290
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4274a5dfc4ad'
down_revision = '8baf97427327'
branch_labels = None
depends_on = None
bind = op.get_context().bind
insp = sa.inspect(bind)
def column_exists(table_name, column_name):
columns = insp.get_columns(table_name)
return any(c["name"] == column_name for c in columns)
def upgrade():
if not column_exists('table_shows', 'ended'):
with op.batch_alter_table('table_shows', schema=None) as batch_op:
batch_op.add_column(sa.Column('ended', sa.TEXT(), nullable=True))
if not column_exists('table_shows', 'lastAired'):
with op.batch_alter_table('table_shows', schema=None) as batch_op:
batch_op.add_column(sa.Column('lastAired', sa.TEXT(), nullable=True))
def downgrade():
pass
Loading…
Cancel
Save