diff --git a/README.md b/README.md index b210dae..a413644 100644 --- a/README.md +++ b/README.md @@ -216,26 +216,27 @@ debug: false # Enable debug logs ### Environment Variable Summary -| Environment Variable | Default Value | Description | -| ------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `PLEX_URL` | *(none)* | URL to your Plex server. Replace `http://plex:32400` with your actual Plex server URL. | -| `PLEX_TOKEN` | *(none)* | Plex authentication token. | -| `PLEX_CONNECTION_MAX_RETRIES` | `300` | Maximum number of connection attempts when connecting to the Plex server. | -| `PLEX_CONNECTION_RETRY_DELAY` | `5` | Delay in seconds between connection retry attempts. | -| `UPDATE_LEVEL` | `show` | Determines whether the update applies to the entire show or just the current season. Accepted values: `show`, `season`. | -| `UPDATE_STRATEGY` | `next` | Chooses whether to update all episodes or only the next one. Accepted values: `all`, `next`. | -| `TRIGGER_ON_PLAY` | `true` | If set to true, playing an episode triggers a language update. | -| `TRIGGER_ON_SCAN` | `true` | If set to true, scanning for new files triggers a language update. | -| `TRIGGER_ON_ACTIVITY` | `false` | If set to true, browsing the Plex library triggers a language update. | -| `REFRESH_LIBRARY_ON_SCAN` | `true` | Refreshes the cached library when the Plex server scans its library. | -| `IGNORE_LABELS` | `PAL_IGNORE` | Comma-separated list of Plex labels. Shows with these labels will be ignored. | -| `IGNORE_LIBRARIES` | *(none)* | Comma-separated list of library names that PAL will ignore when updating subtitle/audio languages. | -| `IGNORE_FILEPATTERNS` | *(none)* | Comma-separated list of regex patterns matched against media file paths. Episodes with matching files are ignored (e.g. `.*coming\.soon.*,.*trailer.*`). | -| `SCHEDULER_ENABLE` | `true` | Enables or disables the scheduler feature. | -| `SCHEDULER_SCHEDULE_TIME` | `02:00` | Time (in `HH:MM` format) when the scheduler starts its task. | -| `NOTIFICATIONS_ENABLE` | `false` | Enables or disables notifications. | -| `NOTIFICATIONS_APPRISE_CONFIGS` | `[]` | JSON array of Apprise notification configurations. See Apprise docs for more information: https://github.com/caronc/apprise. | -| `DEBUG` | `false` | Enables debug mode for verbose logging. | +| Environment Variable | Default Value | Description | +| ------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PLEX_URL` | *(none)* | URL to your Plex server. Replace `http://plex:32400` with your actual Plex server URL. | +| `PLEX_TOKEN` | *(none)* | Plex authentication token. | +| `PLEX_CONNECTION_MAX_RETRIES` | `300` | Maximum number of connection attempts when connecting to the Plex server. | +| `PLEX_CONNECTION_RETRY_DELAY` | `5` | Delay in seconds between connection retry attempts. | +| `UPDATE_LEVEL` | `show` | Determines whether the update applies to the entire show or just the current season. Accepted values: `show`, `season`. | +| `UPDATE_STRATEGY` | `next` | Chooses whether to update all episodes or only the next one. Accepted values: `all`, `next`. | +| `TRIGGER_ON_PLAY` | `true` | If set to true, playing an episode triggers a language update. | +| `TRIGGER_ON_SCAN` | `true` | If set to true, scanning for new files triggers a language update. | +| `TRIGGER_ON_ACTIVITY` | `false` | If set to true, browsing the Plex library triggers a language update. | +| `REFRESH_LIBRARY_ON_SCAN` | `true` | Refreshes the cached library when the Plex server scans its library. | +| `IGNORE_LABELS` | `PAL_IGNORE` | Comma-separated list of Plex labels. Shows with these labels will be ignored. | +| `IGNORE_LIBRARIES` | *(none)* | Comma-separated list of library names that PAL will ignore when updating subtitle/audio languages. | +| `IGNORE_FILEPATTERNS` | *(none)* | Comma-separated list of regex patterns matched against media file paths. Episodes with matching files are ignored (e.g. `.*coming\.soon.*,.*trailer.*`). | +| `SCHEDULER_ENABLE` | `true` | Enables or disables the scheduler feature. | +| `SCHEDULER_SCHEDULE_TIME` | `02:00` | Time (in `HH:MM` format) when the scheduler starts its task. | +| `SCHEDULER_SCHEDULE_DAYS` | [] | Comma-separated list of days when the scheduler should run. Leave empty to run every day. Accepted values: monday, tuesday, wednesday, thursday, friday, saturday, sunday. | +| `NOTIFICATIONS_ENABLE` | `false` | Enables or disables notifications. | +| `NOTIFICATIONS_APPRISE_CONFIGS` | `[]` | JSON array of Apprise notification configurations. See Apprise docs for more information: https://github.com/caronc/apprise. | +| `DEBUG` | `false` | Enables debug mode for verbose logging. | > [!NOTE] > diff --git a/config.example.yaml b/config.example.yaml index 7c85347..963195d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -59,9 +59,22 @@ plexautolanguages: # Whether of not to enable the scheduler, defaults to 'true' # The scheduler will perform a deeper analysis of all recently played TV Shows enable: true - # The time at which the scheduler start its task with the format 'HH:MM', defaults to '02:00' + + # The time at which the scheduler starts its task with the format 'HH:MM', defaults to '02:00' schedule_time: "02:00" + # Days on which the scheduler should run. + # Leave empty to run every day. + # Accepted values: + # - monday + # - tuesday + # - wednesday + # - thursday + # - friday + # - saturday + # - sunday + schedule_days: [] + notifications: # Whether or not to enable the notifications through Apprise, defaults to 'false' # A notification is sent whenever a language change is performed diff --git a/config/default.yaml b/config/default.yaml index bbac202..e6d66b2 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -21,6 +21,7 @@ plexautolanguages: scheduler: enable: true schedule_time: "02:00" + schedule_days: [] notifications: enable: false diff --git a/main.py b/main.py index 309c947..9566a0e 100644 --- a/main.py +++ b/main.py @@ -86,7 +86,9 @@ def __init__(self, user_config_path: str): self.scheduler = None if self.config.get("scheduler.enable"): self.scheduler = Scheduler( - self.config.get("scheduler.schedule_time"), self.scheduler_callback + self.config.get("scheduler.schedule_time"), + self.scheduler_callback, + self.config.get("scheduler.schedule_days") ) # Placeholder for Plex server interactions. diff --git a/plex_auto_languages/utils/configuration.py b/plex_auto_languages/utils/configuration.py index 7b96f66..784c5ee 100644 --- a/plex_auto_languages/utils/configuration.py +++ b/plex_auto_languages/utils/configuration.py @@ -255,6 +255,16 @@ def _postprocess_config(self): ignore_filepatterns_config = self.get("ignore_filepatterns") if isinstance(ignore_filepatterns_config, str): self._config["ignore_filepatterns"] = ignore_filepatterns_config.split(",") + + schedule_days_config = self.get("scheduler.schedule_days") + if schedule_days_config is None: + self._config["scheduler"]["schedule_days"] = [] + elif isinstance(schedule_days_config, str): + self._config["scheduler"]["schedule_days"] = [ + day.strip() + for day in schedule_days_config.split(",") + if day.strip() + ] def _validate_config(self): """ @@ -269,36 +279,76 @@ def _validate_config(self): if self.get("plex.url") == "": logger.error("A Plex URL is required") raise InvalidConfiguration + if self.get("plex.token") == "": logger.error("A Plex Token is required") raise InvalidConfiguration + if self.get("update_level") not in ["show", "season"]: logger.error("The 'update_level' parameter must be either 'show' or 'season'") raise InvalidConfiguration + if self.get("update_strategy") not in ["all", "next"]: logger.error("The 'update_strategy' parameter must be either 'all' or 'next'") raise InvalidConfiguration + if not isinstance(self.get("ignore_labels"), list): logger.error("The 'ignore_labels' parameter must be a list or a string-based comma separated list") raise InvalidConfiguration + if not isinstance(self.get("ignore_libraries"), list): logger.error("The 'ignore_libraries' parameter must be a list or a string-based comma separated list") raise InvalidConfiguration + if not isinstance(self.get("ignore_filepatterns"), list): logger.error("The 'ignore_filepatterns' parameter must be a list or a string-based comma separated list") raise InvalidConfiguration + if self.get("scheduler.enable") and not re.match(r"^\d{2}:\d{2}$", str(self.get("scheduler.schedule_time"))): logger.error("A valid 'schedule_time' parameter with the format 'HH:MM' is required (ex: \"02:30\")") raise InvalidConfiguration + + valid_schedule_days = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + + if self.get("scheduler.enable"): + schedule_days = self.get("scheduler.schedule_days") + + if not isinstance(schedule_days, list): + logger.error("The 'scheduler.schedule_days' parameter must be a list or a comma-separated string") + raise InvalidConfiguration + + invalid_days = [ + day for day in schedule_days + if not isinstance(day, str) or day.strip().lower() not in valid_schedule_days + ] + + if len(invalid_days) > 0: + logger.error( + "The 'scheduler.schedule_days' parameter contains invalid values. " + f"Accepted values are: {', '.join(valid_schedule_days)}" + ) + raise InvalidConfiguration + if self.get("data_path") != "" and not os.path.exists(self.get("data_path")): logger.error("The 'data_path' parameter must be a valid path") raise InvalidConfiguration + if not isinstance(self.get("plex.connection_max_retries"), int) or self.get("plex.connection_max_retries") < 1: logger.error("The 'plex.connection_max_retries' parameter must be a positive integer") raise InvalidConfiguration + if not isinstance(self.get("plex.connection_retry_delay"), (int, float)) or self.get("plex.connection_retry_delay") < 0: logger.error("The 'plex.connection_retry_delay' parameter must be a non-negative number") raise InvalidConfiguration + logger.info("The provided configuration has been successfully validated") def _add_system_config(self): diff --git a/plex_auto_languages/utils/scheduler.py b/plex_auto_languages/utils/scheduler.py index c370fd5..4c77652 100644 --- a/plex_auto_languages/utils/scheduler.py +++ b/plex_auto_languages/utils/scheduler.py @@ -1,5 +1,5 @@ import time -from typing import Callable +from typing import Callable, List, Optional from threading import Thread, Event import schedule @@ -11,7 +11,7 @@ class Scheduler(Thread): """ - A threaded scheduler that executes a callback function at a specified time each day. + A threaded scheduler that executes a callback function at a specified time. This class extends Thread to run the scheduler in the background, allowing the application to perform other tasks while waiting for scheduled events. @@ -19,20 +19,53 @@ class Scheduler(Thread): Attributes: _stop_event (Event): Threading event used to signal the scheduler to stop. + + If no schedule days are provided, the callback runs every day. + If schedule days are provided, the callback runs only on those days. """ - def __init__(self, time_of_day: str, callback: Callable): + VALID_DAYS = { + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + } + + def __init__(self, time_of_day: str, callback: Callable, days: Optional[List[str]] = None): """ - Initialize the scheduler with a daily task. + Initialize the scheduler. Args: time_of_day (str): The time of day to run the callback in 'HH:MM' format. callback (Callable): The function to execute at the specified time. + days (Optional[List[str]]): Days on which the callback should run. + Empty or None means every day. """ super().__init__() - schedule.every().day.at(time_of_day).do(callback) self._stop_event = Event() + normalized_days = list(dict.fromkeys([ + day.strip().lower() + for day in (days or []) + if isinstance(day, str) and day.strip() + ])) + + if len(normalized_days) == 0: + schedule.every().day.at(time_of_day).do(callback) + logger.info(f"Scheduler configured to run every day at {time_of_day}") + return + + for day in normalized_days: + if day not in self.VALID_DAYS: + raise ValueError(f"Invalid scheduler day: {day}") + + getattr(schedule.every(), day).at(time_of_day).do(callback) + + logger.info(f"Scheduler configured to run on {', '.join(normalized_days)} at {time_of_day}") + def run(self) -> None: """ Start the scheduler loop.