# Imports import os import requests import discord from discord.ext import commands from discord import DiscordException, NotFound, app_commands from dotenv import load_dotenv # Environment vars load_dotenv() TOKEN = os.getenv("DISCORD_TOKEN") JELLYFIN_URL = os.getenv("JELLYFIN_URL") JELLYFIN_API_KEY = os.getenv("JELLYFIN_API_KEY") # Constants PLAY_QUERY_LIMIT = 1 SEARCH_QUERY_LIMIT = 5 # Setting the opus path, since Linux can't read it # Request headers headers= { "X-Emby-Token": JELLYFIN_API_KEY } # discord bot setups intents = discord.Intents.default() intents.message_content = True intents.voice_states = True # ----- Classes ----- # # TODO: turn this in to an import in a separate file class JellyfinBot(commands.Bot): def __init__(self): super().__init__(command_prefix="/", intents=intents) async def setup_hook(self): await self.tree.sync() bot = JellyfinBot() @bot.event async def on_ready(): print(f"🪼 Logged in as {bot.user}") # ----- Helper Functions ----- # # makeRequest: # makes a request to the jellyfin server. # Returns an error string or # begin makeRequest ----- async def make_request(url: str, params: dict, interaction: discord.Interaction) -> dict: # Default return value data = dict() try: # Makes the request response = requests.get(url, headers=headers, params=params) response.raise_for_status() # Presets the default value for data if this doesn't work data = response.json().get("Items", []) return data except requests.RequestException as e: await interaction.followup.send("⚠️ Failed to contact Jellyfin server.") print(f"Error: {e}") return data # end makeRequest ----- # channelPlay # does logic for songs in a voice channel # ----- Commands ----- # # Play a song # Play function # Plays a song with the given title in wherever the user is # start play ----- @bot.tree.command(name="play", description="Play a song from Jellyfin") @app_commands.describe(title="Song title to play") async def play(interaction: discord.Interaction, title: str): # Makes the reaction visible to everyone await interaction.response.defer() # Checks if not in a voice channel # Checks if in a 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 url = f"{JELLYFIN_URL}/Items" params = { "searchTerm": title, "Recursive": True, "IncludeItemTypes": "Audio", "Limit": PLAY_QUERY_LIMIT } # makes a request to the server, and returns the data it acquires data = await make_request(url, params, interaction) if not data: await interaction.followup.send(f"❌ No song found matching `{title}`.") return # Finds the item, and displays information about the location print(f"Found {len(data)} items matching `{title}`.") # Deconstructing the dict 'data' song = data[0] song_id = song.get('Id') song_title = song.get('Name') song_artist = song.get('AlbumArtist', ['Unknown Artist']) try: #Setup for ffmpeg audio_stream_url = f"{JELLYFIN_URL}/Audio/{song_id}/stream?static=True" print(f"Stream URL: {audio_stream_url}") headers_str = f"-headers \"X-Emby-Token: {JELLYFIN_API_KEY}\"" # >>----- start PROBLEMS # Here is the problematic library. # 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") # end PROBLEMS -----<< # 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 ----- # Search command # searches for a song # Search function # start search ----- @bot.tree.command(name="search", description="Search for a song on Jellyfin") @app_commands.describe(title="Song title to search for") async def search(interaction: discord.Interaction, title: str): 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 data = await make_request(url, params, interaction) if not data: await interaction.followup.send(f"No song found matching `{title}`.") return lines = [] for song in data: title = song.get("Name") artist = song.get("AlbumArtist", ["Unknown Artist"]) lines.append(f"**{title}** by *{artist}*") await interaction.followup.send("\n".join(lines)) # end search ----- # Disconnect command! # Disconnects from the voice channel @bot.tree.command(name="disconnect", description="Disconnects from current voice channel!") async def disconnect(interaction: discord.Interaction): if interaction.guild.voice_client == None: await interaction.followup.send("I am not in any voice channel!") return await interaction.guild.voice_client.disconnect() # ----- FINALLY ----- # bot.run(TOKEN)