Skip to content

fix(lavalink): delegate YouTube deciphering to a yt-cipher sidecar - #41

Merged
Magalvo merged 1 commit into
mainfrom
fix/youtube-remote-cipher
Aug 22, 2026
Merged

fix(lavalink): delegate YouTube deciphering to a yt-cipher sidecar#41
Magalvo merged 1 commit into
mainfrom
fix/youtube-remote-cipher

Conversation

@Magalvo

@Magalvo Magalvo commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Problem

#40 worked: the TV client now clears the playability check that YouTube's 2026-08-18 User-Agent change was failing. Playback moved one step further and died there instead:

Client [TVHTML5] failed: Must find sig function from script: /s/player/2574220e/player_embed.vflset/el_GR/base.js
Client [ANDROID_VR] failed: This video requires login.
Client [WEB] failed: No supported audio streams available, available types:
Client [WEB_EMBEDDED_PLAYER] failed: Video player configuration error

OAuth applied, client accepted, formats listed — and then the plugin's regex-based extraction cannot find the signature function in the current player script, so there is no playable URL. This is the last step before audio.

Fix

Upstream's standing answer to this error is a remote cipher server, not a plugin fix. The maintainer says so directly in #225 ("the consensus is to use a remote cipher server"), and the plugin README documents the remoteCipher block for exactly this.

This adds yt-cipher to compose.yml on the private network, points plugins.youtube.remoteCipher at it, and shares a YT_CIPHER_PASSWORD between the two sides.

OVERRIDE_PLAYER_VARIANT: IAS is the part that matters, not just running the service: upstream reports only the IAS player variant works consistently, and player_embed — the variant the plugin asks for, and the one named in the error above — is precisely the one that fails.

Verification

Run against this repository's own compose.yml and application.yml, with a throwaway password and no OAuth token at all:

  • Lavalink logs Using remote cipher server with URL "http://yt-cipher:8001", resolving the sidecar by service name over the private network.
  • yt-cipher logs the cache miss and fetch of player_ias.vflset/.../base.js — the IAS variant, not the embed one.
  • Six unrelated videos — kJQP7kiw5Fk, lp0drugrSM4, 8CFh_-qtzeg, YC-QnlIEbJU, xcFbyuzd9S8, CcNo07Xp8aQ — all reach TrackStartEvent and run with no TrackExceptionEvent. The same stack without the sidecar fails all six at the signature stage.

The six-video set is deliberate rather than decorative: #226 documents that dQw4w9WgXcQ passes even on broken builds, so validating against a single video proves nothing.

Notes

  • No healthcheck on the service. The image is a compiled Deno binary on distroless — entrypoint /app/server, user nonroot, with no shell, curl, nc or deno CLI to run a check with (verified by inspecting the image). If it is unreachable, Lavalink logs a cipher error per track.
  • :master is a moving tag because the project publishes no versioned releases. Pin a digest if reproducible deploys matter more than picking up its fixes automatically.
  • YT_CIPHER_PASSWORD is a shared secret between two containers on a private network, not a Google credential. Empty disables auth on the sidecar, which is survivable only because it is never published.

Deploying

git pull && docker compose up -d

Set YT_CIPHER_PASSWORD in .env first (openssl rand -hex 32). up -d alone brings up the new service and recreates Lavalink for the config change; the bot is untouched.

🤖 Generated with Claude Code

With the TV User-Agent fix in place, playback got past the playability check and died one
step later:

  Client [TVHTML5] failed: Must find sig function from script:
  /s/player/2574220e/player_embed.vflset/el_GR/base.js

The plugin's regex-based extraction cannot find the signature function in the current
player script, so a valid OAuth token and an accepted client still yield no stream.
Upstream's standing answer to this error is a remote cipher server rather than a plugin
fix - the maintainer says so directly in #225 - and the README documents the
`remoteCipher` block for exactly this.

Adds yt-cipher to compose.yml on the private network, wires `plugins.youtube.remoteCipher`
at it, and shares a `YT_CIPHER_PASSWORD` between the two. `OVERRIDE_PLAYER_VARIANT: IAS`
is what makes it work: upstream reports only the IAS variant is consistently usable, and
`player_embed` - the variant the plugin asks for - is the one that fails.

Verified end to end against this repository's own compose.yml and application.yml, with a
throwaway password and no OAuth token at all: six unrelated videos
(kJQP7kiw5Fk, lp0drugrSM4, 8CFh_-qtzeg, YC-QnlIEbJU, xcFbyuzd9S8, CcNo07Xp8aQ) all reach
TrackStart and run without a TrackExceptionEvent, where the same stack without the sidecar
fails all six at the signature stage. The six-video set is deliberate: #226 documents that
dQw4w9WgXcQ passes even on broken builds, so validating against one video proves nothing.

No healthcheck on the service: the image is a compiled Deno binary on distroless, with no
shell, curl, nc or deno CLI to run a check with - verified by inspecting the image. Lavalink
logs a cipher error per track if it is unreachable, and it is pinned only by the moving
:master tag because the project publishes no versioned releases; both are noted in the
compose comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Magalvo
Magalvo merged commit 1d40ba8 into main Aug 22, 2026
2 checks passed
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