Refactor radio endpoint requests to remove API key requirement and implement user role checks for permissions

This commit is contained in:
2025-09-24 16:30:54 -04:00
parent d6658512d8
commit 9b32a8f984
4 changed files with 67 additions and 46 deletions

View File

@@ -135,7 +135,6 @@ class ValidRadioSongRequest(BaseModel):
Request model for radio song request.
Attributes:
- **key** (str): API Key.
- **artist** (Optional[str]): Artist to search.
- **song** (Optional[str]): Song to search.
- **artistsong** (Optional[str]): Combined artist-song string.
@@ -143,7 +142,6 @@ class ValidRadioSongRequest(BaseModel):
- **station** (Station): Station name.
"""
key: str
artist: Optional[str] = None
song: Optional[str] = None
artistsong: Optional[str] = None
@@ -180,12 +178,10 @@ class ValidRadioNextRequest(BaseModel):
Request model for radio next track.
Attributes:
- **key** (str): API Key.
- **skipTo** (Optional[str]): UUID to skip to.
- **station** (Station): Station name.
"""
key: str
skipTo: Optional[str] = None
station: Station = "main"
@@ -222,13 +218,11 @@ class ValidRadioQueueShiftRequest(BaseModel):
Request model for radio queue shift.
Attributes:
- **key** (str): API Key.
- **uuid** (str): UUID to shift.
- **next** (Optional[bool]): Play next if true.
- **station** (Station): Station name.
"""
key: str
uuid: str
next: Optional[bool] = False
station: Station = "main"
@@ -239,11 +233,9 @@ class ValidRadioQueueRemovalRequest(BaseModel):
Request model for radio queue removal.
Attributes:
- **key** (str): API Key.
- **uuid** (str): UUID to remove.
- **station** (Station): Station name.
"""
key: str
uuid: str
station: Station = "main"

View File

