2025-01-18 21:45:25 -05:00
|
|
|
import discord
|
|
|
|
import yt_dlp
|
2025-01-19 22:29:16 -05:00
|
|
|
import asyncio
|
2025-01-19 23:19:16 -05:00
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import requests
|
2025-01-22 22:36:19 -05:00
|
|
|
import time
|
2025-01-19 23:19:16 -05:00
|
|
|
from dotenv import load_dotenv
|
2025-01-27 17:34:47 -05:00
|
|
|
from typing import List, Dict, Any
|
2025-01-19 23:19:16 -05:00
|
|
|
|
|
|
|
load_dotenv()
|
2025-01-18 21:45:25 -05:00
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
voice_clients: Dict[int, discord.VoiceClient] = {}
|
|
|
|
music_queues: Dict[int, List[Any]] = {}
|
|
|
|
current_tracks: Dict[int, Any] = {}
|
|
|
|
volumes: Dict[int, float] = {}
|
|
|
|
repeat_modes: Dict[int, str] = {}
|
|
|
|
now_playing_messages: Dict[int, int] = {}
|
|
|
|
progress_tasks: Dict[int, asyncio.Task] = {}
|
|
|
|
_247_mode: Dict[int, bool] = {}
|
|
|
|
|
|
|
|
default_volume: float = 0.05
|
|
|
|
USER_DATA_FILE: str = 'user_data.json'
|
|
|
|
YOUTUBE_API_KEY: str | None = os.getenv("YOUTUBE_API_KEY")
|
2025-01-18 21:45:25 -05:00
|
|
|
|
|
|
|
|
2025-01-19 23:19:16 -05:00
|
|
|
# ---------- Utility Functions ----------
|
2025-01-27 17:34:47 -05:00
|
|
|
def load_user_data() -> Dict[str, Any]:
|
2025-01-19 23:19:16 -05:00
|
|
|
if not os.path.exists(USER_DATA_FILE):
|
|
|
|
return {}
|
|
|
|
with open(USER_DATA_FILE, 'r') as f:
|
|
|
|
return json.load(f)
|
|
|
|
|
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
def save_user_data(data: Dict[str, Any]) -> None:
|
2025-01-19 23:19:16 -05:00
|
|
|
with open(USER_DATA_FILE, 'w') as f:
|
|
|
|
json.dump(data, f, indent=4)
|
|
|
|
|
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
def update_user_history(user_id: int, song_title: str, artist: str) -> None:
|
2025-01-19 23:19:16 -05:00
|
|
|
user_data = load_user_data()
|
|
|
|
if str(user_id) not in user_data:
|
|
|
|
user_data[str(user_id)] = {"history": []}
|
|
|
|
|
|
|
|
user_data[str(user_id)]["history"].append({"title": song_title, "artist": artist})
|
|
|
|
save_user_data(user_data)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Playback Functions ----------
|
2025-01-27 17:34:47 -05:00
|
|
|
async def join_voice(interaction: discord.Interaction) -> bool:
|
2025-01-18 21:45:25 -05:00
|
|
|
guild_id = interaction.guild.id
|
2025-01-22 22:36:19 -05:00
|
|
|
channel = interaction.user.voice.channel
|
2025-01-19 22:29:16 -05:00
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
if guild_id in voice_clients and voice_clients[guild_id].channel == channel:
|
|
|
|
return True
|
2025-01-20 10:35:28 -05:00
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
try:
|
|
|
|
voice_client = await channel.connect()
|
|
|
|
voice_clients[guild_id] = voice_client
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
|
|
await interaction.followup.send(f"❌ **Error:** Could not join the voice channel. {str(e)}")
|
|
|
|
return False
|
2025-01-18 21:45:25 -05:00
|
|
|
|
2025-01-19 22:29:16 -05:00
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
async def play_audio(interaction: discord.Interaction, query: str) -> None:
|
2025-01-19 23:19:16 -05:00
|
|
|
guild_id = interaction.guild.id
|
|
|
|
|
2025-01-22 20:20:11 -05:00
|
|
|
if not interaction.response.is_done():
|
|
|
|
await interaction.response.defer()
|
|
|
|
|
2025-01-19 23:19:16 -05:00
|
|
|
if guild_id not in voice_clients or not voice_clients[guild_id].is_connected():
|
2025-01-22 22:36:19 -05:00
|
|
|
joined = await join_voice(interaction)
|
|
|
|
if not joined:
|
|
|
|
await interaction.followup.send("❌ **Error:** Could not join the voice channel.")
|
2025-01-19 23:19:16 -05:00
|
|
|
return
|
2025-01-18 21:45:25 -05:00
|
|
|
|
2025-01-22 22:50:06 -05:00
|
|
|
asyncio.create_task(fetch_and_queue_song(interaction, query))
|
|
|
|
|
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
async def fetch_and_queue_song(interaction: discord.Interaction, query: str) -> None:
|
2025-01-22 22:50:06 -05:00
|
|
|
guild_id = interaction.guild.id
|
2025-01-18 21:45:25 -05:00
|
|
|
ydl_opts = {
|
|
|
|
'format': 'bestaudio/best',
|
|
|
|
'quiet': True,
|
|
|
|
}
|
2025-01-22 22:36:19 -05:00
|
|
|
try:
|
|
|
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
2025-01-18 21:45:25 -05:00
|
|
|
info = ydl.extract_info(f"ytsearch:{query}", download=False)['entries'][0]
|
2025-01-22 22:36:19 -05:00
|
|
|
except Exception as e:
|
|
|
|
await interaction.followup.send(f"❌ **Error:** Could not find or play the requested audio. {str(e)}")
|
|
|
|
return
|
2025-01-18 21:45:25 -05:00
|
|
|
|
2025-01-19 22:29:16 -05:00
|
|
|
song_url = info['url']
|
2025-01-18 21:45:25 -05:00
|
|
|
title = info.get('title', 'Unknown Title')
|
2025-01-19 23:19:16 -05:00
|
|
|
artist = info.get('uploader', 'Unknown Artist')
|
2025-01-22 22:50:06 -05:00
|
|
|
duration = info.get('duration', 0)
|
2025-01-19 23:19:16 -05:00
|
|
|
|
|
|
|
update_user_history(interaction.user.id, title, artist)
|
2025-01-18 21:45:25 -05:00
|
|
|
|
2025-01-19 22:29:16 -05:00
|
|
|
if guild_id not in music_queues:
|
|
|
|
music_queues[guild_id] = []
|
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
music_queues[guild_id].append((song_url, title, duration))
|
2025-01-22 22:50:06 -05:00
|
|
|
await interaction.followup.send(f"✅ **Added to Queue:** {title}")
|
2025-01-19 22:29:16 -05:00
|
|
|
|
2025-01-22 22:50:06 -05:00
|
|
|
if not voice_clients[guild_id].is_playing():
|
2025-01-19 22:29:16 -05:00
|
|
|
await play_next(interaction)
|
|
|
|
|
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
async def play_next(interaction: discord.Interaction) -> None:
|
2025-01-19 22:29:16 -05:00
|
|
|
guild_id = interaction.guild.id
|
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
if guild_id not in music_queues or not music_queues[guild_id]:
|
2025-01-27 17:34:47 -05:00
|
|
|
if _247_mode.get(guild_id, True):
|
|
|
|
if guild_id in now_playing_messages and now_playing_messages[guild_id]:
|
|
|
|
try:
|
|
|
|
message = await interaction.channel.fetch_message(now_playing_messages[guild_id])
|
|
|
|
embed = discord.Embed(
|
|
|
|
title="🎵 Queue Empty",
|
|
|
|
description="Amber is in 24/7 mode. Add more songs to the queue to resume playback.",
|
|
|
|
color=discord.Color.orange()
|
|
|
|
)
|
|
|
|
await message.edit(embed=embed, view=None)
|
|
|
|
except discord.NotFound:
|
|
|
|
pass
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
if guild_id in voice_clients and voice_clients[guild_id].is_connected():
|
|
|
|
await voice_clients[guild_id].disconnect()
|
|
|
|
del voice_clients[guild_id]
|
|
|
|
return
|
2025-01-19 22:29:16 -05:00
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
song_url, title, duration = music_queues[guild_id].pop(0)
|
2025-01-20 10:35:28 -05:00
|
|
|
current_tracks[guild_id] = (song_url, title)
|
2025-01-19 22:29:16 -05:00
|
|
|
|
2025-01-18 21:45:25 -05:00
|
|
|
ffmpeg_options = {
|
2025-01-19 22:29:16 -05:00
|
|
|
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
|
2025-01-18 21:45:25 -05:00
|
|
|
'options': '-vn',
|
|
|
|
}
|
2025-01-19 22:29:16 -05:00
|
|
|
|
|
|
|
try:
|
2025-01-22 20:34:07 -05:00
|
|
|
voice_client = voice_clients[guild_id]
|
2025-01-19 22:29:16 -05:00
|
|
|
source = discord.FFmpegPCMAudio(song_url, **ffmpeg_options)
|
|
|
|
volume = volumes.get(guild_id, default_volume)
|
|
|
|
source = discord.PCMVolumeTransformer(source, volume=volume)
|
|
|
|
|
|
|
|
voice_client.play(
|
|
|
|
source,
|
|
|
|
after=lambda e: asyncio.run_coroutine_threadsafe(
|
|
|
|
play_next(interaction), interaction.client.loop
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2025-01-22 22:36:19 -05:00
|
|
|
embed = discord.Embed(
|
|
|
|
title="🎵 Now Playing",
|
|
|
|
description=f"**{title}**\n\n`00:00 / {time.strftime('%M:%S', time.gmtime(duration))}`",
|
|
|
|
color=discord.Color.green()
|
|
|
|
)
|
2025-01-27 17:34:47 -05:00
|
|
|
embed.set_thumbnail(url=f"https://img.youtube.com/vi/{song_url.split('?v=')[-1]}/0.jpg")
|
2025-01-23 10:07:40 -05:00
|
|
|
embed.add_field(name="Artist", value=artist, inline=True)
|
|
|
|
embed.set_footer(text="React with 👍 or 👎 to rate this song!")
|
2025-01-22 22:50:06 -05:00
|
|
|
view = PlaybackControls(interaction, guild_id)
|
2025-01-22 22:36:19 -05:00
|
|
|
|
|
|
|
if guild_id in now_playing_messages and now_playing_messages[guild_id]:
|
|
|
|
try:
|
|
|
|
message = await interaction.channel.fetch_message(now_playing_messages[guild_id])
|
2025-01-22 22:50:06 -05:00
|
|
|
await message.edit(embed=embed, view=view)
|
2025-01-22 22:36:19 -05:00
|
|
|
except discord.NotFound:
|
2025-01-22 22:50:06 -05:00
|
|
|
message = await interaction.followup.send(embed=embed, view=view)
|
2025-01-22 22:36:19 -05:00
|
|
|
now_playing_messages[guild_id] = message.id
|
|
|
|
else:
|
2025-01-22 22:50:06 -05:00
|
|
|
message = await interaction.followup.send(embed=embed, view=view)
|
2025-01-22 22:36:19 -05:00
|
|
|
now_playing_messages[guild_id] = message.id
|
|
|
|
|
|
|
|
start_time = time.time()
|
2025-01-23 10:07:40 -05:00
|
|
|
asyncio.create_task(update_progress_bar(interaction, duration, start_time))
|
2025-01-19 22:29:16 -05:00
|
|
|
except Exception as e:
|
2025-01-22 22:36:19 -05:00
|
|
|
await interaction.followup.send(f"❌ **Error:** Could not play the next song. {str(e)}")
|
2025-01-19 22:29:16 -05:00
|
|
|
|
|
|
|
|
2025-01-27 17:34:47 -05:00
|
|
|
async def update_progress_bar(interaction: discord.Interaction, duration: int, start_time: float) -> None:
|
2025-01-22 22:50:06 -05:00
|
|
|
guild_id = interaction.guild.id
|
|
|
|
|
|
|
|
try:
|
|
|
|
while guild_id in voice_clients and voice_clients[guild_id].is_playing():
|
|
|
|
elapsed_time = time.time() - start_time
|
|
|
|
progress = min(elapsed_time / duration, 1.0)
|
|
|
|
progress_blocks = int(progress * 20)
|
|
|
|
progress_bar = "▬" * progress_blocks + "🔘" + "▬" * (20 - progress_blocks)
|
|
|
|
elapsed_time_str = time.strftime("%M:%S", time.gmtime(elapsed_time))
|
|
|
|
duration_str = time.strftime("%M:%S", time.gmtime(duration))
|
|
|
|
|
|
|
|
embed = discord.Embed(
|
|
|
|
title="🎵 Now Playing",
|
|
|
|
description=f"**{current_tracks[guild_id][1]}**\n\n{progress_bar} `{elapsed_time_str} / {duration_str}`",
|
|
|
|
color=discord.Color.green()
|
|
|
|
)
|
|
|
|
|
|
|
|
if guild_id in now_playing_messages and now_playing_messages[guild_id]:
|
|
|
|
message_id = now_playing_messages[guild_id]
|
|
|
|
try:
|
|
|
|
message = await interaction.channel.fetch_message(message_id)
|
|
|
|
await message.edit(embed=embed)
|
|
|
|
except discord.NotFound:
|
|
|
|
break
|
2025-01-23 10:07:40 -05:00
|
|
|
await asyncio.sleep(1)
|
2025-01-22 22:50:06 -05:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|