diff --git a/README.md b/README.md index 51ce252..a9b3b65 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ ECIBot is a Discord bot made in Python. It's main purpose is to be a custom bot * `!search `: Search custom sounds that contains given prompt. Alternative command: !b y !buscar. * `!dalle `: Generates 9 images in a 3 by 3 array by sending the given prompt to Dall-e mini API. It may take up to a minute to get the images from the API. Alternative command: !d. * `!confetti `: Plays the specified number of random Confetti songs. Alternative command: !co. +* `!ytmix `: Plays a mix generated from the given YouTube search or url. Alternative command: !youtubemix +* `!ytmusicmix `: Plays a mix generated from the given YouTube Music search or url. Alternative command: !ytmmix and !youtubemusicmix ## TODO: * [X] Add more commands. diff --git a/main.py b/main.py index 3b888ef..f595bd8 100644 --- a/main.py +++ b/main.py @@ -123,6 +123,8 @@ async def help(ctx: Context): embed_msg.add_field(name="!buscar ", value="Busca sonidos que contengan el argumento añadido. También funciona con !b y !search.", inline=False) embed_msg.add_field(name="!dalle ", value="Genera imagenes según el texto que se le ha introducido. También funciona con !d.", inline=False) embed_msg.add_field(name="!confetti ", value="Reproduce el número especificado de canciones aleatorias de Confetti. También funciona con !co.", inline=False) + embed_msg.add_field(name="!ytmix ", value="Reproduce un mix generado a partir de la búsqueda o url de YouTube introducida. También funciona con !youtubemix.", inline=False) + embed_msg.add_field(name="!ytmusicmix ", value="Reproduce un mix generado a partir de la búsqueda o url de YouTube Music introducida. También funciona con !ytmmix y !youtubemusicmix.", inline=False) await ctx.send(embed=embed_msg) @@ -290,13 +292,52 @@ async def youtubemusic(ctx: Context, *args: str): if "#" not in search_query: search_query += "#songs" - result_url = yt_music_search_and_get_first_result_url(search_query) + result_url = yt_music_search_and_extract_yt_dlp_info(search_query).get('url') if result_url is not None: await play(ctx, result_url) else: await ctx.send(":no_entry_sign: No se ha encontrado ningún contenido.") +@bot.command(aliases=["ytmix"], require_var_positional=True) +async def youtubemix(ctx: Context, *, arg: str = ""): + database.register_user_interaction(ctx.author.name, "youtubemix") + if arg.startswith("http://") or arg.startswith("https://"): + if is_youtube_url(arg): + yt_dlp_info = extract_yt_dlp_info(arg) + await play_youtube_mix(ctx, yt_dlp_info) + else: + await ctx.send(f":no_entry_sign: Solo se puede crear un mix de un vídeo de YouTube.") + elif len(arg) > 0: + yt_dlp_info = yt_search_and_extract_yt_dlp_info(arg) + await play_youtube_mix(ctx, yt_dlp_info) + + +@bot.command(aliases=["ytmmix", "ytmusicmix"], require_var_positional=True) +async def youtubemusicmix(ctx: Context, *, arg: str = ""): + database.register_user_interaction(ctx.author.name, "youtubemusicmix") + if arg.startswith("http://") or arg.startswith("https://"): + if is_youtube_url(arg): + yt_dlp_info = extract_yt_dlp_info(arg) + await play_youtube_mix(ctx, yt_dlp_info) + else: + await ctx.send(f":no_entry_sign: Solo se puede crear un mix de una canción de YouTube.") + elif len(arg) > 0: + yt_music_search_query = arg.rsplit("#", 1)[0] + "#songs" + yt_dlp_info = yt_music_search_and_extract_yt_dlp_info(yt_music_search_query) + await play_youtube_mix(ctx, yt_dlp_info) + + +async def play_youtube_mix(ctx: Context, yt_dlp_info: Any): + if yt_dlp_info is not None and yt_dlp_info.get('id') is not None: + title = yt_dlp_info.get('title') + video_id = yt_dlp_info.get('id') + await ctx.send(f":twisted_rightwards_arrows: Añadiendo el mix de `{title}`...") + await play(ctx, f"https://www.youtube.com/watch?v={video_id}&list=RD{video_id}") + else: + await ctx.send(":no_entry_sign: No se ha encontrado ningún contenido.") + + def dalle_listener(result: DalleImages): dalle_results_queue.append(result) dalle_event.set() diff --git a/youtube.py b/youtube.py index 7816c17..334ad52 100644 --- a/youtube.py +++ b/youtube.py @@ -1,8 +1,9 @@ import logging as log import traceback -from typing import Any, Optional +from typing import Any import yt_dlp +from yt_dlp.extractor.youtube import YoutubeIE YT_DLP_FORMATS = 'bestaudio/251/250/249/233/234/hls-audio-128000-Audio/m4a/worstaudio/worst' YT_DLP_EXTRACTORS = yt_dlp.extractor.gen_extractors() @@ -39,7 +40,7 @@ def yt_search_and_extract_yt_dlp_info(search_query: str) -> Any: traceback.print_exc() -def yt_music_search_and_get_first_result_url(search_query: str) -> Optional[str]: +def yt_music_search_and_extract_yt_dlp_info(search_query: str) -> Any: try: ydl_opts = { 'format': YT_DLP_FORMATS, @@ -48,7 +49,7 @@ def yt_music_search_and_get_first_result_url(search_query: str) -> Optional[str] 'playlist_items': '1', # Only get first item in lists } with yt_dlp.YoutubeDL(ydl_opts) as ydl: - return ydl.extract_info(f"https://music.youtube.com/search?q={search_query}", download=False).get('entries')[0].get('url') + return ydl.extract_info(f"https://music.youtube.com/search?q={search_query}", download=False).get('entries')[0] except Exception: log.error("yt_music_search_and_get_first_result_url >> Exception thrown when extracting YouTube Music search info with yt-dlp.") @@ -60,3 +61,7 @@ def is_suitable_for_yt_dlp(url: str) -> bool: if extractor.suitable(url) and extractor.IE_NAME != 'generic': return True return False + + +def is_youtube_url(url: str) -> bool: + return YoutubeIE.suitable(url)