diff --git a/.gitignore b/.gitignore index 9fb9544..89241ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.mypy_cache __pycache__ config.toml download diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..976ba02 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/.vscode/settings.json b/.vscode/settings.json index b7368ca..039abdf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "python.formatting.provider": "black" + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.analysis.typeCheckingMode": "off" } diff --git a/__main__.py b/__main__.py index 7ecb3af..3c0ca4e 100644 --- a/__main__.py +++ b/__main__.py @@ -5,9 +5,9 @@ from toml import TomlDecodeError from cog import Greetings, Music from config import Config -from framework import Downloader, Redis, Spotify, Youtube +from framework import DiscordPlayer, Downloader, Redis, Spotify, Youtube from logger import Logger -from service import FileManager, QueueManager +from service import FileManager, PlaybackManager, QueueManager from usecase import Sources if __name__ == "__main__": @@ -30,6 +30,12 @@ if __name__ == "__main__": # Redis Client redis = Redis(logger, config.redis, config.appName) + # Discord Player + discordPlayer = DiscordPlayer() + + # Playback Manager + playbackManager = PlaybackManager(discordPlayer) + # Queue Manager queueManager = QueueManager(redis) @@ -53,7 +59,7 @@ if __name__ == "__main__": # Add Cogs bot.add_cog(Greetings(bot, logger, redis)) - bot.add_cog(Music(bot, logger, queueManager, sources)) + bot.add_cog(Music(bot, logger, queueManager, playbackManager, sources)) # Run bot.run(config.discord.token) diff --git a/cog/misc.py b/cog/misc.py index a2929e2..946204b 100644 --- a/cog/misc.py +++ b/cog/misc.py @@ -15,7 +15,7 @@ class Greetings(Cog): @default_permissions(administrator=True) async def redis_set(self, context: ApplicationContext, key: str, value: str): self.logger.info(f"redis set {value} at {key}") - await self.redis.set(key, value) + await self.redis.set("test", key, value) await context.respond(f"redis set {value} at {key}") @@ -23,6 +23,6 @@ class Greetings(Cog): @default_permissions(administrator=True) async def redis_get(self, context: ApplicationContext, key: str): self.logger.info(f"redis get {key}") - value = await self.redis.get(key) + value = await self.redis.get("test", key) await context.respond(f"redis get {key}: {value}") diff --git a/cog/music.py b/cog/music.py index 33dade5..b55a8de 100644 --- a/cog/music.py +++ b/cog/music.py @@ -1,25 +1,91 @@ from logging import Logger -from discord import ApplicationContext, Bot, Cog, slash_command +from discord import ApplicationContext, Bot, Cog, Embed, Interaction, slash_command -from service import QueueManager +from service import PlaybackManager, QueueManager from usecase import Sources class Music(Cog): def __init__( - self, bot: Bot, logger: Logger, queueManager: QueueManager, sources: Sources + self, + bot: Bot, + logger: Logger, + queueManager: QueueManager, + playbackManager: PlaybackManager, + sources: Sources, ): self.bot = bot self.logger = logger self.queueManager = queueManager + self.playbackManager = playbackManager self.sources = sources @slash_command(name="play") async def play(self, context: ApplicationContext, query: str): - async with self.queueManager(context.guild_id) as queue: - interaction = await context.respond(f"searching {query} ...") - entries = await self.sources.processQuery(interaction, query) - queue.add(entries) + if context.author.voice is None: + await context.respond("Not connected to a voice channel") + return - await interaction.edit_original_response(content=entries[0].title.name) + async with self.queueManager(context.guild_id) as queue: + interaction = await context.respond(f"Searching {query}...") + if not isinstance(interaction, Interaction): + return + + entries = await self.sources.processQuery(interaction, query) + + if entries is None: + await interaction.edit_original_response(content=f"{query} not found") + return + + queue.add(entries) + await self.playbackManager.registerQueue( + context.guild_id, + queue, + context.channel, + context.author.voice.channel, + ) + + if entries[0].playlist is not None: + await interaction.edit_original_response( + content=f"{len(entries)} songs added to queue from playlist {entries[0].playlist.name}" + ) + else: + await interaction.edit_original_response( + content=f"{entries[0].title.name} was added to queue" + ) + + @slash_command(name="nowplaying") + async def nowPlaying(self, context: ApplicationContext): + assert self.bot.user + async with self.queueManager(context.guild_id) as queue: + entry = queue.nowPlaying() + + if entry is None: + await context.respond("No song playing") + return + + requester = await self.bot.get_or_fetch_user(entry.requester) + assert requester is not None + + embed = ( + Embed(title=entry.title.name, url=entry.title.url) + .add_field( + name="Artist", value=f"[{entry.artist.name}]({entry.artist.url})" + ) + .set_author( + name="Now Playing", icon_url=self.bot.user.display_avatar.url + ) + .set_image(url=entry.thumbnail) + .set_footer( + text=requester.display_name, icon_url=requester.display_avatar.url + ) + ) + + if entry.playlist is not None: + embed.add_field( + name="Playlist", + value=f"[{entry.playlist.name}]({entry.playlist.url})", + ) + + await context.respond(embed=embed) diff --git a/config.py b/config.py index 1ca0478..58f8c1f 100644 --- a/config.py +++ b/config.py @@ -2,46 +2,46 @@ import toml class DiscordConfig: - def __init__(self, discord_config) -> None: - self.token: str = discord_config["token"] - self.timeout: int = discord_config["timeout"] + def __init__(self, discordConfig) -> None: + self.token: str = discordConfig["token"] + self.timeout: int = discordConfig["timeout"] class YoutubeConfig: - def __init__(self, youtube_config) -> None: - self.language: str = youtube_config["language"] - self.region: str = youtube_config["region"] + def __init__(self, youtubeConfig) -> None: + self.language: str = youtubeConfig["language"] + self.region: str = youtubeConfig["region"] class SpotifyConfig: - def __init__(self, spotify_config) -> None: - self.clientId: str = spotify_config["clientId"] - self.clientSecret: str = spotify_config["clientSecret"] - self.region: str = spotify_config["region"] + def __init__(self, spotifyConfig) -> None: + self.clientId: str = spotifyConfig["clientId"] + self.clientSecret: str = spotifyConfig["clientSecret"] + self.region: str = spotifyConfig["region"] class LoggingConfig: - def __init__(self, logging_config) -> None: - self.level: str = logging_config["level"] + def __init__(self, loggingConfig) -> None: + self.level: str = loggingConfig["level"] class RedisConfig: - def __init__(self, redis_config) -> None: - self.host: str = redis_config["host"] - self.port: int = redis_config["port"] - self.password: str = redis_config["password"] + def __init__(self, redisConfig) -> None: + self.host: str = redisConfig["host"] + self.port: int = redisConfig["port"] + self.password: str = redisConfig["password"] class Config: - def __init__(self, config_path: str) -> None: - self._config = toml.load(config_path) - self.appName: str = self._config["appName"] - self.downloadDirectory: str = self._config["downloadDirectory"] - self.discord = DiscordConfig(self._config["discord"]) - self.youtube = YoutubeConfig(self._config["youtube"]) - self.spotify = SpotifyConfig(self._config["spotify"]) - self.logging = LoggingConfig(self._config["logging"]) - self.redis = RedisConfig(self._config["redis"]) + def __init__(self, configPath: str) -> None: + self.config = toml.load(configPath) + self.appName: str = self.config["appName"] + self.downloadDirectory: str = self.config["downloadDirectory"] + self.discord = DiscordConfig(self.config["discord"]) + self.youtube = YoutubeConfig(self.config["youtube"]) + self.spotify = SpotifyConfig(self.config["spotify"]) + self.logging = LoggingConfig(self.config["logging"]) + self.redis = RedisConfig(self.config["redis"]) def __str__(self) -> str: - return str(self._config) + return str(self.config) diff --git a/entity/queue.py b/entity/queue.py index b9221bd..7bb9b49 100644 --- a/entity/queue.py +++ b/entity/queue.py @@ -3,37 +3,60 @@ from .entry import Entry class Queue: def __init__(self) -> None: - self._entries: list[Entry] = [] + self.entries: list[Entry] = [] + self.playing = False self.cursor = 0 + self.seek = 0 def add(self, entries: list[Entry]) -> None: for entry in entries: - self._entries.append(entry) + self.entries.append(entry) def remove(self, index: int, recursive: bool) -> None: - if not 0 < index < len(self._entries): + if not 0 <= index < len(self.entries): return # if recursive and self[index].playlist is not None: - # first_entry = "" + # firstEntry = "" # else: - self._entries.pop() + self.entries.pop() def move(self, frm: int, to: int) -> None: if ( - not 0 < frm < len(self._entries) - or not 0 < to < len(self._entries) + not 0 <= frm < len(self.entries) + or not 0 <= to < len(self.entries) or frm == to ): - return + return None - self._entries.insert(to, self._entries.pop(frm)) + self.entries.insert(to, self.entries.pop(frm)) + + def moveCursor(self, position: int) -> None: + if not 0 <= position <= len(self.entries): + return None + + self.cursor = position + + def incrementCursor(self) -> None: + self.moveCursor(self.cursor + 1) + + def nowPlaying(self) -> Entry | None: + if not 0 <= self.cursor < len(self.entries): + return None + + return self.entries[self.cursor] + + def startPlaying(self) -> None: + self.playing = True + + def stopPlaying(self) -> None: + self.playing = False def __getitem__(self, index: int) -> Entry | None: - if not 0 < index < len(self._entries): - return + if not 0 <= index < len(self.entries): + return None - return self._entries[index] + return self.entries[index] def __len__(self) -> int: - return len(self._entries) + return len(self.entries) diff --git a/framework/__init__.py b/framework/__init__.py index b56f839..25de506 100644 --- a/framework/__init__.py +++ b/framework/__init__.py @@ -1,3 +1,4 @@ +from .discordPlayer import DiscordPlayer from .downloader import Downloader from .redis import Redis from .spotify import Spotify diff --git a/framework/discordPlayer.py b/framework/discordPlayer.py new file mode 100644 index 0000000..4c7a6d4 --- /dev/null +++ b/framework/discordPlayer.py @@ -0,0 +1,40 @@ +from discord import FFmpegOpusAudio, VoiceChannel, VoiceClient + +from entity import File + + +class DiscordPlayer: + def __init__(self) -> None: + self.clients: dict[int, VoiceClient] = {} + self.timeout = 60 + + async def connect(self, guildId: int, voiceChannel: VoiceChannel) -> None: + if ( + guildId not in self.clients + or self.clients[guildId] is None + or not self.clients[guildId].is_connected() + ): + self.clients[guildId] = await voiceChannel.connect(timeout=self.timeout) + print(f"connect to {voiceChannel.name}") + + async def disconnect(self, guildId: int) -> None: + if guildId in self.clients and self.clients[guildId].is_connected(): + await self.clients[guildId].disconnect() + + def play( + self, guildId: int, file: File | str, seekTime: int = 0, after=None + ) -> None: + self.clients[guildId].play( + FFmpegOpusAudio( + file.name if isinstance(file, File) else file, + bitrate=256, + before_options="-ss %d" % seekTime, + options="-vn", + ), + after=lambda error: self.clients[guildId].loop.create_task(after(error)) + if after is not None + else None, + ) + + def stop(self, guildId) -> None: + self.clients[guildId].stop() diff --git a/framework/redis.py b/framework/redis.py index eaec6ad..a8644d6 100644 --- a/framework/redis.py +++ b/framework/redis.py @@ -1,47 +1,34 @@ import pickle from logging import Logger -import redis.asyncio as redis -from redis.asyncio.lock import Lock +from redis.asyncio import Redis as RedisClient from config import RedisConfig class Redis: def __init__(self, logger: Logger, config: RedisConfig, rootKeyName: str) -> None: - self._client = redis.Redis( + self.client: RedisClient = RedisClient( host=config.host, port=config.port, password=config.password, auto_close_connection_pool=False, ) - self._locks: dict[str, Lock] = {} self.rootKeyName = rootKeyName self.logger = logger - async def _get_lock(self, key) -> Lock: - if key not in self._locks: - self._locks[key] = self._client.lock(key) - return self._locks[key] - - async def acquire(self, key: str) -> None: - lock = await self._get_lock(f"{self.rootKeyName}:queue:{key}") - await lock.acquire() - - async def release(self, key: str) -> None: - lock = await self._get_lock(f"{self.rootKeyName}:queue:{key}") - await lock.release() - - async def get(self, key: str): + async def get(self, context: str, key: str): self.logger.info(f"get value {key} from redis") - value = await self._client.get(f"{self.rootKeyName}:queue:{key}") + value = await self.client.get(f"{self.rootKeyName}:{context}:{key}") if value: return pickle.loads(value) return None - async def set(self, key: str, value) -> None: + async def set(self, context: str, key: str, value) -> None: self.logger.info(f"set value {key} to redis") - await self._client.set(f"{self.rootKeyName}:queue:{key}", pickle.dumps(value)) + await self.client.set( + f"{self.rootKeyName}:{context}:{key}", pickle.dumps(value) + ) async def close(self) -> None: - await self._client.close() + await self.client.close() diff --git a/framework/spotify.py b/framework/spotify.py index 8521176..6097db5 100644 --- a/framework/spotify.py +++ b/framework/spotify.py @@ -21,5 +21,10 @@ class Spotify: async def getTrack(self, url: str): return await self.loop.run_in_executor( - None, lambda: self.client.track(url, self.region) + None, lambda: self.client.track(url, market=self.region) + ) + + async def getPlaylist(self, url: str): + return await self.loop.run_in_executor( + None, lambda: self.client.playlist(url, market=self.region) ) diff --git a/framework/youtube.py b/framework/youtube.py index 33611d9..d54c82b 100644 --- a/framework/youtube.py +++ b/framework/youtube.py @@ -17,6 +17,7 @@ class Youtube: self.params = { "format": "bestaudio/best", "outtmpl": path.join(downloadDirectory, "%(id)s"), + "updatetime": False, "restrictfilenames": True, "noplaylist": True, "ignoreerrors": True, @@ -37,7 +38,7 @@ class Youtube: videosSearch = VideosSearch( query, limit=1, language=self.language, region=self.region ) - return await videosSearch.next() + return (await videosSearch.next())["result"] async def fetchData(self, url: str): info = await self.loop.run_in_executor( @@ -66,7 +67,7 @@ class Youtube: ), ) - async def download(self, url: str, fileName: str, progress=None) -> None: + async def download(self, url: str, fileName: str, progress=None) -> File: if progress is not None: progress_hook = lambda status: self.downloadProgress(progress, status) self.client.add_progress_hook(progress_hook) diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..ea96bd6 --- /dev/null +++ b/lint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +isort . && black . && flake8 . && mypy . diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..cc09c10 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +types-redis==4.5.5.0 +types-requests==2.30.0.0 +types-toml==0.10.8.6 + +black==23.3.0 +flake8==6.0.0 +isort==5.12.0 +mypy==1.2.0 diff --git a/requirements.txt b/requirements.txt index ca1c86c..cfba012 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,3 @@ spotipy==2.23.0 toml==0.10.2 youtube-search-python==1.6.6 yt-dlp==2023.3.4 - -black==23.3.0 -flake8==6.0.0 -isort==5.12.0 diff --git a/service/__init__.py b/service/__init__.py index edf3b5d..51aea63 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -1,2 +1,3 @@ from .fileManager import FileManager +from .playbackManager import PlaybackManager from .queueManager import QueueManager diff --git a/service/fileManager.py b/service/fileManager.py index 4b271f3..44bdc20 100644 --- a/service/fileManager.py +++ b/service/fileManager.py @@ -1,10 +1,11 @@ import re -from os import path +from collections.abc import AsyncIterator +from os import path, stat from time import time from discord import Interaction -from entity import Entry +from entity import Entry, File from framework import Downloader, Youtube @@ -18,7 +19,7 @@ class FileManager: self.youtubeVideoRegex = re.compile( r"(https:\/\/)?(www|music)\.youtube\.com\/(watch\?v=|shorts\/)\w*" ) - self.progressLastUpdate = 0 + self.progressLastUpdate = 0.0 def getFileName(self, entryId: str) -> str: return path.join(self.downloadDirectory, entryId) @@ -27,24 +28,29 @@ class FileManager: self, interaction: Interaction, entry: Entry, - current_size: int, - total_size: int, + current_size: float, + total_size: float, ): - if time() - self.progressLastUpdate > 1: + current_size = current_size / 1024 / 1024 + total_size = total_size / 1024 / 1024 + if time() - self.progressLastUpdate > 1 and total_size > 10: self.progressLastUpdate = time() await interaction.edit_original_response( content=f"Downloading {entry.title.name} [%.2f/%.2f Mo]" - % (current_size / 1024 / 1024, total_size / 1024 / 1024) + % (current_size, total_size) ) - async def download(self, interaction: Interaction, entries: list[Entry]): + async def download( + self, interaction: Interaction, entries: list[Entry] + ) -> AsyncIterator[File]: for entry in entries: - entryId = self.youtube.getId(entry.title.url) + print("DOWNLOAD", entry.source) + entryId = self.youtube.getId(entry.source) fileName = self.getFileName(entryId) if not path.isfile(fileName) and isinstance(entry.source, str): if self.youtubeVideoRegex.match(entry.source) is not None: - entry.source = await self.youtube.download( + yield await self.youtube.download( entry.source, fileName, lambda current_size, total_size: self.downloadProgress( @@ -52,10 +58,12 @@ class FileManager: ), ) else: - entry.source = await self.downloader.download( + yield await self.downloader.download( entry.source, fileName, lambda current_size, total_size: self.downloadProgress( interaction, entry, current_size, total_size ), ) + + yield File(fileName, stat(fileName).st_size) diff --git a/service/playbackManager.py b/service/playbackManager.py new file mode 100644 index 0000000..12c8be7 --- /dev/null +++ b/service/playbackManager.py @@ -0,0 +1,59 @@ +from discord import TextChannel, VoiceChannel + +from entity import Queue +from framework import DiscordPlayer + + +class ActiveQueue: + def __init__( + self, queue: Queue, textChannel: TextChannel, voiceChannel: VoiceChannel + ) -> None: + self.queue = queue + self.textChannel = textChannel + self.voiceChannel = voiceChannel + + +class PlaybackManager: + def __init__(self, player: DiscordPlayer) -> None: + self.player = player + self.queues: dict[int, ActiveQueue] = {} + + async def nextTrack(self, error: Exception, guildId: int): + queue = self.queues[guildId].queue + textChannel = self.queues[guildId].textChannel + queue.incrementCursor() + if queue.cursor < len(queue): + self.startPlayback(guildId) + await textChannel.send(f"Playing {queue[queue.cursor].title.name}") + else: + self.stopPlayback(guildId) + + def startPlayback(self, guildId: int, seekTime=0): + queue = self.queues[guildId].queue + if len(queue) > queue.cursor and not queue.playing: + queue.startPlaying() + self.player.play( + guildId, + queue[queue.cursor].source, + seekTime, + lambda error: self.nextTrack(error, guildId), + ) + + def stopPlayback(self, guildId: int): + queue = self.queues[guildId].queue + if queue.playing: + queue.stopPlaying() + self.player.stop(guildId) + + async def registerQueue( + self, + guildId: int, + queue: Queue, + textChannel: TextChannel, + voiceChannel: VoiceChannel, + ): + if guildId not in self.queues: + self.queues[guildId] = ActiveQueue(queue, textChannel, voiceChannel) + + await self.player.connect(guildId, voiceChannel) + self.startPlayback(guildId) diff --git a/service/queueManager.py b/service/queueManager.py index 9240362..f16111e 100644 --- a/service/queueManager.py +++ b/service/queueManager.py @@ -1,3 +1,4 @@ +from collections.abc import AsyncIterator from contextlib import asynccontextmanager from entity import Queue @@ -10,21 +11,13 @@ class QueueManager: self.queues: dict[int, Queue] = {} @asynccontextmanager - async def __call__(self, guildId: int) -> Queue: - # await self.acquire(guildId) + async def __call__(self, guildId: int) -> AsyncIterator[Queue]: queue = await self.get(guildId) yield queue await self.save(guildId) - # await self.release(guildId) - - async def acquire(self, guildId: int) -> None: - await self.redis.acquire(guildId) - - async def release(self, guildId: int) -> None: - await self.redis.release(guildId) async def get(self, guildId: int) -> Queue: - queue: Queue | None = await self.redis.get(guildId) + queue: Queue | None = await self.redis.get("queue", str(guildId)) if queue is None: queue = Queue() self.queues[guildId] = queue @@ -32,4 +25,4 @@ class QueueManager: return self.queues[guildId] async def save(self, guildId: int) -> None: - await self.redis.set(guildId, self.queues[guildId]) + await self.redis.set("queue", str(guildId), self.queues[guildId]) diff --git a/usecase/sources.py b/usecase/sources.py index 5a89c69..5e20978 100644 --- a/usecase/sources.py +++ b/usecase/sources.py @@ -1,4 +1,3 @@ -import json import re from discord import Interaction @@ -50,8 +49,8 @@ class Sources: if data is None: return None - with open("search_result.json", "w") as file: - file.write(json.dumps(data)) + # with open("search_result.json", "w") as file: + # file.write(json.dumps(data)) entry = Entry( title=Title(name=data["title"], url=data["webpage_url"]), @@ -62,71 +61,138 @@ class Sources: source=data["webpage_url"], ) - await self.fileManager.download(interaction, [entry]) + entry.source = await anext(self.fileManager.download(interaction, [entry])) return [entry] if self.youtube_playlist_regex.match(query) is not None: - data = await self.youtube.getData(query) + data = await self.youtube.fetchData(query) if data is None: return None - with open("search_result.json", "w") as file: - file.write(json.dumps(data)) + # with open("search_result.json", "w") as file: + # file.write(json.dumps(data)) playlist = Playlist( - title=data["title"], + name=data["title"], url=data["webpage_url"], owner=Artist(name=data["channel"], url=data["channel_url"]), ) - return [ + entries = [ Entry( - title=Title(name=data["title"], url=data["webpage_url"]), - artist=Artist(name=data["channel"], url=data["channel_url"]), - duration=data["duration"], - thumbnail=data["thumbnail"], + title=Title( + name=entry_data["title"], url=entry_data["webpage_url"] + ), + artist=Artist( + name=entry_data["channel"], url=entry_data["channel_url"] + ), + duration=entry_data["duration"], + thumbnail=entry_data["thumbnail"], requesterId=interaction.user.id, playlist=playlist, - source=data["url"], + source=entry_data["webpage_url"], ) - for data in data["entries"] + for entry_data in data["entries"] ] + index = 0 + async for file in self.fileManager.download(interaction, entries): + entries[index].source = file + index += 1 + + return entries + if self.spotify_track_regex.match(query) is not None: data = await self.spotify.getTrack(query) - with open("spotify_track_result.json", "w") as file: - file.write(json.dumps(data)) + # with open("spotify_track_result.json", "w") as file: + # file.write(json.dumps(data)) - return [ - Entry( - title=Title( - name=data["name"], url=data["external_urls"]["spotify"] - ), - artist=Artist( - name=data["artists"][0]["name"], - url=data["artists"][0]["external_urls"]["spotify"], - ), - album=Album( - name=data["album"]["name"], - url=data["album"]["external_urls"]["spotify"], - ), - duration=0, - thumbnail=data["album"]["images"][0]["url"], - requesterId=interaction.user.id, - ) - ] + entry = Entry( + title=Title(name=data["name"], url=data["external_urls"]["spotify"]), + artist=Artist( + name=data["artists"][0]["name"], + url=data["artists"][0]["external_urls"]["spotify"], + ), + album=Album( + name=data["album"]["name"], + url=data["album"]["external_urls"]["spotify"], + ), + duration=0, + thumbnail=data["album"]["images"][0]["url"], + requesterId=interaction.user.id, + ) + + entry.source = await self.search(f"{entry.title.name} {entry.artist.name}") + entry.source = await anext(self.fileManager.download(interaction, [entry])) + + return [entry] if ( self.spotify_playlist_regex.match(query) is not None or self.spotify_album_regex.match(query) is not None or self.spotify_artist_regex.match(query) is not None ): - return + data = await self.spotify.getPlaylist(query) - return await self.processQuery(interaction, await self.search(query)) + # with open("spotify_playlist_result.json", "w") as file: + # file.write(json.dumps(data)) - async def search(self, query) -> str: + playlist = Playlist( + name=data["name"], + url=data["external_urls"]["spotify"], + owner=Artist( + name=data["owner"]["display_name"], + url=data["owner"]["external_urls"]["spotify"], + ), + ) + + entries = [ + Entry( + title=Title( + name=entry_data["track"]["name"], + url=entry_data["track"]["external_urls"]["spotify"], + ), + artist=Artist( + name=entry_data["track"]["artists"][0]["name"], + url=entry_data["track"]["artists"][0]["external_urls"][ + "spotify" + ], + ), + album=Album( + name=entry_data["track"]["album"]["name"], + url=entry_data["track"]["album"]["external_urls"]["spotify"], + ), + duration=0, + thumbnail=entry_data["track"]["album"]["images"][0]["url"], + requesterId=interaction.user.id, + ) + for entry_data in data["tracks"]["items"] + ] + + for entry in entries: + entry.source = await self.search( + f"{entry.title.name} {entry.artist.name}" + ) + print(entry.source) + + index = 0 + async for file in self.fileManager.download(interaction, entries): + print(entries[index].source) + entries[index].source = file + print(entries[index].source) + index += 1 + + return entries + + searchResult = await self.search(query) + if searchResult is None: + return None + return await self.processQuery(interaction, searchResult) + + async def search(self, query) -> str | None: result = await self.youtube.searchVideo(query) - return result["result"][0]["link"] + if len(result) == 0: + return None + return result[0]["link"]