mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 12:33:03 +00:00
Add comprehensive backend services and API enhancements
- Complete Spotify integration with downloader and settings - Advanced UX features and audio quality management - Enhanced search capabilities and mobile offline support - Music catalog browser and recap features - Universal downloader and upload functionality - Update tracking system with database models and migrations - Comprehensive service layer architecture - Enhanced lyrics API and streaming capabilities - Extended application builder and startup configuration - New logging infrastructure and services directory
This commit is contained in:
@@ -0,0 +1,601 @@
|
||||
"""
|
||||
Update Tracking API Endpoints
|
||||
|
||||
This module provides REST API endpoints for the artist update tracking system,
|
||||
including following artists, managing preferences, and getting updates.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from swingmusic.db import db
|
||||
from swingmusic.services.update_tracker import update_tracker, FollowLevel, ReleaseType
|
||||
from swingmusic.utils.request import APIError, success_response, error_response
|
||||
from swingmusic.utils.validators import validate_spotify_id, validate_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
update_tracking_bp = Blueprint('update_tracking', __name__, url_prefix='/api/updates')
|
||||
|
||||
|
||||
def get_current_user_id() -> int:
|
||||
"""Get current user ID from Flask-Login"""
|
||||
return current_user.id if current_user.is_authenticated else None
|
||||
|
||||
|
||||
@update_tracking_bp.route('/follow-artist', methods=['POST'])
|
||||
@login_required
|
||||
async def follow_artist():
|
||||
"""
|
||||
Follow an artist for update tracking
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"artist_id": "spotify_artist_id",
|
||||
"artist_name": "Artist Name",
|
||||
"follow_level": "followed|favorite|casual",
|
||||
"auto_download": false,
|
||||
"preferred_quality": "flac"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
# Validate required fields
|
||||
artist_id = data.get('artist_id')
|
||||
artist_name = data.get('artist_name')
|
||||
|
||||
if not artist_id or not artist_name:
|
||||
return error_response("artist_id and artist_name are required", 400)
|
||||
|
||||
if not validate_spotify_id(artist_id):
|
||||
return error_response("Invalid artist ID format", 400)
|
||||
|
||||
# Validate follow level
|
||||
follow_level = data.get('follow_level', 'followed')
|
||||
if follow_level not in ['casual', 'followed', 'favorite']:
|
||||
return error_response("Invalid follow level. Must be: casual, followed, or favorite", 400)
|
||||
|
||||
# Validate quality preference
|
||||
preferred_quality = data.get('preferred_quality', 'flac')
|
||||
if preferred_quality not in ['flac', 'mp3_320', 'mp3_256', 'aac']:
|
||||
return error_response("Invalid quality preference", 400)
|
||||
|
||||
follow_data = {
|
||||
'user_id': get_current_user_id(),
|
||||
'artist_id': artist_id,
|
||||
'artist_name': artist_name,
|
||||
'follow_level': follow_level,
|
||||
'auto_download': data.get('auto_download', False),
|
||||
'preferred_quality': preferred_quality
|
||||
}
|
||||
|
||||
success = await update_tracker.follow_artist(follow_data)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': f'Now following {artist_name}',
|
||||
'artist_id': artist_id,
|
||||
'follow_level': follow_level
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to follow artist", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error following artist: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/unfollow-artist', methods=['POST'])
|
||||
@login_required
|
||||
async def unfollow_artist():
|
||||
"""
|
||||
Unfollow an artist
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"artist_id": "spotify_artist_id"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('artist_id'):
|
||||
return error_response("artist_id is required", 400)
|
||||
|
||||
artist_id = data['artist_id']
|
||||
|
||||
if not validate_spotify_id(artist_id):
|
||||
return error_response("Invalid artist ID format", 400)
|
||||
|
||||
success = await update_tracker.unfollow_artist(get_current_user_id(), artist_id)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'Artist unfollowed successfully',
|
||||
'artist_id': artist_id
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to unfollow artist", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error unfollowing artist: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/recent', methods=['GET'])
|
||||
@login_required
|
||||
async def get_recent_updates():
|
||||
"""
|
||||
Get recent updates for followed artists
|
||||
|
||||
Query Parameters:
|
||||
- limit: Number of updates to return (default: 20, max: 100)
|
||||
- offset: Offset for pagination (default: 0)
|
||||
- release_type: Filter by release type (album, single, ep, compilation)
|
||||
- unread_only: Only return unread updates (true/false)
|
||||
"""
|
||||
try:
|
||||
limit = min(request.args.get('limit', 20, type=int), 100)
|
||||
offset = request.args.get('offset', 0, type=int)
|
||||
release_type = request.args.get('release_type')
|
||||
unread_only = request.args.get('unread_only', 'false').lower() == 'true'
|
||||
|
||||
# Validate release type
|
||||
if release_type and release_type not in ['album', 'single', 'ep', 'compilation']:
|
||||
return error_response("Invalid release type", 400)
|
||||
|
||||
updates = await update_tracker.get_user_updates(
|
||||
get_current_user_id(),
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
release_type=release_type,
|
||||
unread_only=unread_only
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'updates': updates,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'total': len(updates)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent updates: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/settings', methods=['GET'])
|
||||
@login_required
|
||||
async def get_settings():
|
||||
"""
|
||||
Get user's update tracking settings
|
||||
"""
|
||||
try:
|
||||
settings = await update_tracker.get_user_settings(get_current_user_id())
|
||||
return success_response(settings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting settings: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/settings', methods=['POST'])
|
||||
@login_required
|
||||
async def update_settings():
|
||||
"""
|
||||
Update user's update tracking settings
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"enable_artist_monitoring": true,
|
||||
"check_frequency": "daily",
|
||||
"auto_download_favorites": false,
|
||||
"auto_download_followed": false,
|
||||
"max_auto_downloads_per_week": 5,
|
||||
"quality_preference": "flac",
|
||||
"storage_limit_mb": 10240,
|
||||
"notification_channels": {
|
||||
"in_app": true,
|
||||
"push": false,
|
||||
"email": false,
|
||||
"discord": false
|
||||
},
|
||||
"exclude_explicit": false,
|
||||
"preferred_release_types": ["album", "ep", "single"]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
# Validate settings
|
||||
if 'check_frequency' in data and data['check_frequency'] not in ['hourly', 'daily', 'weekly']:
|
||||
return error_response("Invalid check frequency", 400)
|
||||
|
||||
if 'quality_preference' in data and data['quality_preference'] not in ['flac', 'mp3_320', 'mp3_256', 'aac']:
|
||||
return error_response("Invalid quality preference", 400)
|
||||
|
||||
if 'max_auto_downloads_per_week' in data:
|
||||
max_downloads = data['max_auto_downloads_per_week']
|
||||
if not isinstance(max_downloads, int) or max_downloads < 0 or max_downloads > 50:
|
||||
return error_response("Invalid max auto downloads value", 400)
|
||||
|
||||
if 'storage_limit_mb' in data:
|
||||
storage_limit = data['storage_limit_mb']
|
||||
if not isinstance(storage_limit, int) or storage_limit < 100 or storage_limit > 102400:
|
||||
return error_response("Invalid storage limit", 400)
|
||||
|
||||
success = await update_tracker.update_user_settings(get_current_user_id(), data)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'Settings updated successfully',
|
||||
'settings': data
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to update settings", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating settings: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/auto-download/<release_id>', methods=['POST'])
|
||||
@login_required
|
||||
async def auto_download_release(release_id):
|
||||
"""
|
||||
Trigger auto-download for a specific release
|
||||
|
||||
Path Parameters:
|
||||
- release_id: Spotify release ID
|
||||
"""
|
||||
try:
|
||||
if not validate_spotify_id(release_id):
|
||||
return error_response("Invalid release ID format", 400)
|
||||
|
||||
success = await update_tracker.auto_download_release(get_current_user_id(), release_id)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'Download queued successfully',
|
||||
'release_id': release_id
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to queue download", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error auto-downloading release: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/stats', methods=['GET'])
|
||||
@login_required
|
||||
async def get_update_stats():
|
||||
"""
|
||||
Get user's update tracking statistics
|
||||
"""
|
||||
try:
|
||||
stats = await update_tracker.get_user_stats(get_current_user_id())
|
||||
return success_response(stats)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stats: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/followed-artists', methods=['GET'])
|
||||
@login_required
|
||||
async def get_followed_artists():
|
||||
"""
|
||||
Get list of followed artists
|
||||
|
||||
Query Parameters:
|
||||
- limit: Number of artists to return (default: 50, max: 200)
|
||||
- offset: Offset for pagination (default: 0)
|
||||
- follow_level: Filter by follow level (casual, followed, favorite)
|
||||
"""
|
||||
try:
|
||||
limit = min(request.args.get('limit', 50, type=int), 200)
|
||||
offset = request.args.get('offset', 0, type=int)
|
||||
follow_level = request.args.get('follow_level')
|
||||
|
||||
# Validate follow level
|
||||
if follow_level and follow_level not in ['casual', 'followed', 'favorite']:
|
||||
return error_response("Invalid follow level", 400)
|
||||
|
||||
artists = await update_tracker.get_followed_artists(
|
||||
get_current_user_id(),
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
follow_level=follow_level
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'artists': artists,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'total': len(artists)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting followed artists: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/artist/<artist_id>/follow-status', methods=['GET'])
|
||||
@login_required
|
||||
async def get_artist_follow_status(artist_id):
|
||||
"""
|
||||
Get follow status for a specific artist
|
||||
|
||||
Path Parameters:
|
||||
- artist_id: Spotify artist ID
|
||||
"""
|
||||
try:
|
||||
if not validate_spotify_id(artist_id):
|
||||
return error_response("Invalid artist ID format", 400)
|
||||
|
||||
status = await update_tracker.get_artist_follow_status(get_current_user_id(), artist_id)
|
||||
|
||||
if status:
|
||||
return success_response(status)
|
||||
else:
|
||||
return success_response({
|
||||
'is_following': False,
|
||||
'artist_id': artist_id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting artist follow status: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/artist/<artist_id>', methods=['PUT'])
|
||||
@login_required
|
||||
async def update_artist_follow(artist_id):
|
||||
"""
|
||||
Update follow settings for an artist
|
||||
|
||||
Path Parameters:
|
||||
- artist_id: Spotify artist ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"follow_level": "followed|favorite|casual",
|
||||
"auto_download": true,
|
||||
"preferred_quality": "flac",
|
||||
"notification_preferences": {
|
||||
"in_app": true,
|
||||
"push": false,
|
||||
"email": false,
|
||||
"discord": false
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if not validate_spotify_id(artist_id):
|
||||
return error_response("Invalid artist ID format", 400)
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
# Validate follow level
|
||||
if 'follow_level' in data and data['follow_level'] not in ['casual', 'followed', 'favorite']:
|
||||
return error_response("Invalid follow level", 400)
|
||||
|
||||
# Validate quality preference
|
||||
if 'preferred_quality' in data and data['preferred_quality'] not in ['flac', 'mp3_320', 'mp3_256', 'aac']:
|
||||
return error_response("Invalid quality preference", 400)
|
||||
|
||||
success = await update_tracker.update_artist_follow(
|
||||
get_current_user_id(),
|
||||
artist_id,
|
||||
data
|
||||
)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'Artist follow settings updated',
|
||||
'artist_id': artist_id,
|
||||
'settings': data
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to update artist follow settings", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating artist follow: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/release/<release_id>', methods=['GET'])
|
||||
@login_required
|
||||
async def get_release_details(release_id):
|
||||
"""
|
||||
Get details for a specific release update
|
||||
|
||||
Path Parameters:
|
||||
- release_id: Spotify release ID
|
||||
"""
|
||||
try:
|
||||
if not validate_spotify_id(release_id):
|
||||
return error_response("Invalid release ID format", 400)
|
||||
|
||||
release = await update_tracker.get_release_details(get_current_user_id(), release_id)
|
||||
|
||||
if release:
|
||||
return success_response(release)
|
||||
else:
|
||||
return error_response("Release not found", 404)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting release details: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/release/<release_id>/mark-read', methods=['POST'])
|
||||
@login_required
|
||||
async def mark_release_read(release_id):
|
||||
"""
|
||||
Mark a release update as read
|
||||
|
||||
Path Parameters:
|
||||
- release_id: Spotify release ID
|
||||
"""
|
||||
try:
|
||||
if not validate_spotify_id(release_id):
|
||||
return error_response("Invalid release ID format", 400)
|
||||
|
||||
success = await update_tracker.mark_release_read(get_current_user_id(), release_id)
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'Release marked as read',
|
||||
'release_id': release_id
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to mark release as read", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking release as read: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/notifications', methods=['GET'])
|
||||
@login_required
|
||||
async def get_notifications():
|
||||
"""
|
||||
Get user's update notifications
|
||||
|
||||
Query Parameters:
|
||||
- limit: Number of notifications to return (default: 20, max: 100)
|
||||
- offset: Offset for pagination (default: 0)
|
||||
- unread_only: Only return unread notifications (true/false)
|
||||
"""
|
||||
try:
|
||||
limit = min(request.args.get('limit', 20, type=int), 100)
|
||||
offset = request.args.get('offset', 0, type=int)
|
||||
unread_only = request.args.get('unread_only', 'false').lower() == 'true'
|
||||
|
||||
notifications = await update_tracker.get_notifications(
|
||||
get_current_user_id(),
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
unread_only=unread_only
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'notifications': notifications,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'total': len(notifications)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting notifications: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/notifications/mark-all-read', methods=['POST'])
|
||||
@login_required
|
||||
async def mark_all_notifications_read():
|
||||
"""
|
||||
Mark all notifications as read for the user
|
||||
"""
|
||||
try:
|
||||
success = await update_tracker.mark_all_notifications_read(get_current_user_id())
|
||||
|
||||
if success:
|
||||
return success_response({
|
||||
'message': 'All notifications marked as read'
|
||||
})
|
||||
else:
|
||||
return error_response("Failed to mark notifications as read", 500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking all notifications as read: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/search/artists', methods=['GET'])
|
||||
@login_required
|
||||
async def search_artists_to_follow():
|
||||
"""
|
||||
Search for artists to follow
|
||||
|
||||
Query Parameters:
|
||||
- q: Search query
|
||||
- limit: Number of results to return (default: 10, max: 50)
|
||||
"""
|
||||
try:
|
||||
query = request.args.get('q')
|
||||
if not query:
|
||||
return error_response("Search query is required", 400)
|
||||
|
||||
limit = min(request.args.get('limit', 10, type=int), 50)
|
||||
|
||||
artists = await update_tracker.search_artists(query, limit)
|
||||
|
||||
return success_response({
|
||||
'artists': artists,
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'total': len(artists)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching artists: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@update_tracking_bp.route('/export/followed-artists', methods=['GET'])
|
||||
@login_required
|
||||
async def export_followed_artists():
|
||||
"""
|
||||
Export followed artists as JSON or CSV
|
||||
|
||||
Query Parameters:
|
||||
- format: Export format (json|csv) - default: json
|
||||
"""
|
||||
try:
|
||||
export_format = request.args.get('format', 'json').lower()
|
||||
|
||||
if export_format not in ['json', 'csv']:
|
||||
return error_response("Invalid export format. Must be json or csv", 400)
|
||||
|
||||
data = await update_tracker.export_followed_artists(get_current_user_id(), export_format)
|
||||
|
||||
if export_format == 'csv':
|
||||
from flask import Response
|
||||
return Response(
|
||||
data,
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': 'attachment; filename=followed_artists.csv'}
|
||||
)
|
||||
else:
|
||||
return success_response({'followed_artists': data})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting followed artists: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
# Error handlers
|
||||
@update_tracking_bp.errorhandler(404)
|
||||
def not_found(error):
|
||||
return error_response("Endpoint not found", 404)
|
||||
|
||||
|
||||
@update_tracking_bp.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return error_response("Internal server error", 500)
|
||||
Reference in New Issue
Block a user