Compare commits
10 commits
4cbbb36159
...
b5b6e99946
| Author | SHA1 | Date | |
|---|---|---|---|
| b5b6e99946 | |||
| 084337c433 | |||
| 7e941ba9b3 | |||
| affdb684b9 | |||
| af88cea301 | |||
| 9aebef15e1 | |||
| 306dc01cd1 | |||
| 4092ad25f7 | |||
| ad17cff40e | |||
| 520dccbafe |
8 changed files with 112 additions and 4 deletions
1
TODO.md
1
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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
|
||||
|
|
@ -20,6 +32,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
|
||||
|
|
|
|||
5
lavalink/Dockerfile.config-patcher
Normal file
5
lavalink/Dockerfile.config-patcher
Normal file
|
|
@ -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"]
|
||||
6
lavalink/Dockerfile.tokener
Normal file
6
lavalink/Dockerfile.tokener
Normal file
|
|
@ -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"
|
||||
|
|
@ -6,6 +6,7 @@ plugins:
|
|||
youtube:
|
||||
enabled: true
|
||||
clients:
|
||||
- TV
|
||||
- MUSIC
|
||||
- ANDROID_VR
|
||||
- WEB
|
||||
|
|
@ -13,6 +14,9 @@ plugins:
|
|||
remoteCipher:
|
||||
url: "https://cipher.kikkia.dev/"
|
||||
userAgent: "loki-bot"
|
||||
oauth:
|
||||
enabled: true
|
||||
refreshToken: "${YOUTUBE_OAUTH_REFRESH_TOKEN}"
|
||||
lavasrc:
|
||||
providers:
|
||||
- "scsearch:%ISRC%"
|
||||
|
|
@ -25,9 +29,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"
|
||||
|
|
@ -54,3 +61,4 @@ logging:
|
|||
level:
|
||||
root: INFO
|
||||
lavalink: INFO
|
||||
com.github.topi314.lavasrc: INFO
|
||||
|
|
|
|||
16
lavalink/apply-config.sh
Normal file
16
lavalink/apply-config.sh
Normal file
|
|
@ -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"
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue