diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8d27729 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +*.md +LICENSE +Dockerfile +.dockerignore +.gitignore +.github +.git +.idea +docker-compose.yml +venv +__pycache__ +documentation +templates diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f47941f..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 9cde18a..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -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. diff --git a/.github/release.yml b/.github/release.yml deleted file mode 100644 index 7cb3a3f..0000000 --- a/.github/release.yml +++ /dev/null @@ -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: - - "*" \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..1d7fccc --- /dev/null +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/.github/workflows/publish-release-manual.yml b/.github/workflows/publish-release-manual.yml deleted file mode 100644 index 8225b4a..0000000 --- a/.github/workflows/publish-release-manual.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml deleted file mode 100644 index d5a3c21..0000000 --- a/.github/workflows/publish-release.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/upload_unraid_template.yml b/.github/workflows/upload_unraid_template.yml new file mode 100644 index 0000000..279859b --- /dev/null +++ b/.github/workflows/upload_unraid_template.yml @@ -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' diff --git a/.gitignore b/.gitignore index 30ef07b..265a265 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Exclude local Config files config.ini -preroll_schedules.yaml -preroll_schedules.yml +schedules.yaml #Dev environmet .devcontainer diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..bb6f77e --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 88ed072..4ccd2fb 100644 --- a/README.md +++ b/README.md @@ -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 - -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 - -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 Virtual Environments ) +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: plexapi.CONFIG 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 = # 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 - -### Default Usage +Run the script: ```sh python schedule_preroll.py ``` -### Runtime Arguments - -- -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 Examples - - Logging Doc Info +#### 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 for help) | + --- -## Scheduling Script (Optional) +## 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 (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: - ---- - -## Wrapping Up - -> Sit back and enjoy the Intros! +Schedule as frequently as needed for your schedule (ex: hourly, daily, weekly, etc.) --- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7073dff --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..7a1538f --- /dev/null +++ b/entrypoint.sh @@ -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 diff --git a/preroll_schedules.yaml.sample b/preroll_schedules.yaml.sample deleted file mode 100644 index eeabe3c..0000000 --- a/preroll_schedules.yaml.sample +++ /dev/null @@ -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 diff --git a/schedule_preroll.py b/schedule_preroll.py index f5e70bd..73f6ec8 100644 --- a/schedule_preroll.py +++ b/schedule_preroll.py @@ -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) diff --git a/schedules.yaml.sample b/schedules.yaml.sample new file mode 100644 index 0000000..02c01b9 --- /dev/null +++ b/schedules.yaml.sample @@ -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 diff --git a/util/plexutil.py b/util/plexutil.py index 775c7cb..133ec28 100644 --- a/util/plexutil.py +++ b/util/plexutil.py @@ -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) diff --git a/util/schedulefile_schema.json b/util/schedule_file_schema.json similarity index 100% rename from util/schedulefile_schema.json rename to util/schedule_file_schema.json