initial bot
This commit is contained in:
		
						commit
						2c7a5dff6c
					
				
					 7 changed files with 747 additions and 0 deletions
				
			
		
							
								
								
									
										23
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					# Binaries for programs and plugins
 | 
				
			||||||
 | 
					*.exe
 | 
				
			||||||
 | 
					*.exe~
 | 
				
			||||||
 | 
					*.dll
 | 
				
			||||||
 | 
					*.so
 | 
				
			||||||
 | 
					*.dylib
 | 
				
			||||||
 | 
					discord-europa-fm
 | 
				
			||||||
 | 
					europafm-bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Environment files
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Test binary, built with `go test -c`
 | 
				
			||||||
 | 
					*.test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output of the go coverage tool, specifically when used with LiteIDE
 | 
				
			||||||
 | 
					*.out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Dependency directories (remove the comment below to include it)
 | 
				
			||||||
 | 
					# vendor/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Go workspace file
 | 
				
			||||||
 | 
					go.work
 | 
				
			||||||
							
								
								
									
										45
									
								
								Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Dockerfile
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					FROM golang:1.22-alpine AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install build dependencies
 | 
				
			||||||
 | 
					RUN apk add --no-cache git
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set working directory
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy go mod and sum files
 | 
				
			||||||
 | 
					COPY go.mod go.sum ./
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Download dependencies
 | 
				
			||||||
 | 
					RUN go mod download
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy source code
 | 
				
			||||||
 | 
					COPY *.go ./
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Build the application
 | 
				
			||||||
 | 
					RUN CGO_ENABLED=0 GOOS=linux go build -o discord-europa-fm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Use a smaller base image for the final stage
 | 
				
			||||||
 | 
					FROM alpine:latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install ffmpeg
 | 
				
			||||||
 | 
					RUN apk add --no-cache ffmpeg ca-certificates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy the built executable
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					COPY --from=builder /app/discord-europa-fm .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Environment variables
 | 
				
			||||||
 | 
					ENV DISCORD_TOKEN="" \
 | 
				
			||||||
 | 
					    DEBUG="" \
 | 
				
			||||||
 | 
					    GUILD_ID=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run the bot
 | 
				
			||||||
 | 
					CMD if [ -n "$DEBUG" ] && [ -n "$GUILD_ID" ]; then \
 | 
				
			||||||
 | 
					      ./discord-europa-fm -debug -guild "$GUILD_ID"; \
 | 
				
			||||||
 | 
					    elif [ -n "$DEBUG" ]; then \
 | 
				
			||||||
 | 
					      ./discord-europa-fm -debug; \
 | 
				
			||||||
 | 
					    elif [ -n "$GUILD_ID" ]; then \
 | 
				
			||||||
 | 
					      ./discord-europa-fm -guild "$GUILD_ID"; \
 | 
				
			||||||
 | 
					    else \
 | 
				
			||||||
 | 
					      ./discord-europa-fm; \
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
							
								
								
									
										139
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,139 @@
 | 
				
			||||||
 | 
					# Discord EuropaFM Radio Bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A Discord bot that streams EuropaFM Spanish radio to your voice channel.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Go 1.22 or higher
 | 
				
			||||||
 | 
					- FFmpeg installed on your system
 | 
				
			||||||
 | 
					- Discord Bot Token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Install FFmpeg on your system:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - Ubuntu/Debian: `sudo apt install ffmpeg`
 | 
				
			||||||
 | 
					   - macOS: `brew install ffmpeg`
 | 
				
			||||||
 | 
					   - Windows: Download from [FFmpeg website](https://ffmpeg.org/download.html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Create a Discord Bot:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - Go to [Discord Developer Portal](https://discord.com/developers/applications)
 | 
				
			||||||
 | 
					   - Create a new application
 | 
				
			||||||
 | 
					   - Go to the "Bot" tab and click "Add Bot"
 | 
				
			||||||
 | 
					   - Under "Privileged Gateway Intents", enable "Server Members Intent"
 | 
				
			||||||
 | 
					   - Copy your bot token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Invite the bot to your server:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - Go to OAuth2 > URL Generator
 | 
				
			||||||
 | 
					   - Select "bot" and "applications.commands" scopes
 | 
				
			||||||
 | 
					   - Select permissions: "Connect", "Speak", "Send Messages"
 | 
				
			||||||
 | 
					   - Open the generated URL and authorize the bot for your server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. Get your Server ID (for guild-specific command registration):
 | 
				
			||||||
 | 
					   - Enable Developer Mode in Discord (User Settings > Advanced > Developer Mode)
 | 
				
			||||||
 | 
					   - Right-click on your server name in the server list and select "Copy ID"
 | 
				
			||||||
 | 
					   - Alternatively, you can see the server ID in the bot's debug logs when it connects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Running from source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Clone this repository
 | 
				
			||||||
 | 
					2. Run the bot with your Discord token:
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   DISCORD_TOKEN=your_token_here go run main.go
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   Or
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   go run main.go -token your_token_here
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Command-line options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The bot accepts the following command-line options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `-token string`: Discord bot token (can also be set via DISCORD_TOKEN environment variable)
 | 
				
			||||||
 | 
					- `-debug`: Enable debug logging for more verbose output (helpful for troubleshooting)
 | 
				
			||||||
 | 
					- `-guild string`: Register commands for a specific guild (server) instead of globally - this is faster for testing (commands update instantly)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Example with debug mode and guild-specific registration:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					go run main.go -token your_token_here -debug -guild your_server_id
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Using Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Build the Docker image:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   docker build -t discord-europa-fm .
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Run the container:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   docker run -e DISCORD_TOKEN=your_token_here discord-europa-fm
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   With debug mode:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   docker run -e DISCORD_TOKEN=your_token_here -e DEBUG=1 discord-europa-fm
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   With guild-specific command registration:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   docker run -e DISCORD_TOKEN=your_token_here -e GUILD_ID=your_server_id discord-europa-fm
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Alternatively, use docker-compose:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Create a `.env` file with your Discord token:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   DISCORD_TOKEN=your_token_here
 | 
				
			||||||
 | 
					   DEBUG=1
 | 
				
			||||||
 | 
					   GUILD_ID=your_server_id  # Optional: for guild-specific command registration
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Run with docker-compose:
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					   docker-compose up -d
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Once the bot is running, you can use the following slash commands in your Discord server:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `/europafm play` - Start streaming EuropaFM radio in your voice channel
 | 
				
			||||||
 | 
					- `/europafm stop` - Stop streaming and leave the voice channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Troubleshooting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you're having audio issues:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Make sure FFmpeg is properly installed and available in your system PATH
 | 
				
			||||||
 | 
					2. Verify that the bot has permissions to join and speak in voice channels
 | 
				
			||||||
 | 
					3. Check that you're in a voice channel before using the play command
 | 
				
			||||||
 | 
					4. Make sure the radio stream URL is still valid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Unknown Integration Error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you see "Unknown Integration" when using slash commands, it might be because:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. The bot is offline or not properly connected to Discord
 | 
				
			||||||
 | 
					2. The slash commands haven't been properly registered with Discord
 | 
				
			||||||
 | 
					3. The bot may need permissions it doesn't currently have
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Solutions to try:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Run the bot with `-debug` flag to get more detailed logs about what's happening
 | 
				
			||||||
 | 
					2. Use guild-specific command registration with `-guild your_server_id` for faster updates (global commands can take up to an hour to propagate)
 | 
				
			||||||
 | 
					3. Check if your bot has the "applications.commands" scope when you invited it to your server
 | 
				
			||||||
 | 
					4. Try re-inviting the bot with the correct permissions and scopes
 | 
				
			||||||
 | 
					5. Verify that your bot token is correct and not expired
 | 
				
			||||||
 | 
					6. Check the Discord Developer Portal to make sure your application has the bot feature enabled
 | 
				
			||||||
							
								
								
									
										12
									
								
								docker-compose.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								docker-compose.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					version: "3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  bot:
 | 
				
			||||||
 | 
					    build: .
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - DISCORD_TOKEN=${DISCORD_TOKEN}
 | 
				
			||||||
 | 
					      - DEBUG=${DEBUG:-}
 | 
				
			||||||
 | 
					      - GUILD_ID=${GUILD_ID:-}
 | 
				
			||||||
 | 
					    restart: unless-stopped
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - /etc/localtime:/etc/localtime:ro # Sync container time with host
 | 
				
			||||||
							
								
								
									
										18
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					module github.com/fmartingr/discord-europa-fm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.23.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					toolchain go1.24.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/bwmarrin/dgvoice v0.0.0-20210225172318-caaac756e02e
 | 
				
			||||||
 | 
						github.com/bwmarrin/discordgo v0.28.1
 | 
				
			||||||
 | 
						github.com/lmittmann/tint v1.0.7
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/gorilla/websocket v1.4.2 // indirect
 | 
				
			||||||
 | 
						golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
 | 
				
			||||||
 | 
						layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										18
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					github.com/bwmarrin/dgvoice v0.0.0-20210225172318-caaac756e02e h1:IdfGDWLNL/ZAHda33oiKnkoaWWsQ6vyO+MDslrcJ43M=
 | 
				
			||||||
 | 
					github.com/bwmarrin/dgvoice v0.0.0-20210225172318-caaac756e02e/go.mod h1:DT3heoMAQGrOExZ3Rb3TBOQ4Bm+wD4H48KFnt1YfLoQ=
 | 
				
			||||||
 | 
					github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
 | 
				
			||||||
 | 
					github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
 | 
				
			||||||
 | 
					github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 | 
				
			||||||
 | 
					github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 | 
				
			||||||
 | 
					github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
 | 
				
			||||||
 | 
					github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32 h1:/S1gOotFo2sADAIdSGk1sDq1VxetoCWr6f5nxOG0dpY=
 | 
				
			||||||
 | 
					layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32/go.mod h1:yDtyzWZDFCVnva8NGtg38eH2Ns4J0D/6hD+MMeUGdF0=
 | 
				
			||||||
							
								
								
									
										492
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										492
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,492 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"flag"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/bwmarrin/dgvoice"
 | 
				
			||||||
 | 
						"github.com/bwmarrin/discordgo"
 | 
				
			||||||
 | 
						"github.com/lmittmann/tint"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						token          string
 | 
				
			||||||
 | 
						radioURL       = "https://atres-live.europafm.com/live/europafm/bitrate_1.m3u8"
 | 
				
			||||||
 | 
						backupRadioURL = "https://playerservices.streamtheworld.com/api/livestream-redirect/EUROPA_FM.mp3"
 | 
				
			||||||
 | 
						logger         *slog.Logger
 | 
				
			||||||
 | 
						debug          bool
 | 
				
			||||||
 | 
						guildID        string // For testing in a specific guild
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						flag.StringVar(&token, "token", "", "Discord Bot Token")
 | 
				
			||||||
 | 
						flag.BoolVar(&debug, "debug", false, "Enable debug logging")
 | 
				
			||||||
 | 
						flag.StringVar(&guildID, "guild", "", "Specific guild ID for command registration (leave empty for global commands)")
 | 
				
			||||||
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize logger with tint
 | 
				
			||||||
 | 
						var logLevel slog.Level
 | 
				
			||||||
 | 
						if debug {
 | 
				
			||||||
 | 
							logLevel = slog.LevelDebug
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logLevel = slog.LevelInfo
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger = slog.New(tint.NewHandler(os.Stderr, &tint.Options{
 | 
				
			||||||
 | 
							Level:      logLevel,
 | 
				
			||||||
 | 
							TimeFormat: time.RFC3339,
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
						slog.SetDefault(logger)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						logger.Info("Starting EuropaFM Discord bot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if token == "" {
 | 
				
			||||||
 | 
							token = os.Getenv("DISCORD_TOKEN")
 | 
				
			||||||
 | 
							if token == "" {
 | 
				
			||||||
 | 
								logger.Error("No token provided. Please provide it via -token flag or DISCORD_TOKEN environment variable")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check for guild-specific deployment from environment
 | 
				
			||||||
 | 
						if guildID == "" {
 | 
				
			||||||
 | 
							guildID = os.Getenv("GUILD_ID")
 | 
				
			||||||
 | 
							if guildID != "" {
 | 
				
			||||||
 | 
								logger.Info("Using guild-specific deployment from environment variable", "guild_id", guildID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a new Discord session using the provided bot token
 | 
				
			||||||
 | 
						dg, err := discordgo.New("Bot " + token)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error creating Discord session", "error", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enable debug logging for DiscordGo if debug flag is set
 | 
				
			||||||
 | 
						if debug {
 | 
				
			||||||
 | 
							dg.LogLevel = discordgo.LogDebug
 | 
				
			||||||
 | 
							dg.Debug = true
 | 
				
			||||||
 | 
							logger.Debug("DiscordGo debug logging enabled")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Register command handlers
 | 
				
			||||||
 | 
						dg.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
 | 
				
			||||||
 | 
							logInteraction(s, i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if i.Type != discordgo.InteractionApplicationCommand {
 | 
				
			||||||
 | 
								logger.Debug("Ignoring non-application command interaction",
 | 
				
			||||||
 | 
									"type", i.Type,
 | 
				
			||||||
 | 
									"guild_id", i.GuildID)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cmdName := i.ApplicationCommandData().Name
 | 
				
			||||||
 | 
							logger.Info("Received command interaction",
 | 
				
			||||||
 | 
								"command", cmdName,
 | 
				
			||||||
 | 
								"user", i.Member.User.Username,
 | 
				
			||||||
 | 
								"guild_id", i.GuildID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch cmdName {
 | 
				
			||||||
 | 
							case "europafm":
 | 
				
			||||||
 | 
								handleEuropaFMCommand(s, i)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								logger.Warn("Unknown command received", "command", cmdName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add handlers for checking interactions and commands
 | 
				
			||||||
 | 
						dg.AddHandler(logRawInteraction)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add a handler for connection events
 | 
				
			||||||
 | 
						dg.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
 | 
				
			||||||
 | 
							logger.Info("Bot is ready",
 | 
				
			||||||
 | 
								"username", r.User.Username,
 | 
				
			||||||
 | 
								"discriminator", r.User.Discriminator,
 | 
				
			||||||
 | 
								"id", r.User.ID,
 | 
				
			||||||
 | 
								"session_id", r.SessionID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Log guilds the bot is connected to
 | 
				
			||||||
 | 
							logger.Info("Connected to guilds", "count", len(r.Guilds))
 | 
				
			||||||
 | 
							for i, guild := range r.Guilds {
 | 
				
			||||||
 | 
								if i < 10 { // Limit to 10 guilds in logs
 | 
				
			||||||
 | 
									logger.Debug("Connected to guild", "name", guild.Name, "id", guild.ID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set necessary intents
 | 
				
			||||||
 | 
						dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates | discordgo.IntentsGuildMessages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open a websocket connection to Discord
 | 
				
			||||||
 | 
						logger.Info("Opening connection to Discord")
 | 
				
			||||||
 | 
						err = dg.Open()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error opening connection", "error", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get application info to verify bot identity
 | 
				
			||||||
 | 
						app, err := dg.Application("@me")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error getting application info", "error", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Bot application info",
 | 
				
			||||||
 | 
								"id", app.ID,
 | 
				
			||||||
 | 
								"name", app.Name,
 | 
				
			||||||
 | 
								"public", app.BotPublic,
 | 
				
			||||||
 | 
								"require_code_grant", app.BotRequireCodeGrant)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Register slash commands
 | 
				
			||||||
 | 
						targetID := "" // For global commands
 | 
				
			||||||
 | 
						if guildID != "" {
 | 
				
			||||||
 | 
							// For guild-specific commands (faster updates during testing)
 | 
				
			||||||
 | 
							targetID = guildID
 | 
				
			||||||
 | 
							logger.Info("Registering commands for specific guild", "guild_id", guildID)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Registering global commands (may take up to an hour to propagate)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check existing commands
 | 
				
			||||||
 | 
						existingCmds, err := dg.ApplicationCommands(dg.State.User.ID, targetID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error fetching existing commands", "error", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Found existing commands that will be deleted", "count", len(existingCmds))
 | 
				
			||||||
 | 
							for _, cmd := range existingCmds {
 | 
				
			||||||
 | 
								logger.Debug("Found existing command",
 | 
				
			||||||
 | 
									"name", cmd.Name,
 | 
				
			||||||
 | 
									"id", cmd.ID,
 | 
				
			||||||
 | 
									"description", cmd.Description)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Delete existing command
 | 
				
			||||||
 | 
								logger.Info("Deleting existing command", "name", cmd.Name, "id", cmd.ID)
 | 
				
			||||||
 | 
								err := dg.ApplicationCommandDelete(dg.State.User.ID, targetID, cmd.ID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logger.Error("Error deleting command", "name", cmd.Name, "id", cmd.ID, "error", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Wait a bit for Discord to process the deletions
 | 
				
			||||||
 | 
							logger.Info("Waiting for command deletions to process...")
 | 
				
			||||||
 | 
							time.Sleep(2 * time.Second)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Define the command
 | 
				
			||||||
 | 
						cmd := &discordgo.ApplicationCommand{
 | 
				
			||||||
 | 
							Name:        "europafm",
 | 
				
			||||||
 | 
							Description: "Control EuropaFM radio streaming",
 | 
				
			||||||
 | 
							Options: []*discordgo.ApplicationCommandOption{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Type:        discordgo.ApplicationCommandOptionSubCommand,
 | 
				
			||||||
 | 
									Name:        "play",
 | 
				
			||||||
 | 
									Description: "Start streaming EuropaFM radio in your voice channel",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Type:        discordgo.ApplicationCommandOptionSubCommand,
 | 
				
			||||||
 | 
									Name:        "stop",
 | 
				
			||||||
 | 
									Description: "Stop streaming and leave the voice channel",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Register the command
 | 
				
			||||||
 | 
						registeredCmd, err := dg.ApplicationCommandCreate(dg.State.User.ID, targetID, cmd)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error creating slash command",
 | 
				
			||||||
 | 
								"error", err,
 | 
				
			||||||
 | 
								"command", cmd.Name,
 | 
				
			||||||
 | 
								"details", err.Error())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Slash command registered successfully",
 | 
				
			||||||
 | 
								"command", registeredCmd.Name,
 | 
				
			||||||
 | 
								"id", registeredCmd.ID,
 | 
				
			||||||
 | 
								"target_id", targetID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify registered commands
 | 
				
			||||||
 | 
						time.Sleep(1 * time.Second) // Give Discord API a moment to catch up
 | 
				
			||||||
 | 
						verifyCommands, err := dg.ApplicationCommands(dg.State.User.ID, targetID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error fetching commands for verification", "error", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Verified registered commands", "count", len(verifyCommands))
 | 
				
			||||||
 | 
							for _, cmd := range verifyCommands {
 | 
				
			||||||
 | 
								logger.Debug("Verified command", "name", cmd.Name, "id", cmd.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Wait for the signal to terminate
 | 
				
			||||||
 | 
						logger.Info("Bot is now running. Press CTRL-C to exit.")
 | 
				
			||||||
 | 
						sc := make(chan os.Signal, 1)
 | 
				
			||||||
 | 
						signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
 | 
				
			||||||
 | 
						<-sc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Cleanup before exit
 | 
				
			||||||
 | 
						logger.Info("Shutting down...")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Disconnect from all voice channels
 | 
				
			||||||
 | 
						for _, vc := range dg.VoiceConnections {
 | 
				
			||||||
 | 
							vc.Disconnect()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Close the Discord session
 | 
				
			||||||
 | 
						dg.Close()
 | 
				
			||||||
 | 
						logger.Info("Shutdown complete")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Log handler for raw interaction events
 | 
				
			||||||
 | 
					func logRawInteraction(s *discordgo.Session, e *discordgo.Event) {
 | 
				
			||||||
 | 
						if !debug {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.HasPrefix(e.Type, "INTERACTION") {
 | 
				
			||||||
 | 
							logger.Debug("Raw interaction event received",
 | 
				
			||||||
 | 
								"type", e.Type,
 | 
				
			||||||
 | 
								"operation", e.Operation)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Detailed logging for interactions
 | 
				
			||||||
 | 
					func logInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
 | 
				
			||||||
 | 
						if !debug {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.Debug("Interaction details",
 | 
				
			||||||
 | 
							"id", i.ID,
 | 
				
			||||||
 | 
							"application_id", i.AppID,
 | 
				
			||||||
 | 
							"type", i.Type,
 | 
				
			||||||
 | 
							"token", i.Token[:5]+"...", // Log only first few chars of token for security
 | 
				
			||||||
 | 
							"guild_id", i.GuildID,
 | 
				
			||||||
 | 
							"channel_id", i.ChannelID,
 | 
				
			||||||
 | 
							"member", i.Member != nil,
 | 
				
			||||||
 | 
							"user", i.User != nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleEuropaFMCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						options := i.ApplicationCommandData().Options
 | 
				
			||||||
 | 
						if len(options) == 0 {
 | 
				
			||||||
 | 
							logger.Warn("Received europafm command with no subcommand", "interaction_id", i.ID)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						subCommand := options[0].Name
 | 
				
			||||||
 | 
						logger := logger.With("subcommand", subCommand, "user", i.Member.User.Username, "guild_id", i.GuildID)
 | 
				
			||||||
 | 
						logger.Info("Processing command")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Respond to the interaction
 | 
				
			||||||
 | 
						err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
 | 
				
			||||||
 | 
							Type: discordgo.InteractionResponseChannelMessageWithSource,
 | 
				
			||||||
 | 
							Data: &discordgo.InteractionResponseData{
 | 
				
			||||||
 | 
								Content: "Processing your request...",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error responding to interaction", "error", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch subCommand {
 | 
				
			||||||
 | 
						case "play":
 | 
				
			||||||
 | 
							// Find the guild for that channel
 | 
				
			||||||
 | 
							guildID := i.GuildID
 | 
				
			||||||
 | 
							logger := logger.With("guild_id", guildID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Find the voice channel the user is in
 | 
				
			||||||
 | 
							vs, err := findUserVoiceState(ctx, s, guildID, i.Member.User.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Warn("User not in voice channel", "error", err)
 | 
				
			||||||
 | 
								followupMsg(s, i, "You need to be in a voice channel to use this command.")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get channel details
 | 
				
			||||||
 | 
							channel, err := s.Channel(vs.ChannelID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Warn("Failed to get channel details", "channel_id", vs.ChannelID, "error", err)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								logger = logger.With("channel_name", channel.Name, "channel_id", channel.ID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Connect to the voice channel
 | 
				
			||||||
 | 
							logger.Info("Joining voice channel")
 | 
				
			||||||
 | 
							vc, err := s.ChannelVoiceJoin(guildID, vs.ChannelID, false, true)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Error("Error joining voice channel", "error", err)
 | 
				
			||||||
 | 
								followupMsg(s, i, "Error joining voice channel. Please try again.")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Wait for connection to be ready
 | 
				
			||||||
 | 
							time.Sleep(2 * time.Second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Update message
 | 
				
			||||||
 | 
							followupMsg(s, i, "Now streaming EuropaFM radio in your voice channel!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Start streaming - this is a blocking call, so we run it in a goroutine
 | 
				
			||||||
 | 
							logger.Info("Starting audio stream")
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								// Create a stop channel that's never closed for continuous streaming
 | 
				
			||||||
 | 
								stopChan := make(chan bool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Log when streaming ends (should only happen if there's an error)
 | 
				
			||||||
 | 
								defer logger.Info("Audio streaming ended")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Start playing audio with enhanced error logging
 | 
				
			||||||
 | 
								err := playAudioStream(vc, radioURL, stopChan, logger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logger.Error("Audio streaming error", "error", err)
 | 
				
			||||||
 | 
									followupMsg(s, i, "The audio stream has ended unexpectedly. Please try again later.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case "stop":
 | 
				
			||||||
 | 
							// Find voice connection and disconnect
 | 
				
			||||||
 | 
							vc, err := findExistingVoiceConnection(ctx, s, i.GuildID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Info("No active voice connection found", "error", err)
 | 
				
			||||||
 | 
								followupMsg(s, i, "Not currently streaming in any channel.")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logger.Info("Disconnecting from voice channel")
 | 
				
			||||||
 | 
							vc.Disconnect()
 | 
				
			||||||
 | 
							followupMsg(s, i, "Disconnected from voice channel.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper function to send followup messages
 | 
				
			||||||
 | 
					func followupMsg(s *discordgo.Session, i *discordgo.InteractionCreate, content string) {
 | 
				
			||||||
 | 
						_, err := s.FollowupMessageCreate(i.Interaction, false, &discordgo.WebhookParams{
 | 
				
			||||||
 | 
							Content: content,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error sending followup message", "error", err, "content", content)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func findUserVoiceState(ctx context.Context, s *discordgo.Session, guildID, userID string) (*discordgo.VoiceState, error) {
 | 
				
			||||||
 | 
						guild, err := s.State.Guild(guildID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Error getting guild", "guild_id", guildID, "error", err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, vs := range guild.VoiceStates {
 | 
				
			||||||
 | 
							if vs.UserID == userID {
 | 
				
			||||||
 | 
								return vs, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("user not in any voice channel")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func findExistingVoiceConnection(ctx context.Context, s *discordgo.Session, guildID string) (*discordgo.VoiceConnection, error) {
 | 
				
			||||||
 | 
						for _, vc := range s.VoiceConnections {
 | 
				
			||||||
 | 
							if vc.GuildID == guildID {
 | 
				
			||||||
 | 
								return vc, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("no active voice connection found")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func playAudioStream(vc *discordgo.VoiceConnection, url string, stopChan chan bool, logger *slog.Logger) error {
 | 
				
			||||||
 | 
						logger.Info("Starting audio stream with enhanced logging", "url", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up panic recovery
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if r := recover(); r != nil {
 | 
				
			||||||
 | 
								logger.Error("Recovered from panic in audio stream", "panic", r)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to detect what stream format we're dealing with
 | 
				
			||||||
 | 
						if strings.HasSuffix(url, ".m3u8") {
 | 
				
			||||||
 | 
							logger.Info("Detected HLS stream format (.m3u8)")
 | 
				
			||||||
 | 
						} else if strings.HasSuffix(url, ".mp3") {
 | 
				
			||||||
 | 
							logger.Info("Detected MP3 audio format")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("Unknown stream format, attempting to play anyway")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Before we start playing, make sure ffmpeg is available
 | 
				
			||||||
 | 
						ffmpegPath, err := exec.LookPath("ffmpeg")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("ffmpeg not found in PATH", "error", err)
 | 
				
			||||||
 | 
							return fmt.Errorf("ffmpeg not found: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						logger.Info("Found ffmpeg", "path", ffmpegPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check ffmpeg version
 | 
				
			||||||
 | 
						versionCmd := exec.Command(ffmpegPath, "-version")
 | 
				
			||||||
 | 
						versionOutput, err := versionCmd.CombinedOutput()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error("Failed to get ffmpeg version", "error", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							versionLines := strings.Split(string(versionOutput), "\n")
 | 
				
			||||||
 | 
							if len(versionLines) > 0 {
 | 
				
			||||||
 | 
								logger.Info("ffmpeg version", "version", versionLines[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to run a test of ffmpeg with the URL to see if it can connect
 | 
				
			||||||
 | 
						testCmd := exec.Command(ffmpegPath, "-i", url, "-t", "1", "-f", "null", "-")
 | 
				
			||||||
 | 
						logger.Info("Testing stream with ffmpeg", "command", testCmd.String())
 | 
				
			||||||
 | 
						testOutput, err := testCmd.CombinedOutput()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Warn("ffmpeg test returned error but this might be normal", "error", err, "output", string(testOutput))
 | 
				
			||||||
 | 
							// Continue anyway - some errors are normal here
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							logger.Info("ffmpeg test succeeded", "output", string(testOutput))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up a context with timeout for the stream
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(context.Background())
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start a goroutine to monitor the stream
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-ctx.Done():
 | 
				
			||||||
 | 
								// Context was canceled, nothing to do
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							case <-time.After(5 * time.Second):
 | 
				
			||||||
 | 
								// If we're still running after 5 seconds, assume the stream is working
 | 
				
			||||||
 | 
								logger.Info("Stream appears to be working")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start the stream
 | 
				
			||||||
 | 
						logger.Info("Starting audio stream with dgvoice", "url", url)
 | 
				
			||||||
 | 
						dgvoice.PlayAudioFile(vc, url, stopChan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we get here, the stream ended unexpectedly
 | 
				
			||||||
 | 
						logger.Warn("Audio stream ended unexpectedly")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If primary URL fails and it's not the backup URL, try the backup URL
 | 
				
			||||||
 | 
						if url == radioURL && backupRadioURL != "" {
 | 
				
			||||||
 | 
							logger.Info("Primary URL failed, trying backup URL", "backup_url", backupRadioURL)
 | 
				
			||||||
 | 
							dgvoice.PlayAudioFile(vc, backupRadioURL, stopChan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If we get here, the backup stream also ended unexpectedly
 | 
				
			||||||
 | 
							logger.Warn("Backup audio stream also ended unexpectedly")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fmt.Errorf("audio stream ended unexpectedly")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue