diff --git a/README.md b/README.md
index 3798251..68b1dcc 100644
--- a/README.md
+++ b/README.md
@@ -94,10 +94,11 @@ docker run -d \
#### Paths and Environment Variables
-| Path | Description |
-|-----------|-----------------------------------------------------------------------|
-| `/config` | Path to config directory (`config.yaml` should be in this directory) |
-| `/logs` | Path to log directory (`Plex Prerolls.log` will be in this directory) |
+| Path | Description |
+|--------------------------|-----------------------------------------------------------------------------------------------|
+| `/config` | Path to config directory (`config.yaml` should be in this directory) |
+| `/logs` | Path to log directory (`Plex Prerolls.log` will be in this directory) |
+| `/path/to/preroll/files` | Path to the root directory of all preroll files (for [Path Globbing](#path-globbing) feature) |
| Environment Variable | Description |
|----------------------|-------------------------------------------------------------------|
@@ -131,7 +132,8 @@ You can define as many schedules as you want, in the following categories (order
All schedule entries accept an optional `weight` value that can be used to adjust the emphasis of this entry over
others by adding the listed paths multiple times. Since Plex selects a random preroll from the list of paths, having the
same path listed multiple times increases its chances of being selected over paths that only appear once. This allows
-you to combine, e.g. a `date_range` entry with an `always` entry, but place more weight/emphasis on the `date_range` entry.
+you to combine, e.g. a `date_range` entry with an `always` entry, but place more weight/emphasis on the `date_range`
+entry.
```yaml
date_range:
@@ -148,8 +150,10 @@ date_range:
#### Disable Always
Any schedule entry (except for the `always` section) can disable the inclusion of the `always` section by setting the
-`disable_always` value to `true`. This can be useful if you want to make one specific, i.e. `date_range` entry for a holiday,
-and you don't want to include the `always` section for this specific holiday, but you still want to include the `always` section
+`disable_always` value to `true`. This can be useful if you want to make one specific, i.e. `date_range` entry for a
+holiday,
+and you don't want to include the `always` section for this specific holiday, but you still want to include the `always`
+section
for other holidays.
```yaml
@@ -205,6 +209,81 @@ You should [adjust your cron schedule](#scheduling-script) to run the script mor
---
+## Advanced Configuration
+
+### Path Globbing
+
+**NOTE**: This feature will only work if you are running the script/Docker container on the same machine as your Plex
+server.
+
+Instead of listing out each individual preroll file, you can use glob (wildcard) patterns to match multiple files in a
+specific directory.
+The application will search for all files on your local filesystem that match the pattern(s) and automatically translate
+them to Plex-compatible remote paths.
+
+#### Setup
+
+Enable the feature under the advanced section of the config file, and specify the path to the root directory of your
+preroll files, as well as the path to the same directory as seen by Plex.
+
+```yaml
+advanced:
+ path_globbing:
+ enabled: true
+ root_path: /path/to/preroll/directory/in/relation/to/application
+ plex_path: /path/to/same/directory/as/seen/by/plex
+```
+
+For example, if your prerolls on your file system are located at `/mnt/user/media/prerolls` and Plex sees them
+at `/media/prerolls`, you would set the `root_path` to `/mnt/user/media/prerolls` and the `plex_path`
+to `/media/prerolls`.
+
+If you are using the Docker container, you can mount the preroll directory to the container at any location you would
+prefer (recommended: `/files`) and set the `root_path` accordingly.
+
+If you are using the Unraid version of this container, the "Files Path" path is mapped to `/files` by default; you
+should set `root_path` to `/files` and `plex_path` to the same directory as seen by Plex.
+
+#### Usage
+
+In any schedule section, you can use the `path_globs` key to specify a list of glob patterns to match files.
+
+```yaml
+always:
+ enabled: true
+ paths:
+ - /remote/path/1.mp4
+ - /remote/path/2.mp4
+ - /remote/path/3.mp4
+ path_globs:
+ - "*.mp4"
+```
+
+The above example will match all `.mp4` files in the `root_path` directory and append them to the list of prerolls.
+
+If you have organized your prerolls into subdirectories, you can specify specific subdirectories to match, or use `**`
+to match all subdirectories.
+
+```yaml
+always:
+ enabled: true
+ paths:
+ - /remote/path/1.mp4
+ - /remote/path/2.mp4
+ - /remote/path/3.mp4
+ path_globs:
+ - "subdir1/*.mp4"
+ - "subdir2/*.mp4"
+ - "subdir3/**/*.mp4"
+```
+
+You can use both `paths` and `path_globs` in the same section, allowing you to mix and match specific files with glob
+patterns.
+Please note that `paths` entries must be fully-qualified **remote** paths (as seen by Plex), while `path_globs` entries
+are relative to the **local** `root_path` directory.
+
+---
+
## Scheduling Script
**NOTE:** Scheduling is handled automatically in the Docker version of this script via the `CRON_SCHEDULE` environment
diff --git a/config.yaml.example b/config.yaml.example
index fd80c78..049acc1 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -9,9 +9,11 @@ plex:
always:
enabled: true
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
+ path_globs:
+ - "local/path/to/prerolls/*.mp4" # Optional, use globbing to match local paths
count: 10 # Optional, randomly select X many videos from the list rather than all of them
weight: 1 # Optional, how much to emphasize these pre-rolls over others (higher = more likely to play)
@@ -23,30 +25,32 @@ date_range:
start_date: 2020-01-01 # Jan 1st, 2020
end_date: 2020-01-02 # Jan 2nd, 2020
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
+ path_globs:
+ - "local/path/to/prerolls/*.mp4" # Optional, use globbing to match local paths
weight: 2 # Optional, add these paths to the list twice (make up greater percentage of prerolls - more likely to be selected)
disable_always: true # Optional, if present and true, disable the always prerolls when this schedule is active
- start_date: xxxx-07-04 # Every year on July 4th
end_date: xxxx-07-04 # Every year on July 4th
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
disable_always: false
- start_date: xxxx-xx-02 # Every year on the 2nd of every month
end_date: xxxx-xx-03 # Every year on the 3rd of every month
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
- start_date: xxxx-xx-xx 08:00:00 # Every day at 8am
end_date: xxxx-xx-xx 09:30:00 # Every day at 9:30am
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
# Schedule prerolls by week of the year
weekly:
@@ -54,15 +58,17 @@ weekly:
weeks:
- number: 1 # First week of the year
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
+ path_globs:
+ - "local/path/to/prerolls/*.mp4" # Optional, use globbing to match local paths
weight: 1 # Optional, how much to emphasize these pre-rolls over others (higher = more likely to play)
- number: 2 # Second week of the year
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
disable_always: true # If true, disable the always prerolls when this schedule is active
# Schedule prerolls by month of the year
@@ -71,13 +77,21 @@ monthly:
months:
- number: 1 # January
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
weight: 1 # Optional, how much to emphasize these pre-rolls over others (higher = more likely to play)
- number: 2 # February
paths:
- - "path/to/video1.mp4"
- - "path/to/video2.mp4"
- - "path/to/video3.mp4"
+ - "remote/path/to/video1.mp4"
+ - "remote/path/to/video2.mp4"
+ - "remote/path/to/video3.mp4"
+ path_globs:
+ - "local/path/to/prerolls/*.mp4" # Optional, use globbing to match local paths
disable_always: false # If true, disable the always prerolls when this schedule is active
+
+advanced:
+ path_globbing:
+ enabled: true # If true, use globbing to match paths
+ root_path: /files # The root folder to use for globbing
+ plex_path: /path/to/prerolls/in/plex # The path to use for the Plex server
diff --git a/modules/config_parser.py b/modules/config_parser.py
index 43cd5eb..95c589c 100644
--- a/modules/config_parser.py
+++ b/modules/config_parser.py
@@ -4,6 +4,7 @@
import confuse
import yaml
+import modules.files as files
import modules.logs as logging
@@ -28,10 +29,34 @@ def __init__(self, data):
super().__init__(data)
self.data = data
+ def all_paths(self, advanced_settings: 'AdvancedConfig' = None) -> List[str]:
+ paths = []
+ paths.extend(self.remote_paths)
+
+ if not advanced_settings or not advanced_settings.path_globbing.enabled:
+ return paths
+
+ local_files_root = advanced_settings.path_globbing.local_root_folder
+ remote_files_root = advanced_settings.path_globbing.remote_root_folder
+
+ for glob in self.local_path_globs:
+ local_files = files.get_all_files_matching_glob_pattern(directory=local_files_root, pattern=glob)
+ for local_file in local_files:
+ remote_file = files.translate_local_path_to_remote_path(local_path=local_file,
+ local_root_folder=local_files_root,
+ remote_root_folder=remote_files_root)
+ paths.append(remote_file)
+
+ return paths
+
@property
- def paths(self) -> List[str]:
+ def remote_paths(self) -> List[str]:
return self._get_value(key="paths", default=[])
+ @property
+ def local_path_globs(self) -> List[str]:
+ return self._get_value(key="path_globs", default=[])
+
@property
def weight(self) -> int:
return self._get_value(key="weight", default=1)
@@ -67,7 +92,8 @@ def end_date(self) -> str:
return self._get_value(key="end_date", default=None)
def __repr__(self):
- return f"DateRangeEntry(start_date={self.start_date}, end_date={self.end_date}, paths={self.paths}, weight={self.weight})"
+ return (f"DateRangeEntry(start_date={self.start_date}, end_date={self.end_date}, "
+ f"remote_paths={self.remote_paths}, local_path_globs={self.local_path_globs}, weight={self.weight})")
class WeekEntry(NumericalEntry):
@@ -75,7 +101,8 @@ def __init__(self, data):
super().__init__(data=data)
def __repr__(self):
- return f"WeekEntry(number={self.number}, paths={self.paths}, weight={self.weight})"
+ return (f"WeekEntry(number={self.number}, remote_paths={self.remote_paths}, "
+ f"local_path_globs={self.local_path_globs}, weight={self.weight})")
class MonthEntry(NumericalEntry):
@@ -83,7 +110,8 @@ def __init__(self, data):
super().__init__(data=data)
def __repr__(self):
- return f"MonthEntry(number={self.number}, paths={self.paths}, weight={self.weight})"
+ return (f"MonthEntry(number={self.number}, remote_paths={self.remote_paths}, "
+ f"local_path_globs={self.local_path_globs}, weight={self.weight})")
class ConfigSection(YAMLElement):
@@ -134,6 +162,32 @@ def port(self) -> Union[int, None]:
return port
+class PathGlobbingConfig(ConfigSection):
+ def __init__(self, data):
+ super().__init__(section_key="path_globbing", data=data)
+
+ @property
+ def enabled(self) -> bool:
+ return self._get_value(key="enabled", default=False)
+
+ @property
+ def local_root_folder(self) -> str:
+ return self._get_value(key="root_path", default="/")
+
+ @property
+ def remote_root_folder(self) -> str:
+ return self._get_value(key="plex_path", default="/")
+
+
+class AdvancedConfig(ConfigSection):
+ def __init__(self, data):
+ super().__init__(section_key="advanced", data=data)
+
+ @property
+ def path_globbing(self) -> PathGlobbingConfig:
+ return PathGlobbingConfig(data=self.data)
+
+
class ScheduleSection(ConfigSection):
def __init__(self, section_key: str, data):
super().__init__(section_key=section_key, data=data)
@@ -148,20 +202,44 @@ def __init__(self, data):
super(ScheduleSection, self).__init__(section_key="always", data=data)
# Double inheritance doesn't work well with conflicting "data" properties, just re-implement these two functions.
+ def all_paths(self, advanced_settings: 'AdvancedConfig' = None) -> List[str]:
+ paths = []
+ paths.extend(self.remote_paths)
+
+ if not advanced_settings or not advanced_settings.path_globbing.enabled:
+ return paths
+
+ local_files_root = advanced_settings.path_globbing.local_root_folder
+ remote_files_root = advanced_settings.path_globbing.remote_root_folder
+
+ for glob in self.local_path_globs:
+ local_files = files.get_all_files_matching_glob_pattern(directory=local_files_root, pattern=glob)
+ for local_file in local_files:
+ remote_file = files.translate_local_path_to_remote_path(local_path=local_file,
+ local_root_folder=local_files_root,
+ remote_root_folder=remote_files_root)
+ paths.append(remote_file)
+
+ return paths
+
@property
- def paths(self) -> List[str]:
+ def remote_paths(self) -> List[str]:
return self._get_value(key="paths", default=[])
+ @property
+ def local_path_globs(self) -> List[str]:
+ return self._get_value(key="path_globs", default=[])
+
@property
def weight(self) -> int:
return self._get_value(key="weight", default=1)
- @property
- def random_count(self) -> int:
- return self._get_value(key="count", default=len(self.paths))
+ def random_count(self, advanced_settings: 'AdvancedConfig' = None) -> int:
+ return self._get_value(key="count", default=len(self.all_paths(advanced_settings=advanced_settings)))
def __repr__(self):
- return f"AlwaysSection(paths={self.paths}, weight={self.weight}, random_count={self.random_count})"
+ return (f"AlwaysSection(remote_paths={self.remote_paths}, local_path_globs={self.local_path_globs}, "
+ f"weight={self.weight}")
class DateRangeSection(ScheduleSection):
@@ -222,6 +300,7 @@ def __init__(self, app_name: str, config_path: str):
self.date_ranges = DateRangeSection(data=self.config)
self.monthly = MonthlySection(data=self.config)
self.weekly = WeeklySection(data=self.config)
+ self.advanced = AdvancedConfig(data=self.config)
logging.debug(f"Using configuration:\n{self.log()}")
@@ -236,8 +315,8 @@ def all(self) -> dict:
"Plex - URL": self.plex.url,
"Plex - Token": "Exists" if self.plex.token else "Not Set",
"Always - Enabled": self.always.enabled,
- "Always - Paths": self.always.paths,
- "Always - Count": self.always.random_count,
+ "Always - Paths": self.always.all_paths(advanced_settings=self.advanced),
+ "Always - Count": self.always.random_count(advanced_settings=self.advanced),
"Always - Weight": self.always.weight,
"Date Range - Enabled": self.date_ranges.enabled,
"Date Range - Ranges": self.date_ranges.ranges,
@@ -245,6 +324,9 @@ def all(self) -> dict:
"Monthly - Months": self.monthly.months,
"Weekly - Enabled": self.weekly.enabled,
"Weekly - Weeks": self.weekly.weeks,
+ "Advanced - Path Globbing - Enabled": self.advanced.path_globbing.enabled,
+ "Advanced - Path Globbing - Local Root Folder": self.advanced.path_globbing.local_root_folder,
+ "Advanced - Path Globbing - Remote Root Folder": self.advanced.path_globbing.remote_root_folder
}
def log(self) -> str:
diff --git a/modules/files.py b/modules/files.py
new file mode 100644
index 0000000..42d01e0
--- /dev/null
+++ b/modules/files.py
@@ -0,0 +1,32 @@
+import glob
+import os
+from typing import List
+
+
+def get_all_files_matching_glob_pattern(directory: str, pattern: str) -> List[str]:
+ """
+ Get all files matching a glob pattern in a directory.
+
+ Args:
+ directory (str): The directory to search in.
+ pattern (str): The glob pattern to search for.
+
+ Returns:
+ List[str]: A list of file paths that match the glob pattern.
+ """
+ return [file for file in glob.glob(os.path.join(directory, pattern)) if os.path.isfile(file)]
+
+
+def translate_local_path_to_remote_path(local_path: str, local_root_folder: str, remote_root_folder: str) -> str:
+ """
+ Translate a local path to a remote path.
+
+ Args:
+ local_path (str): The local path to translate.
+ local_root_folder (str): The root folder of the local path.
+ remote_root_folder (str): The root folder of the remote path.
+
+ Returns:
+ str: The translated remote path.
+ """
+ return local_path.replace(local_root_folder, remote_root_folder, 1)
diff --git a/modules/schedule_manager.py b/modules/schedule_manager.py
index 29105f7..b01adf6 100644
--- a/modules/schedule_manager.py
+++ b/modules/schedule_manager.py
@@ -21,32 +21,41 @@ def _parse_schedules(self):
logging.info("Parsing schedules...")
if self._config.weekly.enabled:
for week in self._config.weekly.weeks:
- self.weekly_schedules.append(models.schedule_entry_from_week_number(week_number=week.number,
- paths=week.paths,
- weight=week.weight,
- disable_always=week.disable_always))
+ self.weekly_schedules.append(models.schedule_entry_from_week_number(
+ week_number=week.number,
+ paths=week.all_paths(
+ advanced_settings=self._config.advanced),
+ weight=week.weight,
+ disable_always=week.disable_always))
if self._config.monthly.enabled:
for month in self._config.monthly.months:
- self.monthly_schedules.append(models.schedule_entry_from_month_number(month_number=month.number,
- paths=month.paths,
- weight=month.weight,
- disable_always=month.disable_always))
+ self.monthly_schedules.append(models.schedule_entry_from_month_number(
+ month_number=month.number,
+ paths=month.all_paths(
+ advanced_settings=self._config.advanced),
+ weight=month.weight,
+ disable_always=month.disable_always))
if self._config.date_ranges.enabled:
for date_range in self._config.date_ranges.ranges:
- entry = models.schedule_entry_from_date_range(start_date_string=date_range.start_date,
- end_date_string=date_range.end_date,
- paths=date_range.paths,
- weight=date_range.weight,
- name=date_range.name,
- disable_always=date_range.disable_always)
+ entry = models.schedule_entry_from_date_range(
+ start_date_string=date_range.start_date,
+ end_date_string=date_range.end_date,
+ paths=date_range.all_paths(
+ advanced_settings=self._config.advanced),
+ weight=date_range.weight,
+ name=date_range.name,
+ disable_always=date_range.disable_always)
if entry:
self.date_range_schedules.append(entry)
if self._config.always.enabled:
- self.always_schedules.append(models.schedule_entry_from_always(paths=self._config.always.paths,
- count=self._config.always.random_count,
- weight=self._config.always.weight))
+ self.always_schedules.append(models.schedule_entry_from_always(
+ paths=self._config.always.all_paths(
+ advanced_settings=self._config.advanced),
+ count=self._config.always.random_count(
+ advanced_settings=self._config.advanced),
+ weight=self._config.always.weight))
@property
def valid_weekly_schedules(self) -> List[ScheduleEntry]:
diff --git a/templates/plex_prerolls.xml b/templates/plex_prerolls.xml
index 394f0d1..a0dc55a 100644
--- a/templates/plex_prerolls.xml
+++ b/templates/plex_prerolls.xml
@@ -21,6 +21,7 @@
UTC
- /mnt/user/appdata/plex_prerolls/config
+ /mnt/user/appdata/plex_prerolls/config
/mnt/user/appdata/plex_prerolls/logs
+ /mnt/user/appdata/plex_prerolls/files