Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

2 changed files with 112 additions and 378 deletions

490
bot.py
View File

@ -1,24 +1,25 @@
# Imports # Imports
import os import os
from discord.webhook.async_ import interaction_message_response_params
import requests import requests
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord import DiscordException, NotFound, app_commands, guild, user, voice_client from discord import DiscordException, NotFound, app_commands
from dotenv import load_dotenv from dotenv import load_dotenv
# ----- Setups ----- #
# Environment vars # Environment vars
load_dotenv() load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN") TOKEN = os.getenv("DISCORD_TOKEN")
JELLYFIN_URL = os.getenv("JELLYFIN_URL") JELLYFIN_URL = os.getenv("JELLYFIN_URL")
JELLYFIN_API_KEY = os.getenv("JELLYFIN_API_KEY") JELLYFIN_API_KEY = os.getenv("JELLYFIN_API_KEY")
# Constants # Constants
QUERY_LIMIT = 10 PLAY_QUERY_LIMIT = 1
SEARCH_QUERY_LIMIT = 5
# Setting the opus path, since Linux can't read it
# Request headers # Request headers
headers= { headers= {
@ -33,7 +34,6 @@ intents.voice_states = True
# ----- Classes ----- # # ----- Classes ----- #
# TODO: turn this in to an import in a separate file # TODO: turn this in to an import in a separate file
class JellyfinBot(commands.Bot): class JellyfinBot(commands.Bot):
def __init__(self): def __init__(self):
@ -43,35 +43,22 @@ class JellyfinBot(commands.Bot):
await self.tree.sync() await self.tree.sync()
bot = JellyfinBot() bot = JellyfinBot()
# Events
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f"🪼 Logged in as {bot.user}") print(f"🪼 Logged in as {bot.user}")
# ----- Helpful Structures ----- #
guild_queue_dict = dict()
# ----- Helper Functions ----- # # ----- Helper Functions ----- #
# makeRequest: # makeRequest:
# makes a request to the jellyfin server. # makes a request to the jellyfin server.
# Returns an error string or an empty dict # Returns an error string or
# begin makeRequest ----- # begin makeRequest -----
async def make_request(title: str, interaction: discord.Interaction) -> dict: async def make_request(url: str, params: dict, interaction: discord.Interaction) -> dict:
# Default return value # Default return value
data = dict() data = dict()
url = f"{JELLYFIN_URL}/Items"
params = {
"searchTerm": title,
"Recursive": True,
"IncludeItemTypes": "Audio",
"Limit": QUERY_LIMIT
}
try: try:
# Makes the request # Makes the request
@ -88,405 +75,152 @@ async def make_request(title: str, interaction: discord.Interaction) -> dict:
return data return data
# end makeRequest ----- # end makeRequest -----
# channelPlay
# playTrack: # does logic for songs in a voice channel
# guild wide play function
async def playTrack(interaction: discord.Interaction):
voice_client = interaction.guild.voice_client
# First, we make a dictionary that has a list inside.
# Then, we get the name of the guild and put that in the dictionary
# then AFTER, we put the song inside the list that the guild points to
# on the play command, we add that song to the list
headers_str = f"-headers \"X-Emby-Token: {JELLYFIN_API_KEY}\""
try:
while len(guild_queue_dict[interaction.guild_id]) > 0:
# Get song information
song = guild_queue_dict[interaction.guild_id][0]
song_id = song[0]
song_title = song[1]
song_artist = song[2]
# Get voice client
voice_client = interaction.guild.voice_client
# Then fetch this song
headers_str = f"-headers \"X-Emby-Token: {JELLYFIN_API_KEY}\""
audio_stream_url = f"{JELLYFIN_URL}/Audio/{song_id}/stream?static=True"
source = discord.FFmpegOpusAudio(audio_stream_url,
before_options=f'{headers_str} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
options="-vn")
# Then play it on the voice client
voice_client.play(source, after=lambda e: print(f"Finished playing: {e}"))
await interaction.followup.send(f"Now playing: **{song_title}** by *{song_artist}*")
# Cheap wait
# TODO: Make a better way of waiting
while voice_client.is_playing() or voice_client.is_paused():
continue
# Dequeue the array
guild_queue_dict[interaction.guild_id].pop(0)
except discord.ClientException as e:
await interaction.followup.send("Unable to decode your song!")
print(f"Potential FFMPEG/OPUS decoding error: {e}")
return
# clearQueue
# clears the queue. useful for a variety of functions where it needs to
# interrupt the player
def clearQueue(interaction: discord.Interaction):
guild_queue_dict[interaction.guild_id].clear()
return
# ----- Commands ----- # # ----- Commands ----- #
# Connect # Play a song
# Connects to a voice channel
@bot.tree.command(name="connect", description="Connects to the current voice channel")
async def connect(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
try:
voice_status = await interaction.user.fetch_voice()
user_channel = voice_status.channel
voice_client = interaction.guild.voice_client
if voice_client is None:
voice_client = await user_channel.connect()
elif voice_client.channel != user_channel:
await voice_client.disconnect()
voice_client = await user_channel.connect()
except discord.errors.NotFound as e:
print(f"Error: {e}")
await interaction.followup.send("You are not in a voice channel!")
return
except discord.errors.Forbidden as e:
print(f"Error: {e}")
await interaction.followup.send(f"I am not allowed in that voice channel!")
return
await interaction.followup.send(f"Connected!")
return
# Play function
# Plays a song with the given title in wherever the user is
# Disconnect # start play -----
# Disconnects from the voice channel @bot.tree.command(name="play", description="Play a song from Jellyfin")
@bot.tree.command(name="disconnect", description="Disconnects from current voice channel") @app_commands.describe(title="Song title to play")
async def disconnect(interaction: discord.Interaction): async def play(interaction: discord.Interaction, title: str):
# Makes the reaction visible to everyone # Makes the reaction visible to everyone
await interaction.response.defer() await interaction.response.defer()
# Checks if not in a voice channel # Checks if not in a voice channel
if interaction.guild.voice_client == None: # Checks if in a voice channel
await interaction.followup.send("I am not in any voice channel!") try:
# A better way of checking status
voice_status = await interaction.user.fetch_voice()
user_channel = voice_status.channel
voice_client = interaction.guild.voice_client # Enables the voice feature for the bot
# If its not connected to a voice channel, then connect to the channel that the caller is in
if voice_client is None:
voice_client = await user_channel.connect()
# If the caller is in a channel that its not already in, move there
elif voice_client.channel != user_channel:
await voice_client.move_to(user_channel)
voice_client = await user_channel.connect()
#Pause the music that is currently playing
if voice_client.is_playing():
voice_client.stop()
except discord.errors.NotFound as e:
print(f"Error: {e}")
await interaction.followup.send("Nope! Not in a voice!")
return
except discord.errors.Forbidden as e:
print(f"Error: {e}")
await interaction.followup.send("Not allowed in here!")
return return
# disconnects with a helpful message
await interaction.guild.voice_client.disconnect()
await interaction.followup.send("Disconnected!")
return
url = f"{JELLYFIN_URL}/Items"
params = {
"searchTerm": title,
"Recursive": True,
"IncludeItemTypes": "Audio",
"Limit": PLAY_QUERY_LIMIT
}
# Insert # makes a request to the server, and returns the data it acquires
# Inserts a song in the player data = await make_request(url, params, interaction)
@bot.tree.command(name="insert", description="Insert a song into the queue") if not data:
async def insert(interaction: discord.Interaction, title: str): await interaction.followup.send(f"❌ No song found matching `{title}`.")
# Makes the reaction visible to everyone
await interaction.response.defer()
await interaction.followup.send(f"Inserted a song!")
return
# Play
# Play the song at the top of the queue
@bot.tree.command(name="play", description="Plays a song at the top of the queue")
async def play(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
if guild_queue_dict.get(interaction.guild_id) == None:
guild_queue_dict[interaction.guild_id] = []
# Some basic checks
try:
# Check if the bot is connected to a channel
if interaction.guild.voice_client.channel != interaction.user.voice.channel:
await interaction.followup.send(f"Not connected to your voice channel!")
return # After this point, we know that it is in a channel
# Check if the queue is empty
if len(guild_queue_dict[interaction.guild_id]) == 0:
await interaction.followup.send(f"Queue is empty!")
return # After this point, we know the queue has something in it
except discord.errors.NotFound:
await interaction.followup.send(f"You're not connected to a voice channel!")
return return
# Play all items in the queue # Finds the item, and displays information about the location
await playTrack(interaction) print(f"Found {len(data)} items matching `{title}`.")
return # Deconstructing the dict 'data'
song = data[0]
song_id = song.get('Id')
song_title = song.get('Name')
song_artist = song.get('AlbumArtist', ['Unknown Artist'])
# Stop
# Stops the currently playing song, and empties the queue
# TODO: Make clear modular
@bot.tree.command(name="stop", description="Stops the player and empties the queue")
async def stop(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
# Some basic checks
try: try:
# Check if the bot is connected to a channel #Setup for ffmpeg
if interaction.guild.voice_client.channel != interaction.user.voice.channel: audio_stream_url = f"{JELLYFIN_URL}/Audio/{song_id}/stream?static=True"
await interaction.followup.send(f"Not connected to your voice channel!") print(f"Stream URL: {audio_stream_url}")
return # After this point, we know that it is in a channel headers_str = f"-headers \"X-Emby-Token: {JELLYFIN_API_KEY}\""
# Check if the queue is empty # >>----- start PROBLEMS
if guild_queue_dict[interaction.guild_id].count() == 0:
await interaction.followup.send(f"Queue is empty!")
return # After this point, we know the queue has something in it
except:
await interaction.followup.send(f"You're not connected to a voice channel!")
#First, empty the queue, so that if some other user is playing, it ends its play # Here is the problematic library.
clearQueue(interaction) # It might just be looking for Opus and not finding it
source = discord.FFmpegOpusAudio(audio_stream_url,
before_options=f'{headers_str} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
options="-vn")
# Then, stop the player # end PROBLEMS -----<<
interaction.guild.voice_client.stop()
# Play the audio file
voice_client.play(source, after=lambda e: print(f"Finished playing: {e}"))
await interaction.followup.send(f"🎶 Now playing: **{song_title}** by *{song_artist}*")
# If the library does not exist, we fail it
except discord.ClientException as e:
await interaction.followup.send("Unable to decode your song!")
print(f"Potential FFMPEG/OPUS decoding error: {e}")
# end play -----
await interaction.followup.send(f"Stopped the player!")
return
# Search command # Search command
# searches for a song # searches for a song
# Search function
# start search ----- # start search -----
@bot.tree.command(name="search", description="Search for a song on Jellyfin") @bot.tree.command(name="search", description="Search for a song on Jellyfin")
@app_commands.describe(title="Song title to search for") @app_commands.describe(title="Song title to search for")
async def search(interaction: discord.Interaction, title: str): async def search(interaction: discord.Interaction, title: str):
# Makes the reaction visible to everyone
await interaction.response.defer() await interaction.response.defer()
# Builds the url
url = f"{JELLYFIN_URL}/Items"
params = {
"searchTerm": title,
"Recursive": True,
"IncludeItemTypes": "Audio",
"Limit": SEARCH_QUERY_LIMIT
}
# Get the data # Get the data
data = await make_request(title, interaction) data = await make_request(url, params, interaction)
if not data: if not data:
await interaction.followup.send(f"No song found matching `{title}`.") await interaction.followup.send(f"No song found matching `{title}`.")
return return
result_list = [] lines = []
for song in data: for song in data:
result_list.append(f"**{song.get("Name")}** by *{song.get("AlbumArtist", ["Unknown Artist"])}*") title = song.get("Name")
artist = song.get("AlbumArtist", ["Unknown Artist"])
await interaction.followup.send("\n".join(result_list)) lines.append(f"**{title}** by *{artist}*")
return await interaction.followup.send("\n".join(lines))
# end search -----
# Disconnect command!
# Skip # Disconnects from the voice channel
# Skips a song in the player @bot.tree.command(name="disconnect", description="Disconnects from current voice channel!")
@bot.tree.command(name="skip", description="Skip the currently playing song") async def disconnect(interaction: discord.Interaction):
async def skip(interaction: discord.Interaction): if interaction.guild.voice_client == None:
# Makes the reaction visible to everyone await interaction.followup.send("I am not in any voice channel!")
await interaction.response.defer()
# Some basic checks
try:
# Check if the bot is connected to a channel
if interaction.guild.voice_client.channel != interaction.user.voice.channel:
await interaction.followup.send(f"Not connected to your voice channel!")
return # After this point, we know that it is in a channel
# Check if the queue is empty
if guild_queue_dict[interaction.guild_id].count() == 0:
await interaction.followup.send(f"Queue is empty!")
return # After this point, we know the queue has something in it
except:
await interaction.followup.send(f"You're not connected to a voice channel!")
# Since someone else might be playing, we just stop so that the playTrack function
# gets the next track
# Stops the player to skip
interaction.guild.voice_client.stop()
# Epilogue
await interaction.followup.send(f"Skipped the track!")
return
# Pause
# Pauses the player
@bot.tree.command(name="pause", description="Pauses the currently playing song")
async def pause(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
interaction.guild.voice_client.pause()
await interaction.followup.send(f"Paused!")
return
# Resume
# Resumes the player
@bot.tree.command(name="resume", description="Resumes the currently playing song")
async def resume(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
# Some basic checks
try:
# Check if the bot is connected to a channel
if interaction.guild.voice_client.channel != interaction.user.voice.channel:
await interaction.followup.send(f"Not connected to your voice channel!")
return # After this point, we know that it is in a channel
if not interaction.guild.voice_client.is_playing():
await interaction.followup.send(f"The player has nothing to resume!")
return # After this point, we know that it has something to play
except:
await interaction.followup.send(f"You're not connected to a voice channel!")
return return
await interaction.guild.voice_client.disconnect()
# Ideal function
if interaction.guild.voice_client.is_paused():
interaction.guild.voice_client.resume()
await interaction.followup.send(f"Resumed!")
return
# Something that it cannot do!
await interaction.followup.send("Something unexpected happened, and I cannot resume!")
return
# Add
# Enqueue an item
@bot.tree.command(name="add", description="Queues a song into the list")
async def add(interaction: discord.Interaction, title: str):
# Makes the reaction visible to everyone
await interaction.response.defer()
# query the item
data = await make_request(title, interaction)
# Check if data is bogus
if not data:
await interaction.followup.send(f"Couldn't find a song matching {title}")
return
# Gets song information
query_song = data[0]
query_song_id = query_song.get('Id')
query_song_title = query_song.get('Name')
query_song_artist = query_song.get('AlbumArtist', ['Unknown Artist'])
print(f"Found: {query_song_id}, with the title {query_song_title} and artist {query_song_artist}")
# Add song information as a tuple
if guild_queue_dict.get(interaction.guild_id) == None:
guild_queue_dict[interaction.guild_id] = []
guild_queue_dict[interaction.guild_id].append((query_song_id, query_song_title, query_song_artist))
# Epilogue
# Sends informational message
await interaction.followup.send(f"Added **{query_song_title}** by *{query_song_artist}*!")
tracks_queued = len(guild_queue_dict[interaction.guild_id])
# Helpfully tells the user how many tracks until their track is played
if tracks_queued > 1:
await interaction.followup.send(f"Plays after **{tracks_queued}** more tracks.")
return
#Clear
#Clears the queue
@bot.tree.command(name="clear", description="Clears the queue")
async def clear(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
clearQueue(interaction)
await interaction.followup.send(f"Cleared the Queue!")
return
#Queue
#Allows the user to view the queue
@bot.tree.command(name="queue", description="See the queue")
async def queue(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
# Check if queue is uninitialized
if guild_queue_dict.get(interaction.guild_id) == None:
guild_queue_dict[interaction.guild_id] = []
# Add all the items in a queue to a list
if len(guild_queue_dict[interaction.guild_id]) == 0:
await interaction.followup.send(f"The queue is empty!")
return
# The queue has something in it
result_list = []
for song in guild_queue_dict[interaction.guild_id]:
result_list.append(f"**{song[1]}** by *{song[2]}*")
await interaction.followup.send("\n".join(result_list))
return
#Current
# Display currently playing
@bot.tree.command(name="current", description="See whats currently playing")
async def current(interaction: discord.Interaction):
# Makes the reaction visible to everyone
await interaction.response.defer()
#Display the current song
# Some song information
current = guild_queue_dict[interaction.guild_id][0]
current_title = current[1]
current_artist = current[2]
await interaction.followup.send(f"Currently Playing: **{current_title}** by *{current_artist}*")
# Cleanup
await interaction.followup.send(f"Emptied the queue!")
return

BIN
ffmpeg.exe Normal file

Binary file not shown.