From ad91a8e2aedfe75015c62e2f5fd897850d9dead5 Mon Sep 17 00:00:00 2001 From: cloudwithax Date: Tue, 27 Dec 2022 22:23:38 -0500 Subject: [PATCH] add spotify recommendations --- pomice/exceptions.py | 10 ++++++++++ pomice/objects.py | 11 ++++++++--- pomice/player.py | 11 +++++++++++ pomice/pool.py | 36 ++++++++++++++++++++++++++++++++++-- pomice/spotify/__init__.py | 1 - pomice/spotify/client.py | 34 ++++++++++++++++++++++++++++++++-- pomice/spotify/objects.py | 6 +++--- 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/pomice/exceptions.py b/pomice/exceptions.py index f244191..77c42b5 100644 --- a/pomice/exceptions.py +++ b/pomice/exceptions.py @@ -71,6 +71,16 @@ class InvalidSpotifyClientAuthorization(PomiceException): """No Spotify client authorization was provided for track searching.""" pass +class SpotifyRequestException(PomiceException): + """An error occurred when making a request to the Spotify API""" + pass + + +class InvalidSpotifyURL(PomiceException): + """An invalid Spotify URL was passed""" + pass + + class QueueException(Exception): """Base Pomice queue exception.""" pass diff --git a/pomice/objects.py b/pomice/objects.py index 726dcff..191a282 100644 --- a/pomice/objects.py +++ b/pomice/objects.py @@ -1,5 +1,6 @@ import re -from typing import List, Optional +from typing import List, Optional, Union +from discord import Member, User from discord.ext import commands @@ -26,7 +27,8 @@ class Track: search_type: SearchType = SearchType.ytsearch, spotify_track = None, filters: Optional[List[Filter]] = None, - timestamp: Optional[float] = None + timestamp: Optional[float] = None, + requester: Optional[Union[Member, User]] = None ): self.track_id = track_id self.info = info @@ -56,7 +58,10 @@ class Track: self.length = info.get("length") self.ctx = ctx - self.requester = self.ctx.author if ctx else None + if requester: + self.requester = requester + else: + self.requester = self.ctx.author if ctx else None self.is_stream = info.get("isStream") self.is_seekable = info.get("isSeekable") self.position = info.get("position") diff --git a/pomice/player.py b/pomice/player.py index 47b4f0b..1d5a10a 100644 --- a/pomice/player.py +++ b/pomice/player.py @@ -274,6 +274,15 @@ class Player(VoiceProtocol): """ return await self._node.get_tracks(query, ctx=ctx, search_type=search_type, filters=filters) + async def get_recommendations(self, *, query: str, ctx: Optional[commands.Context] = None): + """ + Gets recommendations from Spotify. Query must be a valid Spotify Track URL. + + You can pass in a discord.py Context object to get a + Context object on all tracks that get recommended. + """ + return await self._node.get_recommendations(query=query, ctx=ctx) + async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False): await self.guild.change_voice_state(channel=self.channel, self_deaf=self_deaf, self_mute=self_mute) self._node._players[self.guild.id] = self @@ -450,6 +459,8 @@ class Player(VoiceProtocol): if fast_apply: await self.seek(self.position) + + diff --git a/pomice/pool.py b/pomice/pool.py index 3721fea..63fe7e8 100644 --- a/pomice/pool.py +++ b/pomice/pool.py @@ -22,10 +22,10 @@ from .exceptions import ( InvalidSpotifyClientAuthorization, NodeConnectionFailure, NodeCreationError, - NodeException, NodeNotAvailable, NoNodesAvailable, - TrackLoadError + TrackLoadError, + SpotifyTrackLoadFailed ) from .filters import Filter from .objects import Playlist, Track @@ -466,6 +466,38 @@ class Node: for track in data["tracks"] ] + async def get_recommendations(self, *, query: str, ctx: Optional[commands.Context] = None): + """ + Gets recommendations from Spotify. Query must be a valid Spotify Track URL. + + You can pass in a discord.py Context object to get a + Context object on all tracks that get recommended. + """ + results = await self._spotify_client.get_recommendations(query=query) + tracks = [ + Track( + track_id=track.id, + ctx=ctx, + spotify=True, + spotify_track=track, + info={ + "title": track.name, + "author": track.artists, + "length": track.length, + "identifier": track.id, + "uri": track.uri, + "isStream": False, + "isSeekable": True, + "position": 0, + "thumbnail": track.image, + "isrc": track.isrc + }, + requester=self.bot.user + ) for track in results + ] + + return tracks + class NodePool: """The base class for the node pool. diff --git a/pomice/spotify/__init__.py b/pomice/spotify/__init__.py index 3f012c5..4afb1c1 100644 --- a/pomice/spotify/__init__.py +++ b/pomice/spotify/__init__.py @@ -1,5 +1,4 @@ """Spotify module for Pomice, made possible by cloudwithax 2021""" -from .exceptions import * from .objects import * from .client import Client diff --git a/pomice/spotify/client.py b/pomice/spotify/client.py index 9b4ecae..9872715 100644 --- a/pomice/spotify/client.py +++ b/pomice/spotify/client.py @@ -6,7 +6,7 @@ import aiohttp import orjson as json -from .exceptions import InvalidSpotifyURL, SpotifyRequestException +from ..exceptions import InvalidSpotifyURL, SpotifyRequestException from .objects import * GRANT_URL = "https://accounts.spotify.com/api/token" @@ -16,6 +16,7 @@ SPOTIFY_URL_REGEX = re.compile( ) + class Client: """The base client for the Spotify module of Pomice. This class will do all the heavy lifting of getting all the metadata @@ -110,4 +111,33 @@ class Client: ] next_page_url = next_data["next"] - return Playlist(data, tracks) \ No newline at end of file + return Playlist(data, tracks) + + async def get_recommendations(self, *, query: str): + if not self._bearer_token or time.time() >= self._expiry: + await self._fetch_bearer_token() + + result = SPOTIFY_URL_REGEX.match(query) + spotify_type = result.group("type") + spotify_id = result.group("id") + + if not result: + raise InvalidSpotifyURL("The Spotify link provided is not valid.") + + if not spotify_type == "track": + raise InvalidSpotifyURL("The provided query is not a Spotify track.") + + request_url = REQUEST_URL.format(type="recommendation", id=f"?seed_tracks={spotify_id}") + + async with self.session.get(request_url, headers=self._bearer_headers) as resp: + if resp.status != 200: + raise SpotifyRequestException( + f"Error while fetching results: {resp.status} {resp.reason}" + ) + + data: dict = await resp.json(loads=json.loads) + + tracks = [Track(track) for track in data["tracks"]] + + return tracks + diff --git a/pomice/spotify/objects.py b/pomice/spotify/objects.py index b52b4d5..f46bb69 100644 --- a/pomice/spotify/objects.py +++ b/pomice/spotify/objects.py @@ -5,10 +5,10 @@ class Track: """The base class for a Spotify Track""" def __init__(self, data: dict, image = None) -> None: - self.name = data["name"] + self.name: str = data["name"] self.artists = ", ".join(artist["name"] for artist in data["artists"]) - self.length = data["duration_ms"] - self.id = data["id"] + self.length: float = data["duration_ms"] + self.id: str = data["id"] if data.get("external_ids"): self.isrc = data["external_ids"]["isrc"]