Refactor radio endpoint requests to remove API key requirement and implement user role checks for permissions
This commit is contained in:
@@ -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"
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user