feat: Reimplement download artist discography per groups in artist page
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
fastapi==0.116.1
|
fastapi==0.116.1
|
||||||
uvicorn[standard]==0.35.0
|
uvicorn[standard]==0.35.0
|
||||||
celery==5.5.3
|
celery==5.5.3
|
||||||
deezspot-spotizerr==3.1.2
|
deezspot-spotizerr==3.1.4
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
bcrypt==4.2.1
|
bcrypt==4.2.1
|
||||||
PyJWT==2.10.1
|
PyJWT==2.10.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Request, Depends, Request, Depends
|
from fastapi import APIRouter, Request, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -8,11 +8,9 @@ import logging
|
|||||||
from routes.auth.middleware import require_auth_from_state, User
|
from routes.auth.middleware import require_auth_from_state, User
|
||||||
|
|
||||||
# Import queue management and Spotify info
|
# Import queue management and Spotify info
|
||||||
from routes.utils.get_info import get_spotify_info
|
|
||||||
from routes.utils.celery_queue_manager import download_queue_manager
|
from routes.utils.celery_queue_manager import download_queue_manager
|
||||||
|
|
||||||
# Import authentication dependencies
|
# Import authentication dependencies
|
||||||
from routes.auth.middleware import require_auth_from_state, User
|
|
||||||
|
|
||||||
# Import queue management and Spotify info
|
# Import queue management and Spotify info
|
||||||
from routes.utils.get_info import (
|
from routes.utils.get_info import (
|
||||||
@@ -22,7 +20,6 @@ from routes.utils.get_info import (
|
|||||||
get_playlist,
|
get_playlist,
|
||||||
get_artist,
|
get_artist,
|
||||||
)
|
)
|
||||||
from routes.utils.celery_queue_manager import download_queue_manager
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -33,7 +30,11 @@ class BulkAddLinksRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/bulk-add-spotify-links")
|
@router.post("/bulk-add-spotify-links")
|
||||||
async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, current_user: User = Depends(require_auth_from_state)):
|
async def bulk_add_spotify_links(
|
||||||
|
request: BulkAddLinksRequest,
|
||||||
|
req: Request,
|
||||||
|
current_user: User = Depends(require_auth_from_state),
|
||||||
|
):
|
||||||
added_count = 0
|
added_count = 0
|
||||||
failed_links = []
|
failed_links = []
|
||||||
total_links = len(request.links)
|
total_links = len(request.links)
|
||||||
@@ -56,8 +57,12 @@ async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, cur
|
|||||||
|
|
||||||
spotify_type = match.group(1)
|
spotify_type = match.group(1)
|
||||||
spotify_id = match.group(2)
|
spotify_id = match.group(2)
|
||||||
logger.debug(f"Extracted from link: spotify_type={spotify_type}, spotify_id={spotify_id}")
|
logger.debug(
|
||||||
logger.debug(f"Extracted from link: spotify_type={spotify_type}, spotify_id={spotify_id}")
|
f"Extracted from link: spotify_type={spotify_type}, spotify_id={spotify_id}"
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Extracted from link: spotify_type={spotify_type}, spotify_id={spotify_id}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get basic info to confirm existence and get name/artist
|
# Get basic info to confirm existence and get name/artist
|
||||||
@@ -109,9 +114,13 @@ async def bulk_add_spotify_links(request: BulkAddLinksRequest, req: Request, cur
|
|||||||
|
|
||||||
if task_id:
|
if task_id:
|
||||||
added_count += 1
|
added_count += 1
|
||||||
logger.debug(f"Added {added_count}/{total_links} {spotify_type} '{item_name}' ({spotify_id}) to queue with task_id: {task_id}.")
|
logger.debug(
|
||||||
|
f"Added {added_count}/{total_links} {spotify_type} '{item_name}' ({spotify_id}) to queue with task_id: {task_id}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Failed to add {spotify_type} '{item_name}' ({spotify_id}) to queue.")
|
logger.warning(
|
||||||
|
f"Failed to add {spotify_type} '{item_name}' ({spotify_id}) to queue."
|
||||||
|
)
|
||||||
failed_links.append(link)
|
failed_links.append(link)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from routes.utils.credentials import (
|
|||||||
)
|
)
|
||||||
from routes.utils.celery_queue_manager import get_existing_task_id
|
from routes.utils.celery_queue_manager import get_existing_task_id
|
||||||
from routes.utils.errors import DuplicateDownloadError
|
from routes.utils.errors import DuplicateDownloadError
|
||||||
|
from routes.utils.celery_config import get_config_params
|
||||||
|
|
||||||
|
|
||||||
def download_album(
|
def download_album(
|
||||||
@@ -98,6 +99,7 @@ def download_album(
|
|||||||
spotify_client_id=global_spotify_client_id,
|
spotify_client_id=global_spotify_client_id,
|
||||||
spotify_client_secret=global_spotify_client_secret,
|
spotify_client_secret=global_spotify_client_secret,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=str(get_spotify_blob_path(main)),
|
||||||
)
|
)
|
||||||
dl.download_albumspo(
|
dl.download_albumspo(
|
||||||
link_album=url, # Spotify URL
|
link_album=url, # Spotify URL
|
||||||
@@ -257,6 +259,11 @@ def download_album(
|
|||||||
spotify_client_id=global_spotify_client_id, # Global Spotify keys
|
spotify_client_id=global_spotify_client_id, # Global Spotify keys
|
||||||
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=(
|
||||||
|
str(get_spotify_blob_path(get_config_params().get("spotify")))
|
||||||
|
if get_config_params().get("spotify")
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
dl.download_albumdee( # Deezer URL, download via Deezer
|
dl.download_albumdee( # Deezer URL, download via Deezer
|
||||||
link_album=url,
|
link_album=url,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import logging
|
|||||||
from routes.utils.celery_queue_manager import download_queue_manager
|
from routes.utils.celery_queue_manager import download_queue_manager
|
||||||
from routes.utils.credentials import get_credential, _get_global_spotify_api_creds
|
from routes.utils.credentials import get_credential, _get_global_spotify_api_creds
|
||||||
from routes.utils.errors import DuplicateDownloadError
|
from routes.utils.errors import DuplicateDownloadError
|
||||||
from routes.utils.get_info import get_spotify_info
|
from routes.utils.get_info import get_client, get_artist
|
||||||
|
|
||||||
from deezspot.libutils.utils import get_ids, link_is_valid
|
from deezspot.libutils.utils import get_ids, link_is_valid
|
||||||
|
|
||||||
@@ -77,10 +77,26 @@ def get_artist_discography(
|
|||||||
log_json({"status": "error", "message": msg})
|
log_json({"status": "error", "message": msg})
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Fetch artist once and return grouped arrays without pagination
|
||||||
try:
|
try:
|
||||||
# Use the optimized get_spotify_info function
|
client = get_client()
|
||||||
discography = get_spotify_info(artist_id, "artist_discography")
|
artist_obj = get_artist(client, artist_id)
|
||||||
return discography
|
|
||||||
|
# Normalize groups as arrays of IDs; tolerate dict shape from some sources
|
||||||
|
def normalize_group(val):
|
||||||
|
if isinstance(val, list):
|
||||||
|
return val
|
||||||
|
if isinstance(val, dict):
|
||||||
|
items = val.get("items") or val.get("releases") or []
|
||||||
|
return items if isinstance(items, list) else []
|
||||||
|
return []
|
||||||
|
|
||||||
|
return {
|
||||||
|
"album_group": normalize_group(artist_obj.get("album_group")),
|
||||||
|
"single_group": normalize_group(artist_obj.get("single_group")),
|
||||||
|
"compilation_group": normalize_group(artist_obj.get("compilation_group")),
|
||||||
|
"appears_on_group": normalize_group(artist_obj.get("appears_on_group")),
|
||||||
|
}
|
||||||
except Exception as fetch_error:
|
except Exception as fetch_error:
|
||||||
msg = f"An error occurred while fetching the discography: {fetch_error}"
|
msg = f"An error occurred while fetching the discography: {fetch_error}"
|
||||||
log_json({"status": "error", "message": msg})
|
log_json({"status": "error", "message": msg})
|
||||||
@@ -120,61 +136,55 @@ def download_artist_albums(url, album_type=None, request_args=None, username=Non
|
|||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
# Get watch config to determine which album groups to download
|
# Get watch config to determine which album groups to download
|
||||||
watch_config = get_watch_config()
|
valid_groups = {"album", "single", "compilation", "appears_on"}
|
||||||
allowed_groups = [
|
if album_type and isinstance(album_type, str):
|
||||||
g.lower()
|
requested = [g.strip().lower() for g in album_type.split(",") if g.strip()]
|
||||||
for g in watch_config.get("watchedArtistAlbumGroup", ["album", "single"])
|
allowed_groups = [g for g in requested if g in valid_groups]
|
||||||
]
|
if not allowed_groups:
|
||||||
|
logger.warning(
|
||||||
|
f"album_type query provided but no valid groups found in {requested}; falling back to watch config."
|
||||||
|
)
|
||||||
|
if not album_type or not isinstance(album_type, str) or not allowed_groups:
|
||||||
|
watch_config = get_watch_config()
|
||||||
|
allowed_groups = [
|
||||||
|
g.lower()
|
||||||
|
for g in watch_config.get("watchedArtistAlbumGroup", ["album", "single"])
|
||||||
|
if g.lower() in valid_groups
|
||||||
|
]
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Filtering albums by watchedArtistAlbumGroup setting (exact album_group match): {allowed_groups}"
|
f"Filtering albums by album_type/watch setting (exact album_group match): {allowed_groups}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch all artist albums with pagination
|
# Fetch artist and aggregate group arrays without pagination
|
||||||
|
client = get_client()
|
||||||
|
artist_obj = get_artist(client, artist_id)
|
||||||
|
|
||||||
|
def normalize_group(val):
|
||||||
|
if isinstance(val, list):
|
||||||
|
return val
|
||||||
|
if isinstance(val, dict):
|
||||||
|
items = val.get("items") or val.get("releases") or []
|
||||||
|
return items if isinstance(items, list) else []
|
||||||
|
return []
|
||||||
|
|
||||||
|
group_key_to_type = [
|
||||||
|
("album_group", "album"),
|
||||||
|
("single_group", "single"),
|
||||||
|
("compilation_group", "compilation"),
|
||||||
|
("appears_on_group", "appears_on"),
|
||||||
|
]
|
||||||
|
|
||||||
all_artist_albums = []
|
all_artist_albums = []
|
||||||
offset = 0
|
for key, group_type in group_key_to_type:
|
||||||
limit = 50 # Spotify API limit for artist albums
|
ids = normalize_group(artist_obj.get(key))
|
||||||
|
# transform to minimal album objects with album_group tagging for filtering parity
|
||||||
logger.info(f"Fetching all albums for artist ID: {artist_id} with pagination")
|
for album_id in ids:
|
||||||
|
all_artist_albums.append(
|
||||||
while True:
|
{
|
||||||
logger.debug(
|
"id": album_id,
|
||||||
f"Fetching albums for {artist_id}. Limit: {limit}, Offset: {offset}"
|
"album_group": group_type,
|
||||||
)
|
}
|
||||||
artist_data_page = get_spotify_info(
|
|
||||||
artist_id, "artist_discography", limit=limit, offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
if not artist_data_page or not isinstance(artist_data_page.get("items"), list):
|
|
||||||
logger.warning(
|
|
||||||
f"No album items found or invalid format for artist {artist_id} at offset {offset}. Response: {artist_data_page}"
|
|
||||||
)
|
)
|
||||||
break
|
|
||||||
|
|
||||||
current_page_albums = artist_data_page.get("items", [])
|
|
||||||
if not current_page_albums:
|
|
||||||
logger.info(
|
|
||||||
f"No more albums on page for artist {artist_id} at offset {offset}. Total fetched so far: {len(all_artist_albums)}."
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Fetched {len(current_page_albums)} albums on current page for artist {artist_id}."
|
|
||||||
)
|
|
||||||
all_artist_albums.extend(current_page_albums)
|
|
||||||
|
|
||||||
# Check if Spotify indicates a next page URL
|
|
||||||
if artist_data_page.get("next"):
|
|
||||||
offset += limit # Increment offset by the limit used for the request
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"No next page URL for artist {artist_id}. Pagination complete. Total albums fetched: {len(all_artist_albums)}."
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not all_artist_albums:
|
|
||||||
raise ValueError(
|
|
||||||
f"Failed to retrieve artist data or no albums found for artist ID {artist_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter albums based on the allowed types using album_group field (like in manager.py)
|
# Filter albums based on the allowed types using album_group field (like in manager.py)
|
||||||
filtered_albums = []
|
filtered_albums = []
|
||||||
@@ -201,13 +211,23 @@ def download_artist_albums(url, album_type=None, request_args=None, username=Non
|
|||||||
duplicate_albums = []
|
duplicate_albums = []
|
||||||
|
|
||||||
for album in filtered_albums:
|
for album in filtered_albums:
|
||||||
album_url = album.get("external_urls", {}).get("spotify", "")
|
album_id = album.get("id")
|
||||||
album_name = album.get("name", "Unknown Album")
|
if not album_id:
|
||||||
album_artists = album.get("artists", [])
|
logger.warning("Skipping album without ID in filtered list.")
|
||||||
|
continue
|
||||||
|
# fetch album details to construct URL and names
|
||||||
|
try:
|
||||||
|
album_obj = download_queue_manager.client.get_album(
|
||||||
|
album_id, include_tracks=False
|
||||||
|
) # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
# If download_queue_manager lacks a client, fallback to shared client
|
||||||
|
album_obj = get_client().get_album(album_id, include_tracks=False)
|
||||||
|
album_url = album_obj.get("external_urls", {}).get("spotify", "")
|
||||||
|
album_name = album_obj.get("name", "Unknown Album")
|
||||||
|
artists = album_obj.get("artists", []) or []
|
||||||
album_artist = (
|
album_artist = (
|
||||||
album_artists[0].get("name", "Unknown Artist")
|
artists[0].get("name", "Unknown Artist") if artists else "Unknown Artist"
|
||||||
if album_artists
|
|
||||||
else "Unknown Artist"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not album_url:
|
if not album_url:
|
||||||
|
|||||||
@@ -93,57 +93,6 @@ def get_playlist(
|
|||||||
return client.get_playlist(playlist_in, expand_items=expand_items)
|
return client.get_playlist(playlist_in, expand_items=expand_items)
|
||||||
|
|
||||||
|
|
||||||
def get_spotify_info(
|
|
||||||
spotify_id: str,
|
|
||||||
info_type: str,
|
|
||||||
limit: int = 50,
|
|
||||||
offset: int = 0,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Thin, typed wrapper around common Spotify info lookups using the shared client.
|
|
||||||
|
|
||||||
Currently supports:
|
|
||||||
- "artist_discography": returns a paginated view over the artist's releases
|
|
||||||
combined across album_group/single_group/compilation_group/appears_on_group.
|
|
||||||
|
|
||||||
Returns a mapping with at least: items, total, limit, offset.
|
|
||||||
Also includes a truthy "next" key when more pages are available.
|
|
||||||
"""
|
|
||||||
client = get_client()
|
|
||||||
|
|
||||||
if info_type == "artist_discography":
|
|
||||||
artist = client.get_artist(spotify_id)
|
|
||||||
all_items = []
|
|
||||||
for key in (
|
|
||||||
"album_group",
|
|
||||||
"single_group",
|
|
||||||
"compilation_group",
|
|
||||||
"appears_on_group",
|
|
||||||
):
|
|
||||||
grp = artist.get(key)
|
|
||||||
if isinstance(grp, list):
|
|
||||||
all_items.extend(grp)
|
|
||||||
elif isinstance(grp, dict):
|
|
||||||
items = grp.get("items") or grp.get("releases") or []
|
|
||||||
if isinstance(items, list):
|
|
||||||
all_items.extend(items)
|
|
||||||
total = len(all_items)
|
|
||||||
start = max(0, offset or 0)
|
|
||||||
page_limit = max(1, limit or 50)
|
|
||||||
end = min(total, start + page_limit)
|
|
||||||
page_items = all_items[start:end]
|
|
||||||
has_more = end < total
|
|
||||||
return {
|
|
||||||
"items": page_items,
|
|
||||||
"total": total,
|
|
||||||
"limit": page_limit,
|
|
||||||
"offset": start,
|
|
||||||
"next": bool(has_more),
|
|
||||||
}
|
|
||||||
|
|
||||||
raise ValueError(f"Unsupported info_type: {info_type}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_metadata(playlist_id: str) -> Dict[str, Any]:
|
def get_playlist_metadata(playlist_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Fetch playlist metadata using the shared client without expanding items.
|
Fetch playlist metadata using the shared client without expanding items.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from deezspot.spotloader import SpoLogin
|
|||||||
from deezspot.deezloader import DeeLogin
|
from deezspot.deezloader import DeeLogin
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from routes.utils.credentials import get_credential, _get_global_spotify_api_creds
|
from routes.utils.credentials import get_credential, _get_global_spotify_api_creds
|
||||||
|
from routes.utils.credentials import get_spotify_blob_path
|
||||||
|
from routes.utils.celery_config import get_config_params
|
||||||
from routes.utils.celery_queue_manager import get_existing_task_id
|
from routes.utils.celery_queue_manager import get_existing_task_id
|
||||||
from routes.utils.errors import DuplicateDownloadError
|
from routes.utils.errors import DuplicateDownloadError
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ def download_playlist(
|
|||||||
spotify_client_id=global_spotify_client_id,
|
spotify_client_id=global_spotify_client_id,
|
||||||
spotify_client_secret=global_spotify_client_secret,
|
spotify_client_secret=global_spotify_client_secret,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=str(get_spotify_blob_path(main)),
|
||||||
)
|
)
|
||||||
dl.download_playlistspo(
|
dl.download_playlistspo(
|
||||||
link_playlist=url, # Spotify URL
|
link_playlist=url, # Spotify URL
|
||||||
@@ -265,6 +268,11 @@ def download_playlist(
|
|||||||
spotify_client_id=global_spotify_client_id, # Global Spotify keys
|
spotify_client_id=global_spotify_client_id, # Global Spotify keys
|
||||||
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=(
|
||||||
|
str(get_spotify_blob_path(get_config_params().get("spotify")))
|
||||||
|
if get_config_params().get("spotify")
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
dl.download_playlistdee( # Deezer URL, download via Deezer
|
dl.download_playlistdee( # Deezer URL, download via Deezer
|
||||||
link_playlist=url,
|
link_playlist=url,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from routes.utils.credentials import (
|
|||||||
_get_global_spotify_api_creds,
|
_get_global_spotify_api_creds,
|
||||||
get_spotify_blob_path,
|
get_spotify_blob_path,
|
||||||
)
|
)
|
||||||
|
from routes.utils.celery_config import get_config_params
|
||||||
|
|
||||||
|
|
||||||
def download_track(
|
def download_track(
|
||||||
@@ -90,6 +91,7 @@ def download_track(
|
|||||||
spotify_client_id=global_spotify_client_id, # Global creds
|
spotify_client_id=global_spotify_client_id, # Global creds
|
||||||
spotify_client_secret=global_spotify_client_secret, # Global creds
|
spotify_client_secret=global_spotify_client_secret, # Global creds
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=str(get_spotify_blob_path(main)),
|
||||||
)
|
)
|
||||||
# download_trackspo means: Spotify URL, download via Deezer
|
# download_trackspo means: Spotify URL, download via Deezer
|
||||||
dl.download_trackspo(
|
dl.download_trackspo(
|
||||||
@@ -251,6 +253,11 @@ def download_track(
|
|||||||
spotify_client_id=global_spotify_client_id, # Global Spotify keys for internal Spo use by DeeLogin
|
spotify_client_id=global_spotify_client_id, # Global Spotify keys for internal Spo use by DeeLogin
|
||||||
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
spotify_client_secret=global_spotify_client_secret, # Global Spotify keys
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
|
spotify_credentials_path=(
|
||||||
|
str(get_spotify_blob_path(get_config_params().get("spotify")))
|
||||||
|
if get_config_params().get("spotify")
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
dl.download_trackdee( # Deezer URL, download via Deezer
|
dl.download_trackdee( # Deezer URL, download via Deezer
|
||||||
link_track=url,
|
link_track=url,
|
||||||
|
|||||||
@@ -343,6 +343,25 @@ export const Artist = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadGroup = async (group: "album" | "single" | "compilation" | "appears_on") => {
|
||||||
|
if (!artistId || !artist) return;
|
||||||
|
try {
|
||||||
|
toast.info(`Queueing ${group} downloads for ${artist.name}...`);
|
||||||
|
const response = await apiClient.get(`/artist/download/${artistId}?album_type=${group}`);
|
||||||
|
const count = response.data?.queued_albums?.length ?? 0;
|
||||||
|
if (count > 0) {
|
||||||
|
toast.success(`Queued ${count} ${group}${count > 1 ? "s" : ""}.`);
|
||||||
|
} else {
|
||||||
|
toast.info(`No new ${group} releases to download.`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Failed to queue ${group} downloads:`, error);
|
||||||
|
toast.error(`Failed to queue ${group} downloads`, {
|
||||||
|
description: error.response?.data?.error || "An unexpected error occurred.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleWatch = async () => {
|
const handleToggleWatch = async () => {
|
||||||
if (!artistId || !artist) return;
|
if (!artistId || !artist) return;
|
||||||
try {
|
try {
|
||||||
@@ -493,7 +512,17 @@ export const Artist = () => {
|
|||||||
{/* Albums */}
|
{/* Albums */}
|
||||||
{artistAlbums.length > 0 && (
|
{artistAlbums.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Albums</h2>
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Albums</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownloadGroup("album")}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
|
||||||
|
title="Download all albums"
|
||||||
|
>
|
||||||
|
<img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||||
{artistAlbums.map((album) => (
|
{artistAlbums.map((album) => (
|
||||||
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
||||||
@@ -505,7 +534,17 @@ export const Artist = () => {
|
|||||||
{/* Singles */}
|
{/* Singles */}
|
||||||
{artistSingles.length > 0 && (
|
{artistSingles.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Singles</h2>
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Singles</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownloadGroup("single")}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
|
||||||
|
title="Download all singles"
|
||||||
|
>
|
||||||
|
<img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||||
{artistSingles.map((album) => (
|
{artistSingles.map((album) => (
|
||||||
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
||||||
@@ -517,7 +556,17 @@ export const Artist = () => {
|
|||||||
{/* Compilations */}
|
{/* Compilations */}
|
||||||
{artistCompilations.length > 0 && (
|
{artistCompilations.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Compilations</h2>
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Compilations</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownloadGroup("compilation")}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
|
||||||
|
title="Download all compilations"
|
||||||
|
>
|
||||||
|
<img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||||
{artistCompilations.map((album) => (
|
{artistCompilations.map((album) => (
|
||||||
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
||||||
@@ -529,7 +578,17 @@ export const Artist = () => {
|
|||||||
{/* Appears On */}
|
{/* Appears On */}
|
||||||
{artistAppearsOn.length > 0 && (
|
{artistAppearsOn.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Appears On</h2>
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Appears On</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownloadGroup("appears_on")}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
|
||||||
|
title="Download all appears on"
|
||||||
|
>
|
||||||
|
<img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||||
{artistAppearsOn.map((album) => (
|
{artistAppearsOn.map((album) => (
|
||||||
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user