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

View File

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

View File

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

View File

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