diff --git a/__main__.py b/__main__.py index 67c56c7..b6f84bf 100644 --- a/__main__.py +++ b/__main__.py @@ -3,15 +3,15 @@ from discord import Bot, Intents from config import Config from logger import Logger -from framework import Redis -from cogs import Greetings - +from framework import Redis, Youtube +from cog import Greetings, Music +from service import QueueManager +from usecase import Sources if __name__ == "__main__": # Read Config try: config = Config("config.toml") - print(config) except TomlDecodeError as error: print("Config/DecodeError : %s" % error) exit(1) @@ -23,10 +23,23 @@ if __name__ == "__main__": logger = Logger(config.logging)() # Redis Client - redis = Redis(config.redis) + redis = Redis(logger, config.redis) - # Bot - intents = Intents.default() - bot = Bot(intents=intents) + # Queue Manager + queueManager = QueueManager(redis) + + # Bot Init + bot = Bot(intents=Intents.default()) + + # Youtube Client + youtube = Youtube(bot.loop, config.youtube) + + # Sources + sources = Sources(youtube) + + # Add Cogs bot.add_cog(Greetings(bot, logger, redis)) + bot.add_cog(Music(bot, logger, queueManager, sources)) + + # Run bot.run(config.discord.token) diff --git a/cogs/__init__.py b/cog/__init__.py similarity index 52% rename from cogs/__init__.py rename to cog/__init__.py index 59afff1..a380af2 100644 --- a/cogs/__init__.py +++ b/cog/__init__.py @@ -1 +1,2 @@ from .misc import Greetings +from .music import Music diff --git a/cogs/misc.py b/cog/misc.py similarity index 100% rename from cogs/misc.py rename to cog/misc.py diff --git a/cog/music.py b/cog/music.py new file mode 100644 index 0000000..a92a897 --- /dev/null +++ b/cog/music.py @@ -0,0 +1,35 @@ +from discord import Bot, Cog, ApplicationContext +from discord.commands import slash_command + +from entity import Entry +from logging import Logger +from service import QueueManager +from usecase import Sources + + +class Music(Cog): + def __init__( + self, bot: Bot, logger: Logger, queueManager: QueueManager, sources: Sources + ): + self.bot = bot + self.logger = logger + self.queueManager = queueManager + self.sources = sources + + @slash_command(name="play") + async def play(self, context: ApplicationContext, query: str): + async with self.queueManager(context.guild_id) as queue: + test = await self.sources.processQuery(query) + + queue.add( + Entry( + title="title", + artist="artist", + album="album", + thumbnail="thumb", + link="fdsdfsd", + requesterId=context.author.id, + ) + ) + + await context.respond(str(test)) diff --git a/config.py b/config.py index 67fff72..b3f28e3 100644 --- a/config.py +++ b/config.py @@ -2,17 +2,24 @@ import toml class DiscordConfig: - def __init__(self, discord_config: any) -> None: + def __init__(self, discord_config) -> None: self.token: str = discord_config["token"] + self.timeout: int = discord_config["timeout"] + + +class YoutubeConfig: + def __init__(self, youtube_config) -> None: + self.language: str = youtube_config["language"] + self.region: str = youtube_config["region"] class LoggingConfig: - def __init__(self, logging_config: any) -> None: + def __init__(self, logging_config) -> None: self.level: str = logging_config["level"] class RedisConfig: - def __init__(self, redis_config: any) -> None: + def __init__(self, redis_config) -> None: self.host: str = redis_config["host"] self.port: int = redis_config["port"] self.password: str = redis_config["password"] @@ -21,7 +28,9 @@ class RedisConfig: class Config: def __init__(self, config_path: str) -> None: self._config = toml.load(config_path) + self.appName: str = self._config["appName"] self.discord = DiscordConfig(self._config["discord"]) + self.youtube = YoutubeConfig(self._config["youtube"]) self.logging = LoggingConfig(self._config["logging"]) self.redis = RedisConfig(self._config["redis"]) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 6f49884..c0c1e22 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -5,4 +5,4 @@ services: image: redis:alpine command: /bin/sh -c "redis-server --requirepass dev" ports: - - 127.0.0.1:6379:6379 + - 6379:6379 diff --git a/entity/__init__.py b/entity/__init__.py index e1b33ee..5118729 100644 --- a/entity/__init__.py +++ b/entity/__init__.py @@ -1,2 +1,4 @@ from .queue import Queue from .entry import Entry +from .file import File +from .playlist import Playlist diff --git a/entity/entry.py b/entity/entry.py index 8269d2b..04f855a 100644 --- a/entity/entry.py +++ b/entity/entry.py @@ -1,6 +1,24 @@ +from .playlist import Playlist +from .file import File + + class Entry: - def __init__(self, entry_info) -> None: - self.title: str = entry_info["title"] - self.artist: str = entry_info["artist"] - self.thumbnail: str = entry_info["thumbnail"] - self.requester: str = entry_info["requester"] + def __init__( + self, + title: str, + artist: str, + album: str, + thumbnail: str, + link: str, + requesterId: int, + playlist: Playlist | None = None, + file: File | None = None, + ) -> None: + self.title = title + self.artist = artist + self.album = album + self.thumbnail = thumbnail + self.link = link + self.requester = requesterId + self.playlist = playlist + self.file = file diff --git a/entity/file.py b/entity/file.py new file mode 100644 index 0000000..6f16528 --- /dev/null +++ b/entity/file.py @@ -0,0 +1,5 @@ +class File: + def __init__(self, name: str, path: str, size: int) -> None: + self.name = name + self.path = path + self.size = size diff --git a/entity/playlist.py b/entity/playlist.py new file mode 100644 index 0000000..454eb49 --- /dev/null +++ b/entity/playlist.py @@ -0,0 +1,4 @@ +class Playlist: + def __init__(self, title: str, url: str) -> None: + self.title = title + self.url = url diff --git a/entity/queue.py b/entity/queue.py index ef92d38..0f94f76 100644 --- a/entity/queue.py +++ b/entity/queue.py @@ -1,23 +1,43 @@ -from entry import Entry +from .entry import Entry +from .playlist import Playlist class Queue: def __init__(self) -> None: - self._queue: list[Entry] = [] + self._entries: list[Entry] = [] self.cursor = 0 - def append(self, entry: Entry) -> None: - self._queue.append(entry) + def add(self, entry: Entry) -> None: + self._entries.append(entry) - def remove(self, index: int) -> Entry | None: - if 0 < index < len(self._queue): - return self._queue.pop() + def addPlalist(self, playlist: Playlist) -> None: + for entry in playlist: + self._entries.append(entry) + + def remove(self, index: int, recursive: bool) -> None: + if not 0 < index < len(self._entries): + return + + # if recursive and self[index].playlist is not None: + # first_entry = "" + # else: + self._entries.pop() def move(self, frm: int, to: int) -> None: - if 0 < frm < len(self._queue) and 0 < to < len(self._queue) and frm != to: - self._queue.insert(to, self._queue.pop(frm)) + if ( + not 0 < frm < len(self._entries) + or not 0 < to < len(self._entries) + or frm == to + ): + return + + self._entries.insert(to, self._entries.pop(frm)) def __getitem__(self, index: int) -> Entry | None: - if index < 0 or index > len(self._queue): - return None - return self._queue[index] + if not 0 < index < len(self._entries): + return + + return self._entries[index] + + def __len__(self) -> int: + return len(self._entries) diff --git a/framework/__init__.py b/framework/__init__.py index 247dc55..68f8400 100644 --- a/framework/__init__.py +++ b/framework/__init__.py @@ -1 +1,2 @@ from .redis import Redis +from .youtube import Youtube diff --git a/cogs/music.py b/framework/lastfm.py similarity index 100% rename from cogs/music.py rename to framework/lastfm.py diff --git a/framework/mysql.py b/framework/mysql.py new file mode 100644 index 0000000..e69de29 diff --git a/framework/redis.py b/framework/redis.py index b466f99..219c299 100644 --- a/framework/redis.py +++ b/framework/redis.py @@ -1,3 +1,5 @@ +from logging import Logger +from redis.asyncio.lock import Lock import redis.asyncio as redis import pickle @@ -5,20 +7,39 @@ from config import RedisConfig class Redis: - def __init__(self, config: RedisConfig) -> None: - self.client = redis.Redis( + def __init__(self, logger: Logger, config: RedisConfig) -> None: + self._client = redis.Redis( host=config.host, port=config.port, password=config.password, auto_close_connection_pool=False, ) + self._locks: dict[str, Lock] = {} + self.logger = logger - async def set(self, key: str, value: any) -> None: - await self.client.set(key, pickle.dumps(value)) + 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 get(self, key: str) -> any: - value = await self.client.get(key) - return pickle.loads(value) + async def acquire(self, key: str) -> None: + lock = await self._get_lock(f"djembe:queue:{key}") + await lock.acquire() + + async def release(self, key: str) -> None: + lock = await self._get_lock(f"djembe:queue:{key}") + await lock.release() + + async def get(self, key: str): + self.logger.info(f"get value {key} from redis") + value = await self._client.get(f"djembe:queue:{key}") + if value: + return pickle.loads(value) + return None + + async def set(self, key: str, value) -> None: + self.logger.info(f"set value {key} to redis") + await self._client.set(f"djembe:queue:{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 new file mode 100644 index 0000000..e69de29 diff --git a/framework/youtube.py b/framework/youtube.py new file mode 100644 index 0000000..6737c12 --- /dev/null +++ b/framework/youtube.py @@ -0,0 +1,25 @@ +from yt_dlp import YoutubeDL +from youtubesearchpython.__future__ import VideosSearch +from asyncio import AbstractEventLoop + +from config import YoutubeConfig + + +class Youtube: + def __init__(self, loop: AbstractEventLoop, config: YoutubeConfig) -> None: + self.params = {} + self.client = YoutubeDL(self.params) + self.loop = loop + self.config = config + + async def searchVideo(self, query: str): + videosSearch = VideosSearch( + query, limit=1, language=self.config.language, region=self.config.region + ) + return await videosSearch.next() + + async def get_data(self, url: str) -> str: + data = await self.loop.run_in_executor( + None, lambda: self.client.extract_info(url, download=False) + ) + print(data) diff --git a/requirements.txt b/requirements.txt index fdaade5..d871e6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ py-cord==2.4.1 PyNaCl==1.5.0 toml==0.10.2 redis==4.5.4 +youtube-search-python==1.6.6 +yt-dlp==2023.3.4 diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..87ca17a --- /dev/null +++ b/service/__init__.py @@ -0,0 +1 @@ +from .queueManager import QueueManager diff --git a/service/queueManager.py b/service/queueManager.py new file mode 100644 index 0000000..8b53e63 --- /dev/null +++ b/service/queueManager.py @@ -0,0 +1,35 @@ +from contextlib import asynccontextmanager + +from entity import Queue +from framework import Redis + + +class QueueManager: + def __init__(self, redis: Redis) -> None: + self.redis = redis + self.queues: dict[int, Queue] = {} + + @asynccontextmanager + async def __call__(self, guildId: int) -> Queue: + #await self.acquire(guildId) + 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) + if queue is None: + queue = Queue() + self.queues[guildId] = queue + + return self.queues[guildId] + + async def save(self, guildId: int) -> None: + await self.redis.set(guildId, self.queues[guildId]) diff --git a/service/youtubeDownloader.py b/service/youtubeDownloader.py new file mode 100644 index 0000000..e69de29 diff --git a/test/TODO b/test/TODO new file mode 100644 index 0000000..e69de29 diff --git a/usecase/__init__.py b/usecase/__init__.py new file mode 100644 index 0000000..541f902 --- /dev/null +++ b/usecase/__init__.py @@ -0,0 +1 @@ +from .sources import Sources diff --git a/usecase/sources.py b/usecase/sources.py new file mode 100644 index 0000000..8af3bf7 --- /dev/null +++ b/usecase/sources.py @@ -0,0 +1,11 @@ +import re + +from framework import Youtube + + +class Sources: + def __init__(self, youtube: Youtube) -> None: + self.youtube = youtube + + async def processQuery(self, query: str): + return await self.youtube.searchVideo(query)