Files
spotizerr-dev/routes/credentials.py
2025-08-02 12:46:36 -06:00

293 lines
12 KiB
Python
Executable File

from fastapi import APIRouter, HTTPException, Request
import json
import logging
from routes.utils.credentials import (
get_credential,
list_credentials,
create_credential,
delete_credential,
edit_credential,
init_credentials_db,
# Import new utility functions for global Spotify API creds
_get_global_spotify_api_creds,
save_global_spotify_api_creds,
)
logger = logging.getLogger(__name__)
router = APIRouter()
# Initialize the database and tables when the router is loaded
init_credentials_db()
@router.get("/spotify_api_config")
@router.put("/spotify_api_config")
async def handle_spotify_api_config(request: Request):
"""Handles GET and PUT requests for the global Spotify API client_id and client_secret."""
try:
if request.method == "GET":
client_id, client_secret = _get_global_spotify_api_creds()
if client_id is not None and client_secret is not None:
return {"client_id": client_id, "client_secret": client_secret}
else:
# If search.json exists but is empty/incomplete, or doesn't exist
return {
"warning": "Global Spotify API credentials are not fully configured or file is missing.",
"client_id": client_id or "",
"client_secret": client_secret or "",
}
elif request.method == "PUT":
data = await request.json()
if not data or "client_id" not in data or "client_secret" not in data:
raise HTTPException(
status_code=400,
detail={"error": "Request body must contain 'client_id' and 'client_secret'"}
)
client_id = data["client_id"]
client_secret = data["client_secret"]
if not isinstance(client_id, str) or not isinstance(client_secret, str):
raise HTTPException(
status_code=400,
detail={"error": "'client_id' and 'client_secret' must be strings"}
)
if save_global_spotify_api_creds(client_id, client_secret):
return {"message": "Global Spotify API credentials updated successfully."}
else:
raise HTTPException(
status_code=500,
detail={"error": "Failed to save global Spotify API credentials."}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in /spotify_api_config: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.get("/{service}")
async def handle_list_credentials(service: str):
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
return list_credentials(service)
except ValueError as e: # Should not happen with service check above
raise HTTPException(status_code=400, detail={"error": str(e)})
except Exception as e:
logger.error(f"Error listing credentials for {service}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.get("/{service}/{name}")
async def handle_get_credential(service: str, name: str):
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
# get_credential for Spotify now only returns region and blob_file_path
return get_credential(service, name)
except (ValueError, FileNotFoundError, FileExistsError) as e:
status_code = 400
if isinstance(e, FileNotFoundError):
status_code = 404
elif isinstance(e, FileExistsError):
status_code = 409
logger.warning(f"Client error in /{service}/{name}: {str(e)}")
raise HTTPException(status_code=status_code, detail={"error": str(e)})
except Exception as e:
logger.error(f"Server error in /{service}/{name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.post("/{service}/{name}")
async def handle_create_credential(service: str, name: str, request: Request):
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
data = await request.json()
if not data:
raise HTTPException(status_code=400, detail={"error": "Request body cannot be empty."})
# create_credential for Spotify now expects 'region' and 'blob_content'
# For Deezer, it expects 'arl' and 'region'
# Validation is handled within create_credential utility function
result = create_credential(service, name, data)
return {
"message": f"Credential for '{name}' ({service}) created successfully.",
"details": result,
}
except (ValueError, FileNotFoundError, FileExistsError) as e:
status_code = 400
if isinstance(e, FileNotFoundError):
status_code = 404
elif isinstance(e, FileExistsError):
status_code = 409
logger.warning(f"Client error in /{service}/{name}: {str(e)}")
raise HTTPException(status_code=status_code, detail={"error": str(e)})
except Exception as e:
logger.error(f"Server error in /{service}/{name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.put("/{service}/{name}")
async def handle_update_credential(service: str, name: str, request: Request):
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
data = await request.json()
if not data:
raise HTTPException(status_code=400, detail={"error": "Request body cannot be empty."})
# edit_credential for Spotify now handles updates to 'region', 'blob_content'
# For Deezer, 'arl', 'region'
result = edit_credential(service, name, data)
return {
"message": f"Credential for '{name}' ({service}) updated successfully.",
"details": result,
}
except (ValueError, FileNotFoundError, FileExistsError) as e:
status_code = 400
if isinstance(e, FileNotFoundError):
status_code = 404
elif isinstance(e, FileExistsError):
status_code = 409
logger.warning(f"Client error in /{service}/{name}: {str(e)}")
raise HTTPException(status_code=status_code, detail={"error": str(e)})
except Exception as e:
logger.error(f"Server error in /{service}/{name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.delete("/{service}/{name}")
async def handle_delete_credential(service: str, name: str):
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
# delete_credential for Spotify also handles deleting the blob directory
result = delete_credential(service, name)
return {
"message": f"Credential for '{name}' ({service}) deleted successfully.",
"details": result,
}
except (ValueError, FileNotFoundError, FileExistsError) as e:
status_code = 400
if isinstance(e, FileNotFoundError):
status_code = 404
elif isinstance(e, FileExistsError):
status_code = 409
logger.warning(f"Client error in /{service}/{name}: {str(e)}")
raise HTTPException(status_code=status_code, detail={"error": str(e)})
except Exception as e:
logger.error(f"Server error in /{service}/{name}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
# The '/search/<service>/<name>' route is now obsolete for Spotify and has been removed.
@router.get("/all/{service}")
async def handle_all_credentials(service: str):
"""Lists all credentials for a given service. For Spotify, API keys are global and not listed per account."""
try:
if service not in ["spotify", "deezer"]:
raise HTTPException(
status_code=400,
detail={"error": "Invalid service. Must be 'spotify' or 'deezer'"}
)
credentials_list = []
account_names = list_credentials(service) # This lists names from DB
for name in account_names:
try:
# get_credential for Spotify returns region and blob_file_path.
# For Deezer, it returns arl and region.
account_data = get_credential(service, name)
# We don't add global Spotify API keys here as they are separate
credentials_list.append({"name": name, "details": account_data})
except FileNotFoundError:
logger.warning(
f"Credential name '{name}' listed for service '{service}' but not found by get_credential. Skipping."
)
except Exception as e_inner:
logger.error(
f"Error fetching details for credential '{name}' ({service}): {e_inner}",
exc_info=True,
)
credentials_list.append(
{
"name": name,
"error": f"Could not retrieve details: {str(e_inner)}",
}
)
return credentials_list
except Exception as e:
logger.error(f"Error in /all/{service}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})
@router.get("/markets")
async def handle_markets():
"""
Returns a list of unique market regions for Deezer and Spotify accounts.
"""
try:
deezer_regions = set()
spotify_regions = set()
# Process Deezer accounts
deezer_account_names = list_credentials("deezer")
for name in deezer_account_names:
try:
account_data = get_credential("deezer", name)
if account_data and "region" in account_data and account_data["region"]:
deezer_regions.add(account_data["region"])
except Exception as e:
logger.warning(
f"Could not retrieve region for deezer account {name}: {e}"
)
# Process Spotify accounts
spotify_account_names = list_credentials("spotify")
for name in spotify_account_names:
try:
account_data = get_credential("spotify", name)
if account_data and "region" in account_data and account_data["region"]:
spotify_regions.add(account_data["region"])
except Exception as e:
logger.warning(
f"Could not retrieve region for spotify account {name}: {e}"
)
return {
"deezer": sorted(list(deezer_regions)),
"spotify": sorted(list(spotify_regions)),
}
except Exception as e:
logger.error(f"Error in /markets: {e}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": f"An unexpected error occurred: {str(e)}"})