@@ -23,6 +23,7 @@ from fastapi import (
Depends)
from fastapi_throttle import RateLimiter
from fastapi.responses import RedirectResponse, JSONResponse, FileResponse
from auth.deps import get_current_user
class Radio(FastAPI):
"""Radio Endpoints"""
@@ -52,9 +53,9 @@ class Radio(FastAPI):
if endpoint == "radio/album_art":
methods = ["GET"]
app.add_api_route(
f"/{endpoint}", handler, methods=methods, include_in_schema=False,
f"/{endpoint}", handler, methods=methods, include_in_schema=True,
dependencies=[Depends(
RateLimiter(times=10, seconds=2))] if not endpoint == "radio/np" else None,
RateLimiter(times=25, seconds=2))] if not endpoint == "radio/np" else None,
)
app.add_event_handler("startup", self.on_start)
@@ -65,21 +66,22 @@ class Radio(FastAPI):
await self.radio_util.load_playlists()
async def radio_skip(
self, data: ValidRadioNextRequest, request: Request
self, data: ValidRadioNextRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Skip to the next track in the queue, or to the UUID specified in `skipTo` if provided.
Parameters:
- **data** (ValidRadioNextRequest): Contains the API key, optional UUID to skip to, and station name.
- **data** (ValidRadioNextRequest): Contains optional UUID to skip to, and station name.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Indicates success or failure of the skip operation.
"""
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
try:
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if data.skipTo:
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo, data.station)
if not queue_item:
@@ -115,21 +117,22 @@ class Radio(FastAPI):
raise e # Re-raise HTTPException
async def radio_reshuffle(
self, data: ValidRadioReshuffleRequest, request: Request
self, data: ValidRadioReshuffleRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Reshuffle the play queue.
Parameters:
- **data** (ValidRadioReshuffleRequest): Contains the API key and station name.
- **data** (ValidRadioReshuffleRequest): Contains the station name.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Indicates success of the reshuffle operation.
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
random.shuffle(self.radio_util.active_playlist[data.station])
return JSONResponse(content={"ok": True})
@@ -205,21 +208,22 @@ class Radio(FastAPI):
return JSONResponse(content=out_json)
async def radio_queue_shift(
self, data: ValidRadioQueueShiftRequest, request: Request
self, data: ValidRadioQueueShiftRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Shift the position of a UUID within the queue.
Parameters:
- **data** (ValidRadioQueueShiftRequest): Contains the API key, UUID to shift, and station name.
- **data** (ValidRadioQueueShiftRequest): Contains the UUID to shift, and station name.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Indicates success of the shift operation.
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid, data.station)
if not queue_item:
@@ -242,21 +246,22 @@ class Radio(FastAPI):
)
async def radio_queue_remove(
self, data: ValidRadioQueueRemovalRequest, request: Request
self, data: ValidRadioQueueRemovalRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Remove an item from the current play queue.
Parameters:
- **data** (ValidRadioQueueRemovalRequest): Contains the API key, UUID of the item to remove, and station name.
- **data** (ValidRadioQueueRemovalRequest): Contains the UUID of the item to remove, and station name.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Indicates success of the removal operation.
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid, data.station)
if not queue_item:
@@ -343,24 +348,27 @@ class Radio(FastAPI):
data: ValidRadioNextRequest,
request: Request,
background_tasks: BackgroundTasks,
user=Depends(get_current_user),
) -> JSONResponse:
"""
Get the next track in the queue. The track will be removed from the queue in the process.
Parameters:
- **data** (ValidRadioNextRequest): Contains the API key, optional UUID to skip to, and station name.
- **data** (ValidRadioNextRequest): Contains optional UUID to skip to, and station name.
- **request** (Request): The HTTP request object.
- **background_tasks** (BackgroundTasks): Background tasks for webhook execution.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Contains the next track information.
"""
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
logging.info("Radio get next")
if data.station not in self.radio_util.active_playlist.keys():
raise HTTPException(status_code=500, detail="No such station/not ready")
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if (
not isinstance(self.radio_util.active_playlist[data.station], list)
or not self.radio_util.active_playlist[data.station]
@@ -411,21 +419,23 @@ class Radio(FastAPI):
return JSONResponse(content=next)
async def radio_request(
self, data: ValidRadioSongRequest, request: Request
self, data: ValidRadioSongRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Handle song requests.
Parameters:
- **data** (ValidRadioSongRequest): Contains the API key, artist, song, and station name.
- **data** (ValidRadioSongRequest): Contains artist, song, and station name.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Indicates success or failure of the request.
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
artistsong: Optional[str] = data.artistsong
artist: Optional[str] = data.artist
song: Optional[str] = data.song
@@ -454,7 +464,7 @@ class Radio(FastAPI):
return JSONResponse(content={"result": search})
def radio_typeahead(
self, data: ValidRadioTypeaheadRequest, request: Request
self, data: ValidRadioTypeaheadRequest, request: Request, user=Depends(get_current_user)
) -> JSONResponse:
"""
Handle typeahead queries for the radio.
@@ -462,10 +472,14 @@ class Radio(FastAPI):
Parameters:
- **data** (ValidRadioTypeaheadRequest): Contains the typeahead query.
- **request** (Request): The HTTP request object.
- **user**: Current authenticated user.
Returns:
- **JSONResponse**: Contains the typeahead results.
"""
if "dj" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="Insufficient permissions")
if not isinstance(data.query, str):
return JSONResponse(
status_code=500,

View File

@@ -68,7 +68,7 @@ class RIP(FastAPI):
f"/{endpoint}",
handler,
methods=["GET"] if endpoint != "trip/bulk_fetch" else ["POST"],
include_in_schema=True,
include_in_schema=False,
dependencies=dependencies,
)

View File

@@ -479,17 +479,32 @@ def bulk_download(track_list: list, quality: str = "FLAC"):
except Exception as e:
tb = traceback.format_exc()
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
send_log_to_discord(msg, "ERROR", target)
track_info["error"] = str(e)
if attempt >= MAX_RETRIES:
track_info["status"] = "Failed"
send_log_to_discord(
f"Track {track_id} failed after {attempt} attempts",
"ERROR",
target,
)
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
is_no_stream_url = isinstance(e, RuntimeError) and str(e) == "No stream URL"
if is_no_stream_url:
if attempt == 1 or attempt == MAX_RETRIES:
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
send_log_to_discord(msg, "ERROR", target)
track_info["error"] = str(e)
if attempt >= MAX_RETRIES:
track_info["status"] = "Failed"
send_log_to_discord(
f"Track {track_id} failed after {attempt} attempts",
"ERROR",
target,
)
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
else:
msg = f"Track {track_id} attempt {attempt} failed: {e}\n{tb}"
send_log_to_discord(msg, "ERROR", target)
track_info["error"] = str(e)
if attempt >= MAX_RETRIES:
track_info["status"] = "Failed"
send_log_to_discord(
f"Track {track_id} failed after {attempt} attempts",
"ERROR",
target,
)
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
finally:
try: