updated tech stack for newer versions of python, removed pyaudio. refactored configuration into .env file and integration inside logic, removed repeat config reads
This commit is contained in:
67
.env_example
Normal file
67
.env_example
Normal file
@@ -0,0 +1,67 @@
|
||||
# Groovy-Zilean Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
# NEVER commit .env to git!
|
||||
|
||||
# ===================================
|
||||
# Environment Selection
|
||||
# ===================================
|
||||
# Set to "dev" for development bot, "live" for production
|
||||
ENVIRONMENT=dev
|
||||
|
||||
# ===================================
|
||||
# Discord Bot Tokens
|
||||
# ===================================
|
||||
DISCORD_TOKEN_DEV=
|
||||
DISCORD_TOKEN_LIVE=
|
||||
|
||||
# Bot settings
|
||||
DISCORD_PREFIX=
|
||||
|
||||
# ===================================
|
||||
# Spotify Integration
|
||||
# ===================================
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
|
||||
# ===================================
|
||||
# Database
|
||||
# ===================================
|
||||
# For now (SQLite)
|
||||
DB_PATH=./data/music.db
|
||||
|
||||
# For future (PostgreSQL)
|
||||
# DB_HOST=localhost
|
||||
# DB_PORT=5432
|
||||
# DB_NAME=groovy_zilean
|
||||
# DB_USER=groovy
|
||||
# DB_PASSWORD=your_db_password
|
||||
|
||||
# ===================================
|
||||
# Bot Status/Presence
|
||||
# ===================================
|
||||
# Types: playing, listening, watching, streaming, competing
|
||||
STATUS_TYPE=listening
|
||||
STATUS_TEXT==help | /help
|
||||
# STATUS_URL= # Only needed for streaming type
|
||||
|
||||
# ===================================
|
||||
# Color Scheme (hex colors)
|
||||
# ===================================
|
||||
COLOR_PRIMARY=#7289DA
|
||||
COLOR_SUCCESS=#43B581
|
||||
COLOR_ERROR=#F04747
|
||||
COLOR_WARNING=#FAA61A
|
||||
|
||||
# ===================================
|
||||
# Web Dashboard (Future)
|
||||
# ===================================
|
||||
# DISCORD_CLIENT_ID=your_oauth_client_id
|
||||
# DISCORD_CLIENT_SECRET=your_oauth_secret
|
||||
# DISCORD_REDIRECT_URI=http://localhost:8000/callback
|
||||
# WEB_SECRET_KEY=random_secret_for_sessions
|
||||
|
||||
# ===================================
|
||||
# Logging
|
||||
# ===================================
|
||||
LOG_LEVEL=INFO
|
||||
# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -160,4 +160,4 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
# My stuff
|
||||
data/
|
||||
data/*.db
|
||||
|
||||
201
SETUP.md
Normal file
201
SETUP.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Groovy-Zilean Setup Guide
|
||||
|
||||
Quick start guide for getting groovy-zilean running locally or in production.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.11+** (3.9 deprecated by yt-dlp)
|
||||
- **FFmpeg** installed on your system
|
||||
- Discord Bot Token ([Discord Developer Portal](https://discord.com/developers/applications))
|
||||
- Spotify API Credentials ([Spotify Developer Dashboard](https://developer.spotify.com/dashboard))
|
||||
|
||||
---
|
||||
|
||||
## 1. Clone & Setup Environment
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <your-repo-url>
|
||||
cd groovy-zilean
|
||||
|
||||
# Create virtual environment (keeps dependencies isolated)
|
||||
python3.12 -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# OR
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# Install dependencies
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Configuration
|
||||
|
||||
### Create `.env` file
|
||||
|
||||
```bash
|
||||
# Copy the example file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your favorite editor
|
||||
nano .env
|
||||
# OR
|
||||
vim .env
|
||||
# OR
|
||||
code .env # VS Code
|
||||
```
|
||||
|
||||
### Fill in Required Values
|
||||
|
||||
At minimum, you need:
|
||||
|
||||
```env
|
||||
# Choose environment: "dev" or "live"
|
||||
ENVIRONMENT=dev
|
||||
|
||||
# Discord bot tokens (get from Discord Developer Portal)
|
||||
DISCORD_TOKEN_DEV=your_dev_bot_token_here
|
||||
DISCORD_TOKEN_LIVE=your_live_bot_token_here
|
||||
|
||||
# Spotify credentials (get from Spotify Developer Dashboard)
|
||||
SPOTIFY_CLIENT_ID=your_spotify_client_id
|
||||
SPOTIFY_CLIENT_SECRET=your_spotify_secret
|
||||
```
|
||||
|
||||
**Optional but recommended:**
|
||||
```env
|
||||
# Bot settings
|
||||
DISCORD_PREFIX==
|
||||
STATUS_TYPE=listening
|
||||
STATUS_TEXT=Zilean's Theme
|
||||
|
||||
# Colors (hex format)
|
||||
COLOR_PRIMARY=#7289DA
|
||||
COLOR_SUCCESS=#43B581
|
||||
COLOR_ERROR=#F04747
|
||||
COLOR_WARNING=#FAA61A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Data Directory
|
||||
|
||||
```bash
|
||||
# Create directory for database
|
||||
mkdir -p data
|
||||
|
||||
# The database file (music.db) will be created automatically on first run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Run the Bot
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
# Make sure .env has ENVIRONMENT=dev
|
||||
source venv/bin/activate # If not already activated
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
|
||||
```bash
|
||||
# Change .env to ENVIRONMENT=live
|
||||
source venv/bin/activate
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Switching Between Dev and Live
|
||||
|
||||
**Super easy!** Just change one line in `.env`:
|
||||
|
||||
```bash
|
||||
# For development bot
|
||||
ENVIRONMENT=dev
|
||||
|
||||
# For production bot
|
||||
ENVIRONMENT=live
|
||||
```
|
||||
|
||||
The bot will automatically use the correct token!
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Configuration Error: DISCORD_TOKEN_DEV not found"
|
||||
- Make sure you copied `.env.example` to `.env`
|
||||
- Check that `.env` has the token values filled in
|
||||
- Token should NOT have quotes around it
|
||||
|
||||
### "No module named 'dotenv'"
|
||||
```bash
|
||||
pip install python-dotenv
|
||||
```
|
||||
|
||||
### "FFmpeg not found"
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install ffmpeg
|
||||
|
||||
# macOS
|
||||
brew install ffmpeg
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S ffmpeg
|
||||
```
|
||||
|
||||
### Python version issues
|
||||
yt-dlp requires Python 3.10+. Check your version:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
If too old, install newer Python and recreate venv:
|
||||
```bash
|
||||
python3.12 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
groovy-zilean/
|
||||
├── main.py # Entry point (run this!)
|
||||
├── bot.py # Bot class definition
|
||||
├── config.py # Configuration management
|
||||
├── .env # YOUR secrets (never commit!)
|
||||
├── .env.example # Template (safe to commit)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── cogs/
|
||||
│ └── music/ # Music functionality
|
||||
│ ├── main.py # Commands
|
||||
│ ├── queue.py # Queue management
|
||||
│ ├── util.py # Utilities
|
||||
│ └── translate.py # URL/search handling
|
||||
└── data/
|
||||
└── music.db # SQLite database (auto-created)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Check out `PRODUCTION_ROADMAP.md` for the full development plan
|
||||
- See `README.md` for feature list and usage
|
||||
- Join your test server and try commands!
|
||||
|
||||
**Happy coding!** 🎵⏱️
|
||||
@@ -12,28 +12,7 @@ from cogs.music.help import music_help
|
||||
|
||||
import spotipy
|
||||
from spotipy.oauth2 import SpotifyClientCredentials
|
||||
|
||||
|
||||
# Fix this pls
|
||||
|
||||
import json
|
||||
#from .. import config
|
||||
# Read data from JSON file in ./data/config.json
|
||||
def read_data():
|
||||
with open("./data/config.json", "r") as file:
|
||||
return json.load(file)
|
||||
|
||||
raise Exception("Could not load config data")
|
||||
|
||||
|
||||
def get_spotify_creds():
|
||||
data = read_data()
|
||||
data = data.get("spotify")
|
||||
|
||||
SCID = data.get("SCID")
|
||||
secret = data.get("SECRET")
|
||||
|
||||
return SCID, secret
|
||||
import config # Use centralized config
|
||||
|
||||
|
||||
|
||||
@@ -51,10 +30,13 @@ class music(commands.Cog):
|
||||
help_command.cog = self
|
||||
self.help_command = help_command
|
||||
|
||||
SCID, secret = get_spotify_creds()
|
||||
# Get Spotify credentials from centralized config
|
||||
spotify_id, spotify_secret = config.get_spotify_creds()
|
||||
# Authentication - without user
|
||||
client_credentials_manager = SpotifyClientCredentials(client_id=SCID,
|
||||
client_secret=secret)
|
||||
client_credentials_manager = SpotifyClientCredentials(
|
||||
client_id=spotify_id,
|
||||
client_secret=spotify_secret
|
||||
)
|
||||
|
||||
self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import discord
|
||||
import asyncio
|
||||
|
||||
from .translate import search_song
|
||||
import config
|
||||
|
||||
db_path = "./data/music.db"
|
||||
# Get database path from centralized config
|
||||
db_path = config.get_db_path()
|
||||
|
||||
# Base FFmpeg options (will be modified by effects)
|
||||
BASE_FFMPEG_OPTS = {
|
||||
|
||||
297
config.py
297
config.py
@@ -1,115 +1,198 @@
|
||||
# config.py
|
||||
# This file should parse all configurations within the bot
|
||||
# Modern configuration management using environment variables
|
||||
|
||||
import os
|
||||
import discord
|
||||
from discord import Color
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from typing import Optional
|
||||
|
||||
# Read data from JSON file in ./data/config.json
|
||||
def read_data():
|
||||
with open("./data/config.json", "r") as file:
|
||||
return json.load(file)
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
raise Exception("Could not load config data")
|
||||
|
||||
|
||||
def get_spotify_creds():
|
||||
data = read_data()
|
||||
data = data.get("spotify")
|
||||
|
||||
SCID = data.get("SCID")
|
||||
secret = data.get("SECRET")
|
||||
|
||||
return SCID, secret
|
||||
|
||||
|
||||
# Reading prefix
|
||||
def get_prefix():
|
||||
data = read_data()
|
||||
|
||||
prefix = data.get('prefix')
|
||||
if prefix:
|
||||
return prefix
|
||||
|
||||
raise Exception("Missing config data: prefix")
|
||||
|
||||
|
||||
# Fetch the bot secret token
|
||||
def get_login(bot):
|
||||
data = read_data()
|
||||
if data is False or data.get(f"{bot}bot") is False:
|
||||
raise Exception(f"Missing config data: {bot}bot")
|
||||
|
||||
data = data.get(f"{bot}bot")
|
||||
return data.get("secret")
|
||||
|
||||
|
||||
# Read the status and text data
|
||||
def get_status():
|
||||
data = read_data()
|
||||
|
||||
if data is False or data.get('status') is False:
|
||||
raise Exception("Missing config data: status")
|
||||
|
||||
# Find type
|
||||
data = data.get('status')
|
||||
return translate_status(
|
||||
data.get('type'),
|
||||
data.get('text'),
|
||||
data.get('link')
|
||||
)
|
||||
|
||||
# Get colors from colorscheme
|
||||
def get_color(color):
|
||||
data = read_data()
|
||||
|
||||
if data is False or data.get('status') is False:
|
||||
raise Exception("Missing config data: color")
|
||||
|
||||
# Grab color
|
||||
string_value = data.get("colorscheme").get(color)
|
||||
hex_value = Color.from_str(string_value)
|
||||
return hex_value
|
||||
|
||||
|
||||
# Taking JSON variables and converting them into a presence
|
||||
# Use None url incase not provided
|
||||
def translate_status(status_type, status_text, status_url=None):
|
||||
if status_type == "playing":
|
||||
return discord.Activity(
|
||||
type=discord.ActivityType.playing,
|
||||
name=status_text
|
||||
)
|
||||
|
||||
|
||||
elif status_type == "streaming":
|
||||
return discord.Activity(
|
||||
type=discord.ActivityType.streaming,
|
||||
name=status_text,
|
||||
url=status_url
|
||||
)
|
||||
|
||||
elif status_type == "listening":
|
||||
return discord.Activity(
|
||||
type=discord.ActivityType.listening,
|
||||
name=status_text
|
||||
)
|
||||
|
||||
|
||||
elif status_type == "watching":
|
||||
return discord.Activity(
|
||||
type=discord.ActivityType.watching,
|
||||
name=status_text
|
||||
)
|
||||
|
||||
elif status_type == "competing":
|
||||
return discord.Activity(
|
||||
type=discord.ActivityType.competing,
|
||||
name=status_text
|
||||
)
|
||||
|
||||
#TODO
|
||||
# Implement custom status type
|
||||
# ===================================
|
||||
# Environment Detection
|
||||
# ===================================
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev").lower()
|
||||
IS_PRODUCTION = ENVIRONMENT == "live"
|
||||
IS_DEVELOPMENT = ENVIRONMENT == "dev"
|
||||
|
||||
# ===================================
|
||||
# Discord Configuration
|
||||
# ===================================
|
||||
def get_discord_token() -> str:
|
||||
"""Get the appropriate Discord token based on environment"""
|
||||
if IS_PRODUCTION:
|
||||
token = os.getenv("DISCORD_TOKEN_LIVE")
|
||||
if not token:
|
||||
raise ValueError("DISCORD_TOKEN_LIVE not found in environment!")
|
||||
return token
|
||||
else:
|
||||
raise Exception(f"Invalid status type: {status_type}")
|
||||
token = os.getenv("DISCORD_TOKEN_DEV")
|
||||
if not token:
|
||||
raise ValueError("DISCORD_TOKEN_DEV not found in environment!")
|
||||
return token
|
||||
|
||||
|
||||
def get_prefix() -> str:
|
||||
"""Get command prefix (default: =)"""
|
||||
return os.getenv("DISCORD_PREFIX", "=")
|
||||
|
||||
|
||||
# ===================================
|
||||
# Spotify Configuration
|
||||
# ===================================
|
||||
def get_spotify_creds() -> tuple[str, str]:
|
||||
"""Get Spotify API credentials"""
|
||||
client_id = os.getenv("SPOTIFY_CLIENT_ID")
|
||||
client_secret = os.getenv("SPOTIFY_CLIENT_SECRET")
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise ValueError("Spotify credentials not found in environment!")
|
||||
|
||||
return client_id, client_secret
|
||||
|
||||
|
||||
# ===================================
|
||||
# Database Configuration
|
||||
# ===================================
|
||||
def get_db_path() -> str:
|
||||
"""Get SQLite database path"""
|
||||
return os.getenv("DB_PATH", "./data/music.db")
|
||||
|
||||
|
||||
# Future PostgreSQL config
|
||||
def get_postgres_url() -> Optional[str]:
|
||||
"""Get PostgreSQL connection URL (for future migration)"""
|
||||
host = os.getenv("DB_HOST")
|
||||
port = os.getenv("DB_PORT", "5432")
|
||||
name = os.getenv("DB_NAME")
|
||||
user = os.getenv("DB_USER")
|
||||
password = os.getenv("DB_PASSWORD")
|
||||
|
||||
if all([host, name, user, password]):
|
||||
return f"postgresql://{user}:{password}@{host}:{port}/{name}"
|
||||
return None
|
||||
|
||||
|
||||
# ===================================
|
||||
# Bot Status/Presence
|
||||
# ===================================
|
||||
def get_status() -> discord.Activity:
|
||||
"""Get bot status/presence"""
|
||||
status_type = os.getenv("STATUS_TYPE", "listening").lower()
|
||||
status_text = os.getenv("STATUS_TEXT", "Zilean's Theme")
|
||||
status_url = os.getenv("STATUS_URL")
|
||||
|
||||
return translate_status(status_type, status_text, status_url)
|
||||
|
||||
|
||||
def translate_status(
|
||||
status_type: str,
|
||||
status_text: str,
|
||||
status_url: Optional[str] = None
|
||||
) -> discord.Activity:
|
||||
"""Convert status type string to Discord Activity"""
|
||||
|
||||
status_map = {
|
||||
"playing": discord.ActivityType.playing,
|
||||
"streaming": discord.ActivityType.streaming,
|
||||
"listening": discord.ActivityType.listening,
|
||||
"watching": discord.ActivityType.watching,
|
||||
"competing": discord.ActivityType.competing,
|
||||
}
|
||||
|
||||
activity_type = status_map.get(status_type)
|
||||
if not activity_type:
|
||||
raise ValueError(f"Invalid status type: {status_type}")
|
||||
|
||||
# Streaming requires URL
|
||||
if status_type == "streaming":
|
||||
if not status_url:
|
||||
raise ValueError("Streaming status requires STATUS_URL")
|
||||
return discord.Activity(
|
||||
type=activity_type,
|
||||
name=status_text,
|
||||
url=status_url
|
||||
)
|
||||
|
||||
return discord.Activity(type=activity_type, name=status_text)
|
||||
|
||||
|
||||
# ===================================
|
||||
# Color Scheme
|
||||
# ===================================
|
||||
def get_color(color_name: str) -> Color:
|
||||
"""Get color from environment (hex format)"""
|
||||
color_map = {
|
||||
"primary": os.getenv("COLOR_PRIMARY", "#7289DA"),
|
||||
"success": os.getenv("COLOR_SUCCESS", "#43B581"),
|
||||
"error": os.getenv("COLOR_ERROR", "#F04747"),
|
||||
"warning": os.getenv("COLOR_WARNING", "#FAA61A"),
|
||||
}
|
||||
|
||||
hex_value = color_map.get(color_name.lower())
|
||||
if not hex_value:
|
||||
# Default to Discord blurple
|
||||
hex_value = "#7289DA"
|
||||
|
||||
return Color.from_str(hex_value)
|
||||
|
||||
|
||||
# ===================================
|
||||
# Logging Configuration
|
||||
# ===================================
|
||||
def get_log_level() -> str:
|
||||
"""Get logging level from environment"""
|
||||
return os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
|
||||
|
||||
# ===================================
|
||||
# Legacy Support (for backward compatibility)
|
||||
# ===================================
|
||||
def get_login(bot: str) -> str:
|
||||
"""Legacy function - maps to new get_discord_token()"""
|
||||
# Ignore the 'bot' parameter, use ENVIRONMENT instead
|
||||
return get_discord_token()
|
||||
|
||||
|
||||
# ===================================
|
||||
# Validation
|
||||
# ===================================
|
||||
def validate_config():
|
||||
"""Validate that all required config is present"""
|
||||
errors = []
|
||||
|
||||
# Check Discord token
|
||||
try:
|
||||
get_discord_token()
|
||||
except ValueError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
# Check Spotify creds
|
||||
try:
|
||||
get_spotify_creds()
|
||||
except ValueError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
if errors:
|
||||
error_msg = "\n".join(errors)
|
||||
raise ValueError(f"Configuration errors:\n{error_msg}")
|
||||
|
||||
print(f"✅ Configuration validated (Environment: {ENVIRONMENT})")
|
||||
|
||||
|
||||
# ===================================
|
||||
# Startup Info
|
||||
# ===================================
|
||||
def print_config_info():
|
||||
"""Print configuration summary (without secrets!)"""
|
||||
print("=" * 50)
|
||||
print("🎵 Groovy-Zilean Configuration")
|
||||
print("=" * 50)
|
||||
print(f"Environment: {ENVIRONMENT.upper()}")
|
||||
print(f"Prefix: {get_prefix()}")
|
||||
print(f"Database: {get_db_path()}")
|
||||
print(f"Log Level: {get_log_level()}")
|
||||
print(f"Spotify: {'Configured ✅' if os.getenv('SPOTIFY_CLIENT_ID') else 'Not configured ❌'}")
|
||||
print("=" * 50)
|
||||
|
||||
13
main.py
13
main.py
@@ -3,6 +3,16 @@ from bot import Groovy
|
||||
import config
|
||||
import help
|
||||
|
||||
# Validate configuration before starting
|
||||
try:
|
||||
config.validate_config()
|
||||
config.print_config_info()
|
||||
except ValueError as e:
|
||||
print(f"❌ Configuration Error:\n{e}")
|
||||
print("\n💡 Tip: Copy .env.example to .env and fill in your values")
|
||||
exit(1)
|
||||
|
||||
# Initialize bot with validated config
|
||||
client = Groovy(command_prefix=config.get_prefix(), intents=discord.Intents.all())
|
||||
|
||||
@client.event
|
||||
@@ -25,4 +35,5 @@ async def on_voice_state_update(member, before, after):
|
||||
except Exception as e:
|
||||
print(f"Error auto-disconnecting: {e}")
|
||||
|
||||
client.run(config.get_login("live"))
|
||||
# Run bot with environment-appropriate token
|
||||
client.run(config.get_discord_token())
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Core bot framework
|
||||
discord.py==2.6.4
|
||||
aiohttp==3.8.4
|
||||
PyNaCl==1.5.0
|
||||
spotipy==2.23.0
|
||||
discord.py>=2.6.4
|
||||
aiohttp>=3.9.0
|
||||
PyNaCl>=1.5.0
|
||||
spotipy>=2.23.0
|
||||
|
||||
# Configuration management
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# YouTube extractor
|
||||
yt-dlp>=2025.10.14
|
||||
|
||||
# System dependencies
|
||||
PyAudio==0.2.13
|
||||
mutagen==1.46.0
|
||||
# Audio metadata (if needed by yt-dlp)
|
||||
mutagen>=1.47.0
|
||||
|
||||
Reference in New Issue
Block a user