Merge branch 'shutdown-restart-refactor' of https://github.com/JaiZed/bazarr into shutdown-restart-refactor

This commit is contained in:
JayZed 2024-03-02 16:22:36 -05:00
commit bfa3a008aa
19 changed files with 195 additions and 101 deletions

View File

@ -44,12 +44,12 @@ class SystemLogs(Resource):
if len(include) > 0:
try:
include_compiled = re.compile(include, flags)
except:
except Exception:
include_compiled = None
if len(exclude) > 0:
try:
exclude_compiled = re.compile(exclude, flags)
except:
except Exception:
exclude_compiled = None
elif ignore_case:
include = include.casefold()

View File

@ -40,6 +40,7 @@ def validate_ip_address(ip_string):
ONE_HUNDRED_YEARS_IN_MINUTES = 52560000
ONE_HUNDRED_YEARS_IN_HOURS = 876000
class Validator(OriginalValidator):
# Give the ability to personalize messages sent by the original dynasync Validator class.
default_messages = MappingProxyType(
@ -105,7 +106,7 @@ validators = [
Validator('general.subfolder', must_exist=True, default='current', is_type_of=str),
Validator('general.subfolder_custom', must_exist=True, default='', is_type_of=str),
Validator('general.upgrade_subs', must_exist=True, default=True, is_type_of=bool),
Validator('general.upgrade_frequency', must_exist=True, default=12, is_type_of=int,
Validator('general.upgrade_frequency', must_exist=True, default=12, is_type_of=int,
is_in=[6, 12, 24, ONE_HUNDRED_YEARS_IN_HOURS]),
Validator('general.days_to_upgrade_subs', must_exist=True, default=7, is_type_of=int, gte=0, lte=30),
Validator('general.upgrade_manual', must_exist=True, default=True, is_type_of=bool),
@ -487,25 +488,27 @@ def get_settings():
settings_to_return[k].update({subk: subv})
return settings_to_return
def validate_log_regex():
# handle bug in dynaconf that changes strings to numbers, so change them back to str
if not isinstance(settings.log.include_filter, str):
settings.log.include_filter = str(settings.log.include_filter)
settings.log.include_filter = str(settings.log.include_filter)
if not isinstance(settings.log.exclude_filter, str):
settings.log.exclude_filter = str(settings.log.exclude_filter)
settings.log.exclude_filter = str(settings.log.exclude_filter)
if (settings.log.use_regex):
if settings.log.use_regex:
# compile any regular expressions specified to see if they are valid
# if invalid, tell the user which one
try:
re.compile(settings.log.include_filter)
except:
except Exception:
raise ValidationError(f"Include filter: invalid regular expression: {settings.log.include_filter}")
try:
re.compile(settings.log.exclude_filter)
except:
except Exception:
raise ValidationError(f"Exclude filter: invalid regular expression: {settings.log.exclude_filter}")
def save_settings(settings_items):
configure_debug = False
configure_captcha = False
@ -522,8 +525,7 @@ def save_settings(settings_items):
undefined_subtitles_track_default_changed = False
audio_tracks_parsing_changed = False
reset_providers = False
check_log_regex = False
# Subzero Mods
update_subzero = False
subzero_mods = get_array_from(settings.general.subzero_mods)

View File

@ -323,7 +323,7 @@ def get_providers_auth():
'response': settings.whisperai.response,
'timeout': settings.whisperai.timeout,
'ffmpeg_path': _FFMPEG_BINARY,
'loglevel': settings.whisperai.loglevel,
'loglevel': settings.whisperai.loglevel,
}
}

View File

@ -56,10 +56,10 @@ class NoExceptionFormatter(logging.Formatter):
def formatException(self, record):
return ''
class UnwantedWaitressMessageFilter(logging.Filter):
def filter(self, record):
if settings.general.debug == True:
if settings.general.debug is True:
# no filtering in debug mode
return True
@ -76,8 +76,8 @@ class UnwantedWaitressMessageFilter(logging.Filter):
"Exception when servicing %r",
[],
]
wanted = True
wanted = True
listLength = len(unwantedMessages)
for i in range(0, listLength, 2):
if record.msg == unwantedMessages[i]:
@ -86,7 +86,7 @@ class UnwantedWaitressMessageFilter(logging.Filter):
if len(unwantedMessages[i+1]) == 0 or str(exceptionTuple[1]) in unwantedMessages[i+1]:
wanted = False
break
return wanted
@ -163,7 +163,7 @@ def configure_logging(debug=False):
logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR)
logging.getLogger("waitress").setLevel(logging.ERROR)
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
logging.getLogger("knowit").setLevel(logging.CRITICAL)
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
logging.getLogger("guessit").setLevel(logging.WARNING)

