Sonarr for Sports – Automated file matching, renaming, and metadata for sports content in Plex.
Metadata-driven automation that turns chaotic sports releases into Plex-perfect TV libraries—no brittle scripts, just declarative YAML.
Love watching sports replays in Plex but hate manually renaming files and setting metadata? Traditional tools like Sonarr don't work for sports because there's no centralized database like TheTVDB. Every sport structures their seasons differently (F1 has races, UFC has events, NFL has weeks), and release groups use wildly inconsistent naming schemes.
Playbook is a complete pipeline that bridges the gap between messy downloads and perfectly organized Plex libraries:
Custom scrapers pull sports schedules from various sources (SportsDB, official APIs, manual curation) and structure them as YAML files that mirror how Plex expects TV shows: Show → Season → Episode. This is the foundation – every sport gets its own "TVDb" equivalent.
Playbook scans your downloads, parses filenames using regex patterns (built-in packs for F1, MotoGP, UFC, NFL, NBA, NHL, etc.), matches them against the YAML database, and automatically renames/moves them to your Plex library with perfect naming.
The same YAML files that power matching also feed Kometa to set posters, summaries, air dates, and episode titles. One source of truth for everything.
- One YAML file does it all: episode matching + metadata + Kometa integration
- Declarative: Swap leagues, change folder structures, or add new release groups without touching Python
- Complete automation: From download to Plex-ready with proper artwork and descriptions
- Built for sports: Handles special cases like sprint races, prelims, qualifying sessions, and multi-part events
Before running for real, test with a dry-run to confirm metadata downloads and filesystem access:
docker run --rm -it \
-e DRY_RUN=true \
-e VERBOSE=true \
-e SOURCE_DIR="/downloads" \
-e DESTINATION_DIR="/library" \
-e CACHE_DIR="/cache" \
-v /config:/config \
-v /downloads:/data/source \
-v /library:/data/destination \
-v /cache:/var/cache/playbook \
ghcr.io/s0len/playbook:latest --dry-run --verbose- Playbook
- The Problem
- How Playbook Solves It
- Why It's a Game-Changer
- Quick Verification
- Table of Contents
- Quickstart
- Architecture at a Glance
- Configuration Deep Dive
- Run Modes & CLI
- Logging & Observability
- Directory Conventions
- Plex Metadata via Kometa
- Downloading Sports with Autobrr
- Plex Library Setup
- Extending to New Sports
- Troubleshooting & FAQ
- Development
- Roadmap
- License
- Support
- Sample NHL Regular Season Filenames
- Sample Figure Skating Grand Prix Filenames
Before running the organizer for real, confirm:
playbook.yamlexists (copyconfig/playbook.sample.yamland tailor it).SOURCE_DIR,DESTINATION_DIR, andCACHE_DIRpoint at mounted paths with the right permissions.- You can reach the remote metadata URLs from the host/container (validate with the dry-run above).
Important: The container validates that
SOURCE_DIR,DESTINATION_DIR, andCACHE_DIRare defined through environment variables or thesettingsblock in your config. It exits with an error instead of silently creating/data/...defaults, so wire these paths explicitly.
docker run -d \
--name playbook \
-e TZ="UTC" \
-e SOURCE_DIR="/downloads" \
-e DESTINATION_DIR="/library" \
-e CACHE_DIR="/cache" \
-v /config:/config \
-v /downloads:/data/source \
-v /library:/data/destination \
-v /cache:/var/cache/playbook \
-v /logs:/var/log/playbook \
ghcr.io/s0len/playbook:latest- Copy the sample configuration:
cp config/playbook.sample.yaml /config/playbook.yaml. - Update
playbook.yamlwith your directories, enabled sports, and any overrides. - Tail the logs (
docker logs -f playbook) to watch the first pass.
Tip: Dry-run everything first.
docker run --rm -it \ -e DRY_RUN=true \ -e VERBOSE=true \ -e SOURCE_DIR="/downloads" \ -e DESTINATION_DIR="/library" \ -e CACHE_DIR="/cache" \ -v /config:/config \ -v /downloads:/data/source \ -v /library:/data/destination \ -v /cache:/var/cache/playbook \ -v /logs:/var/log/playbook \ ghcr.io/s0len/playbook:latest --dry-run --verbose
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m playbook.cli --config /path/to/playbook.yaml --dry-run --verboseTips:
- Set
SOURCE_DIR,DESTINATION_DIR, andCACHE_DIRenv vars (or the equivalent entries insettings)—the container will refuse to start if these are missing. - Use
LOG_LEVEL=DEBUGorVERBOSE=trueto mirror the Docker verbosity locally. - When running from source, the entrypoint script
entrypoint.shmirrors the Docker environment variable contract.
Use the bjw-s/app-template chart with Flux to keep a cluster deployment reconciled. The example below mirrors the Docker settings and mounts persistent cache/log directories alongside the config file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s-labs/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: &app playbook
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: app-template
values:
controllers:
main:
type: deployment
containers:
app:
image:
repository: ghcr.io/s0len/playbook
tag: develop@sha256:586d8e06fae7d156d47130ed18b1a619a47d2c5378345e3f074ee6c282f09f02
pullPolicy: Always
env:
WATCH_MODE: true
LOG_LEVEL: INFO
CONFIG_PATH: /config/playbook.yaml
CACHE_DIR: /settings/cache
LOG_DIR: /settings/logs
SOURCE_DIR: /data/torrents/sport
DESTINATION_DIR: /data/media/sport
envFrom:
- secretRef:
name: playbook-secret
persistence:
settings:
existingClaim: playbook-settings
globalMounts:
- path: /settings
data:
type: nfs
server: "${TRUENAS_IP}"
path: /mnt/rust/data
globalMounts:
- path: /data
config:
type: configMap
name: playbook-configmap
globalMounts:
- path: /config/playbook.yaml
subPath: playbook.yaml
readOnly: trueQuick checklist:
- Create a
playbook-secretwith any sensitive values (kubectl create secret generic ... --from-literal=API_TOKEN=...). - Mount a
playbook-configmapcontaining yourplaybook.yaml(or use anexternalSecret). - Backing storage: either bind an existing PVC (
settings) for cache/logs or swap in another persistence strategy. The NFS block mounts downloads and media libraries. - Enable
file_watcher.enabled(or setWATCH_MODE=true) to keep Playbook running continuously; leave it disabled for ad-hoc batch runs. - Add
reloader.stakater.com/auto: "true"(already in the example) to hot-reload when the config map changes.
Under the hood, Playbook follows this flow:
┌────────────────┐ fetch + cache ┌─────────────────────┐
│ Remote YAML │ ───────────────────▶ │ Metadata Normalizer │
└────────────────┘ └────────┬────────────┘
│ normalized Show/Season/Episode
┌───────▼────────┐
source files + globs + aliases │ Matching Engine │
──────────────────────────────────────▶ │ (regex + TTL) │
└───────┬────────┘
│ context (season, episode, templates)
┌───────▼────────┐
│ Templating │
│ & Sanitization │
└───────┬────────┘
│ destination path
┌───────▼────────┐
│ Link/Copy/Sym │
└────────────────┘
- Metadata fetch & cache: remote YAML is downloaded with
requests, cached on disk, and refreshed when TTLs expire. - Normalization: structured dataclasses infer round numbers, preserve summaries, and attach aliases.
- Matching: regex capture groups, alias tables, and fuzzy matching link filenames to metadata episodes.
- Templating: rich context feeds customizable templates for root folders, season directories, and filenames.
- Action: files are hardlinked (default), copied, or symlinked into the library, respecting
skip_existingand priority rules.
Start with config/playbook.sample.yaml. The schema mirrors playbook.config dataclasses.
| Field | Description | Default |
|---|---|---|
source_dir |
Root directory containing downloads to normalize. | /data/source |
destination_dir |
Library root where organized folders/files are created. | /data/destination |
cache_dir |
Metadata cache directory (metadata/<hash>.json). Safe to delete to force refetch. |
/data/cache |
dry_run |
When true, logs intent but skips filesystem writes. |
false |
skip_existing |
Leave destination files untouched unless a higher-priority release arrives. | true |
link_mode |
Default link behavior: hardlink, copy, or symlink. |
hardlink |
notifications.batch_daily |
When true, queue per-sport notifications for the day and edit a single Discord message instead of posting every file. |
false |
notifications.flush_time |
Local time boundary (HH:MM) used to roll daily batches forward. Entries before this time count toward the previous day. |
"00:00" |
notifications.mentions |
Map sport_id (supports glob patterns, plus default) to a Discord mention string (role ID, @here, etc.) to prepend to matching notifications. |
{} |
file_watcher.enabled |
When true, Playbook keeps running and reacts to filesystem events; when false, the CLI performs a single pass and exits. |
false |
file_watcher.paths |
Directories to observe; defaults to source_dir when empty. Relative entries resolve under source_dir. |
[] |
file_watcher.include / ignore |
Glob filters to allow/skip events (e.g. ignore *.part). |
[] / ["*.part","*.tmp"] |
file_watcher.debounce_seconds |
Minimum seconds between watcher-triggered runs. Batches bursts of events into a single processor pass. | 5 |
file_watcher.reconcile_interval |
Forces a full scan every N seconds even if no events arrive, ensuring missed events are caught. | 900 |
destination.* |
Default templates for root folder, season folder, and filename. | See sample |
Define Discord notifications under notifications.targets: each entry describes a destination (single-event or batched) and can have its own mention overrides. Enable notifications.batch_daily if you prefer a rolling per-day embed instead of individual posts. Use notifications.flush_time to control when the “day” ends (useful for overnight events).
Use notifications.mentions to opt specific Discord roles or users into certain sports. Entries are keyed by the sport’s ID (plus an optional default fallback) and the value is any mentionable string (<@&ROLE_ID>, @here, etc.). Keys can include shell-style wildcards (e.g. formula1_*), and Playbook automatically falls back to the base ID before any variant suffix (premier_league also covers premier_league_2025_26). Mentions are prepended to both single-event and batched messages, so subscribers only get pinged for the sports they care about:
notifications:
mentions:
premier_league: "<@&123456789012345678>"
formula1: "<@&222333444555666777>" # Automatically applies to formula1_2025, formula1_2026, etc.
default: "@everyone" # optional fallback when no explicit entry exists
notifications:
targets:
- type: discord
webhook_env: DISCORD_WEBHOOK_URL # Keep secrets in env vars/Secrets, not the config file.
# webhook_url: ${DISCORD_WEBHOOK_URL} # Optional inline expansion if you already template configs elsewhere.
mentions:
formula1: "<@&999>" # Overrides/extends the global mentions for this webhook only.
- type: discord
webhook_url: https://discord.com/api/webhooks/alt
mentions:
premier_league: "<@&1234>"webhook_env tells Playbook to read the runtime environment for the URL, so you can mount a Kubernetes/Docker secret as env vars without ever writing the secret into the ConfigMap. If you already have your own templating flow you can continue to use webhook_url with ${VAR} substitution; both options are supported.
Older releases supported settings.discord_webhook_url; that field has been removed. If it still exists in your config you'll get a startup error — move the value into notifications.targets as shown above.
notifications.targets lets you fan out the same event to multiple destinations. Supported type values today are:
discord(single event or rolling daily embed, as above)slack(simple text payload, optional template)webhook(generic JSON payload, fully templatable)email(SMTP with configurable subject/body templates)autoscan(new) — ping the Autoscan manual trigger so Plex/Emby/Jellyfin rescans a directory as soon as Playbook links a file
Autoscan support mirrors the manual trigger endpoint: Playbook issues a POST /triggers/<name>?dir=... call with the directory that just received a processed file. Add a block like this under notifications.targets:
notifications:
targets:
- type: autoscan
url: http://autoscan:3030 # Base Autoscan URL (http/s)
trigger: manual # Optional when using the default manual endpoint
username: ${AUTOSCAN_USERNAME:-} # Optional basic-auth credentials
password: ${AUTOSCAN_PASSWORD:-}
rewrite:
- from: ${DESTINATION_DIR:-/data/destination}
to: /mnt/unionfs/Media # Rewrite Playbook’s path to what Autoscan/Plex can see
timeout: 10 # Seconds before the request is considered failed (default 10)
verify_ssl: true # ⚠️ SECURITY WARNING: Setting false disables SSL/TLS verification and exposes you to MITM attacks - only for development with self-signed certsEvery successful new/changed event sends the parent directory of the destination file as a dir query parameter. Add more rewrite entries if Autoscan lives inside a container with different mount points.
Enable file_watcher.enabled to react to filesystem events instead of relying on periodic scans. The watcher listens for create, modify, and move events under source_dir (or the directories listed in file_watcher.paths). Globs in include/ignore cull noisy files, debounce_seconds batches rapid-fire events into a single processor run, and reconcile_interval guarantees a periodic full scan just in case the platform drops events.
Each sport defines metadata, source detection, and matching behavior. Example below uses the Formula 1 2025 feed 1.
- id: formula1_2025
name: Formula 1 2025
enabled: true
metadata:
url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/formula1/2025.yaml
show_key: Formula1 2025
ttl_hours: 12
season_overrides:
Pre-Season Testing:
season_number: 0
round: 0
source_globs:
- "Formula.1.*"
source_extensions:
- .mkv
- .mp4
allow_unmatched: false
file_patterns:
- regex: "(?i)^Formula\\.1\\.(?P<year>\\d{4})\\.Round(?P<round>\\d{2})\\.(?P<location>[^.]+)\\.(?P<session>[^.]+)"
description: Canonical multi-session weekend releases
season_selector:
mode: round
group: round
episode_selector:
group: session
session_aliases:
Race: ["Race"]
Sprint: ["Sprint.Race", "Sprint"]
Qualifying: ["Qualifying", "Quali"]
Free Practice 1: ["FP1", "Free.Practice.1"]Key fields:
enabled: toggle sports on/off without deleting them.source_globs/source_extensions: coarse filters before pattern matching.allow_unmatched: downgrade pattern failures to informational logs (no warnings).link_mode: override global link behavior for a specific sport.
regex– Must supply the capture groups consumed by selectors and templates (e.g.,round,session,location).season_selector– Maps captures to a season. Supported modes:round,key,title,sequential. Addoffsetormappingfor fine-grained control.episode_selector– Chooses which capture identifies an episode.allow_fallback_to_titlelets the matcher fall back to fuzzy title comparisons, anddefault_valueforces a canonical session when the regex doesn’t capture one (useful for release groups that omitPrelims/Main Cardtags).session_aliases– Augment metadata aliases with release-specific tokens (case-insensitive, normalized).priority– Lower numbers win when multiple patterns match the same file (defaults to100).destination_*overrides – Apply sport- or pattern-specific templates without touching global settings.
Built-in templates: the project now ships curated pattern sets for Formula 1, MotoGP, Moto2, Moto3, Isle of Man TT, NFL, and UFC. Reference them from a sport entry via:
pattern_sets:
- formula1You can still inline file_patterns (alone or in addition to templates) for overrides or experiments. Review src/playbook/pattern_templates.yaml for the complete list and structure.
Templates accept rich context built from the match:
| Key | Meaning |
|---|---|
sport_id, sport_name |
Sport metadata from the config. |
show_title, show_key |
Raw and display titles from the metadata feed. |
season_title, season_number, season_round, season_year |
Season fields with overrides applied. |
episode_title, episode_number, episode_summary, episode_originally_available |
Episode details and optional air date (YYYY-MM-DD). |
location, session, round, … |
Any capture group from the regex. |
source_filename, source_stem, extension, suffix, relative_source |
Safe access to the original file name and path components. |
Filename components are sanitized automatically (lowercasing dangerous characters, trimming whitespace, removing forbidden characters).
Reuse a base sport definition across seasons or release groups using variants:
- id: indycar
name: IndyCar
metadata:
url: https://example.com/indycar/base.yaml
variants:
- year: 2024
metadata:
url: https://example.com/indycar-2024.yaml
- year: 2025
metadata:
url: https://example.com/indycar-2025.yamlEach variant inherits the base config, tweaks fields from the variant block, and receives an auto-generated id/name when not explicitly set.
python -m playbook.cli powers both the Docker entrypoint and local runs.
| CLI Flag | Environment | Default | Notes |
|---|---|---|---|
--config PATH |
CONFIG_PATH |
/config/playbook.yaml |
Path to the YAML config. |
--dry-run |
DRY_RUN |
Inherits settings.dry_run |
Force no-write mode. |
--verbose |
VERBOSE / DEBUG |
false |
Enables console DEBUG output. |
--log-level LEVEL |
LOG_LEVEL |
INFO (or DEBUG with --verbose) |
File log level. |
--console-level LEVEL |
CONSOLE_LEVEL |
matches file level | Console log level. |
--log-file PATH |
LOG_FILE / LOG_DIR |
./playbook.log |
Rotates to *.previous on start. |
--clear-processed-cache |
CLEAR_PROCESSED_CACHE |
false |
Truthy to reset processed file cache before processing. |
--watch |
WATCH_MODE=true |
settings.file_watcher.enabled |
Force filesystem watcher mode (keep Playbook running). |
--no-watch |
WATCH_MODE=false |
false |
Disable watcher mode even if the config enables it. |
Environment variables always win over config defaults, and CLI flags win over environment variables.
Playbook features rich, color-formatted help with practical examples for every command. Use --help for quick reference or --examples for a comprehensive cookbook-style guide:
# Main help with all available commands
playbook --help
# Command-specific help with brief examples
playbook run --help
playbook validate-config --help
playbook kometa-trigger --help
# Extended examples and usage patterns
playbook run --examples
playbook validate-config --examples
playbook kometa-trigger --examplesThe help output includes:
- Usage examples – Real-world commands you can copy-paste
- Environment variables – Alternative ways to configure options
- Tips & best practices – Common workflows and gotchas
- Docker variants – How to run the same command in containers
All help content is formatted with colors and icons for easy scanning. On non-interactive terminals (CI/CD, redirected output), Playbook automatically falls back to plain text.
Preflight your YAML before running the processor:
python -m playbook.cli validate-config --config /config/playbook.yaml --diff-sampleThe validator enforces the JSON schema, confirms referenced pattern sets exist, and then calls the same loader used by the runtime. Add --show-trace to surface Python tracebacks for deeper debugging. --diff-sample compares your file to config/playbook.sample.yaml to highlight customizations.
Continuous mode example:
docker run -d \
-e WATCH_MODE=true \
ghcr.io/s0len/playbook:latest --watchPlaybook stays alive and reruns automatically whenever the watcher observes filesystem changes (or when the reconcile timer forces a full scan). Use --no-watch (or WATCH_MODE=false) for single-pass batch runs.
- Log entries now use a multi-line block layout (timestamp + header + aligned key/value pairs) so dense sections breathe.
- INFO-level runs show grouped counts per sport/source; add
--verbose/LOG_LEVEL=DEBUGto expand into per-file diagnostics. - Each pass ends with a
Run Recapblock (duration, totals, Kometa trigger state, destination samples) for quick scanning. - On each run, the previous log rotates to
playbook.log.previous, andLOG_DIR=/var/log/playbookkeeps files persistent.
A typical library after one Formula 1 weekend might look like:
Formula 1 2025/
└── 01 Bahrain Grand Prix/
├── Formula 1 - S01E01 - Free Practice 1.mkv
├── Formula 1 - S01E02 - Qualifying.mkv
├── Formula 1 - S01E03 - Sprint.mkv
└── Formula 1 - S01E04 - Race.mkv
Hardlinks preserve disk space; switch to copy or symlink when cross-filesystem moves are required.
Playbook only handles file and folder layout. To get rich titles, posters and collections in Plex, you can pair it with Kometa and the same YAML metadata feeds.
Add something like this to your Kometa config.yml (library name can be whatever you use for sports, e.g. Sport):
libraries:
Sport:
metadata_files:
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/formula1/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/formula-e/2025-2026.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/indycar-series/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/isle-of-man-tt.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/moto2/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/moto3/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/motogp/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/nba/2025-2026.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/nfl/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/premier-league/2025-2026.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/uefa-champions-league/2025-2026.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/ufc/2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/womens-uefa-euro.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/wsbk-2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/wssp-2025.yaml
- url: https://raw.githubusercontent.com/s0len/meta-manager-config/refs/heads/main/metadata/wssp300-2025.yamlPlaybook can nudge Kometa automatically after each ingest cycle. Configure it once under settings.kometa_trigger and it will fire once at the end of every processor run whenever at least one brand-new file was linked, so duplicate runs are avoided automatically.
Use this when Playbook and Kometa live in the same cluster:
settings:
kometa_trigger:
enabled: true
mode: kubernetes
namespace: media
cronjob_name: kometa-sport
job_name_prefix: kometa-sport-triggered-by-playbookPlaybook clones the CronJob's jobTemplate so Kometa uses the exact same Pod spec you already trust. Jobs are labeled trigger=playbook, which makes it easy to monitor and tail logs:
kubectl -n media get jobs -l trigger=playbook
kubectl -n media logs job/kometa-sport-triggered-by-playbook-20241121-123456-abcdIf a Job already exists (e.g., Kometa is still running from a previous batch), Playbook logs the conflict and waits for the next ingest cycle.
No Kubernetes? Set mode: docker and Playbook will shell out to docker run (or podman, etc.) with whatever libraries and config path you provide:
settings:
kometa_trigger:
enabled: true
mode: docker
docker:
binary: docker
image: kometateam/kometa
config_path: /srv/media/Kometa/config # host path
libraries: "Sports|TV Shows - 4K"
extra_args:
- --config
- /config/config.ymlUnder the hood Playbook runs a command similar to:
docker run --rm \
-v "/srv/media/Kometa/config:/config:rw" \
kometateam/kometa \
--run-libraries "Sports|TV Shows - 4K" \
--config /config/config.ymlAdd any additional Kometa CLI flags to docker.extra_args, and mount a different config path/container path if needed. Logs from the container are captured and surfaced in playbook.log, so failures stand out quickly.
-
Mount the Docker socket so the daemon is reachable:
-v /var/run/docker.sock:/var/run/docker.sock. -
Mount the client binaries into the container (paths vary, discover them with
command -v dockerandcommand -v com.docker.clion macOS):-v $(command -v docker):/usr/local/bin/docker \ -v $(command -v com.docker.cli):/usr/local/bin/com.docker.cli
Without these mounts the trigger will log clear errors (look for Kometa docker trigger requires...).
Already running Kometa in Docker Compose? Set docker.container_name (plus optional exec_python / exec_script) and Playbook will switch to docker exec instead of launching a new container:
docker exec kometa \
python3 /app/kometa/kometa.py \
--config /config/config.yml \
--library "Sports" \
--runAll other fields (libraries, extra_args, environment overrides) still apply, so you can reuse the same knobs regardless of whether you spin up a fresh container or exec into an existing one.
For absolute control you can provide docker.exec_command (a list such as ["python3", "/app/kometa/kometa.py"]), in which case Playbook appends your libraries/extra_args without any shell gymnastics.
Need to test the integration without running the full ingest loop? Use the dedicated CLI helper:
python -m playbook.cli kometa-trigger --config /config/playbook.yaml --mode dockerIt loads your config, instantiates the trigger, and logs the full docker command/output to playbook.log (or whatever --log-file you specify), making it easy to diagnose missing mounts or permissions.
Playbook does not download anything itself – it expects files to appear in SOURCE_DIR from a downloader (qBittorrent, Deluge, etc.). One way to automate this is with Autobrr.
Below is one approach using Autobrr filters and regexes targeted at specific sports and release groups.
For each sport you care about:
- Create a filter in Autobrr (e.g.
F1 1080p MWR,EPL 1080p NiGHTNiNJAS, etc.). - Select the trackers where your sports are available.
- Under Advanced → Release names → Match releases, paste a regex that:
- matches the sport name and year
- restricts to the resolution you want (e.g.
1080p) - optionally restricts to specific release groups (e.g.
MWR,NiGHTNiNJAS,DNU,GAMETiME,VERUM).
These are examples that pair well with the built-in pattern packs and metadata feeds:
# Premier League (EPL) 1080p releases by NiGHTNiNJAS
epl.*1080p.*nightninjas
# Formula 1 multi-session weekends by MWR
(F1|Formula.*1).*\d{4}.Round\d+.*[^.]+\.*?(Drivers.*Press.*Conference|Weekend.*Warm.*Up|FP\d?|Practice|Sprint.Qualifying|Sprint|Qualifying|Pre.Qualifying|Post.Qualifying|Race|Pre.Race|Post.Race|Sprint.Race|Feature.*Race).*1080p.*MWR
# Formula E by MWR
formulae\.\d{4}\.round\d+\.(?:[A-Za-z]+(?:\.[A-Za-z]+)?)\.(?:preview.show|qualifying|race)\..*h264.*-mwr
# IndyCar by MWR
indycar.*\d{4}\.round\d+\.(?:[A-Za-z]+(?:\.[A-Za-z]+)?)\.(?:qualifying|race)\..*h264.*-MWR
# Isle of Man TT by DNU
isle.of.man.tt.*DNU
# MotoGP by DNU
motogp.*\d{4}.*round\d.*((fp\d?|practice|sprint|qualifying|q1|q2|race)).*DNU
# NBA 1080p by GAMETiME
nba.*1080p.*gametime
# NHL RS 60fps feeds
nhl.*rs.*(720p|1080p).*en60fps
# NFL by NiGHTNiNJAS
nfl.*nightninjas
# UFC by VERUM
ufc[ ._-].*?\d{3}.*verum
# WorldSBK / WorldSSP / WorldSSP300 by MWR
(wsbk|wssp|wssp300).*\d{4}.round\d+.[^.]+.(fp\d?|season.preview|superpole|race.one|race.two|war.up(one|two)?|weekend.highlights).*h264.*mwr
UFC releases must now include the matchup slug (e.g., UFC 322 Della Maddalena vs Makhachev) so Playbook can align each file with the correct metadata season. Event numbers alone are ignored by the new title-based matching.
To let Plex correctly index everything that Playbook creates, set up a dedicated TV library that points at your Playbook destination directory.
-
In the Plex web UI, go to Libraries → Add Library.
-
Choose:
- Library type:
TV Shows - Name: e.g.
Sport,Sports, or whatever fits your setup.
- Library type:
-
Click Next and under Add folders, select the same folder you configured as
DESTINATION_DIRfor Playbook (or the sports subfolder inside it). -
Click Advanced and set:
- Scanner:
Plex Series Scanner - Agent:
Personal Media Shows - Episode sorting:
Newest first
- Scanner:
-
Save the library, then run a Scan Library Files once Playbook has populated the destination folder.
Using TV Shows + Plex Series Scanner + Personal Media Shows ensures Plex treats each sport/season/session as proper TV episodes, while Kometa applies all the rich metadata on top.
- Start from
playbook.sample.yamland enable the sport by listing the appropriatepattern_sets(e.g.,formula1,motogp). - Update the
metadata.url/show_key, along withsource_globsandsource_extensionsfor your release group. - If no template exists yet (or you need tweaks), copy the closest set from
pattern_templates.yamlinto thepattern_sets:section of your config and adjust the regex/aliases. - Run
--dry-run --verboseand review both console output andplaybook.logfor skipped/ignored diagnostics. - Iterate on patterns, aliases, and templates until every file links where you expect—then consider opening a PR to upstream the new template.
- Nothing gets processed: Confirm the
source_diris mounted, readable, and matches yoursource_globs. EnableDEBUGto see ignored reasons. - Metadata looks stale: Delete the cache directory (
rm -rf /var/cache/playbook/metadata) or lowerttl_hours. - Hardlinks fail: Set
link_mode: copy(globally or per sport) when crossing filesystems or writing to SMB/NFS shares. - Pattern matches but wrong season: Adjust
season_selectormappings or useseason_overridesto force numbers for exhibitions/pre-season events. - Need to re-run immediately: Run
python -m playbook.cli --no-watch ...(or setWATCH_MODE=false) to perform an on-demand single pass even if your watcher deployment is already running.
git clone https://github.com/s0len/playbook.git
cd playbook
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Run the CLI locally:
python -m playbook.cli --config config/playbook.sample.yaml --dry-run --verbose. - Build the container image:
docker build -t playbook:dev .. - Follow standard Python formatting (e.g.,
ruff,black) to keep diffs tidy. - Install test tooling:
pip install -r requirements-dev.txt. - Run the automated tests:
pytest. - Bootstrap a brand-new sandbox (e.g., Cursor/MCP agents) and run the full test suite in one step:
bash scripts/bootstrap_and_test.sh. - Validate filename samples: edit
tests/data/pattern_samples.yamland runpytest tests/test_pattern_samples.pyto confirm new or modified patterns resolve correctly. - Open a draft pull request early—sample configs and matching logic benefit from collaborative review.
- Additional pattern packs (MotoGP, IndyCar, NBA, NFL, NHL) with ready-to-use regex + alias tables.
- Optional webhook/websocket triggers to react to new downloads instantly.
- Strategy plugins for bespoke numbering or archive workflows.
- Web UI to inspect matches, stats, and activity history.
- Telemetry toggles for Prometheus/Grafana dashboards.
Distributed under the GNU GPLv3.
Questions, feature ideas, or metadata feed requests? Open an issue or start a discussion. For bespoke integrations, reach out via the issue tracker and we can coordinate.
Bundle the nhl pattern set with the NHL 2025-2026 metadata feed to normalize releases such as:
NHL RS 2025 New Jersey Devils vs Buffalo Sabres 28 11 720pEN60fps MSG.mkvNHL 18-10-2025 RS Edmonton Oilers vs New Jersey Devils 1080p60_EN_MSGSN.mkvNHL RS 2025 New Jersey Devils vs Washington Capitals 15 11 720pEN60fps MonumentalS.mkvNHL.2025.RS.Blue.Jackets.vs.Devils.1080pEN60fps.mkv
Bundle the figure_skating_grand_prix pattern set with the Figure Skating Grand Prix 2025 metadata feed to normalize releases such as:
Figure Skating Grand Prix France 2025 Pairs Short Program 17 10 720pEN50fps ESFigure Skating Grand Prix France 2025 Ice Dancing Rhythm Dance 18 10 720pEN50fps ESFigure Skating Grand Prix China 2025 Mixed Pairs Short Program 24 10 720pEN50fps ESFigure Skating Grand Prix China 2025 Exhibition Gala 26 10 720pEN50fps ESFigure Skating Grand Prix Canada 2025 Ice Dancing Free Program 02 11 720pEN50fps ESFigure Skating Grand Prix Canada 2025 Men Free Program 02 11 720pEN50fps ESFigure Skating Grand Prix Japan 2025 Ice Dancing Free Program 08 11 720pEN50fps ESFigure Skating USA Grand Prix 2025 Pairs Short Program 15 11 720pEN50fps ESFigure Skating Grand Prix Espoo 2025 Exhibition Gala 23 11 720pEN50fps ESFigure Skating Grand Prix Final 2025 Women Free Program 06 12 1080pEN50fps.mkv