misc
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,7 @@ class Misc(FastAPI):
|
||||
self.upload_activity_image,
|
||||
methods=["POST"],
|
||||
dependencies=[Depends(RateLimiter(times=10, seconds=2))],
|
||||
include_in_schema=False,
|
||||
)
|
||||
|
||||
logging.debug("Loading NaaS reasons")
|
||||
|
||||
@@ -47,12 +47,12 @@ class Radio(FastAPI):
|
||||
self.sr_util = SRUtil()
|
||||
self.lrclib = LRCLib()
|
||||
self.lrc_cache: Dict[str, Optional[str]] = {}
|
||||
self.lrc_cache_locks = {}
|
||||
self.lrc_cache_locks: Dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||
self.playlists_loaded: bool = False
|
||||
# WebSocket connection management
|
||||
self.active_connections: Dict[str, Set[WebSocket]] = {}
|
||||
# Initialize broadcast locks to prevent duplicate events
|
||||
self.broadcast_locks = defaultdict(asyncio.Lock)
|
||||
self.broadcast_locks: Dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||
self.endpoints: dict = {
|
||||
"radio/np": self.radio_now_playing,
|
||||
"radio/request": self.radio_request,
|
||||
@@ -71,9 +71,9 @@ class Radio(FastAPI):
|
||||
if endpoint == "radio/album_art":
|
||||
methods = ["GET"]
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=methods, include_in_schema=True,
|
||||
f"/{endpoint}", handler, methods=methods, include_in_schema=False,
|
||||
dependencies=[Depends(
|
||||
RateLimiter(times=25, seconds=2))] if not endpoint == "radio/np" else None,
|
||||
RateLimiter(times=25, seconds=2))],
|
||||
)
|
||||
|
||||
# Add WebSocket route
|
||||
@@ -83,12 +83,8 @@ class Radio(FastAPI):
|
||||
|
||||
app.add_websocket_route("/radio/ws/{station}", websocket_route_handler)
|
||||
|
||||
app.add_event_handler("startup", self.on_start)
|
||||
|
||||
async def on_start(self) -> None:
|
||||
# Initialize locks in the event loop
|
||||
self.lrc_cache_locks = defaultdict(asyncio.Lock)
|
||||
self.broadcast_locks = defaultdict(asyncio.Lock)
|
||||
# Load playlists for all stations
|
||||
stations = ", ".join(self.radio_util.db_queries.keys())
|
||||
logging.info("radio: Initializing stations:\n%s", stations)
|
||||
await self.radio_util.load_playlists()
|
||||
|
||||
119
endpoints/rip.py
119
endpoints/rip.py
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from fastapi import FastAPI, Request, Response, Depends
|
||||
from fastapi import FastAPI, Request, Response, Depends, HTTPException
|
||||
from fastapi_throttle import RateLimiter
|
||||
from fastapi.responses import JSONResponse
|
||||
from utils.sr_wrapper import SRUtil
|
||||
@@ -63,22 +63,42 @@ class RIP(FastAPI):
|
||||
"trip/bulk_fetch": self.bulk_fetch_handler,
|
||||
"trip/job/{job_id:path}": self.job_status_handler,
|
||||
"trip/jobs/list": self.job_list_handler,
|
||||
"trip/auth/start": self.tidal_auth_start_handler,
|
||||
"trip/auth/check": self.tidal_auth_check_handler,
|
||||
}
|
||||
|
||||
# Store pending device codes for auth flow
|
||||
self._pending_device_codes: dict[str, str] = {}
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
dependencies = [Depends(RateLimiter(times=8, seconds=2))]
|
||||
app.add_api_route(
|
||||
f"/{endpoint}",
|
||||
handler,
|
||||
methods=["GET"] if endpoint != "trip/bulk_fetch" else ["POST"],
|
||||
methods=["GET"] if endpoint not in ("trip/bulk_fetch", "trip/auth/check") else ["POST"],
|
||||
include_in_schema=False,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
async def startup(self) -> None:
|
||||
"""Initialize Tidal keepalive. Call this from your app's lifespan context manager."""
|
||||
try:
|
||||
await self.trip_util.start_keepalive()
|
||||
logger.info("Tidal keepalive task started successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start Tidal keepalive task: {e}")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Stop Tidal keepalive. Call this from your app's lifespan context manager."""
|
||||
try:
|
||||
await self.trip_util.stop_keepalive()
|
||||
logger.info("Tidal keepalive task stopped successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping Tidal keepalive task: {e}")
|
||||
|
||||
def _format_job(self, job: Job):
|
||||
"""
|
||||
Helper to normalize job data into JSON.
|
||||
|
||||
Parameters:
|
||||
- job (Job): The job object to format.
|
||||
|
||||
@@ -132,6 +152,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with artists or 404.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
# support optional grouping to return one primary per display name
|
||||
# with `alternatives` for disambiguation (use ?group=true)
|
||||
group = bool(request.query_params.get("group", False))
|
||||
@@ -154,6 +176,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with albums or 404.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
albums = await self.trip_util.get_albums_by_artist_id(artist_id)
|
||||
if not albums:
|
||||
return Response(status_code=404, content="Not found")
|
||||
@@ -178,6 +202,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with tracks or 404.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
tracks = await self.trip_util.get_tracks_by_album_id(album_id, quality)
|
||||
if not tracks:
|
||||
return Response(status_code=404, content="Not Found")
|
||||
@@ -198,6 +224,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with tracks or 404.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
logging.critical("Searching for tracks by artist: %s, song: %s", artist, song)
|
||||
tracks = await self.trip_util.get_tracks_by_artist_song(artist, song)
|
||||
if not tracks:
|
||||
@@ -223,6 +251,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with stream URL or 404.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
track = await self.trip_util.get_stream_url_by_track_id(track_id, quality)
|
||||
if not track:
|
||||
return Response(status_code=404, content="Not found")
|
||||
@@ -245,6 +275,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **Response**: JSON response with job info or error.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
if not data or not data.track_ids or not data.target:
|
||||
return JSONResponse(
|
||||
content={
|
||||
@@ -296,7 +328,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **JSONResponse**: Job status and result or error.
|
||||
"""
|
||||
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
job = None
|
||||
try:
|
||||
# Try direct fetch first
|
||||
@@ -334,6 +367,8 @@ class RIP(FastAPI):
|
||||
Returns:
|
||||
- **JSONResponse**: List of jobs.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
jobs_info = []
|
||||
seen = set()
|
||||
|
||||
@@ -385,3 +420,79 @@ class RIP(FastAPI):
|
||||
jobs_info.sort(key=job_sort_key, reverse=True)
|
||||
|
||||
return {"jobs": jobs_info}
|
||||
|
||||
async def tidal_auth_start_handler(
|
||||
self, request: Request, user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Start Tidal device authorization flow.
|
||||
|
||||
Returns a URL that the user must visit to authorize the application.
|
||||
After visiting the URL and authorizing, call /trip/auth/check to complete.
|
||||
|
||||
Returns:
|
||||
- **JSONResponse**: Contains device_code and verification_url.
|
||||
"""
|
||||
try:
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
device_code, verification_url = await self.trip_util.start_device_auth()
|
||||
# Store device code for this session
|
||||
self._pending_device_codes[user.get("sub", "default")] = device_code
|
||||
return JSONResponse(
|
||||
content={
|
||||
"device_code": device_code,
|
||||
"verification_url": verification_url,
|
||||
"message": "Visit the URL to authorize, then call /trip/auth/check",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Tidal auth start failed: %s", e)
|
||||
return JSONResponse(
|
||||
content={"error": str(e)},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
async def tidal_auth_check_handler(
|
||||
self, request: Request, user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check if Tidal device authorization is complete.
|
||||
|
||||
Call this after the user has visited the verification URL.
|
||||
|
||||
Returns:
|
||||
- **JSONResponse**: Contains success status and message.
|
||||
"""
|
||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
device_code = self._pending_device_codes.get(user.get("sub", "default"))
|
||||
if not device_code:
|
||||
return JSONResponse(
|
||||
content={"error": "No pending authorization. Call /trip/auth/start first."},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
success, error = await self.trip_util.check_device_auth(device_code)
|
||||
if success:
|
||||
# Clear the pending code
|
||||
self._pending_device_codes.pop(user.get("sub", "default"), None)
|
||||
return JSONResponse(
|
||||
content={"success": True, "message": "Tidal authorization complete!"}
|
||||
)
|
||||
elif error == "pending":
|
||||
return JSONResponse(
|
||||
content={"success": False, "pending": True, "message": "Waiting for user to authorize..."}
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={"success": False, "error": error},
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Tidal auth check failed: %s", e)
|
||||
return JSONResponse(
|
||||
content={"error": str(e)},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ from fastapi import FastAPI, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_throttle import RateLimiter
|
||||
from typing import Optional, Union
|
||||
from utils.yt_utils import sign_video_id
|
||||
from .constructors import ValidYTSearchRequest
|
||||
|
||||
|
||||
class YT(FastAPI):
|
||||
"""
|
||||
YT Endpoints
|
||||
@@ -57,6 +57,7 @@ class YT(FastAPI):
|
||||
return JSONResponse(
|
||||
content={
|
||||
"video_id": yt_video_id,
|
||||
"video_token": sign_video_id(yt_video_id) if yt_video_id else None,
|
||||
"extras": yts_res[0],
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user