Upgrading PlayController and Spotify Connection

This commit is contained in:
Rafael Vargas
2022-03-26 17:42:49 -04:00
parent f30513f710
commit 4c66c64041
15 changed files with 213 additions and 163 deletions

View File

@@ -1,13 +1,20 @@
import asyncio
from Exceptions.Exceptions import DownloadingError, Error
from discord.ext.commands import Context from discord.ext.commands import Context
from discord import Client from discord import Client
from Controllers.AbstractController import AbstractController from Controllers.AbstractController import AbstractController
from Exceptions.Exceptions import ImpossibleMove, UnknownError from Exceptions.Exceptions import ImpossibleMove, UnknownError
from Controllers.ControllerResponse import ControllerResponse from Controllers.ControllerResponse import ControllerResponse
from Music.Downloader import Downloader
from Music.Searcher import Searcher
from Music.Song import Song
class PlayController(AbstractController): class PlayController(AbstractController):
def __init__(self, ctx: Context, bot: Client) -> None: def __init__(self, ctx: Context, bot: Client) -> None:
super().__init__(ctx, bot) super().__init__(ctx, bot)
self.__searcher = Searcher()
self.__down = Downloader()
async def run(self, args: str) -> ControllerResponse: async def run(self, args: str) -> ControllerResponse:
track = " ".join(args) track = " ".join(args)
@@ -25,7 +32,47 @@ class PlayController(AbstractController):
embed = self.embeds.UNKNOWN_ERROR() embed = self.embeds.UNKNOWN_ERROR()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)
await self.player.play(self.ctx, track, requester) try:
musics = await self.__searcher.search(track)
for music in musics:
song = Song(music, self.player.playlist, requester)
self.player.playlist.add_song(song)
quant = len(musics)
songs_preload = self.player.playlist.songs_to_preload
await self.__down.preload(songs_preload)
if quant == 1:
pos = len(self.player.playlist)
song = self.__down.finish_one_song(song)
if song.problematic:
embed = self.embeds.SONG_PROBLEMATIC()
error = DownloadingError()
response = ControllerResponse(self.ctx, embed, error)
elif not self.player.playing:
embed = self.embeds.SONG_ADDED(song.title)
response = ControllerResponse(self.ctx, embed)
else:
embed = self.embeds.SONG_ADDED_TWO(song.info, pos)
response = ControllerResponse(self.ctx, embed)
else:
embed = self.embeds.SONGS_ADDED(quant)
response = ControllerResponse(self.ctx, embed)
asyncio.create_task(self.player.play(self.ctx))
return response
except Exception as err:
if isinstance(err, Error):
print(f'DEVELOPER NOTE -> PlayController Error: {err.message}')
error = err
embed = self.embeds.CUSTOM_ERROR(error)
else:
error = UnknownError()
embed = self.embeds.UNKNOWN_ERROR()
return ControllerResponse(self.ctx, embed, error)
def __user_connected(self) -> bool: def __user_connected(self) -> bool:
if self.ctx.author.voice: if self.ctx.author.voice:

View File

@@ -1,3 +1,4 @@
import asyncio
from discord.ext.commands import Context from discord.ext.commands import Context
from discord import Client from discord import Client
from Controllers.AbstractController import AbstractController from Controllers.AbstractController import AbstractController
@@ -16,7 +17,7 @@ class ShuffleController(AbstractController):
self.player.playlist.shuffle() self.player.playlist.shuffle()
songs = self.player.playlist.songs_to_preload songs = self.player.playlist.songs_to_preload
await self.__down.preload(songs) asyncio.create_task(self.__down.preload(songs))
embed = self.embeds.SONGS_SHUFFLED() embed = self.embeds.SONGS_SHUFFLED()
return ControllerResponse(self.ctx, embed) return ControllerResponse(self.ctx, embed)
except Exception as e: except Exception as e:

View File

@@ -34,6 +34,16 @@ class BadCommandUsage(Error):
super().__init__(message, title, *args) super().__init__(message, title, *args)
class DownloadingError(Error):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class SpotifyError(Error):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class UnknownError(Error): class UnknownError(Error):
def __init__(self, message='', title='', *args: object) -> None: def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args) super().__init__(message, title, *args)

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
from typing import List from typing import List
from numpy import extract
from Config.Config import Configs from Config.Config import Configs
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -25,7 +27,7 @@ class Downloader():
'default_search': 'auto', 'default_search': 'auto',
'playliststart': 0, 'playliststart': 0,
'extract_flat': False, 'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH, 'playlistend': config.MAX_PLAYLIST_FORCED_LENGTH,
} }
__BASE_URL = 'https://www.youtube.com/watch?v={}' __BASE_URL = 'https://www.youtube.com/watch?v={}'
@@ -127,7 +129,10 @@ class Downloader():
extracted_info = ydl.extract_info(search, download=False) extracted_info = ydl.extract_info(search, download=False)
if self.__failed_to_extract(extracted_info): if self.__failed_to_extract(extracted_info):
self.__get_forced_extracted_info(extracted_info) extracted_info = self.__get_forced_extracted_info(title)
if extracted_info is None:
return {}
if self.__is_multiple_musics(extracted_info): if self.__is_multiple_musics(extracted_info):
return extracted_info['entries'][0] return extracted_info['entries'][0]

View File

@@ -36,71 +36,7 @@ class Player(commands.Cog):
def playlist(self) -> Playlist: def playlist(self) -> Playlist:
return self.__playlist return self.__playlist
async def play(self, ctx: Context, track: str, requester: str) -> str: async def play(self, ctx: Context) -> str:
try:
links, provider = self.__searcher.search(track)
if provider == Provider.Unknown or links == None:
embed = Embed(
title=self.__config.ERROR_TITLE,
description=self.__config.INVALID_INPUT,
colours=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
return None
if provider == Provider.YouTube:
links = await self.__down.extract_info(links[0])
if len(links) == 0:
embed = Embed(
title=self.__config.ERROR_TITLE,
description="This video is unavailable",
colours=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
return None
songs_quant = 0
for info in links:
song = self.__playlist.add_song(info, requester)
songs_quant += 1
songs_preload = self.__playlist.songs_to_preload
await self.__down.preload(songs_preload)
except Exception as e:
print(f'DEVELOPER NOTE -> Error while Downloading in Player: {e}')
embed = Embed(
title=self.__config.ERROR_TITLE,
description=self.__config.DOWNLOADING_ERROR,
colours=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
return
if songs_quant == 1:
song = self.__down.finish_one_song(song)
pos = len(self.__playlist)
if song.problematic:
embed = Embed(
title=self.__config.ERROR_TITLE,
description=self.__config.DOWNLOADING_ERROR,
colours=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
return None
elif not self.__playing:
embed = Embed(
title=self.__config.SONG_PLAYER,
description=self.__config.SONG_ADDED.format(song.title),
colour=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
else:
embed = self.__format_embed(song.info, self.__config.SONG_ADDED_TWO, pos)
await ctx.send(embed=embed)
else:
embed = Embed(
title=self.__config.SONG_PLAYER,
description=self.__config.SONGS_ADDED.format(songs_quant),
colour=self.__config.COLOURS['blue'])
await ctx.send(embed=embed)
if not self.__playing: if not self.__playing:
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)

View File

@@ -92,8 +92,7 @@ class Playlist(IPlaylist):
self.__current = last_song self.__current = last_song
return self.__current # return the song return self.__current # return the song
def add_song(self, identifier: str, requester: str) -> Song: def add_song(self, song: Song) -> Song:
song = Song(identifier=identifier, playlist=self, requester=requester)
self.__queue.append(song) self.__queue.append(song)
return song return song

View File

@@ -1,40 +1,44 @@
from Exceptions.Exceptions import InvalidInput, SpotifyError
from Music.Downloader import Downloader
from Music.Types import Provider from Music.Types import Provider
from Music.Spotify import SpotifySearch from Music.Spotify import SpotifySearch
from Utils.Utils import is_url from Utils.Utils import Utils
from Config.Messages import SearchMessages
class Searcher(): class Searcher():
def __init__(self) -> None: def __init__(self) -> None:
self.__Spotify = SpotifySearch() self.__Spotify = SpotifySearch()
self.__messages = SearchMessages()
self.__down = Downloader()
def search(self, music: str) -> list: async def search(self, track: str) -> list:
provider = self.__identify_source(music) provider = self.__identify_source(track)
if provider == Provider.Unknown:
raise InvalidInput(self.__messages.UNKNOWN_INPUT, self.__messages.UNKNOWN_INPUT_TITLE)
if provider == Provider.YouTube: elif provider == Provider.YouTube:
return [music], Provider.YouTube musics = await self.__down.extract_info(track)
return musics
elif provider == Provider.Spotify: elif provider == Provider.Spotify:
if self.__Spotify.connected == True: try:
musics = self.__Spotify.search(music) musics = self.__Spotify.search(track)
return musics, Provider.Name return musics
else: except:
print('DEVELOPER NOTE -> Spotify Not Connected') raise SpotifyError(self.__messages.SPOTIFY_ERROR, self.__messages.GENERIC_TITLE)
return [], Provider.Unknown
elif provider == Provider.Name: elif provider == Provider.Name:
return [music], Provider.Name return [track]
elif provider == Provider.Unknown: def __identify_source(self, track) -> Provider:
return None, Provider.Unknown if not Utils.is_url(track):
def __identify_source(self, music) -> Provider:
if not is_url(music):
return Provider.Name return Provider.Name
if "https://www.youtu" in music or "https://youtu.be" in music or "https://music.youtube" in music: if "https://www.youtu" in track or "https://youtu.be" in track or "https://music.youtube" in track:
return Provider.YouTube return Provider.YouTube
if "https://open.spotify.com" in music: if "https://open.spotify.com" in track:
return Provider.Spotify return Provider.Spotify
return Provider.Unknown return Provider.Unknown

View File

@@ -17,17 +17,15 @@ class Song(ISong):
self.__required_keys = ['url'] self.__required_keys = ['url']
for key in self.__required_keys: for key in self.__required_keys:
if key in info: if key in info.keys():
self.__info[key] = info[key] self.__info[key] = info[key]
else: else:
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}') print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
self.destroy() self.destroy()
for key in self.__usefull_keys: for key in self.__usefull_keys:
if key in info: if key in info.keys():
self.__info[key] = info[key] self.__info[key] = info[key]
else:
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
@property @property
def source(self) -> str: def source(self) -> str:

View File

@@ -1,4 +1,4 @@
import spotipy from spotipy import Spotify
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
from Config.Config import Configs from Config.Config import Configs
@@ -9,86 +9,67 @@ class SpotifySearch():
self.__connected = False self.__connected = False
self.__connect() self.__connect()
@property def __connect(self) -> None:
def connected(self):
return self.__connected
def __connect(self) -> bool:
try: try:
# Initialize the connection with Spotify API auth = SpotifyClientCredentials(self.__config.SPOTIFY_ID, self.__config.SPOTIFY_SECRET)
self.__api = spotipy.Spotify(auth_manager=SpotifyClientCredentials( self.__api = Spotify(auth_manager=auth)
client_id=self.__config.SPOTIFY_ID, client_secret=self.__config.SPOTIFY_SECRET))
self.__connected = True self.__connected = True
return True
except Exception as e: except Exception as e:
print(f'DEVELOPER NOTE -> Spotify Connection {e}') print(f'DEVELOPER NOTE -> Spotify Connection Error {e}')
return False
def search(self, music=str) -> list: def search(self, music: str) -> list:
type = music.split('/')[3].split('?')[0] type = music.split('/')[3].split('?')[0]
code = music.split('/')[4].split('?')[0] code = music.split('/')[4].split('?')[0]
if type == 'album': musics = []
musics = self.__get_album(code)
elif type == 'playlist': if self.__connected:
musics = self.__get_playlist(code) if type == 'album':
elif type == 'track': musics = self.__get_album(code)
musics = self.__get_track(code) elif type == 'playlist':
elif type == 'artist': musics = self.__get_playlist(code)
musics = self.__get_artist(code) elif type == 'track':
else: musics = self.__get_track(code)
return None elif type == 'artist':
musics = self.__get_artist(code)
return musics return musics
def __get_album(self, code=str) -> list: def __get_album(self, code: str) -> list:
if self.__connected == True: results = self.__api.album_tracks(code)
try: musics = results['items']
results = self.__api.album_tracks(code)
musics = results['items']
while results['next']: # Get the next pages while results['next']: # Get the next pages
results = self.__api.next(results) results = self.__api.next(results)
musics.extend(results['items']) musics.extend(results['items'])
musicsTitle = [] musicsTitle = []
for music in musics: for music in musics:
try: title = self.__extract_title(music)
title = self.__extract_title(music) musicsTitle.append(title)
musicsTitle.append(title)
except:
pass
return musicsTitle
except Exception as e:
raise e
def __get_playlist(self, code=str) -> list: return musicsTitle
try:
results = self.__api.playlist_items(code)
itens = results['items']
while results['next']: # Load the next pages def __get_playlist(self, code: str) -> list:
results = self.__api.next(results) results = self.__api.playlist_items(code)
itens.extend(results['items']) itens = results['items']
musics = [] while results['next']: # Load the next pages
for item in itens: results = self.__api.next(results)
musics.append(item['track']) itens.extend(results['items'])
titles = [] musics = []
for music in musics: for item in itens:
try: musics.append(item['track'])
title = self.__extract_title(music)
titles.append(title)
except Exception as e:
raise e
return titles titles = []
for music in musics:
title = self.__extract_title(music)
titles.append(title)
except Exception as e: return titles
raise e
def __get_track(self, code=str) -> list: def __get_track(self, code: str) -> list:
results = self.__api.track(code) results = self.__api.track(code)
name = results['name'] name = results['name']
artists = '' artists = ''
@@ -97,7 +78,7 @@ class SpotifySearch():
return [f'{name} {artists}'] return [f'{name} {artists}']
def __get_artist(self, code=str) -> list: def __get_artist(self, code: str) -> list:
results = self.__api.artist_top_tracks(code, country='BR') results = self.__api.artist_top_tracks(code, country='BR')
musics_titles = [] musics_titles = []

12
Utils/Sender.py Normal file
View File

@@ -0,0 +1,12 @@
from discord.ext.commands import Context
from discord import Embed
class Sender:
@classmethod
async def send_embed(cls, ctx: Context, embed: Embed) -> None:
pass
@classmethod
async def send_message(cls, ctx: Context, message: Embed) -> None:
pass

View File

@@ -5,6 +5,33 @@ from functools import wraps, partial
config = Configs() config = Configs()
class Utils:
@classmethod
def format_time(cls, duration) -> str:
if not duration:
return "00:00"
hours = duration // 60 // 60
minutes = duration // 60 % 60
seconds = duration % 60
return "{}{}{:02d}:{:02d}".format(
hours if hours else "",
":" if hours else "",
minutes,
seconds)
@classmethod
def is_url(cls, string) -> bool:
regex = re.compile(
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
if re.search(regex, string):
return True
else:
return False
def is_connected(ctx): def is_connected(ctx):
try: try:
voice_channel = ctx.guild.voice_client.channel voice_channel = ctx.guild.voice_client.channel

View File

@@ -1,3 +1,4 @@
from Exceptions.Exceptions import Error
from discord import Embed from discord import Embed
from Config.Config import Configs from Config.Config import Configs
from Config.Colors import Colors from Config.Colors import Colors
@@ -23,32 +24,36 @@ class Embeds:
) )
return embed return embed
def SONG_ADDED_TWO(self, info: dict, pos: int) -> Embed:
embed = self.SONG_INFO(info, self.__config.SONG_ADDED_TWO, pos)
return embed
def INVALID_INPUT(self) -> Embed: def INVALID_INPUT(self) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.ERROR_TITLE, title=self.__config.ERROR_TITLE,
description=self.__config.INVALID_INPUT, description=self.__config.INVALID_INPUT,
colours=self.__colors.BLUE) colour=self.__colors.BLACK)
return embed return embed
def UNAVAILABLE_VIDEO(self) -> Embed: def UNAVAILABLE_VIDEO(self) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.ERROR_TITLE, title=self.__config.ERROR_TITLE,
description=self.__config.VIDEO_UNAVAILABLE, description=self.__config.VIDEO_UNAVAILABLE,
colours=self.__colors.BLUE) colour=self.__colors.BLACK)
return embed return embed
def DOWNLOADING_ERROR(self) -> Embed: def DOWNLOADING_ERROR(self) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.ERROR_TITLE, title=self.__config.ERROR_TITLE,
description=self.__config.DOWNLOADING_ERROR, description=self.__config.DOWNLOADING_ERROR,
colours=self.__colors.BLUE) colour=self.__colors.BLACK)
return embed return embed
def SONG_ADDED(self, title: str) -> Embed: def SONG_ADDED(self, title: str) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.SONG_PLAYER, title=self.__config.SONG_PLAYER,
description=self.__config.SONG_ADDED.format(title), description=self.__config.SONG_ADDED.format(title),
colours=self.__colors.BLUE) colour=self.__colors.BLUE)
return embed return embed
def SONGS_ADDED(self, quant: int) -> Embed: def SONGS_ADDED(self, quant: int) -> Embed:
@@ -62,7 +67,7 @@ class Embeds:
embedvc = Embed( embedvc = Embed(
title=title, title=title,
description=f"[{info['title']}]({info['original_url']})", description=f"[{info['title']}]({info['original_url']})",
color=self.__colors.BLUE colour=self.__colors.BLUE
) )
embedvc.add_field(name=self.__config.SONGINFO_UPLOADER, embedvc.add_field(name=self.__config.SONGINFO_UPLOADER,
@@ -115,6 +120,14 @@ class Embeds:
) )
return embed return embed
def CUSTOM_ERROR(self, error: Error) -> Embed:
embed = Embed(
title=error.title,
description=error.message,
colour=self.__colors.BLACK
)
return embed
def WRONG_LENGTH_INPUT(self) -> Embed: def WRONG_LENGTH_INPUT(self) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.BAD_COMMAND_TITLE, title=self.__config.BAD_COMMAND_TITLE,
@@ -201,6 +214,13 @@ class Embeds:
) )
return embed return embed
def SONG_PROBLEMATIC(self) -> Embed:
embed = Embed(
title=self.__config.ERROR_TITLE,
description=self.__config.DOWNLOADING_ERROR,
colour=self.__colors.BLACK)
return embed
def NO_CHANNEL(self) -> Embed: def NO_CHANNEL(self) -> Embed:
embed = Embed( embed = Embed(
title=self.__config.IMPOSSIBLE_MOVE, title=self.__config.IMPOSSIBLE_MOVE,

View File

@@ -69,3 +69,13 @@ class Messages(Singleton):
self.BAD_COMMAND = f'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' self.BAD_COMMAND = f'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better'
self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot>' self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot>'
self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.' self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.'
class SearchMessages(Singleton):
def __init__(self) -> None:
if not super().created:
config = Configs()
self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play'
self.UNKNOWN_INPUT_TITLE = 'Nothing Found'
self.SPOTIFY_ERROR = 'Spotify could not process any songs with this input, verify your link or try again later.'
self.GENERIC_TITLE = 'Input could not be processed'

View File

@@ -17,6 +17,7 @@ class Configs(Singleton):
self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.' self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.'
self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_LENGTH = 50
self.MAX_PLAYLIST_FORCED_LENGTH = 5
self.MAX_PRELOAD_SONGS = 10 self.MAX_PRELOAD_SONGS = 10
self.MAX_SONGS_HISTORY = 15 self.MAX_SONGS_HISTORY = 15

View File

@@ -1,4 +1,3 @@
from distutils.command.config import config
from discord import Intents, Client from discord import Intents, Client
from os import listdir from os import listdir
from Config.Config import Configs from Config.Config import Configs