View File

@ -8,7 +8,6 @@ from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.events import EVENT_JOB_SUBMITTED, EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from apscheduler.jobstores.base import JobLookupError
from datetime import datetime, timedelta
from calendar import day_name
from random import randrange
@ -40,17 +39,20 @@ from dateutil.relativedelta import relativedelta
NO_INTERVAL = "None"
NEVER_DATE = "Never"
ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
def a_long_time_from_now(job):
# currently defined as more than a year from now
delta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
return delta.total_seconds() > ONE_YEAR_IN_SECONDS
def in_a_century():
century = datetime.now() + relativedelta(years=100)
return century.year
class Scheduler:
def __init__(self):
@ -133,7 +135,6 @@ class Scheduler:
return ", ".join(strings)
def get_time_from_cron(cron):
year = str(cron[0])
day = str(cron[4])
hour = str(cron[5])
@ -183,8 +184,8 @@ class Scheduler:
else:
interval = get_time_from_cron(job.trigger.fields)
task_list.append({'name': job.name, 'interval': interval,
'next_run_in': next_run, 'next_run_time': next_run, 'job_id': job.id,
'job_running': running})
'next_run_in': next_run, 'next_run_time': next_run, 'job_id': job.id,
'job_running': running})
return task_list
@ -218,9 +219,8 @@ class Scheduler:
trigger = CronTrigger(day_of_week=settings.backup.day, hour=settings.backup.hour)
elif backup == "Manually":
trigger = CronTrigger(year=in_a_century())
self.aps_scheduler.add_job(backup_to_zip, trigger,
max_instances=1, coalesce=True, misfire_grace_time=15, id='backup',
name='Backup Database and Configuration File', replace_existing=True)
self.aps_scheduler.add_job(backup_to_zip, trigger, max_instances=1, coalesce=True, misfire_grace_time=15,
id='backup', name='Backup Database and Configuration File', replace_existing=True)
def __sonarr_full_update_task(self):
if settings.general.use_sonarr:

View File

@ -1,6 +1,7 @@
# coding=utf-8
import os
import io
from threading import Thread
@ -20,6 +21,7 @@ from app.get_args import args # noqa E402
from app.check_update import apply_update, check_releases, check_if_new_update # noqa E402
from app.config import settings, configure_proxy_func, base_url # noqa E402
from init import * # noqa E402
import logging # noqa E402
# Install downloaded update
if bazarr_version != '':

View File

