mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
4338dd1d9c
- 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
602 lines
19 KiB
Python
602 lines
19 KiB
Python
"""
|
|
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)
|