Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
>
Expand Down
15 changes: 14 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plexautolanguages:
scheduler:
enable: true
schedule_time: "02:00"
schedule_days: []

notifications:
enable: false
Expand Down
4 changes: 3 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions plex_auto_languages/utils/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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):
Expand Down
43 changes: 38 additions & 5 deletions plex_auto_languages/utils/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time
from typing import Callable
from typing import Callable, List, Optional
from threading import Thread, Event
import schedule

Expand All @@ -11,28 +11,61 @@

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.
The scheduler can be gracefully shut down when needed.

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.
Expand Down