- Edit file path names

- Add Docker + Docker Compose with cron job
- Clean, update README.md
This commit is contained in:
nwithan8 2023-12-07 18:57:30 -07:00
parent 1fdb20a998
commit 16d2738356
18 changed files with 460 additions and 443 deletions

13
.dockerignore Normal file
View File

@ -0,0 +1,13 @@
*.md
LICENSE
Dockerfile
.dockerignore
.gitignore
.github
.git
.idea
docker-compose.yml
venv
__pycache__
documentation
templates

View File

@ -1,36 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. macOS, Ubuntu, Alpine]
- Version [e.g. 22]
- Python Version:
**Setup (please complete the following information):**
- Did you ensure all required modules are installed? yes / no
- Does code/modules work outside the context of this script or program? yes / no
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[REQUEST]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

20
.github/release.yml vendored
View File

@ -1,20 +0,0 @@
# .github/release.yml
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes 🛠
labels:
- Semver-Major
- breaking-change
- title: Exciting New Features 🎉
labels:
- Semver-Minor
- enhancement
- title: Other Changes
labels:
- "*"

85
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: Build & Publish Docker image
on:
release:
types: [ created ]
secrets:
DOCKER_USERNAME:
required: true
DOCKER_TOKEN:
required: true
workflow_dispatch:
inputs:
version:
type: string
description: Version number
required: true
jobs:
publish:
name: Build & Publish to DockerHub and GitHub Packages
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '[no build]') == false
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Establish variables
id: vars
run: |
VERSION=${{ github.event.inputs.version || github.ref_name }}
echo ::set-output name=version::${VERSION}
echo ::set-output name=today::$(date +'%Y-%m-%d')
echo "::set-output name=year::$(date +'%Y')"
- name: Update version number
uses: jacobtomlinson/gha-find-replace@2.0.0
with:
find: "VERSIONADDEDBYGITHUB"
replace: "${{ steps.vars.outputs.version }}"
regex: false
- name: Update copyright year
uses: jacobtomlinson/gha-find-replace@2.0.0
with:
find: "YEARADDEDBYGITHUB"
replace: "${{ steps.vars.outputs.year }}"
regex: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
id: docker-buildx
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/armhf,linux/arm64
tags: |
nwithan8/plex_prerolls:latest
nwithan8/plex_prerolls:${{ steps.vars.outputs.version }}
ghcr.io/nwithan8/plex_prerolls:latest
ghcr.io/nwithan8/plex_prerolls:${{ steps.vars.outputs.version }}
labels: |
org.opencontainers.image.title=plex_prerolls
org.opencontainers.image.version=${{ steps.vars.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.today }}

View File

@ -1,19 +0,0 @@
# .github/workflows/publish-release-manual.yml
name: Publish release (manual)
on:
workflow_dispatch:
inputs:
tag:
description: The tag to publish
required: true
jobs:
publish:
runs-on: ubuntu-latest
name: Publish release
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: refs/tags/${{ github.event.inputs.tag }}
- name: Publish release
uses: eloquent/github-release-action@v3

View File

@ -1,15 +0,0 @@
# .github/workflows/publish-release.yml
name: Publish release
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
name: Publish release
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Publish release
uses: eloquent/github-release-action@v3

View File

@ -0,0 +1,36 @@
name: Copy Unraid Community Applications template(s) to templates repository
on:
release:
types: [ created ]
workflow_dispatch: ~
jobs:
copy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Establish variables
id: vars
run: |
VERSION=${{ github.event.inputs.version || github.ref_name }}
echo ::set-output name=version::${VERSION}
echo ::set-output name=today::$(date +'%Y-%m-%d')
- name: Open PR with template changes to unraid_templates
uses: nwithan8/action-pull-request-another-repo@v1.1.1
env:
API_TOKEN_GITHUB: ${{ secrets.PR_OPEN_GITHUB_TOKEN }}
with:
# Will mirror folder structure (copying "templates" folder to "templates" folder in destination repo)
source_folder: 'templates'
destination_repo: 'nwithan8/unraid_templates'
destination_base_branch: 'main'
destination_head_branch: prerolls-${{ steps.vars.outputs.version }}
user_email: 'nwithan8@users.noreply.github.com'
user_name: 'nwithan8'
pull_request_assignees: 'nwithan8'

3
.gitignore vendored
View File

@ -1,7 +1,6 @@
# Exclude local Config files
config.ini
preroll_schedules.yaml
preroll_schedules.yml
schedules.yaml
#Dev environmet
.devcontainer

40
Dockerfile Executable file
View File

