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` |
|
| M12 | 🟢 **LOW** | `m!play` игнорирует неудачу подключения — «Добавлено в очередь», а звука нет | `MusicBotPlayHandler.java:36` |
|
||||||
| M13 | 🟢 **LOW** | `destroy()` глотает `InterruptedException` без восстановления флага | `GuildMusicManager.java:64-66` |
|
| M13 | 🟢 **LOW** | `destroy()` глотает `InterruptedException` без восстановления флага | `GuildMusicManager.java:64-66` |
|
||||||
| M14 | 🟢 **LOW** | `r!volume` рапортует успех при отсутствии радио-сессии | `RadioSubcommands.java:108-113` |
|
| 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
|
#### Core / database
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
services:
|
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:
|
lavalink:
|
||||||
image: ghcr.io/lavalink-devs/lavalink:4-alpine
|
image: ghcr.io/lavalink-devs/lavalink:4-alpine
|
||||||
container_name: loki-lavalink
|
container_name: loki-lavalink
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- spotify-tokener
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:2333:2333"
|
- "127.0.0.1:2333:2333"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -13,6 +24,7 @@ services:
|
||||||
JAVA_OPTS: "-Xmx1G -Xms256M"
|
JAVA_OPTS: "-Xmx1G -Xms256M"
|
||||||
SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID}
|
SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID}
|
||||||
SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET}
|
SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET}
|
||||||
|
YOUTUBE_OAUTH_REFRESH_TOKEN: ${YOUTUBE_OAUTH_REFRESH_TOKEN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -q -O /dev/null --header=Authorization:Hch4HiVCxkKs9JPiSnrt4Q http://localhost:2333/v4/info || exit 1"]
|
test: ["CMD-SHELL", "wget -q -O /dev/null --header=Authorization:Hch4HiVCxkKs9JPiSnrt4Q http://localhost:2333/v4/info || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
@ -20,6 +32,16 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 20s
|
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:
|
bot:
|
||||||
build: .
|
build: .
|
||||||
container_name: loki-bot
|
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:
|
youtube:
|
||||||
enabled: true
|
enabled: true
|
||||||
clients:
|
clients:
|
||||||
|
- TV
|
||||||
- MUSIC
|
- MUSIC
|
||||||
- ANDROID_VR
|
- ANDROID_VR
|
||||||
- WEB
|
- WEB
|
||||||
|
|
@ -13,6 +14,9 @@ plugins:
|
||||||
remoteCipher:
|
remoteCipher:
|
||||||
url: "https://cipher.kikkia.dev/"
|
url: "https://cipher.kikkia.dev/"
|
||||||
userAgent: "loki-bot"
|
userAgent: "loki-bot"
|
||||||
|
oauth:
|
||||||
|
enabled: true
|
||||||
|
refreshToken: "${YOUTUBE_OAUTH_REFRESH_TOKEN}"
|
||||||
lavasrc:
|
lavasrc:
|
||||||
providers:
|
providers:
|
||||||
- "scsearch:%ISRC%"
|
- "scsearch:%ISRC%"
|
||||||
|
|
@ -25,9 +29,12 @@ plugins:
|
||||||
spotify:
|
spotify:
|
||||||
clientId: "${SPOTIFY_CLIENT_ID}"
|
clientId: "${SPOTIFY_CLIENT_ID}"
|
||||||
clientSecret: "${SPOTIFY_CLIENT_SECRET}"
|
clientSecret: "${SPOTIFY_CLIENT_SECRET}"
|
||||||
|
spDc: "AQDVrLVOyLLT3ZuiLoY32uH8HotvY_Lw67ay1lejf55S7O3f_O9IefxZ8XDf3pYklpwl5Klei2NeYUPRZ-cbkpQg5S3RyMjuiTmBxdAuMlugA61_wAy-bXD979aYDK44Bid6UKxLHjh0kFQZ_tX71DAUGezA2XjeLcRi_-TLcKHWKZVKPhGniUOZZEhg2Ij9t6valbqSbDOQ1t0dtQ"
|
||||||
countryCode: "RU"
|
countryCode: "RU"
|
||||||
playlistLoadLimit: 10
|
playlistLoadLimit: 10
|
||||||
albumLoadLimit: 10
|
albumLoadLimit: 10
|
||||||
|
preferPartnerApi: true
|
||||||
|
customTokenEndpoint: "http://spotify-tokener:8080/api/token"
|
||||||
applemusic:
|
applemusic:
|
||||||
countryCode: "RU"
|
countryCode: "RU"
|
||||||
mediaType: "music"
|
mediaType: "music"
|
||||||
|
|
@ -54,3 +61,4 @@ logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
lavalink: 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.StatsEvent;
|
||||||
import dev.arbjerg.lavalink.client.event.TrackEndEvent;
|
import dev.arbjerg.lavalink.client.event.TrackEndEvent;
|
||||||
import dev.arbjerg.lavalink.client.event.TrackStartEvent;
|
import dev.arbjerg.lavalink.client.event.TrackStartEvent;
|
||||||
|
import dev.arbjerg.lavalink.client.event.WebSocketClosedEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -65,5 +66,18 @@ class LavalinkEventRouter {
|
||||||
manager.getScheduler().onTrackEnd(event.getTrack(), event.getEndReason());
|
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.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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 java.util.function.LongConsumer;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
@ -27,6 +30,13 @@ public class LavalinkManager {
|
||||||
private final BotConfig config;
|
private final BotConfig config;
|
||||||
private final QueueRepository queueRepository;
|
private final QueueRepository queueRepository;
|
||||||
private volatile LongConsumer voiceDisconnectHandler;
|
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
|
@Inject
|
||||||
public LavalinkManager(BotConfig config, QueueRepository queueRepository) {
|
public LavalinkManager(BotConfig config, QueueRepository queueRepository) {
|
||||||
|
|
@ -46,6 +56,17 @@ public class LavalinkManager {
|
||||||
|
|
||||||
this.client = new LavalinkClient(Helpers.getUserIdFromToken(botToken));
|
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(
|
client.addNode(
|
||||||
new NodeOptions.Builder()
|
new NodeOptions.Builder()
|
||||||
.setName("main")
|
.setName("main")
|
||||||
|
|
@ -53,11 +74,26 @@ public class LavalinkManager {
|
||||||
.setPassword(lavalinkConfig.getPassword())
|
.setPassword(lavalinkConfig.getPassword())
|
||||||
.setHttpTimeout(30000L)
|
.setHttpTimeout(30000L)
|
||||||
.build());
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
new LavalinkEventRouter(client, musicManagers).register();
|
private void checkNodeHealth() {
|
||||||
|
try {
|
||||||
LoggerDecorator.music(
|
var nodes = client.getNodes();
|
||||||
logger, "LavalinkManager инициализирован, нода: " + lavalinkConfig.getWsUri());
|
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() {
|
public LavalinkClient getClient() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue