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:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user