update references to codey.lol -> codey.horse [new domain]
This commit is contained in:
@@ -5,9 +5,9 @@ A modern FastAPI-based backend providing various endpoints for media, authentica
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This server is built with [FastAPI](https://fastapi.tiangolo.com/) and provides a comprehensive API for multiple services. API documentation is available in three formats:
|
This server is built with [FastAPI](https://fastapi.tiangolo.com/) and provides a comprehensive API for multiple services. API documentation is available in three formats:
|
||||||
- **Swagger UI**: [https://api.codey.lol/docs](https://api.codey.lol/docs) - Classic interactive API explorer with "Try it out" functionality
|
- **Swagger UI**: [https://api.codey.horse/docs](https://api.codey.horse/docs) - Classic interactive API explorer with "Try it out" functionality
|
||||||
- **Scalar**: [https://api.codey.lol/scalar](https://api.codey.lol/scalar) - Modern, fast interactive API documentation (recommended)
|
- **Scalar**: [https://api.codey.horse/scalar](https://api.codey.horse/scalar) - Modern, fast interactive API documentation (recommended)
|
||||||
- **ReDoc**: [https://api.codey.lol/redoc](https://api.codey.lol/redoc) - Clean, read-only documentation with better visual design
|
- **ReDoc**: [https://api.codey.horse/redoc](https://api.codey.horse/redoc) - Clean, read-only documentation with better visual design
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
|
|||||||
18
base.py
18
base.py
@@ -77,7 +77,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="codey.lol API",
|
title="codey.horse API",
|
||||||
version="1.0",
|
version="1.0",
|
||||||
contact={"name": "codey"},
|
contact={"name": "codey"},
|
||||||
redirect_slashes=False,
|
redirect_slashes=False,
|
||||||
@@ -90,13 +90,13 @@ app = FastAPI(
|
|||||||
util = importlib.import_module("util").Utilities(app, constants)
|
util = importlib.import_module("util").Utilities(app, constants)
|
||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
"https://codey.lol",
|
"https://codey.horse",
|
||||||
"https://old.codey.lol",
|
"https://old.codey.horse",
|
||||||
"https://api.codey.lol",
|
"https://api.codey.horse",
|
||||||
"https://status.boatson.boats",
|
"https://status.boatson.boats",
|
||||||
"https://_new.codey.lol",
|
"https://_new.codey.horse",
|
||||||
"http://localhost:4321",
|
"http://localhost:4321",
|
||||||
"https://local.codey.lol:4321",
|
"https://local.codey.horse:4321",
|
||||||
]
|
]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -113,7 +113,7 @@ app.add_middleware(
|
|||||||
def scalar_docs():
|
def scalar_docs():
|
||||||
# Replace default FastAPI favicon with site favicon
|
# Replace default FastAPI favicon with site favicon
|
||||||
html_response = get_scalar_api_reference(
|
html_response = get_scalar_api_reference(
|
||||||
openapi_url="/openapi.json", title="codey.lol API"
|
openapi_url="/openapi.json", title="codey.horse API"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
body = (
|
body = (
|
||||||
@@ -123,7 +123,7 @@ def scalar_docs():
|
|||||||
)
|
)
|
||||||
body = body.replace(
|
body = body.replace(
|
||||||
"https://fastapi.tiangolo.com/img/favicon.png",
|
"https://fastapi.tiangolo.com/img/favicon.png",
|
||||||
"https://codey.lol/images/favicon.png",
|
"https://codey.horse/images/favicon.png",
|
||||||
)
|
)
|
||||||
# Build fresh response so Content-Length matches modified body
|
# Build fresh response so Content-Length matches modified body
|
||||||
return HTMLResponse(content=body, status_code=html_response.status_code)
|
return HTMLResponse(content=body, status_code=html_response.status_code)
|
||||||
@@ -135,7 +135,7 @@ def scalar_docs():
|
|||||||
@app.get("/favicon.ico", include_in_schema=False)
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
"""Redirect favicon requests to the site icon."""
|
"""Redirect favicon requests to the site icon."""
|
||||||
return RedirectResponse("https://codey.lol/images/favicon.png")
|
return RedirectResponse("https://codey.horse/images/favicon.png")
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import regex
|
|||||||
import aiosqlite as sqlite3
|
import aiosqlite as sqlite3
|
||||||
from fastapi import FastAPI, HTTPException, Depends
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
from fastapi_throttle import RateLimiter
|
from fastapi_throttle import RateLimiter
|
||||||
|
from fastapi.requests import Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from typing import LiteralString, Optional, Union, Iterable
|
from typing import LiteralString, Optional, Union, Iterable
|
||||||
from regex import Pattern
|
from regex import Pattern
|
||||||
@@ -128,7 +129,7 @@ class LyricSearch(FastAPI):
|
|||||||
return JSONResponse(content=[])
|
return JSONResponse(content=[])
|
||||||
return JSONResponse(content=typeahead)
|
return JSONResponse(content=typeahead)
|
||||||
|
|
||||||
async def lyric_search_handler(self, data: ValidLyricRequest) -> JSONResponse:
|
async def lyric_search_handler(self, data: ValidLyricRequest, request: Request) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Search for lyrics.
|
Search for lyrics.
|
||||||
|
|
||||||
@@ -317,4 +318,13 @@ class LyricSearch(FastAPI):
|
|||||||
if not data.extra:
|
if not data.extra:
|
||||||
result.pop("src")
|
result.pop("src")
|
||||||
|
|
||||||
|
# Check if the request is coming from the old domain
|
||||||
|
host = request.headers.get("host", "")
|
||||||
|
if "api.codey.lol" in host:
|
||||||
|
warning_message = "Warning: The API domain is moving to api.codey.horse. Please update your scripts to use the new domain."
|
||||||
|
if "lyrics" in result:
|
||||||
|
result["lyrics"] = f"{warning_message}<br>{result['lyrics']}"
|
||||||
|
elif "lrc" in result:
|
||||||
|
result["lrc"] = f"{warning_message}\n{result['lrc']}"
|
||||||
|
|
||||||
return JSONResponse(content=result)
|
return JSONResponse(content=result)
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ class Radio(FastAPI):
|
|||||||
logging.debug("album_art_handler Exception: %s", str(e))
|
logging.debug("album_art_handler Exception: %s", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url="https://codey.lol/images/radio_art_default.jpg", status_code=302
|
url="https://codey.horse/images/radio_art_default.jpg", status_code=302
|
||||||
)
|
)
|
||||||
|
|
||||||
async def radio_now_playing(self, request: Request,
|
async def radio_now_playing(self, request: Request,
|
||||||
|
|||||||
@@ -686,7 +686,10 @@ class RIP(FastAPI):
|
|||||||
self, video_id: str, request: Request, user=Depends(get_current_user)
|
self, video_id: str, request: Request, user=Depends(get_current_user)
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""
|
"""
|
||||||
Download a video file.
|
Download a video file via streaming.
|
||||||
|
|
||||||
|
Streams the video directly from the HLS source through ffmpeg
|
||||||
|
to the client without buffering the entire file to disk first.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- **video_id** (str): The Tidal video ID.
|
- **video_id** (str): The Tidal video ID.
|
||||||
@@ -694,10 +697,11 @@ class RIP(FastAPI):
|
|||||||
- **user**: Current user (dependency).
|
- **user**: Current user (dependency).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- **Response**: The video file as a streaming response.
|
- **Response**: Streaming video response.
|
||||||
"""
|
"""
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import StreamingResponse
|
||||||
import os
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
if "trip" not in user.get("roles", []) and "admin" not in user.get("roles", []):
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
@@ -710,32 +714,76 @@ class RIP(FastAPI):
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get video metadata for filename
|
# Get video metadata and stream URL concurrently
|
||||||
metadata = await self.trip_util.get_video_metadata(vid_id)
|
metadata, stream_url = await asyncio.gather(
|
||||||
|
self.trip_util.get_video_metadata(vid_id),
|
||||||
|
self.trip_util.get_video_stream_url(vid_id),
|
||||||
|
)
|
||||||
|
|
||||||
if not metadata:
|
if not metadata:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"error": "Video not found"},
|
content={"error": "Video not found"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Download the video
|
if not stream_url:
|
||||||
file_path = await self.trip_util.download_video(vid_id)
|
|
||||||
if not file_path or not os.path.exists(file_path):
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"error": "Failed to download video"},
|
content={"error": "Video stream not available"},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate a nice filename
|
# Build a safe filename
|
||||||
artist = metadata.get("artist", "Unknown")
|
artist = metadata.get("artist", "Unknown")
|
||||||
title = metadata.get("title", f"video_{vid_id}")
|
title = metadata.get("title", f"video_{vid_id}")
|
||||||
# Sanitize filename
|
safe_filename = re.sub(r'[<>:"|?*\x00-\x1F]', '', f"{artist} - {title}.mp4")
|
||||||
safe_filename = f"{artist} - {title}.mp4".replace("/", "-").replace("\\", "-")
|
safe_filename = safe_filename.replace("/", "-").replace("\\", "-")
|
||||||
|
|
||||||
return FileResponse(
|
async def stream_video():
|
||||||
path=file_path,
|
"""Stream ffmpeg output directly to client."""
|
||||||
filename=safe_filename,
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-analyzeduration", "10M",
|
||||||
|
"-probesize", "10M",
|
||||||
|
"-i", stream_url,
|
||||||
|
"-c:v", "copy",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-b:a", "256k",
|
||||||
|
"-af", "aresample=async=1:first_pts=0",
|
||||||
|
"-movflags", "frag_keyframe+empty_moov+faststart",
|
||||||
|
"-f", "mp4",
|
||||||
|
"pipe:1",
|
||||||
|
]
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = await proc.stdout.read(256 * 1024) # 256KB chunks
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
yield chunk
|
||||||
|
finally:
|
||||||
|
if proc.returncode is None:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await proc.wait()
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_video(),
|
||||||
media_type="video/mp4",
|
media_type="video/mp4",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{safe_filename}"',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def video_bulk_fetch_handler(
|
async def video_bulk_fetch_handler(
|
||||||
|
|||||||
2
util.py
2
util.py
@@ -13,7 +13,7 @@ class Utilities:
|
|||||||
|
|
||||||
def __init__(self, app: FastAPI, constants):
|
def __init__(self, app: FastAPI, constants):
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.blocked_redirect_uri = "https://codey.lol"
|
self.blocked_redirect_uri = "https://codey.horse"
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def get_blocked_response(self, path: Optional[str] = None):
|
def get_blocked_response(self, path: Optional[str] = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user