Skip to content

fix: use yt-dlp youtube search for spotify-sourced tracks - #610

Merged
LucasSantana-Dev merged 4 commits into
mainfrom
fix/spotify-stream-wrong-version
Apr 14, 2026
Merged

LucasSantana-Dev merged 4 commits into
mainfrom
fix/spotify-stream-wrong-version

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Apr 14, 2026 •

Copy link
Copy Markdown
Owner

Problem

When a user plays a direct Spotify URL (e.g. `/play https://open.spotify.com/track/...\`), the bot plays a wrong version — remix, cover, or live recording — instead of the original.

Root cause: `open.spotify.com` was in `ALLOWED_YTDLP_DOMAINS`, so yt-dlp was attempted against the Spotify URL (which always fails — Spotify is DRM-protected). The bridge then fell back to a bare SoundCloud search like `"Title Artist"` with no version qualifiers, so SoundCloud's first result was whatever it ranked highest — often a remix.

Fix

  • Remove `open.spotify.com` from `ALLOWED_YTDLP_DOMAINS` — yt-dlp can never stream Spotify URLs
  • Add `streamViaYtDlpSearch()` — spawns yt-dlp with a `ytsearch1:` query to search YouTube directly
  • Update `createResilientStream` — for Spotify-sourced tracks, try `ytsearch1:"Title Artist official audio"` via yt-dlp before falling back to SoundCloud

Priority order for Spotify tracks is now:

  1. yt-dlp YouTube search (`"Title Artist official audio"`) → finds official YouTube upload
  2. SoundCloud full query fallback (unchanged)
  3. SoundCloud title-only / core title fallbacks (unchanged)

Tests

  • `streamViaYtDlpSearch`: 3 new tests (success, empty query rejection, exit code error)
  • `createResilientStream`: 2 new Spotify-path tests
  • Updated domain test: removed `open.spotify.com` from allowed list, added explicit rejection test

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Spotify tracks now use intelligent search-based streaming with automatic fallback to SoundCloud if the primary method fails, improving reliability and compatibility.
  • Bug Fixes

    • Removed problematic direct URL processing for Spotify sources.
  • Tests

    • Extended test coverage for search-based streaming strategies and Spotify source detection.

Spotify URLs cannot be streamed directly by yt-dlp (DRM), so the bridge
was always failing and falling back to a bare SoundCloud search like
"Title Artist" — which finds remixes and covers.

- Remove open.spotify.com from ALLOWED_YTDLP_DOMAINS (yt-dlp always fails)
- Add streamViaYtDlpSearch() for ytsearch1: YouTube queries via yt-dlp
- In createResilientStream, Spotify-sourced tracks now try yt-dlp with
  "Title Artist official audio" before falling back to SoundCloud
- Tests: add streamViaYtDlpSearch suite + Spotify bridge path cases
@vercel

vercel Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Ready Ready Preview, Comment Apr 14, 2026 5:02pm

@github-actions github-actions Bot added the bot label Apr 14, 2026
@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@LucasSantana-Dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 56 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 31 minutes and 56 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d572860-f737-4a0b-b1e4-fc855d349e6f

📥 Commits

Reviewing files that changed from the base of the PR and between 5aa7dbb and dede2f6.

📒 Files selected for processing (1)
  • packages/bot/src/handlers/player/playerFactory.ts
📝 Walkthrough

Walkthrough

The PR adds Spotify URL detection and specialized handling to the audio streaming pipeline. When Spotify URLs are detected, the system routes them through a new streamViaYtDlpSearch function that performs YouTube searches via yt-dlp instead of direct streaming, with SoundCloud search as a fallback.

Changes

Cohort / File(s) Summary
Test Coverage Expansion
packages/bot/src/handlers/player/playerFactory.bridge.spec.ts
Added import for new streamViaYtDlpSearch function. Removed Spotify domain from allowlist tests for streamViaYtDlp and added explicit rejection test. Introduced comprehensive test suite for streamViaYtDlpSearch covering yt-dlp spawning with ytsearch1 queries, empty query rejection, and error handling. Extended createResilientStream tests to validate Spotify URL detection, YouTube search fallback, and SoundCloud chaining.
Streaming Logic Enhancement
packages/bot/src/handlers/player/playerFactory.ts
Removed open.spotify.com from ALLOWED_YTDLP_DOMAINS allowlist. Added new streamViaYtDlpSearch export that spawns yt-dlp with ytsearch1:<query> arguments and enforces 20s timeout. Modified createResilientStream to detect Spotify URLs, bypass direct yt-dlp streaming for Spotify tracks, and route them through YouTube search via the new function before falling back to SoundCloud. Updated logging formatting for clarity.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant createResilientStream
    participant isSpotifyDetection as Spotify Detection
    participant streamViaYtDlpSearch
    participant ytdlpYoutube as yt-dlp (ytsearch1)
    participant StreamSoundCloud as SoundCloud Fallback

    Client->>createResilientStream: Track with Spotify URL
    createResilientStream->>isSpotifyDetection: Check if open.spotify.com
    isSpotifyDetection-->>createResilientStream: true
    createResilientStream->>streamViaYtDlpSearch: Query: title + author + "official audio"
    streamViaYtDlpSearch->>ytdlpYoutube: Spawn with ytsearch1:<query>
    alt YouTube Search Success
        ytdlpYoutube-->>streamViaYtDlpSearch: Stream first chunk
        streamViaYtDlpSearch-->>createResilientStream: Readable stream
        createResilientStream-->>Client: Audio stream
    else YouTube Search Fails
        ytdlpYoutube-->>streamViaYtDlpSearch: Exit non-zero / timeout
        streamViaYtDlpSearch-->>createResilientStream: Rejection
        createResilientStream->>StreamSoundCloud: Fallback to SoundCloud search
        StreamSoundCloud-->>createResilientStream: Readable stream
        createResilientStream-->>Client: Audio stream
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • LucasSantana-Dev/Lucky#581: Modifies Spotify extractor registration and search support, complementing the Spotify-specific routing added in this PR.
  • LucasSantana-Dev/Lucky#515: Adjusts fallback priority to route Spotify-sourced tracks through YouTube before SoundCloud, directly aligns with the new Spotify handling strategy.
  • LucasSantana-Dev/Lucky#550: Introduces provider health checks to optimize the fallback chain in createResilientStream, affecting the same fallback logic extended by this PR.

Suggested labels

bot, size/l

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the main change: removing Spotify from ALLOWED_YTDLP_DOMAINS and adding streamViaYtDlpSearch to route Spotify tracks through YouTube search instead of direct yt-dlp.

✏️ 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 fix/spotify-stream-wrong-version

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 (2)
packages/bot/src/handlers/player/playerFactory.ts (1)

283-347: Consider extracting shared yt-dlp spawn logic to reduce duplication.

streamViaYtDlpSearch shares ~90% of its implementation with streamViaYtDlp (spawn setup, settled guard, stderr collection, PassThrough piping, timeout handling). A helper function could reduce duplication and ensure both paths stay in sync.

This is not blocking given the code works correctly, but worth considering for maintainability.

♻️ Example helper extraction
function spawnYtDlpStream(
    args: string[],
    timeoutMs: number,
    errorPrefix: string,
): Promise<Readable> {
    return new Promise<Readable>((resolve, reject) => {
        const proc = spawn('yt-dlp', args, { stdio: ['ignore', 'pipe', 'pipe'] })
        
        const timeout = setTimeout(() => {
            proc.kill()
            reject(new Error(`${errorPrefix}: timed out`))
        }, timeoutMs)

        const stderrChunks: Buffer[] = []
        proc.stderr!.on('data', (chunk: Buffer) => stderrChunks.push(chunk))

        let settled = false
        // ... rest of shared logic
    })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/handlers/player/playerFactory.ts` around lines 283 - 347,
streamViaYtDlpSearch duplicates almost all logic from streamViaYtDlp; extract
the shared spawn/timeout/stderr/settled/PassThrough piping logic into a helper
(e.g. spawnYtDlpStream(args: string[], timeoutMs: number, errorPrefix: string):
Promise<Readable>) and have both streamViaYtDlpSearch and streamViaYtDlp call it
with their specific yt-dlp args (like `ytsearch1:${query}` vs a direct URL), a
20_000 timeout, and distinct errorPrefix strings; move the spawn call,
stderrChunks collection, settled guard, timeout clear, proc.once('error'),
proc.once('close') handling, and the firstChunk piping into that helper so both
functions simply build args and return spawnYtDlpStream(...).
packages/bot/src/handlers/player/playerFactory.bridge.spec.ts (1)

396-430: Consider adding tests for spawn error and timeout scenarios.

The test suite covers the main cases, but streamViaYtDlp has additional edge-case tests (spawn error event at lines 355-369, timeout, double-settle race guard) that would provide more confidence for streamViaYtDlpSearch as well.

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

In `@packages/bot/src/handlers/player/playerFactory.bridge.spec.ts` around lines
396 - 430, Add tests to cover spawn `error` event, timeout, and double-settle
race for streamViaYtDlpSearch: reuse the existing spawnMock helpers (e.g.,
makeSpawnError, makeSpawnSuccess/makeSpawnTimeout or create a mock that emits an
'error' event) to add three specs mirroring streamViaYtDlp tests—(1) simulate
the child process emitting an 'error' and assert the promise rejects with that
error, (2) simulate a hung/slow child or a makeSpawnTimeout to trigger the
timeout path and assert rejection with a timeout message, and (3) simulate both
an exit/error/timeout race to ensure streamViaYtDlpSearch only resolves/rejects
once (double-settle guard) by asserting spawnMock is called once and the
returned promise settles exactly once; reference streamViaYtDlpSearch,
spawnMock, and your helper factories
(makeSpawnError/makeSpawnSuccess/makeSpawnTimeout) when adding the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bot/src/handlers/player/playerFactory.ts`:
- Around line 400-421: The Spotify branch builds ytQuery from
cleanSearchQuery(cleanedTitle, cleanedAuthor) and can produce an
empty/whitespace-only query; update the isSpotifyUrl block (around ytQuery,
cleanSearchQuery, and streamViaYtDlpSearch usage) to trim and check the cleaned
query before calling streamViaYtDlpSearch, and if it's empty skip the YouTube
search (log a warning via warnLog/infoLog mentioning the empty cleaned query) so
execution falls through to the existing SoundCloud fallback; ensure you
reference the same variables (ytQuery, cleanedTitle, cleanedAuthor) and preserve
the existing try/catch behavior only when a non-empty query is present.

---

Nitpick comments:
In `@packages/bot/src/handlers/player/playerFactory.bridge.spec.ts`:
- Around line 396-430: Add tests to cover spawn `error` event, timeout, and
double-settle race for streamViaYtDlpSearch: reuse the existing spawnMock
helpers (e.g., makeSpawnError, makeSpawnSuccess/makeSpawnTimeout or create a
mock that emits an 'error' event) to add three specs mirroring streamViaYtDlp
tests—(1) simulate the child process emitting an 'error' and assert the promise
rejects with that error, (2) simulate a hung/slow child or a makeSpawnTimeout to
trigger the timeout path and assert rejection with a timeout message, and (3)
simulate both an exit/error/timeout race to ensure streamViaYtDlpSearch only
resolves/rejects once (double-settle guard) by asserting spawnMock is called
once and the returned promise settles exactly once; reference
streamViaYtDlpSearch, spawnMock, and your helper factories
(makeSpawnError/makeSpawnSuccess/makeSpawnTimeout) when adding the tests.

In `@packages/bot/src/handlers/player/playerFactory.ts`:
- Around line 283-347: streamViaYtDlpSearch duplicates almost all logic from
streamViaYtDlp; extract the shared spawn/timeout/stderr/settled/PassThrough
piping logic into a helper (e.g. spawnYtDlpStream(args: string[], timeoutMs:
number, errorPrefix: string): Promise<Readable>) and have both
streamViaYtDlpSearch and streamViaYtDlp call it with their specific yt-dlp args
(like `ytsearch1:${query}` vs a direct URL), a 20_000 timeout, and distinct
errorPrefix strings; move the spawn call, stderrChunks collection, settled
guard, timeout clear, proc.once('error'), proc.once('close') handling, and the
firstChunk piping into that helper so both functions simply build args and
return spawnYtDlpStream(...).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 084cf575-eb1a-4b95-a550-3bb34d4e03c5

📥 Commits

Reviewing files that changed from the base of the PR and between 41c8e1e and 5aa7dbb.

📒 Files selected for processing (2)
  • packages/bot/src/handlers/player/playerFactory.bridge.spec.ts
  • packages/bot/src/handlers/player/playerFactory.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Quality Gates
  • GitHub Check: SonarCloud Scan
🔇 Additional comments (7)
packages/bot/src/handlers/player/playerFactory.bridge.spec.ts (3)

57-61: LGTM!

Import correctly updated to include streamViaYtDlpSearch for the new test coverage.


327-332: LGTM!

Good explicit test to ensure Spotify URLs are rejected by streamViaYtDlp since yt-dlp cannot stream DRM-protected Spotify content.


562-597: LGTM!

Good coverage for the Spotify-specific routing: verifies that Spotify URLs bypass direct yt-dlp streaming in favor of YouTube search, and that SoundCloud fallback works when the search fails.

packages/bot/src/handlers/player/playerFactory.ts (4)

77-78: LGTM!

Log message formatting improvements. No functional changes.

Also applies to: 85-86, 104-106, 113-115


366-377: LGTM!

Spotify URL detection and debug logging are correctly implemented. The includes('open.spotify.com') check is sufficient for user-facing Spotify track URLs.


380-398: LGTM!

Correctly guards the direct yt-dlp path to skip Spotify URLs, which cannot be streamed directly due to DRM.


349-359: LGTM!

Docstring accurately reflects the updated fallback chain.

Comment thread packages/bot/src/handlers/player/playerFactory.ts
Cleanup from rebase conflict - remove duplicate old implementation.
@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit 14e9fd4 into main Apr 14, 2026
12 checks passed
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
* fix: use yt-dlp youtube search for spotify-sourced tracks

Spotify URLs cannot be streamed directly by yt-dlp (DRM), so the bridge
was always failing and falling back to a bare SoundCloud search like
"Title Artist" — which finds remixes and covers.

- Remove open.spotify.com from ALLOWED_YTDLP_DOMAINS (yt-dlp always fails)
- Add streamViaYtDlpSearch() for ytsearch1: YouTube queries via yt-dlp
- In createResilientStream, Spotify-sourced tracks now try yt-dlp with
  "Title Artist official audio" before falling back to SoundCloud
- Tests: add streamViaYtDlpSearch suite + Spotify bridge path cases

* fix: remove duplicate streamviaytdlpsearch function

Cleanup from rebase conflict - remove duplicate old implementation.

* chore: suppress sonarcloud s4036 hotspot on yt-dlp spawn

* refactor: delegate streamviaytdlpsearch to streamviaytdlp
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/spotify-stream-wrong-version branch May 23, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — dede2f64 Deployed Apr 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant