❯ An automated RPlay live recorder designed for long-running Docker deployments.
Built with Docker, Python, Poetry, Pydantic, yt-dlp, and FFmpeg.
- 📝 Description
⚠️ 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.
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
- flat archive layout with timestamp-prefixed
.tsfiles per session - 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 leaves raw
.tsfiles in place for manual recovery - fail-fast startup validation for legacy config path upgrades
- Docker-first deployment for long-running operation
- Create your environment file: copy
.env.exampleto.env. - 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 -fProduction:
- Docker
- valid RPlay account credentials
- stable network connectivity
- enough disk space for
.tsrecordings 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. Both local runs and the bundled docker-compose.yaml use .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 mounts
.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 - a timestamp prefix (
YYYYMMDD_HHMMSS_) is derived fromrecording_started_atin machine local time - raw files are written directly to
archive/<creator>/using this prefix for isolation
- each live stream gets a session key based on
-
Download raw transport stream files
- yt-dlp writes raw outputs as
.tsdirectly intoarchive/<creator>/ - each download task uses a
10-second socket timeout - transient task failures automatically retry up to
3attempts total with exponential backoff HTTP 404on the stream playlist is retried with exponential backoff before the session is marked blockedHTTP 403is still treated as immediate blocked/private accessHTTP 401is treated as an authentication failure instead of a blocked session- raw filenames carry the session timestamp prefix, for example:
20260306_120000_#Creator 2026-03-06 Title.ts20260306_120000_#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 inarchive/<creator>/matching the session prefix 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
.tsfiles matching the session prefix are deleted fromarchive/<creator>/ - on merge failure, the
.tsfiles remain inarchive/<creator>/for manual inspection and recovery
- on success, the
-
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 use a clean 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
Raw .ts files carry a timestamp prefix for session isolation:
20260306_120000_#Creator 2026-03-06 Title.ts- prefix format is
YYYYMMDD_HHMMSS_in machine local time - prefix uniquely identifies the recording session; files from different sessions never collide
# 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
│ ├── 20260306_120000_#Creator 2026-03-06 Title.ts ← active or failed session
│ └── 20260306_130000_#Creator 2026-03-06 Title.ts ← active or failed session
├── config/
│ ├── .gitkeep
│ └── config.yaml
├── .env # credentials and runtime settings
├── logs/
└── docker-compose.yaml
Notes:
.tsfiles with a timestamp prefix are either active downloads or unmerged fragments from a failed session- on successful merge the matching
.tsfiles are deleted automatically - on merge failure the
.tsfiles remain in place for manual inspection and recovery - 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:
- the
.tsfiles matching the failed session prefix remain inarchive/<creator>/ - 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 inarchive/<creator>/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.


