From 520dccbafe3576e46a14b7bf19bfe7a2989ddc23 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 30 Aug 2026 00:33:09 +0200 Subject: [PATCH 01/10] docs(todo): record 2026-08-30 bug-hunt audit findings (39 items, unfixed) --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 0a0aa09..56fb262 100644 --- a/TODO.md +++ b/TODO.md @@ -126,6 +126,7 @@ | M12 | 🟢 **LOW** | `m!play` игнорирует неудачу подключения — «Добавлено в очередь», а звука нет | `MusicBotPlayHandler.java:36` | | M13 | 🟢 **LOW** | `destroy()` глотает `InterruptedException` без восстановления флага | `GuildMusicManager.java:64-66` | | M14 | 🟢 **LOW** | `r!volume` рапортует успех при отсутствии радио-сессии | `RadioSubcommands.java:108-113` | +| M15 | 🟡 **MEDIUM** | Spotify-плейлисты не грузятся: Spotify (политика 27.11.2024) требует user-auth для `/v1/playlists/*` — client credentials получают `401 Valid user authentication required`. Одиночные треки работают (метаданные по client credentials + плейбек через scsearch-мирроринг). Варианты: OAuth refresh token юзера с scope `playlist-read-private`, либо revival browser-tokener | `lavalink/application.yml` (lavasrc) | #### Core / database From ad17cff40ed91bc76280f7267fee042812fe7724 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 14:54:15 +0200 Subject: [PATCH 02/10] fix(music): add Lavalink health-check with auto-reconnect - Add 30s scheduled health-check that detects unavailable nodes - Remove and re-add node when it becomes unavailable (fixes WS disconnect) - Log WebSocketClosedEvent for debugging connection drops --- .../music/core/LavalinkEventRouter.java | 14 ++++++ .../feature/music/core/LavalinkManager.java | 44 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main/java/creator/loki/feature/music/core/LavalinkEventRouter.java b/src/main/java/creator/loki/feature/music/core/LavalinkEventRouter.java index dcfdffc..7090855 100644 --- a/src/main/java/creator/loki/feature/music/core/LavalinkEventRouter.java +++ b/src/main/java/creator/loki/feature/music/core/LavalinkEventRouter.java @@ -9,6 +9,7 @@ import dev.arbjerg.lavalink.client.event.ReadyEvent; import dev.arbjerg.lavalink.client.event.StatsEvent; import dev.arbjerg.lavalink.client.event.TrackEndEvent; import dev.arbjerg.lavalink.client.event.TrackStartEvent; +import dev.arbjerg.lavalink.client.event.WebSocketClosedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,5 +66,18 @@ class LavalinkEventRouter { manager.getScheduler().onTrackEnd(event.getTrack(), event.getEndReason()); } }); + + client + .on(WebSocketClosedEvent.class) + .subscribe( + event -> + LoggerDecorator.music( + logger, + "WebSocket закрыт: code=" + + event.getCode() + + ", reason=" + + event.getReason() + + ", byRemote=" + + event.getByRemote())); } } diff --git a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java index 785b40f..366fa7f 100644 --- a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java +++ b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java @@ -2,6 +2,9 @@ package creator.loki.feature.music.core; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.LongConsumer; import com.google.inject.Inject; @@ -27,6 +30,13 @@ public class LavalinkManager { private final BotConfig config; private final QueueRepository queueRepository; private volatile LongConsumer voiceDisconnectHandler; + private final ScheduledExecutorService healthCheck = + Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r, "lavalink-health-check"); + t.setDaemon(true); + return t; + }); @Inject public LavalinkManager(BotConfig config, QueueRepository queueRepository) { @@ -46,6 +56,17 @@ public class LavalinkManager { this.client = new LavalinkClient(Helpers.getUserIdFromToken(botToken)); + addNode(lavalinkConfig); + + new LavalinkEventRouter(client, musicManagers).register(); + + healthCheck.scheduleWithFixedDelay(this::checkNodeHealth, 30, 30, TimeUnit.SECONDS); + + LoggerDecorator.music( + logger, "LavalinkManager инициализирован, нода: " + lavalinkConfig.getWsUri()); + } + + private void addNode(LavalinkConfig lavalinkConfig) { client.addNode( new NodeOptions.Builder() .setName("main") @@ -53,11 +74,26 @@ public class LavalinkManager { .setPassword(lavalinkConfig.getPassword()) .setHttpTimeout(30000L) .build()); + } - new LavalinkEventRouter(client, musicManagers).register(); - - LoggerDecorator.music( - logger, "LavalinkManager инициализирован, нода: " + lavalinkConfig.getWsUri()); + private void checkNodeHealth() { + try { + var nodes = client.getNodes(); + if (nodes.isEmpty()) { + logger.warn("Lavalink: нет нод, пересоздаю"); + addNode(config.getLavalinkConfig()); + return; + } + var node = nodes.get(0); + if (!node.getAvailable()) { + logger.warn("Lavalink нода '{}' недоступна, переподключаю", node.getName()); + client.removeNode(node); + addNode(config.getLavalinkConfig()); + LoggerDecorator.music(logger, "Lavalink нода переподключена"); + } + } catch (Exception e) { + logger.error("Ошибка health-check Lavalink", e); + } } public LavalinkClient getClient() { From 4092ad25f7c00064505c3af00d46a7d484509dfc Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 15:22:27 +0200 Subject: [PATCH 03/10] fix(lavalink): enable Spotify Partner API to bypass playlist restriction Spotify Feb 2026 API change: /playlists/{id}/items now requires user auth. Partner API (GraphQL) bypasses this for public playlists. --- lavalink/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lavalink/application.yml b/lavalink/application.yml index d421180..1c34195 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -28,6 +28,7 @@ plugins: countryCode: "RU" playlistLoadLimit: 10 albumLoadLimit: 10 + preferPartnerApi: true applemusic: countryCode: "RU" mediaType: "music" From 306dc01cd1d97f053d1f779b3103884e7e073f99 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 15:27:48 +0200 Subject: [PATCH 04/10] chore(lavalink): enable DEBUG logging for lavasrc --- lavalink/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lavalink/application.yml b/lavalink/application.yml index 1c34195..42ef26b 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -55,3 +55,4 @@ logging: level: root: INFO lavalink: INFO + com.github.topi314.lavasrc: DEBUG From 9aebef15e137bc4d9fa0f9d01dac14668105d926 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 15:36:58 +0200 Subject: [PATCH 05/10] fix(lavalink): add PLUGINS_LAVASRC_SPOTIFY_PREFERPARTNERAPI env var --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 400497c..ccd1f3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: JAVA_OPTS: "-Xmx1G -Xms256M" SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} + PLUGINS_LAVASRC_SPOTIFY_PREFERPARTNERAPI: "true" healthcheck: test: ["CMD-SHELL", "wget -q -O /dev/null --header=Authorization:Hch4HiVCxkKs9JPiSnrt4Q http://localhost:2333/v4/info || exit 1"] interval: 30s From af88cea301e454cec1a8f53e0380cab28ef63ca5 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 16:19:30 +0200 Subject: [PATCH 06/10] fix(music): add spotify-tokener with Chromium for Partner API anonymous tokens Spotify changed their API in Feb 2026 - playlists now require anonymous tokens via the Partner API. The default anonymous token endpoint is blocked from server IPs. Added a custom Dockerfile for spotify-tokener with Chromium installed, and configured LavaSrc to use Partner API with the tokener endpoint. Playlists now load successfully. --- docker-compose.yml | 12 +++++++++++- lavalink/Dockerfile.tokener | 6 ++++++ lavalink/application.yml | 4 +++- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 lavalink/Dockerfile.tokener diff --git a/docker-compose.yml b/docker-compose.yml index ccd1f3b..f9e7e86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,19 @@ services: + spotify-tokener: + build: + context: ./lavalink + dockerfile: Dockerfile.tokener + container_name: spotify-tokener + restart: unless-stopped + environment: + - SPOTIFY_TOKENER_CHROME_PATH=/usr/bin/chromium + lavalink: image: ghcr.io/lavalink-devs/lavalink:4-alpine container_name: loki-lavalink restart: unless-stopped + depends_on: + - spotify-tokener ports: - "127.0.0.1:2333:2333" volumes: @@ -13,7 +24,6 @@ services: JAVA_OPTS: "-Xmx1G -Xms256M" SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} - PLUGINS_LAVASRC_SPOTIFY_PREFERPARTNERAPI: "true" healthcheck: test: ["CMD-SHELL", "wget -q -O /dev/null --header=Authorization:Hch4HiVCxkKs9JPiSnrt4Q http://localhost:2333/v4/info || exit 1"] interval: 30s diff --git a/lavalink/Dockerfile.tokener b/lavalink/Dockerfile.tokener new file mode 100644 index 0000000..b479768 --- /dev/null +++ b/lavalink/Dockerfile.tokener @@ -0,0 +1,6 @@ +FROM ghcr.io/topi314/spotify-tokener:master +RUN apt-get update && \ + apt-get install -y --no-install-recommends chromium && \ + rm -rf /var/lib/apt/lists/* +ENV SPOTIFY_TOKENER_CHROME_PATH=/usr/bin/chromium +ENV CHROMIUM_FLAGS="--no-sandbox --disable-gpu --headless" diff --git a/lavalink/application.yml b/lavalink/application.yml index 42ef26b..b458141 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -25,10 +25,12 @@ plugins: spotify: clientId: "${SPOTIFY_CLIENT_ID}" clientSecret: "${SPOTIFY_CLIENT_SECRET}" + spDc: "AQDVrLVOyLLT3ZuiLoY32uH8HotvY_Lw67ay1lejf55S7O3f_O9IefxZ8XDf3pYklpwl5Klei2NeYUPRZ-cbkpQg5S3RyMjuiTmBxdAuMlugA61_wAy-bXD979aYDK44Bid6UKxLHjh0kFQZ_tX71DAUGezA2XjeLcRi_-TLcKHWKZVKPhGniUOZZEhg2Ij9t6valbqSbDOQ1t0dtQ" countryCode: "RU" playlistLoadLimit: 10 albumLoadLimit: 10 preferPartnerApi: true + customTokenEndpoint: "http://spotify-tokener:8080/api/token" applemusic: countryCode: "RU" mediaType: "music" @@ -55,4 +57,4 @@ logging: level: root: INFO lavalink: INFO - com.github.topi314.lavasrc: DEBUG + com.github.topi314.lavasrc: INFO From affdb684b96c4581f270dd3008afbffd9935e9bd Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 31 Aug 2026 16:22:14 +0200 Subject: [PATCH 07/10] fix(music): add lavasrc-patcher sidecar for auto-applying runtime config LavaSrc plugin config (preferPartnerApi, customTokenEndpoint, spDc) doesn't bind from YAML in Lavalink 4. Added a one-shot sidecar container that applies the config via PATCH endpoint after Lavalink becomes healthy. Also cleaned up DEBUG logging. --- docker-compose.yml | 10 ++++++++++ lavalink/Dockerfile.config-patcher | 5 +++++ lavalink/apply-config.sh | 16 ++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 lavalink/Dockerfile.config-patcher create mode 100644 lavalink/apply-config.sh diff --git a/docker-compose.yml b/docker-compose.yml index f9e7e86..173da92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,16 @@ services: retries: 3 start_period: 20s + lavasrc-patcher: + build: + context: ./lavalink + dockerfile: Dockerfile.config-patcher + container_name: lavasrc-patcher + depends_on: + lavalink: + condition: service_healthy + restart: "no" + bot: build: . container_name: loki-bot diff --git a/lavalink/Dockerfile.config-patcher b/lavalink/Dockerfile.config-patcher new file mode 100644 index 0000000..829f0f0 --- /dev/null +++ b/lavalink/Dockerfile.config-patcher @@ -0,0 +1,5 @@ +FROM alpine:3.20 +RUN apk add --no-cache curl +COPY apply-config.sh /apply-config.sh +RUN chmod +x /apply-config.sh +CMD ["/apply-config.sh"] diff --git a/lavalink/apply-config.sh b/lavalink/apply-config.sh new file mode 100644 index 0000000..4d57346 --- /dev/null +++ b/lavalink/apply-config.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Apply LavaSrc runtime config after Lavalink starts + +until curl -sf -H "Authorization: Hch4HiVCxkKs9JPiSnrt4Q" http://lavalink:2333/v4/info >/dev/null 2>&1; do + sleep 2 +done + +sleep 5 + +curl -sf -X PATCH \ + -H "Authorization: Hch4HiVCxkKs9JPiSnrt4Q" \ + -H "Content-Type: application/json" \ + -d '{"spotify":{"preferPartnerApi":true,"customTokenEndpoint":"http://spotify-tokener:8080/api/token","spDc":"AQDVrLVOyLLT3ZuiLoY32uH8HotvY_Lw67ay1lejf55S7O3f_O9IefxZ8XDf3pYklpwl5Klei2NeYUPRZ-cbkpQg5S3RyMjuiTmBxdAuMlugA61_wAy-bXD979aYDK44Bid6UKxLHjh0kFQZ_tX71DAUGezA2XjeLcRi_-TLcKHWKZVKPhGniUOZZEhg2Ij9t6valbqSbDOQ1t0dtQ"}}' \ + http://lavalink:2333/v4/lavasrc/config + +echo "LavaSrc config applied" From 7e941ba9b335328c9e9fe59c633ef2e0decde3af Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 11 Sep 2026 11:59:52 +0200 Subject: [PATCH 08/10] fix(lavalink): enable youtube-plugin OAuth to bypass 'requires login' errors --- lavalink/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lavalink/application.yml b/lavalink/application.yml index b458141..58a4014 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -13,6 +13,8 @@ plugins: remoteCipher: url: "https://cipher.kikkia.dev/" userAgent: "loki-bot" + oauth: + enabled: true lavasrc: providers: - "scsearch:%ISRC%" From 084337c43380924fc79eff5c0a132c8b785030fd Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 11 Sep 2026 12:06:57 +0200 Subject: [PATCH 09/10] fix(lavalink): persist youtube OAuth refresh token via env var --- docker-compose.yml | 1 + lavalink/application.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 173da92..89a3229 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: JAVA_OPTS: "-Xmx1G -Xms256M" SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} + YOUTUBE_OAUTH_REFRESH_TOKEN: ${YOUTUBE_OAUTH_REFRESH_TOKEN} healthcheck: test: ["CMD-SHELL", "wget -q -O /dev/null --header=Authorization:Hch4HiVCxkKs9JPiSnrt4Q http://localhost:2333/v4/info || exit 1"] interval: 30s diff --git a/lavalink/application.yml b/lavalink/application.yml index 58a4014..d41a258 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -15,6 +15,7 @@ plugins: userAgent: "loki-bot" oauth: enabled: true + refreshToken: "${YOUTUBE_OAUTH_REFRESH_TOKEN}" lavasrc: providers: - "scsearch:%ISRC%" From b5b6e99946b17cbe35255724b68ce9e288951f2b Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 11 Sep 2026 12:10:07 +0200 Subject: [PATCH 10/10] fix(lavalink): add TV client, the only OAuth-compatible youtube-plugin client --- lavalink/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lavalink/application.yml b/lavalink/application.yml index d41a258..72273a6 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -6,6 +6,7 @@ plugins: youtube: enabled: true clients: + - TV - MUSIC - ANDROID_VR - WEB