From 9b32a8f984e596b2cf6e14837c9f17ccd72d3da1 Mon Sep 17 00:00:00 2001 From: codey Date: Wed, 24 Sep 2025 16:30:54 -0400 Subject: [PATCH] Refactor radio endpoint requests to remove API key requirement and implement user role checks for permissions --- endpoints/constructors.py | 8 ----- endpoints/radio.py | 66 ++++++++++++++++++++++++--------------- endpoints/rip.py | 2 +- utils/rip_background.py | 37 +++++++++++++++------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/endpoints/constructors.py b/endpoints/constructors.py index 0334040..ac16050 100644 --- a/endpoints/constructors.py +++ b/endpoints/constructors.py @@ -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" diff --git a/endpoints/radio.py b/endpoints/radio.py index a5c696c..1ec87be 100644 --- a/endpoints/radio.py +++ b/endpoints/radio.py @@ -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, diff --git a/endpoints/rip.py b/endpoints/rip.py index 02a68b6..0a9355d 100644 --- a/endpoints/rip.py +++ b/endpoints/rip.py @@ -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, ) diff --git a/utils/rip_background.py b/utils/rip_background.py index 5519351..306496b 100644 --- a/utils/rip_background.py +++ b/utils/rip_background.py @@ -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: