Add comprehensive documentation for Quizzical Beats

- Created a detailed database schema document outlining tables, relationships, and key fields.
- Added OAuth integration documentation covering Spotify and Dropbox authentication processes.
- Introduced a FAQ section addressing common user inquiries about the application.
- Developed a user-friendly index page for easy navigation of the documentation.
- Specified documentation dependencies in requirements.txt for building the documentation site.
- Expanded user guide with sections on account management, creating rounds, exporting rounds, getting started, importing songs, and user interface navigation.
- Updated mkdocs.yml for improved site structure and navigation.
This commit is contained in:
Christian Krakau-Louis
2025-05-13 11:25:20 +02:00
parent 4646c80a15
commit 204941a1dc
23 changed files with 3558 additions and 11 deletions
+787
View File
@@ -0,0 +1,787 @@
# API Reference
This document provides a comprehensive reference for the Quizzical Beats API endpoints.
## Authentication
All API endpoints require authentication unless specified otherwise.
### Authentication Methods
The API supports two authentication methods:
1. **Session Cookie**: For browser-based applications
2. **API Key**: For programmatic access
#### API Key Authentication
To use API key authentication:
1. Generate an API key in your profile settings
2. Include the key in the `X-API-Key` header with each request:
```
X-API-Key: your-api-key-here
```
### Rate Limiting
API requests are rate-limited to prevent abuse:
- 100 requests per hour for standard users
- 300 requests per hour for admin users
Rate limit headers are included in all responses:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1620567890
```
## Response Format
All API responses are in JSON format with a consistent structure:
```json
{
"status": "success|error",
"data": { ... },
"message": "Optional message",
"pagination": {
"page": 1,
"per_page": 20,
"total": 42,
"pages": 3
}
}
```
## Error Handling
When an error occurs, the response will have status code 4xx or 5xx and include an error message:
```json
{
"status": "error",
"message": "Descriptive error message",
"code": "ERROR_CODE"
}
```
Common error codes:
- `UNAUTHORIZED`: Authentication failed
- `FORBIDDEN`: Permission denied
- `NOT_FOUND`: Resource not found
- `VALIDATION_ERROR`: Invalid input data
- `RATE_LIMITED`: Rate limit exceeded
## API Endpoints
### User Endpoints
#### Get Current User
```
GET /api/user
```
Returns information about the currently authenticated user.
**Response:**
```json
{
"status": "success",
"data": {
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"is_admin": false,
"created_at": "2025-01-15T12:34:56Z"
}
}
```
#### Update User Profile
```
PUT /api/user
```
Update the current user's profile information.
**Request Body:**
```json
{
"username": "new_username",
"email": "new_email@example.com"
}
```
**Response:**
```json
{
"status": "success",
"data": {
"id": 123,
"username": "new_username",
"email": "new_email@example.com",
"is_admin": false,
"created_at": "2025-01-15T12:34:56Z"
},
"message": "Profile updated successfully"
}
```
### Song Endpoints
#### List Songs
```
GET /api/songs
```
Returns a paginated list of songs in the user's library.
**Query Parameters:**
- `page`: Page number (default: 1)
- `per_page`: Items per page (default: 20, max: 100)
- `search`: Search term
- `sort`: Sort field (title, artist, album, year)
- `order`: Sort order (asc, desc)
- `genre`: Filter by genre
- `year`: Filter by year
**Response:**
```json
{
"status": "success",
"data": [
{
"id": 456,
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"year": 2010,
"genre": "Rock",
"preview_url": "https://example.com/preview.mp3",
"spotify_id": "spotify:track:abcdef123456",
"duration_ms": 240000
},
// More songs...
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 42,
"pages": 3
}
}
```
#### Get Song
```
GET /api/songs/{id}
```
Returns detailed information about a specific song.
**Response:**
```json
{
"status": "success",
"data": {
"id": 456,
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"year": 2010,
"genre": "Rock",
"preview_url": "https://example.com/preview.mp3",
"spotify_id": "spotify:track:abcdef123456",
"duration_ms": 240000,
"added_by": 123,
"created_at": "2025-02-10T15:30:45Z",
"popularity": 75,
"tags": [
{
"id": 789,
"name": "Summer Hits",
"color": "#ff5500"
}
]
}
}
```
#### Create Song
```
POST /api/songs
```
Add a new song to the user's library.
**Request Body:**
```json
{
"title": "New Song",
"artist": "New Artist",
"album": "New Album",
"year": 2025,
"genre": "Pop",
"spotify_id": "spotify:track:xyz789",
"preview_url": "https://example.com/preview.mp3"
}
```
**Response:**
```json
{
"status": "success",
"data": {
"id": 457,
"title": "New Song",
"artist": "New Artist",
"album": "New Album",
"year": 2025,
"genre": "Pop",
"preview_url": "https://example.com/preview.mp3",
"spotify_id": "spotify:track:xyz789",
"added_by": 123,
"created_at": "2025-05-11T09:12:34Z"
},
"message": "Song added successfully"
}
```
#### Update Song
```
PUT /api/songs/{id}
```
Update an existing song.
**Request Body:**
```json
{
"title": "Updated Title",
"artist": "Updated Artist",
"album": "Updated Album",
"year": 2020,
"genre": "Electronic"
}
```
**Response:**
```json
{
"status": "success",
"data": {
"id": 456,
"title": "Updated Title",
"artist": "Updated Artist",
"album": "Updated Album",
"year": 2020,
"genre": "Electronic",
"preview_url": "https://example.com/preview.mp3",
"spotify_id": "spotify:track:abcdef123456"
},
"message": "Song updated successfully"
}
```
#### Delete Song
```
DELETE /api/songs/{id}
```
Remove a song from the user's library.
**Response:**
```json
{
"status": "success",
"message": "Song deleted successfully"
}
```
### Round Endpoints
#### List Rounds
```
GET /api/rounds
```
Returns a paginated list of the user's quiz rounds.
**Query Parameters:**
- `page`: Page number (default: 1)
- `per_page`: Items per page (default: 20, max: 100)
- `search`: Search term
- `sort`: Sort field (name, created_at)
- `order`: Sort order (asc, desc)
- `tag`: Filter by tag ID
**Response:**
```json
{
"status": "success",
"data": [
{
"id": 789,
"name": "80s Rock Classics",
"description": "Classic rock hits from the 1980s",
"created_at": "2025-03-20T14:25:36Z",
"song_count": 10,
"round_type": "decade",
"tags": [
{
"id": 123,
"name": "80s",
"color": "#3366ff"
},
{
"id": 456,
"name": "Rock",
"color": "#cc0000"
}
]
},
// More rounds...
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 15,
"pages": 1
}
}
```
#### Get Round
```
GET /api/rounds/{id}
```
Returns detailed information about a specific round, including its songs.
**Response:**
```json
{
"status": "success",
"data": {
"id": 789,
"name": "80s Rock Classics",
"description": "Classic rock hits from the 1980s",
"created_at": "2025-03-20T14:25:36Z",
"user_id": 123,
"is_public": true,
"round_type": "decade",
"intro_file": "/mp3/intros/80s_intro.mp3",
"outro_file": "/mp3/outros/rock_outro.mp3",
"songs": [
{
"id": 101,
"title": "Sweet Child O' Mine",
"artist": "Guns N' Roses",
"year": 1987,
"position": 1,
"question": "Name this iconic 80s rock song",
"answer": "Sweet Child O' Mine by Guns N' Roses",
"points": 10,
"preview_url": "https://example.com/preview1.mp3"
},
// More songs...
],
"tags": [
{
"id": 123,
"name": "80s",
"color": "#3366ff"
},
{
"id": 456,
"name": "Rock",
"color": "#cc0000"
}
]
}
}
```
#### Create Round
```
POST /api/rounds
```
Create a new quiz round.
**Request Body:**
```json
{
"name": "New Quiz Round",
"description": "A fresh music quiz round",
"round_type": "mixed",
"is_public": true,
"song_ids": [101, 102, 103, 104],
"tag_ids": [123, 456]
}
```
**Response:**
```json
{
"status": "success",
"data": {
"id": 790,
"name": "New Quiz Round",
"description": "A fresh music quiz round",
"created_at": "2025-05-11T10:15:20Z",
"user_id": 123,
"is_public": true,
"round_type": "mixed",
"song_count": 4
},
"message": "Round created successfully"
}
```
#### Update Round
```
PUT /api/rounds/{id}
```
Update an existing round.
**Request Body:**
```json
{
"name": "Updated Round Name",
"description": "Updated description",
"is_public": false,
"song_ids": [101, 102, 105, 106],
"tag_ids": [123, 789]
}
```
**Response:**
```json
{
"status": "success",
"data": {
"id": 789,
"name": "Updated Round Name",
"description": "Updated description",
"is_public": false,
"song_count": 4
},
"message": "Round updated successfully"
}
```
#### Delete Round
```
DELETE /api/rounds/{id}
```
Delete a quiz round.
**Response:**
```json
{
"status": "success",
"message": "Round deleted successfully"
}
```
### Export Endpoints
#### Export Round to Dropbox
```
POST /rounds/{round_id}/export-to-dropbox
```
Export a round to the user's connected Dropbox account.
**Request Body Parameters:**
```
include_mp3s: boolean (default: true) - Whether to include MP3 files in the export
include_pdf: boolean (default: true) - Whether to include PDF in the export
custom_folder: string (optional) - Additional subfolder path within the user's configured export path
```
**Response:**
```json
{
"success": true,
"message": "Round exported to Dropbox successfully",
"shared_links": {
"text": "https://www.dropbox.com/s/abc123/round_123_metadata.json?dl=0",
"pdf": "https://www.dropbox.com/s/def456/round_123.pdf?dl=0",
"mp3": "https://www.dropbox.com/s/ghi789/round_123.mp3?dl=0"
}
}
```
**Error Response:**
```json
{
"success": false,
"message": "Error exporting to Dropbox: <error details>",
"redirect": "URL for MP3 generation if needed"
}
```
#### List Dropbox Folders
```
GET /api/dropbox/folders
```
List folders from the user's Dropbox account.
**Query Parameters:**
```
path: string - The path to list folders from (default: root)
```
**Response:**
```json
{
"success": true,
"folders": [
{
"name": "Folder Name",
"path": "/Folder Name",
"is_dir": true
},
{
"name": "Documents",
"path": "/Documents",
"is_dir": true
}
]
}
```
#### Create Dropbox Folder
```
POST /api/dropbox/create-folder
```
Create a new folder in the user's Dropbox account.
**Request Body:**
```json
{
"parent_path": "/path/to/parent",
"folder_name": "New Folder"
}
```
**Response:**
```json
{
"success": true,
"message": "Folder created successfully",
"folder": {
"name": "New Folder",
"path": "/path/to/parent/New Folder",
"is_dir": true
}
}
```
### Dropbox OAuth Endpoints
#### Connect Dropbox Account
```
GET /users/dropbox/connect
```
Initiates the OAuth flow for connecting a Dropbox account.
**Response:**
Redirects to Dropbox OAuth authorization page
#### Dropbox OAuth Callback
```
GET /users/dropbox/callback
```
Handles the OAuth callback from Dropbox.
**Query Parameters:**
```
code: string - The authorization code from Dropbox
error: string - Error message if authorization failed
```
**Response:**
Redirects back to user profile page with a success or error message
#### Disconnect Dropbox Account
```
POST /users/dropbox/disconnect
```
Disconnects the user's Dropbox account.
**Response:**
Redirects back to user profile page with a success message
### Spotify Integration Endpoints
#### Get User Playlists
```
GET /api/spotify/playlists
```
Get the current user's Spotify playlists.
**Response:**
```json
{
"status": "success",
"data": [
{
"id": "spotify:playlist:abcdef123456",
"name": "My Awesome Playlist",
"owner": "spotify_user123",
"track_count": 42,
"image_url": "https://example.com/playlist_cover.jpg"
},
// More playlists...
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 35,
"pages": 2
}
}
```
#### Import Playlist
```
POST /api/spotify/import/playlist
```
Import songs from a Spotify playlist.
**Request Body:**
```json
{
"playlist_id": "spotify:playlist:abcdef123456",
"limit": 20
}
```
**Response:**
```json
{
"status": "success",
"data": {
"import_id": "imp_789012",
"playlist_name": "My Awesome Playlist",
"status": "processing",
"songs_found": 42,
"songs_to_import": 20,
"estimated_completion": "45 seconds"
},
"message": "Import started"
}
```
### Health Check Endpoint
```
GET /api/health
```
Get system health information (admin only).
**Response:**
```json
{
"status": "success",
"data": {
"version": "1.0.0",
"uptime": "5d 12h 37m",
"database": {
"status": "connected",
"size": "42MB",
"migrations": "up-to-date"
},
"storage": {
"available": "1.2GB",
"used": "345MB"
},
"services": {
"spotify": "connected",
"dropbox": "connected",
"email": "connected"
}
}
}
```
## Webhook Notifications
Quizzical Beats can send webhook notifications for certain events.
### Configuring Webhooks
Webhooks are configured in the admin settings:
1. Go to Admin > System > Webhooks
2. Add a new webhook URL
3. Select which events to receive notifications for
### Webhook Events
- `round.created`: A new round was created
- `round.exported`: A round was exported
- `import.completed`: A Spotify import was completed
- `backup.completed`: A system backup was completed
### Webhook Payload
```json
{
"event": "round.exported",
"timestamp": "2025-05-11T10:30:45Z",
"data": {
"round_id": 789,
"round_name": "80s Rock Classics",
"user_id": 123,
"username": "john_doe",
"export_format": "zip",
"destination": "dropbox"
}
}
```
## API Versioning
The current API version is v1. The version is specified in the URL path:
```
/api/v1/resource
```
For backward compatibility, requests to `/api/resource` will be directed to the latest stable API version.
+285
View File
@@ -0,0 +1,285 @@
# Architecture Overview
This document provides a comprehensive overview of the Quizzical Beats architecture, designed to help developers understand the system structure and components.
## Application Structure
Quizzical Beats follows a modular Flask application structure:
```
musicround/
├── __init__.py # Application factory
├── config.py # Configuration management
├── models.py # Database models
├── version.py # Version information
├── errors.py # Error handling
├── deezer_client.py # Deezer API integration
├── helpers/ # Utility modules
│ ├── __init__.py
│ ├── auth_helpers.py # Authentication utilities
│ ├── backup_helper.py # Backup management
│ ├── dropbox_helper.py # Dropbox integration
│ ├── email_helper.py # Email functionality
│ ├── import_helper.py # Song import utilities
│ ├── metadata.py # Song metadata processing
│ ├── spotify_direct.py # Spotify API client
│ └── utils.py # General utilities
├── mp3/ # Audio file storage
├── routes/ # Route definitions
│ ├── __init__.py
│ ├── api.py # API endpoints
│ ├── auth.py # Authentication routes
│ ├── core.py # Core application routes
│ ├── db_admin.py # Database administration
│ ├── deezer_routes.py # Deezer integration
│ ├── generate.py # Content generation
│ ├── import.py # Generic import functionality
│ ├── import_routes.py # Import interface routes
│ ├── import_songs.py # Song import functionality
│ ├── process.py # Audio processing
│ ├── rounds.py # Quiz round management
│ └── users.py # User account management
├── static/ # Static files (CSS, JS, images)
└── templates/ # Jinja2 HTML templates
├── admin/ # Admin interface templates
├── auth/ # Authentication templates
└── ... (other template categories)
```
## Key Components
### Application Factory
The application is initialized using a factory pattern in `__init__.py`. This allows for flexible configuration and testing:
```python
def create_app():
app = Flask(__name__)
# Load configuration
app.config.from_object(Config)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)
# Register blueprints
from musicround.routes import core, auth, rounds, users, import_songs, import_routes, generate, process, api, deezer_routes, db_admin
app.register_blueprint(core.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(rounds.bp)
app.register_blueprint(users.bp)
app.register_blueprint(import_songs.bp)
app.register_blueprint(import_routes.bp)
app.register_blueprint(generate.bp)
app.register_blueprint(process.bp)
app.register_blueprint(api.bp)
app.register_blueprint(deezer_routes.bp)
app.register_blueprint(db_admin.bp)
return app
```
### Configuration Management
Configuration is handled in `config.py` using environment variables loaded from a `.env` file:
```python
class Config:
# Core configuration
DEBUG = os.getenv("DEBUG", "True") == "True"
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-key-please-change')
# API keys for various services
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
DEEPL_API_KEY = os.getenv("DEEPL_API_KEY")
MEANINGCLOUD_API_KEY = os.getenv("MEANINGCLOUD_API_KEY")
LASTFM_API_KEY = os.getenv("LASTFM_API_KEY")
# Database configuration
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'sqlite:///data/song_data.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# OAuth provider configurations
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
DROPBOX_APP_KEY = os.getenv("DROPBOX_APP_KEY")
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
# ... other configuration
```
### Database Models
The data model is defined in `models.py` using SQLAlchemy ORM:
```python
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=True)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
rounds = db.relationship('Round', backref='author', lazy=True)
# OAuth tokens and preferences
class Song(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
artist = db.Column(db.String(200), nullable=False)
spotify_id = db.Column(db.String(50), nullable=True)
preview_url = db.Column(db.String(255), nullable=True)
year = db.Column(db.Integer, nullable=True)
# Audio features and metadata
class Round(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
songs = db.relationship('RoundSong', backref='round', lazy=True, cascade="all, delete-orphan")
# Round configuration and settings
```
### Authentication System
The authentication system supports:
1. **Local Authentication**: Username/password authentication
2. **OAuth Providers**:
- Spotify
- Google
- Authentik (OpenID Connect)
3. **Role-Based Access Control**: Admin vs. regular users
OAuth integration is handled through dedicated helper functions in `auth_helpers.py`:
```python
def get_spotify_oauth():
# Configure OAuth for Spotify
def get_google_oauth():
# Configure OAuth for Google
def get_authentik_oauth():
# Configure OAuth for Authentik
```
### External Integrations
#### Spotify Integration
The `spotify_direct.py` module provides:
- Authentication with Spotify API
- Playlist import functionality
- Track search and metadata retrieval
- Audio feature access
#### Dropbox Integration
The `dropbox_helper.py` module enables:
- OAuth authentication with Dropbox
- File export to Dropbox
- Folder management in Dropbox
- Shared link generation
#### Deezer Integration
The `deezer_client.py` and related routes provide:
- Authentication with Deezer API
- Playlist import
- Track search and preview access
#### OpenAI Integration
AI-powered features use the OpenAI API for:
- Round generation suggestions
- Lyric analysis
- Song categorization
### Backup System
The backup system in `backup_helper.py` provides:
```python
def create_backup(include_mp3=True, include_config=True, backup_name=None):
# Create ZIP archive with database and optional files
def restore_from_backup(backup_file, force=False):
# Restore system from backup archive
def list_backups():
# List available backups with metadata
def verify_backup(backup_path):
# Check backup integrity
```
Features include:
- Database dumps using SQLite backup API
- MP3 file inclusion in backups
- Configuration file backup
- Scheduled backups
- Retention policy management
## Request Flow
1. Request arrives at the Flask application
2. Blueprint routes direct to the appropriate view function
3. Authentication middleware checks for required permissions
4. View function processes the request:
- Database queries via SQLAlchemy models
- External API calls where needed
- Business logic processing
5. Response is rendered using Jinja2 templates
6. Rendered HTML is returned to the client
### Example Routes
```python
@bp.route('/rounds/<int:round_id>')
@login_required
def view_round(round_id):
round = Round.query.get_or_404(round_id)
# Check permissions
# Process data
return render_template('rounds/view.html', round=round)
@bp.route('/rounds/<int:round_id>/export-to-dropbox', methods=['POST'])
@login_required
def export_to_dropbox(round_id):
round = Round.query.get_or_404(round_id)
# Check permissions
# Export to Dropbox
return jsonify({'success': True, 'message': 'Export successful'})
```
## System Health Monitoring
The health monitoring system provides dashboards for:
1. **Database Health**: Connection status, table counts, size
2. **Storage Health**: Directory status, file counts, permissions
3. **External Service Status**: API connectivity checks
4. **Version Information**: Application version, dependencies
## Extension Points
To extend Quizzical Beats, consider these integration points:
1. **New OAuth Providers**: Add provider configuration in `auth_helpers.py`
2. **Additional Export Formats**: Implement in the rounds routes
3. **New Music Data Sources**: Create a new client module similar to `spotify_direct.py` or `deezer_client.py`
4. **Custom Audio Processing**: Extend the functionality in the `process.py` routes
5. **AI Features**: Enhance OpenAI integration for additional content generation
## Technology Stack
- **Backend**: Python 3.8+, Flask 2.x
- **Database**: SQLAlchemy 1.4+ with SQLite/PostgreSQL/MySQL
- **Frontend**: TailwindCSS, Alpine.js, vanilla JavaScript
- **Authentication**: Flask-Login, OAuth integrations
- **APIs**: Spotify, Deezer, Dropbox, OpenAI, DeepL
- **Media Processing**: FFmpeg, MP3 manipulation libraries
- **Testing**: Pytest for unit and integration tests
+195
View File
@@ -0,0 +1,195 @@
# Contributing to Quizzical Beats
This guide provides information for developers who want to contribute to the Quizzical Beats project.
## Getting Started
### Development Environment Setup
1. Fork the repository on GitHub
2. Clone your fork locally:
```bash
git clone https://github.com/YOUR-USERNAME/musicround.git
cd musicround
```
3. Set up a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
4. Install development dependencies:
```bash
pip install -r requirements-dev.txt
```
5. Set up pre-commit hooks:
```bash
pre-commit install
```
6. Configure your environment variables for development:
```bash
cp .env.example .env.dev
# Edit .env.dev with your development settings
```
## Development Workflow
### Branching Strategy
We use a simplified Git flow approach:
- `main`: Production-ready code
- `develop`: Main development branch
- Feature branches: Created from `develop` for new features
- Bugfix branches: Created from `develop` for bug fixes
- Hotfix branches: Created from `main` for critical fixes
Naming conventions:
- Feature branches: `feature/short-description`
- Bug fix branches: `bugfix/issue-number-description`
- Hotfix branches: `hotfix/issue-number-description`
### Making Changes
1. Create a new branch from `develop`:
```bash
git checkout develop
git pull origin develop
git checkout -b feature/your-feature-name
```
2. Make your changes, following the coding standards
3. Run tests to ensure your changes don't break existing functionality:
```bash
pytest
```
4. Commit your changes with a descriptive message:
```bash
git commit -am "Add feature: short description
More detailed explanation of the changes if needed.
Fixes #123"
```
5. Push your branch to your fork:
```bash
git push origin feature/your-feature-name
```
6. Create a pull request from your branch to the `develop` branch of the main repository
## Coding Standards
### Python Style Guide
We follow PEP 8 with some modifications:
- Line length: 100 characters maximum
- Use 4 spaces for indentation (no tabs)
- Use docstrings for all classes and functions
- Follow Google's Python Style Guide for docstrings
### Flask-Specific Guidelines
- Organize routes by functionality in blueprints
- Keep view functions small and focused
- Use decorators for common patterns
- Prefer class-based views for complex endpoints
### Testing Guidelines
- Write tests for all new features
- Maintain or improve test coverage
- Structure tests in a similar way to the code they test
- Use fixtures for common setup
- Mock external services in tests
## Pull Request Process
1. Ensure your code passes all tests and linting checks
2. Update documentation if your changes affect it
3. Add your changes to the CHANGELOG.md under "Unreleased"
4. Request a review from at least one maintainer
5. Address any feedback from the reviewer
6. Once approved, a maintainer will merge your PR
## Database Migrations
When making changes to the database schema:
1. Create a new migration script in the `migrations/` directory
2. Name it descriptively (e.g., `add_user_preferences.py`)
3. Implement both upgrade and downgrade paths
4. Test the migration in both directions
5. Document the changes in the database schema documentation
Example migration script:
```python
# migrations/add_user_preferences.py
def upgrade(db):
db.execute("""
ALTER TABLE user
ADD COLUMN preferences JSON NULL
""")
def downgrade(db):
db.execute("""
ALTER TABLE user
DROP COLUMN preferences
""")
```
## Documentation Guidelines
When contributing to the documentation:
1. Use Markdown for all documentation files
2. Keep language clear and concise
3. Include code examples where appropriate
4. Follow the existing documentation structure
5. Update the documentation when implementing new features
## Release Process
Our release process follows these steps:
1. Features and bugfixes are merged into `develop`
2. When ready for release, we:
- Create a release branch `release/X.Y.Z`
- Update version number in `version.py`
- Finalize CHANGELOG.md
- Run final tests
3. The release branch is merged into `main`
4. A tag is created for the release
5. `main` is merged back into `develop`
## Getting Help
If you need help or have questions:
- Check the existing documentation
- Look at similar features or patterns in the codebase
- Reach out on the project issues page
- Contact the maintainers directly
## Code of Conduct
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
### Our Standards
- Be respectful and inclusive
- Accept constructive criticism gracefully
- Focus on what's best for the community
- Show empathy towards other community members
## License
By contributing to Quizzical Beats, you agree that your contributions will be licensed under the project's MIT License.
+289
View File
@@ -0,0 +1,289 @@
# Database Schema
This document provides an overview of the Quizzical Beats database schema, including tables, relationships, and key fields.
## Entity Relationship Diagram
The following diagram illustrates the relationships between the main entities in Quizzical Beats:
```
+---------------+ +---------------+ +---------------+
| User | | Round | | Song |
+---------------+ +---------------+ +---------------+
| id |<----->| id | | id |
| username | | name | | title |
| email | | round_type | | artist |
| password_hash | | songs |-------| spotify_id |
| is_admin | | round_criteria| | deezer_id |
| roles |----+ | created_at | | isrc |
| auth_provider | | | updated_at | | preview_url |
| oauth_tokens | | | mp3_generated | | cover_url |
+---------------+ | | pdf_generated | | tags |----+
^ | +---------------+ | audio_features| |
| | +---------------+ |
| | ^ |
| v | |
+---------------+ +---------------+ +---------------+ |
| UserPreferences| | Role | | RoundExport | |
+---------------+ +---------------+ +---------------+ |
| id | | id | | id | |
| user_id | | name | | round_id | |
| default_tts | | description | | user_id | |
| enable_intro | +---------------+ | export_type | |
| theme | | timestamp | |
+---------------+ | destination | |
+---------------+ |
|
+---------------+ +---------------+ |
| SystemSetting | | Tag |<--------+
+---------------+ +---------------+
| id | | id |
| key | | name |
| value | | created_at |
+---------------+ +---------------+
```
## Tables
### User
The `User` table stores user account information and authentication details.
| Column | Type | Description |
|-----------------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| username | String(80) | User's display name |
| email | String(120) | User's email address |
| password_hash | String(255) | Hashed password (nullable for OAuth-only users) |
| first_name | String(50) | User's first name |
| last_name | String(50) | User's last name |
| active | Boolean | Account active status |
| is_admin | Boolean | Administrator privileges flag |
| created_at | DateTime | Account creation timestamp |
| last_login | DateTime | Last login timestamp |
| reset_token | String(100) | Password reset token |
| reset_token_expiry | DateTime | Token expiration time |
| auth_provider | String(20) | Authentication provider (local, google, etc.) |
| oauth_id | String(100) | Spotify user ID |
| spotify_token | Text | Spotify access token |
| spotify_refresh_token | Text | Spotify refresh token |
| spotify_token_expiry | DateTime | Spotify token expiration |
| google_id | String(100) | Google user ID |
| google_token | Text | Google access token |
| google_refresh_token | Text | Google refresh token |
| authentik_id | String(100) | Authentik user ID |
| authentik_token | Text | Authentik access token |
| authentik_refresh_token | Text | Authentik refresh token |
| dropbox_id | String(100) | Dropbox user ID |
| dropbox_token | Text | Dropbox access token |
| dropbox_refresh_token | Text | Dropbox refresh token |
| dropbox_token_expiry | DateTime | Dropbox token expiration |
| dropbox_export_path | String(255) | User's preferred Dropbox export folder |
| intro_mp3 | String(255) | Custom intro MP3 path |
| outro_mp3 | String(255) | Custom outro MP3 path |
| replay_mp3 | String(255) | Custom replay MP3 path |
### UserPreferences
The `UserPreferences` table stores user-specific settings.
| Column | Type | Description |
|----------------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| user_id | Integer | Foreign key to User |
| default_tts_service | String(32) | Default text-to-speech service (polly, etc.) |
| enable_intro | Boolean | Whether to enable intro sound |
| theme | String(16) | UI theme preference (light, dark) |
### Role
The `Role` table defines user roles for permission management.
| Column | Type | Description |
|-------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| name | String(50) | Role name |
| description | String(255) | Role description |
### user_roles
The `user_roles` table is an association table linking users to roles.
| Column | Type | Description |
|-------------|--------------|--------------------------------------------------|
| user_id | Integer | Foreign key to User |
| role_id | Integer | Foreign key to Role |
### Song
The `Song` table stores detailed information about music tracks from various sources.
| Column | Type | Description |
|-----------------------|--------------|-------------------------------------------------|
| id | Integer | Primary key |
| spotify_id | String(100) | Spotify track ID |
| deezer_id | Integer | Deezer track ID |
| isrc | String(20) | International Standard Recording Code |
| title | String(200) | Song title |
| artist | String(200) | Artist name |
| album_name | String(200) | Album name |
| genre | String(100) | Music genre |
| year | Integer | Release year |
| preview_url | String(500) | Primary audio preview URL |
| cover_url | String(500) | Primary album cover URL |
| spotify_preview_url | String(500) | Spotify-specific preview URL |
| deezer_preview_url | String(500) | Deezer-specific preview URL |
| apple_preview_url | String(500) | Apple Music preview URL |
| youtube_preview_url | String(500) | YouTube preview URL |
| spotify_cover_url | String(500) | Spotify cover image URL |
| deezer_cover_url | String(500) | Deezer cover image URL |
| apple_cover_url | String(500) | Apple Music cover image URL |
| popularity | Integer | Popularity score (0-100) |
| used_count | Integer | Number of times used in rounds |
| source | String(20) | Data source (spotify, deezer, acrcloud) |
| import_date | DateTime | When the song was imported |
| added_at | DateTime | When the song was added |
| last_used | DateTime | When the song was last used |
| metadata_sources | String(500) | Comma-separated list of metadata sources |
| acousticness | Float | Spotify audio feature - acousticness (0.0-1.0) |
| danceability | Float | Spotify audio feature - danceability (0.0-1.0) |
| energy | Float | Spotify audio feature - energy (0.0-1.0) |
| instrumentalness | Float | Spotify audio feature - instrumentalness |
| key | Integer | Spotify audio feature - musical key |
| liveness | Float | Spotify audio feature - liveness (0.0-1.0) |
| loudness | Float | Spotify audio feature - loudness (dB) |
| mode | Integer | Spotify audio feature - modality (major/minor) |
| speechiness | Float | Spotify audio feature - speechiness (0.0-1.0) |
| tempo | Float | Spotify audio feature - tempo (BPM) |
| time_signature | Integer | Spotify audio feature - time signature |
| valence | Float | Spotify audio feature - valence (0.0-1.0) |
| duration_ms | Integer | Track duration in milliseconds |
| analysis_url | String(500) | URL to full audio analysis |
| additional_data | Text | Additional data as JSON |
### Tag
The `Tag` table stores tags for categorizing songs.
| Column | Type | Description |
|-------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| name | String(50) | Tag name |
| created_at | DateTime | Creation timestamp |
### SongTag
The `SongTag` table links songs to tags.
| Column | Type | Description |
|-------------|--------------|--------------------------------------------------|
| song_id | Integer | Foreign key to Song |
| tag_id | Integer | Foreign key to Tag |
| created_at | DateTime | When the tag was applied |
### Round
The `Round` table stores music quiz rounds.
| Column | Type | Description |
|-----------------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| name | String(200) | Round name |
| round_type | String(50) | Type of round (genre, decade, etc.) |
| round_criteria_used | String(500) | Criteria used to generate the round |
| songs | Text | JSON string of song IDs in order |
| genre | String(100) | Genre of the round (if applicable) |
| decade | String(10) | Decade of the round (if applicable) |
| tag | String(50) | Tag of the round (if applicable) |
| created_at | DateTime | Creation timestamp |
| updated_at | DateTime | Last update timestamp |
| mp3_generated | Boolean | Flag indicating if MP3 has been generated |
| pdf_generated | Boolean | Flag indicating if PDF has been generated |
| last_generated_at | DateTime | When files were last generated |
### RoundExport
The `RoundExport` table tracks exports of rounds to various destinations.
| Column | Type | Description |
|---------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| round_id | Integer | Foreign key to Round |
| user_id | Integer | Foreign key to User |
| export_type | String(20) | Export type (dropbox, email, etc.) |
| timestamp | DateTime | Export timestamp |
| destination | String(500) | Destination (path, email, etc.) |
| include_mp3s | Boolean | Whether MP3s were included |
| status | String(20) | Export status (success, failed) |
| error_message | Text | Error message if export failed |
### SystemSetting
The `SystemSetting` table stores application-wide settings.
| Column | Type | Description |
|-------------|--------------|--------------------------------------------------|
| id | Integer | Primary key |
| key | String(64) | Setting key |
| value | Text | Setting value |
## Key Relationships
### User Relationships
- **User → UserPreferences**: One-to-one. A user has one set of preferences.
- **User ↔ Roles**: Many-to-many through user_roles. A user can have multiple roles, and a role can be assigned to multiple users.
- **User → RoundExports**: One-to-many. A user can create multiple exports.
### Song Relationships
- **Song ↔ Tags**: Many-to-many through SongTag. A song can have multiple tags, and a tag can be applied to multiple songs.
- **Song → Rounds**: Many-to-many (implicit). Songs are referenced in the Round.songs field as a JSON string of IDs.
### Round Relationships
- **Round → RoundExports**: One-to-many. A round can have multiple exports.
- **Round → Songs**: Many-to-many (implicit). A round contains multiple songs referenced by ID.
## Data Model Features
### OAuth Integration
The User model integrates OAuth provider information directly:
- Support for Spotify, Google, Authentik and Dropbox OAuth providers
- Token storage and refresh token functionality
- Provider-specific user IDs
### Audio Features
The Song model includes detailed audio features from Spotify:
- Acoustic characteristics (acousticness, instrumentalness)
- Rhythmic characteristics (tempo, time_signature)
- Mood characteristics (valence, energy, danceability)
- Technical characteristics (loudness, key, mode)
### Multi-Source Integration
Songs can be imported from multiple sources:
- Spotify API
- Deezer API
- ACRCloud identification service
- Each song stores source-specific IDs and URLs
### Tagging System
The tagging system allows flexible organization:
- Songs can be tagged for easier categorization
- Tags provide a way to group songs by custom criteria
## Data Migrations
The database schema evolves over time through migrations. Migration scripts are stored in the `migrations/` directory:
- `add_preview_urls.py`: Added Song.preview_url field
- `add_song_fields.py`: Added additional metadata fields to Song
- `add_spotify_audio_features.py`: Added audio analysis data
- `add_oauth_providers.py`: Extended OAuth provider support
- `add_tag_system.py`: Added tagging functionality
- `add_dropbox_oauth.py`: Added Dropbox OAuth support
- `add_dropbox_export_path.py`: Added export path tracking
+167
View File
@@ -0,0 +1,167 @@
# OAuth Integration
This document details how Quizzical Beats integrates with OAuth providers, including Spotify and Dropbox.
## Overview
Quizzical Beats uses OAuth 2.0 to authenticate with third-party services. The current OAuth implementation provides:
- API access to third-party services (Spotify API, Dropbox files)
- Token storage and refresh mechanisms
- Fallback strategies when tokens expire
## OAuth Provider Configuration
### Spotify OAuth
Spotify OAuth is used for API access:
```python
SPOTIFY_CLIENT_ID = os.environ.get('SPOTIFY_CLIENT_ID')
SPOTIFY_CLIENT_SECRET = os.environ.get('SPOTIFY_CLIENT_SECRET')
SPOTIFY_REDIRECT_URI = os.environ.get('SPOTIFY_REDIRECT_URI', 'http://localhost:5000/auth/spotify/callback')
```
### Dropbox OAuth
Dropbox OAuth enables file export functionality:
```python
DROPBOX_APP_KEY = os.environ.get('DROPBOX_APP_KEY')
DROPBOX_APP_SECRET = os.environ.get('DROPBOX_APP_SECRET')
DROPBOX_REDIRECT_URI = os.environ.get('DROPBOX_REDIRECT_URI', 'http://localhost:5000/users/dropbox/callback')
```
## Dropbox Integration Implementation
The Dropbox OAuth integration is implemented directly in the User model:
```python
class User(db.Model):
# Other user fields...
# Dropbox OAuth fields
dropbox_id = db.Column(db.String(100), nullable=True)
dropbox_token = db.Column(db.Text(), nullable=True)
dropbox_refresh_token = db.Column(db.Text(), nullable=True)
dropbox_token_expiry = db.Column(db.DateTime(), nullable=True)
dropbox_export_path = db.Column(db.String(255), nullable=True)
```
### Dropbox Authentication Flow
1. User initiates Dropbox connection from their profile page
2. Application redirects to Dropbox's authorization page
3. User grants permission to the application
4. Dropbox redirects back to our callback URL with an authorization code
5. Application exchanges the code for access and refresh tokens
6. Tokens and basic user info are stored in the user's record
Example of the callback handler:
```python
@users_bp.route('/dropbox/callback')
@login_required
def dropbox_callback():
# Handle errors
if 'error' in request.args:
flash(f'Dropbox authorization failed: {error}', 'error')
return redirect(url_for('users.profile'))
# Exchange authorization code for tokens
code = request.args.get('code')
token_info = exchange_code_for_token(code)
# Store tokens in the user model
current_user.dropbox_token = token_info.get('access_token')
current_user.dropbox_refresh_token = token_info.get('refresh_token')
# Store expiration time
expires_in = token_info.get('expires_in', 14400) # Default to 4 hours
current_user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)
# Get and store account info
account_info = get_dropbox_account_info(current_user.dropbox_token)
if account_info:
current_user.dropbox_id = account_info.get('account_id')
db.session.commit()
return redirect(url_for('users.profile'))
```
## Token Management
### Token Refresh
Tokens are refreshed when they expire. The Dropbox implementation uses:
```python
def get_current_user_dropbox_token():
"""Get a valid Dropbox access token for the current user, refreshing if needed"""
if not current_user or not current_user.is_authenticated:
return None
# Check if token exists and is valid
if (current_user.dropbox_token and
current_user.dropbox_token_expiry and
current_user.dropbox_token_expiry > datetime.now() + timedelta(minutes=5)):
return current_user.dropbox_token
# Token is missing or about to expire - try to refresh
if current_user.dropbox_refresh_token:
# Refresh the token
token_info = refresh_dropbox_token(current_user.dropbox_refresh_token)
if token_info and 'access_token' in token_info:
# Update token in database
current_user.dropbox_token = token_info['access_token']
expires_in = token_info.get('expires_in', 14400)
current_user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)
db.session.commit()
return current_user.dropbox_token
return None
```
### Token Revocation
Users can disconnect their Dropbox accounts:
```python
@users_bp.route('/dropbox/disconnect', methods=['POST'])
@login_required
def dropbox_disconnect():
"""Disconnect user's Dropbox account"""
# Revoke token if present
if current_user.dropbox_token:
try:
revoke_token(current_user.dropbox_token)
except Exception as e:
# Log the error but continue
pass
# Clear Dropbox credentials
current_user.dropbox_token = None
current_user.dropbox_refresh_token = None
current_user.dropbox_token_expiry = None
current_user.dropbox_id = None
db.session.commit()
return redirect(url_for('users.profile'))
```
## Security Considerations
When working with OAuth:
- Always use HTTPS in production
- Store tokens securely
- Implement proper token refresh
- Handle token revocation when users disconnect accounts
- Request minimal scope access
- Validate all OAuth-related inputs
- Use the official provider documentation for the most up-to-date OAuth implementation details