@ -21,10 +21,13 @@ from .parser import movieParser
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_MOVIES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def update_all_movies():
movies_full_scan_subtitles()
logging.info('BAZARR All existing movie subtitles indexed from disk.')
@ -63,6 +66,7 @@ def get_movie_monitored_status(movie_id):
else:
return bool_map[existing_movie_monitored[0]]
# Insert new movies in DB
def add_movie(added_movie, send_event):
try:
@ -158,7 +162,7 @@ def update_movies(send_event=True):
# Only movies that Radarr says have files downloaded will be kept up to date in the DB
if movie['hasFile'] is True:
if 'movieFile' in movie:
if sync_monitored:
if sync_monitored:
if get_movie_monitored_status(movie['tmdbId']) != movie['monitored']:
# monitored status is not the same as our DB
trace(f"{i}: (Monitor Status Mismatch) {movie['title']}")
@ -187,8 +191,8 @@ def update_movies(send_event=True):
add_movie(parsed_movie, send_event)
movies_added.append(parsed_movie['title'])
else:
trace(f"{i}: (Skipped File Missing) {movie['title']}")
files_missing += 1
trace(f"{i}: (Skipped File Missing) {movie['title']}")
files_missing += 1
if send_event:
hide_progress(id='movies_progress')
@ -196,10 +200,12 @@ def update_movies(send_event=True):
trace(f"Skipped {files_missing} file missing movies out of {i}")
if sync_monitored:
trace(f"Skipped {skipped_count} unmonitored movies out of {i}")
trace(f"Processed {i - files_missing - skipped_count} movies out of {i} " +
f"with {len(movies_added)} added, {len(movies_updated)} updated and {len(movies_deleted)} deleted")
trace(f"Processed {i - files_missing - skipped_count} movies out of {i} "
f"with {len(movies_added)} added, {len(movies_updated)} updated and "
f"{len(movies_deleted)} deleted")
else:
trace(f"Processed {i - files_missing} movies out of {i} with {len(movies_added)} added and {len(movies_updated)} updated")
trace(f"Processed {i - files_missing} movies out of {i} with {len(movies_added)} added and "
f"{len(movies_updated)} updated")
logging.debug('BAZARR All movies synced from Radarr into database.')

View File

@ -115,27 +115,27 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
parsed_movie = {'radarrId': int(movie["id"]),
'title': movie["title"],
'path': os.path.join(movie["path"], movie['movieFile']['relativePath']),
'tmdbId': str(movie["tmdbId"]),
'poster': poster,
'fanart': fanart,
'audio_language': str(audio_language),
'sceneName': sceneName,
'monitored': str(bool(movie['monitored'])),
'year': str(movie['year']),
'sortTitle': movie['sortTitle'],
'alternativeTitles': alternativeTitles,
'format': format,
'resolution': resolution,
'video_codec': videoCodec,
'audio_codec': audioCodec,
'overview': overview,
'imdbId': imdbId,
'movie_file_id': int(movie['movieFile']['id']),
'tags': str(tags),
'file_size': movie['movieFile']['size']}
'title': movie["title"],
'path': os.path.join(movie["path"], movie['movieFile']['relativePath']),
'tmdbId': str(movie["tmdbId"]),
'poster': poster,
'fanart': fanart,
'audio_language': str(audio_language),
'sceneName': sceneName,
'monitored': str(bool(movie['monitored'])),
'year': str(movie['year']),
'sortTitle': movie['sortTitle'],
'alternativeTitles': alternativeTitles,
'format': format,
'resolution': resolution,
'video_codec': videoCodec,
'audio_codec': audioCodec,
'overview': overview,
'imdbId': imdbId,
'movie_file_id': int(movie['movieFile']['id']),
'tags': str(tags),
'file_size': movie['movieFile']['size']}
if action == 'insert':
parsed_movie['subtitles'] = '[]'
parsed_movie['profileId'] = movie_default_profile

View File

@ -12,7 +12,7 @@ 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 sonarr.info import get_sonarr_info
from .parser import episodeParser
from .utils import get_episodes_from_sonarr_api, get_episodesFiles_from_sonarr_api
@ -21,10 +21,13 @@ from .utils import get_episodes_from_sonarr_api, get_episodesFiles_from_sonarr_a
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)
@ -32,7 +35,8 @@ def get_episodes_monitored_table(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.')
@ -74,7 +78,6 @@ def sync_episodes(series_id, send_event=True):
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)
@ -122,7 +125,7 @@ def sync_episodes(series_id, send_event=True):
episodes_to_add.append(episodeParser(episode))
else:
return
if sync_monitored:
# try to avoid unnecessary database calls
if settings.general.debug:
@ -175,7 +178,6 @@ def sync_episodes(series_id, send_event=True):
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

View File

