Skip to content

Commit

Permalink
Suggested songs with last.fm API, minor IMDb and ChatGPT fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
stekc committed Oct 9, 2024
1 parent f00e831 commit bc6175b
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ TOKEN=YOUR_BOT_TOKEN_HERE
QUICKVIDS_TOKEN=YOUR_QUICKVIDS_TOKEN_HERE
TOPGG_TOKEN=YOUR_TOPGG_TOKEN_HERE
OPENAI_TOKEN=
LASTFM_TOKEN=
HEALTHCHECKS_URL=
2 changes: 1 addition & 1 deletion cogs/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async def chatgpt(
}
)

completion = self.openai.chat.completions.create(
completion = await self.openai.chat.completions.create(
model=model,
messages=prompt,
)
Expand Down
14 changes: 6 additions & 8 deletions cogs/movies.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ async def check_enabled(self, site: str, config, guild_id: int = None):
return False
return True

async def process_movie_data(self, movie, context=None):
"""Helper function to process movie data and create embed and buttons."""
async def process_movie_data(self, movie, context=None, is_imdb_link=False):
mtitle = movie.get("Title", "Unknown Title")
if year := movie.get("Year"):
mtitle += f" ({year})"
Expand All @@ -53,9 +52,9 @@ async def process_movie_data(self, movie, context=None):
mid = movie.get("ImdbId")

recommendations = movie.get("Recommendations", [])
recommended = "\n".join(
recommended = "\n\n".join(
[
f"[{rec['Title']}](https://www.themoviedb.org/movie/{rec['TmdbId']})"
f"<:movie:1293632067313078383> **[{rec['Title']}](https://www.themoviedb.org/movie/{rec['TmdbId']})**"
for rec in recommendations
]
)
Expand Down Expand Up @@ -91,7 +90,7 @@ async def callback(self, interaction: discord.Interaction):
)

view = View()
if mid:
if mid and not is_imdb_link:
view.add_item(
discord.ui.Button(
style=discord.ButtonStyle.link,
Expand Down Expand Up @@ -154,7 +153,7 @@ async def on_message(self, message: discord.Message):
return

movie = movie_data[0]
embed, view = await self.process_movie_data(movie)
embed, view = await self.process_movie_data(movie, is_imdb_link=True)

await message.reply(embed=embed, view=view)
await self.config_cog.increment_link_fix_count("imdb")
Expand All @@ -169,7 +168,6 @@ async def on_message(self, message: discord.Message):
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
async def imdb(self, context: Context, *, query: str):
"""Search for a movie on IMDb"""
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.radarr.video/v1/search?q={quote_plus(query)}&year="
Expand All @@ -180,7 +178,7 @@ async def imdb(self, context: Context, *, query: str):
return await context.send("No results found.")

movie = movie_data[0]
await self.process_movie_data(movie, context)
await self.process_movie_data(movie, context, is_imdb_link=False)


async def setup(bot):
Expand Down
189 changes: 177 additions & 12 deletions cogs/songs.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json
import os
import re
from contextlib import suppress
from urllib.parse import quote_plus

import aiohttp
import discord
from aiocache import cached
from discord import app_commands
from discord.ext import commands

from utils.colorthief import get_color
Expand All @@ -21,6 +23,46 @@
}


class SuggestedSongsButton(discord.ui.Button):
def __init__(self, cog, artist, title, color):
super().__init__(
style=discord.ButtonStyle.secondary,
emoji="🔥",
)
self.cog = cog
self.artist = artist
self.title = title
self.color = color

async def callback(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
embed = discord.Embed(
color=self.color,
description="<a:discordloading:1199066225381228546> Fetching suggested songs...",
)
msg = await interaction.followup.send(embed=embed, ephemeral=True)
suggested_songs = await self.cog.suggested_songs(self.artist, self.title)
if suggested_songs:
suggested_songs_str = "\n\n".join(
[
f"**<:icons_music:1293362305886589010> {song['artist']} - {song['name']}**\n<:website:1290793095734100008> [[Apple Music]]({song['apple_music']}) [[Spotify]]({song['spotify']}) [[YouTube]]({song['youtube']})"
for song in suggested_songs
]
)
embed = discord.Embed(
title="Suggested Songs",
description=suggested_songs_str,
color=self.color,
)
await msg.edit(embed=embed)
else:
await msg.edit(
embed=discord.Embed(
description="No suggested songs found.", color=self.color
)
)


class Songs(commands.Cog, name="songs"):
def __init__(self, bot):
self.bot = bot
Expand All @@ -39,6 +81,7 @@ def __init__(self, bot):
r"music\.apple\.com\/[a-zA-Z]{2}\/album\/[a-zA-Z\d%\(\)-]+\/[\d]{1,10}\?i=[\d]{1,15}|"
r"music\.youtube\.com\/watch\?v=[A-Za-z0-9_-]{11})"
)
self.thumbnail = None

async def check_enabled(self, site: str, config, guild_id: int = None):
if guild_id is None:
Expand All @@ -49,6 +92,31 @@ async def check_enabled(self, site: str, config, guild_id: int = None):
return False
return True

@cached(ttl=86400)
async def suggested_songs(self, artist: str, track: str):
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://ws.audioscrobbler.com/2.0/?method=track.getsimilar&artist={quote_plus(artist)}&track={quote_plus(track)}&api_key={os.getenv('LASTFM_TOKEN')}&format=json"
) as resp:
if resp.status != 200:
return None
res = await resp.json()
suggested_songs = []
for track in res["similartracks"]["track"][:5]:
spotify_url = await self.lastfm_to_spotify(track["url"])
if spotify_url:
links = await self.get_song_links(spotify_url)
suggested_songs.append(
{
"name": track["name"],
"artist": track["artist"]["name"],
"spotify": links.get("spotify") + "?autoplay=0",
"apple_music": links.get("appleMusic"),
"youtube": links.get("youtube"),
}
)
return suggested_songs

@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if not message.guild:
Expand All @@ -71,6 +139,7 @@ async def on_message(self, message: discord.Message):
spotify_link = await self.lastfm_to_spotify(lastfm_link)
if spotify_link:
await self.generate_view(message, spotify_link)
await self.config_cog.increment_link_fix_count("songs")
return
if match := self.pattern.search(message.content.strip("<>")):
link = match.group(0)
Expand All @@ -95,38 +164,61 @@ async def lastfm_to_spotify(self, link: str):
else:
return None

@cached(ttl=86400)
async def get_song_links(self, url: str):
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.song.link/v1-alpha.1/links?url={url}"
) as resp:
if resp.status != 200:
return None
res = await resp.json()

