diff --git a/.env_example b/.env_example new file mode 100644 index 0000000..af04bb2 --- /dev/null +++ b/.env_example @@ -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 diff --git a/.gitignore b/.gitignore index 554e8a0..01f4f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,4 @@ cython_debug/ #.idea/ # My stuff -data/ +data/*.db diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..65c2fd2 --- /dev/null +++ b/SETUP.md @@ -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 +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!** 🎵⏱️ diff --git a/cogs/music/main.py b/cogs/music/main.py index 0a5d045..6398e3a 100644 --- a/cogs/music/main.py +++ b/cogs/music/main.py @@ -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) diff --git a/cogs/music/queue.py b/cogs/music/queue.py index 0a420e0..24b15e1 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -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 = { diff --git a/config.py b/config.py index 02422ee..0828245 100644 --- a/config.py +++ b/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) diff --git a/main.py b/main.py index 8b4cac8..2c4a7bb 100644 --- a/main.py +++ b/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()) diff --git a/requirements.txt b/requirements.txt index 4702160..2e2bd46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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