Adding some typing to the project and rebuilding the Downloader

This commit is contained in:
Rafael Vargas
2022-03-19 17:17:38 -04:00
parent 5510b5af78
commit e59efb0010
11 changed files with 300 additions and 180 deletions

View File

@@ -3,3 +3,4 @@ discord.py[voice]
python-decouple==3.5 python-decouple==3.5
spotipy==2.19.0 spotipy==2.19.0
yt-dlp==2021.12.1 yt-dlp==2021.12.1
typing_extensions==4.0.1

43
teste.py Normal file
View File

@@ -0,0 +1,43 @@
from vulkan.music.Downloader import Downloader
from vulkan.music.Playlist import Playlist
from vulkan.music.Song import Song
import asyncio
from yt_dlp import YoutubeDL
from vulkan.music.Types import Provider
# Link pego de mix
link1 = 'https://youtu.be/5w61TizfZXY?list=RDMM5w61TizfZXY'
# Video especifico
link2 = 'https://www.youtube.com/watch?v=WFEtDqLLv84&ab_channel=MMAK'
# Link pego de mix
link3 = 'https://www.youtube.com/watch?v=5w61TizfZXY&list=RDMM5w61TizfZXY&ab_channel=CantusFidei'
# Playlist
link4 = 'https://www.youtube.com/playlist?list=PLbbKJHHZR9SgWK6SBOwnTaaQauvhjJaNE'
# Nome
link5 = 'Rumbling'
down = Downloader()
playlist = Playlist()
__YDL_OPTIONS = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': True,
'playlistend': 5,
'noplaylist': True
}
async def main():
down = Downloader()
link = 'https://youtu.be/5w61TizfZXY?list=RDMM5w61TizfZXY'
infos = await down.extract_info('Rumbling')
song = playlist.add_song('Rumbling', 'Rafael')
await down.preload([song])
print(song.source)
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -59,7 +59,7 @@ class Control(commands.Cog):
if not my_error: if not my_error:
raise error raise error
else: else:
print(error) print(f'DEVELOPER NOTE -> Comand Error: {error}')
embed = discord.Embed( embed = discord.Embed(
title=config.ERROR_TITLE, title=config.ERROR_TITLE,
description=config.UNKNOWN_ERROR, description=config.UNKNOWN_ERROR,

View File

@@ -1,16 +1,17 @@
import discord from typing import Dict
from discord import Guild, Client, Embed
from discord.ext import commands from discord.ext import commands
from discord.ext.commands import Context
from config import config from config import config
from config import help from config import help
from vulkan.music.Player import Player from vulkan.music.Player import Player
from vulkan.music.utils import * from vulkan.music.utils import is_connected
class Music(commands.Cog): class Music(commands.Cog):
def __init__(self, bot) -> None: def __init__(self, bot) -> None:
self.__guilds = {} self.__guilds: Dict[Guild, Player] = {}
self.__bot: discord.Client = bot self.__bot: Client = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self) -> None: async def on_ready(self) -> None:
@@ -23,29 +24,29 @@ class Music(commands.Cog):
print(f'Player for guild {guild.name} created') print(f'Player for guild {guild.name} created')
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild) -> None: async def on_guild_join(self, guild: Guild) -> None:
"""Load a player when joining a guild""" """Load a player when joining a guild"""
self.__guilds[guild] = Player(self.__bot, guild) self.__guilds[guild] = Player(self.__bot, guild)
print(f'Player for guild {guild.name} created') print(f'Player for guild {guild.name} created')
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_remove(self, guild) -> None: async def on_guild_remove(self, guild: Guild) -> None:
"""Removes the player of the guild if banned""" """Removes the player of the guild if banned"""
if guild in self.__guilds.keys(): if guild in self.__guilds.keys():
self.__guilds.pop(guild, None) self.__guilds.pop(guild, None)
print(f'Player for guild {guild.name} destroyed') print(f'Player for guild {guild.name} destroyed')
@commands.command(name="play", help=help.HELP_PLAY, description=help.HELP_PLAY_LONG, aliases=['p', 'tocar']) @commands.command(name="play", help=help.HELP_PLAY, description=help.HELP_PLAY_LONG, aliases=['p', 'tocar'])
async def play(self, ctx, *args) -> None: async def play(self, ctx: Context, *args) -> None:
track = " ".join(args) track = " ".join(args)
requester = ctx.author.name requester = ctx.author.name
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
await self.__send_embed(ctx, config.ERROR_TITLE, config.NO_GUILD, 'red') await self.__send_embed(ctx, config.ERROR_TITLE, config.NO_GUILD, 'red')
return return
if is_connected(ctx) == None: if is_connected(ctx) is None:
success = await player.connect(ctx) success = await player.connect(ctx)
if success == False: if success == False:
await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red') await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red')
@@ -54,34 +55,34 @@ class Music(commands.Cog):
await player.play(ctx, track, requester) await player.play(ctx, track, requester)
@commands.command(name="queue", help=help.HELP_QUEUE, description=help.HELP_QUEUE_LONG, aliases=['q', 'fila']) @commands.command(name="queue", help=help.HELP_QUEUE, description=help.HELP_QUEUE_LONG, aliases=['q', 'fila'])
async def queue(self, ctx) -> None: async def queue(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
embed = await player.queue() embed = await player.queue()
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="skip", help=help.HELP_SKIP, description=help.HELP_SKIP_LONG, aliases=['s', 'pular']) @commands.command(name="skip", help=help.HELP_SKIP, description=help.HELP_SKIP_LONG, aliases=['s', 'pular'])
async def skip(self, ctx) -> None: async def skip(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
await player.skip(ctx) await player.skip(ctx)
@commands.command(name='stop', help=help.HELP_STOP, description=help.HELP_STOP_LONG, aliases=['parar']) @commands.command(name='stop', help=help.HELP_STOP, description=help.HELP_STOP_LONG, aliases=['parar'])
async def stop(self, ctx) -> None: async def stop(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
await player.stop() await player.stop()
@commands.command(name='pause', help=help.HELP_PAUSE, description=help.HELP_PAUSE_LONG, aliases=['pausar']) @commands.command(name='pause', help=help.HELP_PAUSE, description=help.HELP_PAUSE_LONG, aliases=['pausar'])
async def pause(self, ctx) -> None: async def pause(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
success = await player.pause() success = await player.pause()
@@ -89,9 +90,9 @@ class Music(commands.Cog):
await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_PAUSED, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_PAUSED, 'blue')
@commands.command(name='resume', help=help.HELP_RESUME, description=help.HELP_RESUME_LONG, aliases=['soltar']) @commands.command(name='resume', help=help.HELP_RESUME, description=help.HELP_RESUME_LONG, aliases=['soltar'])
async def resume(self, ctx) -> None: async def resume(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
success = await player.resume() success = await player.resume()
@@ -99,12 +100,12 @@ class Music(commands.Cog):
await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_RESUMED, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_RESUMED, 'blue')
@commands.command(name='prev', help=help.HELP_PREV, description=help.HELP_PREV_LONG, aliases=['anterior']) @commands.command(name='prev', help=help.HELP_PREV, description=help.HELP_PREV_LONG, aliases=['anterior'])
async def prev(self, ctx) -> None: async def prev(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
if is_connected(ctx) == None: if is_connected(ctx) is None:
success = await player.connect(ctx) success = await player.connect(ctx)
if success == False: if success == False:
await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red') await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red')
@@ -113,35 +114,35 @@ class Music(commands.Cog):
await player.play_prev(ctx) await player.play_prev(ctx)
@commands.command(name='history', help=help.HELP_HISTORY, description=help.HELP_HISTORY_LONG, aliases=['historico']) @commands.command(name='history', help=help.HELP_HISTORY, description=help.HELP_HISTORY_LONG, aliases=['historico'])
async def history(self, ctx) -> None: async def history(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
embed = player.history() embed = player.history()
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='loop', help=help.HELP_LOOP, description=help.HELP_LOOP_LONG, aliases=['l', 'repeat']) @commands.command(name='loop', help=help.HELP_LOOP, description=help.HELP_LOOP_LONG, aliases=['l', 'repeat'])
async def loop(self, ctx, args: str) -> None: async def loop(self, ctx: Context, args: str) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
description = await player.loop(args) description = await player.loop(args)
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
@commands.command(name='clear', help=help.HELP_CLEAR, description=help.HELP_CLEAR_LONG, aliases=['c', 'limpar']) @commands.command(name='clear', help=help.HELP_CLEAR, description=help.HELP_CLEAR_LONG, aliases=['c', 'limpar'])
async def clear(self, ctx) -> None: async def clear(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
await player.clear() await player.clear()
@commands.command(name='np', help=help.HELP_NP, description=help.HELP_NP_LONG, aliases=['playing', 'now']) @commands.command(name='np', help=help.HELP_NP, description=help.HELP_NP_LONG, aliases=['playing', 'now'])
async def now_playing(self, ctx) -> None: async def now_playing(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
embed = await player.now_playing() embed = await player.now_playing()
@@ -149,34 +150,34 @@ class Music(commands.Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='shuffle', help=help.HELP_SHUFFLE, description=help.HELP_SHUFFLE_LONG, aliases=['aleatorio']) @commands.command(name='shuffle', help=help.HELP_SHUFFLE, description=help.HELP_SHUFFLE_LONG, aliases=['aleatorio'])
async def shuffle(self, ctx) -> None: async def shuffle(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
description = await player.shuffle() description = await player.shuffle()
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
@commands.command(name='move', help=help.HELP_MOVE, description=help.HELP_MOVE_LONG, aliases=['m', 'mover']) @commands.command(name='move', help=help.HELP_MOVE, description=help.HELP_MOVE_LONG, aliases=['m', 'mover'])
async def move(self, ctx, pos1, pos2='1') -> None: async def move(self, ctx: Context, pos1, pos2='1') -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
description = await player.move(pos1, pos2) description = await player.move(pos1, pos2)
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
@commands.command(name='remove', help=help.HELP_REMOVE, description=help.HELP_REMOVE_LONG, aliases=['remover']) @commands.command(name='remove', help=help.HELP_REMOVE, description=help.HELP_REMOVE_LONG, aliases=['remover'])
async def remove(self, ctx, position) -> None: async def remove(self, ctx: Context, position) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
if player == None: if player is None:
return return
else: else:
description = await player.remove(position) description = await player.remove(position)
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue') await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
@commands.command(name='reset', help=help.HELP_RESET, description=help.HELP_RESET_LONG, aliases=['resetar']) @commands.command(name='reset', help=help.HELP_RESET, description=help.HELP_RESET_LONG, aliases=['resetar'])
async def reset(self, ctx) -> None: async def reset(self, ctx: Context) -> None:
player = self.__get_player(ctx) player = self.__get_player(ctx)
try: try:
await player.force_stop() await player.force_stop()
@@ -191,20 +192,20 @@ class Music(commands.Cog):
player = self.__get_player(ctx) player = self.__get_player(ctx)
print(f'Player for guild {ctx.guild} created') print(f'Player for guild {ctx.guild} created')
async def __send_embed(self, ctx, title='', description='', colour='grey') -> None: async def __send_embed(self, ctx: Context, title='', description='', colour='grey') -> None:
try: try:
colour = config.COLOURS[colour] colour = config.COLOURS[colour]
except: except:
colour = config.COLOURS['grey'] colour = config.COLOURS['grey']
embedvc = discord.Embed( embedvc = Embed(
title=title, title=title,
description=description, description=description,
colour=colour colour=colour
) )
await ctx.send(embed=embedvc) await ctx.send(embed=embedvc)
async def __clean_messages(self, ctx) -> None: async def __clean_messages(self, ctx: Context) -> None:
last_messages = await ctx.channel.history(limit=5).flatten() last_messages = await ctx.channel.history(limit=5).flatten()
for message in last_messages: for message in last_messages:
@@ -219,7 +220,7 @@ class Music(commands.Cog):
except: except:
continue continue
def __get_player(self, ctx) -> Player: def __get_player(self, ctx: Context) -> Player:
try: try:
return self.__guilds[ctx.guild] return self.__guilds[ctx.guild]
except: except:

View File

@@ -0,0 +1,3 @@
class Database:
def __init__(self) -> None:
pass

View File

@@ -1,75 +1,111 @@
import asyncio import asyncio
import concurrent.futures from typing import List
from config import config from config import config
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from yt_dlp.utils import ExtractorError, DownloadError from concurrent.futures import ThreadPoolExecutor
from vulkan.music.Song import Song from vulkan.music.Song import Song
from vulkan.music.utils import is_url from vulkan.music.utils import is_url, run_async
class Downloader(): class Downloader():
"""Download musics direct URL and title or Source from Youtube using a music name or Youtube URL""" """Download musics direct URL and title or Source from Youtube using a music name or Youtube URL"""
__YDL_OPTIONS = {'format': 'bestaudio/best',
def __init__(self) -> None: 'default_search': 'auto',
self.__YDL_OPTIONS = {'format': 'bestaudio/best', 'playliststart': 0,
'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
}
__YDL_OPTIONS_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto', 'default_search': 'auto',
'playliststart': 0, 'playliststart': 0,
'extract_flat': True, 'extract_flat': True,
'playlistend': config.MAX_PLAYLIST_LENGTH, 'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
} }
__YDL_OPTIONS_FORCE_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
}
__BASE_URL = 'https://www.youtube.com/watch?v={}'
def download_one(self, song: Song) -> Song: def __init__(self) -> None:
self.__music_keys_only = ['resolution', 'fps', 'quality']
self.__not_extracted_keys_only = ['ie_key']
self.__not_extracted_not_keys = ['entries']
self.__playlist_keys = ['entries']
async def finish_one_song(self, song: Song) -> Song:
"""Receives a song object, finish his download and return it""" """Receives a song object, finish his download and return it"""
if song.identifier == None: if song.identifier == None:
return None return None
if is_url(song.identifier): # Youtube URL if is_url(song.identifier):
song_info = self.__download_url(song.identifier) song_info = self.__download_url(song.identifier)
else: # Song name
song_info = self.__download_title(song.identifier)
if song_info == None:
song.destroy() # Destroy the music with problems
return None
else: else:
song_info = await self.__download_title(song.identifier)
song.finish_down(song_info) song.finish_down(song_info)
return song return song
def extract_youtube_link(self, playlist_url: str) -> list: async def preload(self, songs: List[Song]) -> None:
"""Download the full info of the songs objects"""
for song in songs:
asyncio.ensure_future(self.__download_song(song))
@run_async
def extract_info(self, url: str) -> List[dict]:
"""Extract all songs direct URL from a Youtube Link """Extract all songs direct URL from a Youtube Link
Arg: Url String Arg: Url String
Return: List with the direct youtube URL of each song Return: List with the direct youtube URL of each song
""" """
if is_url(playlist_url): # If Url if is_url(url): # If Url
options = self.__YDL_OPTIONS options = Downloader.__YDL_OPTIONS_EXTRACT
options['extract_flat'] = True options['extract_flat'] = False
with YoutubeDL(options) as ydl: with YoutubeDL(options) as ydl:
try: try:
result = ydl.extract_info(playlist_url, download=False) print('Normal Extraction')
songs_identifiers = [] print('A')
extracted_info = ydl.extract_info(url, download=False)
print('B')
if self.__failed_to_extract(extracted_info):
print('Forcing Extraction')
extracted_info = self.__get_forced_extracted_info(url)
if result.get('entries'): # If got a dict of musics if self.__is_music(extracted_info):
for entry in result['entries']: print('Is Music')
songs_identifiers.append( return [extracted_info['original_url']]
f"https://www.youtube.com/watch?v={entry['id']}")
else: # Or a single music elif self.__is_multiple_musics(extracted_info):
songs_identifiers.append(result['original_url']) print('Multiple Musics')
songs = []
for song in extracted_info['entries']:
songs.append(self.__BASE_URL.format(song['id']))
return songs
return songs_identifiers # Return a list else: # Failed to extract the songs
except (ExtractorError, DownloadError) as e: print(f'DEVELOPER NOTE -> Failed to Extract URL {url}')
return []
except Exception as e:
print(f'DEVELOPER NOTE -> Error Extracting Music: {e}')
return None return None
else: else:
return None return []
async def preload(self, songs: list) -> None: def __get_forced_extracted_info(self, url: str) -> list:
"""Download the full info of the song object""" options = Downloader.__YDL_OPTIONS_FORCE_EXTRACT
for song in songs: with YoutubeDL(options) as ydl:
asyncio.ensure_future(self.__download_songs(song)) try:
extracted_info = ydl.extract_info(url, download=False)
return extracted_info
except Exception as e:
print(f'DEVELOPER NOTE -> Error Forcing Extract Music: {e}')
return []
def __download_url(self, url) -> dict: def __download_url(self, url) -> dict:
"""Download musics full info and source from Music URL """Download musics full info and source from Music URL
@@ -77,62 +113,79 @@ class Downloader():
Arg: URL from Youtube Arg: URL from Youtube
Return: Dict with the full youtube information of the music, including source to play it Return: Dict with the full youtube information of the music, including source to play it
""" """
options = self.__YDL_OPTIONS options = Downloader.__YDL_OPTIONS
options['extract_flat'] = False
with YoutubeDL(options) as ydl: with YoutubeDL(options) as ydl:
try: try:
result = ydl.extract_info(url, download=False) result = ydl.extract_info(url, download=False)
return result return result
except (ExtractorError, DownloadError) as e: # Any type of error in download except Exception as e: # Any type of error in download
print(f'DEVELOPER NOTE -> Error Downloading URL {e}')
return None return None
async def __download_songs(self, song: Song) -> None: async def __download_song(self, song: Song) -> None:
"""Download a music object asynchronously""" """Download a music object asynchronously"""
if song.source != None: # If Music already preloaded if song.source is not None: # If Music already preloaded
return return None
def download_song(song): def __download_func(song: Song) -> None:
if is_url(song.identifier): # Youtube URL if is_url(song.identifier):
song_info = self.__download_url(song.identifier) song_info = self.__download_url(song.identifier)
else: # Song name
song_info = self.__download_title(song.identifier)
if song_info == None:
song.destroy() # Remove the song with problems from the playlist
else: else:
song_info = self.__download_title(song.identifier)
song.finish_down(song_info) song.finish_down(song_info)
# Creating a loop task to download each song # Creating a loop task to download each song
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor( executor = ThreadPoolExecutor(max_workers=config.MAX_PRELOAD_SONGS)
max_workers=config.MAX_PRELOAD_SONGS fs = {loop.run_in_executor(executor, __download_func, song)}
) await asyncio.wait(fs=fs, return_when=asyncio.ALL_COMPLETED)
await asyncio.wait(fs={loop.run_in_executor(executor, download_song, song)},
return_when=asyncio.ALL_COMPLETED)
@run_async
def __download_title(self, title: str) -> dict: def __download_title(self, title: str) -> dict:
"""Download a music full information using his name. """Download a music full information using his name.
Arg: Music Name Arg: Music Name
Return: A dict containing the song information Return: A dict containing the song information
""" """
if type(title) != str: options = Downloader.__YDL_OPTIONS
return None with YoutubeDL(options) as ydl:
config = self.__YDL_OPTIONS
config['extract_flat'] = False
with YoutubeDL(self.__YDL_OPTIONS) as ydl:
try: try:
search = f"ytsearch:{title}" search = f'ytsearch:{title}'
result = ydl.extract_info(search, download=False) extracted_info = ydl.extract_info(search, download=False)
if result == None: if self.__failed_to_extract(extracted_info):
return None self.__get_forced_extracted_info(extracted_info)
# Return a dict with the full info of first music if self.__is_multiple_musics(extracted_info):
return result['entries'][0] return extracted_info['entries'][0]
else:
print(f'DEVELOPER NOTE -> Failed to extract title {title}')
return {}
except Exception as e: except Exception as e:
return None print(f'DEVELOPER NOTE -> Error downloading title {title}: {e}')
return {}
def __is_music(self, extracted_info: dict) -> bool:
for key in self.__music_keys_only:
if key not in extracted_info.keys():
return False
return True
def __is_multiple_musics(self, extracted_info: dict) -> bool:
for key in self.__playlist_keys:
if key not in extracted_info.keys():
return False
return True
def __failed_to_extract(self, extracted_info: dict) -> bool:
if type(extracted_info) is not dict:
return False
for key in self.__not_extracted_keys_only:
if key not in extracted_info.keys():
return False
for key in self.__not_extracted_not_keys:
if key in extracted_info.keys():
return False
return True

View File

@@ -1,8 +1,8 @@
import discord
from discord.ext import commands from discord.ext import commands
from config import config from config import config
import datetime from discord import Client, Guild, FFmpegPCMAudio, Embed
from discord.ext.commands import Context
from datetime import timedelta
from vulkan.music.Downloader import Downloader from vulkan.music.Downloader import Downloader
from vulkan.music.Playlist import Playlist from vulkan.music.Playlist import Playlist
from vulkan.music.Searcher import Searcher from vulkan.music.Searcher import Searcher
@@ -12,24 +12,22 @@ from vulkan.music.utils import *
class Player(commands.Cog): class Player(commands.Cog):
def __init__(self, bot, guild): def __init__(self, bot: Client, guild: Guild):
self.__searcher: Searcher = Searcher() self.__searcher: Searcher = Searcher()
self.__down: Downloader = Downloader() self.__down: Downloader = Downloader()
self.__playlist: Playlist = Playlist() self.__playlist: Playlist = Playlist()
self.__bot: discord.Client = bot self.__bot: Client = bot
self.__guild: discord.Guild = guild self.__guild: Guild = guild
self.__timer = Timer(self.__timeout_handler) self.__timer = Timer(self.__timeout_handler)
self.__playing = False self.__playing = False
# Flag to control if the player should stop totally the playing # Flag to control if the player should stop totally the playing
self.__force_stop = False self.__force_stop = False
self.YDL_OPTIONS = {'format': 'bestaudio', 'noplaylist': 'True'}
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'} 'options': '-vn'}
async def connect(self, ctx) -> bool: async def connect(self, ctx: Context) -> bool:
if not ctx.author.voice: if not ctx.author.voice:
return False return False
@@ -37,10 +35,10 @@ class Player(commands.Cog):
await ctx.author.voice.channel.connect(reconnect=True, timeout=None) await ctx.author.voice.channel.connect(reconnect=True, timeout=None)
return True return True
def __play_next(self, error, ctx) -> None: def __play_next(self, error, ctx: Context) -> None:
if self.__force_stop: # If it's forced to stop player if self.__force_stop: # If it's forced to stop player
self.__force_stop = False self.__force_stop = False
return return None
song = self.__playlist.next_song() song = self.__playlist.next_song()
@@ -50,7 +48,7 @@ class Player(commands.Cog):
else: else:
self.__playing = False self.__playing = False
async def __play_music(self, ctx, song: Song) -> None: async def __play_music(self, ctx: Context, song: Song) -> None:
try: try:
source = self.__ensure_source(song) source = self.__ensure_source(song)
if source == None: if source == None:
@@ -58,7 +56,7 @@ class Player(commands.Cog):
self.__playing = True self.__playing = True
player = discord.FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
self.__guild.voice_client.play( self.__guild.voice_client.play(
player, after=lambda e: self.__play_next(e, ctx)) player, after=lambda e: self.__play_next(e, ctx))
@@ -72,30 +70,29 @@ class Player(commands.Cog):
except: except:
self.__play_next(None, ctx) self.__play_next(None, ctx)
async def play(self, ctx, track=str, requester=str) -> str: async def play(self, ctx: Context, track: str, requester: str) -> str:
try: try:
songs_names, provider = self.__searcher.search(track) links, provider = self.__searcher.search(track)
if provider == Provider.Unknown or songs_names == None: if provider == Provider.Unknown or links == None:
embed = discord.Embed( embed = Embed(
title=config.ERROR_TITLE, title=config.ERROR_TITLE,
description=config.INVALID_INPUT, description=config.INVALID_INPUT,
colours=config.COLOURS['blue']) colours=config.COLOURS['blue'])
await ctx.send(embed=embed) await ctx.send(embed=embed)
return return None
elif provider == Provider.YouTube: if provider == Provider.YouTube:
songs_names = self.__down.extract_youtube_link(songs_names[0]) links = await self.__down.extract_info(links[0])
songs_quant = 0 songs_quant = len(links)
for name in songs_names: for info in links:
song = self.__playlist.add_song(name, requester) song = self.__playlist.add_song(info, requester)
songs_quant += 1
songs_preload = self.__playlist.songs_to_preload songs_preload = self.__playlist.songs_to_preload
await self.__down.preload(songs_preload) await self.__down.preload(songs_preload)
except Exception as e:
except: print(f'DEVELOPER NOTE -> Error while Downloading in Player: {e}')
embed = discord.Embed( embed = Embed(
title=config.ERROR_TITLE, title=config.ERROR_TITLE,
description=config.DOWNLOADING_ERROR, description=config.DOWNLOADING_ERROR,
colours=config.COLOURS['blue']) colours=config.COLOURS['blue'])
@@ -103,18 +100,18 @@ class Player(commands.Cog):
return return
if songs_quant == 1: if songs_quant == 1:
song = self.__down.download_one(song) song = await self.__down.finish_one_song(song)
pos = len(self.__playlist) pos = len(self.__playlist)
if song == None: if song.problematic:
embed = discord.Embed( embed = Embed(
title=config.ERROR_TITLE, title=config.ERROR_TITLE,
description=config.DOWNLOADING_ERROR, description=config.DOWNLOADING_ERROR,
colours=config.COLOURS['blue']) colours=config.COLOURS['blue'])
await ctx.send(embed=embed) await ctx.send(embed=embed)
return return None
elif not self.__playing: elif not self.__playing:
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.SONG_ADDED.format(song.title), description=config.SONG_ADDED.format(song.title),
colour=config.COLOURS['blue']) colour=config.COLOURS['blue'])
@@ -123,7 +120,7 @@ class Player(commands.Cog):
embed = self.__format_embed(song.info, config.SONG_ADDED_TWO, pos) embed = self.__format_embed(song.info, config.SONG_ADDED_TWO, pos)
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.SONGS_ADDED.format(songs_quant), description=config.SONGS_ADDED.format(songs_quant),
colour=config.COLOURS['blue']) colour=config.COLOURS['blue'])
@@ -133,20 +130,20 @@ class Player(commands.Cog):
first_song = self.__playlist.next_song() first_song = self.__playlist.next_song()
await self.__play_music(ctx, first_song) await self.__play_music(ctx, first_song)
async def play_prev(self, ctx) -> None: async def play_prev(self, ctx: Context) -> None:
"""Stop the currently playing cycle, load the previous song and play""" """Stop the currently playing cycle, load the previous song and play"""
if self.__playlist.looping_one or self.__playlist.looping_all: # Do not allow play if loop if self.__playlist.looping_one or self.__playlist.looping_all: # Do not allow play if loop
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.LOOP_ON, description=config.LOOP_ON,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
return return None
song = self.__playlist.prev_song() # Prepare the prev song to play again song = self.__playlist.prev_song() # Prepare the prev song to play again
if song == None: if song == None:
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.NOT_PREVIOUS, description=config.NOT_PREVIOUS,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
@@ -161,7 +158,7 @@ class Player(commands.Cog):
await self.__play_music(ctx, song) await self.__play_music(ctx, song)
async def queue(self) -> discord.Embed: async def queue(self) -> Embed:
if self.__playlist.looping_one: if self.__playlist.looping_one:
info = self.__playlist.current.info info = self.__playlist.current.info
title = config.ONE_SONG_LOOPING title = config.ONE_SONG_LOOPING
@@ -191,7 +188,7 @@ class Player(commands.Cog):
song_name = song.title if song.title else config.SONG_DOWNLOADING song_name = song.title if song.title else config.SONG_DOWNLOADING
text += f"**`{pos}` - ** {song_name} - `{format_time(song.duration)}`\n" text += f"**`{pos}` - ** {song_name} - `{format_time(song.duration)}`\n"
embed = discord.Embed( embed = Embed(
title=title, title=title,
description=text, description=text,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
@@ -199,9 +196,9 @@ class Player(commands.Cog):
return embed return embed
async def skip(self, ctx) -> bool: async def skip(self, ctx: Context) -> bool:
if self.__playlist.looping_one: if self.__playlist.looping_one:
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.LOOP_ON, description=config.LOOP_ON,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
@@ -215,7 +212,7 @@ class Player(commands.Cog):
else: else:
return False return False
def history(self) -> discord.Embed: def history(self) -> Embed:
history = self.__playlist.songs_history history = self.__playlist.songs_history
if len(history) == 0: if len(history) == 0:
@@ -226,7 +223,7 @@ class Player(commands.Cog):
for pos, song in enumerate(history, start=1): for pos, song in enumerate(history, start=1):
text += f"**`{pos}` - ** {song.title} - `{format_time(song.duration)}`\n" text += f"**`{pos}` - ** {song.title} - `{format_time(song.duration)}`\n"
embed = discord.Embed( embed = Embed(
title=config.HISTORY_TITLE, title=config.HISTORY_TITLE,
description=text, description=text,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
@@ -234,7 +231,7 @@ class Player(commands.Cog):
return embed return embed
async def stop(self) -> bool: async def stop(self) -> bool:
if self.__guild.voice_client == None: if self.__guild.voice_client is None:
return False return False
if self.__guild.voice_client.is_connected(): if self.__guild.voice_client.is_connected():
@@ -246,6 +243,9 @@ class Player(commands.Cog):
async def force_stop(self) -> None: async def force_stop(self) -> None:
try: try:
if self.__guild.voice_client is None:
return
self.__guild.voice_client.stop() self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect() await self.__guild.voice_client.disconnect()
self.__playlist.clear() self.__playlist.clear()
@@ -288,9 +288,9 @@ class Player(commands.Cog):
async def clear(self) -> None: async def clear(self) -> None:
self.__playlist.clear() self.__playlist.clear()
async def now_playing(self) -> discord.Embed: async def now_playing(self) -> Embed:
if not self.__playing: if not self.__playing:
embed = discord.Embed( embed = Embed(
title=config.SONG_PLAYER, title=config.SONG_PLAYER,
description=config.PLAYER_NOT_PLAYING, description=config.PLAYER_NOT_PLAYING,
colour=config.COLOURS['blue'] colour=config.COLOURS['blue']
@@ -348,9 +348,9 @@ class Player(commands.Cog):
result = self.__playlist.remove_song(position) result = self.__playlist.remove_song(position)
return result return result
def __format_embed(self, info: dict, title='', position='Playing Now') -> discord.Embed: def __format_embed(self, info: dict, title='', position='Playing Now') -> Embed:
"""Configure the embed to show the song information""" """Configure the embed to show the song information"""
embedvc = discord.Embed( embedvc = Embed(
title=title, title=title,
description=f"[{info['title']}]({info['original_url']})", description=f"[{info['title']}]({info['original_url']})",
color=config.COLOURS['blue'] color=config.COLOURS['blue']
@@ -368,7 +368,7 @@ class Player(commands.Cog):
embedvc.set_thumbnail(url=info['thumbnail']) embedvc.set_thumbnail(url=info['thumbnail'])
if 'duration' in info.keys(): if 'duration' in info.keys():
duration = str(datetime.timedelta(seconds=info['duration'])) duration = str(timedelta(seconds=info['duration']))
embedvc.add_field(name=config.SONGINFO_DURATION, embedvc.add_field(name=config.SONGINFO_DURATION,
value=f"{duration}", value=f"{duration}",
inline=True) inline=True)

View File

@@ -1,4 +1,5 @@
from collections import deque from collections import deque
from typing import List
from config import config from config import config
import random import random
@@ -35,7 +36,7 @@ class Playlist(IPlaylist):
return self.__current return self.__current
@property @property
def songs_to_preload(self) -> list: def songs_to_preload(self) -> List[Song]:
return list(self.__queue)[:config.MAX_PRELOAD_SONGS] return list(self.__queue)[:config.MAX_PRELOAD_SONGS]
def __len__(self) -> int: def __len__(self) -> int:
@@ -68,7 +69,6 @@ class Playlist(IPlaylist):
return None return None
self.__current = self.__queue.popleft() self.__current = self.__queue.popleft()
return self.__current return self.__current
def prev_song(self) -> Song: def prev_song(self) -> Song:

View File

@@ -15,22 +15,22 @@ class Searcher():
Arg -> User Input, a string with the Arg -> User Input, a string with the
Return -> A list of musics names and Provider Type Return -> A list of musics names and Provider Type
""" """
url_type = self.__identify_source(music) provider = self.__identify_source(music)
if url_type == Provider.YouTube: if provider == Provider.YouTube:
return [music], Provider.YouTube return [music], Provider.YouTube
elif url_type == Provider.Spotify: elif provider == Provider.Spotify:
if self.__Spotify.connected == True: if self.__Spotify.connected == True:
musics = self.__Spotify.search(music) musics = self.__Spotify.search(music)
return musics, Provider.Name return musics, Provider.Name
else: else:
return [], Provider.Unknown return [], Provider.Unknown
elif url_type == Provider.Name: elif provider == Provider.Name:
return [music], Provider.Name return [music], Provider.Name
elif url_type == Provider.Unknown: elif provider == Provider.Unknown:
return None, Provider.Unknown return None, Provider.Unknown
def __identify_source(self, music) -> Provider: def __identify_source(self, music) -> Provider:
@@ -38,11 +38,10 @@ class Searcher():
if not is_url(music): if not is_url(music):
return Provider.Name return Provider.Name
if "https://www.youtu" in music or "https://youtu.be" in music: if "https://www.youtu" in music or "https://youtu.be" in music or "https://music.youtube" in music:
return Provider.YouTube return Provider.YouTube
if "https://open.spotify.com" in music: if "https://open.spotify.com" in music:
return Provider.Spotify return Provider.Spotify
# If no match
return Provider.Unknown return Provider.Unknown

View File

@@ -13,16 +13,24 @@ class Song(ISong):
def finish_down(self, info: dict) -> None: def finish_down(self, info: dict) -> None:
"""Get and store the full information of the song""" """Get and store the full information of the song"""
self.__usefull_keys = ['url', 'duration', self.__usefull_keys = ['duration',
'title', 'webpage_url', 'title', 'webpage_url',
'channel', 'id', 'uploader', 'channel', 'id', 'uploader',
'thumbnail', 'original_url'] 'thumbnail', 'original_url']
self.__required_keys = ['url']
for key in self.__required_keys:
if key in info:
self.__info[key] = info[key]
else:
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
self.destroy()
for key in self.__usefull_keys: for key in self.__usefull_keys:
try: if key in info:
self.__info[key] = info[key] self.__info[key] = info[key]
except Exception as e: else:
raise e print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
@property @property
def source(self) -> str: def source(self) -> str:
@@ -58,6 +66,7 @@ class Song(ISong):
def destroy(self) -> None: def destroy(self) -> None:
"""Mark this song with problems and removed from the playlist due to any type of error""" """Mark this song with problems and removed from the playlist due to any type of error"""
print(f'DEVELOPER NOTE -> Music self destroying {self.__identifier}')
self.__problematic = True self.__problematic = True
self.__playlist.destroy_song(self) self.__playlist.destroy_song(self)

View File

@@ -1,6 +1,7 @@
import re import re
import asyncio import asyncio
from config import config from config import config
from functools import wraps, partial
def is_connected(ctx): def is_connected(ctx):
@@ -53,3 +54,13 @@ class Timer:
def cancel(self): def cancel(self):
self.__task.cancel() self.__task.cancel()
def run_async(func):
@wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = asyncio.get_event_loop()
partial_func = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, partial_func)
return run