From b31ab2bffd3da36c8a0f61a8a2bc39bca4d1e95f Mon Sep 17 00:00:00 2001 From: NoxRare <90888719+NoxRare@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:19:23 +0200 Subject: [PATCH 01/12] Update EN.json Adjusted subtext & fixed minor typos --- langs/EN.json | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/langs/EN.json b/langs/EN.json index 336b108..d305005 100644 --- a/langs/EN.json +++ b/langs/EN.json @@ -8,8 +8,8 @@ "alreadyConnected": "Already connected to a voice channel.", "noPermission": "Sorry! i don't have permissions to join or speak in your voice channel.", "noCreatePermission": "Sorry! i don't have permissions to create a song requesting channel.", - "noPlaySource": "Can't found any playable sources!", - "noPlayer": "No player has found on this server.", + "noPlaySource": "Can't find any playable sources!", + "noPlayer": "No player has been found on this server.", "notVote": "This command requires your vote! Type `/vote` for more info.", "missingIntents": "Sorry, this command cannot be executed because the bot is missing the required request intent: `({0})`.", "languageNotFound": "No language pack found! please select an existing language pack.", @@ -17,12 +17,12 @@ "setPrefix": "Done! My prefix in your server is now `{0}`. Try running `{1}ping` to test it out.", "setDJ": "Set the dj to {0}.", "setqueue": "Set queue mode to `{0}`.", - "247": "Now you have `{0}` 24/7 mode.", - "bypassVote": "Now you have `{0}` voting system.", + "247": "24/7 mode is now `{0}`.", + "bypassVote": "You have `{0}` voting system.", "setVolume": "Set the volume to `{0}`%", - "togglecontroller": "Now you have `{0}` the music controller.", - "toggleDuplicateTrack": "Now you have `{0}` to prevent duplicate track in the queue.", - "toggleControllerMsg": "Now you have `{0}` messages from the music controller.", + "togglecontroller": "You now have `{0}` the music controller.", + "toggleDuplicateTrack": "You have now `{0}` duplicate track prevention.", + "toggleControllerMsg": "You now have `{0}` messages from the music controller.", "settingsMenu": "Server Settings | {0}", "settingsTitle": "❤️ Basic Info:", "settingsValue": "```Prefix: {0}\nLanguage: {1}\nMusic Controller: {2}\nDJ Role: @{3}\nVote Bypass: {4}\n24/7: {5}\nDefault Volume: {6}%\nPlay Time: {7}```", @@ -35,35 +35,35 @@ "pingTitle2": "Player Info:", "pingfield1": "```Shard ID: {0}/{1}\nShard Latency: {2:.3f}s {3}\nRegion: {4}```", "pingfield2": "```Node: {0} - {1:.3f}s\nPlayers: {2}\nVoice Region: {3}```", - "addEffect": "Apply the effect `{0}` filter.", + "addEffect": "Applied the `{0}` effect.", "clearEffect": "The sound effects have been cleared!", - "FilterTagAlreadyInUse": "This sound effects has already in use! Please use /cleareffect to remove it.", + "FilterTagAlreadyInUse": "This sound effect is already in use! Please use /cleareffect to remove it.", - "playlistViewTitle": "📜 All {0}'s Playlists", + "playlistViewTitle": "📜 All of {0}'s Playlists", "playlistViewHeaders": "ID:,Time:,Name:,Tracks:", - "playlistFooter": "Type /playlist play [playlist] to add the playlist the into queue.", + "playlistFooter": "Type /playlist play [playlist] to add a playlist the into the queue.", "playlistNotFound": "Playlist [`{0}`] not found. Type /playlist view to view all your playlist.", "playlistNotAccess": "Sorry! You are not allowed to access this playlist!", "playlistNoTrack": "Sorry! There are no tracks in the playlist [`{0}`].", "playlistNotAllow": "This command is not allowed on linked and shared playlists.", "playlistPlay": "Added the playlist [`{0}`] with `{1}` songs to the queue.", "playlistOverText": "Sorry! The name of the playlist cannot exceed 10 characters.", - "playlistSameName": "Sorry! This name cannot be the same as your new name.", - "playlistDeleteError": "You are not allowed to delete default playlist.", + "playlistSameName": "Sorry! Please select a new name for the playlist.", + "playlistDeleteError": "You are not allowed to delete the default playlist.", "playlistRemove": "You have removed playlist [`{0}`].", - "playlistSendErrorPlayer": "Sorry! You are not able to send invitation to yourself.", - "playlistSendErrorBot": "Sorry! You are not able to send invitation to a bot.", + "playlistSendErrorPlayer": "Sorry! You are not able to send an invitation to yourself.", + "playlistSendErrorBot": "Sorry! You are not able to send an invitation to a bot.", "playlistBelongs": "Sorry! This playlist belongs to <@{0}>.", "playlistShare": "Sorry! This playlist has been shared with {0}.", - "playlistSent": "Sorry! You have sent an invitation before.", + "playlistSent": "Sorry! You have already sent an invitation to that user before.", "noPlaylistAcc": "{0} didn't create a playlist account.", "overPlaylistCreation": "You cannot create more than `{0}` playlists!", "playlistExists": "Playlist [`{0}`] already exists.", - "playlistNotInvalidUrl": "Please enter a valid link or public spotify or youtube playlist link.", + "playlistNotInvalidUrl": "Please enter a valid public spotify or youtube playlist link.", "playlistCreated": "You have created `{0}` playlist. Type /playlist view for more info.", "playlistRenamed": "You have renamed `{0}` to `{1}`.", "playlistLimitTrack": "You have reached the limit! You can only add `{0}` songs to your playlist.", - "playlistPlaylistLink": "You are not allowed to use playlist link.", + "playlistPlaylistLink": "You are not allowed to use playlist links.", "playlistStream": "You are not allowed to add streaming videos to your playlist.", "playlistPositionNotFound": "Can't find position `{0}` from your playlist [`{1}`]!", "playlistRemoved": "👋 Removed **{0}** from {1}'s playlist [`{2}`].", @@ -90,13 +90,13 @@ "missingPerms_function": "Only DJ or Admin can use this function.", "timeFormatError": "Incorrect time format. Example: 2:42 or 12:39:31", "lyricsNotFound": "Lyrics not found. Type /lyrics to find the lyrics.", - "missingTrackInfo": "Some track infos are missing.", + "missingTrackInfo": "Some track details are missing.", "noVoiceChannel": "Voice Channel Not Found!", "playlistAddError": "You are not allowed to add streaming videos to your playlist!", "playlistAddError2": "There was a problem adding tracks to the playlist!", "playlistlimited": "You have reached the limit! You can only add {0} songs to your playlist.", - "playlistrepeated": "There is already the same track in your playlist!", + "playlistrepeated": "This track is already in your playlist!", "playlistAdded": "❤️ Added **{0}** into {1}'s playlist [`{2}`]!", "playerDropdown": "Select a song to skip to ...", @@ -165,14 +165,14 @@ "cleared": "Cleared all tracks in the `{0}`", "removed": "`{0}` tracks were removed from the queue.", "forward": "Forward the player to **{0}**", - "rewind": "Rewind the player to **{0}**", + "rewind": "Rewound the player to **{0}**", "replay": "Replaying the current song.", - "swapped": "`{0}` and `{1}` are swapped", + "swapped": "`{0}` and `{1}` have been swapped", "moved": "Moved `{0}` to `{1}`", "autoplay": "Autoplay mode is now **{0}**", "notdj": "You are not DJ. The current DJ is {0}.", - "djToMe": "You cannot transfer the DJ to yourself or a bot.", + "djToMe": "You cannot transfer role of DJ to yourself or a bot.", "djnotinchannel": "`{0}` is not in the voice channel.", "djswap": "You have transferred the role of dj to `{0}`.", @@ -191,4 +191,4 @@ "setStageAnnounceTemplate": "Done! From now on, voice status like the one you're in now will be named according to your template. You should see it update in a few seconds.", "createSongRequestChannel": "A song request channel ({0}) has been created! You can start requesting any song by name or URL in that channel, without needing to use the bot prefix." -} \ No newline at end of file +} From 66780740812b391e03b10652cf101b95db9fa6df Mon Sep 17 00:00:00 2001 From: VeeoN Date: Fri, 11 Apr 2025 17:46:58 +0200 Subject: [PATCH 02/12] Added Polish Language Pack --- langs/PL.json | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 langs/PL.json diff --git a/langs/PL.json b/langs/PL.json new file mode 100644 index 0000000..f084e85 --- /dev/null +++ b/langs/PL.json @@ -0,0 +1,194 @@ + { + "unknownException": "⚠️ Coś poszło nie tak podczas wykonywania polecenia. Spróbuj ponownie później lub dołąćz do serwera discord w celu uzyskania pomocy.", + "enabled": "włączony", + "disabled": "wyłączony", + + "nodeReconnect": "Spróbuj ponownie! Trwa ponowne łącznie z node'm.", + "noChannel": "Brak kanału głosowego do dołącznia. Dołącz do jakiegoś, lub wskaż kanał komendą /connect", + "alreadyConnected": "Już połączono z kanałem głosowym.", + "noPermission": "Niestety nie mam uprawnień, aby dołączyć do Twojego kanału głosowego lub na nim mówić.", + "noCreatePermission": "Niestety, nie mam uprawnień, aby stworzyć kanał do żądania utworów.", + "noPlaySource": "Nie można znaleźć żadnych odtwarzalnych źródeł!", + "noPlayer": "Nie znaleziono żadnego odtwarzacza na tym serwerze.", + "notVote": "To polecenie wymaga głosowania! Wpisz `/vote`, aby uzyskać więcej informacji.", + "missingIntents": "Niestety to polecenie nie może zostać wykonane, ponieważ bot nie posiada wymaganej intencji żądania: `({0})`.", + "languageNotFound": "Nie znaleziono pakietu językowego! wybierz istniejący pakiet językowy.", + "changedLanguage": "Pomyślnie zmieniono pakiet językowy na: `{0}` .", + "setPrefix": "Gotowe! Mój prefix na tym serwerze to teraz `{1}`. Spróbuj uruchomić polecenie `{1}ping`, aby go przetestować.", + "setDJ": "Ustawiono DJ jako {0}.", + "setqueue": "Zmienionio tryb kolejki na `{0}`.", + "247": "Tryb 24/7: `{0}`", + "bypassVote": "System głosowania: `{0}`", + "setVolume": "Ustawiono głośność na `{0}`%", + "togglecontroller": "Kontroler muzyki: `{0}`", + "toggleDuplicateTrack": "Unikanie duplikowania pozycji w kolejce: `{0}`", + "toggleControllerMsg": "Wiadomości od kontrolera muzyki: `{0}`", + "settingsMenu": "Ustawnienia serwera | {0}", + "settingsTitle": "❤️ Podstawowe informacje:", + "settingsValue": "```Prefix: {0}\nJęzyk: {1}\nKontroler muzyki: {2}\nRola DJ: @{3}\nOminięcie głosowania: {4}\n24/7: {5}\nDomyślna głośność: {6}%\nCzas odtwarzania: {7}```", + "settingsTitle2": "🔗 Informacje o kolejce:", + "settingsValue2": "```Tryb kolejki: {0}\nMaksymlna liczba utworów: {1}\nZduplikowane pozycje: {2}```", + "settingsTitle3": "🎤 Informacie w statusie kanału głosowego:", + "settingsPermTitle": "✨ Uprawnienia:", + "settingsPermValue": "```{0} Administrator\n{1} Zarządzanie_serwerem\n{2} Zarządzanie_kanałem\n{3} Zarządzanie_wiadomościami```", + "pingTitle1": "Informacje o bocie:", + "pingTitle2": "Infomracje o odtwarzaczu:", + "pingfield1": "```ID Shard'a: {0}/{1}\nOpóźnienie shard'a: {2:.3f}s {3}\nRegion: {4}```", + "pingfield2": "```Węzeł: {0} - {1:.3f}s\nOdtwarzacze: {2}\nRegion: {3}```", + "addEffect": "Nałożono efekt: `{0}`", + "clearEffect": "Efekty dźwiękowe zostały wyczyszczone.", + "FilterTagAlreadyInUse": "Ten efekt dźwiękowy jest już w używany! Użyj /cleareffect aby go wyłączyć.", + + "playlistViewTitle": "📜 Playlisty użytkownika {0}:", + "playlistViewHeaders": "ID:,Czas:,Nazwa:,Ilość pozycji:", + "playlistFooter": "Użyj /playlist play [nazwa_playlisty] by dodać playlistę do kolejki.", + "playlistNotFound": "Playlista [`{0}`] nie została znaleziona. Użyj /playlist view by zobaczyć wszystkie swoje playlisty.", + "playlistNotAccess": "Przepraszamy! Nie masz uprawnień do tej playlisty!", + "playlistNoTrack": "Bład! W playliście [`{0}`] nie ma utworów.", + "playlistNotAllow": "Ta komenda jest niedozwolona w odniesienu do udostępnionych playlist.", + "playlistPlay": "Dodano playlistę [`{0}`] z `{1}` pozycjami do kolejki.", + "playlistOverText": "Bład! Nazwa playlisty nie może być dłuższa niż 10 znaków.", + "playlistSameName": "Bład! Nowa nazwa musi się różnić od starej.", + "playlistDeleteError": "Nie możesz usuwać domyślnej playlisty", + "playlistRemove": "Usunąłeś playlistę [`{0}`].", + "playlistSendErrorPlayer": "Błąd! Nie możesz wysłać zaproszenia do samego siebie.", + "playlistSendErrorBot": "Błąd! Nie możesz wysyłać zaproszeń do bota.", + "playlistBelongs": "Błąd! Ta playlista należy do <@{0}>.", + "playlistShare": "Błąd! Ta playlista została udostępniona {0}.", + "playlistSent": "Błąd! Zaproszenie zostało już wcześniej wysłane.", + "noPlaylistAcc": "{0} nie stworzył konta do przechowywania playlist.", + "overPlaylistCreation": "Nie możesz stworzyć więcej niż `{0}` playlist!", + "playlistExists": "Playlista [`{0}`] już istnieje.", + "playlistNotInvaildUrl": "Podaj poprawny link do playlisty z Spotify lub Youtube.", + "playlistCreated": "Stworzyłeś playlistę: `{0}`. Użyj /playlist view by uzyskać więcej informacji.", + "playlistRenamed": "Zmieniłeś nazwę `{0}` na `{1}`.", + "playlistLimitTrack": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.", + "playlistPlaylistLink": "Linki do playlist są nie obsługiwane.", + "playlistStream": "Nie możesz dodać transmisji na żywo do playlisty.", + "playlistPositionNotFound": "Pozycja `{0}` z playlisty [`{1}`] nie została znaleziona!", + "playlistRemoved": "👋 Usunięto **{0}** z playlisty [`{2}`] użytkownika {1}.", + "playlistClear": "Pomyślnie wyczyściłeś swoja playlistę: [`{0}`].", + "playlistView": "Szczegóły playlisty", + "playlistViewDesc": "```Nazwa | ID: {0} | {1}\nLiczba utworów: {2}\nWłaściciel: {3}\nTyp: {4}\n```", + "playlistViewPermsValue": "📖 Odczyt: ✓ ✍🏽 Zapis: {0} 🗑️ Usuwanie: {1}", + "playlistViewPermsValue2": "📖 Odczyt: {0}", + "playlistViewTrack": "Utwory", + "playlistViewPage": "Strona: {0}/{1} | Całkowity czas trwania: {2}", + "inboxFull": "Błąd! Skrzynka odbiorcza {0} jest pełna.", + "inboxNoMsg": "W twojej skrzynce odbiorczej nie ma wiadomości.", + "invitationSent": "Zaproszenie wysłane do {0}.", + + "notInChannel": "{0}, musisz być na kanale {1} by używać komend głosowych. Jeżeli jesteś na kanale połącz się ponownie!", + "noTrackPlaying": "Aktualnie nic nie jest odtwarzane", + "noTrackFound": "Nie znaleziono utworu! Podaj poprawny adres URL.", + "noLinkSupport": "Komenda /search nie obsługuje linków!", + "voted": "Zagłosowałeś!", + "missingPerms_pos": "Tylko DJ lub administracja może zmieniać pozycje.", + "missingPerms_mode": "Tylko DJ lub administracja może zmieniać tryb pętli.", + "missingPerms_queue": "Tylko DJ lub administracja może usuwać pozycje z kolejki.", + "missingPerms_autoplay": "Tylko DJ lub administracja może włączać lub wyłączać tryb autoplay!", + "missingPerms_function": "Tylko DJ lub administracja może używać tej funkcji.", + "timeFormatError": "Niepoprawny format czasu. Przykład: 2:42 lub 12:39:31", + "lyricsNotFound": "Tekst nie został znaleziony. Użyj /lyrics by znaleźć tekst piosenki.", + "missingTrackInfo": "Brakuje niektórych informacji o utworze.", + "noVoiceChannel": "Kanał nie został znaleziony", + + "playlistAddError": "Niemożesz dodawać transmisji na żywo do playlisty!", + "playlistAddError2": "Podczas dodawania utworu do playlisty wystąpił błąd!", + "playlistlimited": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.", + "playlistrepeated": "Ten utwór znajduje sie już na playliście!", + "playlistAdded": "❤️ Dodano **{0}** do playlisty [`{2}`] użytkownika {1}!", + + "playerDropdown": "Wybierz pozycję do której pominąć ...", + "playerFilter": "Wybierz efekt który chesz nałożyć ...", + + "buttonBack": "Cofnij", + "buttonPause": "Pauza", + "buttonResume": "Wznów", + "buttonSkip": "Pomiń", + "buttonLeave": "Opuść", + "buttonLoop": "Pętla", + "buttonVolumeUp": "Głośność +", + "buttonVolumeDown": "Głośność -", + "buttonVolumeMute": "Wycisz", + "buttonVolumeUnmute": "Odcisz", + "buttonAutoPlay": "Autoodtwarzanie", + "buttonShuffle": "Losowe odtwarzanie", + "buttonForward": "Przewiń do przodu", + "buttonRewind": "Przewiń do tyłu", + "buttonLyrics": "Tekst utworu", + + "nowplayingDesc": "**Aktualnie odtwarzane:**\n```{0}```", + "nowplayingField": "Następne:", + "nowplayingLink": "Słuchaj na: {0}", + + "connect": "Połączono do {0}", + + "live": "LIVE", + "playlistLoad": " 🎶 Dodano playlistę **{0}** z `{1}` pozycjami do kolejki.", + "trackLoad": "Rozpoczęto odtwarzanie **[{0}](<{1}>)** autorstwa **{2}** (`{3}`).\n", + "trackLoad_pos": "Dodano **[{0}](<{1}>)** autorstwa **{3}** (`{3}`) do kolejki na pozycji **{4}**\n", + + "searchTitle": "Wyszukiwana fraza: {0}", + "searchDesc": "➥ Platforma: {0} **{1}**\n➥ Wyniki: **{2}**\n\n{3}", + "searchWait": "Wybierz utwór, który chcesz dodać do playlisty.", + "searchTimeout": "Upłynął czas wyszukiwania. Spróbuj ponownie później.", + "searchSuccess": "Dodano utwór do kolejki.", + + "queueTitle": "Nadchodząca kolejka:", + "historyTitle": "Historia kolejki:", + "viewTitle": "Kolejka utworów", + "viewDesc": "**Aktualnie odtwarzane: [Kliknij mnie]({0}) ⮯**\n{1}", + "viewFooter": "Strona: {0}/{1} | Całkowity czas trwania: {2}", + + "pauseError": "Odtwarzacz jest już zapauzowany.", + "pauseVote": "{0} zagłosował nad zapauzowaniem odtwarzania. [{1}/{2}]", + "paused": "`{0}` zapauzował odtwarznaie.", + "resumeError": "Odtwarzacz nie jest zapauzowany.", + "resumeVote": "{0} zagłosował nad wznowieniem odtwarzania. [{1}/{2}]", + "resumed": "`{0}` wznowił odtwarzanie.", + "shuffleError": "Dodaj więcej utworów przed przemieszaniem kolejki.", + "shuffleVote": "{0} zagłosował nad przemieszaniem kolejki. [{1}/{2}]", + "shuffled": "Kolejka została przemieszana.", + "skipError": "Nie ma utworu, do którego można by pominąć.", + "skipVote": "{0} zagłosował za pominięciem tego utworu. [{1}/{2}]", + "skipped": "`{0}` pominął utwór.", + + "backVote": "{0} zagłosował za przejściem do poprzedniego utworu. [{1}/{2}]", + "backed": "`{0}` cofnął do poprzedniego utworu.", + + "leaveVote": "{0} zagłosował nad zatrzyamniem odtwarzaza. [{1}/{2}]", + "left": "`{0}` zatrzymał odtwarzacz.", + + "seek": "Ustawiono odtwarzacz na **{0}**", + "repeat": "Tryb pętli został ustawiony na: `{0}`", + "cleared": "Wyczyszczono wszystkie utwory w: `{0}`", + "removed": "`{0}` utworów zostało usunięte z kolejki.", + "forward": "Przewinięto odtwarzacz do **{0}**", + "rewind": "Cofnięto odtwarzacz do **{0}**", + "replay": "Ponowne odtwarzanie bieżącego utworu.", + "swapped": "`{0}` oraz `{1}` są zamienione", + "moved": "Przeniesiono `{0}` do `{1}`", + "autoplay": "Tryb autoodwarzania jest teraz **{0}**", + + "notdj": "Nie jesteś DJ'em. Obecnym DJ'em jest {0}.", + "djToMe": "Nie możesz przekazać roli DJ'a sobie lub botu.", + "djnotinchannel": "`{0}` nie jest na kanale głosowym.", + "djswap": "Przekazałeś rolę DJ'a użytkownikowi `{0}`.", + + "chaptersDropdown": "Wybierz rozdział do którego przeskoczyć ...", + "noChaptersFound": "Nie znaleziono żadnych rozdziałów!", + "chatpersNotSupport": "Ta komenda obsługuje jedynie utwory z YouTube'a!", + + "voicelinkQueueFull": "Przepraszamy osiągnięto limit `{0}` pozycji w kolejce!", + "voicelinkOutofList": "Podaj właściwy indeks utworu!", + "voicelinkDuplicateTrack": "Błąd! Ten utwór znajduje się już w kolejce.", + + "deocdeError": "Coś poszło nie tak podczas dekodowania pliku!", + "invalidStartTime": "Niepoprawny czas startu, musi on mieścić się pomiędzy `00:00` a `{0}`", + "invalidEndTime": "Niepoprawny czas końca, musi on mieścić się pomiędzy `00:00` a `{0}`", + "invalidTimeOrder": "Czas zakończenia odtwarzania nie może być mniejszy lub równy czasowi startu", + + "SetStageAnnounceTemplate": "Gotowe! Od tego momentu status kanału w którym znajduje się bot będzie wyglądał tak jak podany szablon. Aktualizacja powinna nastąpić za kilka sekund.", + "createSongRequestChannel": "Utworzono kanał do obsługi bota - ({0})! Możesz w nim dodawać utwory po nazwie lub adresie URL bez dodawnia prefixu" +} \ No newline at end of file From c1c39fd1c5c1de031c49cbebb61da477337b4bd5 Mon Sep 17 00:00:00 2001 From: VeeoN Date: Sat, 12 Apr 2025 20:42:11 +0200 Subject: [PATCH 03/12] Fixed few issues in PL language file. --- langs/PL.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/langs/PL.json b/langs/PL.json index f084e85..e2d8d40 100644 --- a/langs/PL.json +++ b/langs/PL.json @@ -14,7 +14,7 @@ "missingIntents": "Niestety to polecenie nie może zostać wykonane, ponieważ bot nie posiada wymaganej intencji żądania: `({0})`.", "languageNotFound": "Nie znaleziono pakietu językowego! wybierz istniejący pakiet językowy.", "changedLanguage": "Pomyślnie zmieniono pakiet językowy na: `{0}` .", - "setPrefix": "Gotowe! Mój prefix na tym serwerze to teraz `{1}`. Spróbuj uruchomić polecenie `{1}ping`, aby go przetestować.", + "setPrefix": "Gotowe! Mój prefix na tym serwerze to teraz `{0}`. Spróbuj uruchomić polecenie `{1}ping`, aby go przetestować.", "setDJ": "Ustawiono DJ jako {0}.", "setqueue": "Zmienionio tryb kolejki na `{0}`.", "247": "Tryb 24/7: `{0}`", @@ -59,7 +59,7 @@ "noPlaylistAcc": "{0} nie stworzył konta do przechowywania playlist.", "overPlaylistCreation": "Nie możesz stworzyć więcej niż `{0}` playlist!", "playlistExists": "Playlista [`{0}`] już istnieje.", - "playlistNotInvaildUrl": "Podaj poprawny link do playlisty z Spotify lub Youtube.", + "playlistNotInvalidUrl": "Podaj poprawny link do playlisty z Spotify lub Youtube.", "playlistCreated": "Stworzyłeś playlistę: `{0}`. Użyj /playlist view by uzyskać więcej informacji.", "playlistRenamed": "Zmieniłeś nazwę `{0}` na `{1}`.", "playlistLimitTrack": "Osiągnąłeś limit! Możesz dodać maksymalnie `{0}` pozycji do playlisty.", @@ -127,7 +127,7 @@ "live": "LIVE", "playlistLoad": " 🎶 Dodano playlistę **{0}** z `{1}` pozycjami do kolejki.", "trackLoad": "Rozpoczęto odtwarzanie **[{0}](<{1}>)** autorstwa **{2}** (`{3}`).\n", - "trackLoad_pos": "Dodano **[{0}](<{1}>)** autorstwa **{3}** (`{3}`) do kolejki na pozycji **{4}**\n", + "trackLoad_pos": "Dodano **[{0}](<{1}>)** autorstwa **{2}** (`{3}`) do kolejki na pozycji **{4}**\n", "searchTitle": "Wyszukiwana fraza: {0}", "searchDesc": "➥ Platforma: {0} **{1}**\n➥ Wyniki: **{2}**\n\n{3}", @@ -189,6 +189,6 @@ "invalidEndTime": "Niepoprawny czas końca, musi on mieścić się pomiędzy `00:00` a `{0}`", "invalidTimeOrder": "Czas zakończenia odtwarzania nie może być mniejszy lub równy czasowi startu", - "SetStageAnnounceTemplate": "Gotowe! Od tego momentu status kanału w którym znajduje się bot będzie wyglądał tak jak podany szablon. Aktualizacja powinna nastąpić za kilka sekund.", + "setStageAnnounceTemplate": "Gotowe! Od tego momentu status kanału w którym znajduje się bot będzie wyglądał tak jak podany szablon. Aktualizacja powinna nastąpić za kilka sekund.", "createSongRequestChannel": "Utworzono kanał do obsługi bota - ({0})! Możesz w nim dodawać utwory po nazwie lub adresie URL bez dodawnia prefixu" } \ No newline at end of file From ac56a22caae143f838ddfad13ba15099ad5ad7d7 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:17:10 +0800 Subject: [PATCH 04/12] Updated the transformer --- voicelink/__init__.py | 6 +- voicelink/formatter.py | 242 ----------------------------- voicelink/objects.py | 4 +- voicelink/player.py | 4 +- voicelink/transformer.py | 322 +++++++++++++++++++++++++++++++++++++++ voicelink/utils.py | 3 +- 6 files changed, 330 insertions(+), 251 deletions(-) delete mode 100644 voicelink/formatter.py create mode 100644 voicelink/transformer.py diff --git a/voicelink/__init__.py b/voicelink/__init__.py index e14e65a..bedf3f3 100644 --- a/voicelink/__init__.py +++ b/voicelink/__init__.py @@ -22,9 +22,9 @@ SOFTWARE. """ __version__ = "1.4" -__author__ = 'Vocard Development, Choco' +__author__ = 'Vocard Development, ChocoMeow' __license__ = "MIT" -__copyright__ = "Copyright 2023 - present (c) Vocard Development, Choco" +__copyright__ = "Copyright 2023 - present (c) Vocard Development, ChocoMeow" from .enums import SearchType, LoopType from .events import * @@ -35,4 +35,4 @@ from .player import Player, connect_channel from .pool import * from .queue import * from .placeholders import Placeholders, build_embed -from .formatter import encode, decode +from .transformer import encode, decode diff --git a/voicelink/formatter.py b/voicelink/formatter.py deleted file mode 100644 index 71480fd..0000000 --- a/voicelink/formatter.py +++ /dev/null @@ -1,242 +0,0 @@ -"""MIT License - -Copyright (c) 2023 - present Vocard Development - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from __future__ import annotations - -import base64, io, abc, struct, dataclasses - -from typing import Union, BinaryIO, Optional, TYPE_CHECKING -if TYPE_CHECKING: - from .objects import Track - -@dataclasses.dataclass(frozen=True) -class Codec: - encoding: str - error_handler: str - - def encode(self, data: str) -> bytes: - return data.encode(self.encoding, self.error_handler) - - def decode(self, data: bytes) -> str: - return data.decode(self.encoding, self.error_handler) - -UTF8 = Codec("utf-8", "surrogatepass") - -_FORMAT_BOOL = "?" -_FORMAT_BYTE = "b" -_FORMAT_INT = ">i" -_FORMAT_LONG = ">q" -_FORMAT_USHORT = ">H" - -class HasStream(abc.ABC): - @property - @abc.abstractmethod - def stream(self) -> BinaryIO: - ... - -class Reader(HasStream): - def __init__(self, stream: Union[BinaryIO, HasStream]) -> None: - self._stream: BinaryIO = stream.stream if isinstance(stream, HasStream) else stream - - @property - def stream(self) -> BinaryIO: - return self._stream - - def read_bool(self) -> bool: - return struct.unpack(_FORMAT_BOOL, self._stream.read(1))[0] - - def read_byte(self) -> int: - return struct.unpack(_FORMAT_BYTE, self._stream.read(1))[0] - - def read_int(self) -> int: - return struct.unpack(_FORMAT_INT, self._stream.read(4))[0] - - def read_long(self) -> int: - return struct.unpack(_FORMAT_LONG, self._stream.read(8))[0] - - def read_ushort(self) -> int: - return struct.unpack(_FORMAT_USHORT, self._stream.read(2))[0] - - def read_utf(self) -> str: - length = self.read_ushort() - data = self._stream.read(length) - return UTF8.decode(data) - - def read_optional_utf(self) -> Optional[str]: - if self.read_bool(): - return self.read_utf() - else: - return None - -class Writer: - _stream: BinaryIO - - def __init__(self, stream: Union[BinaryIO, HasStream] = None ) -> None: - if stream is None: - stream = io.BytesIO() - elif isinstance(stream, HasStream): - stream = stream.stream - - self._stream = stream - self._string_codec: Codec = UTF8 - - @property - def stream(self) -> BinaryIO: - return self._stream - - def write_bool(self, data: bool) -> None: - self._stream.write(struct.pack(_FORMAT_BOOL, data)) - - def write_byte(self, data: int) -> None: - self._stream.write(struct.pack(_FORMAT_BYTE, data)) - - def write_int(self, data: int) -> None: - self._stream.write(struct.pack(_FORMAT_INT, data)) - - def write_long(self, data: int) -> None: - self._stream.write(struct.pack(_FORMAT_LONG, data)) - - def write_ushort(self, data: int) -> None: - self._stream.write(struct.pack(_FORMAT_USHORT, data)) - - def write_utf(self, data: str) -> None: - data = self._string_codec.encode(data) - self.write_ushort(len(data)) - self._stream.write(data) - - def write_optional_utf(self, data: Optional[str]) -> None: - if data is None: - self.write_bool(False) - else: - self.write_bool(True) - self.write_utf(data) - -class MessageInput(HasStream): - def __init__(self, stream: Union[BinaryIO, HasStream]) -> None: - self._stream: Reader = Reader(stream) - self._flags: int = 0 - self._size: int = 0 - - @property - def stream(self) -> BinaryIO: - return self._stream.stream - - @property - def flags(self) -> int: - return self._flags - - def next(self) -> Optional[Reader]: - value = self._stream.read_int() - self._flags = (value & 0xC0000000) >> 30 - self._size = value & 0x3FFFFFFF - - if not self._size: - return None - - data = self._stream.stream.read(self._size) - - return Reader(io.BytesIO(data)) - -class MessageOutput(HasStream): - _stream: Writer - _body_stream: io.BytesIO - - def __init__(self, stream: Union[BinaryIO, HasStream]) -> None: - self._string_codec: Codec = UTF8 - - self._stream = Writer(stream) - self._body_stream = io.BytesIO() - - @property - def stream(self) -> BinaryIO: - return self._stream.stream - - def start(self) -> Writer: - self._body_stream.truncate(0) - self._body_stream.seek(0) - return Writer(self._body_stream) - - def commit(self, flags: int = None) -> None: - data = self._body_stream.getvalue() - header = len(data) - if flags: - header |= flags << 30 - - self._stream.write_int(header) - self._stream.stream.write(data) - - def finish(self) -> None: - self._stream.write_int(0) - -class TrackDecoder: - """TrackDecoder for track messages.""" - - def decode(self, stream: MessageInput): - """Decode an entire message and return the `Track`.""" - - body_reader = stream.next() - if not body_reader: - raise ValueError("empty stream") - - version = body_reader.read_byte() - - return { - "title": body_reader.read_utf(), - "author": body_reader.read_utf(), - "length": body_reader.read_long(), - "identifier": body_reader.read_utf(), - "is_stream": body_reader.read_bool(), - "uri": body_reader.read_optional_utf(), - "artworkUrl": None if version not in [0, 3] else body_reader.read_optional_utf(), - "isrc": None if version != 3 else body_reader.read_optional_utf(), - "sourceName": body_reader.read_utf(), - "position": body_reader.read_long() - } - -class TrackEncoder: - def encode(self, stream: MessageOutput, track: Track) -> None: - body_writer = stream.start() - - body_writer.write_byte(0) - body_writer.write_utf(track.title) - body_writer.write_utf(track.author) - body_writer.write_long(track.length) - body_writer.write_utf(track.identifier) - body_writer.write_bool(track.is_stream) - body_writer.write_optional_utf(track.uri) - body_writer.write_optional_utf(track.thumbnail) - body_writer.write_utf(track.source) - body_writer.write_long(0) - - stream.commit() - -def decode(data: Union[str, bytes]) -> dict: - decoded = base64.b64decode(data) - stream = MessageInput(io.BytesIO(decoded)) - return TrackDecoder().decode(stream) - -def encode(track) -> bytes: - buf = io.BytesIO() - stream = MessageOutput(buf) - TrackEncoder().encode(stream, track) - return base64.b64encode(buf.getvalue()).decode("utf-8") diff --git a/voicelink/objects.py b/voicelink/objects.py index 56fbd46..2852fd6 100644 --- a/voicelink/objects.py +++ b/voicelink/objects.py @@ -33,7 +33,7 @@ from function import ( time as ctime ) -from .formatter import encode +from .transformer import encode YOUTUBE_REGEX = re.compile(r'(https?://)?(www\.)?youtube\.(com|nl)/watch\?v=([-\w]+)') @@ -108,7 +108,7 @@ class Track: @property def track_id(self) -> str: if not self._track_id: - self._track_id = encode(self) + self._track_id = encode(self.info) return self._track_id diff --git a/voicelink/player.py b/voicelink/player.py index 5db80cc..81148e6 100644 --- a/voicelink/player.py +++ b/voicelink/player.py @@ -582,11 +582,11 @@ class Player(VoiceProtocol): data = { "encodedTrack": track.track_id, - "position": str(start if start else track.position) + "position": str(start or 0) } if end or track.end_time: - data["endTime"] = str(end if end else track.end_time) + data["endTime"] = str(end or track.end_time) await self.send(method=RequestMethod.PATCH, query=f"noReplace={ignore_if_playing}", data=data) if self._node.yt_ratelimit: diff --git a/voicelink/transformer.py b/voicelink/transformer.py new file mode 100644 index 0000000..eeb4b37 --- /dev/null +++ b/voicelink/transformer.py @@ -0,0 +1,322 @@ +""" +MIT License + +Copyright (c) 2017-present Devoxin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import struct + +from io import BytesIO +from base64 import b64decode, b64encode +from typing import Any, Callable, Dict, Mapping, Optional, Tuple, Final + +V2_KEYSET = {'title', 'author', 'length', 'identifier', 'isStream', 'uri', 'sourceName', 'position'} +V3_KEYSET = V2_KEYSET | {'artworkUrl', 'isrc'} + +class _MissingObj: + __slots__ = () + + def __repr__(self): + return '...' + +MISSING: Any = _MissingObj() + +class DataReader: + __slots__ = ('_buf', '_mark') + + def __init__(self, base64_str: str): + self._buf: Final[BytesIO] = BytesIO(b64decode(base64_str)) + self._mark: Optional[int] = None + + @property + def remaining(self) -> int: + return self._buf.getbuffer().nbytes - self._buf.tell() + + def mark(self) -> None: + self._mark = self._buf.tell() + + def rewind(self) -> None: + if self._mark is None or not isinstance(self._mark, int): + raise IOError('Cannot rewind buffer without a marker!') + + if self._mark < 0: + raise IOError('Cannot rewind buffer to a negative position!') + + self._buf.seek(self._mark) + self._mark = None + + def _read(self, count: int) -> bytes: + return self._buf.read(count) + + def read_byte(self) -> bytes: + return self._read(1) + + def read_boolean(self) -> bool: + result, = struct.unpack('B', self.read_byte()) + return result != 0 + + def read_unsigned_short(self) -> int: + result, = struct.unpack('>H', self._read(2)) + return result + + def read_int(self) -> int: + result, = struct.unpack('>i', self._read(4)) + return result + + def read_long(self) -> int: + result, = struct.unpack('>Q', self._read(8)) + return result + + def read_nullable_utf(self, utfm: bool = False) -> Optional[str]: + exists = self.read_boolean() + + if not exists: + return None + + return self.read_utfm() if utfm else self.read_utf().decode() + + def read_utf(self) -> bytes: + text_length = self.read_unsigned_short() + return self._read(text_length) + + def read_utfm(self) -> str: + text_length = self.read_unsigned_short() + utf_string = self._read(text_length) + return read_utfm(text_length, utf_string) + +class DataWriter: + __slots__ = ('_buf',) + + def __init__(self): + self._buf: Final[BytesIO] = BytesIO() + + def _write(self, data): + self._buf.write(data) + + def write_byte(self, byte): + self._buf.write(byte) + + def write_boolean(self, boolean: bool): + enc = struct.pack('B', 1 if boolean else 0) + self.write_byte(enc) + + def write_unsigned_short(self, short: int): + enc = struct.pack('>H', short) + self._write(enc) + + def write_int(self, integer: int): + enc = struct.pack('>i', integer) + self._write(enc) + + def write_long(self, long_value: int): + enc = struct.pack('>Q', long_value) + self._write(enc) + + def write_nullable_utf(self, utf_string: Optional[str]): + self.write_boolean(bool(utf_string)) + + if utf_string: + self.write_utf(utf_string) + + def write_utf(self, utf_string: str): + utf = utf_string.encode('utf8') + byte_len = len(utf) + + if byte_len > 65535: + raise OverflowError('UTF string may not exceed 65535 bytes!') + + self.write_unsigned_short(byte_len) + self._write(utf) + + def finish(self) -> bytes: + with BytesIO() as track_buf: + byte_len = self._buf.getbuffer().nbytes + flags = byte_len | (1 << 30) + enc_flags = struct.pack('>i', flags) + track_buf.write(enc_flags) + + self._buf.seek(0) + track_buf.write(self._buf.read()) + self._buf.close() + + track_buf.seek(0) + return track_buf.read() + +def decode_probe_info(reader: DataReader) -> Mapping[str, Any]: + probe_info = reader.read_utf().decode() + return {'probe_info': probe_info} + +def decode_lavasrc_fields(reader: DataReader) -> Mapping[str, Any]: + if reader.remaining <= 8: + return {} + + album_name = reader.read_nullable_utf() + album_url = reader.read_nullable_utf() + artist_url = reader.read_nullable_utf() + artist_artwork_url = reader.read_nullable_utf() + preview_url = reader.read_nullable_utf() + is_preview = reader.read_boolean() + + return { + 'album_name': album_name, + 'album_url': album_url, + 'artist_url': artist_url, + 'artist_artwork_url': artist_artwork_url, + 'preview_url': preview_url, + 'is_preview': is_preview + } + +DEFAULT_DECODER_MAPPING: Dict[str, Callable[[DataReader], Mapping[str, Any]]] = { + 'http': decode_probe_info, + 'local': decode_probe_info, + 'deezer': decode_lavasrc_fields, + 'spotify': decode_lavasrc_fields, + 'applemusic': decode_lavasrc_fields +} + +def read_utfm(utf_len: int, utf_bytes: bytes) -> str: + chars = [] + count = 0 + + while count < utf_len: + char = utf_bytes[count] & 0xff + if char > 127: + break + + count += 1 + chars.append(chr(char)) + + while count < utf_len: + char = utf_bytes[count] & 0xff + shift = char >> 4 + + if 0 <= shift <= 7: + count += 1 + chars.append(chr(char)) + elif 12 <= shift <= 13: + count += 2 + if count > utf_len: + raise UnicodeDecodeError('utf8', b'', 0, utf_len, 'malformed input: partial character at end') + char2 = utf_bytes[count - 1] + if (char2 & 0xC0) != 0x80: + raise UnicodeDecodeError('utf8', b'', 0, utf_len, f'malformed input around byte {count}') + + char_shift = ((char & 0x1F) << 6) | (char2 & 0x3F) + chars.append(chr(char_shift)) + elif shift == 14: + count += 3 + if count > utf_len: + raise UnicodeDecodeError('utf8', b'', 0, utf_len, 'malformed input: partial character at end') + + char2 = utf_bytes[count - 2] + char3 = utf_bytes[count - 1] + + if (char2 & 0xC0) != 0x80 or (char3 & 0xC0) != 0x80: + raise UnicodeDecodeError('utf8', b'', 0, utf_len, f'malformed input around byte {(count - 1)}') + + char_shift = ((char & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0) + chars.append(chr(char_shift)) + else: + raise UnicodeDecodeError('utf8', b'', 0, utf_len, f'malformed input around byte {count}') + + return ''.join(chars).encode('utf-16', 'surrogatepass').decode('utf-16') + +def _read_track_common(reader: DataReader) -> Tuple[str, str, int, str, bool, Optional[str]]: + title = reader.read_utfm() + author = reader.read_utfm() + length = reader.read_long() + identifier = reader.read_utf().decode() + is_stream = reader.read_boolean() + uri = reader.read_nullable_utf() + return (title, author, length, identifier, is_stream, uri) + +def _write_track_common(track: Dict[str, Any], writer: DataWriter): + writer.write_utf(track['title']) + writer.write_utf(track['author']) + writer.write_long(track['length']) + writer.write_utf(track['identifier']) + writer.write_boolean(track['isStream']) + writer.write_nullable_utf(track['uri']) + +def decode( + track: str, + source_decoders: Mapping[str, Callable[[DataReader], Mapping[str, Any]]] = MISSING +) -> dict: + + decoders = DEFAULT_DECODER_MAPPING.copy() + + if source_decoders is not MISSING: + decoders.update(source_decoders) + + reader = DataReader(track) + + flags = (reader.read_int() & 0xC0000000) >> 30 + version, = struct.unpack('B', reader.read_byte()) if flags & 1 != 0 else (1,) + + title, author, length, identifier, is_stream, uri = _read_track_common(reader) + extra_fields = {} + + if version == 3: + extra_fields['artworkUrl'] = reader.read_nullable_utf() + extra_fields['isrc'] = reader.read_nullable_utf() + + source = reader.read_utf().decode() + source_specific_fields = {} + + if source in decoders: + source_specific_fields.update(decoders[source](reader)) + + position = reader.read_long() + + return { + 'title': title, + 'author': author, + 'length': length, + 'identifier': identifier, + 'isStream': is_stream, + 'uri': uri, + 'isSeekable': not is_stream, + 'sourceName': source, + 'position': position, + **extra_fields + } + +def encode( + track: Dict[str, Any], + source_encoders: Mapping[str, Callable[[DataWriter, Dict[str, Any]], None]] = MISSING +) -> str: + assert V3_KEYSET <= track.keys() + + writer = DataWriter() + version = struct.pack('B', 3) + writer.write_byte(version) + _write_track_common(track, writer) + writer.write_nullable_utf(track['artworkUrl']) + writer.write_nullable_utf(track['isrc']) + writer.write_utf(track['sourceName']) + + if source_encoders is not MISSING and track['sourceName'] in source_encoders: + source_encoders[track['sourceName']](writer, track) + + writer.write_long(track['position']) + + enc = writer.finish() + return b64encode(enc).decode() \ No newline at end of file diff --git a/voicelink/utils.py b/voicelink/utils.py index 3a6c4bc..fbb2689 100644 --- a/voicelink/utils.py +++ b/voicelink/utils.py @@ -203,5 +203,4 @@ class Ping: ((self._host, self._port), None)) s_runtime = 1000 * (cost_time) - return s_runtime - + return s_runtime \ No newline at end of file From d23aba15db602b46c836f4fdf1bc93badf014c95 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:17:16 +0800 Subject: [PATCH 05/12] Update application.yml --- lavalink/application.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lavalink/application.yml b/lavalink/application.yml index 5969f11..9c17b12 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -100,10 +100,10 @@ plugins: recommendationsLoadLimit: 10 # Number of tracks lavalink: plugins: - - dependency: "dev.lavalink.youtube:youtube-plugin:1.11.5" + - dependency: "dev.lavalink.youtube:youtube-plugin:1.12.0" + snapshot: false + - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.6.0" snapshot: false - - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:06b7cab" - snapshot: true # - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin # repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default # snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository From acee4160551dd0e45962d2810d9b65bc1b02d1eb Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:07:36 +0300 Subject: [PATCH 06/12] Improved RU translations --- langs/RU.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/langs/RU.json b/langs/RU.json index 3d352bd..07fed11 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -21,8 +21,8 @@ "bypassVote": "Теперь у вас `{0}` система голосования.", "setVolume": "Установить громкость на `{0}`%", "togglecontroller": "Теперь у вас `{0}` контроллер музыки.", - "toggleDuplicateTrack": "Теперь у вас `{0}` предотвращение дублирования трека в очереди.", - "toggleControllerMsg": "Теперь у вас `{0}` сообщения от контроллера музыки.", + "toggleDuplicateTrack": "Теперь у вас `{0}` детектор дубликатов в очереди.", + "toggleControllerMsg": "Теперь у вас `{0}` сообщения от плеера.", "settingsMenu": "Настройки сервера | {0}", "settingsTitle": "❤️ Основная информация:", "settingsValue": "```Префикс: {0}\nЯзык: {1}\nВключить контроллер музыки: {2}\nDJ роль: @{3}\nОбход голосования: {4}\n24/7: {5}\nГромкость по умолчанию: {6}%\nВремя проигрывания: {7}```", @@ -35,7 +35,7 @@ "pingTitle2": "Информация о плеере:", "pingfield1": "```ID Шарда: {0}/{1}\nЗадержка Шарда: {2:.3f}s {3}\nРегион: {4}```", "pingfield2": "```Сервис: {0} - {1:.3f}s\nИгроки: {2}\nРегион Голоса: {3}```", - "addEffect": "Примените эффект `{0}` фильтр.", + "addEffect": "Применен `{0}` эффект.", "clearEffect": "Звуковые эффекты были очищены!", "FilterTagAlreadyInUse": "Этот звуковой эффект уже используется! Пожалуйста, используйте /cleareffect <Тег>, чтобы удалить его.", @@ -96,7 +96,7 @@ "playlistAddError": "Вам не разрешено добавлять потоковые видео в плейлист!", "playlistAddError2": "Произошла ошибка при добавлении треков в плейлист!", "playlistlimited": "Вы достигли лимита! Вы можете добавить только {0} треков в свой плейлист.", - "playlistrepeated": "Такой же трек уже есть в вашем плейлисте!", + "playlistrepeated": "Такой трек уже есть в плейлисте!", "playlistAdded": "❤️ Добавлен **{0}** в плейлист пользователя {1} [`{2}`]!", "playerDropdown": "Выберите трек для перехода ...", @@ -159,7 +159,7 @@ "leaveVote": "Пользователь `{0}` проголосовал за остановку плеера. [{1}/{2}]", "left": "Пользователь `{0}` остановил плеер.", - "seek": "Установить плеер на **{0}**", + "seek": "Перемотать на **{0}**", "repeat": "Режим повтора `{0}`", "cleared": "Очищены все треки в `{0}`", "removed": "Удалено `{0}` треков из очереди.", @@ -184,10 +184,10 @@ "voicelinkDuplicateTrack": "Извините, этот трек уже есть в очереди.", "deocdeError": "Что-то пошло не так при декодировании файла!", - "invalidStartTime": "Hевозможное время начала! Вход времени должен быть внутри `00:00` и `{0}`.", - "invalidEndTime": "Невозможное время конца! Вход времени должен быть внутри `00:00` и `{0}`.", - "invalidTimeOrder": "Время конца не может быть меньше или равно времени начала.", + "invalidStartTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`.", + "invalidEndTime": "Недействительное значение! Диапозон времени должен быть между `00:00` и `{0}`.", + "invalidTimeOrder": "Время конца трека не может быть меньше или равно времени начала.", - "setStageAnnounceTemplate": "Готово! С этого момента статус голоса, как тот, в котором вы находитесь сейчас, будет называться в соответствии с вашим шаблоном. Вы должны увидеть обновление через несколько секунд.", - "createSongRequestChannel": "Канал для запроса песен ({0}) создан! Вы можете начать запрашивать любую песню по названию или URL в этом канале без использования префикса бота." -} \ No newline at end of file + "setStageAnnounceTemplate": "Готово! Новые настройки статуса канала скоро применятся в соответсвии с вашим шаблоном.", + "createSongRequestChannel": "Канал для запроса песен ({0}) создан! Вы можете запросить любую песню по названию или URL в этом канале без использования префикса бота." +} From 619faf9573dc5ad4199c7d596e909b709b0c177e Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:08:27 +0300 Subject: [PATCH 07/12] Update RU.json --- langs/RU.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/RU.json b/langs/RU.json index 07fed11..d0293d0 100644 --- a/langs/RU.json +++ b/langs/RU.json @@ -21,7 +21,7 @@ "bypassVote": "Теперь у вас `{0}` система голосования.", "setVolume": "Установить громкость на `{0}`%", "togglecontroller": "Теперь у вас `{0}` контроллер музыки.", - "toggleDuplicateTrack": "Теперь у вас `{0}` детектор дубликатов в очереди.", + "toggleDuplicateTrack": "Теперь у вас `{0}` фильтр дубликатов в очереди.", "toggleControllerMsg": "Теперь у вас `{0}` сообщения от плеера.", "settingsMenu": "Настройки сервера | {0}", "settingsTitle": "❤️ Основная информация:", From 9edd76e42e25ac0ff258ae7016fd2a15e4b2e8c4 Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:12:49 +0300 Subject: [PATCH 08/12] Update docker-compose.yml Made MongoDB service as a default installation method, added healthcheck for MongoDB, added "dashboard" service(read the docs on how to install it) --- docker-compose.yml | 75 ++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 841cfde..bb4e576 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ # READ THIS BEFORE INSTALL! -# This is a docker-compose file for running Vocard with Lavalink and MongoDB(optional). -# You can selfhost MongoDB. Just uncomment lines starting with single "#" below in the compose file. +# This is a docker-compose file for running Vocard with Lavalink and MongoDB. # In order to run this, you need to have Docker and Docker Compose installed. # You can install Docker from https://docs.docker.com/get-docker/ # and Docker Compose from https://docs.docker.com/compose/install/ @@ -42,9 +41,11 @@ name: vocard services: lavalink: container_name: lavalink - #image: ghcr.io/lavalink-devs/lavalink:latest + # image: ghcr.io/lavalink-devs/lavalink:latest + # Use build to run Lavalink on the latest JRE 23 version (for better performance) build: - dockerfile: ./lavalink/Dockerfile-lavalink + context: ./lavalink + dockerfile: ./Dockerfile-lavalink restart: unless-stopped environment: - _JAVA_OPTIONS=-Xmx1G @@ -61,32 +62,54 @@ services: test: nc -z -v localhost 2333 interval: 10s timeout: 5s - retries: 3 + retries: 5 - #mongo: - # container_name: mongo - # image: mongo:latest - # - # restart: unless-stopped - # volumes: - # - ./data/mongo/db:/data/db - # - ./data/mongo/conf:/data/configdb - # environment: - # - MONGO_INITDB_ROOT_USERNAME=admin - # - MONGO_INITDB_ROOT_PASSWORD=admin - # expose: - # - "27017" - # networks: - # - vocard - # command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"] + vocard-db: + container_name: vocard-db + image: mongo:8 + restart: unless-stopped + volumes: + - ./data/mongo/db:/data/db + - ./data/mongo/conf:/data/configdb + environment: + - MONGO_INITDB_ROOT_USERNAME=admin # For your MongoDB URL use "mongodb://admin:admin@vocard-db:27017" + - MONGO_INITDB_ROOT_PASSWORD=admin + expose: + - "27017" + networks: + - vocard + command: ["mongod", "--oplogSize=1024", "--wiredTigerCacheSizeGB=1", "--auth", "--noscripting"] + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + + # vocard-dashboard: + # container_name: vocard-dashboard + # image: ghcr.io/chocomeow/vocard-dashboard:latest + # restart: unless-stopped + # # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line. + # # build: + # # context: ./dashboard + # # dockerfile: ./Dockerfile + # volumes: + # - ./dashboard/settings.json:/app/settings.json + # ports: + # - 8000:8000 + # networks: + # - vocard + # - web vocard: container_name: vocard restart: unless-stopped # If you want to build the image from the Dockerfile, uncomment the "build" lines and comment the "image" line. - # image: ghcr.io/choco/vocard:latest - build: - dockerfile: ./Dockerfile + image: ghcr.io/chocomeow/vocard:latest + # build: + # dockerfile: ./Dockerfile volumes: - ./settings.json:/app/settings.json networks: @@ -94,8 +117,10 @@ services: depends_on: lavalink: condition: service_healthy - # mongo: + # vocard-dashboard: # condition: service_started + vocard-db: + condition: service_healthy networks: vocard: From 5bd19c65f51638e52a291de53ff36a9e86e6f16c Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:13:48 +0300 Subject: [PATCH 09/12] Optimized Dockerfile build + added JRE 24 support(experimental) --- lavalink/Dockerfile-lavalink | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lavalink/Dockerfile-lavalink b/lavalink/Dockerfile-lavalink index 265ab4d..48804af 100644 --- a/lavalink/Dockerfile-lavalink +++ b/lavalink/Dockerfile-lavalink @@ -1,17 +1,15 @@ -FROM eclipse-temurin:23-jre-noble - -RUN apt-get update && \ - apt-get install -y netcat-openbsd && \ - rm -rf /var/lib/apt/lists/* +FROM eclipse-temurin:24-jre-noble RUN groupadd -g 322 lavalink && \ useradd -u 322 -g lavalink -m -d /opt/Lavalink lavalink WORKDIR /opt/Lavalink -RUN wget -O Lavalink.jar https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar +ADD --chown=lavalink:lavalink https://github.com/lavalink-devs/Lavalink/releases/download/4.0.8/Lavalink.jar . -RUN chown -R lavalink:lavalink /opt/Lavalink +RUN apt-get update && \ + apt-get install -y --no-install-recommends netcat-openbsd && \ + rm -rf /var/lib/apt/lists/* USER lavalink From 65822dca99485211d69e99a6c51c8cc877b5a468 Mon Sep 17 00:00:00 2001 From: Azarath7 <158289825+Azarath7@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:26:40 +0300 Subject: [PATCH 10/12] Update docker-compose.yml small doc fix --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bb4e576..6a74b4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ # build: # dockerfile: ./Dockerfile # and comment this -# image: ghcr.io/choco/vocard:latest +# image: ghcr.io/chocomeow/vocard:latest # example - `root@docker:/opt/vocard/config# wget https://github.com/ChocoMeow/Vocard/archive/refs/heads/main.zip` # Step 4: Configure application.yml and settings.json in the config directory. From 0f077d49e8a56bbc443aa9a05940a25f3d9e9b6b Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:40:24 +0800 Subject: [PATCH 11/12] Update debug.py --- views/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/debug.py b/views/debug.py index 0b4c2dd..4f76e10 100644 --- a/views/debug.py +++ b/views/debug.py @@ -396,7 +396,7 @@ class DebugView(discord.ui.View): player_data = [] for identifier, node in voicelink.NodePool._nodes.items(): for guild_id, player in node._players.copy().items(): - if not player.guild.me.voice or not player.current: + if player.guild.me is None or player.guild.me.voice or not player.current: continue player_data.append(player.data) From 769adbe1e71a4ce31f4807b8e2950de5add90765 Mon Sep 17 00:00:00 2001 From: Choco <94597336+ChocoMeow@users.noreply.github.com> Date: Sat, 19 Apr 2025 15:42:37 +0800 Subject: [PATCH 12/12] Update update.py --- update.py | 90 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/update.py b/update.py index c8a5a5d..dbba0ab 100644 --- a/update.py +++ b/update.py @@ -21,14 +21,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import requests, zipfile, os, shutil, argparse +import argparse +import os +import requests +import zipfile +import shutil +import subprocess from io import BytesIO ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -__version__ = "v2.7.0" +__version__ = "v2.7.1" +# URLs for update and migration GITHUB_API_URL = "https://api.github.com/repos/ChocoMeow/Vocard/releases/latest" VOCARD_URL = "https://github.com/ChocoMeow/Vocard/archive/" +MIGRATION_SCRIPT_URL = f"https://raw.githubusercontent.com/ChocoMeow/Vocard-Magration/main/{__version__}.py" IGNORE_FILES = ["settings.json", "logs", "last-session.json"] class bcolors: @@ -49,8 +56,11 @@ def check_version(with_msg=False): response = requests.get(GITHUB_API_URL) latest_version = response.json().get("name", __version__) if with_msg: - msg = f"{bcolors.OKGREEN}Your bot is up-to-date! - {latest_version}{bcolors.ENDC}" if latest_version == __version__ else \ - f"{bcolors.WARNING}Your bot is not up-to-date! The latest version is {latest_version} and you are currently running version {__version__}\n. Run `python update.py -l` to update your bot!{bcolors.ENDC}" + msg = ( + f"{bcolors.OKGREEN}Your bot is up-to-date! - {latest_version}{bcolors.ENDC}" + if latest_version == __version__ + else f"{bcolors.WARNING}Your bot is not up-to-date! The latest version is {latest_version} and you are currently running version {__version__}\nRun `python update.py -l` to update your bot!{bcolors.ENDC}" + ) print(msg) return latest_version @@ -61,14 +71,14 @@ def download_file(version=None): version (str): the version to download. If None, download the latest version. Returns: - BytesIO: the downloaded zip file. + Response: the downloaded zip file content. """ version = version if version else check_version() print(f"Downloading Vocard version: {version}") response = requests.get(VOCARD_URL + version + ".zip") if response.status_code == 404: print(f"{bcolors.FAIL}Warning: Version not found!{bcolors.ENDC}") - exit() + exit(1) print("Download Completed") return response @@ -76,32 +86,35 @@ def install(response, version): """Install the downloaded version of the project. Args: - response (BytesIO): the downloaded zip file. + response (Response): the downloaded zip file content. version (str): the version to install. """ - user_input = input(f"{bcolors.WARNING}--------------------------------------------------------------------------\n" - "Note: Before proceeding, please ensure that there are no personal files or\n" \ - "sensitive information in the directory you're about to delete. This action\n" \ - "is irreversible, so it's important to double-check that you're making the \n" \ - f"right decision. {bcolors.ENDC} Continue with caution? (Y/n) ") + user_input = input( + f"{bcolors.WARNING}--------------------------------------------------------------------------\n" + "Note: Before proceeding, please ensure that there are no personal files or\n" + "sensitive information in the directory you're about to delete. This action\n" + "is irreversible, so it's important to double-check that you're making the \n" + f"right decision. {bcolors.ENDC} Continue with caution? (Y/n) " + ) if user_input.lower() in ["y", "yes"]: print("Installing ...") zfile = zipfile.ZipFile(BytesIO(response.content)) zfile.extractall(ROOT_DIR) - version = version.replace("v", "") - source_dir = os.path.join(ROOT_DIR, f"Vocard-{version}") + # Remove 'v' from the version string for folder name. + version_without_v = version.replace("v", "") + source_dir = os.path.join(ROOT_DIR, f"Vocard-{version_without_v}") if os.path.exists(source_dir): for filename in os.listdir(ROOT_DIR): - if filename in IGNORE_FILES + [f"Vocard-{version}"]: + if filename in IGNORE_FILES + [f"Vocard-{version_without_v}"]: continue - filename = os.path.join(ROOT_DIR, filename) - if os.path.isdir(filename): - shutil.rmtree(filename) + filename_path = os.path.join(ROOT_DIR, filename) + if os.path.isdir(filename_path): + shutil.rmtree(filename_path) else: - os.remove(filename) + os.remove(filename_path) for filename in os.listdir(source_dir): shutil.move(os.path.join(source_dir, filename), os.path.join(ROOT_DIR, filename)) os.rmdir(source_dir) @@ -109,13 +122,45 @@ def install(response, version): else: print("Update canceled!") +def run_migration(): + """Download, execute, and remove the migration script.""" + confirm = input( + f"{bcolors.WARNING}WARNING: Please ensure you have taken a backup before proceeding.\n" + f"Are you sure you want to run the migration? (Y/n): {bcolors.ENDC} " + ) + if confirm.lower() not in ["y", "yes"]: + print("Migration canceled!") + return + + print("Downloading migration script...") + response = requests.get(MIGRATION_SCRIPT_URL) + if response.status_code != 200: + print(f"{bcolors.FAIL}Failed to download migration script. Status code: {response.status_code}{bcolors.ENDC}") + exit(1) + + migration_filename = "temp_migration.py" + with open(migration_filename, "w", encoding="utf-8") as f: + f.write(response.text) + + print("Executing migration script...") + try: + subprocess.run(["python", migration_filename], check=True) + print(f"{bcolors.OKGREEN}Migration script executed successfully.{bcolors.ENDC}") + except subprocess.CalledProcessError as e: + print(f"{bcolors.FAIL}Migration script execution failed: {e}{bcolors.ENDC}") + finally: + if os.path.exists(migration_filename): + os.remove(migration_filename) + print("Temporary migration script deleted.") + def parse_args(): """Parse command line arguments.""" - parser = argparse.ArgumentParser(description='Update script for Vocard.') + parser = argparse.ArgumentParser(description='Update and migration script for Vocard.') parser.add_argument('-c', '--check', action='store_true', help='Check the current version of the Vocard') parser.add_argument('-v', '--version', type=str, help='Install the specified version of the Vocard') parser.add_argument('-l', '--latest', action='store_true', help='Install the latest version of the Vocard from Github') parser.add_argument('-b', '--beta', action='store_true', help='Install the beta version of the Vocard from Github') + parser.add_argument('-m', '--migration', action='store_true', help='Download and run the migration script from Github') return parser.parse_args() def main(): @@ -138,8 +183,9 @@ def main(): elif args.beta: response = download_file("refs/heads/beta") install(response, "beta") - pass - + + elif args.migration: + run_migration() else: print(f"{bcolors.FAIL}No arguments provided. Run `python update.py -h` for help.{bcolors.ENDC}")