update references to codey.lol -> codey.horse [new domain]

This commit is contained in:
2026-02-21 08:00:38 -05:00
parent d6689b9c38
commit 83285ff08c
6 changed files with 89 additions and 31 deletions

View File

@@ -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
View File

@@ -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")
""" """

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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):