links = {}
for platform in ["spotify", "appleMusic", "youtube"]:
if platform_data := res.get("linksByPlatform", {}).get(platform):
links[platform] = platform_data.get("url")

return links

async def generate_view(self, message: discord.Message, link: str):
links = await self.get_song_links(link)
if not links:
return None

async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.song.link/v1-alpha.1/links?url={link}"
) as resp:
if resp.status != 200:
return None
res = await resp.json()

res = await resp.text()
res = json.loads(res)

spotify_data = res.get("linksByPlatform").get("spotify")
spotify_data = res.get("linksByPlatform", {}).get("spotify")
unique_id = (
spotify_data.get("entityUniqueId")
if spotify_data is not None
else res.get("entityUniqueId")
)
data = res.get("entitiesByUniqueId").get(unique_id)
data = res.get("entitiesByUniqueId", {}).get(unique_id, {})
artist = data.get("artistName")
title = data.get("title")
thumbnail = data.get("thumbnailUrl")

if not all([artist, title, thumbnail]):
return

color = await get_color(thumbnail)
view = discord.ui.View()
original_platform = None
has_spotify_or_apple = False
has_youtube = False

view.add_item(SuggestedSongsButton(self, artist, title, color))

for platform, body in platforms.items():
if (platform_links := res.get("linksByPlatform").get(platform)) is not None:
platform_url = platform_links.get("url")
if platform in links:
platform_url = links[platform]
if platform_url in link:
original_platform = platform
if platform in ["spotify", "appleMusic"]:
Expand All @@ -149,7 +241,7 @@ async def generate_view(self, message: discord.Message, link: str):
has_youtube and has_spotify_or_apple
)
if should_reply:
embed = discord.Embed(color=await get_color(thumbnail))
embed = discord.Embed(color=color)
embed.set_author(name=f"{artist} - {title}", icon_url=thumbnail)

if message.channel.permissions_for(message.guild.me).send_messages:
Expand All @@ -164,10 +256,83 @@ async def generate_view(self, message: discord.Message, link: str):
else:
await message.reply(view=view, mention_author=False)

if not message.author.bot and not message.author.id == 356268235697553409:
if self.suppress_embed_pattern.search(link):
with suppress(discord.errors.Forbidden, discord.errors.NotFound):
await message.edit(suppress=True)
if not message.author.bot and not message.author.id == 356268235697553409:
if self.suppress_embed_pattern.search(link):
with suppress(discord.errors.Forbidden, discord.errors.NotFound):
await message.edit(suppress=True)

@app_commands.command(name="song", description="Generate a fixed embed for a song.")
@app_commands.describe(url="The URL of the song.")
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
async def song_command(self, interaction: discord.Interaction, url: str):
await interaction.response.defer()

if not self.pattern.match(url):
await interaction.followup.send(
"Invalid song URL. Please provide a valid Spotify, Apple Music, or YouTube link.",
ephemeral=True,
)
return

links = await self.get_song_links(url)
if not links:
await interaction.followup.send(
"Unable to fetch song information.", ephemeral=True
)
return

async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.song.link/v1-alpha.1/links?url={url}"
) as resp:
if resp.status != 200:
await interaction.followup.send(
"Unable to fetch song information.", ephemeral=True
)
return
res = await resp.json()

spotify_data = res.get("linksByPlatform", {}).get("spotify")
unique_id = (
spotify_data.get("entityUniqueId")
if spotify_data is not None
else res.get("entityUniqueId")
)
data = res.get("entitiesByUniqueId", {}).get(unique_id, {})
artist = data.get("artistName")
title = data.get("title")
self.thumbnail = data.get("thumbnailUrl")

if not all([artist, title, self.thumbnail]):
await interaction.followup.send(
"Unable to fetch complete song information.", ephemeral=True
)
return

color = await get_color(self.thumbnail)
embed = discord.Embed(color=color)
embed.set_author(name=f"{artist} - {title}", icon_url=self.thumbnail)

view = discord.ui.View()
view.add_item(SuggestedSongsButton(self, artist, title, color))

for platform, body in platforms.items():
if platform in links:
platform_url = links[platform]
view.add_item(
discord.ui.Button(
style=discord.ButtonStyle.link,
emoji=body["emote"],
url=(
platform_url + "?autoplay=0"
if platform.lower() == "spotify"
else platform_url
),
)
)

await interaction.followup.send(embed=embed, view=view)


async def setup(bot):
Expand Down

0 comments on commit bc6175b

Please sign in to comment.