@ -0,0 +1,40 @@
FROM python:3.11-alpine3.18
WORKDIR /
# Install Python and other utilities
RUN apk add --no-cache --update alpine-sdk git wget python3 python3-dev ca-certificates musl-dev libc-dev gcc bash nano linux-headers && \
python3 -m ensurepip && \
pip3 install --no-cache-dir --upgrade pip setuptools
# Copy requirements.txt from build machine to WORKDIR (/app) folder (important we do this BEFORE copying the rest of the files to avoid re-running pip install on every code change)
COPY requirements.txt requirements.txt
# Install Python requirements
RUN pip3 install --no-cache-dir -r requirements.txt
# Make Docker /config volume for optional config file
VOLUME /config
# Copy logging.conf file from build machine to Docker /config folder
COPY logging.conf /config/
# Copy example config file from build machine to Docker /config folder
# Also copies any existing config.ini file from build machine to Docker /config folder, (will cause the bot to use the existing config file if it exists)
COPY config.ini* /config/
# Copy example schedule file from build machine to Docker /config folder
# Also copies any existing schedules.yaml file from build machine to Docker /config folder, (will cause the bot to use the existing schedule file if it exists)
COPY schedules.yaml* /config/
# Make Docker /logs volume for log file
VOLUME /logs
# Copy source code from build machine to WORKDIR (/app) folder
COPY . .
# Delete unnecessary files in WORKDIR (/app) folder (not caught by .dockerignore)
RUN echo "**** removing unneeded files ****"
RUN rm -rf /requirements.txt
# Run entrypoint.sh script
ENTRYPOINT ["sh", "/entrypoint.sh"]

318
README.md
View File

