Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ SPOTIFY_COUNTRY_CODE=PT
# Full procedure: docs/canonical/operations.md.
YOUTUBE_OAUTH_REFRESH_TOKEN=

# Shared secret between Lavalink and the yt-cipher sidecar (see compose.yml), which deciphers
# YouTube's signature challenges. Both sides read this same variable, so any value works as
# long as it matches - generate one with `openssl rand -hex 32`. Leaving it empty disables
# auth on the sidecar; that is survivable only because it is never published off the private
# Docker network. It is not a YouTube or Google credential.
YT_CIPHER_PASSWORD=

# Runtime
BOT_STATUS_TEXT=música | /play
LOG_LEVEL=info
Expand Down
35 changes: 35 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ services:
# for the first boot: Lavalink then prints a device-login code to link a burner account.
# See docs/canonical/operations.md.
YOUTUBE_OAUTH_REFRESH_TOKEN: "${YOUTUBE_OAUTH_REFRESH_TOKEN:-}"
# Shared with the yt-cipher service below. Empty disables auth on it, which is tolerable
# only because it is never published off this private network.
YT_CIPHER_PASSWORD: "${YT_CIPHER_PASSWORD:-}"
depends_on:
# service_started, not service_healthy: yt-cipher's image is distroless (see below).
# Lavalink calls it lazily per track, so a brief head start is all this needs to buy.
yt-cipher:
condition: service_started
volumes:
- ./lavalink/application.yml:/opt/Lavalink/application.yml:ro
healthcheck:
Expand Down Expand Up @@ -65,6 +73,33 @@ services:
networks:
- dijay-private

# Deciphers YouTube's signature/n-parameter challenges for Lavalink. Not optional: the
# plugin's own regex-based extraction fails on the current player script
# ("Must find sig function from script: .../player_embed.vflset/<locale>/base.js"), which
# blocks playback on the very last step even with OAuth working. Upstream's answer to that
# error is this service - see docs/canonical/operations.md.
yt-cipher:
# :master is a moving tag because the project publishes no versioned releases. Pin a
# digest here if reproducible deploys matter more than picking up its fixes automatically.
image: ghcr.io/kikkia/yt-cipher:master
restart: unless-stopped
environment:
API_TOKEN: "${YT_CIPHER_PASSWORD:-}"
# Upstream: only the IAS player variant works consistently, and the variant the plugin
# asks for (player_embed) is exactly the one that fails. Forcing IAS is the fix.
OVERRIDE_PLAYER_VARIANT: IAS
# No healthcheck: 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. Lavalink logs a cipher error per track if it is down.
mem_limit: 1g
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
networks:
- dijay-private

networks:
dijay-private:
driver: bridge
Expand Down
29 changes: 28 additions & 1 deletion docs/canonical/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ one of two distinct ways — check the Lavalink logs to tell them apart, since t

**Resolution fails** (`/play` replies "Não encontrei nenhuma faixa", the bot never joins):
Lavalink logs show `ScriptExtractionException: Must find sig function`. YouTube changed its
player script and the plugin's own scraping is outdated.
player script and the plugin's own scraping is outdated. Deciphering is delegated to the
`yt-cipher` sidecar precisely so this class of breakage is absorbed there — see the section
below before reaching for a plugin bump.

**Resolution succeeds but playback is silent** (a track title shows, the bot joins, no audio):
Lavalink logs show a `TrackExceptionEvent` with something like `This video requires login` or
Expand Down Expand Up @@ -140,6 +142,31 @@ touched `src/` is not live until:
Check `docker compose ps` — the `CREATED` column shows the image age. A bot container created
long before the last deploy is running old code.

### The yt-cipher sidecar

`compose.yml` runs `yt-cipher`, which deciphers YouTube's signature and n-parameter challenges
on Lavalink's behalf (`plugins.youtube.remoteCipher` in `application.yml`). It is not optional.
The plugin's own regex-based extraction fails on the current player script:

`Must find sig function from script: /s/player/<id>/player_embed.vflset/<locale>/base.js`

which lands on the very last step of playback — OAuth working, the client accepted, formats
listed, and then no stream. Upstream's standing answer to that error is a remote cipher server
rather than a plugin fix; the maintainer says so directly in
[issue #225](https://github.com/lavalink-devs/youtube-source/issues/225).

`OVERRIDE_PLAYER_VARIANT: IAS` in `compose.yml` matters: upstream reports only the IAS variant
works consistently, and `player_embed` — the one the plugin asks for — is exactly the one that
fails. Forcing IAS is what makes the failure go away.

Set `YT_CIPHER_PASSWORD` in `.env` to any value; both sides read the same variable, so it only
has to match. Generate one with `openssl rand -hex 32`. Empty disables auth on the sidecar,
which is survivable only because it is never published off the private Docker network. It is
not a Google credential and has nothing to do with the OAuth token.

The image has no healthcheck because it is a compiled Deno binary on distroless — no shell,
curl, nc or deno CLI to run one with. If it is down, Lavalink logs a cipher error per track.

### Completing the YouTube OAuth login

`plugins.youtube.oauth` is enabled in `lavalink/application.yml`, but it does nothing until an
Expand Down
11 changes: 11 additions & 0 deletions lavalink/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ plugins:
# recreate lavalink again so it survives restarts without redoing the device login.
# See https://github.com/lavalink-devs/youtube-source for details, including the
# alternative (narrower, WEB/WEBEMBEDDED-only) poToken option.
# Signature/n-parameter deciphering is delegated to the yt-cipher sidecar. The plugin's
# own extraction fails on the current player script ("Must find sig function from script:
# .../player_embed.vflset/<locale>/base.js"), and upstream's standing answer to that error
# is a remote cipher server rather than a plugin fix - the maintainer says as much in
# https://github.com/lavalink-devs/youtube-source/issues/225. The sidecar is forced to the
# IAS player variant (see compose.yml), which is what makes the embed-script failure go
# away. Without this block, playback dies at the format stage even with OAuth working.
remoteCipher:
url: "http://yt-cipher:8001"
password: "${YT_CIPHER_PASSWORD:}"
userAgent: "DiJay"
oauth:
enabled: true
refreshToken: "${YOUTUBE_OAUTH_REFRESH_TOKEN:}"
Expand Down
Loading