From 1b430cb695dd541f80223d743f378135064f4fec Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Jul 2026 16:44:51 +0200 Subject: init --- main.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 main.py (limited to 'main.py') diff --git a/main.py b/main.py new file mode 100644 index 0000000..62ae849 --- /dev/null +++ b/main.py @@ -0,0 +1,188 @@ +import discord +from discord.ext import commands +import asyncio +import os +import re +import subprocess +from dotenv import load_dotenv + +load_dotenv() + +TOKEN = os.getenv("DISCORD_TOKEN") +PIPE_PATH = os.getenv("PIPE_PATH", "/opt/spotify-bot/spotify_pipe") + +ENV_DIR = "/etc/librespot" + +intents = discord.Intents.default() +intents.message_content = True +intents.voice_states = True +bot = commands.Bot(command_prefix="!", intents=intents) + + +@bot.event +async def on_ready(): + print(f"Bot is online as {bot.user}") + if not os.path.exists(ENV_DIR): + print(f"Warning: {ENV_DIR} does not exist. Please create it manually.") + + +@bot.event +async def on_voice_state_update(member, before, after): + vc = member.guild.voice_client + if vc and vc.channel: + human_members = [m for m in vc.channel.members if not m.bot] + if len(human_members) == 0: + print(f"No humans left in {vc.channel.name}. Disconnecting...") + await vc.disconnect() + + + +@bot.command() +async def setup(ctx): + user_id = str(ctx.author.id) + await ctx.send("⏳ **Spinning up your private server speaker...**") + + subprocess.run(["sudo", "systemctl", "restart", f"librespot@{user_id}"]) + subprocess.run(["sudo", "systemctl", "enable", f"librespot@{user_id}"]) + + await asyncio.sleep(2) + + log_output = subprocess.run( + ["journalctl", "-u", f"librespot@{user_id}", "-n", "20", "--no-pager"], + capture_output=True, text=True + ).stdout + + matches = re.findall(r"https://accounts\.spotify\.com/[^\s]+", log_output) + + if matches: + oauth_url = matches[-1] + + instructions = ( + f"πŸ”— **Step 1:** Click this link to log in and authorize your account:\n<{oauth_url}>\n\n" + f"❌ **Step 2:** After you click 'Agree', your browser will show a broken page saying **'Unable to connect'** or **'Firefox can’t connect to the server'**.\n" + f"**THIS IS NORMAL!** \n\n" + f"πŸ“‹ **Step 3:** Look at your browser's top address bar, **copy the entire broken URL** (it starts with `http://127.0.0.1:5588...`), and reply to me here like this:\n" + f"`!auth `" + ) + await ctx.send(instructions) + else: + await ctx.send( + "❌ Hrm, I couldn't extract the authorization link. Ask the server owner to check the system logs.") + + +@bot.command() +async def auth(ctx, url: str): + # Safety check: Make sure they actually gave us the redirect URL + if "127.0.0.1:5588" not in url and "localhost:5588" not in url: + await ctx.send( + "❌ That doesn't look like the correct redirect link. Make sure you copy the URL from the broken page's address bar!") + return + + await ctx.send("πŸ”„ **Forwarding authentication token to your speaker instance...**") + + local_url = url.replace("localhost", "127.0.0.1") + + result = subprocess.run(["curl", "-s", local_url], capture_output=True, text=True) + + await ctx.send("πŸŽ‰ **Success! Your speaker is authenticated.** Check your Spotify app's 'Devices' menuβ€”your server speaker will appear shortly!") +@bot.command() +async def speaker_on(ctx): + discord_id = str(ctx.author.id) + try: + subprocess.run(["sudo", "systemctl", "start", f"librespot@{discord_id}"], check=True) + await ctx.send("πŸ”Š Your personal speaker is now **Online**.") + except subprocess.CalledProcessError: + await ctx.send("❌ Failed to start. Have you used `!setup` yet?") + + +@bot.command() +async def speaker_off(ctx): + discord_id = str(ctx.author.id) + try: + subprocess.run(["sudo", "systemctl", "stop", f"librespot@{discord_id}"], check=True) + await ctx.send("πŸ”‡ Your personal speaker is now **Offline**.") + except subprocess.CalledProcessError: + await ctx.send("❌ Failed to stop the speaker.") + + + +@bot.command() +async def join(ctx): + if ctx.author.voice: + await ctx.author.voice.channel.connect() + await ctx.send(f"Joined {ctx.author.voice.channel.name}") + else: + await ctx.send("You need to be in a voice channel first!") + + +if not hasattr(bot, "active_pipes"): + bot.active_pipes = {} +if not hasattr(bot, "active_loops"): + bot.active_loops = set() + + +@bot.command() +async def play(ctx): + vc = ctx.voice_client + if not vc: + return await ctx.send("Use !join first!") + + discord_id = str(ctx.author.id) + user_pipe = f"/opt/spotify-bot/pipes/{discord_id}_pipe" + + # Make sure their pipe actually exists + if not os.path.exists(user_pipe): + return await ctx.send("❌ I can't find your audio pipe! Have you run `!setup` or `!speaker_on`?") + + guild_id = ctx.guild.id + + bot.active_pipes[guild_id] = user_pipe + + if vc.is_playing(): + await ctx.send(f"πŸ”„ Swapping stream to {ctx.author.display_name}'s Spotify...") + vc.stop() + return + + if guild_id not in bot.active_loops: + bot.active_loops.add(guild_id) + await ctx.send(f"πŸ”Š Hooking into {ctx.author.display_name}'s Spotify stream...") + + ffmpeg_options = { + 'before_options': '-f s16le -ar 44100 -ac 2', + 'options': '-loglevel warning' + } + + try: + while vc.is_connected(): + if not vc.is_playing(): + # ALWAYS fetch the latest pipe assigned to this server + current_pipe = bot.active_pipes.get(guild_id) + + if current_pipe and os.path.exists(current_pipe): + try: + audio_source = discord.FFmpegPCMAudio(current_pipe, **ffmpeg_options) + transformed_source = discord.PCMVolumeTransformer(audio_source, volume=1.0) + vc.play(transformed_source) + print(f"Successfully connected voice client to current target pipe: {current_pipe}") + except discord.ClientException: + pass + except Exception as e: + print(f"Pipeline sync error: {e}") + + await asyncio.sleep(1) + finally: + bot.active_loops.discard(guild_id) + bot.active_pipes.pop(guild_id, None) +@bot.command() +async def stop(ctx): + vc = ctx.voice_client + if vc: + await vc.disconnect() + await ctx.send("Disconnected.") + + +if TOKEN: + bot.run(TOKEN) +else: + print("Error: DISCORD_TOKEN not found in environment variables or .env file.") + -- cgit v1.2.3