❯ An automated RPlay live recorder designed for long-running Docker deployments.
Built with Docker, Python, Poetry, Pydantic, yt-dlp, and FFmpeg.
- 📝 Description
- 🆕 What's New in v2
⚠️ v2 Upgrade Notes- ✨ Features
- 🚀 Quick Start
- 📘 Usage Guide
- 🛠️ Development
- 🔧 Project Structure
- 👥 Contributing
- 📜 License
rplay-live-dl monitors a configured list of RPlay creators, starts recording automatically when a stream goes live, and stores finished recordings under archive/<creator>/. It is designed for long-running Docker deployments where configuration, archive files, and logs are mounted from the host.
Warning
Vibe Coding Notice: versions with the -vibe suffix (for example 2.0.0-vibe) are AI-assisted releases. They pass automated tests, but you should still review breaking changes before upgrading production deployments.
Version 2.0.0-vibe introduces a session-aware download pipeline:
- each live session is tracked by
creator_oid + stream oid - raw media is downloaded into a per-session staging directory as
.ts - completed raw outputs are merged into a final
.mp4 - the visible final filename stays compatible with older releases
- a new live session can start even while the previous session is still merging
- startup now fails fast when the app detects the legacy
./config.yamlpath - detailed runtime behavior is documented below in Download and Merge Flow
apiBaseUrlnow lives inconfig/config.yaml, is reloaded every poll, and is auto-written withhttps://api.rplay.liveif missing
2.0.0-vibe contains a breaking config-path change.
| Before v2 | Since 2.0.0-vibe |
|---|---|
./config.yaml |
./config/config.yaml |
| mount one file | mount the whole ./config directory |
Upgrade checklist:
- Move your config file from
./config.yamlto./config/config.yaml. - Update Docker volume mounts to use
./config:/app/config. - Restart the container.
Startup protection:
- if
./config/config.yamlis missing - and legacy
./config.yamlstill exists - the app exits early with a migration error instead of silently starting with the wrong mount layout
- automated live monitoring for multiple creators
- session-aware download tracking to avoid creator-level blocking
- raw
.tsstaging plus final.mp4output for better HLS stability - immediate merge queueing after raw download completion
- legacy-compatible final filenames such as
#Creator 2026-03-06 Title.mp4 - duplicate title protection with suffixed outputs like
_1,_2, and so on - paid/private stream detection with blocked-session handling
- failed merge preservation under
archive/<creator>/_failed/ - fail-fast startup validation for legacy config path upgrades
- Docker-first deployment for long-running operation
- Create your environment file.
- Local / Poetry run: copy
.env.exampleto.env - Included
docker-compose.yaml: copy.env.exampletoenv, or update the compose volume to mount.envinstead
- Local / Poetry run: copy
- Create
config/config.yamlfromconfig.yaml.example. - Fill in your RPlay credentials, creator list, and optionally
apiBaseUrl. - Optionally set
LOG_LEVEL=DEBUGwhen you want verbose lifecycle logs. - Start the service with Docker Compose.
- Watch logs until the first polling cycle succeeds.
# 1) Prepare config files
cp .env.example .env
mkdir -p config
cp config.yaml.example config/config.yaml
# 2) Start the service
docker compose up -d
# 3) Follow logs
docker compose logs -fIf you use the stock docker-compose.yaml without modification, replace step 1 with:
cp .env.example envProduction:
- Docker
- valid RPlay account credentials
- stable network connectivity
- enough disk space for raw
.tsstaging and final.mp4files
Development:
- Python 3.11+
- Poetry
- FFmpeg
- Log in to
rplay.live - Open browser DevTools (
F12) - Execute
localStorage.getItem('_AUTHORIZATION_') - Copy the returned token
- Visit
https://rplay.live/myinfo/ - Copy your
User Number
- Visit the creator profile page
- Open DevTools → Network
- Refresh the page and search for
CreatorOid - Copy the creator ID
Copy .env.example to .env for local runs. If you use the bundled docker-compose.yaml, the host file named env is mounted to /app/.env.
Full example:
# Required: RPlay account credentials
AUTH_TOKEN=your_auth_token
USER_OID=your_user_oid
# Optional: monitor poll interval in seconds (10-3600)
INTERVAL=60
# Optional: application log level
LOG_LEVEL=INFO
# Optional: surface yt-dlp internal debug chatter (`1`, `true`, `yes`, `on`)
LOG_YTDLP_INTERNAL=false
# Optional: log rotation settings
LOG_MAX_SIZE_MB=5
LOG_BACKUP_COUNT=5
LOG_RETENTION_DAYS=30
# Optional: startup metadata shown as Git SHA in logs
# Usually injected automatically during Docker image builds
APP_GIT_SHA=Environment variables:
| Variable | Required | Default | Validation / accepted values | Purpose |
|---|---|---|---|---|
AUTH_TOKEN |
yes | none | non-empty | RPlay auth token used for API and stream access |
USER_OID |
yes | none | non-empty | Your RPlay user identifier |
INTERVAL |
no | 60 |
integer 10-3600 |
Poll interval in seconds |
LOG_LEVEL |
no | INFO |
DEBUG, INFO, WARNING, ERROR, CRITICAL; invalid values fall back to INFO |
Console and file log verbosity |
LOG_YTDLP_INTERNAL |
no | false |
truthy values: 1, true, yes, on |
Enables noisy yt-dlp internal debug lines |
LOG_MAX_SIZE_MB |
no | 5 |
integer 1-100 |
Maximum size of each log file before rotation |
LOG_BACKUP_COUNT |
no | 5 |
integer 1-50 |
Number of rotated log files to keep |
LOG_RETENTION_DAYS |
no | 30 |
integer 1-365 |
Age-based cleanup window for old logs |
APP_GIT_SHA |
no | empty | free-form string | Startup version metadata shown in logs; usually injected by Docker/image builds |
Notes:
- local runs load from
.envor process environment variables - the bundled Docker Compose file maps a host file named
envto/app/.env LOG_YTDLP_INTERNAL=trueis only for deep diagnosis; it is intentionally noisy
Copy config.yaml.example to config/config.yaml and edit it like this:
# Optional. If missing, the app keeps using the default and writes it back.
apiBaseUrl: https://api.rplay.live
creators:
- name: "Creator Nickname 1"
id: "Creator OID 1"
- name: "Creator Nickname 2"
id: "Creator OID 2"Configuration keys:
| Key | Required | Default | Validation | Purpose |
|---|---|---|---|---|
apiBaseUrl |
no | https://api.rplay.live |
absolute URL; surrounding whitespace is trimmed and trailing / is removed |
Base URL for the RPlay API |
creators |
no | empty list | YAML list | Creators to monitor |
creators[].name |
yes | none | non-empty, max 100 characters |
Display name used in logs, folder names, and final filenames |
creators[].id |
yes | none | non-empty | Creator OID from the RPlay profile/network requests |
Notes:
- if
apiBaseUrlis missing, the app keeps using the default and writes the key back intoconfig/config.yaml - the monitor re-reads
config/config.yamlon every poll, so updatingapiBaseUrlin a running Docker deployment does not require a container restart - an invalid
apiBaseUrlis treated as a config error and the current poll is skipped until the file is fixed - you can temporarily leave
creators: []while validating a deployment
The v2 runtime uses a session-aware download pipeline.
-
Poll
- the monitor loads
config/config.yaml - it refreshes
apiBaseUrlfrom config before calling the API - it checks live status for all configured creators
- the monitor loads
-
Create a session
- each live stream gets a session key based on
creator_oidand the APIoid - the raw download is isolated in
archive/<creator>/.staging/<session_dir>/ session_diris the filesystem-safe form of the session key, not the raw key string
- each live stream gets a session key based on
-
Download raw transport stream files
- yt-dlp writes raw outputs as
.ts - each download task uses a
10-second socket timeout - transient task failures automatically retry up to
3attempts total with exponential backoff HTTP 404on a fresh stream is retried before the current session is marked blockedHTTP 403is still treated as immediate blocked/private accessHTTP 401is treated as an authentication failure instead of a blocked session- raw staging names keep the familiar visible format, for example:
#Creator 2026-03-06 Title.ts#Creator 2026-03-06 Title_1.ts
- yt-dlp writes raw outputs as
-
Queue merge immediately after download completes
- as soon as raw download finishes, the merge job is submitted to the merge executor
- the control loop can move on quickly, so a new live session from the same creator can be picked up without waiting for the old merge to finish
-
Merge into final
.mp4- all
.tsfiles in that session staging directory are merged into one final.mp4 - even if only one raw
.tsfile exists, the final visible output is still.mp4
- all
-
Clean up or preserve for recovery
- on success, the merged
.tsfiles are deleted and the session staging directory is removed - on merge failure or timeout, the whole session staging directory is moved to
archive/<creator>/_failed/<session_dir>/
- on success, the merged
-
Observe lifecycle logs
- set
LOG_LEVEL=DEBUGin.envto see stream-candidate evaluation and skip reasons - set
LOG_YTDLP_INTERNAL=trueonly when you need raw yt-dlp internal chatter in addition to app logs - the default
INFOlevel keeps routine output readable for long-running Docker deployments
- set
Visible final outputs intentionally keep the old naming style:
- first session:
#Creator 2026-03-06 Title.mp4 - second session on the same day with the same title:
#Creator 2026-03-06 Title_1.mp4 - later duplicates continue as
_2,_3, and so on
This means:
- users still see the same filename pattern as previous releases
- collisions are resolved only at the final visible output layer
- raw session isolation happens inside
.staging, not in the archive root
# Start recording
docker compose up -d
# View logs
docker compose logs -f
# Stop recording
docker compose down
# Update image and restart
docker compose pull
docker compose up -dThe bundled docker-compose.yaml mounts:
./env→/app/.env./config→/app/config./archive→/app/archive./logs→/app/logs
docker run -d \
-v $(pwd)/.env:/app/.env \
-v $(pwd)/config:/app/config \
-v $(pwd)/archive:/app/archive \
-v $(pwd)/logs:/app/logs \
paverz/rplay-live-dl:latestTypical runtime layout:
rplay-live-dl/
├── archive/
│ └── Creator/
│ ├── #Creator 2026-03-06 Title.mp4
│ ├── #Creator 2026-03-06 Title_1.mp4
│ ├── .staging/
│ │ └── creator_oid_2026-03-06T12_00_00/
│ │ └── #Creator 2026-03-06 Title.ts
│ └── _failed/
│ └── creator_oid_2026-03-06T13_00_00/
│ └── #Creator 2026-03-06 Title.ts
├── config/
│ ├── .gitkeep
│ └── config.yaml
├── env # used by the bundled docker-compose.yaml
├── .env # used for local runs or custom docker run usage
├── logs/
└── docker-compose.yaml
Notes:
.stagingis an internal working area for active or just-finished sessions_failedkeeps raw files visible for manual inspection and recovery.stagingand_failedappear only when there is active or failed session data- final user-facing recordings live directly under
archive/<creator>/
Symptom:
- the app exits with a config migration error
Cause:
./config.yamlstill exists, but./config/config.yamldoes not
Fix:
- move
./config.yamlto./config/config.yaml - update Docker to mount
./config:/app/config
Check:
AUTH_TOKENis still validUSER_OIDis correct- creator ID is correct
- there is enough free disk space
- logs do not show API or connection failures
Behavior:
401usually meansAUTH_TOKENis missing, expired, or invalid403is treated as immediate blocked/private/paid access404can appear for a few seconds right after stream start; the downloader retries automatically before marking the current session blocked- timeout-like transport errors also retry automatically within the same download task
- after retries are exhausted, the current session is marked blocked or failed according to the final error
- a later new session from that creator can still be retried normally
Check:
- refresh
AUTH_TOKENif logs show401 - if repeated
403persists, confirm the stream is not paid/private for your account - if repeated
404persists after the automatic retries, wait a few seconds and confirm the stream actually remained live
Behavior:
- raw files are preserved under
archive/<creator>/_failed/<session_dir>/ - the app does not silently delete the session fragments
Check:
- FFmpeg availability in the runtime image
- file-system permissions
- available disk space
- the preserved raw
.tsfiles for manual recovery
Behavior:
- graceful shutdown may wait for active merge work to finish
- this is expected for a long-running recorder that prioritizes keeping completed raw work recoverable
Install dependencies:
poetry install --with devRun locally:
poetry run python main.pyRun tests:
poetry run pytestRun tests with coverage:
poetry run pytest --cov --cov-report=xmlrplay-live-dl/
├── .github/
│ └── workflows/
│ ├── coverage.yml
│ ├── main.yaml
│ └── test.yml
├── core/
│ ├── config.py
│ ├── constants.py
│ ├── download_merge_executor.py
│ ├── downloader.py
│ ├── env.py
│ ├── live_stream_monitor.py
│ ├── logger.py
│ ├── rplay.py
│ ├── scheduler.py
│ └── utils.py
├── models/
│ ├── config.py
│ ├── download.py
│ ├── env.py
│ └── rplay.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_config.py
│ ├── test_download_merge_executor.py
│ ├── test_download_models.py
│ ├── test_downloader.py
│ ├── test_env.py
│ ├── test_live_stream_monitor.py
│ ├── test_logger.py
│ ├── test_merge_flow.py
│ ├── test_models.py
│ ├── test_monitor_events.py
│ ├── test_rplay.py
│ ├── test_scheduler.py
│ └── test_utils.py
├── images/
│ ├── auth_token.png
│ ├── creator_oid.png
│ └── user_oid.png
├── config/
│ └── .gitkeep
├── .dockerignore
├── .env.example
├── .gitignore
├── LICENSE
├── config.yaml.example
├── docker-compose.yaml
├── Dockerfile
├── main.py
├── poetry.lock
├── pyproject.toml
├── requirements.txt
└── README.md
- 💬 Join the Discussions: Share ideas, ask questions, or discuss operational trade-offs.
- 🐛 Report Issues: Submit bugs, regressions, or feature requests.
- 💡 Submit Pull Requests: Review open PRs and contribute improvements.
Contribution Workflow
- Fork the repository to your own GitHub account.
- Clone locally:
git clone https://github.com/Zhen-Bo/rplay-live-dl
- Create a focused branch:
git checkout -b your-change
- Make your changes and keep the scope tight.
- Run the relevant verification before opening a PR:
poetry run pytest
- Commit with a clear message using Conventional Commit style when possible.
- Push your branch and open a pull request.
- Describe the change clearly with test evidence and any config or operational impact.
PR checklist:
- Follows the existing project style and naming conventions.
- Uses Conventional Commit style for commit messages when practical.
- Includes tests for behavior changes, or clearly explains why tests were not needed.
- Updates documentation and example config files when user-facing behavior changes.
- Calls out any breaking change, migration step, or deployment impact.
This project is licensed under the MIT License. See LICENSE for details.


