diff options
| author | alex <[email protected]> | 2026-07-13 16:44:51 +0200 |
|---|---|---|
| committer | alex <[email protected]> | 2026-07-13 16:44:51 +0200 |
| commit | 1b430cb695dd541f80223d743f378135064f4fec (patch) | |
| tree | 7426e91a25530379372c3d1570819180fbaebe32 /main.py | |
| download | spotify-into-discord-1b430cb695dd541f80223d743f378135064f4fec.tar.xz spotify-into-discord-1b430cb695dd541f80223d743f378135064f4fec.zip | |
Diffstat (limited to 'main.py')
| -rw-r--r-- | main.py | 188 |
1 files changed, 188 insertions, 0 deletions
@@ -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) + + +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.") + + +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() + + + +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 <paste_your_broken_url>`" + ) + 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.") + + +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!") +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?") + + +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.") + + + +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() + + +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) +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.") + |