@ -1,171 +1,51 @@
# Schedule Plex server related Pre-roll intro videos
# Plex Preroll Scheduler
A helper script to automate management of Plex pre-rolls. \
Define when you want different pre-rolls to play throughout the year.
A script to automate management of Plex pre-rolls.
Ideas include:
Define when you want different pre-rolls to play throughout the year. For example:
- Holiday pre-roll rotations
- Special occasions
- Summer/Winter/Seasonal rotations
- Seasonal rotations
- Breaking up the monotony
- Keeping your family on their toes!
Simple steps:
> 1. Config the schedule
> 2. Schedule script on server
> 3. ...
> 4. Profit!
See [Installation & Setup](#install) section
---
## Schedule Rules
## Installation and Usage
Schedule priority for a given Date:
### Run Script Directly
1. **misc** \
always_use - always includes in listing (append)
#### Requirements
2. **date_range** \
Include listing for the specified Start/End date range that include the given Date \
Range can be specified as a Date or DateTime \
Advanced features to have recurring timeframes \
**overrides usage of *week/month/default* listings
- Python 3.8+
3. **weekly** \
Include listing for the specified WEEK of the year for the given Date \
**override usage of *month/default* listings
4. **monthly** \
Include listing for the specified MONTH of the year for the given Date \
**overrides usage of *default* listings
5. **default** \
Default listing used of none of above apply to the given Date
Note: Script tries to find the closest matching range if multiple overlap at same time
---
## Installation & Setup <a id="install"></a>
Grab a copy of the code
Clone the repo:
```sh
cd /path/to/your/location
git clone https://github.com/BrianLindner/plex-schedule-prerolls.git
git clone https://github.com/nwithan8/plex-schedule-prerolls.git
```
### Install Requirements <a id="requirements"></a>
Requires:
- Python 3.8+ [may work on 3.6+ but not tested]
- See `requirements.txt` for Python modules and versions [link](requirements.txt)
- plexapi, configparser, pyyaml, etc.
Install Python requirements \
(highly recomend using <a href="https://docs.python.org/3/tutorial/venv.html" target="_blank">Virtual Environments</a> )
Install Python requirements:
```sh
pip install -r requirements.txt
```
### Create `config.ini` file with Plex connection information
Copy `config.ini.sample` to `config.ini` and complete the `[auth]` section with your Plex server information.
Script checks for:
Copy `schedules.yaml.sample` to `schedules.yaml` and [edit your schedule](#schedule-rules).
- local ./config.ini (See: [Sample](config.ini.sample))
- PlexAPI global config.ini
- Custom location config.ini (see [Arguments](#arguments))
(See: <a href="https://python-plexapi.readthedocs.io/en/latest/configuration.html" target="_blank">plexapi.CONFIG</a> for more info)
Rename `config.ini.sample` -> `config.ini` and update to your environment
Example `config.ini`
```ini
[auth]
server_baseurl = http://127.0.0.1:32400 # your plex server url
server_token = <PLEX_TOKEN> # access token
```
### Create `preroll_schedules.yaml` file with desired schedule
#### Date Range Section Scheduling
Use it for *Day* or *Ranges of Dates* needs \
Now with Time support! (optional)
Formatting Supported:
- Dates: yyyy-mm-dd
- DateTime: yyyy-mm-dd hh:mm:ss (24hr time format)
Rename `preroll_schedules.yaml.sample` -> `preroll_schedules.yaml` and update for your environment
Example YAML config layout (See: [Sample](preroll_schedules.yaml.sample) for more info)
```yaml
---
monthly:
enabled: (yes/no)
jan: /path/to/file.mp4;/path/to/file.mp4
...
dec: /path/to/file.mp4;/path/to/file.mp4
date_range:
enabled: (yes/no)
ranges:
- start_date: 2020-01-01
end_date: 2020-01-01
path: /path/to/video.mp4
- start_date: 2020-07-03
end_date: 2020-07-05
path: /path/to/video.mp4
- start_date: 2020-12-19
end_date: 2020-12-26
path: /path/to/video.mp4
weekly:
enabled: (yes/no)
"1": /path/to/file(s)
...
"52": /path/to/file(s)
misc:
enabled: (yes/no)
always_use: /path/to/file(s)
default:
enabled: (yes/no)
path: /path/to/file.mp4;/path/to/file.mp4
```
See [Advancecd Date Ranges](#advanced_date) for additional features
## Usage <a id="usage"></a>
### Default Usage
Run the script:
```sh
python schedule_preroll.py
```
### Runtime Arguments <a id="arguments" ></a>
- -v : version information
- -h : help information
- -c : config.ini (local or PlexAPI system central) for Connection Info (see [config.ini.sample](config.ini.sample))
- -s : preroll_schedules.yaml for various scheduling information (see [spreroll_schedules.yaml.sample](preroll_schedules.yaml.sample))
- -lc : location of custom logger.conf config file \
See:
- Sample [logger config](logging.conf)
- Logger usage <a href="https://github.com/amilstead/python-logging-examples/blob/master/configuration/fileConfig/config.ini" target="_blank" >Examples</a>
- Logging <a href="https://www.internalpointers.com/post/logging-python-sub-modules-and-configuration-files" target="_blank">Doc Info</a>
#### Advanced Usage
```sh
python schedule_preroll.py -h
$ python schedule_preroll.py -h
usage: schedule_preroll.py [-h] [-v] [-l LOG_CONFIG_FILE] [-c CONFIG_FILE] [-s SCHEDULE_FILE]
@ -179,100 +59,134 @@ optional arguments:
-c CONFIG_FILE, --config-path CONFIG_FILE
Path to Config.ini to use for Plex Server info. [Default: ./config.ini]
-s SCHEDULE_FILE, --schedule-path SCHEDULE_FILE
Path to pre-roll schedule file (YAML) to be use. [Default: ./preroll_schedules.yaml]
Path to pre-roll schedule file (YAML) to be use. [Default: ./schedules.yaml]
```
### Runtime Arguments Example
##### Example
```sh
python schedule_preroll.py \
-c path/to/custom/config.ini \
-s path/to/custom/preroll_schedules.yaml \
-s path/to/custom/schedules.yaml \
-lc path/to/custom/logger.conf
```
### Run as Docker Container
#### Requirements
- Docker
#### Docker Compose
Complete the provided `docker-compose.yml` file and run:
```sh
docker-compose up -d
```
#### Docker CLI
```sh
docker run -d \
--name=plex_prerolls \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Etc/UTC \
-e CRON_SCHEDULE="0 0 * * *" \
-v /path/to/config:/config \
-v /path/to/logs:/logs \
--restart unless-stopped \
nwithan8/plex_prerolls:latest
```
#### Paths and Environment Variables
| Path | Description |
|-----------|-------------------------------------------------------------------------------------|
| `/config` | Path to config files (`config.ini` and `schedule.yaml` should be in this directory) |
| `/logs` | Path to log files (`schedule_preroll.log` will be in this directory) |
| Environment Variable | Description |
|----------------------|-------------------------------------------------------------------|
| `PUID` | UID of user to run as |
| `PGID` | GID of user to run as |
| `TZ` | Timezone to use for cron schedule |
| `CRON_SCHEDULE` | Cron schedule to run script (see <https://crontab.guru> for help) |
---
## Scheduling Script (Optional) <a id="scheduling"></a>
## Schedule Rules
Schedules follow the following priority:
1. **misc**: Items listed in `always_use` will always be included (appended) to the preroll list
2. **date_range**: Schedule based on a specific date/time range
3. **weekly**: Schedule based on a specific week of the year
4. **monthly**: Schedule based on a specific month of the year
5. **default**: Default item to use if none of the above apply
For any conflicting schedules, the script tries to find the closest matching range and highest priority.
### Advanced Scheduling
#### Date Range Section Scheduling
`date_range` entries can accept both dates (`yyyy-mm-dd`) and datetimes (`yyyy-mm-dd hh:mm:ss`, 24-hour time).
`date_range` entries can also accept wildcards for any of the date/time fields. This can be useful for scheduling recurring events, such as annual events, "first-of-the-month" events, or even hourly events.
```yaml
date_range:
enabled: true
ranges:
# Each entry requires start_date, end_date, path values
- start_date: 2020-01-01 # Jan 1st, 2020
end_date: 2020-01-02 # Jan 2nd, 2020
path: /path/to/video.mp4
- start_date: xxxx-07-04 # Every year on July 4th
end_date: xxxx-07-04 # Every year on July 4th
path: /path/to/video.mp4
- 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
path: /path/to/video.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
path: /path/to/holiday_video.mp4
```
You should [adjust your cron schedule](#scheduling-script) to run the script more frequently if you use this feature.
---
## Scheduling Script
**NOTE:** Scheduling is handled automatically in the Docker version of this script via the `CRON_SCHEDULE` environment variable.
### Linux
Add to system scheduler:
Linux:
```sh
crontab -e
```
Place desired schedule (example below for everyday at midnight)
Place desired schedule (example below for every day at midnight)
```sh
0 0 * * * python /path/to/schedule_preroll.py >/dev/null 2>&1
```
or \
(Optional) Wrap in a shell script: \
useful if running other scripts/commands, using venv encapsulation, customizing arguments
You can also wrap the execution in a shell script (useful if running other scripts/commands, using venv encapsulation, customizing arguments, etc.)
```sh
0 0 * * * /path/to/schedule_preroll.sh >/dev/null 2>&1
```
Schedule as frequently as needed for your environment and how specific and to your personal rotation schedule needs
---
## Advanced Date Range Section Scheduling <a id="advanced_date"></a> (Optional)
Date Ranges with Recurring Timeframes \
Useful for static dates or times where you want recurring preroll activity
Examples:
- Every Morning
- Yearly holidays (Halloween, New Years, Independence)
- Birthdays, Anniversaries
For either Start and/or End date of range \
Substitute "xx" for date/times to schedule for "any" \
Substitute "xxxx" for recurring year
- xxxx-xx-01 - Every first of month
- xxxx-xx-xx - Every day
- xxxx-xx-xx 08:00:00 - every day from 8am
- xxxx-01-01 - Every year on Jan 1 (new years day)
if using Time, still must have a full datetime pattern (ex: hour, minute, second hh:mm:ss)
```yaml
#every July 4
- start_date: xxxx-07-04
end_date: xxxx-07-04
path: /path/to/video.mp4
# every first of month, all day
- start_date: xxxx-xx-01
end_date: xxxx-xx-01
path: /path/to/video.mp4
# 8-9 am every day
- start_date: xxxx-xx-xx 08:00:00
end_date: xxxx-xx-xx 08:59:59
path: /path/to/video.mp4
```
Note: Detailed time based schedules benefit from increased running of the Python script for frequently - ex: Hourly \
(See: [Scheduling Script](#scheduling) section)
---
## Config `logger.conf` to your needs (Optional)
See: <a href="https://docs.python.org/3/howto/logging.html" target="_blank"><https://docs.python.org/3/howto/logging.html></a>
---
## Wrapping Up
> Sit back and enjoy the Intros!
Schedule as frequently as needed for your schedule (ex: hourly, daily, weekly, etc.)
---

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
version: "3.9"
services:
plex_prerolls:
image: nwithan8/plex_prerolls:latest
volumes:
- /path/to/config:/config
- /path/to/logs:/logs
environment:
CRON_SCHEDULE: "0 0 * * *" # Run every day at midnight, see https://crontab.guru/ for help
TZ: America/New_York

22
entrypoint.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/sh
# Create cron directory
mkdir -p /etc/cron.d
# Read cron schedule from environment variable
CRON_SCHEDULE=${CRON_SCHEDULE:-"0 0 * * *"} # Default to midnight every day if not supplied
# Schedule cron job with supplied cron schedule
echo "$CRON_SCHEDULE python3 /schedule_preroll.py -c /config/config.ini -s /config/schedules.yaml -lc /config/logging.conf > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/cron.d/schedule_preroll
# Give execution rights on the cron job
chmod 0644 /etc/cron.d/schedule_preroll
# Apply cron job
crontab /etc/cron.d/schedule_preroll
# Create the log file to be able to run tail
touch /var/log/cron.log
# Run the command on container startup
crond && tail -f /var/log/cron.log

View File

@ -1,56 +0,0 @@
---
# all Key items must be in lowercase
# all PATH entries will be case sensitive based on your Environment (Linux, Windows)
monthly:
# If enabled, List out each month (lowercase;3-char abreviation) (optional)
# dont have to have all the months here, script will skip but log a Warning
enabled: Yes
jan: /path/to/video.mp4
feb:
mar:
apr:
may: /path/to/video.mp4
jun:
jul:
aug:
sep: /path/to/video.mp4;/path/to/video.mp4
oct:
nov:
dec:
date_range:
# If enabled, use a list of various date ranges/timeframes for different lists of pre-rolls
# list must be under ranges:
# each entry requires start_date, end_date, path values
enabled: Yes
ranges:
- start_date: 2020-01-01
end_date: 2020-01-01
path: /path/to/video.mp4
- start_date: 2020-07-03
end_date: 2020-07-05
path: /path/to/video.mp4
- start_date: 2020-12-19
end_date: 2020-12-26
path: /path/to/video.mp4
# every year on Dec 25
- start_date: xxxx-12-25
end_date: xxxx-12-25
path: /path/to/holiday_video.mp4
weekly:
# If enabled, list any weeks of the year to have specific prerolls 1-52 (optional)
# Dont need to have all the week numbers; the script skips over missing entries
# each week number must be surrounded by quotes
enabled: No
"1":
"2": /path/to/video.mkv
"52":
misc:
# If enabled, additional items for preroll selecton processing
# always_use: If enabled, always include these video files in pre-role listing
enabled: Yes
always_use: /path/to/video.mp4
# TBD: Future Use - other features
default:
# If enabled, Default listing of prerolls to use if no Schedule (above) is specified for date
enabled: Yes
path: /path/to/video1.mp4;/path/to/video3.mp4;/path/to/video4.mp4

View File

@ -15,8 +15,8 @@ Optional Arguments:
Path to Config.ini to use for Plex Server info.
[Default: ./config.ini]
-s SCHEDULE_FILE, --schedule-path SCHEDULE_FILE
Path to pre-roll schedule file (YAML) to be use.
[Default: ./preroll_schedules.yaml]
Path to pre-roll schedule file (YAML) to be used.
[Default: ./schedules.yaml]
Requirements:
- See Requirements.txt for Python modules
@ -24,7 +24,7 @@ Requirements:
Scheduling:
Add to system scheduler such as:
> crontab -e
> 0 0 * * * python path/to/schedule_preroll.py >/dev/null 2>&1
> 0 0 * * * python path/to/schedule_pre_roll.py >/dev/null 2>&1
Raises:
FileNotFoundError: [description]
@ -60,8 +60,8 @@ script_dir = os.path.dirname(__file__)
class ScheduleEntry(NamedTuple):
type: str
startdate: datetime
enddate: datetime
start_date: datetime
end_date: datetime
force: bool
path: str
@ -81,7 +81,7 @@ def arguments() -> Namespace:
config_default = "./config.ini"
log_config_default = "./logging.conf"
schedule_default = "./preroll_schedules.yaml"
schedule_default = "./schedules.yaml"
parser = ArgumentParser(description=f"{description}")
parser.add_argument(
"-v",
@ -141,18 +141,18 @@ def schedule_types() -> ScheduleType:
return schema
def week_range(year: int, weeknum: int) -> Tuple[datetime, datetime]:
def week_range(year: int, week_num: int) -> Tuple[datetime, datetime]:
"""Return the starting/ending date range of a given year/week
Args:
year (int): Year to calc range for
weeknum (int): Month of the year (1-12)
week_num (int): Month of the year (1-12)
Returns:
DateTime: Start date of the Year/Month
DateTime: End date of the Year/Month
"""
start = datetime.strptime(f"{year}-W{int(weeknum) - 1}-0", "%Y-W%W-%w").date()
start = datetime.strptime(f"{year}-W{int(week_num) - 1}-0", "%Y-W%W-%w").date()
end = start + timedelta(days=6)
start = datetime.combine(start, datetime.min.time())
@ -161,18 +161,18 @@ def week_range(year: int, weeknum: int) -> Tuple[datetime, datetime]:
return (start, end)
def month_range(year: int, monthnum: int) -> Tuple[datetime, datetime]:
def month_range(year: int, month_num: int) -> Tuple[datetime, datetime]:
"""Return the starting/ending date range of a given year/month
Args:
year (int): Year to calc range for
monthnum (int): Month of the year (1-12)
month_num (int): Month of the year (1-12)
Returns:
DateTime: Start date of the Year/Month
DateTime: End date of the Year/Month
"""
start = date(year, monthnum, 1)
start = date(year, month_num, 1)
next_month = start.replace(day=28) + timedelta(days=4)
end = next_month - timedelta(days=next_month.day)
@ -247,23 +247,23 @@ def make_datetime(value: Union[str, date, datetime], lowtime: bool = True) -> da
year, month, day = today.year, today.month, today.day
hour, minute, second = time.hour, time.minute, time.second
# start parsing the Time out, for later additional processing
dateparts = value.lower().split("-")
# start parsing the timeout, for later additional processing
date_parts = value.lower().split("-")
year = today.year if dateparts[0] == "xxxx" else int(dateparts[0])
month = today.month if dateparts[1] == "xx" else int(dateparts[1])
year = today.year if date_parts[0] == "xxxx" else int(date_parts[0])
month = today.month if date_parts[1] == "xx" else int(date_parts[1])
dateparts_day = dateparts[2].split(" ")
date_parts_day = date_parts[2].split(" ")
day = today.day if dateparts_day[0] == "xx" else int(dateparts_day[0])
day = today.day if date_parts_day[0] == "xx" else int(date_parts_day[0])
# attempt to parse out Time components
if len(dateparts_day) > 1:
timeparts = dateparts_day[1].split(":")
if len(timeparts) > 1:
hour = now.hour if timeparts[0] == "xx" else int(timeparts[0])
minute = now.minute if timeparts[1] == "xx" else int(timeparts[1])
second = now.second + 1 if timeparts[2] == "xx" else int(timeparts[2])
if len(date_parts_day) > 1:
time_parts = date_parts_day[1].split(":")
if len(time_parts) > 1:
hour = now.hour if time_parts[0] == "xx" else int(time_parts[0])
minute = now.minute if time_parts[1] == "xx" else int(time_parts[1])
second = now.second + 1 if time_parts[2] == "xx" else int(time_parts[2])
dt_val = datetime(year, month, day, hour, minute, second)
logger.debug("Datetime-> '%s'", dt_val)
@ -278,7 +278,7 @@ def make_datetime(value: Union[str, date, datetime], lowtime: bool = True) -> da
return dt_val
def schedulefile_contents(schedule_filename: Optional[str]) -> dict[str, any]: # type: ignore
def schedule_file_contents(schedule_filename: Optional[str]) -> dict[str, any]: # type: ignore
"""Returns a contents of the provided YAML file and validates
Args:
@ -290,7 +290,7 @@ def schedulefile_contents(schedule_filename: Optional[str]) -> dict[str, any]:
Returns:
YAML Contents: YAML structure of Dict[str, Any]
"""
default_files = ["preroll_schedules.yaml", "preroll_schedules.yml"]
default_files = ["schedules.yaml"]
filename = None
if schedule_filename not in ("", None):
@ -312,11 +312,11 @@ def schedulefile_contents(schedule_filename: Optional[str]) -> dict[str, any]:
# if we still cant find a schedule file, we abort
if not filename:
filestr = '" / "'.join(default_files)
logger.error('Missing schedule file: "%s"', filestr)
raise FileNotFoundError(filestr)
file_str = '" / "'.join(default_files)
logger.error('Missing schedule file: "%s"', file_str)
raise FileNotFoundError(file_str)
schema_filename = os.path.join(script_dir, "util/schedulefile_schema.json")
schema_filename = os.path.join(script_dir, "util/schedule_file_schema.json")
logger.debug('using schema validation file "%s"', schema_filename)
@ -354,17 +354,17 @@ def schedulefile_contents(schedule_filename: Optional[str]) -> dict[str, any]:
raise
if not v.validate(contents): # type: ignore
logger.error("Preroll-Schedule YAML Validation Error: %s", v.errors) # type: ignore
raise yaml.YAMLError(f"Preroll-Schedule YAML Validation Error: {v.errors}") # type: ignore
logger.error("Pre-roll Schedule YAML Validation Error: %s", v.errors) # type: ignore
raise yaml.YAMLError(f"Pre-roll Schedule YAML Validation Error: {v.errors}") # type: ignore
return contents
def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]:
"""Return a listing of defined preroll schedules for searching/use
def pre_roll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]:
"""Return a listing of defined pre_roll schedules for searching/use
Args:
schedule_file (str): path/to/schedule_preroll.yaml style config file (YAML Format)
schedule_file (str): path/to/schedule_pre_roll.yaml style config file (YAML Format)
Raises:
FileNotFoundError: If no schedule config file exists
@ -373,7 +373,7 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
list: list of ScheduleEntries
"""
contents = schedulefile_contents(schedule_file) # type: ignore
contents = schedule_file_contents(schedule_file) # type: ignore
today = date.today()
schedule: List[ScheduleEntry] = []
@ -399,8 +399,8 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
entry = ScheduleEntry(
type=schedule_section,
force=False,
startdate=start,
enddate=end,
start_date=start,
end_date=end,
path=path,
)
@ -432,8 +432,8 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
entry = ScheduleEntry(
type=schedule_section,
force=False,
startdate=start,
enddate=end,
start_date=start,
end_date=end,
path=path,
)
@ -468,8 +468,8 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
entry = ScheduleEntry(
type=schedule_section,
force=force, # type: ignore
startdate=start,
enddate=end,
start_date=start,
end_date=end,
path=path,
)
@ -496,8 +496,8 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
entry = ScheduleEntry(
type=schedule_section,
force=False,
startdate=datetime(today.year, today.month, today.day, 0, 0, 0),
enddate=datetime(today.year, today.month, today.day, 23, 59, 59),
start_date=datetime(today.year, today.month, today.day, 0, 0, 0),
end_date=datetime(today.year, today.month, today.day, 23, 59, 59),
path=path,
)
@ -521,8 +521,8 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
entry = ScheduleEntry(
type=schedule_section,
force=False,
startdate=datetime(today.year, today.month, today.day, 0, 0, 0),
enddate=datetime(today.year, today.month, today.day, 23, 59, 59),
start_date=datetime(today.year, today.month, today.day, 0, 0, 0),
end_date=datetime(today.year, today.month, today.day, 23, 59, 59),
path=path,
)
@ -539,7 +539,7 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
raise ValueError(msg)
# Sort list so most recent Ranges appear first
schedule.sort(reverse=True, key=lambda x: x.startdate)
schedule.sort(reverse=True, key=lambda x: x.start_date)
logger.debug("***START Schedule Set to be used***")
logger.debug(schedule)
@ -549,14 +549,14 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
def build_listing_string(items: List[str], play_all: bool = False) -> str:
"""Build the Plex formatted string of preroll paths
"""Build the Plex formatted string of pre_roll paths
Args:
items (list): List of preroll video paths to place into a string listing
items (list): List of pre_roll video paths to place into a string listing
play_all (bool, optional): Play all videos. [Default: False (Random choice)]
Returns:
string: CSV Listing (, or ;) based on play_all param of preroll video paths
string: CSV Listing (, or ;) based on play_all param of pre_roll video paths
"""
if len(items) == 0:
@ -572,8 +572,8 @@ def build_listing_string(items: List[str], play_all: bool = False) -> str:
return listing
def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[datetime] = None) -> str:
"""Return listing of preroll videos to be used by Plex
def pre_roll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[datetime] = None) -> str:
"""Return listing of pre_roll videos to be used by Plex
Args:
schedule (List[ScheduleEntry]): List of schedule entries (See: getPrerollSchedule)
@ -581,7 +581,7 @@ def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[dateti
Useful for simulating what different dates produce
Returns:
string: listing of preroll video paths to be used for Extras. CSV style: (;|,)
string: listing of pre_roll video paths to be used for Extras. CSV style: (;|,)
"""
listing = ""
entries = schedule_types()
@ -598,8 +598,8 @@ def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[dateti
# process the schedule for the given date
for entry in schedule:
try:
entry_start = entry.startdate
entry_end = entry.enddate
entry_start = entry.start_date
entry_end = entry.end_date
if not isinstance(entry_start, datetime): # type: ignore
entry_start = datetime.combine(entry_start, datetime.min.time())
if not isinstance(entry_end, datetime): # type: ignore
@ -628,7 +628,7 @@ def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[dateti
# check new schedule item against exist list
for e in entries[entry_type]:
duration_new = duration_seconds(entry_start, entry_end)
duration_curr = duration_seconds(e.startdate, e.enddate)
duration_curr = duration_seconds(e.start_date, e.end_date)
# only the narrowest timeframe should stay
# disregard if a force entry is there
@ -655,10 +655,10 @@ def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[dateti
if entries["monthly"] and not entries["weekly"] and not entries["date_range"]:
merged_list.extend([p.path for p in entries["monthly"]]) # type: ignore
if (
entries["default"]
and not entries["monthly"]
and not entries["weekly"]
and not entries["date_range"]
entries["default"]
and not entries["monthly"]
and not entries["weekly"]
and not entries["date_range"]
):
merged_list.extend([p.path for p in entries["default"]]) # type: ignore
@ -667,27 +667,27 @@ def preroll_listing(schedule: List[ScheduleEntry], for_datetime: Optional[dateti
return listing
def save_preroll_listing(plex: PlexServer, preroll_listing: Union[str, List[str]]) -> None:
def save_pre_roll_listing(plex: PlexServer, pre_roll_listing: Union[str, List[str]]) -> None:
"""Save Plex Preroll info to PlexServer settings
Args:
plex (PlexServer): Plex server to update
preroll_listing (str, list[str]): csv listing or List of preroll paths to save
pre_roll_listing (str, list[str]): csv listing or List of pre_roll paths to save
"""
# if happend to send in an Iterable List, merge to a string
if isinstance(preroll_listing, list):
preroll_listing = build_listing_string(list(preroll_listing))
# if happened to send in an Iterable List, merge to a string
if isinstance(pre_roll_listing, list):
pre_roll_listing = build_listing_string(list(pre_roll_listing))
logger.debug('Attempting save of pre-rolls: "%s"', preroll_listing)
logger.debug('Attempting save of pre-rolls: "%s"', pre_roll_listing)
plex.settings.get("cinemaTrailersPrerollID").set(preroll_listing) # type: ignore
plex.settings.get("cinemaTrailersPrerollID").set(pre_roll_listing) # type: ignore
try:
plex.settings.save() # type: ignore
except Exception as e:
logger.error('Unable to save Pre-Rolls to Server: "%s"', plex.friendlyName, exc_info=e) # type: ignore
raise e
logger.info('Saved Pre-Rolls: Server: "%s" Pre-Rolls: "%s"', plex.friendlyName, preroll_listing) # type: ignore
logger.info('Saved Pre-Rolls: Server: "%s" Pre-Rolls: "%s"', plex.friendlyName, pre_roll_listing) # type: ignore
if __name__ == "__main__":
@ -716,12 +716,12 @@ if __name__ == "__main__":
logger.error("Error connecting to Plex", exc_info=e)
raise e
schedule = preroll_schedule(args.schedule_file)
prerolls = preroll_listing(schedule)
schedule = pre_roll_schedule(args.schedule_file)
pre_rolls = pre_roll_listing(schedule)
if args.do_test_run:
msg = f"Test Run of Plex Pre-Rolls: **Nothing being saved**\n{prerolls}\n"
msg = f"Test Run of Plex Pre-Rolls: **Nothing being saved**\n{pre_rolls}\n"
logger.debug(msg)
print(msg)
else:
save_preroll_listing(plex, prerolls)
save_pre_roll_listing(plex, pre_rolls)

64
schedules.yaml.sample Normal file
View File

@ -0,0 +1,64 @@
# All keys must be in lowercase
# All paths will be case-sensitive based on your environment (Linux, Windows)
# Order of precedence:
# 1. Misc always_use
# 2. Date range
# 3. Weekly
# 4. Monthly
# 5. Default
# Additional items for preroll selection processing
misc:
enabled: true
always_use: /path/to/video.mp4 # If enabled, always include these video files in pre-role listing
# Schedule prerolls by date and time frames
date_range:
enabled: false
ranges:
# Each entry requires start_date, end_date, path values
- start_date: 2020-01-01 # Jan 1st, 2020
end_date: 2020-01-02 # Jan 2nd, 2020
path: /path/to/video.mp4
- start_date: xxxx-07-04 # Every year on July 4th
end_date: xxxx-07-04 # Every year on July 4th
path: /path/to/video.mp4
- 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
path: /path/to/video.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
path: /path/to/holiday_video.mp4
# Schedule prerolls by week of the year
weekly:
# No need to have all the week numbers; the script skips over missing entries
# Each week number must be surrounded by quotes
enabled: false
"1":
"2": /path/to/video.mkv
"52":
# Schedule prerolls by month of the year
monthly:
# No need to have all the months here, script will skip over missing entries (logs a warning)
# Keys must be lowercase, three-char month abbreviation
enabled: false
jan: /path/to/video.mp4
feb:
mar:
apr:
may: /path/to/video.mp4
jun:
jul:
aug:
sep: /path/to/video.mp4;/path/to/video.mp4
oct:
nov:
dec:
# Use a default preroll if no other schedule is matched
default:
enabled: true
path: /path/to/video.mp4

View File

@ -21,7 +21,7 @@ SCRIPT_NAME = os.path.splitext(filename)[0]
def plex_config(config_file: Optional[str] = "") -> Dict[str, str]:
"""Return Plex Config paramaters for connection info {PLEX_URL, PLEX_TOKEN}\n
"""Return Plex Config parameters for connection info {PLEX_URL, PLEX_TOKEN}\n
Attempts to use one of either:\n
* supplier path/to/config file (INI Format)
* local config.ini (primary)