@ -5,7 +5,6 @@ import logging
from sqlalchemy.exc import IntegrityError
from app.config import settings
from sonarr.info import url_sonarr
from subtitles.indexer.series import list_missing_subtitles
from sonarr.rootfolder import check_sonarr_rootfolder
from app.database import TableShows, TableLanguagesProfiles, database, insert, update, delete, select
@ -20,10 +19,13 @@ from .utils import get_profile_list, get_tags, get_series_from_sonarr_api
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_SERIES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def get_series_monitored_table():
series_monitored = database.execute(
select(TableShows.tvdbId, TableShows.monitored))\
@ -31,6 +33,7 @@ def get_series_monitored_table():
series_dict = dict((x, y) for x, y in series_monitored)
return series_dict
def update_series(send_event=True):
check_sonarr_rootfolder()
apikey_sonarr = settings.sonarr.apikey
@ -74,7 +77,7 @@ def update_series(send_event=True):
series_monitored = get_series_monitored_table()
skipped_count = 0
trace(f"Starting sync for {series_count} shows")
for i, show in enumerate(series):
if send_event:
show_progress(id='series_progress',
@ -152,7 +155,7 @@ def update_series(send_event=True):
removed_series = list(set(current_shows_db) - set(current_shows_sonarr))
for series in removed_series:
# try to avoid unnecessary database calls
# try to avoid unnecessary database calls
if settings.general.debug:
series_title = database.execute(select(TableShows.title).where(TableShows.sonarrSeriesId == series)).first()[0]
trace(f"Deleting {series_title}")

View File

@ -264,7 +264,10 @@ def list_missing_subtitles_movies(no=None, send_event=True):
event_stream(type='badges')
def movies_full_scan_subtitles(use_cache=settings.radarr.use_ffprobe_cache):
def movies_full_scan_subtitles(use_cache=None):
if use_cache is None:
use_cache = settings.radarr.use_ffprobe_cache
movies = database.execute(
select(TableMovies.path))\
.all()

View File

@ -266,7 +266,10 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
event_stream(type='badges')
def series_full_scan_subtitles(use_cache=settings.sonarr.use_ffprobe_cache):
def series_full_scan_subtitles(use_cache=None):
if use_cache is None:
use_cache = settings.sonarr.use_ffprobe_cache
episodes = database.execute(
select(TableEpisodes.path))\
.all()

View File

@ -26,8 +26,19 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr
if not use_subsync_threshold or (use_subsync_threshold and percent_score < float(subsync_threshold)):
subsync = SubSyncer()
subsync.sync(video_path=video_path, srt_path=srt_path, srt_lang=srt_lang,
sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, radarr_id=radarr_id)
sync_kwargs = {
'video_path': video_path,
'srt_path': srt_path,
'srt_lang': srt_lang,
'max_offset_seconds': str(settings.subsync.max_offset_seconds),
'no_fix_framerate': settings.subsync.no_fix_framerate,
'gss': settings.subsync.gss,
'reference': None, # means choose automatically within video file
'sonarr_series_id': sonarr_series_id,
'sonarr_episode_id': sonarr_episode_id,
'radarr_id': radarr_id,
}
subsync.sync(**sync_kwargs)
del subsync
gc.collect()
return True

View File

@ -30,9 +30,9 @@ class SubSyncer:
self.vad = 'subs_then_webrtc'
self.log_dir_path = os.path.join(args.config_dir, 'log')
def sync(self, video_path, srt_path, srt_lang, sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None,
reference=None, max_offset_seconds=str(settings.subsync.max_offset_seconds),
no_fix_framerate=settings.subsync.no_fix_framerate, gss=settings.subsync.gss):
def sync(self, video_path, srt_path, srt_lang,
max_offset_seconds, no_fix_framerate, gss, reference=None,
sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None):
self.reference = video_path
self.srtin = srt_path
if self.srtin.casefold().endswith('.ass'):

View File

@ -16,7 +16,7 @@ def _escape(in_str):
def pp_replace(pp_command, episode, subtitles, language, language_code2, language_code3, episode_language,
episode_language_code2, episode_language_code3, score, subtitle_id, provider, uploader,
episode_language_code2, episode_language_code3, score, subtitle_id, provider, uploader,
release_info, series_id, episode_id):
pp_command = re.sub(r'[\'"]?{{directory}}[\'"]?', _escape(os.path.dirname(episode)), pp_command)
pp_command = re.sub(r'[\'"]?{{episode}}[\'"]?', _escape(episode), pp_command)

View File

@ -270,7 +270,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
if not os.path.exists(file):
logging.error(f'Video file "{file}" cannot be found for analysis')
return None
# if we have ffprobe available
if ffprobe_path:
try:

View File

@ -5,6 +5,7 @@
<base href="{{baseUrl}}" />
<meta charset="utf-8" />
<link rel="icon" type="image/x-icon" href="./images/favicon.ico" />
<link rel="manifest" href="manifest.json" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"

View File

@ -0,0 +1,26 @@
{
"name": "Bazarr",
"short_name": "Bazarr",
"description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements.",
"start_url": "/",
"display": "standalone",
"theme_color": "#be4bdb",
"background_color": "#ffffff",
"icons": [
{
"src": "/images/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/images/mstile-150x150.png",
"sizes": "150x150",
"type": "image/png"
}
]
}

View File

@ -1,4 +1,4 @@
import { Code, Table } from "@mantine/core";
import { Code, Space, Table } from "@mantine/core";
import { FunctionComponent } from "react";
import {
Check,
@ -134,7 +134,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-general-subfolder"
></Selector>
<Message>
Choose the folder you wish to store/read the subtitles
Choose the folder you wish to store/read the subtitles.
</Message>
<CollapseBox
settingKey="settings-general-subfolder"
@ -171,7 +171,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
}}
options={embeddedSubtitlesParserOption}
></Selector>
<Message>Embedded subtitles video parser</Message>
<Message>Embedded Subtitles video parser.</Message>
<Check
label="Ignore Embedded PGS Subtitles"
settingKey="settings-general-ignore_pgs_subs"
@ -198,7 +198,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-general-embedded_subs_show_desired"
></Check>
<Message>
Hide embedded subtitles for languages that are not currently
Hide Embedded Subtitles for languages that are not currently
desired.
</Message>
</CollapseBox>
@ -218,7 +218,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
mb="lg"
></Slider>
<Message>
Number of days to go back in history to upgrade subtitles
Number of days to go back in history to upgrade subtitles.
</Message>
<Check
label="Upgrade Manually Downloaded or Translated Subtitles"
@ -232,22 +232,24 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Section>
<Section header="Encoding">
<Check
label="Encode Subtitles To UTF8"
label="Encode Subtitles To UTF-8"
settingKey="settings-general-utf8_encode"
></Check>
<Message>
Re-encode downloaded Subtitles to UTF8. Should be left enabled in most
case.
Re-encode downloaded subtitles to UTF-8. Should be left enabled in
most cases.
</Message>
</Section>
<Section header="Permissions">
<Check
label="Change file permission (chmod)"
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 4 digit octal</Message>
<Message>
Must be a 4 digit octal number. Only for non-Windows systems.
</Message>
</CollapseBox>
</Section>
<Section header="Performance / Optimization">
@ -266,9 +268,9 @@ const SettingsSubtitlesView: FunctionComponent = () => {
options={adaptiveSearchingDelayOption}
></Selector>
<Message>
The delay from the first search to adaptive searching applying.
During this window Bazarr will continue to search for subtitles,
even if they have been searched for recently.
The delay from the first search to adaptive searching taking effect.
During this time window Bazarr will continue to search for
subtitles, even if they have been searched for recently.
</Message>
<Selector
settingKey="settings-general-adaptive_searching_delta"
@ -286,20 +288,20 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-general-multithreading"
></Check>
<Message>
Search multiple providers at once (Don't choose this on low powered
devices)
Search multiple providers at once. (Don't choose this on low powered
devices).
</Message>
<Check
label="Skip video file hash calculation"
settingKey="settings-general-skip_hashing"
></Check>
<Message>
Skip video file hashing during search process to prevent sleeping hard
disk drive from waking-up. On the other hand, this may decrease your
search results scores.
Skip video file hashing during search process to prevent a sleeping
hard disk drive from waking up. However, this may decrease your search
results scores.
</Message>
</Section>
<Section header="Subzero Modifications">
<Section header="Sub-Zero Modifications">
<Check
label="Hearing Impaired"
settingOptions={{ onLoaded: SubzeroModification("remove_HI") }}
@ -368,14 +370,14 @@ const SettingsSubtitlesView: FunctionComponent = () => {
playback devices.
</Message>
</Section>
<Section header="Synchronizarion / Alignement">
<Section header="Synchronization / Alignment">
<Check
label="Always use Audio Track as Reference for Syncing"
settingKey="settings-subsync-force_audio"
></Check>
<Message>
Use the audio track as reference for syncing, instead of using the
embedded subtitle.
Use the audio track as reference for syncing, instead of the embedded
subtitle.
</Message>
<Check
label="No Fix Framerate"
@ -386,7 +388,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
between reference and subtitles.
</Message>
<Check
label="Gold-Section Search"
label="Golden-Section Search"
settingKey="settings-subsync-gss"
></Check>
<Message>
@ -394,7 +396,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
framerate ratio between video and subtitles.
</Message>
<Selector
label="Max offset seconds"
label="Max Offset Seconds"
options={syncMaxOffsetSecondsOptions}
settingKey="settings-subsync-max_offset_seconds"
defaultValue={60}
@ -407,8 +409,8 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-subsync-use_subsync"
></Check>
<Message>
Enable the automatic subtitles synchronization after downloading a
subtitles.
Enable automatic subtitles synchronization after downloading a
subtitle.
</Message>
<CollapseBox indent settingKey="settings-subsync-use_subsync">
<MultiSelector
@ -428,40 +430,73 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Series Score Threshold"
settingKey="settings-subsync-use_subsync_threshold"
></Check>
<CollapseBox settingKey="settings-subsync-use_subsync_threshold">
<CollapseBox
indent
settingKey="settings-subsync-use_subsync_threshold"
>
<Slider settingKey="settings-subsync-subsync_threshold"></Slider>
<Space />
<Message>
Only series subtitles with scores <b>below</b> this value will be
automatically synchronized.
</Message>
</CollapseBox>
<Check
label="Movies Score Threshold"
settingKey="settings-subsync-use_subsync_movie_threshold"
></Check>
<CollapseBox settingKey="settings-subsync-use_subsync_movie_threshold">
<CollapseBox
indent
settingKey="settings-subsync-use_subsync_movie_threshold"
>
<Slider settingKey="settings-subsync-subsync_movie_threshold"></Slider>
<Space />
<Message>
Only movie subtitles with scores <b>below</b> this value will be
automatically synchronized.
</Message>
</CollapseBox>
</CollapseBox>
</Section>
<Section header="Custom post-processing">
<Section header="Custom Post-Processing">
<Check
settingKey="settings-general-use_postprocessing"
label="Custom Post-Processing"
></Check>
<Message>
Enable the post-processing execution after downloading a subtitles.
Enable automatic execution of the post-processing command specified
below after downloading a subtitle.
</Message>
<CollapseBox indent settingKey="settings-general-use_postprocessing">
<Check
settingKey="settings-general-use_postprocessing_threshold"
label="Series Score Threshold"
></Check>
<CollapseBox settingKey="settings-general-use_postprocessing_threshold">
<CollapseBox
indent
settingKey="settings-general-use_postprocessing_threshold"
>
<Slider settingKey="settings-general-postprocessing_threshold"></Slider>
<Space />
<Message>
Only series subtitles with scores <b>below</b> this value will be
automatically post-processed.
</Message>
</CollapseBox>
<Check
settingKey="settings-general-use_postprocessing_threshold_movie"
label="Movies Score Threshold"
></Check>
<CollapseBox settingKey="settings-general-use_postprocessing_threshold_movie">
<CollapseBox
indent
settingKey="settings-general-use_postprocessing_threshold_movie"
>
<Slider settingKey="settings-general-postprocessing_threshold_movie"></Slider>
<Space />
<Message>
Only movie subtitles with scores <b>below</b> this value will be
automatically post-processed.
</Message>
</CollapseBox>
<Text
label="Command"