From d53887dd1a6b71b29c7a55ee0453a37f9ab965a5 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 19 Jan 2025 23:19:16 -0500 Subject: [PATCH] Added /recommend which recommands songs based on users song history. Fixed it so that Amber should auto-join your voice channel when you /play as long as you're already in a voice channel. --- .gitignore | 1 + audio.py | 158 +++++++++++++++++++++++++++++++++++++++++++--------- commands.py | 23 +++++++- 3 files changed, 155 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 15201ac..b93aeac 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,4 @@ cython_debug/ # PyPI configuration file .pypirc +/user_data.json \ No newline at end of file diff --git a/audio.py b/audio.py index 720a0f7..f4ef658 100644 --- a/audio.py +++ b/audio.py @@ -1,26 +1,71 @@ import discord import yt_dlp import asyncio +import os +import json +import random +import requests +from dotenv import load_dotenv + +load_dotenv() voice_clients = {} # Track active voice connections music_queues = {} # Per-guild song queue current_tracks = {} # Currently playing tracks volumes = {} # Volume levels per guild -default_volume = 0.5 # Default volume (50%) +default_volume = 0.05 # Default volume (5%) +USER_DATA_FILE = 'user_data.json' +YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY") # Replace with your API key + + +# ---------- Utility Functions ---------- +def load_user_data(): + if not os.path.exists(USER_DATA_FILE): + return {} + with open(USER_DATA_FILE, 'r') as f: + return json.load(f) + + +def save_user_data(data): + with open(USER_DATA_FILE, 'w') as f: + json.dump(data, f, indent=4) + + +def update_user_history(user_id, song_title, artist): + user_data = load_user_data() + if str(user_id) not in user_data: + user_data[str(user_id)] = {"history": []} + + # Add the song to the user's history + user_data[str(user_id)]["history"].append({"title": song_title, "artist": artist}) + save_user_data(user_data) + + +# ---------- Playback Functions ---------- +async def join_voice(interaction: discord.Interaction): + if interaction.user.voice is None or interaction.user.voice.channel is None: + await interaction.response.send_message("You need to be in a voice channel for me to join.") + return None + + channel = interaction.user.voice.channel + guild_id = interaction.guild.id + + if guild_id not in voice_clients: + voice_clients[guild_id] = await channel.connect() + await interaction.followup.send(f"✅ **Joined {channel.name}.**") + return voice_clients[guild_id] async def play_audio(interaction: discord.Interaction, query: str): guild_id = interaction.guild.id - if guild_id not in voice_clients: - await interaction.response.send_message("Amber is not connected to a voice channel.") - return - - voice_client = voice_clients[guild_id] - - if not voice_client.is_connected(): - await interaction.response.send_message("Amber is not in a voice channel.") - return + # Ensure Amber is connected to a voice channel + if guild_id not in voice_clients or not voice_clients[guild_id].is_connected(): + voice_client = await join_voice(interaction) + if voice_client is None: # If join failed + return + else: + voice_client = voice_clients[guild_id] await interaction.response.defer() @@ -38,6 +83,10 @@ async def play_audio(interaction: discord.Interaction, query: str): song_url = info['url'] title = info.get('title', 'Unknown Title') + artist = info.get('uploader', 'Unknown Artist') + + # Log the song in the user's history + update_user_history(interaction.user.id, title, artist) # Add the song to the queue if guild_id not in music_queues: @@ -100,6 +149,28 @@ async def stop_audio(interaction: discord.Interaction): await interaction.response.send_message("❌ **No music is playing.**") +async def pause_audio(interaction: discord.Interaction): + """Pauses the currently playing song.""" + guild_id = interaction.guild.id + + if guild_id in voice_clients and voice_clients[guild_id].is_playing(): + voice_clients[guild_id].pause() # Pause playback + await interaction.response.send_message("⏸️ **Music paused.**") + else: + await interaction.response.send_message("❌ **No music is currently playing.**") + + +async def resume_audio(interaction: discord.Interaction): + """Resumes playback of a paused song.""" + guild_id = interaction.guild.id + + if guild_id in voice_clients and voice_clients[guild_id].is_paused(): + voice_clients[guild_id].resume() # Resume playback + await interaction.response.send_message("▶️ **Music resumed.**") + else: + await interaction.response.send_message("❌ **No paused music to resume.**") + + async def set_volume(interaction: discord.Interaction, level: int): guild_id = interaction.guild.id @@ -120,22 +191,6 @@ async def set_volume(interaction: discord.Interaction, level: int): await interaction.response.send_message(f"🔊 **Volume set to {level}%**.") -async def join_voice(interaction: discord.Interaction): - if interaction.user.voice is None or interaction.user.voice.channel is None: - await interaction.response.send_message("You need to be in a voice channel for me to join.") - return - - channel = interaction.user.voice.channel - guild_id = interaction.guild.id - - # Connect to the voice channel - if guild_id not in voice_clients: - voice_clients[guild_id] = await channel.connect() - await interaction.response.send_message(f"✅ **Joined {channel.name}.**") - else: - await interaction.response.send_message("❌ **I am already in a voice channel.**") - - async def leave_voice(interaction: discord.Interaction): guild_id = interaction.guild.id @@ -149,3 +204,54 @@ async def leave_voice(interaction: discord.Interaction): await interaction.response.send_message("❌ **I am not connected to any voice channel.**") else: await interaction.response.send_message("❌ **I am not connected to any voice channel.**") + + +# ---------- Recommendations ---------- +def fetch_related_songs(song_title, artist): + """ + Fetch related songs using the YouTube Data API v3. + Returns a list of recommendations in the format: + [ "Title - URL" ] + """ + query = f"{song_title} {artist} related songs" + url = "https://www.googleapis.com/youtube/v3/search" + params = { + "q": query, + "key": YOUTUBE_API_KEY, + "type": "video", + "part": "snippet", + "maxResults": 5 + } + + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + return [ + f'{item["snippet"]["title"]} - https://www.youtube.com/watch?v={item["id"]["videoId"]}' + for item in data.get("items", []) + ] + else: + print(f"Error fetching related songs: {response.status_code} {response.text}") + return [] + + +def generate_recommendations(user_id): + """ + Generate recommendations using the last song the user listened to. + """ + user_data = load_user_data() + user_history = user_data.get(str(user_id), {}).get("history", []) + + if not user_history: + return ["No recommendations yet. Start playing some songs!"] + + # Use the last played song to fetch recommendations + last_song = user_history[-1] + title = last_song["title"] + artist = last_song["artist"] + recommendations = fetch_related_songs(title, artist) + + if recommendations: + return recommendations + else: + return ["No recommendations could be fetched at this time."] diff --git a/commands.py b/commands.py index 0ec8a04..549b7c2 100644 --- a/commands.py +++ b/commands.py @@ -1,6 +1,6 @@ import discord from discord import app_commands -from audio import play_audio, stop_audio, set_volume, join_voice, leave_voice +from audio import play_audio, stop_audio, set_volume, join_voice, leave_voice, pause_audio, resume_audio, generate_recommendations async def setup_commands(client, guild_id=None): @@ -20,8 +20,29 @@ async def setup_commands(client, guild_id=None): async def stop(interaction: discord.Interaction): await stop_audio(interaction) + @client.tree.command(name="pause", description="Pause the current song.") + async def pause(interaction: discord.Interaction): + await pause_audio(interaction) + + @client.tree.command(name="resume", description="Resume the paused song.") + async def resume(interaction: discord.Interaction): + await resume_audio(interaction) + + @client.tree.command(name="volume", description="Set playback volume.") async def volume(interaction: discord.Interaction, level: int): await set_volume(interaction, level) + @client.tree.command(name="recommend", description="Get song recommendations based on your listening history.") + async def recommend(interaction: discord.Interaction): + user_id = interaction.user.id + recommendations = generate_recommendations(user_id) + + embed = discord.Embed( + title="🎵 Recommended Songs", + description="\n".join(recommendations), + color=discord.Color.green() + ) + await interaction.response.send_message(embed=embed) + await client.tree.sync()