Skip to content

feat: Twitch VOD support#551

Merged
biodrone merged 17 commits into
stagingfrom
feat/twitch-vod-support
Apr 14, 2026
Merged

feat: Twitch VOD support#551
biodrone merged 17 commits into
stagingfrom
feat/twitch-vod-support

Conversation

@biodrone

@biodrone biodrone commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #231

Summary

  • Adds per-channel vod: true config toggle to download Twitch past broadcasts instead of live streams
  • New GetVods gRPC RPC enumerates VODs via yt-dlp; existing GetStream resolves each VOD URL through Streamlink/yt-dlp
  • SQLite database tracks download state with status lifecycle (downloading/completed/failed) and stale threshold detection for crash recovery
  • Separate -vod-out / -vod-move flags allow routing VOD downloads to different directories than live streams
  • VOD files use {user}_vod_{id}_{title}.mp4 naming with stream copy (no re-encoding)

Implementation Tasks

  • Task 1: Config — add vod and vod_limit fields to Streamer struct
  • Task 2: Proto — add GetVods RPC, VodRequest, VodResponse, VodInfo messages
  • Task 3: Python server — implement GetVods handler using yt-dlp for enumeration
  • Task 4: SQLite — create VOD tracking database with status-based lifecycle
  • Task 5: Go gRPC client — add getVods() function
  • Task 6: Download — add downloadVOD() with VOD-specific naming and DB tracking
  • Task 7: Main loop — wire VOD branch into tick loop with ShouldDownloadVOD gating
  • Task 8: Docker — add /app/data volume for SQLite persistence
  • Task 9: Docs — update README with VOD configuration, flags, and usage
  • Task 10: Integration test — add VOD download test phase

Test plan

  • TestParseConfig_VODFields — VOD config parsing
  • TestVodDB_InitAndClose — DB creation
  • TestVodDB_FullLifecycle — started → completed lifecycle
  • TestVodDB_FailedVODIsRetried — failed VODs are retried
  • TestVodDB_StaleDownloadIsRetried — stale downloads detected and retried
  • TestVodDB_DifferentVODsAreIndependent — VOD isolation
  • Integration test: VOD download phase added to tests/integration/run.sh
  • Manual: configure a channel with vod: true and verify VODs download and are tracked in DB

Summary by CodeRabbit

  • New Features

    • Twitch VOD downloads per-channel (vod: true) with optional vod_limit
    • New CLI flags: -data, -vod-out, -vod-move to persist and separate VOD output/move paths
    • Safer VOD filenames, stream-copy by default, retrying downloads and graceful shutdown wait for in-progress VODs
  • Documentation

    • README and Docker guidance updated for VOD setup, naming, directory semantics, and Twitch-only note
  • Tests

    • Added unit and integration tests covering VOD config, DB lifecycle, and end-to-end VOD downloads

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@biodrone has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 3 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d2a6c1a0-850d-402d-bba2-fd8d80794736

📥 Commits

Reviewing files that changed from the base of the PR and between c1126ad and d3b9df3.

📒 Files selected for processing (1)
  • tests/integration/run.sh
📝 Walkthrough

Walkthrough

Adds Twitch VOD download support: new CLI flags and Docker mount for persistent VOD data, per-channel YAML VOD settings, SQLite-backed VOD state tracking, gRPC VOD listing endpoint and client, yt-dlp + FFmpeg download flow, regenerated protobuf stubs, tests, and README updates.

Changes

