diff --git a/bazarr/app/config.py b/bazarr/app/config.py index 2f5ff9a7f..0106aacf9 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -34,6 +34,9 @@ def validate_ip_address(ip_string): return False +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( @@ -99,14 +102,15 @@ 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, is_in=[6, 12, 24]), + 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), Validator('general.anti_captcha_provider', must_exist=True, default=None, is_type_of=(NoneType, str), is_in=[None, 'anti-captcha', 'death-by-captcha']), - Validator('general.wanted_search_frequency', must_exist=True, default=6, is_type_of=int, is_in=[6, 12, 24]), + Validator('general.wanted_search_frequency', must_exist=True, default=6, is_type_of=int, is_in=[6, 12, 24, ONE_HUNDRED_YEARS_IN_HOURS]), Validator('general.wanted_search_frequency_movie', must_exist=True, default=6, is_type_of=int, - is_in=[6, 12, 24]), + is_in=[6, 12, 24, ONE_HUNDRED_YEARS_IN_HOURS]), Validator('general.subzero_mods', must_exist=True, default='', is_type_of=str), Validator('general.dont_notify_manual_actions', must_exist=True, default=False, is_type_of=bool), Validator('general.hi_extension', must_exist=True, default='hi', is_type_of=str, is_in=['hi', 'cc', 'sdh']), @@ -151,9 +155,7 @@ validators = [ Validator('sonarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23), Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool), Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int, - is_in=[15, 60, 180, 360, 720, 1440]), - Validator('sonarr.episodes_sync', must_exist=True, default=60, is_type_of=int, - is_in=[15, 60, 180, 360, 720, 1440]), + is_in=[15, 60, 180, 360, 720, 1440, ONE_HUNDRED_YEARS_IN_MINUTES]), Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list), Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list), Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool), @@ -174,7 +176,7 @@ validators = [ Validator('radarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23), Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool), Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int, - is_in=[15, 60, 180, 360, 720, 1440]), + is_in=[15, 60, 180, 360, 720, 1440, ONE_HUNDRED_YEARS_IN_MINUTES]), Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list), Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool), Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool), @@ -409,8 +411,6 @@ str_keys = ['chmod'] # Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time if settings.sonarr.series_sync < 15: settings.sonarr.series_sync = 60 -if settings.sonarr.episodes_sync < 15: - settings.sonarr.episodes_sync = 60 if settings.radarr.movies_sync < 15: settings.radarr.movies_sync = 60 @@ -534,7 +534,7 @@ def save_settings(settings_items): if key in ['update_schedule', 'settings-general-use_sonarr', 'settings-general-use_radarr', 'settings-general-auto_update', 'settings-general-upgrade_subs', - 'settings-sonarr-series_sync', 'settings-sonarr-episodes_sync', 'settings-radarr-movies_sync', + 'settings-sonarr-series_sync', 'settings-radarr-movies_sync', 'settings-sonarr-full_update', 'settings-sonarr-full_update_day', 'settings-sonarr-full_update_hour', 'settings-radarr-full_update', 'settings-radarr-full_update_day', 'settings-radarr-full_update_hour', 'settings-general-wanted_search_frequency', 'settings-general-wanted_search_frequency_movie', diff --git a/bazarr/app/scheduler.py b/bazarr/app/scheduler.py index 81d368fe5..cbd68886e 100644 --- a/bazarr/app/scheduler.py +++ b/bazarr/app/scheduler.py @@ -36,6 +36,20 @@ if not args.no_update: else: from .check_update import check_releases +from dateutil.relativedelta import relativedelta + +NO_INTERVAL = "None" +NEVER_DATE = "Never" +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: @@ -106,7 +120,9 @@ class Scheduler: ('minute', 60), ('second', 1) ] - + if seconds > ONE_YEAR_IN_SECONDS: + # more than a year is None + return NO_INTERVAL strings = [] for period_name, period_seconds in periods: if seconds > period_seconds: @@ -118,14 +134,11 @@ class Scheduler: def get_time_from_cron(cron): year = str(cron[0]) - if year == "2100": - return "Never" - day = str(cron[4]) hour = str(cron[5]) if day == "*": - text = "everyday" + text = "every day" else: text = f"every {day_name[int(day)]}" @@ -136,12 +149,20 @@ class Scheduler: task_list = [] for job in self.aps_scheduler.get_jobs(): - next_run = 'Never' + next_run = NEVER_DATE if job.next_run_time: - next_run = pretty.date(job.next_run_time.replace(tzinfo=None)) - if isinstance(job.trigger, CronTrigger): - if job.next_run_time and str(job.trigger.__getstate__()['fields'][0]) != "2100": + if a_long_time_from_now(job): + # Never for IntervalTrigger jobs + next_run = NEVER_DATE + else: next_run = pretty.date(job.next_run_time.replace(tzinfo=None)) + if isinstance(job.trigger, CronTrigger): + if a_long_time_from_now(job): + # Never for CronTrigger jobs + next_run = NEVER_DATE + else: + if job.next_run_time: + next_run = pretty.date(job.next_run_time.replace(tzinfo=None)) if job.id in self.__running_tasks: running = True @@ -149,13 +170,21 @@ class Scheduler: running = False if isinstance(job.trigger, IntervalTrigger): - interval = f"every {get_time_from_interval(job.trigger.__getstate__()['interval'])}" + interval = get_time_from_interval(job.trigger.__getstate__()['interval']) + if interval != NO_INTERVAL: + interval = f"every {interval}" + # else: + # interval = "100 Year Interval" 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}) elif isinstance(job.trigger, CronTrigger): - task_list.append({'name': job.name, 'interval': get_time_from_cron(job.trigger.fields), - 'next_run_in': next_run, 'next_run_time': next_run, 'job_id': job.id, - 'job_running': running}) + if a_long_time_from_now(job): + interval = NO_INTERVAL + 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}) return task_list @@ -175,29 +204,23 @@ class Scheduler: def __cache_cleanup_task(self): self.aps_scheduler.add_job(cache_maintenance, IntervalTrigger(hours=24), max_instances=1, coalesce=True, - misfire_grace_time=15, id='cache_cleanup', name='Cache maintenance') + misfire_grace_time=15, id='cache_cleanup', name='Cache Maintenance') def __check_health_task(self): self.aps_scheduler.add_job(check_health, IntervalTrigger(hours=6), max_instances=1, coalesce=True, - misfire_grace_time=15, id='check_health', name='Check health') + misfire_grace_time=15, id='check_health', name='Check Health') def __automatic_backup(self): backup = settings.backup.frequency if backup == "Daily": - self.aps_scheduler.add_job( - backup_to_zip, CronTrigger(hour=settings.backup.hour), max_instances=1, coalesce=True, - misfire_grace_time=15, id='backup', name='Backup database and configuration file', - replace_existing=True) + trigger = CronTrigger(hour=settings.backup.hour) elif backup == "Weekly": - self.aps_scheduler.add_job( - backup_to_zip, CronTrigger(day_of_week=settings.backup.day, hour=settings.backup.hour), - max_instances=1, coalesce=True, misfire_grace_time=15, id='backup', - name='Backup database and configuration file', replace_existing=True) + trigger = CronTrigger(day_of_week=settings.backup.day, hour=settings.backup.hour) elif backup == "Manually": - try: - self.aps_scheduler.remove_job(job_id='backup') - except JobLookupError: - pass + 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) def __sonarr_full_update_task(self): if settings.general.use_sonarr: @@ -206,18 +229,18 @@ class Scheduler: self.aps_scheduler.add_job( update_all_episodes, CronTrigger(hour=settings.sonarr.full_update_hour), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_all_episodes', - name='Index all Episode Subtitles from disk', replace_existing=True) + name='Index All Episode Subtitles from Disk', replace_existing=True) elif full_update == "Weekly": self.aps_scheduler.add_job( update_all_episodes, CronTrigger(day_of_week=settings.sonarr.full_update_day, hour=settings.sonarr.full_update_hour), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_all_episodes', - name='Index all Episode Subtitles from disk', replace_existing=True) + name='Index All Episode Subtitles from Disk', replace_existing=True) elif full_update == "Manually": self.aps_scheduler.add_job( - update_all_episodes, CronTrigger(year='2100'), max_instances=1, coalesce=True, + update_all_episodes, CronTrigger(year=in_a_century()), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_all_episodes', - name='Index all Episode Subtitles from disk', replace_existing=True) + name='Index All Episode Subtitles from Disk', replace_existing=True) def __radarr_full_update_task(self): if settings.general.use_radarr: @@ -226,17 +249,17 @@ class Scheduler: self.aps_scheduler.add_job( update_all_movies, CronTrigger(hour=settings.radarr.full_update_hour), max_instances=1, coalesce=True, misfire_grace_time=15, - id='update_all_movies', name='Index all Movie Subtitles from disk', replace_existing=True) + id='update_all_movies', name='Index All Movie Subtitles from Disk', replace_existing=True) elif full_update == "Weekly": self.aps_scheduler.add_job( update_all_movies, CronTrigger(day_of_week=settings.radarr.full_update_day, hour=settings.radarr.full_update_hour), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_all_movies', - name='Index all Movie Subtitles from disk', replace_existing=True) + name='Index All Movie Subtitles from Disk', replace_existing=True) elif full_update == "Manually": self.aps_scheduler.add_job( - update_all_movies, CronTrigger(year='2100'), max_instances=1, coalesce=True, misfire_grace_time=15, - id='update_all_movies', name='Index all Movie Subtitles from disk', replace_existing=True) + update_all_movies, CronTrigger(year=in_a_century()), max_instances=1, coalesce=True, misfire_grace_time=15, + id='update_all_movies', name='Index All Movie Subtitles from Disk', replace_existing=True) def __update_bazarr_task(self): if not args.no_update and os.environ["BAZARR_VERSION"] != '': @@ -248,7 +271,7 @@ class Scheduler: misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True) else: self.aps_scheduler.add_job( - check_if_new_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', name=task_name, + check_if_new_update, CronTrigger(year=in_a_century()), hour=4, id='update_bazarr', name=task_name, replace_existing=True) self.aps_scheduler.add_job( check_releases, IntervalTrigger(hours=3), max_instances=1, coalesce=True, misfire_grace_time=15, @@ -269,13 +292,13 @@ class Scheduler: wanted_search_missing_subtitles_series, IntervalTrigger(hours=int(settings.general.wanted_search_frequency)), max_instances=1, coalesce=True, misfire_grace_time=15, id='wanted_search_missing_subtitles_series', replace_existing=True, - name='Search for wanted Series Subtitles') + name='Search for Missing Series Subtitles') if settings.general.use_radarr: self.aps_scheduler.add_job( wanted_search_missing_subtitles_movies, IntervalTrigger(hours=int(settings.general.wanted_search_frequency_movie)), max_instances=1, coalesce=True, misfire_grace_time=15, id='wanted_search_missing_subtitles_movies', - name='Search for wanted Movies Subtitles', replace_existing=True) + name='Search for Missing Movies Subtitles', replace_existing=True) def __upgrade_subtitles_task(self): if settings.general.upgrade_subs and \ @@ -283,7 +306,7 @@ class Scheduler: self.aps_scheduler.add_job( upgrade_subtitles, IntervalTrigger(hours=int(settings.general.upgrade_frequency)), max_instances=1, coalesce=True, misfire_grace_time=15, id='upgrade_subtitles', - name='Upgrade previously downloaded Subtitles', replace_existing=True) + name='Upgrade Previously Downloaded Subtitles', replace_existing=True) else: try: self.aps_scheduler.remove_job(job_id='upgrade_subtitles') @@ -293,6 +316,9 @@ class Scheduler: def __randomize_interval_task(self): for job in self.aps_scheduler.get_jobs(): if isinstance(job.trigger, IntervalTrigger): + # do not randomize the Never jobs + if job.trigger.interval.total_seconds() > ONE_YEAR_IN_SECONDS: + continue self.aps_scheduler.modify_job(job.id, next_run_time=datetime.now(tz=self.timezone) + timedelta(seconds=randrange( diff --git a/frontend/src/pages/Settings/Scheduler/index.tsx b/frontend/src/pages/Settings/Scheduler/index.tsx index e2a236197..3bd6da91a 100644 --- a/frontend/src/pages/Settings/Scheduler/index.tsx +++ b/frontend/src/pages/Settings/Scheduler/index.tsx @@ -12,7 +12,6 @@ import { backupOptions, dayOptions, diskUpdateOptions, - episodesSyncOptions, moviesSyncOptions, seriesSyncOptions, upgradeOptions, @@ -32,26 +31,19 @@ const SettingsSchedulerView: FunctionComponent = () => {
- - -
@@ -88,7 +80,7 @@ const SettingsSchedulerView: FunctionComponent = () => { @@ -144,7 +136,7 @@ const SettingsSchedulerView: FunctionComponent = () => {
diff --git a/frontend/src/pages/Settings/Scheduler/options.ts b/frontend/src/pages/Settings/Scheduler/options.ts index ab193774b..c2eadcbae 100644 --- a/frontend/src/pages/Settings/Scheduler/options.ts +++ b/frontend/src/pages/Settings/Scheduler/options.ts @@ -1,6 +1,7 @@ import { SelectorOption } from "@/components"; export const seriesSyncOptions: SelectorOption[] = [ + { label: "Manually", value: 52560000 }, { label: "15 Minutes", value: 15 }, { label: "1 Hour", value: 60 }, { label: "3 Hours", value: 180 }, @@ -9,8 +10,6 @@ export const seriesSyncOptions: SelectorOption[] = [ { label: "24 Hours", value: 1440 }, ]; -export const episodesSyncOptions = seriesSyncOptions; - export const moviesSyncOptions = seriesSyncOptions; export const diskUpdateOptions: SelectorOption[] = [ @@ -32,6 +31,7 @@ export const dayOptions: SelectorOption[] = [ ]; export const upgradeOptions: SelectorOption[] = [ + { label: "Manually", value: 876000 }, { label: "6 Hours", value: 6 }, { label: "12 Hours", value: 12 }, { label: "24 Hours", value: 24 }, diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx index 9457abd41..d0efa8e91 100644 --- a/frontend/src/pages/Settings/components/forms.tsx +++ b/frontend/src/pages/Settings/components/forms.tsx @@ -146,6 +146,7 @@ export const Slider: FunctionComponent = (props) => { diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts index 55112c873..89cb42a6d 100644 --- a/frontend/src/types/settings.d.ts +++ b/frontend/src/types/settings.d.ts @@ -144,7 +144,6 @@ declare namespace Settings { full_update_hour: number; only_monitored: boolean; series_sync: number; - episodes_sync: number; excluded_tags: string[]; excluded_series_types: SonarrSeriesType[]; }