Merge pull request #60 from ChocoMeow/beta
Vocard v2.7.1 Update: bug fixes, and code clean-up
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
194
langs/PL.json
Normal 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"
|
||||
}
|
||||
@@ -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 в этом канале без использования префикса бота."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
90
update.py
90
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}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
322
voicelink/transformer.py
Normal 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()
|
||||
@@ -203,5 +203,4 @@ class Ping:
|
||||
((self._host, self._port), None))
|
||||
s_runtime = 1000 * (cost_time)
|
||||
|
||||
return s_runtime
|
||||
|
||||
return s_runtime
|
||||
Reference in New Issue
Block a user