Cohort / File(s) Summary
Docs & Examples
README.md, config/config.yml.example
Documented new CLI flags (-data, -vod-out, -vod-move), per-channel vod/vod_limit YAML entries, VOD control flow, Docker mount guidance, and Twitch-only VOD notes.
Config & Parsing Tests
config.go, config_reader_test.go
Added Streamer.VOD bool and Streamer.VODLimit int YAML fields; unit test TestParseConfig_VODFields validates parsing and defaults.
Main / CLI runtime
streamdl.go, entrypoint_client.sh
Added CLI flags for VOD paths and data dir, VOD-mode control flow (VodDB init, getVods, ClaimVOD, download goroutines, graceful shutdown wait), and create /app/data in entrypoint.
VOD DB & Tests
vod_db.go, vod_db_test.go
New SQLite-backed VodDB with Init/Close, ClaimVOD (with staleness logic), MarkVODCompleted/Failed; tests cover lifecycle, staleness, failure-retry, and isolation.
Download Logic
download_stream.go
Added sanitizeFilename and downloadVOD to prepare paths, run FFmpeg (injects -c:v copy -c:a copy -movflags +faststart, ensure -y), handle interrupts, move completed files, and update VodDB.
gRPC: Proto, Server, Client
protos/stream.proto, streamdl_proto_srv.py, grpc_client.go
New GetVods RPC and messages (VodRequest, VodInfo, VodResponse); Python server handler GetVods using yt-dlp to list VODs; Go client getVods and VodResult with timeout and mapped gRPC errors.
gRPC Generated Stubs
stream_pb2.py, stream_pb2_grpc.py
Regenerated stubs adding GetVods, runtime/version validation, client/server wiring for the new RPC, and compatibility checks.
Integration Tests & CI
tests/integration/docker-compose.integration.yml, tests/integration/run.sh, .coderabbit.yaml
Mount /app/data in integration compose; extended run script with VOD phase selecting a Twitch channel, enabling VOD in config, restarting client, and asserting VOD progress/completion; added review config excluding generated pb files.
Tooling & Dependencies
go.mod, pyproject.toml
Added/updated indirect Go modules and bumped protobuf runtime minimum; added grpcio-tools and tightened protobuf min version in Python tooling.
Misc (proto gen artifacts)
stream_pb2.py, stream_pb2_grpc.py
Updated generated Python protobuf/grpc modules to include new messages/methods and runtime checks.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Config as Config/YAML
    participant GrpcSrv as gRPC Server
    participant YtDlp as yt-dlp
    participant VodDB as SQLite VodDB
    participant FFmpeg as FFmpeg

    Client->>Config: read streamer (vod, vod_limit)
    Client->>GrpcSrv: GetVods(site, user, limit)
    GrpcSrv->>YtDlp: list videos (playlist metadata)
    YtDlp-->>GrpcSrv: vod metadata
    GrpcSrv-->>Client: VodResponse(vods)

    loop per VOD candidate
        Client->>VodDB: ClaimVOD(id, staleThreshold)
        VodDB-->>Client: claimed? (true/false)
        alt claimed
            Client->>GrpcSrv: GetStream("videos/"+id) → download URL
            GrpcSrv-->>Client: download URL
            Client->>FFmpeg: spawn ffmpeg (-c:v copy -c:a copy ...)
            FFmpeg-->>Client: exit success/failure
            alt success
                Client->>VodDB: MarkVODCompleted(id)
            else failure
                Client->>VodDB: MarkVODFailed(id)
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I hopped through configs with a thump and nod,
Claimed VODs in SQLite, tidy and odd,
yt-dlp hummed lists from a Twitchy shore,
FFmpeg stitched bytes — then hoppity, more!
Carrots cached in /app/data—hooray, download stored.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Twitch VOD support' directly and clearly summarizes the main feature addition in the changeset.
Description check ✅ Passed The PR description follows the template with issue reference (#231), comprehensive summary of changes, implementation tasks, and test plan.
Linked Issues check ✅ Passed The changeset implements all core requirements from issue #231: per-channel VOD config toggle, GetVods gRPC RPC with yt-dlp enumeration, SQLite tracking with status lifecycle and stale detection, and separate output directories.
Out of Scope Changes check ✅ Passed All changes are directly aligned with VOD support implementation. Minor updates to go.mod dependencies and CodeRabbit configuration are supporting changes within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/twitch-vod-support

Comment @coderabbitai help to get the list of available commands and usage tips.

Add ability to download past broadcasts (VODs) from Twitch channels.
Per-channel `vod: true` config toggle switches from live stream to VOD
mode, with a SQLite database tracking download state to avoid
re-downloading and handle crash recovery via stale threshold detection.

- Add GetVods gRPC RPC using yt-dlp for VOD enumeration
- Add SQLite VOD tracking with status-based lifecycle (downloading/completed/failed)
- Add downloadVOD function with stream copy and VOD-specific filenames
- Add -data, -vod-out, -vod-move flags for configurable paths
- Wire VOD branch into main tick loop with ShouldDownloadVOD gating
- Add VOD phase to integration test
- Update README and example config
@biodrone biodrone mentioned this pull request Apr 14, 2026
8 tasks
@biodrone biodrone marked this pull request as ready for review April 14, 2026 06:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 133-144: The README currently implies /app/data is the fixed VOD
DB location; update the wording to explicitly state that /app/data is the
default data directory and that it can be overridden with the -data flag (or
equivalent CLI/config option), and instruct users to mount the directory they
specify with -data rather than always mounting /app/data; reference the README
lines describing the SQLite DB path and the Docker volume snippet so you change
those sentences to mention the default and the configurable -data option.

In `@stream_pb2_grpc.py`:
- Around line 19-24: The RuntimeError message in stream_pb2_grpc.py concatenates
several string literals; remove the unnecessary f-string prefix from the second
literal (the one containing " but the generated code in stream_pb2_grpc.py
depends on") because it has no interpolation, leaving the other f-strings with
placeholders ({GRPC_VERSION}, {GRPC_GENERATED_VERSION}) unchanged so the
formatting still works.

In `@stream_pb2.py`:
- Around line 12-19: The generated protobuf runtime check in
_runtime_version.ValidateProtobufRuntimeVersion for 'stream.proto' requires
protobuf >=5.29.0, so update the pyproject.toml protobuf dependency to "protobuf
>=5.29.0,<6.0.0" (leave grpcio as-is since the existing >=1.80.0 already
satisfies the generated requirement); make this change to the project's
dependency manifest and run a quick install/lock to verify the new constraint is
applied.

In `@streamdl.go`:
- Around line 109-149: The VOD download goroutines started by
downloadVOD(streamer.User, ...) send on response but are not counted in the
shutdown drain (only len(urls) for live streams is waited on), so add VOD jobs
to the shutdown tracking: either increment the same live-stream tracking
structure (e.g., append a placeholder to urls) or maintain a shared counter /
sync.WaitGroup that you increment before launching downloadVOD and
decrement/send when the goroutine finishes; update the shutdown path to wait for
both live stream URLs and VOD jobs (or wait on the WaitGroup) so response sends
from VOD jobs are drained before os.Exit. Ensure the change touches the VOD loop
where downloadVOD is invoked and the shutdown/drain logic that currently uses
len(urls).
- Around line 67-72: Currently InitVodDB is called unconditionally during
startup (InitVodDB, vodDB, dataDir), which forces creation/opening of
streamdl.db even when VOD is not enabled and may fail on unwritable default
paths; change the startup flow to check the runtime/config flag that indicates
VOD tracking is enabled (the same config or function that controls vod: true
entries) and only call InitVodDB and defer vodDB.Close() when that flag is set,
otherwise skip DB initialization and the log.Infof message about the VOD DB
entirely; ensure any code paths that reference vodDB are gated by the same flag
so no nil usages occur.
- Around line 174-185: The rate-limit handling around getStream is unreachable
because the inner retry uses an else-if chain that prevents the outer "Rate
Limited Thrice" branch from ever executing; change the logic in the getStream ->
retry -> downloadStream sequence (references: getStream, urls, urlsMu,
downloader call downloadStream, streamer.User) to perform up to three explicit
attempts (e.g., a small loop or a helper that retries up to 3 times) and only
after the third consecutive "rate limited" error log "Rate Limited Thrice" and
skip; ensure each retry sleeps before the next attempt and that successful
attempts update urls/urlsMu and call downloadStream as now.

In `@tests/integration/run.sh`:
- Around line 254-263: The VOD test is flaky because it sets
VOD_CHANNEL="$LIVE_CHANNEL"; change run.sh so VOD_CHANNEL is not reused from
LIVE_CHANNEL but instead comes from a dedicated VOD fixture variable (e.g.
VOD_CHANNEL_FALLBACK or VOD_FIXTURE) or a prioritized list of known channels
with VODs: check an env var like VOD_CHANNEL_OVERRIDE first, then fall back to a
hardcoded reliable VOD channel name or list and select the first available;
update the config generation that writes config.yml (the block referencing
VOD_CHANNEL, quality, vod, vod_limit) to use this new VOD channel variable
(instead of LIVE_CHANNEL) so the VOD phase uses a channel that is guaranteed to
have past broadcasts.
- Around line 250-253: The test currently only removes OUTPUT_DIR outputs but
leaves the persisted VOD SQLite DB in SCRIPT_DIR/data, causing stale state to
skip Phase 5; update the script to also remove or reset the DB files under
"$SCRIPT_DIR/data" before Phase 5 (e.g., delete *.sqlite / *.db or the known VOD
DB filename) so the database state is fresh alongside the OUTPUT_DIR cleanup
(look for usages of OUTPUT_DIR and SCRIPT_DIR in the run.sh snippet to place the
removal).

In `@vod_db_test.go`:
- Around line 84-85: The test currently ignores returned errors from DB calls
like MarkVODStarted and MarkVODFailed (and other DB operations in this test), so
update each call to capture the error and fail the test on error (e.g., err :=
db.MarkVODStarted(...); if err != nil { t.Fatalf("MarkVODStarted failed: %v",
err) } or use require.NoError/AssertNoError), and do the same for MarkVODFailed
and any other DB/test helper calls in this file that drop errors so failures are
surfaced.

In `@vod_db.go`:
- Around line 54-59: Replace the separate check (ShouldDownloadVOD) and claim
(MarkVODStarted) flow with a single atomic DB operation that transitions the row
to "downloading"/"started" and then check rowsAffected to see if we won the
claim; specifically, in the code paths referencing ShouldDownloadVOD and
MarkVODStarted, execute an UPDATE downloaded_vods SET status='downloading',
started_at=? WHERE vod_id=? AND (status!='downloading' OR started_at<?) (using
the passed staleThreshold time for the started_at cutoff) and check
result.RowsAffected()==1 to indicate success; if the row may not exist, attempt
an INSERT ... ON CONFLICT(vod_id) DO UPDATE SET status='downloading',
started_at=EXCLUDED.started_at WHERE downloaded_vods.status!='downloading' OR
downloaded_vods.started_at<? and still check rowsAffected to determine claim
ownership; replace any prior separate QueryRow checks in ShouldDownloadVOD and
separate MarkVODStarted implementations with this single Exec+RowsAffected
pattern.
- Around line 100-121: MarkVODCompleted and MarkVODFailed currently treat
zero-row updates as success; change both to check the Exec result's RowsAffected
(from the result of v.db.Exec) and return an error when rowsAffected == 0 (e.g.,
sql.ErrNoRows or a descriptive fmt.Errorf("vod not found: %s", vodID)), logging
that as a failure; ensure you capture and check the Exec error first, then call
res.RowsAffected() and return/log appropriately in both MarkVODCompleted and
MarkVODFailed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8dd6296e-17d3-4029-ba56-0c9ff7e35738

📥 Commits

Reviewing files that changed from the base of the PR and between b4dfa30 and af000db.

⛔ Files ignored due to path filters (4)
  • go.sum is excluded by !**/*.sum
  • protos/stream.pb.go is excluded by !**/*.pb.go
  • protos/stream_grpc.pb.go is excluded by !**/*.pb.go
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • README.md
  • config.go
  • config/config.yml.example
  • config_reader_test.go
  • download_stream.go
  • entrypoint_client.sh
  • go.mod
  • grpc_client.go
  • protos/stream.proto
  • pyproject.toml
  • stream_pb2.py
  • stream_pb2_grpc.py
  • streamdl.go
  • streamdl_proto_srv.py
  • tests/integration/docker-compose.integration.yml
  • tests/integration/run.sh
  • vod_db.go
  • vod_db_test.go

Comment thread README.md
Comment thread stream_pb2_grpc.py
Comment on lines +19 to +24
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in stream_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "stream_pb2_grpc.py" -type f

Repository: dangeroustech/StreamDL

Length of output: 86


🏁 Script executed:

sed -n '19,24p' ./stream_pb2_grpc.py

Repository: dangeroustech/StreamDL

Length of output: 440


Remove extraneous f prefix from line 21 (Ruff F541).

Line 21 uses an f-string with no placeholder expressions, which violates the F541 rule.

Proposed fix
-        + f' but the generated code in stream_pb2_grpc.py depends on'
+        + ' but the generated code in stream_pb2_grpc.py depends on'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in stream_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
' but the generated code in stream_pb2_grpc.py depends on'
f' grpcio>={GRPC_GENERATED_VERSION}.'
f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
🧰 Tools
🪛 Ruff (0.15.9)

[error] 21-21: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@stream_pb2_grpc.py` around lines 19 - 24, The RuntimeError message in
stream_pb2_grpc.py concatenates several string literals; remove the unnecessary
f-string prefix from the second literal (the one containing " but the generated
code in stream_pb2_grpc.py depends on") because it has no interpolation, leaving
the other f-strings with placeholders ({GRPC_VERSION}, {GRPC_GENERATED_VERSION})
unchanged so the formatting still works.

Comment thread stream_pb2.py
Comment thread streamdl.go Outdated
Comment thread streamdl.go
Comment thread tests/integration/run.sh Outdated
Comment thread tests/integration/run.sh Outdated
Comment thread vod_db_test.go Outdated
Comment thread vod_db.go Outdated
Comment thread vod_db.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.coderabbit.yaml (1)

5-5: Consider removing redundant quotes.

The quotes around "staging" are unnecessary in YAML for simple strings without special characters.

✨ Proposed simplification
-      - "staging"
+      - staging
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.coderabbit.yaml at line 5, Remove the unnecessary surrounding quotes from
the string "staging" in the YAML list entry so it becomes an unquoted scalar;
locate the quoted item "staging" in the .coderabbit.yaml list and replace it
with staging (no quotes) to simplify the YAML and avoid redundant quoting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.coderabbit.yaml:
- Line 5: Remove the unnecessary surrounding quotes from the string "staging" in
the YAML list entry so it becomes an unquoted scalar; locate the quoted item
"staging" in the .coderabbit.yaml list and replace it with staging (no quotes)
to simplify the YAML and avoid redundant quoting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a510dbb-590c-4e82-83e5-739fb7e6205f

📥 Commits

Reviewing files that changed from the base of the PR and between af000db and 87adf3f.

📒 Files selected for processing (1)
  • .coderabbit.yaml

@biodrone

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

VOD downloads now use a WaitGroup so the shutdown path waits for
all in-progress VOD jobs before exiting, not just live streams.
Eliminates TOCTOU race by combining the check-and-claim into a single
SQL operation that uses ON CONFLICT with a WHERE clause and checks
RowsAffected. Also fixes dropped errors in tests.
Return an error when the VOD is not found in the database instead of
silently succeeding with zero rows affected.
Only create the SQLite database when a channel with vod:true is
encountered, avoiding failures on unwritable default paths when
VOD is not configured. Handles config reloads that add VOD channels.
Removes the data directory so the SQLite DB is fresh, preventing
previously tracked VODs from causing the test to skip downloads.
Use a known high-profile channel (kaicenat) that reliably has VODs
instead of reusing the live stream channel. Overridable via VOD_CHANNEL
env var.
Bump minimum protobuf version to match generated code requirements.
Add stream_pb2*.py to CodeRabbit path filters since they are
auto-generated and should not be reviewed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/integration/run.sh (1)

275-275: Tighten VOD artifact match to .mp4 to avoid false positives.

Line 275 currently matches any filename containing _vod_. Since the downloader writes VODs as .mp4, matching that extension makes pass/fail checks more reliable.

Proposed patch
-  VOD_FILE=$(find "$OUTPUT_DIR/complete" -name "*_vod_*" -size +0c 2>/dev/null | head -1) || true
+  VOD_FILE=$(find "$OUTPUT_DIR/complete" -name "*_vod_*.mp4" -size +0c 2>/dev/null | head -1) || true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/run.sh` at line 275, The VOD_FILE find pattern is too broad
(currently searching for "*_vod_*") and can pick up non-MP4 artifacts; update
the find invocation that sets VOD_FILE to only match MP4 VOD files by
restricting the name pattern to include the .mp4 extension (target the VOD_FILE
assignment using the existing variable and search directory OUTPUT_DIR/complete
so the script only picks files like *_vod_*.mp4).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/integration/run.sh`:
- Line 275: The VOD_FILE find pattern is too broad (currently searching for
"*_vod_*") and can pick up non-MP4 artifacts; update the find invocation that
sets VOD_FILE to only match MP4 VOD files by restricting the name pattern to
include the .mp4 extension (target the VOD_FILE assignment using the existing
variable and search directory OUTPUT_DIR/complete so the script only picks files
like *_vod_*.mp4).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0943bc62-a8b3-4e21-b77e-331029a27611

📥 Commits

Reviewing files that changed from the base of the PR and between 87adf3f and 2c02376.

📒 Files selected for processing (8)
  • .coderabbit.yaml
  • README.md
  • download_stream.go
  • pyproject.toml
  • streamdl.go
  • tests/integration/run.sh
  • vod_db.go
  • vod_db_test.go
✅ Files skipped from review due to trivial changes (1)
  • .coderabbit.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • pyproject.toml
  • README.md
  • vod_db_test.go
  • streamdl.go
  • vod_db.go
  • download_stream.go

@biodrone

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/integration/run.sh (1)

271-287: Phase 5 should not require full VOD completion within 180s.

VOD_TIMEOUT=180 plus a “completed file only” assertion is brittle for full-length VODs (see streamdl.go Line 113-150 and download_stream.go Line 485-490 flow). Consider making this configurable and treating “download started with non-zero growth” as success for this phase.

♻️ Suggested change
-VOD_TIMEOUT=180
+VOD_TIMEOUT="${VOD_TIMEOUT:-600}"
 VOD_FILE=""
+VOD_PROGRESS=""
 while [ $VOD_ELAPSED -lt $VOD_TIMEOUT ]; do
   VOD_FILE=$(find "$OUTPUT_DIR/complete" -name "*_vod_*.mp4" -size +0c 2>/dev/null | head -1) || true
   if [ -n "$VOD_FILE" ]; then
     break
   fi
+  VOD_PROGRESS=$(find "$OUTPUT_DIR/incomplete" -name "*_vod_*.mp4" -size +1000c 2>/dev/null | head -1) || true
+  if [ -n "$VOD_PROGRESS" ]; then
+    echo "--- VOD download started: $VOD_PROGRESS ---"
+    break
+  fi
   sleep 5
   VOD_ELAPSED=$((VOD_ELAPSED + 5))
 done

-if [ -z "$VOD_FILE" ]; then
-  echo "FAIL: No VOD file found after ${VOD_TIMEOUT}s"
+if [ -z "$VOD_FILE" ] && [ -z "$VOD_PROGRESS" ]; then
+  echo "FAIL: No VOD download activity found after ${VOD_TIMEOUT}s"
   $DC -f "$COMPOSE_FILE" logs client 2>&1 | tail -30
   exit 1
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/run.sh` around lines 271 - 287, The test currently requires
finding a completed VOD file within a hardcoded VOD_TIMEOUT=180 loop (variables
VOD_ELAPSED, VOD_FILE) which is brittle for long VODs; make the timeout
configurable (e.g., via env VOD_TIMEOUT or test flag) and change the success
condition to detect that a download has started by accepting a
non-zero-sized/increasing file (check file size growth across iterations) rather
than only a fully completed file; update the loop that uses VOD_ELAPSED/VOD_FILE
to read the configurable timeout and to treat a found file with size >0 and
observed growth between checks as success so Phase 5 passes earlier while full
completion can be validated later (this aligns with the flow in streamdl.go and
download_stream.go).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/run.sh`:
- Around line 255-257: Replace the single default VOD channel assignment
VOD_CHANNEL="${VOD_CHANNEL:-teampgp}" with logic that iterates a prioritized
candidate list (e.g., CANDIDATE_VOD_CHANNELS) and probes each channel for
published VODs; set VOD_CHANNEL to the first candidate that has VODs, and if
none are found, set a flag to skip Phase 5 (the Phase 5 execution block) so the
script exits or continues without running Phase 5. Update the VOD selection code
and the Phase 5 gating logic so Phase 5 only runs when VOD_CHANNEL is set to a
candidate that was verified to have VODs.

---

Nitpick comments:
In `@tests/integration/run.sh`:
- Around line 271-287: The test currently requires finding a completed VOD file
within a hardcoded VOD_TIMEOUT=180 loop (variables VOD_ELAPSED, VOD_FILE) which
is brittle for long VODs; make the timeout configurable (e.g., via env
VOD_TIMEOUT or test flag) and change the success condition to detect that a
download has started by accepting a non-zero-sized/increasing file (check file
size growth across iterations) rather than only a fully completed file; update
the loop that uses VOD_ELAPSED/VOD_FILE to read the configurable timeout and to
treat a found file with size >0 and observed growth between checks as success so
Phase 5 passes earlier while full completion can be validated later (this aligns
with the flow in streamdl.go and download_stream.go).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c24c5dc7-7450-43ed-a356-9d974f181bc8

📥 Commits

Reviewing files that changed from the base of the PR and between 2c02376 and 4d7b0ac.

📒 Files selected for processing (1)
  • tests/integration/run.sh

Comment thread tests/integration/run.sh Outdated
Accept in-progress downloads (>1KB in /incomplete) as proof the
pipeline works, not just completed files. Makes the test faster and
less flaky for long VODs. VOD_TIMEOUT is now configurable via env var.
Iterate a prioritized list of channels (teampgp first) and probe for
published VODs using yt-dlp before running Phase 5. Skip gracefully
if no candidates have VODs. Overridable via VOD_CHANNEL env var.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/integration/run.sh (2)

275-275: Quote variable in numeric comparison.

Shellcheck recommends quoting $VOD_TIMEOUT to prevent word splitting, even though the risk is minimal here.

🔧 Proposed fix
-while [ $VOD_ELAPSED -lt $VOD_TIMEOUT ]; do
+while [ "$VOD_ELAPSED" -lt "$VOD_TIMEOUT" ]; do
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/run.sh` at line 275, The numeric comparison in the while
loop uses an unquoted variable $VOD_TIMEOUT which can cause word-splitting or
empty-variable issues; update the condition while [ $VOD_ELAPSED -lt
$VOD_TIMEOUT ]; do to quote the right-hand operand (e.g., while [ "$VOD_ELAPSED"
-lt "$VOD_TIMEOUT" ]; do) and also consider quoting $VOD_ELAPSED to be safe,
ensuring both VOD_ELAPSED and VOD_TIMEOUT are treated as single numeric tokens
in tests/integration/run.sh.

281-286: Consider whether accepting in-progress downloads provides sufficient validation.

The test now passes when it finds a VOD download >1KB in the incomplete directory, without waiting for completion. While this speeds up the test and reduces flakiness from long VODs, it means the integration test doesn't verify that:

  • VODs can be downloaded to completion
  • The completed file is moved to the correct directory
  • The VOD database marks the download as completed

This trade-off prioritizes test speed over coverage. If this is intentional, consider adding a comment explaining the rationale, or alternatively, use a smaller timeout for completed files before falling back to checking in-progress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/run.sh` around lines 281 - 286, The current check in
tests/integration/run.sh uses VOD_PROGRESS (find in "$OUTPUT_DIR/incomplete"
>1KB) and accepts an in-progress file, which skips validating completion, file
move and DB state; either (A) add a short wait-and-check sequence before falling
back: first poll for a completed VOD in "$OUTPUT_DIR/completed" with a small
timeout, verify the final filename exists and then verify the VOD DB record is
marked completed, and only if that fails fall back to the existing in-progress
check, or (B) if accepting in-progress is intentional, add a clear comment above
the VOD_PROGRESS block explaining the trade-off and why the test does not wait
for completion (including references to "$OUTPUT_DIR/incomplete",
"$OUTPUT_DIR/completed", and the VOD DB completion check).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/integration/run.sh`:
- Line 275: The numeric comparison in the while loop uses an unquoted variable
$VOD_TIMEOUT which can cause word-splitting or empty-variable issues; update the
condition while [ $VOD_ELAPSED -lt $VOD_TIMEOUT ]; do to quote the right-hand
operand (e.g., while [ "$VOD_ELAPSED" -lt "$VOD_TIMEOUT" ]; do) and also
consider quoting $VOD_ELAPSED to be safe, ensuring both VOD_ELAPSED and
VOD_TIMEOUT are treated as single numeric tokens in tests/integration/run.sh.
- Around line 281-286: The current check in tests/integration/run.sh uses
VOD_PROGRESS (find in "$OUTPUT_DIR/incomplete" >1KB) and accepts an in-progress
file, which skips validating completion, file move and DB state; either (A) add
a short wait-and-check sequence before falling back: first poll for a completed
VOD in "$OUTPUT_DIR/completed" with a small timeout, verify the final filename
exists and then verify the VOD DB record is marked completed, and only if that
fails fall back to the existing in-progress check, or (B) if accepting
in-progress is intentional, add a clear comment above the VOD_PROGRESS block
explaining the trade-off and why the test does not wait for completion
(including references to "$OUTPUT_DIR/incomplete", "$OUTPUT_DIR/completed", and
the VOD DB completion check).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 75334b66-8399-4157-b2f5-18a49baa764f

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7b0ac and 8438861.

📒 Files selected for processing (1)
  • tests/integration/run.sh

@biodrone

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@biodrone

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/integration/run.sh (1)

343-346: Include server logs in the VOD timeout path.

A timeout here can come from server-side VOD enumeration or URL resolution, but this branch only dumps client logs. Mirroring the live-phase diagnostics will make VOD failures much easier to debug.

♻️ Suggested improvement
 if [ -z "$VOD_FILE" ] && [ -z "$VOD_PROGRESS" ]; then
   echo "FAIL: No VOD download activity found after ${VOD_TIMEOUT}s"
+  echo ""
+  echo "--- Client logs ---"
   $DC -f "$COMPOSE_FILE" logs client 2>&1 | tail -30
+  echo ""
+  echo "--- Server logs ---"
+  $DC -f "$COMPOSE_FILE" logs server 2>&1 | tail -30
   exit 1
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/run.sh` around lines 343 - 346, The timeout branch that
checks VOD_FILE and VOD_PROGRESS and currently only dumps client logs should
also capture server logs to aid debugging; update the failure path where
VOD_TIMEOUT is used (the block that runs $DC -f "$COMPOSE_FILE" logs client ...)
to also run $DC -f "$COMPOSE_FILE" logs server (and optionally other relevant
services) and include their last lines (e.g., tail -30) so server-side VOD
enumeration/URL resolution errors are visible alongside client logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/run.sh`:
- Around line 269-305: The probe loop currently treats every ERROR the same as
NO_VODS and allows the script to exit 0; update the logic around the for-loop
that iterates CANDIDATE_VOD_CHANNELS so it tracks whether any probe completed
successfully (e.g., set a flag like PROBE_SUCCEEDED when RESULT is not "ERROR")
while still setting VOD_CHANNEL when RESULT == "HAS_VODS"; after the loop, if
VOD_CHANNEL is empty but PROBE_SUCCEEDED is false, fail the test (exit non-zero)
to surface probe failures, otherwise preserve the existing skip path when probes
ran but returned only NO_VODS.

---

Nitpick comments:
In `@tests/integration/run.sh`:
- Around line 343-346: The timeout branch that checks VOD_FILE and VOD_PROGRESS
and currently only dumps client logs should also capture server logs to aid
debugging; update the failure path where VOD_TIMEOUT is used (the block that
runs $DC -f "$COMPOSE_FILE" logs client ...) to also run $DC -f "$COMPOSE_FILE"
logs server (and optionally other relevant services) and include their last
lines (e.g., tail -30) so server-side VOD enumeration/URL resolution errors are
visible alongside client logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 407c521e-8449-4e1a-99b3-bf59dd1843bc

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7b0ac and c1126ad.

📒 Files selected for processing (1)
  • tests/integration/run.sh

Comment thread tests/integration/run.sh
Include server logs alongside client logs when VOD download times out.
Fail the test if all VOD probes errored (connectivity issue) rather
than skipping gracefully, which is reserved for when probes succeed
but no channels have VODs.
@biodrone

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@biodrone biodrone merged commit ecd7cca into staging Apr 14, 2026
8 checks passed
@biodrone biodrone deleted the feat/twitch-vod-support branch April 14, 2026 08:28
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant