add spotify recommendations

This commit is contained in:
cloudwithax 2022-12-27 22:23:38 -05:00
parent 0d00fe9f36
commit ad91a8e2ae
7 changed files with 98 additions and 11 deletions

View File

@ -71,6 +71,16 @@ class InvalidSpotifyClientAuthorization(PomiceException):
"""No Spotify client authorization was provided for track searching.""" """No Spotify client authorization was provided for track searching."""
pass 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): class QueueException(Exception):
"""Base Pomice queue exception.""" """Base Pomice queue exception."""
pass pass

View File

@ -1,5 +1,6 @@
import re import re
from typing import List, Optional from typing import List, Optional, Union
from discord import Member, User
from discord.ext import commands from discord.ext import commands
@ -26,7 +27,8 @@ class Track:
search_type: SearchType = SearchType.ytsearch, search_type: SearchType = SearchType.ytsearch,
spotify_track = None, spotify_track = None,
filters: Optional[List[Filter]] = 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.track_id = track_id
self.info = info self.info = info
@ -56,7 +58,10 @@ class Track:
self.length = info.get("length") self.length = info.get("length")
self.ctx = ctx 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_stream = info.get("isStream")
self.is_seekable = info.get("isSeekable") self.is_seekable = info.get("isSeekable")
self.position = info.get("position") self.position = info.get("position")

View File

@ -274,6 +274,15 @@ class Player(VoiceProtocol):
""" """
return await self._node.get_tracks(query, ctx=ctx, search_type=search_type, filters=filters) 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): 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) await self.guild.change_voice_state(channel=self.channel, self_deaf=self_deaf, self_mute=self_mute)
self._node._players[self.guild.id] = self self._node._players[self.guild.id] = self
@ -456,3 +465,5 @@ class Player(VoiceProtocol):

View File

@ -22,10 +22,10 @@ from .exceptions import (
InvalidSpotifyClientAuthorization, InvalidSpotifyClientAuthorization,
NodeConnectionFailure, NodeConnectionFailure,
NodeCreationError, NodeCreationError,
NodeException,
NodeNotAvailable, NodeNotAvailable,
NoNodesAvailable, NoNodesAvailable,
TrackLoadError TrackLoadError,
SpotifyTrackLoadFailed
) )
from .filters import Filter from .filters import Filter
from .objects import Playlist, Track from .objects import Playlist, Track
@ -466,6 +466,38 @@ class Node:
for track in data["tracks"] 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: class NodePool:
"""The base class for the node pool. """The base class for the node pool.

View File

@ -1,5 +1,4 @@
"""Spotify module for Pomice, made possible by cloudwithax 2021""" """Spotify module for Pomice, made possible by cloudwithax 2021"""
from .exceptions import *
from .objects import * from .objects import *
from .client import Client from .client import Client

View File

@ -6,7 +6,7 @@ import aiohttp
import orjson as json import orjson as json
from .exceptions import InvalidSpotifyURL, SpotifyRequestException from ..exceptions import InvalidSpotifyURL, SpotifyRequestException
from .objects import * from .objects import *
GRANT_URL = "https://accounts.spotify.com/api/token" GRANT_URL = "https://accounts.spotify.com/api/token"
@ -16,6 +16,7 @@ SPOTIFY_URL_REGEX = re.compile(
) )
class Client: class Client:
"""The base client for the Spotify module of Pomice. """The base client for the Spotify module of Pomice.
This class will do all the heavy lifting of getting all the metadata This class will do all the heavy lifting of getting all the metadata
@ -111,3 +112,32 @@ class Client:
next_page_url = next_data["next"] next_page_url = next_data["next"]
return Playlist(data, tracks) 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

View File

@ -5,10 +5,10 @@ class Track:
"""The base class for a Spotify Track""" """The base class for a Spotify Track"""
def __init__(self, data: dict, image = None) -> None: 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.artists = ", ".join(artist["name"] for artist in data["artists"])
self.length = data["duration_ms"] self.length: float = data["duration_ms"]
self.id = data["id"] self.id: str = data["id"]
if data.get("external_ids"): if data.get("external_ids"):
self.isrc = data["external_ids"]["isrc"] self.isrc = data["external_ids"]["isrc"]