mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
Reorganize repository structure for better organization
- Move backend code to swingmusic/ folder - Move client applications to root level (swingmusic-android, swingmusic-desktop, swingmusic-webclient) - Remove intermediate backend/ and clients/ folders - Update README with new folder structure and setup instructions - Clean and organized repository layout
This commit is contained in:
@@ -1,621 +0,0 @@
|
||||
"""
|
||||
Mobile Offline Mode API Endpoints
|
||||
|
||||
This module provides REST API endpoints for mobile offline functionality,
|
||||
including device management, sync operations, and offline library access.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
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.mobile_offline_service import mobile_offline_service, OfflineQuality, SyncStatus
|
||||
from swingmusic.utils.request import APIError, success_response, error_response
|
||||
from swingmusic.utils.validators import validate_device_info, validate_track_ids
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mobile_offline_bp = Blueprint('mobile_offline', __name__, url_prefix='/api/mobile-offline')
|
||||
|
||||
|
||||
def get_current_user_id() -> int:
|
||||
"""Get current user ID from Flask-Login"""
|
||||
return current_user.id if current_user.is_authenticated else None
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/register', methods=['POST'])
|
||||
@login_required
|
||||
async def register_device():
|
||||
"""
|
||||
Register a new mobile device for offline sync
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"name": "iPhone 14 Pro",
|
||||
"type": "ios",
|
||||
"storage_capacity": 256000000000,
|
||||
"available_storage": 128000000000,
|
||||
"preferences": {
|
||||
"auto_sync": true,
|
||||
"wifi_only": true,
|
||||
"quality": "balanced"
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
# Validate device information
|
||||
device_info = validate_device_info(data)
|
||||
|
||||
# Register device
|
||||
device = await mobile_offline_service.register_device(user_id, device_info)
|
||||
|
||||
return success_response({
|
||||
'message': 'Device registered successfully',
|
||||
'device': {
|
||||
'device_id': device.device_id,
|
||||
'name': device.device_name,
|
||||
'type': device.device_type,
|
||||
'storage_capacity': device.storage_capacity,
|
||||
'available_storage': device.available_storage,
|
||||
'offline_quality': device.offline_quality.value,
|
||||
'auto_sync_enabled': device.auto_sync_enabled,
|
||||
'sync_status': device.sync_status.value,
|
||||
'created_at': device.created_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error registering device: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices', methods=['GET'])
|
||||
@login_required
|
||||
async def get_user_devices():
|
||||
"""
|
||||
Get all registered devices for the current user
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
# This would get all devices for the user from database
|
||||
# For now, return empty list as placeholder
|
||||
devices = []
|
||||
|
||||
return success_response({
|
||||
'devices': devices,
|
||||
'total_count': len(devices)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user devices: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>', methods=['GET'])
|
||||
@login_required
|
||||
async def get_device_info(device_id: str):
|
||||
"""
|
||||
Get specific device information
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
device = await mobile_offline_service._get_device(device_id, user_id)
|
||||
if not device:
|
||||
return error_response("Device not found", 404)
|
||||
|
||||
return success_response({
|
||||
'device': {
|
||||
'device_id': device.device_id,
|
||||
'name': device.device_name,
|
||||
'type': device.device_type,
|
||||
'storage_capacity': device.storage_capacity,
|
||||
'available_storage': device.available_storage,
|
||||
'last_sync': device.last_sync.isoformat() if device.last_sync else None,
|
||||
'sync_status': device.sync_status.value,
|
||||
'offline_quality': device.offline_quality.value,
|
||||
'auto_sync_enabled': device.auto_sync_enabled,
|
||||
'sync_preferences': device.sync_preferences,
|
||||
'created_at': device.created_at.isoformat(),
|
||||
'updated_at': device.updated_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting device info: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/settings', methods=['PUT'])
|
||||
@login_required
|
||||
async def update_device_settings(device_id: str):
|
||||
"""
|
||||
Update device settings
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"offline_quality": "high_quality",
|
||||
"auto_sync_enabled": true,
|
||||
"sync_preferences": {
|
||||
"wifi_only": true,
|
||||
"auto_cleanup": true
|
||||
},
|
||||
"available_storage": 120000000000
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
# Validate settings
|
||||
if 'offline_quality' in data:
|
||||
try:
|
||||
OfflineQuality(data['offline_quality'])
|
||||
except ValueError:
|
||||
return error_response("Invalid offline quality", 400)
|
||||
|
||||
# Update settings
|
||||
success = await mobile_offline_service.update_device_settings(user_id, device_id, data)
|
||||
|
||||
if not success:
|
||||
return error_response("Failed to update device settings", 500)
|
||||
|
||||
return success_response({
|
||||
'message': 'Device settings updated successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating device settings: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/offline-library', methods=['GET'])
|
||||
@login_required
|
||||
async def get_offline_library(device_id: str):
|
||||
"""
|
||||
Get offline library for device
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
|
||||
Query Parameters:
|
||||
- include_tracks: Include track details (default: true)
|
||||
- include_queue: Include sync queue status (default: true)
|
||||
- include_storage: Include storage usage (default: true)
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
# Parse include flags
|
||||
include_flags = {
|
||||
'tracks': request.args.get('include_tracks', 'true').lower() == 'true',
|
||||
'queue': request.args.get('include_queue', 'true').lower() == 'true',
|
||||
'storage': request.args.get('include_storage', 'true').lower() == 'true'
|
||||
}
|
||||
|
||||
# Get offline library
|
||||
library_data = await mobile_offline_service.get_offline_library(user_id, device_id)
|
||||
|
||||
# Build response based on include flags
|
||||
response_data = {
|
||||
'device': library_data['device'],
|
||||
'last_sync': library_data['last_sync'],
|
||||
'sync_status': library_data['sync_status']
|
||||
}
|
||||
|
||||
if include_flags['tracks']:
|
||||
response_data['offline_tracks'] = library_data['offline_tracks']
|
||||
|
||||
if include_flags['queue']:
|
||||
response_data['sync_queue'] = library_data['sync_queue']
|
||||
|
||||
if include_flags['storage']:
|
||||
response_data['storage_usage'] = library_data['storage_usage']
|
||||
|
||||
return success_response({
|
||||
'offline_library': response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting offline library: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/add-tracks', methods=['POST'])
|
||||
@login_required
|
||||
async def add_tracks_to_offline(device_id: str):
|
||||
"""
|
||||
Add tracks to offline library
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"track_ids": ["track1", "track2", "track3"],
|
||||
"quality": "high_quality"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
track_ids = data.get('track_ids', [])
|
||||
if not track_ids:
|
||||
return error_response("track_ids are required", 400)
|
||||
|
||||
# Validate track IDs
|
||||
validate_track_ids(track_ids)
|
||||
|
||||
# Parse quality
|
||||
quality = None
|
||||
if 'quality' in data:
|
||||
try:
|
||||
quality = OfflineQuality(data['quality'])
|
||||
except ValueError:
|
||||
return error_response("Invalid quality", 400)
|
||||
|
||||
# Add tracks to offline library
|
||||
queue_items = await mobile_offline_service.add_to_offline_library(
|
||||
user_id, device_id, track_ids, quality
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'message': f'Added {len(queue_items)} tracks to offline library',
|
||||
'queue_items': [
|
||||
{
|
||||
'queue_id': item.queue_id,
|
||||
'track_id': item.track_id,
|
||||
'priority': item.priority,
|
||||
'quality': item.quality,
|
||||
'status': item.status,
|
||||
'added_at': item.added_at.isoformat()
|
||||
}
|
||||
for item in queue_items
|
||||
]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding tracks to offline library: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/sync-playlist/<playlist_id>', methods=['POST'])
|
||||
@login_required
|
||||
async def sync_playlist_offline(device_id: str, playlist_id: str):
|
||||
"""
|
||||
Sync entire playlist for offline playback
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
- playlist_id: Playlist ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"quality": "balanced"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Parse quality
|
||||
quality = None
|
||||
if 'quality' in data:
|
||||
try:
|
||||
quality = OfflineQuality(data['quality'])
|
||||
except ValueError:
|
||||
return error_response("Invalid quality", 400)
|
||||
|
||||
# Sync playlist
|
||||
queue_items = await mobile_offline_service.sync_playlist_offline(
|
||||
user_id, device_id, playlist_id, quality
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'message': f'Playlist sync started with {len(queue_items)} tracks',
|
||||
'queue_items': [
|
||||
{
|
||||
'queue_id': item.queue_id,
|
||||
'track_id': item.track_id,
|
||||
'priority': item.priority,
|
||||
'quality': item.quality,
|
||||
'status': item.status,
|
||||
'added_at': item.added_at.isoformat()
|
||||
}
|
||||
for item in queue_items
|
||||
]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing playlist offline: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/remove-tracks', methods=['POST'])
|
||||
@login_required
|
||||
async def remove_tracks_from_offline(device_id: str):
|
||||
"""
|
||||
Remove tracks from offline library
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"track_ids": ["track1", "track2", "track3"]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
track_ids = data.get('track_ids', [])
|
||||
if not track_ids:
|
||||
return error_response("track_ids are required", 400)
|
||||
|
||||
# Validate track IDs
|
||||
validate_track_ids(track_ids)
|
||||
|
||||
# Remove tracks
|
||||
success = await mobile_offline_service.remove_from_offline_library(
|
||||
user_id, device_id, track_ids
|
||||
)
|
||||
|
||||
if not success:
|
||||
return error_response("Failed to remove tracks from offline library", 500)
|
||||
|
||||
return success_response({
|
||||
'message': f'Removed {len(track_ids)} tracks from offline library'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing tracks from offline library: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/sync-progress', methods=['GET'])
|
||||
@login_required
|
||||
async def get_sync_progress(device_id: str):
|
||||
"""
|
||||
Get sync progress for device
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
progress_data = await mobile_offline_service.get_sync_progress(user_id, device_id)
|
||||
|
||||
return success_response({
|
||||
'sync_progress': progress_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting sync progress: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/force-sync', methods=['POST'])
|
||||
@login_required
|
||||
async def force_sync_now(device_id: str):
|
||||
"""
|
||||
Force immediate sync for device
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
success = await mobile_offline_service.force_sync_now(user_id, device_id)
|
||||
|
||||
if not success:
|
||||
return error_response("Failed to force sync", 500)
|
||||
|
||||
return success_response({
|
||||
'message': 'Sync started successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error forcing sync: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/storage-info', methods=['GET'])
|
||||
@login_required
|
||||
async def get_storage_info(device_id: str):
|
||||
"""
|
||||
Get detailed storage information for device
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
|
||||
# Get device info
|
||||
device = await mobile_offline_service._get_device(device_id, user_id)
|
||||
if not device:
|
||||
return error_response("Device not found", 404)
|
||||
|
||||
# Get storage usage
|
||||
storage_usage = await mobile_offline_service._get_storage_usage(device_id)
|
||||
|
||||
# Calculate additional info
|
||||
usage_percentage = (storage_usage.used_space / storage_usage.total_capacity * 100) if storage_usage.total_capacity > 0 else 0
|
||||
|
||||
return success_response({
|
||||
'storage_info': {
|
||||
'total_capacity': storage_usage.total_capacity,
|
||||
'used_space': storage_usage.used_space,
|
||||
'available_space': storage_usage.available_space,
|
||||
'usage_percentage': round(usage_percentage, 2),
|
||||
'offline_tracks_count': storage_usage.offline_tracks_count,
|
||||
'offline_tracks_size': storage_usage.offline_tracks_size,
|
||||
'other_data_size': storage_usage.other_data_size,
|
||||
'quality_breakdown': storage_usage.quality_breakdown,
|
||||
'needs_cleanup': usage_percentage > 90,
|
||||
'recommendations': _get_storage_recommendations(usage_percentage, storage_usage)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting storage info: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/devices/<device_id>/cleanup', methods=['POST'])
|
||||
@login_required
|
||||
async def cleanup_storage(device_id: str):
|
||||
"""
|
||||
Cleanup storage by removing old/unused content
|
||||
|
||||
Path Parameters:
|
||||
- device_id: Device ID
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"strategy": "least_played|oldest|all",
|
||||
"free_space_bytes": 1000000000
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response("Request body is required", 400)
|
||||
|
||||
strategy = data.get('strategy', 'least_played')
|
||||
free_space_bytes = data.get('free_space_bytes', 0)
|
||||
|
||||
# Validate strategy
|
||||
valid_strategies = ['least_played', 'oldest', 'all']
|
||||
if strategy not in valid_strategies:
|
||||
return error_response(f"Invalid strategy. Must be one of: {valid_strategies}", 400)
|
||||
|
||||
# Perform cleanup
|
||||
# This would implement the actual cleanup logic
|
||||
freed_space = await mobile_offline_service._cleanup_old_content(device_id, free_space_bytes)
|
||||
|
||||
return success_response({
|
||||
'message': f'Cleanup completed',
|
||||
'freed_space': freed_space,
|
||||
'strategy_used': strategy
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
@mobile_offline_bp.route('/quality-presets', methods=['GET'])
|
||||
@login_required
|
||||
async def get_quality_presets():
|
||||
"""
|
||||
Get available quality presets for offline downloads
|
||||
"""
|
||||
try:
|
||||
presets = {
|
||||
'space_saver': {
|
||||
'name': 'Space Saver',
|
||||
'description': 'Low quality, maximum storage efficiency',
|
||||
'estimated_size_per_track': '3MB',
|
||||
'recommended_for': 'Limited storage, large libraries',
|
||||
'formats': ['MP3 128kbps', 'AAC 128kbps']
|
||||
},
|
||||
'balanced': {
|
||||
'name': 'Balanced',
|
||||
'description': 'Medium quality, good balance',
|
||||
'estimated_size_per_track': '6MB',
|
||||
'recommended_for': 'Most users, good quality',
|
||||
'formats': ['MP3 256kbps', 'AAC 256kbps']
|
||||
},
|
||||
'high_quality': {
|
||||
'name': 'High Quality',
|
||||
'description': 'High quality, more storage usage',
|
||||
'estimated_size_per_track': '12MB',
|
||||
'recommended_for': 'Audiophiles, premium headphones',
|
||||
'formats': ['MP3 320kbps', 'AAC 320kbps', 'OGG Vorbis']
|
||||
},
|
||||
'lossless': {
|
||||
'name': 'Lossless',
|
||||
'description': 'Lossless quality, maximum storage usage',
|
||||
'estimated_size_per_track': '30MB',
|
||||
'recommended_for': 'Critical listening, unlimited storage',
|
||||
'formats': ['FLAC', 'ALAC', 'WAV']
|
||||
}
|
||||
}
|
||||
|
||||
return success_response({
|
||||
'quality_presets': presets
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality presets: {e}")
|
||||
return error_response("Internal server error", 500)
|
||||
|
||||
|
||||
def _get_storage_recommendations(usage_percentage: float, storage_usage) -> List[str]:
|
||||
"""Get storage recommendations based on usage"""
|
||||
recommendations = []
|
||||
|
||||
if usage_percentage > 95:
|
||||
recommendations.extend([
|
||||
"Critical: Storage almost full",
|
||||
"Remove least played tracks immediately",
|
||||
"Consider upgrading to higher capacity device"
|
||||
])
|
||||
elif usage_percentage > 90:
|
||||
recommendations.extend([
|
||||
"Storage nearly full",
|
||||
"Enable auto-cleanup settings",
|
||||
"Remove old or rarely played tracks"
|
||||
])
|
||||
elif usage_percentage > 80:
|
||||
recommendations.extend([
|
||||
"Storage getting full",
|
||||
"Consider using space saver quality",
|
||||
"Review offline library regularly"
|
||||
])
|
||||
elif usage_percentage > 70:
|
||||
recommendations.extend([
|
||||
"Moderate storage usage",
|
||||
"Monitor storage regularly",
|
||||
"Consider quality adjustments"
|
||||
])
|
||||
else:
|
||||
recommendations.extend([
|
||||
"Storage usage is healthy",
|
||||
"Continue current settings",
|
||||
"Consider adding more content if desired"
|
||||
])
|
||||
|
||||
return recommendations
|
||||
Reference in New Issue
Block a user