Merge branch 'dev' into fixup-bulk-add-celery

This commit is contained in:
Phlogi
2025-08-27 21:17:47 +02:00
committed by GitHub
26 changed files with 1617 additions and 1141 deletions

View File

@@ -5,11 +5,12 @@ import uuid
import time
from routes.utils.celery_queue_manager import download_queue_manager
from routes.utils.celery_tasks import store_task_info, store_task_status, ProgressState
from routes.utils.get_info import get_spotify_info
from routes.utils.get_info import get_client, get_album
from routes.utils.errors import DuplicateDownloadError
# Import authentication dependencies
from routes.auth.middleware import require_auth_from_state, User
# Config and credentials helpers
router = APIRouter()
@@ -34,7 +35,8 @@ async def handle_download(
# Fetch metadata from Spotify
try:
album_info = get_spotify_info(album_id, "album")
client = get_client()
album_info = get_album(client, album_id)
if (
not album_info
or not album_info.get("name")
@@ -155,6 +157,7 @@ async def get_album_info(
"""
Retrieve Spotify album metadata given a Spotify album ID.
Expects a query parameter 'id' that contains the Spotify album ID.
Returns the raw JSON from get_album in routes.utils.get_info.
"""
spotify_id = request.query_params.get("id")
@@ -162,27 +165,9 @@ async def get_album_info(
return JSONResponse(content={"error": "Missing parameter: id"}, status_code=400)
try:
# Optional pagination params for tracks
limit_param = request.query_params.get("limit")
offset_param = request.query_params.get("offset")
limit = int(limit_param) if limit_param is not None else None
offset = int(offset_param) if offset_param is not None else None
# Fetch album metadata
album_info = get_spotify_info(spotify_id, "album")
# Fetch album tracks with pagination
album_tracks = get_spotify_info(
spotify_id, "album_tracks", limit=limit, offset=offset
)
# Merge tracks into album payload in the same shape Spotify returns on album
album_info["tracks"] = album_tracks
client = get_client()
album_info = get_album(client, spotify_id)
return JSONResponse(content=album_info, status_code=200)
except ValueError as ve:
return JSONResponse(
content={"error": f"Invalid limit/offset: {str(ve)}"}, status_code=400
)
except Exception as e:
error_data = {"error": str(e), "traceback": traceback.format_exc()}
return JSONResponse(content=error_data, status_code=500)

View File

@@ -18,10 +18,9 @@ from routes.utils.watch.db import (
get_watched_artists,
add_specific_albums_to_artist_table,
remove_specific_albums_from_artist_table,
is_album_in_artist_db,
)
from routes.utils.watch.manager import check_watched_artists, get_watch_config
from routes.utils.get_info import get_spotify_info
from routes.utils.get_info import get_client, get_artist, get_album
# Import authentication dependencies
from routes.auth.middleware import require_auth_from_state, User
@@ -66,9 +65,6 @@ async def handle_artist_download(
)
try:
# Import and call the updated download_artist_albums() function.
# from routes.utils.artist import download_artist_albums # Already imported at top
# Delegate to the download_artist_albums function which will handle album filtering
successfully_queued_albums, duplicate_albums = download_artist_albums(
url=url,
@@ -118,13 +114,15 @@ async def cancel_artist_download():
@router.get("/info")
async def get_artist_info(
request: Request, current_user: User = Depends(require_auth_from_state),
limit: int = Query(10, ge=1), # default=10, must be >=1
offset: int = Query(0, ge=0) # default=0, must be >=0
request: Request,
current_user: User = Depends(require_auth_from_state),
limit: int = Query(10, ge=1), # default=10, must be >=1
offset: int = Query(0, ge=0), # default=0, must be >=0
):
"""
Retrieves Spotify artist metadata given a Spotify artist ID.
Expects a query parameter 'id' with the Spotify artist ID.
Returns the raw JSON from get_artist in routes.utils.get_info.
"""
spotify_id = request.query_params.get("id")
@@ -132,37 +130,8 @@ async def get_artist_info(
return JSONResponse(content={"error": "Missing parameter: id"}, status_code=400)
try:
# Get artist metadata first
artist_metadata = get_spotify_info(spotify_id, "artist")
# Get artist discography for albums
artist_discography = get_spotify_info(spotify_id, "artist_discography", limit=limit, offset=offset)
# Combine metadata with discography
artist_info = {**artist_metadata, "albums": artist_discography}
# If artist_info is successfully fetched and has albums,
# check if the artist is watched and augment album items with is_locally_known status
if (
artist_info
and artist_info.get("albums")
and artist_info["albums"].get("items")
):
watched_artist_details = get_watched_artist(
spotify_id
) # spotify_id is the artist ID
if watched_artist_details: # Artist is being watched
for album_item in artist_info["albums"]["items"]:
if album_item and album_item.get("id"):
album_id = album_item["id"]
album_item["is_locally_known"] = is_album_in_artist_db(
spotify_id, album_id
)
elif album_item: # Album object exists but no ID
album_item["is_locally_known"] = False
# If not watched, or no albums, is_locally_known will not be added.
# Frontend should handle absence of this key as false.
client = get_client()
artist_info = get_artist(client, spotify_id)
return JSONResponse(content=artist_info, status_code=200)
except Exception as e:
return JSONResponse(
@@ -191,15 +160,9 @@ async def add_artist_to_watchlist(
if get_watched_artist(artist_spotify_id):
return {"message": f"Artist {artist_spotify_id} is already being watched."}
# Get artist metadata directly for name and basic info
artist_metadata = get_spotify_info(artist_spotify_id, "artist")
client = get_client()
artist_metadata = get_artist(client, artist_spotify_id)
# Get artist discography for album count
artist_album_list_data = get_spotify_info(
artist_spotify_id, "artist_discography"
)
# Check if we got artist metadata
if not artist_metadata or not artist_metadata.get("name"):
logger.error(
f"Could not fetch artist metadata for {artist_spotify_id} from Spotify."
@@ -211,24 +174,22 @@ async def add_artist_to_watchlist(
},
)
# Check if we got album data
if not artist_album_list_data or not isinstance(
artist_album_list_data.get("items"), list
# Derive a rough total album count from groups if present
total_albums = 0
for key in (
"album_group",
"single_group",
"compilation_group",
"appears_on_group",
):
logger.warning(
f"Could not fetch album list details for artist {artist_spotify_id} from Spotify. Proceeding with metadata only."
)
grp = artist_metadata.get(key)
if isinstance(grp, list):
total_albums += len(grp)
# Construct the artist_data object expected by add_artist_db
artist_data_for_db = {
"id": artist_spotify_id,
"name": artist_metadata.get("name", "Unknown Artist"),
"albums": { # Mimic structure if add_artist_db expects it for total_albums
"total": artist_album_list_data.get("total", 0)
if artist_album_list_data
else 0
},
# Add any other fields add_artist_db might expect from a true artist object if necessary
"albums": {"total": total_albums},
}
add_artist_db(artist_data_for_db)
@@ -446,21 +407,25 @@ async def mark_albums_as_known_for_artist(
detail={"error": f"Artist {artist_spotify_id} is not being watched."},
)
client = get_client()
fetched_albums_details = []
for album_id in album_ids:
try:
# We need full album details. get_spotify_info with type "album" should provide this.
album_detail = get_spotify_info(album_id, "album")
if album_detail and album_detail.get("id"):
fetched_albums_details.append(album_detail)
else:
logger.warning(
f"Could not fetch details for album {album_id} when marking as known for artist {artist_spotify_id}."
try:
for album_id in album_ids:
try:
album_detail = get_album(client, album_id)
if album_detail and album_detail.get("id"):
fetched_albums_details.append(album_detail)
else:
logger.warning(
f"Could not fetch details for album {album_id} when marking as known for artist {artist_spotify_id}."
)
except Exception as e:
logger.error(
f"Failed to fetch Spotify details for album {album_id}: {e}"
)
except Exception as e:
logger.error(
f"Failed to fetch Spotify details for album {album_id}: {e}"
)
finally:
# No need to close_client here, as get_client is shared
pass
if not fetched_albums_details:
return {

View File

@@ -11,25 +11,42 @@ from routes.auth.middleware import require_auth_from_state, User
from routes.utils.get_info import get_spotify_info
from routes.utils.celery_queue_manager import download_queue_manager
# Assuming these imports are available for queue management and Spotify info
from routes.utils.get_info import (
get_client,
get_track,
get_album,
get_playlist,
get_artist,
)
router = APIRouter()
logger = logging.getLogger(__name__)
class BulkAddLinksRequest(BaseModel):
links: List[str]
@router.post("/bulk-add-spotify-links")
async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, current_user: User = Depends(require_auth_from_state)):
added_count = 0
failed_links = []
total_links = len(request.links)
client = get_client()
for link in request.links:
# Assuming links are pre-filtered by the frontend,
# but still handle potential errors during info retrieval or unsupported types
# Extract type and ID from the link directly using regex
match = re.match(r"https://open\.spotify\.com(?:/intl-[a-z]{2})?/(track|album|playlist|artist)/([a-zA-Z0-9]+)(?:\?.*)?", link)
match = re.match(
r"https://open\.spotify\.com(?:/intl-[a-z]{2})?/(track|album|playlist|artist)/([a-zA-Z0-9]+)(?:\?.*)?",
link,
)
if not match:
logger.warning(f"Could not parse Spotify link (unexpected format after frontend filter): {link}")
logger.warning(
f"Could not parse Spotify link (unexpected format after frontend filter): {link}"
)
failed_links.append(link)
continue
@@ -39,18 +56,30 @@ async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, cur
try:
# Get basic info to confirm existence and get name/artist
# For playlists, we might want to get full info later when adding to queue
if spotify_type == "playlist":
item_info = get_spotify_info(spotify_id, "playlist_metadata")
item_info = get_playlist(client, spotify_id, expand_items=False)
elif spotify_type == "track":
item_info = get_track(client, spotify_id)
elif spotify_type == "album":
item_info = get_album(client, spotify_id)
elif spotify_type == "artist":
# Not queued below, but fetch to validate link and name if needed
item_info = get_artist(client, spotify_id)
else:
item_info = get_spotify_info(spotify_id, spotify_type)
logger.warning(
f"Unsupported Spotify type: {spotify_type} for link: {link}"
)
failed_links.append(link)
continue
item_name = item_info.get("name", "Unknown Name")
artist_name = ""
if spotify_type in ["track", "album"]:
artists = item_info.get("artists", [])
if artists:
artist_name = ", ".join([a.get("name", "Unknown Artist") for a in artists])
artist_name = ", ".join(
[a.get("name", "Unknown Artist") for a in artists]
)
elif spotify_type == "playlist":
owner = item_info.get("owner", {})
artist_name = owner.get("display_name", "Unknown Owner")
@@ -77,8 +106,16 @@ async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, cur
added_count += 1
logger.debug(f"Added {added_count}/{total_links} {spotify_type} '{item_name}' ({spotify_id}) to queue with task_id: {task_id}.")
else:
logger.warning(f"Failed to add {spotify_type} '{item_name}' ({spotify_id}) to queue.")
logger.warning(
f"Unsupported Spotify type for download: {spotify_type} for link: {link}"
)
failed_links.append(link)
continue
added_count += 1
logger.debug(
f"Added {added_count + 1}/{total_links} {spotify_type} '{item_name}' ({spotify_id}) to queue."
)
except Exception as e:
logger.error(f"Error processing Spotify link {link}: {e}", exc_info=True)

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.responses import JSONResponse
import json
import traceback
import logging # Added logging import
import uuid # For generating error task IDs
@@ -20,10 +19,9 @@ from routes.utils.watch.db import (
get_watched_playlist,
get_watched_playlists,
add_specific_tracks_to_playlist_table,
remove_specific_tracks_from_playlist_table,
is_track_in_playlist_db, # Added import
remove_specific_tracks_from_playlist_table, # Added import
)
from routes.utils.get_info import get_spotify_info # Already used, but ensure it's here
from routes.utils.get_info import get_client, get_playlist, get_track
from routes.utils.watch.manager import (
check_watched_playlists,
get_watch_config,
@@ -31,7 +29,9 @@ from routes.utils.watch.manager import (
from routes.utils.errors import DuplicateDownloadError
# Import authentication dependencies
from routes.auth.middleware import require_auth_from_state, require_admin_from_state, User
from routes.auth.middleware import require_auth_from_state, User
from routes.utils.celery_config import get_config_params
from routes.utils.credentials import get_spotify_blob_path
logger = logging.getLogger(__name__) # Added logger initialization
router = APIRouter()
@@ -43,7 +43,11 @@ def construct_spotify_url(item_id: str, item_type: str = "track") -> str:
@router.get("/download/{playlist_id}")
async def handle_download(playlist_id: str, request: Request, current_user: User = Depends(require_auth_from_state)):
async def handle_download(
playlist_id: str,
request: Request,
current_user: User = Depends(require_auth_from_state),
):
# Retrieve essential parameters from the request.
# name = request.args.get('name') # Removed
# artist = request.args.get('artist') # Removed
@@ -51,11 +55,14 @@ async def handle_download(playlist_id: str, request: Request, current_user: User
# Construct the URL from playlist_id
url = construct_spotify_url(playlist_id, "playlist")
orig_params["original_url"] = str(request.url) # Update original_url to the constructed one
orig_params["original_url"] = str(
request.url
) # Update original_url to the constructed one
# Fetch metadata from Spotify using optimized function
try:
from routes.utils.get_info import get_playlist_metadata
playlist_info = get_playlist_metadata(playlist_id)
if (
not playlist_info
@@ -66,7 +73,7 @@ async def handle_download(playlist_id: str, request: Request, current_user: User
content={
"error": f"Could not retrieve metadata for playlist ID: {playlist_id}"
},
status_code=404
status_code=404,
)
name_from_spotify = playlist_info.get("name")
@@ -79,14 +86,13 @@ async def handle_download(playlist_id: str, request: Request, current_user: User
content={
"error": f"Failed to fetch metadata for playlist {playlist_id}: {str(e)}"
},
status_code=500
status_code=500,
)
# Validate required parameters
if not url: # This check might be redundant now but kept for safety
return JSONResponse(
content={"error": "Missing required parameter: url"},
status_code=400
content={"error": "Missing required parameter: url"}, status_code=400
)
try:
@@ -106,7 +112,7 @@ async def handle_download(playlist_id: str, request: Request, current_user: User
"error": "Duplicate download detected.",
"existing_task": e.existing_task,
},
status_code=409
status_code=409,
)
except Exception as e:
# Generic error handling for other issues during task submission
@@ -136,25 +142,23 @@ async def handle_download(playlist_id: str, request: Request, current_user: User
"error": f"Failed to queue playlist download: {str(e)}",
"task_id": error_task_id,
},
status_code=500
status_code=500,
)
return JSONResponse(
content={"task_id": task_id},
status_code=202
)
return JSONResponse(content={"task_id": task_id}, status_code=202)
@router.get("/download/cancel")
async def cancel_download(request: Request, current_user: User = Depends(require_auth_from_state)):
async def cancel_download(
request: Request, current_user: User = Depends(require_auth_from_state)
):
"""
Cancel a running playlist download process by its task id.
"""
task_id = request.query_params.get("task_id")
if not task_id:
return JSONResponse(
content={"error": "Missing task id (task_id) parameter"},
status_code=400
content={"error": "Missing task id (task_id) parameter"}, status_code=400
)
# Use the queue manager's cancellation method.
@@ -165,124 +169,94 @@ async def cancel_download(request: Request, current_user: User = Depends(require
@router.get("/info")
async def get_playlist_info(request: Request, current_user: User = Depends(require_auth_from_state)):
async def get_playlist_info(
request: Request, current_user: User = Depends(require_auth_from_state)
):
"""
Retrieve Spotify playlist metadata given a Spotify playlist ID.
Expects a query parameter 'id' that contains the Spotify playlist ID.
"""
spotify_id = request.query_params.get("id")
include_tracks = request.query_params.get("include_tracks", "false").lower() == "true"
if not spotify_id:
return JSONResponse(
content={"error": "Missing parameter: id"},
status_code=400
)
try:
# Use the optimized playlist info function
from routes.utils.get_info import get_playlist_info_optimized
playlist_info = get_playlist_info_optimized(spotify_id, include_tracks=include_tracks)
# If playlist_info is successfully fetched, check if it's watched
# and augment track items with is_locally_known status
if playlist_info and playlist_info.get("id"):
watched_playlist_details = get_watched_playlist(playlist_info["id"])
if watched_playlist_details: # Playlist is being watched
if playlist_info.get("tracks") and playlist_info["tracks"].get("items"):
for item in playlist_info["tracks"]["items"]:
if item and item.get("track") and item["track"].get("id"):
track_id = item["track"]["id"]
item["track"]["is_locally_known"] = is_track_in_playlist_db(
playlist_info["id"], track_id
)
elif item and item.get(
"track"
): # Track object exists but no ID
item["track"]["is_locally_known"] = False
# If not watched, or no tracks, is_locally_known will not be added, or tracks won't exist to add it to.
# Frontend should handle absence of this key as false.
return JSONResponse(
content=playlist_info, status_code=200
)
except Exception as e:
error_data = {"error": str(e), "traceback": traceback.format_exc()}
return JSONResponse(content=error_data, status_code=500)
@router.get("/metadata")
async def get_playlist_metadata(request: Request, current_user: User = Depends(require_auth_from_state)):
"""
Retrieve only Spotify playlist metadata (no tracks) to avoid rate limiting.
Expects a query parameter 'id' that contains the Spotify playlist ID.
Always returns the raw JSON from get_playlist with expand_items=False.
"""
spotify_id = request.query_params.get("id")
if not spotify_id:
return JSONResponse(
content={"error": "Missing parameter: id"},
status_code=400
)
return JSONResponse(content={"error": "Missing parameter: id"}, status_code=400)
try:
# Use the optimized playlist metadata function
from routes.utils.get_info import get_playlist_metadata
playlist_metadata = get_playlist_metadata(spotify_id)
# Resolve active account's credentials blob
cfg = get_config_params() or {}
active_account = cfg.get("spotify")
if not active_account:
return JSONResponse(
content={"error": "Active Spotify account not set in configuration."},
status_code=500,
)
blob_path = get_spotify_blob_path(active_account)
if not blob_path.exists():
return JSONResponse(
content={
"error": f"Spotify credentials blob not found for account '{active_account}'"
},
status_code=500,
)
return JSONResponse(
content=playlist_metadata, status_code=200
)
except Exception as e:
error_data = {"error": str(e), "traceback": traceback.format_exc()}
return JSONResponse(content=error_data, status_code=500)
client = get_client()
try:
playlist_info = get_playlist(client, spotify_id, expand_items=False)
finally:
pass
@router.get("/tracks")
async def get_playlist_tracks(request: Request, current_user: User = Depends(require_auth_from_state)):
"""
Retrieve playlist tracks with pagination support for progressive loading.
Expects query parameters: 'id' (playlist ID), 'limit' (optional), 'offset' (optional).
"""
spotify_id = request.query_params.get("id")
limit = int(request.query_params.get("limit", 50))
offset = int(request.query_params.get("offset", 0))
if not spotify_id:
return JSONResponse(
content={"error": "Missing parameter: id"},
status_code=400
)
try:
# Use the optimized playlist tracks function
from routes.utils.get_info import get_playlist_tracks
tracks_data = get_playlist_tracks(spotify_id, limit=limit, offset=offset)
return JSONResponse(
content=tracks_data, status_code=200
)
return JSONResponse(content=playlist_info, status_code=200)
except Exception as e:
error_data = {"error": str(e), "traceback": traceback.format_exc()}
return JSONResponse(content=error_data, status_code=500)
@router.put("/watch/{playlist_spotify_id}")
async def add_to_watchlist(playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)):
async def add_to_watchlist(
playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)
):
"""Adds a playlist to the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
raise HTTPException(status_code=403, detail={"error": "Watch feature is currently disabled globally."})
raise HTTPException(
status_code=403,
detail={"error": "Watch feature is currently disabled globally."},
)
logger.info(f"Attempting to add playlist {playlist_spotify_id} to watchlist.")
try:
# Check if already watched
if get_watched_playlist(playlist_spotify_id):
return {"message": f"Playlist {playlist_spotify_id} is already being watched."}
return {
"message": f"Playlist {playlist_spotify_id} is already being watched."
}
# Fetch playlist details from Spotify to populate our DB (metadata only)
cfg = get_config_params() or {}
active_account = cfg.get("spotify")
if not active_account:
raise HTTPException(
status_code=500,
detail={"error": "Active Spotify account not set in configuration."},
)
blob_path = get_spotify_blob_path(active_account)
if not blob_path.exists():
raise HTTPException(
status_code=500,
detail={
"error": f"Spotify credentials blob not found for account '{active_account}'"
},
)
client = get_client()
try:
playlist_data = get_playlist(
client, playlist_spotify_id, expand_items=False
)
finally:
pass
# Fetch playlist details from Spotify to populate our DB
from routes.utils.get_info import get_playlist_metadata
playlist_data = get_playlist_metadata(playlist_spotify_id)
if not playlist_data or "id" not in playlist_data:
logger.error(
f"Could not fetch details for playlist {playlist_spotify_id} from Spotify."
@@ -291,19 +265,11 @@ async def add_to_watchlist(playlist_spotify_id: str, current_user: User = Depend
status_code=404,
detail={
"error": f"Could not fetch details for playlist {playlist_spotify_id} from Spotify."
}
},
)
add_playlist_db(playlist_data) # This also creates the tracks table
# REMOVED: Do not add initial tracks directly to DB.
# The playlist watch manager will pick them up as new and queue downloads.
# Tracks will be added to DB only after successful download via Celery task callback.
# initial_track_items = playlist_data.get('tracks', {}).get('items', [])
# if initial_track_items:
# from routes.utils.watch.db import add_tracks_to_playlist_db # Keep local import for clarity
# add_tracks_to_playlist_db(playlist_spotify_id, initial_track_items)
logger.info(
f"Playlist {playlist_spotify_id} added to watchlist. Its tracks will be processed by the watch manager."
)
@@ -317,11 +283,16 @@ async def add_to_watchlist(playlist_spotify_id: str, current_user: User = Depend
f"Error adding playlist {playlist_spotify_id} to watchlist: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail={"error": f"Could not add playlist to watchlist: {str(e)}"})
raise HTTPException(
status_code=500,
detail={"error": f"Could not add playlist to watchlist: {str(e)}"},
)
@router.get("/watch/{playlist_spotify_id}/status")
async def get_playlist_watch_status(playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)):
async def get_playlist_watch_status(
playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)
):
"""Checks if a specific playlist is being watched."""
logger.info(f"Checking watch status for playlist {playlist_spotify_id}.")
try:
@@ -337,22 +308,31 @@ async def get_playlist_watch_status(playlist_spotify_id: str, current_user: User
f"Error checking watch status for playlist {playlist_spotify_id}: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail={"error": f"Could not check watch status: {str(e)}"})
raise HTTPException(
status_code=500, detail={"error": f"Could not check watch status: {str(e)}"}
)
@router.delete("/watch/{playlist_spotify_id}")
async def remove_from_watchlist(playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)):
async def remove_from_watchlist(
playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)
):
"""Removes a playlist from the watchlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
raise HTTPException(status_code=403, detail={"error": "Watch feature is currently disabled globally."})
raise HTTPException(
status_code=403,
detail={"error": "Watch feature is currently disabled globally."},
)
logger.info(f"Attempting to remove playlist {playlist_spotify_id} from watchlist.")
try:
if not get_watched_playlist(playlist_spotify_id):
raise HTTPException(
status_code=404,
detail={"error": f"Playlist {playlist_spotify_id} not found in watchlist."}
detail={
"error": f"Playlist {playlist_spotify_id} not found in watchlist."
},
)
remove_playlist_db(playlist_spotify_id)
@@ -369,12 +349,16 @@ async def remove_from_watchlist(playlist_spotify_id: str, current_user: User = D
)
raise HTTPException(
status_code=500,
detail={"error": f"Could not remove playlist from watchlist: {str(e)}"}
detail={"error": f"Could not remove playlist from watchlist: {str(e)}"},
)
@router.post("/watch/{playlist_spotify_id}/tracks")
async def mark_tracks_as_known(playlist_spotify_id: str, request: Request, current_user: User = Depends(require_auth_from_state)):
async def mark_tracks_as_known(
playlist_spotify_id: str,
request: Request,
current_user: User = Depends(require_auth_from_state),
):
"""Fetches details for given track IDs and adds/updates them in the playlist's local DB table."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
@@ -382,7 +366,7 @@ async def mark_tracks_as_known(playlist_spotify_id: str, request: Request, curre
status_code=403,
detail={
"error": "Watch feature is currently disabled globally. Cannot mark tracks."
}
},
)
logger.info(
@@ -397,19 +381,22 @@ async def mark_tracks_as_known(playlist_spotify_id: str, request: Request, curre
status_code=400,
detail={
"error": "Invalid request body. Expecting a JSON array of track Spotify IDs."
}
},
)
if not get_watched_playlist(playlist_spotify_id):
raise HTTPException(
status_code=404,
detail={"error": f"Playlist {playlist_spotify_id} is not being watched."}
detail={
"error": f"Playlist {playlist_spotify_id} is not being watched."
},
)
fetched_tracks_details = []
client = get_client()
for track_id in track_ids:
try:
track_detail = get_spotify_info(track_id, "track")
track_detail = get_track(client, track_id)
if track_detail and track_detail.get("id"):
fetched_tracks_details.append(track_detail)
else:
@@ -443,11 +430,18 @@ async def mark_tracks_as_known(playlist_spotify_id: str, request: Request, curre
f"Error marking tracks as known for playlist {playlist_spotify_id}: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail={"error": f"Could not mark tracks as known: {str(e)}"})
raise HTTPException(
status_code=500,
detail={"error": f"Could not mark tracks as known: {str(e)}"},
)
@router.delete("/watch/{playlist_spotify_id}/tracks")
async def mark_tracks_as_missing_locally(playlist_spotify_id: str, request: Request, current_user: User = Depends(require_auth_from_state)):
async def mark_tracks_as_missing_locally(
playlist_spotify_id: str,
request: Request,
current_user: User = Depends(require_auth_from_state),
):
"""Removes specified tracks from the playlist's local DB table."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
@@ -455,7 +449,7 @@ async def mark_tracks_as_missing_locally(playlist_spotify_id: str, request: Requ
status_code=403,
detail={
"error": "Watch feature is currently disabled globally. Cannot mark tracks."
}
},
)
logger.info(
@@ -470,13 +464,15 @@ async def mark_tracks_as_missing_locally(playlist_spotify_id: str, request: Requ
status_code=400,
detail={
"error": "Invalid request body. Expecting a JSON array of track Spotify IDs."
}
},
)
if not get_watched_playlist(playlist_spotify_id):
raise HTTPException(
status_code=404,
detail={"error": f"Playlist {playlist_spotify_id} is not being watched."}
detail={
"error": f"Playlist {playlist_spotify_id} is not being watched."
},
)
deleted_count = remove_specific_tracks_from_playlist_table(
@@ -495,22 +491,32 @@ async def mark_tracks_as_missing_locally(playlist_spotify_id: str, request: Requ
f"Error marking tracks as missing (deleting locally) for playlist {playlist_spotify_id}: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail={"error": f"Could not mark tracks as missing: {str(e)}"})
raise HTTPException(
status_code=500,
detail={"error": f"Could not mark tracks as missing: {str(e)}"},
)
@router.get("/watch/list")
async def list_watched_playlists_endpoint(current_user: User = Depends(require_auth_from_state)):
async def list_watched_playlists_endpoint(
current_user: User = Depends(require_auth_from_state),
):
"""Lists all playlists currently in the watchlist."""
try:
playlists = get_watched_playlists()
return playlists
except Exception as e:
logger.error(f"Error listing watched playlists: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"Could not list watched playlists: {str(e)}"})
raise HTTPException(
status_code=500,
detail={"error": f"Could not list watched playlists: {str(e)}"},
)
@router.post("/watch/trigger_check")
async def trigger_playlist_check_endpoint(current_user: User = Depends(require_auth_from_state)):
async def trigger_playlist_check_endpoint(
current_user: User = Depends(require_auth_from_state),
):
"""Manually triggers the playlist checking mechanism for all watched playlists."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
@@ -518,7 +524,7 @@ async def trigger_playlist_check_endpoint(current_user: User = Depends(require_a
status_code=403,
detail={
"error": "Watch feature is currently disabled globally. Cannot trigger check."
}
},
)
logger.info("Manual trigger for playlist check received for all playlists.")
@@ -535,12 +541,14 @@ async def trigger_playlist_check_endpoint(current_user: User = Depends(require_a
)
raise HTTPException(
status_code=500,
detail={"error": f"Could not trigger playlist check for all: {str(e)}"}
detail={"error": f"Could not trigger playlist check for all: {str(e)}"},
)
@router.post("/watch/trigger_check/{playlist_spotify_id}")
async def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)):
async def trigger_specific_playlist_check_endpoint(
playlist_spotify_id: str, current_user: User = Depends(require_auth_from_state)
):
"""Manually triggers the playlist checking mechanism for a specific playlist."""
watch_config = get_watch_config()
if not watch_config.get("enabled", False):
@@ -548,7 +556,7 @@ async def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str, cur
status_code=403,
detail={
"error": "Watch feature is currently disabled globally. Cannot trigger check."
}
},
)
logger.info(
@@ -565,7 +573,7 @@ async def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str, cur
status_code=404,
detail={
"error": f"Playlist {playlist_spotify_id} is not in the watchlist. Add it first."
}
},
)
# Run check_watched_playlists with the specific ID
@@ -590,5 +598,5 @@ async def trigger_specific_playlist_check_endpoint(playlist_spotify_id: str, cur
status_code=500,
detail={
"error": f"Could not trigger playlist check for {playlist_spotify_id}: {str(e)}"
}
},
)

View File

@@ -1,12 +1,11 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi import APIRouter, Request, Depends
from fastapi.responses import JSONResponse
import json
import traceback
import uuid
import time
from routes.utils.celery_queue_manager import download_queue_manager
from routes.utils.celery_tasks import store_task_info, store_task_status, ProgressState
from routes.utils.get_info import get_spotify_info
from routes.utils.get_info import get_client, get_track
from routes.utils.errors import DuplicateDownloadError
# Import authentication dependencies
@@ -21,7 +20,11 @@ def construct_spotify_url(item_id: str, item_type: str = "track") -> str:
@router.get("/download/{track_id}")
async def handle_download(track_id: str, request: Request, current_user: User = Depends(require_auth_from_state)):
async def handle_download(
track_id: str,
request: Request,
current_user: User = Depends(require_auth_from_state),
):
# Retrieve essential parameters from the request.
# name = request.args.get('name') # Removed
# artist = request.args.get('artist') # Removed
@@ -31,15 +34,18 @@ async def handle_download(track_id: str, request: Request, current_user: User =
# Fetch metadata from Spotify
try:
track_info = get_spotify_info(track_id, "track")
client = get_client()
track_info = get_track(client, track_id)
if (
not track_info
or not track_info.get("name")
or not track_info.get("artists")
):
return JSONResponse(
content={"error": f"Could not retrieve metadata for track ID: {track_id}"},
status_code=404
content={
"error": f"Could not retrieve metadata for track ID: {track_id}"
},
status_code=404,
)
name_from_spotify = track_info.get("name")
@@ -51,15 +57,16 @@ async def handle_download(track_id: str, request: Request, current_user: User =
except Exception as e:
return JSONResponse(
content={"error": f"Failed to fetch metadata for track {track_id}: {str(e)}"},
status_code=500
content={
"error": f"Failed to fetch metadata for track {track_id}: {str(e)}"
},
status_code=500,
)
# Validate required parameters
if not url:
return JSONResponse(
content={"error": "Missing required parameter: url"},
status_code=400
content={"error": "Missing required parameter: url"}, status_code=400
)
# Add the task to the queue with only essential parameters
@@ -84,7 +91,7 @@ async def handle_download(track_id: str, request: Request, current_user: User =
"error": "Duplicate download detected.",
"existing_task": e.existing_task,
},
status_code=409
status_code=409,
)
except Exception as e:
# Generic error handling for other issues during task submission
@@ -116,25 +123,23 @@ async def handle_download(track_id: str, request: Request, current_user: User =
"error": f"Failed to queue track download: {str(e)}",
"task_id": error_task_id,
},
status_code=500
status_code=500,
)
return JSONResponse(
content={"task_id": task_id},
status_code=202
)
return JSONResponse(content={"task_id": task_id}, status_code=202)
@router.get("/download/cancel")
async def cancel_download(request: Request, current_user: User = Depends(require_auth_from_state)):
async def cancel_download(
request: Request, current_user: User = Depends(require_auth_from_state)
):
"""
Cancel a running download process by its task id.
"""
task_id = request.query_params.get("task_id")
if not task_id:
return JSONResponse(
content={"error": "Missing process id (task_id) parameter"},
status_code=400
content={"error": "Missing process id (task_id) parameter"}, status_code=400
)
# Use the queue manager's cancellation method.
@@ -145,7 +150,9 @@ async def cancel_download(request: Request, current_user: User = Depends(require
@router.get("/info")
async def get_track_info(request: Request, current_user: User = Depends(require_auth_from_state)):
async def get_track_info(
request: Request, current_user: User = Depends(require_auth_from_state)
):
"""
Retrieve Spotify track metadata given a Spotify track ID.
Expects a query parameter 'id' that contains the Spotify track ID.
@@ -153,14 +160,11 @@ async def get_track_info(request: Request, current_user: User = Depends(require_
spotify_id = request.query_params.get("id")
if not spotify_id:
return JSONResponse(
content={"error": "Missing parameter: id"},
status_code=400
)
return JSONResponse(content={"error": "Missing parameter: id"}, status_code=400)
try:
# Use the get_spotify_info function (already imported at top)
track_info = get_spotify_info(spotify_id, "track")
client = get_client()
track_info = get_track(client, spotify_id)
return JSONResponse(content=track_info, status_code=200)
except Exception as e:
error_data = {"error": str(e), "traceback": traceback.format_exc()}