From cfe9a2fc3153d513347cf85ad209bbe6d19fe9e3 Mon Sep 17 00:00:00 2001 From: Marcin Macierzanka Date: Fri, 21 Aug 2026 16:21:57 +0200 Subject: [PATCH] fix: store playback history per Discord server --- README.md | 2 ++ tests/test_history_repository.py | 35 ++++++++++++++++++++ zyrarbot/application.py | 2 +- zyrarbot/storage/history_repository.py | 45 ++++++++++++++++++-------- zyrarbot/ui/bot/client.py | 4 +++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 tests/test_history_repository.py diff --git a/README.md b/README.md index 6129077..3532989 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ jest dostępny pod `http://localhost` po wykonaniu `docker compose up --build`. Panel obsługuje wiele serwerów Discorda. Wybór serwera w nagłówku przełącza jego osobną kolejkę, historię, bazę plików i ustawienia odtwarzania. Istniejące dane ze starego, wspólnego formatu są przypisywane do pierwszego użytego serwera. +Historia jest zapisywana w osobnych plikach `config/history/.json`, a +uploady w katalogach `uploads//`. ## Testy diff --git a/tests/test_history_repository.py b/tests/test_history_repository.py new file mode 100644 index 0000000..49a9dc3 --- /dev/null +++ b/tests/test_history_repository.py @@ -0,0 +1,35 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from zyrarbot.music.history_entry import HistoryEntry +from zyrarbot.storage.history_repository import HistoryRepository +from zyrarbot.storage.json_file_store import JsonFileStore + + +class HistoryRepositoryTest(unittest.TestCase): + def test_history_is_stored_in_separate_files_and_legacy_data_is_migrated(self): + with tempfile.TemporaryDirectory() as directory: + legacy_path = Path(directory) / "history.json" + entry = HistoryEntry( + id="track-1", + title="Track", + url="https://example.test/track", + duration=10, + is_local=False, + filepath=None, + ) + legacy_path.write_text(json.dumps({"10": [entry.to_dict()]}), encoding="utf-8") + repository = HistoryRepository(JsonFileStore(str(legacy_path))) + + repository.migrate_legacy(10) + + self.assertEqual(repository.list(10)[0].id, "track-1") + self.assertEqual(repository.list(20), []) + self.assertTrue((Path(directory) / "history" / "10.json").exists()) + self.assertFalse((Path(directory) / "history" / "20.json").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/zyrarbot/application.py b/zyrarbot/application.py index 8cfe3e9..a81722d 100644 --- a/zyrarbot/application.py +++ b/zyrarbot/application.py @@ -43,7 +43,7 @@ class Application: self.uploads = UploadsRepository(self.settings.uploads_dir) self.tracks = TrackFactory(self.uploads, MediaProbe()) - self.bot = MusicBot(self.settings, self.tracks, self.uploads, self.config) + self.bot = MusicBot(self.settings, self.tracks, self.uploads, self.config, self.history) self.guilds = GuildResolver(self.bot) self.player = MusicPlayer( bot=self.bot, diff --git a/zyrarbot/storage/history_repository.py b/zyrarbot/storage/history_repository.py index 7a23d45..f19966d 100644 --- a/zyrarbot/storage/history_repository.py +++ b/zyrarbot/storage/history_repository.py @@ -1,17 +1,40 @@ from __future__ import annotations +import os + from zyrarbot.music.history_entry import HistoryEntry from zyrarbot.storage.json_file_store import JsonFileStore class HistoryRepository: - """Historia odtwarzania per serwer, buforowana i utrwalana w history.json.""" + """Historia odtwarzania w osobnym pliku dla każdego serwera.""" 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 + self._guild_stores: dict[int, JsonFileStore] = {} + self._legacy_migrated = False + + def _guild_store(self, guild_id: int) -> JsonFileStore: + if guild_id not in self._guild_stores: + directory = os.path.join(os.path.dirname(self._store.path), "history") + path = os.path.join(directory, f"{guild_id}.json") + self._guild_stores[guild_id] = JsonFileStore(path) + return self._guild_stores[guild_id] + + def migrate_legacy(self, first_guild_id: int) -> None: + """Move the old shared history file into one file per guild.""" + if self._legacy_migrated: + return + + legacy_data = self._store.read() + for key, entries in legacy_data.items(): + if key == "history" and isinstance(entries, list): + self._write_entries(first_guild_id, entries) + elif key.isdigit() and isinstance(entries, list): + self._write_entries(int(key), entries) + self._legacy_migrated = True def list(self, guild_id: int) -> list[HistoryEntry]: if guild_id not in self._cache: @@ -30,16 +53,7 @@ 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)) - 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 = self._guild_store(guild_id).get("entries", []) raw_entries = raw_entries or [] entries: list[HistoryEntry] = [] seen: set[tuple[str, str | None]] = set() @@ -53,4 +67,9 @@ class HistoryRepository: return entries def _persist(self, guild_id: int) -> None: - self._store.set(str(guild_id), [entry.to_dict() for entry in self._cache.get(guild_id, [])]) + self._guild_store(guild_id).set( + "entries", [entry.to_dict() for entry in self._cache.get(guild_id, [])] + ) + + def _write_entries(self, guild_id: int, entries: list[dict]) -> None: + self._guild_store(guild_id).set("entries", entries[: self._limit]) diff --git a/zyrarbot/ui/bot/client.py b/zyrarbot/ui/bot/client.py index f0738cb..212e76f 100644 --- a/zyrarbot/ui/bot/client.py +++ b/zyrarbot/ui/bot/client.py @@ -9,6 +9,7 @@ from zyrarbot.audio.track_factory import TrackFactory from zyrarbot.music.music_player import MusicPlayer from zyrarbot.settings import Settings from zyrarbot.storage.guild_config_repository import GuildConfigRepository +from zyrarbot.storage.history_repository import HistoryRepository from zyrarbot.storage.uploads_repository import UploadsRepository from zyrarbot.ui.bot.cogs.control_cog import ControlCog from zyrarbot.ui.bot.cogs.playback_cog import PlaybackCog @@ -32,6 +33,7 @@ class MusicBot(commands.Bot): tracks: TrackFactory, uploads: UploadsRepository, config: GuildConfigRepository, + history: HistoryRepository, ): intents = discord.Intents.default() intents.message_content = True @@ -42,6 +44,7 @@ class MusicBot(commands.Bot): self.tracks = tracks self.uploads = uploads self.config = config + self.history = history self.player: MusicPlayer | None = None def _create_music_cogs(self) -> tuple[commands.Cog, ...]: @@ -77,5 +80,6 @@ class MusicBot(commands.Bot): first_guild_id = self.guilds[0].id self.uploads.migrate_legacy(first_guild_id) self.config.migrate_legacy(first_guild_id) + self.history.migrate_legacy(first_guild_id) if self.user: logger.info("Zalogowano jako %s (%s)", self.user.name, self.user.id)