Files
Tomas Dvorak 6e8fedf534 first commit
2026-04-13 17:46:58 +02:00

323 lines
10 KiB
Python

"""Mobile offline sync API."""
from __future__ import annotations
from typing import Any
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from swingmusic.services.mobile_offline_service import mobile_offline_service
from swingmusic.utils.auth import get_current_userid
mobile_offline_bp = Blueprint(
"mobile_offline", __name__, url_prefix="/api/mobile-offline"
)
def _ok(payload: dict[str, Any], status: int = 200):
return payload, status
def _fail(message: str, status: int = 400):
return {"error": message}, status
@mobile_offline_bp.post("/devices/register")
@jwt_required()
def register_device():
body = request.get_json(silent=True) or {}
userid = get_current_userid()
try:
device = mobile_offline_service.register_device(userid, body)
except Exception as error:
return _fail(f"Failed to register device: {error}", 500)
return _ok({"device": device}, 201)
@mobile_offline_bp.get("/devices")
@jwt_required()
def get_devices():
userid = get_current_userid()
devices = mobile_offline_service.list_devices(userid)
return _ok({"devices": devices, "total_count": len(devices)})
@mobile_offline_bp.get("/devices/<device_id>")
@jwt_required()
def get_device(device_id: str):
userid = get_current_userid()
device = mobile_offline_service.get_device(userid, device_id)
if not device:
return _fail("Device not found", 404)
return _ok({"device": device})
@mobile_offline_bp.put("/devices/<device_id>/settings")
@jwt_required()
def update_device_settings(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
success = mobile_offline_service.update_device_settings(userid, device_id, body)
if not success:
return _fail("Device not found", 404)
return _ok({"success": True})
@mobile_offline_bp.get("/devices/<device_id>/offline-library")
@jwt_required()
def get_offline_library(device_id: str):
userid = get_current_userid()
try:
payload = mobile_offline_service.get_offline_library(userid, device_id)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to get offline library: {error}", 500)
return _ok({"offline_library": payload})
@mobile_offline_bp.post("/devices/<device_id>/add-tracks")
@jwt_required()
def add_tracks_to_offline(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
track_items = body.get("tracks") or body.get("track_ids") or []
if not isinstance(track_items, list) or not track_items:
return _fail("tracks or track_ids must be a non-empty list", 400)
quality = body.get("quality")
collection = body.get("collection")
try:
queue_items = mobile_offline_service.add_to_offline_library(
userid,
device_id,
track_items,
quality=quality,
collection=collection,
)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to add tracks: {error}", 500)
return _ok(
{
"success": True,
"queue_items": queue_items,
"added_count": len(queue_items),
}
)
@mobile_offline_bp.post("/devices/<device_id>/sync-playlist/<playlist_id>")
@jwt_required()
def sync_playlist_offline(device_id: str, playlist_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
try:
queue_items = mobile_offline_service.sync_playlist_offline(
userid,
device_id,
playlist_id,
quality=body.get("quality"),
)
except ValueError as error:
return _fail(str(error), 400)
except Exception as error:
return _fail(f"Failed to sync playlist: {error}", 500)
return _ok(
{"success": True, "queue_items": queue_items, "added_count": len(queue_items)}
)
@mobile_offline_bp.post("/devices/<device_id>/sync-collection")
@jwt_required()
def sync_collection_offline(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
collection_type = str(body.get("collection_type") or "").strip().lower()
collection_id = str(body.get("collection_id") or "").strip()
quality = body.get("quality")
if collection_type not in {"album", "artist", "playlist"}:
return _fail("collection_type must be one of: album, artist, playlist", 400)
if not collection_id:
return _fail("collection_id is required", 400)
trackhashes = mobile_offline_service.tracks_for_collection(
collection_type=collection_type,
collection_id=collection_id,
)
if not trackhashes:
return _fail("No tracks found for collection", 404)
try:
queue_items = mobile_offline_service.add_to_offline_library(
userid,
device_id,
trackhashes,
quality=quality,
collection=f"{collection_type}:{collection_id}",
)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to sync collection: {error}", 500)
return _ok(
{"success": True, "queue_items": queue_items, "added_count": len(queue_items)}
)
@mobile_offline_bp.post("/devices/<device_id>/remove-tracks")
@jwt_required()
def remove_tracks_from_offline(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
trackhashes = body.get("trackhashes") or body.get("track_ids") or []
if not isinstance(trackhashes, list) or not trackhashes:
return _fail("trackhashes or track_ids must be a non-empty list", 400)
success = mobile_offline_service.remove_from_offline_library(
userid, device_id, trackhashes
)
if not success:
return _fail("Device not found", 404)
return _ok({"success": True, "removed_count": len(trackhashes)})
@mobile_offline_bp.get("/devices/<device_id>/sync-progress")
@jwt_required()
def get_sync_progress(device_id: str):
userid = get_current_userid()
try:
progress = mobile_offline_service.get_sync_progress(userid, device_id)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to fetch sync progress: {error}", 500)
return _ok({"sync_progress": progress})
@mobile_offline_bp.post("/devices/<device_id>/force-sync")
@jwt_required()
def force_sync_now(device_id: str):
userid = get_current_userid()
success = mobile_offline_service.force_sync_now(userid, device_id)
if not success:
return _fail("Device not found", 404)
return _ok({"success": True})
@mobile_offline_bp.get("/devices/<device_id>/storage-info")
@jwt_required()
def get_storage_info(device_id: str):
userid = get_current_userid()
try:
usage = mobile_offline_service.get_storage_usage(userid, device_id)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to get storage info: {error}", 500)
usage_percentage = 0.0
if usage.total_capacity > 0:
usage_percentage = round((usage.used_space / usage.total_capacity) * 100.0, 2)
return _ok(
{
"storage_info": {
"total_capacity": usage.total_capacity,
"used_space": usage.used_space,
"available_space": usage.available_space,
"usage_percentage": usage_percentage,
"offline_tracks_count": usage.offline_tracks_count,
"offline_tracks_size": usage.offline_tracks_size,
"other_data_size": usage.other_data_size,
"quality_breakdown": usage.quality_breakdown,
"needs_cleanup": usage_percentage >= 90.0,
}
}
)
@mobile_offline_bp.post("/devices/<device_id>/cleanup")
@jwt_required()
def cleanup_storage(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
strategy = str(body.get("strategy") or "least_played")
if strategy not in {"least_played", "oldest", "all"}:
return _fail("strategy must be one of: least_played, oldest, all", 400)
free_space_bytes = int(body.get("free_space_bytes") or 0)
freed = mobile_offline_service.cleanup_device_content(
userid,
device_id,
strategy=strategy,
free_space_bytes=free_space_bytes,
)
return _ok({"success": True, "freed_space": freed, "strategy": strategy})
@mobile_offline_bp.post("/devices/<device_id>/events/batch")
@jwt_required()
def append_events(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
events = body.get("events")
if not isinstance(events, list):
return _fail("events must be a list", 400)
try:
result = mobile_offline_service.append_events(userid, device_id, events)
except ValueError as error:
return _fail(str(error), 404)
except Exception as error:
return _fail(f"Failed to append events: {error}", 500)
mark_synced = body.get("mark_synced")
if isinstance(mark_synced, list):
mobile_offline_service.mark_events_synced(userid, device_id, mark_synced)
return _ok({"success": True, **result})
@mobile_offline_bp.post("/devices/<device_id>/events/mark-synced")
@jwt_required()
def mark_events_synced(device_id: str):
body = request.get_json(silent=True) or {}
userid = get_current_userid()
event_ids = body.get("event_ids")
if event_ids is not None and not isinstance(event_ids, list):
return _fail("event_ids must be a list", 400)
updated = mobile_offline_service.mark_events_synced(userid, device_id, event_ids)
return _ok({"success": True, "updated": updated})
@mobile_offline_bp.get("/quality-presets")
@jwt_required()
def get_quality_presets():
return _ok({"quality_presets": mobile_offline_service.quality_presets()})