Merge pull request #60 from ChocoMeow/beta

Vocard v2.7.1 Update: bug fixes, and code clean-up
This commit is contained in:
Choco
2025-04-19 17:24:20 +08:00
committed by GitHub
14 changed files with 687 additions and 345 deletions

View File

@@ -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/
@@ -21,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.
@@ -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:

View File

@@ -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 <Tag> to remove it.",
"FilterTagAlreadyInUse": "This sound effect is already in use! Please use /cleareffect <Tag> 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 <Song Name> <Author> 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."
}
}

194
langs/PL.json Normal file
View File

@@ -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 `{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}`",
"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 <Efekt> 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.",
"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.",
"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 <nazwa utworu> <autor> 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 **{2}** (`{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"
}

View File

@@ -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 в этом канале без использования префикса бота."
}
"setStageAnnounceTemplate": "Готово! Новые настройки статуса канала скоро применятся в соответсвии с вашим шаблоном.",
"createSongRequestChannel": "Канал для запроса песен ({0}) создан! Вы можете запросить любую песню по названию или URL в этом канале без использования префикса бота."
}

View File

@@ -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

View File

@@ -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

View File

@@ -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}")

View File

@@ -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)

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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:

322
voicelink/transformer.py Normal file
View File

@@ -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()

View File

@@ -203,5 +203,4 @@ class Ping:
((self._host, self._port), None))
s_runtime = 1000 * (cost_time)
return s_runtime
return s_runtime