aboutsummaryrefslogtreecommitdiff
path: root/code/utils
diff options
context:
space:
mode:
Diffstat (limited to 'code/utils')
-rw-r--r--code/utils/command_tree.py47
-rw-r--r--code/utils/config.py61
-rw-r--r--code/utils/custom_sources.py72
-rw-r--r--code/utils/source_helpers/apple/album.py74
-rw-r--r--code/utils/source_helpers/apple/playlist.py88
-rw-r--r--code/utils/source_helpers/apple/song.py68
-rw-r--r--code/utils/source_helpers/parse.py91
-rw-r--r--code/utils/source_helpers/spotify/album.py68
-rw-r--r--code/utils/source_helpers/spotify/artist.py77
-rw-r--r--code/utils/source_helpers/spotify/playlist.py68
-rw-r--r--code/utils/source_helpers/spotify/song.py63
11 files changed, 710 insertions, 67 deletions
diff --git a/code/utils/command_tree.py b/code/utils/command_tree.py
index 3d9214d..afdbd2c 100644
--- a/code/utils/command_tree.py
+++ b/code/utils/command_tree.py
@@ -1,9 +1,8 @@
import discord
from discord import app_commands
from discord.ext.commands.errors import *
-import datetime
-from utils.config import BOT_COLOR
+from utils.config import create_embed
from utils.custom_sources import LoadError
@@ -38,16 +37,9 @@ class Tree(app_commands.CommandTree):
# Custom Error class for the `create_player` function
# Issues that arise may be user not in vc, user not in correct vc, missing perms, etc.
elif isinstance(error, CheckPlayerError):
- embed = discord.Embed(
+ embed = create_embed(
title=error.info["title"],
description=error.info["description"],
- color=BOT_COLOR,
- )
- embed.set_footer(
- text=datetime.datetime.now(datetime.timezone.utc).strftime(
- "%Y-%m-%d %H:%M:%S"
- )
- + " UTC"
)
try:
await interaction.response.send_message(
@@ -62,46 +54,13 @@ class Tree(app_commands.CommandTree):
isinstance(error, app_commands.CheckFailure)
and interaction.command.name in music_commands
):
- embed = discord.Embed(
+ embed = create_embed(
title="Player Creation Error",
description=(
"An error occured when trying to create a player. Please"
" submit a bug report with </bug:1224840889906499626> if"
" this issue persists."
),
- color=BOT_COLOR,
- )
- embed.set_footer(
- text=datetime.datetime.now(datetime.timezone.utc).strftime(
- "%Y-%m-%d %H:%M:%S"
- )
- + " UTC"
- )
- try:
- await interaction.response.send_message(
- embed=embed, ephemeral=True
- )
- except discord.errors.InteractionResponded:
- await interaction.followup.send(embed=embed, ephemeral=True)
-
- # If a Spotify song is linked but cannot be found on a provider (e.g. YouTube)
- elif isinstance(error, LoadError):
- embed = discord.Embed(
- title="Nothing Found",
- description=(
- "Spotify does not allow direct play, meaning songs have to"
- " be found on a supported provider. In this case, the song"
- " couldn't be found. Please try again with a different"
- " song, or try searching for just the name and artist"
- " manually rather than sending a link."
- ),
- color=BOT_COLOR,
- )
- embed.set_footer(
- text=datetime.datetime.now(datetime.timezone.utc).strftime(
- "%Y-%m-%d %H:%M:%S"
- )
- + " UTC"
)
try:
await interaction.response.send_message(
diff --git a/code/utils/config.py b/code/utils/config.py
index 4569cd4..f2e0d01 100644
--- a/code/utils/config.py
+++ b/code/utils/config.py
@@ -8,6 +8,7 @@ import sys
import discord
import logging
import requests
+from datetime import datetime
from colorlog import ColoredFormatter
log_level = logging.DEBUG
@@ -32,7 +33,8 @@ BOT_COLOR = None
BOT_INVITE_LINK = None
FEEDBACK_CHANNEL_ID = None
BUG_CHANNEL_ID = None
-YOUTUBE_SUPPORT = None
+LOG_SONGS = False
+YOUTUBE_SUPPORT = False
SPOTIFY_CLIENT_ID = None
SPOTIFY_CLIENT_SECRET = None
GENIUS_CLIENT_ID = None
@@ -53,10 +55,17 @@ schema = {
"bot_invite_link": {"type": "string"},
"feedback_channel_id": {"type": "integer"},
"bug_channel_id": {"type": "integer"},
- "youtube_support": {"type": "boolean"},
+ "log_songs": {"type": "boolean"},
},
"required": ["token"],
},
+ "youtube": {
+ "type": "object",
+ "properties": {
+ "enabled": {"type": "boolean"},
+ },
+ "required": ["enabled"],
+ },
"spotify": {
"type": "object",
"properties": {
@@ -117,7 +126,10 @@ bot_info:
bot_invite_link: ""
feedback_channel_id: ""
bug_channel_id: ""
- youtube_support: false
+ log_songs: true
+
+youtube:
+ enabled: false
spotify:
spotify_client_id: ""
@@ -148,7 +160,7 @@ lavalink:
# Thouroughly validate all of the options in the config.yaml file
def validate_config(file_contents):
- global TOKEN, BOT_COLOR, BOT_INVITE_LINK, FEEDBACK_CHANNEL_ID, BUG_CHANNEL_ID, YOUTUBE_SUPPORT, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, GENIUS_CLIENT_ID, GENIUS_CLIENT_SECRET, OPENAI_API_KEY, LAVALINK_HOST, LAVALINK_PORT, LAVALINK_PASSWORD
+ global TOKEN, BOT_COLOR, BOT_INVITE_LINK, FEEDBACK_CHANNEL_ID, BUG_CHANNEL_ID, LOG_SONGS, YOUTUBE_SUPPORT, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, GENIUS_CLIENT_ID, GENIUS_CLIENT_SECRET, OPENAI_API_KEY, LAVALINK_HOST, LAVALINK_PORT, LAVALINK_PASSWORD
config = yaml.safe_load(file_contents)
try:
@@ -208,10 +220,12 @@ def validate_config(file_contents):
else:
BUG_CHANNEL_ID = config["bot_info"]["bug_channel_id"]
- if "youtube_support" in config["bot_info"]:
- YOUTUBE_SUPPORT = bool(config["bot_info"]["youtube_support"])
- else:
- YOUTUBE_SUPPORT = False
+ if "log_songs" in config["bot_info"]:
+ LOG_SONGS = bool(config["bot_info"]["log_songs"])
+
+ # Check for YouTube support
+ if "youtube" in config:
+ YOUTUBE_SUPPORT = bool(config["youtube"]["enabled"])
#
# If the SPOTIFY section is present, make sure the client ID and secret are valid
@@ -274,3 +288,34 @@ def validate_config(file_contents):
LAVALINK_HOST = config["lavalink"]["host"]
LAVALINK_PORT = config["lavalink"]["port"]
LAVALINK_PASSWORD = config["lavalink"]["password"]
+
+
+"""
+Template for embeds
+"""
+
+
+def create_embed(
+ title: str = None,
+ description: str = None,
+ color=None,
+ footer=None,
+ thumbnail=None,
+):
+ embed = discord.Embed(
+ title=title,
+ description=description,
+ color=color if color else BOT_COLOR,
+ )
+
+ if footer:
+ embed.set_footer(text=footer)
+ # else:
+ # embed.set_footer(
+ # text=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + " UTC"
+ # )
+
+ if thumbnail:
+ embed.set_thumbnail(url=thumbnail)
+
+ return embed
diff --git a/code/utils/custom_sources.py b/code/utils/custom_sources.py
index 40544c1..5cf2295 100644
--- a/code/utils/custom_sources.py
+++ b/code/utils/custom_sources.py
@@ -6,6 +6,8 @@ from lavalink import (
PlaylistInfo,
)
+from utils.config import YOUTUBE_SUPPORT
+
class LoadError(
Exception
@@ -32,21 +34,22 @@ class CustomAudioTrack(DeferredAudioTrack):
LoadType.EMPTY,
LoadType.ERROR,
):
- ytmsearch = f"ytmsearch:{self.title} {self.author}"
- results = await client.get_tracks(ytmsearch)
-
- if not results.tracks or results.load_type in (
- LoadType.EMPTY,
- LoadType.ERROR,
- ):
- ytsearch = f"ytsearch:{self.title} {self.author} audio"
- results = await client.get_tracks(ytsearch)
+ if YOUTUBE_SUPPORT:
+ ytmsearch = f"ytmsearch:{self.title} {self.author}"
+ results = await client.get_tracks(ytmsearch)
if not results.tracks or results.load_type in (
LoadType.EMPTY,
LoadType.ERROR,
):
- raise LoadError
+ ytsearch = f"ytsearch:{self.title} {self.author} audio"
+ results = await client.get_tracks(ytsearch)
+
+ if not results.tracks or results.load_type in (
+ LoadType.EMPTY,
+ LoadType.ERROR,
+ ):
+ raise LoadError
first_track = results.tracks[
0
@@ -70,6 +73,10 @@ class SpotifySource(Source):
) # Initialising our custom source with the name 'custom'.
async def load_item(self, user, metadata):
+ try:
+ artwork_url = metadata["album"]["images"][0]["url"]
+ except IndexError:
+ artwork_url = None
track = CustomAudioTrack(
{ # Create an instance of our CustomAudioTrack.
"identifier": metadata[
@@ -82,7 +89,7 @@ class SpotifySource(Source):
"title": metadata["name"],
"uri": metadata["external_urls"]["spotify"],
"duration": metadata["duration_ms"],
- "artworkUrl": metadata["album"]["images"][0]["url"],
+ "artworkUrl": artwork_url,
},
requester=user,
)
@@ -91,6 +98,11 @@ class SpotifySource(Source):
)
async def load_album(self, user, metadata):
+ try:
+ artwork_url = metadata["images"][0]["url"]
+ except IndexError:
+ artwork_url = None
+
tracks = []
for track in metadata["tracks"][
"items"
@@ -108,7 +120,7 @@ class SpotifySource(Source):
"title": track["name"],
"uri": track["external_urls"]["spotify"],
"duration": track["duration_ms"],
- "artworkUrl": metadata["images"][0]["url"],
+ "artworkUrl": artwork_url,
},
requester=user,
)
@@ -123,6 +135,10 @@ class SpotifySource(Source):
for track in metadata["tracks"][
"items"
]: # Loop through each track in the playlist.
+ try:
+ artwork_url = track["track"]["album"]["images"][0]["url"]
+ except IndexError:
+ artwork_url = None
tracks.append(
CustomAudioTrack(
{ # Create an instance of our CustomAudioTrack.
@@ -136,9 +152,7 @@ class SpotifySource(Source):
"title": track["track"]["name"],
"uri": track["track"]["external_urls"]["spotify"],
"duration": track["track"]["duration_ms"],
- "artworkUrl": track["track"]["album"]["images"][0][
- "url"
- ],
+ "artworkUrl": artwork_url,
},
requster=user,
)
@@ -148,6 +162,34 @@ class SpotifySource(Source):
LoadType.PLAYLIST, tracks, playlist_info=PlaylistInfo.none()
)
+ async def load_artist(self, user, metadata):
+ tracks = []
+ for track in metadata["tracks"]:
+ try:
+ artwork_url = track["album"]["images"][0]["url"]
+ except IndexError:
+ artwork_url = None
+ tracks.append(
+ CustomAudioTrack(
+ {
+ "identifier": track["id"],
+ "isSeekable": True,
+ "author": track["artists"][0]["name"],
+ "length": track["duration_ms"],
+ "isStream": False,
+ "title": track["name"],
+ "uri": track["external_urls"]["spotify"],
+ "duration": track["duration_ms"],
+ "artworkUrl": artwork_url,
+ },
+ requester=user,
+ )
+ )
+
+ return LoadResult(
+ LoadType.PLAYLIST, tracks, playlist_info=PlaylistInfo.none()
+ )
+
"""
Custom Source for Apple Music links
diff --git a/code/utils/source_helpers/apple/album.py b/code/utils/source_helpers/apple/album.py
new file mode 100644
index 0000000..aa4ea0d
--- /dev/null
+++ b/code/utils/source_helpers/apple/album.py
@@ -0,0 +1,74 @@
+import datetime
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the album info from the Apple Music API
+ """
+ album_id = query.split("/album/")[1].split("/")[1]
+
+ try:
+ # Get the album info
+ response = requests.get(
+ f"https://api.music.apple.com/v1/catalog/us/albums/{album_id}",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Album Not Found",
+ description=(
+ "The album could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Apple Music API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the album info
+ album = response.json()
+ name = album["data"][0]["attributes"]["name"]
+ artist = album["data"][0]["attributes"]["artistName"]
+ num_tracks = len(album["data"][0]["relationships"]["tracks"]["data"])
+ except IndexError:
+ LOG.error("Failed unpacking Apple Music album info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Apple Music API")
+ return None, None
+
+ # Extract artwork URL, if available
+ artwork_url = (
+ album["data"][0]["attributes"].get("artwork", {}).get("url", None)
+ )
+ if artwork_url:
+ artwork_url = artwork_url.replace("{w}x{h}", "300x300")
+
+ embed = create_embed(
+ title="Album Queued",
+ description=(
+ f"**{name}** by **{artist}**\n"
+ f"` {num_tracks} ` tracks\n\n"
+ f"Queued by: {user.mention}"
+ ),
+ thumbnail=artwork_url,
+ )
+
+ return album, embed
diff --git a/code/utils/source_helpers/apple/playlist.py b/code/utils/source_helpers/apple/playlist.py
new file mode 100644
index 0000000..65dfbf8
--- /dev/null
+++ b/code/utils/source_helpers/apple/playlist.py
@@ -0,0 +1,88 @@
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the playlist info from the Apple Music API
+ """
+ playlist_id = query.split("/playlist/")[1].split("/")[1]
+ try:
+ # Get all of the tracks in the playlist (limit at 100)
+ response = requests.get(
+ f"https://api.music.apple.com/v1/catalog/us/playlists/{playlist_id}/tracks?limit=100",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Playlist Not Found",
+ description=(
+ "The playlist could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Apple Music API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ playlist = response.json()
+
+ # Get the general playlist info (name, artwork)
+ response = requests.get(
+ f"https://api.music.apple.com/v1/catalog/us/playlists/{playlist_id}",
+ headers=headers,
+ )
+
+ response.raise_for_status()
+ # Unpack the playlist info
+ playlist_info = response.json()
+ name = playlist_info["data"][0]["attributes"]["name"]
+ num_tracks = len(playlist["data"])
+ except IndexError:
+ LOG.error("Failed unpacking Apple Music playlist info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Apple Music API")
+ return None, None
+
+ # Extract artwork URL, if available
+ artwork_url = (
+ playlist_info["data"][0]["attributes"]
+ .get("artwork", {})
+ .get("url", None)
+ )
+ if artwork_url:
+ artwork_url = artwork_url.replace("{w}x{h}", "300x300")
+
+ embed = create_embed(
+ title="Playlist Queued",
+ description=(
+ f"**{name}**\n` {num_tracks} ` tracks\n\nQueued by: {user.mention}"
+ ),
+ thumbnail=artwork_url,
+ )
+
+ # Add small alert if the playlist is the max size
+ if len(playlist["data"]) == 100:
+ embed.description += (
+ "\n\n*This playlist is longer than the 100 song"
+ " maximum. Only the first 100 songs will be"
+ " queued.*"
+ )
+
+ return playlist, embed
diff --git a/code/utils/source_helpers/apple/song.py b/code/utils/source_helpers/apple/song.py
new file mode 100644
index 0000000..4190b63
--- /dev/null
+++ b/code/utils/source_helpers/apple/song.py
@@ -0,0 +1,68 @@
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the song info from the Apple Music API
+ """
+ song_id = query.split("/album/")[1].split("?i=")[1]
+
+ try:
+ # Get the song info
+ response = requests.get(
+ f"https://api.music.apple.com/v1/catalog/us/songs/{song_id}",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Song Not Found",
+ description=(
+ "The song could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Apple Music API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the song info
+ song = response.json()
+ name = song["data"][0]["attributes"]["name"]
+ artist = song["data"][0]["attributes"]["artistName"]
+ except IndexError:
+ LOG.error("Failed unpacking Apple Music song info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Apple Music API")
+ return None, None
+
+ # Extract artwork URL, if available
+ artwork_url = (
+ song["data"][0]["attributes"].get("artwork", {}).get("url", None)
+ )
+ if artwork_url:
+ artwork_url = artwork_url.replace("{w}x{h}", "300x300")
+
+ embed = create_embed(
+ title="Song Queued",
+ description=f"**{name}** by **{artist}**\n\nQueued by {user.mention}",
+ thumbnail=artwork_url,
+ )
+
+ return song, embed
diff --git a/code/utils/source_helpers/parse.py b/code/utils/source_helpers/parse.py
new file mode 100644
index 0000000..b23a895
--- /dev/null
+++ b/code/utils/source_helpers/parse.py
@@ -0,0 +1,91 @@
+import discord
+
+from utils.source_helpers.apple import (
+ album as apple_album,
+ playlist as apple_playlist,
+ song as apple_song,
+)
+from utils.source_helpers.spotify import (
+ album as spotify_album,
+ artist as spotify_artist,
+ playlist as spotify_playlist,
+ song as spotify_song,
+)
+from utils.custom_sources import AppleSource, SpotifySource
+
+
+async def parse_custom_source(
+ self, provider: str, query: str, user: discord.User
+):
+ """
+ Parse the query and run the appropriate functions to get the results/info
+
+ Return the results and an embed or None, None
+ """
+ load_funcs = {
+ "apple": {
+ "album": apple_album.load,
+ "playlist": apple_playlist.load,
+ "song": apple_song.load,
+ },
+ "spotify": {
+ "album": spotify_album.load,
+ "artist": spotify_artist.load,
+ "playlist": spotify_playlist.load,
+ "song": spotify_song.load,
+ },
+ }
+
+ headers = {
+ "apple": self.bot.apple_headers,
+ "spotify": self.bot.spotify_headers,
+ }
+
+ sources = {
+ "apple": AppleSource,
+ "spotify": SpotifySource,
+ }
+ # Catch all songs
+ if "?i=" in query or "/track/" in query:
+ song, embed = await load_funcs[provider]["song"](
+ headers[provider], query, user
+ )
+
+ if song:
+ results = await sources[provider].load_item(self, user, song)
+ else:
+ return None, embed
+ # Catch all playlists
+ elif "/playlist/" in query:
+ playlist, embed = await load_funcs[provider]["playlist"](
+ headers[provider], query, user
+ )
+
+ if playlist:
+ results = await sources[provider].load_playlist(
+ self, user, playlist
+ )
+ else:
+ return None, embed
+ # Catch all albums
+ elif "/album/" in query:
+ album, embed = await load_funcs[provider]["album"](
+ headers[provider], query, user
+ )
+
+ if album:
+ results = await sources[provider].load_album(self, user, album)
+ else:
+ return None, embed
+ # Catch Spotify artists
+ elif "/artist/" in query:
+ artist, embed = await load_funcs[provider]["artist"](
+ headers[provider], query, user
+ )
+
+ if artist:
+ results = await sources[provider].load_artist(self, user, artist)
+ else:
+ return None, embed
+
+ return results, embed
diff --git a/code/utils/source_helpers/spotify/album.py b/code/utils/source_helpers/spotify/album.py
new file mode 100644
index 0000000..0ebc7d5
--- /dev/null
+++ b/code/utils/source_helpers/spotify/album.py
@@ -0,0 +1,68 @@
+import datetime
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the album info from the Spotify API
+ """
+ album_id = query.split("/album/")[1]
+
+ try:
+ # Get the album info
+ response = requests.get(
+ f"https://api.spotify.com/v1/albums/{album_id}",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Album Not Found",
+ description=(
+ "The album could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Spotify API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the album info
+ album = response.json()
+ name = album["name"]
+ artist = album["artists"][0]["name"]
+ num_tracks = len(album["tracks"]["items"])
+ artwork_url = album["images"][0]["url"]
+ except IndexError:
+ LOG.error("Failed unpacking Spotify album info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Spotify API")
+ return None, None
+
+ embed = create_embed(
+ title="Album Queued",
+ description=(
+ f"**{name}** by **{artist}**\n"
+ f"` {num_tracks} ` tracks\n\n"
+ f"Queued by: {user.mention}"
+ ),
+ thumbnail=artwork_url,
+ )
+
+ return album, embed
diff --git a/code/utils/source_helpers/spotify/artist.py b/code/utils/source_helpers/spotify/artist.py
new file mode 100644
index 0000000..995e208
--- /dev/null
+++ b/code/utils/source_helpers/spotify/artist.py
@@ -0,0 +1,77 @@
+import datetime
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the artists top tracks from the Spotify API
+ """
+ artist_id = query.split("/artist/")[1]
+
+ try:
+ # Get the artists songs
+ response = requests.get(
+ f"https://api.spotify.com/v1/artists/{artist_id}/top-tracks",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Artist Not Found",
+ description=(
+ "Either the provided link is malformed, the artist does"
+ " not exist, or the artist does not have any songs."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Spotify API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the artists songs
+ artist = response.json()
+ name = artist["tracks"][0]["artists"][0]["name"]
+ num_tracks = len(artist["tracks"])
+
+ # Get the artist info (for the thumbnail)
+ response = requests.get(
+ f"https://api.spotify.com/v1/artists/{artist_id}",
+ headers=headers,
+ )
+
+ response.raise_for_status()
+ try:
+ artwork_url = response.json()["images"][0]["url"]
+ except IndexError:
+ artwork_url = None
+
+ except IndexError:
+ LOG.error("Failed unpacking Spotify artist info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Spotify API")
+ return None, None
+
+ embed = create_embed(
+ title="Artist Queued",
+ description=(
+ f"Top `{num_tracks}` track by **{name}**\n\n"
+ f"Queued by {user.mention}"
+ ),
+ thumbnail=artwork_url,
+ )
+ return artist, embed
diff --git a/code/utils/source_helpers/spotify/playlist.py b/code/utils/source_helpers/spotify/playlist.py
new file mode 100644
index 0000000..7ca9c6a
--- /dev/null
+++ b/code/utils/source_helpers/spotify/playlist.py
@@ -0,0 +1,68 @@
+import datetime
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the playlist info from the Spotify API
+ """
+ playlist_id = query.split("/playlist/")[1]
+
+ try:
+ # Get the playlist info
+ response = requests.get(
+ f"https://api.spotify.com/v1/playlists/{playlist_id}",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Playlist Not Found",
+ description=(
+ "The playlist could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Spotify API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the playlist info
+ playlist = response.json()
+ name = playlist["name"]
+ owner = playlist["owner"]["display_name"]
+ num_tracks = len(playlist["tracks"]["items"])
+ artwork_url = playlist["images"][0]["url"]
+ except IndexError:
+ LOG.error("Failed unpacking Spotify playlist info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Spotify API")
+ return None, None
+
+ embed = create_embed(
+ title="Playlist Queued",
+ description=(
+ f"**{name}** from **{owner}**\n"
+ f"` {num_tracks} ` tracks\n\n"
+ f"Queued by {user.mention}"
+ ),
+ thumbnail=artwork_url,
+ )
+
+ return playlist, embed
diff --git a/code/utils/source_helpers/spotify/song.py b/code/utils/source_helpers/spotify/song.py
new file mode 100644
index 0000000..b0c7379
--- /dev/null
+++ b/code/utils/source_helpers/spotify/song.py
@@ -0,0 +1,63 @@
+import datetime
+import discord
+import requests
+from typing import Tuple, Optional
+from requests.exceptions import JSONDecodeError
+
+from utils.config import create_embed, LOG
+
+
+async def load(
+ headers: dict,
+ query: str,
+ user: discord.User,
+) -> Tuple[Optional[dict], Optional[discord.Embed]]:
+ """
+ Get the song info from the Spotify API
+ """
+ song_id = query.split("/track/")[1]
+
+ try:
+ # Get the song info
+ response = requests.get(
+ f"https://api.spotify.com/v1/tracks/{song_id}",
+ headers=headers,
+ )
+
+ if response.status_code == 404:
+ embed = create_embed(
+ title="Song Not Found",
+ description=(
+ "The song could not be found as the provided link is"
+ " invalid. Please try again."
+ ),
+ )
+ return None, embed
+
+ if response.status_code == 401:
+ LOG.error(
+ "Could not authorize with Spotify API. Likely need to"
+ " restart the bot."
+ )
+ return None, None
+
+ response.raise_for_status()
+ # Unpack the song info
+ song = response.json()
+ name = song["name"]
+ artist = song["artists"][0]["name"]
+ artwork_url = song["album"]["images"][0]["url"]
+ except IndexError:
+ LOG.error("Failed unpacking Spotify song info")
+ return None, None
+ except (JSONDecodeError, requests.HTTPError):
+ LOG.error("Failed making request to Spotify API")
+ return None, None
+
+ embed = create_embed(
+ title="Song Queued",
+ description=f"**{name}** by **{artist}**\n\nQueued by {user.mention}",
+ thumbnail=artwork_url,
+ )
+
+ return song, embed