refactor: isolate application state per Discord server

This commit is contained in:
mmacierzanka committed 2026-08-21 16:14:22 +02:00
1 parent 94be2c7c79
commit 64b428555c
30 files changed
+330 -471

No files matched your search

+14 -7
View File
@@ -59,15 +59,22 @@ class MusicPlayer:
def is_ready(self) -> bool:
return self._bot.is_ready()
@property
@property
def default_plays(self) -> int:
"""Legacy fallback used when no guild context is available."""
return self._config.get_default_plays()
def set_default_plays(self, value: int) -> None:
self._config.set_default_plays(value)
def guild_default_plays(self, guild_id: int) -> int:
return self._config.get_default_plays(guild_id)
def plays_left(self, track: YTDLSource) -> int:
return track.plays_left if track.plays_left is not None else self.default_plays
def set_default_plays(self, guild_id: int, value: int) -> None:
self._config.set_default_plays(guild_id, value)
def plays_left(self, track: YTDLSource, guild_id: int | None = None) -> int:
return (
track.plays_left if track.plays_left is not None else self.guild_default_plays(guild_id)
)
def history(self, guild_id: int) -> list[HistoryEntry]:
return self._history.list(guild_id)
@@ -124,7 +131,7 @@ class MusicPlayer:
if channel:
state.notification_channel = channel
if track.plays_left is None:
track.plays_left = self.default_plays
track.plays_left = self.guild_default_plays(guild_id)
state.queue.append(track)
@@ -200,7 +207,7 @@ class MusicPlayer:
retired_index: int | None = None
if advance and state.current is not None and 0 <= state.index < len(state.queue):
current = state.queue[state.index]
if current is state.current and self.plays_left(current) <= 0:
if current is state.current and self.plays_left(current, guild_id) <= 0:
retired_index = state.index
state.queue.pop(state.index)
state.index -= 1
@@ -239,7 +246,7 @@ class MusicPlayer:
return
track.played = True
track.plays_left = self.plays_left(track) - 1
track.plays_left = self.plays_left(track, guild_id) - 1
track.volume = state.volume
state.queue[next_index] = track
+43 -10
View File
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
class GuildConfigRepository:
"""Dostęp do config.json: kanały powiadomień per serwer + globalne ustawienia."""
"""Dostęp do konfiguracji przechowywanej osobno dla każdego serwera."""
DEFAULT_PLAYS_KEY = "default_plays"
@@ -16,21 +16,52 @@ class GuildConfigRepository:
self._store = store
self._fallback_plays = fallback_plays
def _guild_config(self, guild_id: int) -> dict[str, int | None]:
data = self._store.read()
key = str(guild_id)
value = data.get(key)
# Migrate the old format {"guild_id": channel_id} lazily.
if isinstance(value, dict):
return value
config: dict[str, int | None] = {}
if value is not None:
try:
config["notification_channel_id"] = int(value)
except (TypeError, ValueError):
logger.error(
"Nieprawidłowe ID kanału w konfiguracji serwera %s: %r", guild_id, value
)
# The old default was global. The first guild that is accessed owns it.
legacy_default = data.get(self.DEFAULT_PLAYS_KEY)
if legacy_default is not None:
try:
config["default_plays"] = int(legacy_default)
except (TypeError, ValueError):
logger.error("Nieprawidłowa wartość %s: %r", self.DEFAULT_PLAYS_KEY, legacy_default)
data.pop(self.DEFAULT_PLAYS_KEY, None)
data[key] = config
self._store.write(data)
return config
def get_notification_channel_id(self, guild_id: int) -> int | None:
value = self._store.get(str(guild_id))
if value is None:
return None
value = self._guild_config(guild_id).get("notification_channel_id")
try:
return int(value)
return int(value) if value is not None else None
except (TypeError, ValueError):
logger.error("Nieprawidłowe ID kanału w konfiguracji serwera %s: %r", guild_id, value)
return None
def set_notification_channel_id(self, guild_id: int, channel_id: int | None) -> None:
self._store.set(str(guild_id), channel_id)
config = self._guild_config(guild_id)
config["notification_channel_id"] = channel_id
self._store.set(str(guild_id), config)
def get_default_plays(self) -> int:
value = self._store.get(self.DEFAULT_PLAYS_KEY)
def get_default_plays(self, guild_id: int | None = None) -> int:
if guild_id is None:
return self._fallback_plays
value = self._guild_config(guild_id).get("default_plays")
if value is None:
return self._fallback_plays
try:
@@ -39,5 +70,7 @@ class GuildConfigRepository:
logger.error("Nieprawidłowa wartość %s: %r", self.DEFAULT_PLAYS_KEY, value)
return self._fallback_plays
def set_default_plays(self, value: int) -> None:
self._store.set(self.DEFAULT_PLAYS_KEY, int(value))
def set_default_plays(self, guild_id: int, value: int) -> None:
config = self._guild_config(guild_id)
config["default_plays"] = int(value)
self._store.set(str(guild_id), config)
+13 -2
View File
@@ -5,12 +5,13 @@ from zyrarbot.storage.json_file_store import JsonFileStore
class HistoryRepository:
"""Historia odtwarzania per serwer, buforowana w pamięci i utrwalana w history.json."""
"""Historia odtwarzania per serwer, buforowana i utrwalana w history.json."""
def __init__(self, store: JsonFileStore, limit: int = 50):
self._store = store
self._limit = limit
self._cache: dict[int, list[HistoryEntry]] = {}
self._legacy_guild_id: int | None = None
def list(self, guild_id: int) -> list[HistoryEntry]:
if guild_id not in self._cache:
@@ -29,7 +30,17 @@ class HistoryRepository:
return next((item for item in self.list(guild_id) if item.id == entry_id), None)
def _load(self, guild_id: int) -> list[HistoryEntry]:
raw_entries = self._store.get(str(guild_id), []) or []
raw_entries = self._store.get(str(guild_id))
if raw_entries is None and self._legacy_guild_id is None:
legacy = self._store.get("history", [])
if isinstance(legacy, list):
raw_entries = legacy
self._legacy_guild_id = guild_id
self._store.set(str(guild_id), legacy)
data = self._store.read()
data.pop("history", None)
self._store.write(data)
raw_entries = raw_entries or []
entries: list[HistoryEntry] = []
seen: set[tuple[str, str | None]] = set()
for raw in raw_entries:
+48 -16
View File
@@ -12,22 +12,51 @@ class UploadsRepository:
def __init__(self, directory: str):
self._directory = directory
self._legacy_guild_id: int | None = None
@property
def directory(self) -> str:
return self._directory
def resolve(self, filename: str) -> str:
return os.path.join(self._directory, os.path.basename(filename))
def _guild_directory(self, guild_id: int | None) -> str:
if guild_id is None:
return self._directory
self._migrate_legacy(guild_id)
return os.path.join(self._directory, str(guild_id))
def exists(self, filename: str) -> bool:
return os.path.isfile(self.resolve(filename))
def _migrate_legacy(self, guild_id: int) -> None:
"""Przenosi stare, niesegregowane pliki do pierwszego użytego serwera."""
if self._legacy_guild_id is not None:
return
def list(self) -> list[UploadFileInfo]:
os.makedirs(self._directory, exist_ok=True)
legacy_files = [
name
for name in os.listdir(self._directory)
if os.path.isfile(os.path.join(self._directory, name)) and not name.startswith(".")
]
target = os.path.join(self._directory, str(guild_id))
if legacy_files:
os.makedirs(target, exist_ok=True)
for name in legacy_files:
shutil.move(
os.path.join(self._directory, name),
os.path.join(target, name),
)
self._legacy_guild_id = guild_id
def resolve(self, filename: str, guild_id: int | None = None) -> str:
return os.path.join(self._guild_directory(guild_id), os.path.basename(filename))
def exists(self, filename: str, guild_id: int | None = None) -> bool:
return os.path.isfile(self.resolve(filename, guild_id))
def list(self, guild_id: int | None = None) -> list[UploadFileInfo]:
directory = self._guild_directory(guild_id)
os.makedirs(directory, exist_ok=True)
files = []
for name in os.listdir(self._directory):
path = os.path.join(self._directory, name)
for name in os.listdir(directory):
path = os.path.join(directory, name)
if not os.path.isfile(path) or name.startswith("."):
continue
files.append(
@@ -40,25 +69,28 @@ class UploadsRepository:
files.sort(key=lambda item: item.mtime, reverse=True)
return files
def search(self, query: str, limit: int = 25) -> list[str]:
def search(self, query: str, limit: int = 25, guild_id: int | None = None) -> list[str]:
needle = query.lower()
return [item.filename for item in self.list() if needle in item.filename.lower()][:limit]
return [item.filename for item in self.list(guild_id) if needle in item.filename.lower()][
:limit
]
def save(self, filename: str, source: BinaryIO) -> str:
os.makedirs(self._directory, exist_ok=True)
def save(self, filename: str, source: BinaryIO, guild_id: int | None = None) -> str:
directory = self._guild_directory(guild_id)
os.makedirs(directory, exist_ok=True)
name = os.path.basename(filename)
with open(self.resolve(name), "wb") as target:
with open(self.resolve(name, guild_id), "wb") as target:
shutil.copyfileobj(source, target)
return name
def delete(self, filename: str) -> bool:
path = self.resolve(filename)
def delete(self, filename: str, guild_id: int | None = None) -> bool:
path = self.resolve(filename, guild_id)
if not os.path.isfile(path):
return False
os.remove(path)
return True
def touch(self, filename: str) -> None:
path = self.resolve(filename)
def touch(self, filename: str, guild_id: int | None = None) -> None:
path = self.resolve(filename, guild_id)
if os.path.isfile(path):
os.utime(path, None)
+11 -8
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from fastapi import WebSocket
@@ -9,23 +10,25 @@ class ConnectionManager:
"""Rejestr aktywnych połączeń WebSocket panelu webowego."""
def __init__(self) -> None:
self._connections: list[WebSocket] = []
self._connections: dict[WebSocket, int | None] = {}
@property
def has_listeners(self) -> bool:
return bool(self._connections)
async def connect(self, websocket: WebSocket) -> None:
async def connect(self, websocket: WebSocket, guild_id: int | None = None) -> None:
await websocket.accept()
self._connections.append(websocket)
self._connections[websocket] = guild_id
def disconnect(self, websocket: WebSocket) -> None:
if websocket in self._connections:
self._connections.remove(websocket)
self._connections.pop(websocket, None)
async def broadcast(self, message: dict[str, Any]) -> None:
for connection in list(self._connections):
async def broadcast(
self, message: dict[str, Any] | Callable[[int | None], dict[str, Any]]
) -> None:
for connection, guild_id in list(self._connections.items()):
try:
await connection.send_json(message)
payload = message(guild_id) if callable(message) else message
await connection.send_json(payload)
except Exception: # noqa: BLE001 - zerwane połączenie po prostu usuwamy
self.disconnect(connection)
+8 -5
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import discord
from fastapi import HTTPException, Request, WebSocket
from fastapi import HTTPException, Query, Request, WebSocket
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.music.music_player import MusicPlayer
@@ -56,9 +56,12 @@ def get_socket_status_service(websocket: WebSocket) -> StatusService:
return websocket.app.state.status_service
def get_active_guild(request: Request) -> discord.Guild:
"""Serwer, którym steruje panel; brak kontekstu kończy się błędem 400."""
guild = get_guilds(request).active_guild()
def get_active_guild(request: Request, guild_id: int | None = Query(default=None)) -> discord.Guild:
"""Zwraca serwer wskazany przez panel lub pierwszy znany serwer."""
guilds = get_guilds(request)
guild = next((item for item in guilds.guilds() if item.id == guild_id), None)
if guild_id is None:
guild = guilds.active_guild()
if guild is None:
raise HTTPException(status_code=400, detail=NO_GUILD_DETAIL)
raise HTTPException(status_code=404, detail="Unknown Discord server")
return guild
-2
View File
@@ -11,7 +11,6 @@ from zyrarbot.ui.api.connection_manager import ConnectionManager
from zyrarbot.ui.api.routers import (
playback_router,
queue_router,
settings_router,
status_router,
uploads_router,
voice_router,
@@ -27,7 +26,6 @@ ROUTERS = (
queue_router.router,
voice_router.router,
uploads_router.router,
settings_router.router,
websocket_router.router,
)
@@ -1,7 +0,0 @@
from pydantic import BaseModel
class DefaultPlaysRequest(BaseModel):
"""Domyślna liczba odtworzeń przypisywana nowym utworom."""
default_plays: int
@@ -1,8 +0,0 @@
from pydantic import BaseModel
class TextChannelRequest(BaseModel):
"""Ustawienie kanału powiadomień; puste ``channel_id`` czyści ustawienie."""
channel_id: str
guild_id: str | None = None
+3 -3
View File
@@ -65,13 +65,13 @@ async def play_file(
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
) -> dict[str, Any]:
_require_voice(player, guild)
if not uploads.exists(request.filename):
if not uploads.exists(request.filename, guild.id):
raise HTTPException(status_code=404, detail="Plik nie istnieje na serwerze")
try:
# Odświeżenie mtime utrzymuje plik na szczycie listy ostatnio używanych.
uploads.touch(request.filename)
track = await tracks.from_file(uploads.resolve(request.filename))
uploads.touch(request.filename, guild.id)
track = await tracks.from_file(uploads.resolve(request.filename, guild.id))
result = await player.enqueue(guild.id, track, from_web=True)
except Exception as error:
logger.error("Nie udało się odtworzyć pliku z panelu: %s", error)
@@ -1,82 +0,0 @@
from __future__ import annotations
import logging
from typing import Any
import discord
from fastapi import APIRouter, Depends, HTTPException
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.settings import Settings
from zyrarbot.storage.guild_config_repository import GuildConfigRepository
from zyrarbot.ui.api.dependencies import (
get_broadcaster,
get_config,
get_guilds,
get_player,
get_settings,
)
from zyrarbot.ui.api.requests.default_plays_request import DefaultPlaysRequest
from zyrarbot.ui.api.requests.text_channel_request import TextChannelRequest
from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster
from zyrarbot.ui.bot.guild_resolver import GuildResolver
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/settings", tags=["settings"])
@router.post("/text_channel")
async def set_text_channel(
request: TextChannelRequest,
guilds: GuildResolver = Depends(get_guilds),
player: MusicPlayer = Depends(get_player),
config: GuildConfigRepository = Depends(get_config),
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
) -> dict[str, Any]:
if request.channel_id in ("", "0"):
if not request.guild_id:
raise HTTPException(status_code=400, detail="guild_id required to reset channel")
guild_id = int(request.guild_id)
player.set_notification_channel(guild_id, None)
config.set_notification_channel_id(guild_id, None)
await broadcaster.broadcast()
return {"status": "success", "channel_id": None, "guild_id": str(guild_id)}
channel = guilds.channel(int(request.channel_id))
if not isinstance(channel, discord.TextChannel):
raise HTTPException(status_code=404, detail="Text channel not found")
guild_id = channel.guild.id
if request.guild_id and int(request.guild_id) != guild_id:
raise HTTPException(
status_code=400, detail="Channel does not belong to the specified guild"
)
player.set_notification_channel(guild_id, channel)
config.set_notification_channel_id(guild_id, channel.id)
try:
await channel.send(
"🔔 Ten kanał został ustawiony jako główny kanał powiadomień dla ŻyrarBOT "
f"na serwerze **{channel.guild.name}** z panelu webowego."
)
except discord.DiscordException as error:
logger.error("Nie udało się wysłać wiadomości testowej: %s", error)
await broadcaster.broadcast()
return {"status": "success", "channel_id": request.channel_id, "guild_id": str(guild_id)}
@router.post("/default_plays")
async def set_default_plays(
request: DefaultPlaysRequest,
player: MusicPlayer = Depends(get_player),
settings: Settings = Depends(get_settings),
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
) -> dict[str, Any]:
if not settings.is_valid_plays(request.default_plays):
raise HTTPException(status_code=400, detail="Invalid default plays value")
player.set_default_plays(request.default_plays)
await broadcaster.broadcast()
return {"status": "success", "default_plays": request.default_plays}
+7 -3
View File
@@ -2,18 +2,22 @@ from __future__ import annotations
from typing import Any
import discord
from fastapi import APIRouter, Depends
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.ui.api.dependencies import get_player, get_status_service
from zyrarbot.ui.api.dependencies import get_active_guild, get_player, get_status_service
from zyrarbot.ui.api.status_service import StatusService
router = APIRouter(prefix="/api", tags=["status"])
@router.get("/status")
async def get_status(status: StatusService = Depends(get_status_service)) -> dict[str, Any]:
return status.snapshot()
async def get_status(
guild: discord.Guild = Depends(get_active_guild),
status: StatusService = Depends(get_status_service),
) -> dict[str, Any]:
return status.snapshot(guild.id)
@router.get("/health")
+12 -6
View File
@@ -2,26 +2,31 @@ from __future__ import annotations
from typing import Any
import discord
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from zyrarbot.storage.uploads_repository import UploadsRepository
from zyrarbot.ui.api.dependencies import get_uploads
from zyrarbot.ui.api.dependencies import get_active_guild, get_uploads
router = APIRouter(prefix="/api", tags=["uploads"])
@router.get("/uploads")
async def list_uploads(uploads: UploadsRepository = Depends(get_uploads)) -> dict[str, Any]:
return {"files": [item.to_dict() for item in uploads.list()]}
async def list_uploads(
guild: discord.Guild = Depends(get_active_guild),
uploads: UploadsRepository = Depends(get_uploads),
) -> dict[str, Any]:
return {"files": [item.to_dict() for item in uploads.list(guild.id)]}
@router.post("/upload")
async def upload(
file: UploadFile = File(...),
guild: discord.Guild = Depends(get_active_guild),
uploads: UploadsRepository = Depends(get_uploads),
) -> dict[str, str]:
try:
filename = uploads.save(file.filename or "plik", file.file)
filename = uploads.save(file.filename or "plik", file.file, guild.id)
except OSError as error:
raise HTTPException(status_code=500, detail=f"Błąd zapisu pliku: {error}") from error
return {"status": "success", "filename": filename}
@@ -30,12 +35,13 @@ async def upload(
@router.delete("/uploads/{filename}")
async def delete_upload(
filename: str,
guild: discord.Guild = Depends(get_active_guild),
uploads: UploadsRepository = Depends(get_uploads),
) -> dict[str, str]:
if not uploads.exists(filename):
if not uploads.exists(filename, guild.id):
raise HTTPException(status_code=404, detail="Plik nie istnieje")
try:
uploads.delete(filename)
uploads.delete(filename, guild.id)
except OSError as error:
raise HTTPException(status_code=500, detail=str(error)) from error
return {"status": "deleted"}
+4 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from zyrarbot.ui.api.connection_manager import ConnectionManager
from zyrarbot.ui.api.dependencies import get_connections, get_socket_status_service
@@ -18,10 +18,11 @@ async def websocket_endpoint(
websocket: WebSocket,
connections: ConnectionManager = Depends(get_connections),
status: StatusService = Depends(get_socket_status_service),
guild_id: int | None = Query(default=None),
) -> None:
await connections.connect(websocket)
await connections.connect(websocket, guild_id)
try:
await websocket.send_json(status.snapshot())
await websocket.send_json(status.snapshot(guild_id))
while True:
# Klient nic nie wysyła - czekamy wyłącznie na rozłączenie.
await websocket.receive_text()
+1 -1
View File
@@ -24,7 +24,7 @@ class StatusBroadcaster:
if not self._connections.has_listeners:
return
try:
await self._connections.broadcast(self._status.snapshot())
await self._connections.broadcast(self._status.snapshot)
except Exception as error: # noqa: BLE001 - rozgłaszanie nie może ubić pętli
logger.error("Błąd rozgłaszania stanu: %s", error)
+18 -12
View File
@@ -19,14 +19,19 @@ class StatusService:
self._player = player
self._guilds = guilds
def snapshot(self) -> dict[str, Any]:
def snapshot(self, guild_id: int | None = None) -> dict[str, Any]:
guild = (
next((item for item in self._guilds.guilds() if item.id == guild_id), None)
if guild_id is not None
else self._guilds.active_guild()
)
payload: dict[str, Any] = {
"guilds": self._guilds_payload(),
"voice_channels": self._voice_channels_payload(),
"default_plays": self._player.default_plays,
"voice_channels": self._voice_channels_payload(guild),
"default_plays": self._player.guild_default_plays(guild.id)
if guild
else self._player.default_plays,
}
guild = self._guilds.active_guild()
if guild is None:
payload.update(self._idle_payload())
return payload
@@ -63,8 +68,8 @@ class StatusService:
"loop_single": state.loop_mode.loops_track,
"loop_playlist": state.loop_mode.loops_queue,
"volume": int(state.volume * 100),
"current": self._current_payload(state.current),
"queue": [self._track_payload(track) for track in state.queue],
"current": self._current_payload(state.current, guild.id),
"queue": [self._track_payload(track, guild.id) for track in state.queue],
"queue_index": state.index,
"active_voice_channel": self._active_voice_payload(voice_client),
"text_channels": [
@@ -79,7 +84,7 @@ class StatusService:
],
}
def _current_payload(self, track: YTDLSource | None) -> dict[str, Any] | None:
def _current_payload(self, track: YTDLSource | None, guild_id: int) -> dict[str, Any] | None:
if track is None:
return None
return {
@@ -88,15 +93,15 @@ class StatusService:
"id": track.id,
"duration": track.duration,
"elapsed": round(track.elapsed, 1),
"plays_left": self._player.plays_left(track),
"plays_left": self._player.plays_left(track, guild_id),
}
def _track_payload(self, track: YTDLSource) -> dict[str, Any]:
def _track_payload(self, track: YTDLSource, guild_id: int) -> dict[str, Any]:
return {
"title": track.title,
"url": track.display_url,
"id": track.id,
"plays_left": self._player.plays_left(track),
"plays_left": self._player.plays_left(track, guild_id),
}
def _active_voice_payload(
@@ -112,7 +117,7 @@ class StatusService:
"guild_id": str(channel.guild.id),
}
def _voice_channels_payload(self) -> list[dict[str, Any]]:
def _voice_channels_payload(self, selected_guild: discord.Guild | None) -> list[dict[str, Any]]:
return [
{
"guild_name": guild.name,
@@ -121,6 +126,7 @@ class StatusService:
"channel_id": str(channel.id),
}
for guild in self._guilds.guilds()
if selected_guild is None or guild.id == selected_guild.id
for channel in guild.voice_channels
]
+15 -6
View File
@@ -32,7 +32,9 @@ class PlaybackCog(MusicCogBase, name="Playback"):
async def _uploads_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
names = self._uploads.search(current, limit=AUTOCOMPLETE_LIMIT)
names = self._uploads.search(
current, limit=AUTOCOMPLETE_LIMIT, guild_id=interaction.guild.id
)
return [app_commands.Choice(name=name, value=name) for name in names]
async def _history_autocomplete(
@@ -52,7 +54,12 @@ class PlaybackCog(MusicCogBase, name="Playback"):
return
await interaction.response.defer()
try:
track = await self._tracks.resolve(query, loop=self.bot.loop)
if self._uploads.exists(query, interaction.guild.id):
track = await self._tracks.from_file(
self._uploads.resolve(query, interaction.guild.id)
)
else:
track = await self._tracks.resolve(query, loop=self.bot.loop)
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001
logger.error("Komenda /play nie powiodła się: %s", error)
@@ -65,11 +72,13 @@ class PlaybackCog(MusicCogBase, name="Playback"):
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
if not self._uploads.exists(filename):
if not self._uploads.exists(filename, interaction.guild.id):
await interaction.followup.send("Nie znaleziono pliku na serwerze.", ephemeral=True)
return
try:
track = await self._tracks.from_file(self._uploads.resolve(filename))
track = await self._tracks.from_file(
self._uploads.resolve(filename, interaction.guild.id)
)
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001
logger.error("Komenda /playfile nie powiodła się: %s", error)
@@ -95,13 +104,13 @@ class PlaybackCog(MusicCogBase, name="Playback"):
try:
if entry.is_local:
filepath = entry.replay_source or ""
if not self._uploads.exists(filepath):
if not self._uploads.exists(filepath, guild_id):
await interaction.followup.send(
"Plik lokalny z historii już nie istnieje na serwerze.", ephemeral=True
)
return
track = await self._tracks.from_file(
self._uploads.resolve(filepath), title=entry.title
self._uploads.resolve(filepath, guild_id), title=entry.title
)
else:
track = await self._tracks.from_url(entry.url or "", loop=self.bot.loop)
+3 -2
View File
@@ -29,7 +29,7 @@ class QueueCog(MusicCogBase, name="Queue"):
lines: list[str] = []
if state.current and not 0 <= state.index < len(state.queue):
title = discord.utils.escape_markdown(state.current.title or "")
plays = self.player.plays_left(state.current)
plays = self.player.plays_left(state.current, interaction.guild.id)
lines.append(
f"▶️ **{title}** (obecnie odtwarzane - ostatnie odtworzenie) [🔁 {plays}]\n---"
)
@@ -38,7 +38,8 @@ class QueueCog(MusicCogBase, name="Queue"):
prefix = "▶️ **" if is_current else f"{position + 1}. "
suffix = "** (obecnie odtwarzane)" if is_current else ""
title = discord.utils.escape_markdown(track.title or "")
lines.append(f"{prefix}{title}{suffix} [🔁 {self.player.plays_left(track)}]")
plays = self.player.plays_left(track, interaction.guild.id)
lines.append(f"{prefix}{title}{suffix} [🔁 {plays}]")
preview = "\n".join(lines[:QUEUE_PREVIEW_LIMIT])
if len(lines) > QUEUE_PREVIEW_LIMIT:
preview += f"\n... i {len(lines) - QUEUE_PREVIEW_LIMIT} więcej."
+1 -1
View File
@@ -49,7 +49,7 @@ class SettingsCog(MusicCogBase, name="Settings"):
"❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True
)
return
self.player.set_default_plays(count)
self.player.set_default_plays(interaction.guild.id, count)
await self.player.trigger_state_change()
await interaction.response.send_message(
f"✅ Domyślna liczba odtworzeń dla nowych utworów została ustawiona na: **{count}**."