Shaurya commited on
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
YouTube Music API Wrapper β
|
| 3 |
=============================================================
|
| 4 |
|
| 5 |
WHAT'S NEW IN v6
|
|
@@ -61,7 +61,6 @@ from concurrent.futures import ThreadPoolExecutor
|
|
| 61 |
from contextlib import asynccontextmanager
|
| 62 |
from typing import Any, Dict, List, Optional
|
| 63 |
|
| 64 |
-
import base64
|
| 65 |
import diskcache
|
| 66 |
import httpx
|
| 67 |
from fastapi import FastAPI, HTTPException, Query, Request, Response
|
|
@@ -70,22 +69,20 @@ from fastapi.middleware.gzip import GZipMiddleware
|
|
| 70 |
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 71 |
from slowapi.errors import RateLimitExceeded
|
| 72 |
from slowapi.util import get_remote_address
|
| 73 |
-
from starlette.middleware.base import BaseHTTPMiddleware
|
| 74 |
-
from starlette.responses import Response as StarletteResponse
|
| 75 |
from ytmusicapi import YTMusic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
from download_bp import router as download_router
|
| 77 |
|
| 78 |
-
#
|
| 79 |
-
|
| 80 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 81 |
-
|
| 82 |
-
DOCS_PASSWORD = os.environ.get("DOCS_PASSWORD", "")
|
| 83 |
|
| 84 |
class DocsAuthMiddleware(BaseHTTPMiddleware):
|
| 85 |
async def dispatch(self, request: Request, call_next):
|
| 86 |
if request.url.path in ("/docs", "/openapi.json", "/redoc"):
|
| 87 |
-
if not DOCS_PASSWORD:
|
| 88 |
-
return await call_next(request)
|
| 89 |
auth = request.headers.get("Authorization", "")
|
| 90 |
if auth.startswith("Basic "):
|
| 91 |
try:
|
|
@@ -95,12 +92,16 @@ class DocsAuthMiddleware(BaseHTTPMiddleware):
|
|
| 95 |
return await call_next(request)
|
| 96 |
except Exception:
|
| 97 |
pass
|
| 98 |
-
return
|
| 99 |
"Unauthorized", status_code=401,
|
| 100 |
headers={"WWW-Authenticate": 'Basic realm="Musicanaz Docs"'},
|
| 101 |
)
|
| 102 |
return await call_next(request)
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 105 |
# Logging
|
| 106 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -271,13 +272,23 @@ _http_client: httpx.AsyncClient | None = None
|
|
| 271 |
|
| 272 |
|
| 273 |
def _get_http_client() -> httpx.AsyncClient:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
global _http_client
|
| 275 |
if _http_client is None or _http_client.is_closed:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
_http_client = httpx.AsyncClient(
|
| 277 |
timeout=httpx.Timeout(10.0),
|
| 278 |
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
|
| 279 |
follow_redirects=True,
|
| 280 |
headers={"User-Agent": "MusicAPI/6.0 (compatible; ytmusicapi)"},
|
|
|
|
| 281 |
)
|
| 282 |
return _http_client
|
| 283 |
|
|
@@ -294,7 +305,7 @@ limiter = Limiter(key_func=get_remote_address, default_limits=["200/minute"])
|
|
| 294 |
@asynccontextmanager
|
| 295 |
async def lifespan(app: FastAPI):
|
| 296 |
# Startup
|
| 297 |
-
log.info("Starting YouTube Music API
|
| 298 |
asyncio.create_task(_background_warmup())
|
| 299 |
asyncio.create_task(_auto_refresh_loop())
|
| 300 |
yield
|
|
@@ -316,22 +327,19 @@ app = FastAPI(
|
|
| 316 |
"Production-grade FastAPI wrapper for ytmusicapi. "
|
| 317 |
"Two-level cache, rate limiting, iTunes + Deezer enrichment, circuit breaker."
|
| 318 |
),
|
| 319 |
-
version="
|
| 320 |
lifespan=lifespan,
|
| 321 |
)
|
| 322 |
|
| 323 |
app.state.limiter = limiter
|
| 324 |
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 325 |
-
# DocsAuthMiddleware added LAST so it wraps everything (runs first on request)
|
| 326 |
-
app.add_middleware(DocsAuthMiddleware)
|
| 327 |
app.add_middleware(GZipMiddleware, minimum_size=300)
|
| 328 |
app.add_middleware(
|
| 329 |
CORSMiddleware,
|
| 330 |
allow_origins=["*"],
|
| 331 |
-
allow_methods=["GET", "
|
| 332 |
allow_headers=["*"],
|
| 333 |
)
|
| 334 |
-
app.include_router(download_router)
|
| 335 |
|
| 336 |
# 4 workers: ytmusicapi is I/O-bound β more workers waste RAM
|
| 337 |
_executor = ThreadPoolExecutor(max_workers=4)
|
|
@@ -339,7 +347,15 @@ _executor = ThreadPoolExecutor(max_workers=4)
|
|
| 339 |
|
| 340 |
async def run(fn, *args, **kwargs):
|
| 341 |
loop = asyncio.get_running_loop() # get_event_loop() removed in Python 3.12
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 345 |
# Per-locale YTMusic instance pool (LRU, capped at 12)
|
|
@@ -900,12 +916,18 @@ def _flatten_mood_categories(raw: Any) -> list[dict]:
|
|
| 900 |
# Health + admin
|
| 901 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 902 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
@app.get("/health", tags=["meta"])
|
| 904 |
async def health():
|
| 905 |
hits, misses = _disk_cache.stats()
|
| 906 |
return {
|
| 907 |
"status": "ok",
|
| 908 |
-
"version": "
|
| 909 |
"uptime_seconds": round(time.monotonic() - _START_TIME),
|
| 910 |
"ytm_instances": len(_ytm_pool),
|
| 911 |
"proxy_enabled": _PROXIES is not None,
|
|
@@ -1017,16 +1039,17 @@ async def search_suggestions(
|
|
| 1017 |
response: Response,
|
| 1018 |
query: str,
|
| 1019 |
detailed_runs: bool = False,
|
|
|
|
| 1020 |
language: str = Query("en"),
|
| 1021 |
):
|
| 1022 |
-
cache_key = f"suggest:{query}:{language}"
|
| 1023 |
cached = _cache_short.get(cache_key)
|
| 1024 |
if cached is not None:
|
| 1025 |
response.headers.update(_cc(30))
|
| 1026 |
return cached
|
| 1027 |
try:
|
| 1028 |
raw = await run(
|
| 1029 |
-
get_ytm(
|
| 1030 |
)
|
| 1031 |
suggestions: list[str] = []
|
| 1032 |
if isinstance(raw, list):
|
|
@@ -1053,15 +1076,16 @@ async def get_artist(
|
|
| 1053 |
request: Request,
|
| 1054 |
response: Response,
|
| 1055 |
artist_id: str,
|
|
|
|
| 1056 |
language: str = Query("en"),
|
| 1057 |
):
|
| 1058 |
-
cache_key = f"artist:{artist_id}:{language}"
|
| 1059 |
cached = _cache_long.get(cache_key)
|
| 1060 |
if cached is not None:
|
| 1061 |
response.headers.update(_cc(600))
|
| 1062 |
return cached
|
| 1063 |
try:
|
| 1064 |
-
data = await run(get_ytm(
|
| 1065 |
if not data:
|
| 1066 |
raise HTTPException(404, "Artist not found")
|
| 1067 |
data["thumbnails"] = best_thumbnails_list(data.get("thumbnails") or [])
|
|
@@ -1100,13 +1124,13 @@ async def get_artist_songs(
|
|
| 1100 |
limit: int = Query(500, ge=1, le=5000),
|
| 1101 |
language: str = Query("en"),
|
| 1102 |
):
|
| 1103 |
-
cache_key = f"artist_songs:{artist_id}:{limit}:{language}"
|
| 1104 |
cached = _cache_long.get(cache_key)
|
| 1105 |
if cached is not None:
|
| 1106 |
response.headers.update(_cc(600))
|
| 1107 |
return cached
|
| 1108 |
try:
|
| 1109 |
-
ytm = get_ytm(
|
| 1110 |
artist_data = await run(ytm.get_artist, artist_id)
|
| 1111 |
if not artist_data:
|
| 1112 |
raise HTTPException(404, "Artist not found")
|
|
@@ -1200,15 +1224,16 @@ async def get_artist_albums_endpoint(
|
|
| 1200 |
request: Request,
|
| 1201 |
response: Response,
|
| 1202 |
artist_id: str,
|
|
|
|
| 1203 |
language: str = Query("en"),
|
| 1204 |
):
|
| 1205 |
-
cache_key = f"artist_albums:{artist_id}:{language}"
|
| 1206 |
cached = _cache_long.get(cache_key)
|
| 1207 |
if cached is not None:
|
| 1208 |
response.headers.update(_cc(600))
|
| 1209 |
return cached
|
| 1210 |
try:
|
| 1211 |
-
ytm = get_ytm(
|
| 1212 |
artist_data = await run(ytm.get_artist, artist_id)
|
| 1213 |
if not artist_data:
|
| 1214 |
raise HTTPException(404, "Artist not found")
|
|
@@ -1297,15 +1322,16 @@ async def get_album(
|
|
| 1297 |
request: Request,
|
| 1298 |
response: Response,
|
| 1299 |
album_id: str,
|
|
|
|
| 1300 |
language: str = Query("en"),
|
| 1301 |
):
|
| 1302 |
-
cache_key = f"album:{album_id}:{language}"
|
| 1303 |
cached = _cache_long.get(cache_key)
|
| 1304 |
if cached is not None:
|
| 1305 |
response.headers.update(_cc(3600))
|
| 1306 |
return cached
|
| 1307 |
try:
|
| 1308 |
-
data = await run(get_ytm(
|
| 1309 |
if not data:
|
| 1310 |
raise HTTPException(404, "Album not found")
|
| 1311 |
data["thumbnails"] = best_thumbnails_list(data.get("thumbnails") or [])
|
|
@@ -1325,14 +1351,20 @@ async def get_album(
|
|
| 1325 |
|
| 1326 |
@app.get("/song/{video_id}")
|
| 1327 |
@limiter.limit("60/minute")
|
| 1328 |
-
async def get_song(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
cache_key = f"song:{video_id}"
|
| 1330 |
cached = _cache_long.get(cache_key)
|
| 1331 |
if cached is not None:
|
| 1332 |
response.headers.update(_cc(3600))
|
| 1333 |
return cached
|
| 1334 |
try:
|
| 1335 |
-
data = await run(get_ytm().get_song, video_id)
|
| 1336 |
raw = (
|
| 1337 |
data.get("thumbnail", {}).get("thumbnails") or
|
| 1338 |
data.get("thumbnails") or []
|
|
@@ -1352,14 +1384,20 @@ async def get_song(request: Request, response: Response, video_id: str):
|
|
| 1352 |
|
| 1353 |
@app.get("/stream/{video_id}")
|
| 1354 |
@limiter.limit("60/minute")
|
| 1355 |
-
async def get_stream(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
cache_key = f"stream:{video_id}"
|
| 1357 |
cached = _cache_long.get(cache_key)
|
| 1358 |
if cached is not None:
|
| 1359 |
response.headers.update(_cc(3600))
|
| 1360 |
return cached
|
| 1361 |
try:
|
| 1362 |
-
song_data = await run(get_ytm().get_song, video_id)
|
| 1363 |
vd = song_data.get("videoDetails", {})
|
| 1364 |
if not vd:
|
| 1365 |
raise HTTPException(404, "Video not found")
|
|
@@ -1560,17 +1598,18 @@ async def get_playlist(
|
|
| 1560 |
limit: int = Query(100, ge=1, le=500),
|
| 1561 |
related: bool = False,
|
| 1562 |
suggestions_limit: int = 0,
|
|
|
|
| 1563 |
language: str = Query("en"),
|
| 1564 |
):
|
| 1565 |
clean_id = playlist_id[2:] if playlist_id.startswith("VL") else playlist_id
|
| 1566 |
-
cache_key = f"playlist:{clean_id}:{limit}:{language}"
|
| 1567 |
cached = _cache_medium.get(cache_key)
|
| 1568 |
if cached is not None:
|
| 1569 |
response.headers.update(_cc(300))
|
| 1570 |
return cached
|
| 1571 |
try:
|
| 1572 |
data = await run(
|
| 1573 |
-
get_ytm(
|
| 1574 |
clean_id, limit, related, suggestions_limit,
|
| 1575 |
)
|
| 1576 |
if not data:
|
|
@@ -1597,16 +1636,17 @@ async def get_podcast(
|
|
| 1597 |
response: Response,
|
| 1598 |
podcast_id: str,
|
| 1599 |
limit: int = Query(50, ge=1, le=200),
|
|
|
|
| 1600 |
language: str = Query("en"),
|
| 1601 |
):
|
| 1602 |
clean_id = podcast_id[2:] if (podcast_id.startswith("VL") and not podcast_id.startswith("VLM")) else podcast_id
|
| 1603 |
-
cache_key = f"podcast:{clean_id}:{limit}:{language}"
|
| 1604 |
cached = _cache_long.get(cache_key)
|
| 1605 |
if cached is not None:
|
| 1606 |
response.headers.update(_cc(1800))
|
| 1607 |
return cached
|
| 1608 |
|
| 1609 |
-
ytm = get_ytm(
|
| 1610 |
episodes_raw: list = []
|
| 1611 |
meta: dict = {}
|
| 1612 |
|
|
@@ -1714,14 +1754,19 @@ async def get_watch_playlist(
|
|
| 1714 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1715 |
|
| 1716 |
@app.get("/lyrics/{browse_id}")
|
| 1717 |
-
async def get_lyrics(
|
| 1718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1719 |
cached = _cache_long.get(cache_key)
|
| 1720 |
if cached is not None:
|
| 1721 |
response.headers.update(_cc(7200))
|
| 1722 |
return cached
|
| 1723 |
try:
|
| 1724 |
-
data = await run(get_ytm().get_lyrics, browse_id)
|
| 1725 |
_cache_long.set(cache_key, data)
|
| 1726 |
response.headers.update(_cc(7200))
|
| 1727 |
return data
|
|
@@ -1730,14 +1775,19 @@ async def get_lyrics(response: Response, browse_id: str):
|
|
| 1730 |
|
| 1731 |
|
| 1732 |
@app.get("/lyrics_by_video/{video_id}")
|
| 1733 |
-
async def get_lyrics_by_video(
|
| 1734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1735 |
cached = _cache_long.get(cache_key)
|
| 1736 |
if cached is not None:
|
| 1737 |
response.headers.update(_cc(7200))
|
| 1738 |
return cached
|
| 1739 |
|
| 1740 |
-
ytm = get_ytm()
|
| 1741 |
try:
|
| 1742 |
watch = await run(ytm.get_watch_playlist, video_id, None, 1)
|
| 1743 |
lyrics_id = watch.get("lyrics")
|
|
@@ -2131,14 +2181,15 @@ async def get_explore(
|
|
| 2131 |
async def get_user(
|
| 2132 |
response: Response,
|
| 2133 |
channel_id: str,
|
|
|
|
| 2134 |
language: str = Query("en"),
|
| 2135 |
):
|
| 2136 |
-
cache_key = f"user:{channel_id}:{language}"
|
| 2137 |
cached = _cache_long.get(cache_key)
|
| 2138 |
if cached is not None:
|
| 2139 |
return cached
|
| 2140 |
try:
|
| 2141 |
-
data = await run(get_ytm(
|
| 2142 |
_cache_long.set(cache_key, data)
|
| 2143 |
return data
|
| 2144 |
except Exception as e:
|
|
@@ -2150,10 +2201,11 @@ async def get_user_playlists(
|
|
| 2150 |
response: Response,
|
| 2151 |
channel_id: str,
|
| 2152 |
params: Optional[str] = None,
|
|
|
|
| 2153 |
language: str = Query("en"),
|
| 2154 |
):
|
| 2155 |
try:
|
| 2156 |
-
data = await run(get_ytm(
|
| 2157 |
return data
|
| 2158 |
except Exception as e:
|
| 2159 |
raise HTTPException(500, detail=str(e))
|
|
|
|
| 1 |
"""
|
| 2 |
+
YouTube Music API Wrapper β v7 (Production / Render 512 MB)
|
| 3 |
=============================================================
|
| 4 |
|
| 5 |
WHAT'S NEW IN v6
|
|
|
|
| 61 |
from contextlib import asynccontextmanager
|
| 62 |
from typing import Any, Dict, List, Optional
|
| 63 |
|
|
|
|
| 64 |
import diskcache
|
| 65 |
import httpx
|
| 66 |
from fastapi import FastAPI, HTTPException, Query, Request, Response
|
|
|
|
| 69 |
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 70 |
from slowapi.errors import RateLimitExceeded
|
| 71 |
from slowapi.util import get_remote_address
|
|
|
|
|
|
|
| 72 |
from ytmusicapi import YTMusic
|
| 73 |
+
# At the top with your other imports
|
| 74 |
+
import base64
|
| 75 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 76 |
+
from fastapi import Request
|
| 77 |
+
from fastapi.responses import Response
|
| 78 |
from download_bp import router as download_router
|
| 79 |
|
| 80 |
+
# /docs Basic Auth middleware
|
| 81 |
+
DOCS_PASSWORD = os.environ.get("DOCS_PASSWORD", "prakamya05")
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
class DocsAuthMiddleware(BaseHTTPMiddleware):
|
| 84 |
async def dispatch(self, request: Request, call_next):
|
| 85 |
if request.url.path in ("/docs", "/openapi.json", "/redoc"):
|
|
|
|
|
|
|
| 86 |
auth = request.headers.get("Authorization", "")
|
| 87 |
if auth.startswith("Basic "):
|
| 88 |
try:
|
|
|
|
| 92 |
return await call_next(request)
|
| 93 |
except Exception:
|
| 94 |
pass
|
| 95 |
+
return Response(
|
| 96 |
"Unauthorized", status_code=401,
|
| 97 |
headers={"WWW-Authenticate": 'Basic realm="Musicanaz Docs"'},
|
| 98 |
)
|
| 99 |
return await call_next(request)
|
| 100 |
|
| 101 |
+
# Register middleware + router (after app = FastAPI(...))
|
| 102 |
+
app.add_middleware(DocsAuthMiddleware)
|
| 103 |
+
app.include_router(download_router)
|
| 104 |
+
|
| 105 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
# Logging
|
| 107 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 272 |
|
| 273 |
|
| 274 |
def _get_http_client() -> httpx.AsyncClient:
|
| 275 |
+
"""
|
| 276 |
+
Shared async HTTP client for external APIs (iTunes, Deezer).
|
| 277 |
+
Routes through the rotating proxy when PROXY_USERNAME / PROXY_PASSWORD are set
|
| 278 |
+
so all outbound traffic uses the same IP pool as ytmusicapi.
|
| 279 |
+
"""
|
| 280 |
global _http_client
|
| 281 |
if _http_client is None or _http_client.is_closed:
|
| 282 |
+
proxy_url = (
|
| 283 |
+
f"http://{_PROXY_USER}:{_PROXY_PASS}@{_PROXY_HOST}:{_PROXY_PORT}"
|
| 284 |
+
if _PROXY_USER and _PROXY_PASS else None
|
| 285 |
+
)
|
| 286 |
_http_client = httpx.AsyncClient(
|
| 287 |
timeout=httpx.Timeout(10.0),
|
| 288 |
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
|
| 289 |
follow_redirects=True,
|
| 290 |
headers={"User-Agent": "MusicAPI/6.0 (compatible; ytmusicapi)"},
|
| 291 |
+
**({"proxy": proxy_url} if proxy_url else {}),
|
| 292 |
)
|
| 293 |
return _http_client
|
| 294 |
|
|
|
|
| 305 |
@asynccontextmanager
|
| 306 |
async def lifespan(app: FastAPI):
|
| 307 |
# Startup
|
| 308 |
+
log.info("Starting YouTube Music API v7 β¦")
|
| 309 |
asyncio.create_task(_background_warmup())
|
| 310 |
asyncio.create_task(_auto_refresh_loop())
|
| 311 |
yield
|
|
|
|
| 327 |
"Production-grade FastAPI wrapper for ytmusicapi. "
|
| 328 |
"Two-level cache, rate limiting, iTunes + Deezer enrichment, circuit breaker."
|
| 329 |
),
|
| 330 |
+
version="7.0.0",
|
| 331 |
lifespan=lifespan,
|
| 332 |
)
|
| 333 |
|
| 334 |
app.state.limiter = limiter
|
| 335 |
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
|
|
|
| 336 |
app.add_middleware(GZipMiddleware, minimum_size=300)
|
| 337 |
app.add_middleware(
|
| 338 |
CORSMiddleware,
|
| 339 |
allow_origins=["*"],
|
| 340 |
+
allow_methods=["GET", "DELETE"],
|
| 341 |
allow_headers=["*"],
|
| 342 |
)
|
|
|
|
| 343 |
|
| 344 |
# 4 workers: ytmusicapi is I/O-bound β more workers waste RAM
|
| 345 |
_executor = ThreadPoolExecutor(max_workers=4)
|
|
|
|
| 347 |
|
| 348 |
async def run(fn, *args, **kwargs):
|
| 349 |
loop = asyncio.get_running_loop() # get_event_loop() removed in Python 3.12
|
| 350 |
+
def _safe_call():
|
| 351 |
+
try:
|
| 352 |
+
return fn(*args, **kwargs)
|
| 353 |
+
except StopIteration as exc:
|
| 354 |
+
# PEP 479: StopIteration raised inside a generator propagated through
|
| 355 |
+
# run_in_executor causes "TypeError: StopIteration interacts badly with
|
| 356 |
+
# generators". Convert it to RuntimeError so asyncio handles it cleanly.
|
| 357 |
+
raise RuntimeError(f"ytmusicapi StopIteration caught: {exc}") from exc
|
| 358 |
+
return await loop.run_in_executor(_executor, _safe_call)
|
| 359 |
|
| 360 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 361 |
# Per-locale YTMusic instance pool (LRU, capped at 12)
|
|
|
|
| 916 |
# Health + admin
|
| 917 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 918 |
|
| 919 |
+
@app.get("/", include_in_schema=False)
|
| 920 |
+
async def root():
|
| 921 |
+
"""Root route β satisfies HF Spaces / Render health-check pings on /."""
|
| 922 |
+
return {"name": "Musicanaz API", "version": "6.0.0", "status": "ok", "docs": "/docs"}
|
| 923 |
+
|
| 924 |
+
|
| 925 |
@app.get("/health", tags=["meta"])
|
| 926 |
async def health():
|
| 927 |
hits, misses = _disk_cache.stats()
|
| 928 |
return {
|
| 929 |
"status": "ok",
|
| 930 |
+
"version": "7.0.0",
|
| 931 |
"uptime_seconds": round(time.monotonic() - _START_TIME),
|
| 932 |
"ytm_instances": len(_ytm_pool),
|
| 933 |
"proxy_enabled": _PROXIES is not None,
|
|
|
|
| 1039 |
response: Response,
|
| 1040 |
query: str,
|
| 1041 |
detailed_runs: bool = False,
|
| 1042 |
+
country: str = Query("ZZ"),
|
| 1043 |
language: str = Query("en"),
|
| 1044 |
):
|
| 1045 |
+
cache_key = f"suggest:{query}:{country}:{language}"
|
| 1046 |
cached = _cache_short.get(cache_key)
|
| 1047 |
if cached is not None:
|
| 1048 |
response.headers.update(_cc(30))
|
| 1049 |
return cached
|
| 1050 |
try:
|
| 1051 |
raw = await run(
|
| 1052 |
+
get_ytm(country, language).get_search_suggestions, query, detailed_runs
|
| 1053 |
)
|
| 1054 |
suggestions: list[str] = []
|
| 1055 |
if isinstance(raw, list):
|
|
|
|
| 1076 |
request: Request,
|
| 1077 |
response: Response,
|
| 1078 |
artist_id: str,
|
| 1079 |
+
country: str = Query("ZZ"),
|
| 1080 |
language: str = Query("en"),
|
| 1081 |
):
|
| 1082 |
+
cache_key = f"artist:{artist_id}:{country}:{language}"
|
| 1083 |
cached = _cache_long.get(cache_key)
|
| 1084 |
if cached is not None:
|
| 1085 |
response.headers.update(_cc(600))
|
| 1086 |
return cached
|
| 1087 |
try:
|
| 1088 |
+
data = await run(get_ytm(country, language).get_artist, artist_id)
|
| 1089 |
if not data:
|
| 1090 |
raise HTTPException(404, "Artist not found")
|
| 1091 |
data["thumbnails"] = best_thumbnails_list(data.get("thumbnails") or [])
|
|
|
|
| 1124 |
limit: int = Query(500, ge=1, le=5000),
|
| 1125 |
language: str = Query("en"),
|
| 1126 |
):
|
| 1127 |
+
cache_key = f"artist_songs:{artist_id}:{limit}:{country}:{language}"
|
| 1128 |
cached = _cache_long.get(cache_key)
|
| 1129 |
if cached is not None:
|
| 1130 |
response.headers.update(_cc(600))
|
| 1131 |
return cached
|
| 1132 |
try:
|
| 1133 |
+
ytm = get_ytm(country, language)
|
| 1134 |
artist_data = await run(ytm.get_artist, artist_id)
|
| 1135 |
if not artist_data:
|
| 1136 |
raise HTTPException(404, "Artist not found")
|
|
|
|
| 1224 |
request: Request,
|
| 1225 |
response: Response,
|
| 1226 |
artist_id: str,
|
| 1227 |
+
country: str = Query("ZZ"),
|
| 1228 |
language: str = Query("en"),
|
| 1229 |
):
|
| 1230 |
+
cache_key = f"artist_albums:{artist_id}:{country}:{language}"
|
| 1231 |
cached = _cache_long.get(cache_key)
|
| 1232 |
if cached is not None:
|
| 1233 |
response.headers.update(_cc(600))
|
| 1234 |
return cached
|
| 1235 |
try:
|
| 1236 |
+
ytm = get_ytm(country, language)
|
| 1237 |
artist_data = await run(ytm.get_artist, artist_id)
|
| 1238 |
if not artist_data:
|
| 1239 |
raise HTTPException(404, "Artist not found")
|
|
|
|
| 1322 |
request: Request,
|
| 1323 |
response: Response,
|
| 1324 |
album_id: str,
|
| 1325 |
+
country: str = Query("ZZ"),
|
| 1326 |
language: str = Query("en"),
|
| 1327 |
):
|
| 1328 |
+
cache_key = f"album:{album_id}:{country}:{language}"
|
| 1329 |
cached = _cache_long.get(cache_key)
|
| 1330 |
if cached is not None:
|
| 1331 |
response.headers.update(_cc(3600))
|
| 1332 |
return cached
|
| 1333 |
try:
|
| 1334 |
+
data = await run(get_ytm(country, language).get_album, album_id)
|
| 1335 |
if not data:
|
| 1336 |
raise HTTPException(404, "Album not found")
|
| 1337 |
data["thumbnails"] = best_thumbnails_list(data.get("thumbnails") or [])
|
|
|
|
| 1351 |
|
| 1352 |
@app.get("/song/{video_id}")
|
| 1353 |
@limiter.limit("60/minute")
|
| 1354 |
+
async def get_song(
|
| 1355 |
+
request: Request,
|
| 1356 |
+
response: Response,
|
| 1357 |
+
video_id: str,
|
| 1358 |
+
country: str = Query("ZZ"),
|
| 1359 |
+
language: str = Query("en"),
|
| 1360 |
+
):
|
| 1361 |
cache_key = f"song:{video_id}"
|
| 1362 |
cached = _cache_long.get(cache_key)
|
| 1363 |
if cached is not None:
|
| 1364 |
response.headers.update(_cc(3600))
|
| 1365 |
return cached
|
| 1366 |
try:
|
| 1367 |
+
data = await run(get_ytm(country, language).get_song, video_id)
|
| 1368 |
raw = (
|
| 1369 |
data.get("thumbnail", {}).get("thumbnails") or
|
| 1370 |
data.get("thumbnails") or []
|
|
|
|
| 1384 |
|
| 1385 |
@app.get("/stream/{video_id}")
|
| 1386 |
@limiter.limit("60/minute")
|
| 1387 |
+
async def get_stream(
|
| 1388 |
+
request: Request,
|
| 1389 |
+
response: Response,
|
| 1390 |
+
video_id: str,
|
| 1391 |
+
country: str = Query("ZZ"),
|
| 1392 |
+
language: str = Query("en"),
|
| 1393 |
+
):
|
| 1394 |
cache_key = f"stream:{video_id}"
|
| 1395 |
cached = _cache_long.get(cache_key)
|
| 1396 |
if cached is not None:
|
| 1397 |
response.headers.update(_cc(3600))
|
| 1398 |
return cached
|
| 1399 |
try:
|
| 1400 |
+
song_data = await run(get_ytm(country, language).get_song, video_id)
|
| 1401 |
vd = song_data.get("videoDetails", {})
|
| 1402 |
if not vd:
|
| 1403 |
raise HTTPException(404, "Video not found")
|
|
|
|
| 1598 |
limit: int = Query(100, ge=1, le=500),
|
| 1599 |
related: bool = False,
|
| 1600 |
suggestions_limit: int = 0,
|
| 1601 |
+
country: str = Query("ZZ"),
|
| 1602 |
language: str = Query("en"),
|
| 1603 |
):
|
| 1604 |
clean_id = playlist_id[2:] if playlist_id.startswith("VL") else playlist_id
|
| 1605 |
+
cache_key = f"playlist:{clean_id}:{limit}:{country}:{language}"
|
| 1606 |
cached = _cache_medium.get(cache_key)
|
| 1607 |
if cached is not None:
|
| 1608 |
response.headers.update(_cc(300))
|
| 1609 |
return cached
|
| 1610 |
try:
|
| 1611 |
data = await run(
|
| 1612 |
+
get_ytm(country, language).get_playlist,
|
| 1613 |
clean_id, limit, related, suggestions_limit,
|
| 1614 |
)
|
| 1615 |
if not data:
|
|
|
|
| 1636 |
response: Response,
|
| 1637 |
podcast_id: str,
|
| 1638 |
limit: int = Query(50, ge=1, le=200),
|
| 1639 |
+
country: str = Query("ZZ"),
|
| 1640 |
language: str = Query("en"),
|
| 1641 |
):
|
| 1642 |
clean_id = podcast_id[2:] if (podcast_id.startswith("VL") and not podcast_id.startswith("VLM")) else podcast_id
|
| 1643 |
+
cache_key = f"podcast:{clean_id}:{limit}:{country}:{language}"
|
| 1644 |
cached = _cache_long.get(cache_key)
|
| 1645 |
if cached is not None:
|
| 1646 |
response.headers.update(_cc(1800))
|
| 1647 |
return cached
|
| 1648 |
|
| 1649 |
+
ytm = get_ytm(country, language)
|
| 1650 |
episodes_raw: list = []
|
| 1651 |
meta: dict = {}
|
| 1652 |
|
|
|
|
| 1754 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1755 |
|
| 1756 |
@app.get("/lyrics/{browse_id}")
|
| 1757 |
+
async def get_lyrics(
|
| 1758 |
+
response: Response,
|
| 1759 |
+
browse_id: str,
|
| 1760 |
+
country: str = Query("ZZ"),
|
| 1761 |
+
language: str = Query("en"),
|
| 1762 |
+
):
|
| 1763 |
+
cache_key = f"lyrics:{browse_id}:{country}:{language}"
|
| 1764 |
cached = _cache_long.get(cache_key)
|
| 1765 |
if cached is not None:
|
| 1766 |
response.headers.update(_cc(7200))
|
| 1767 |
return cached
|
| 1768 |
try:
|
| 1769 |
+
data = await run(get_ytm(country, language).get_lyrics, browse_id)
|
| 1770 |
_cache_long.set(cache_key, data)
|
| 1771 |
response.headers.update(_cc(7200))
|
| 1772 |
return data
|
|
|
|
| 1775 |
|
| 1776 |
|
| 1777 |
@app.get("/lyrics_by_video/{video_id}")
|
| 1778 |
+
async def get_lyrics_by_video(
|
| 1779 |
+
response: Response,
|
| 1780 |
+
video_id: str,
|
| 1781 |
+
country: str = Query("ZZ"),
|
| 1782 |
+
language: str = Query("en"),
|
| 1783 |
+
):
|
| 1784 |
+
cache_key = f"lyrics_vid:{video_id}:{country}:{language}"
|
| 1785 |
cached = _cache_long.get(cache_key)
|
| 1786 |
if cached is not None:
|
| 1787 |
response.headers.update(_cc(7200))
|
| 1788 |
return cached
|
| 1789 |
|
| 1790 |
+
ytm = get_ytm(country, language)
|
| 1791 |
try:
|
| 1792 |
watch = await run(ytm.get_watch_playlist, video_id, None, 1)
|
| 1793 |
lyrics_id = watch.get("lyrics")
|
|
|
|
| 2181 |
async def get_user(
|
| 2182 |
response: Response,
|
| 2183 |
channel_id: str,
|
| 2184 |
+
country: str = Query("ZZ"),
|
| 2185 |
language: str = Query("en"),
|
| 2186 |
):
|
| 2187 |
+
cache_key = f"user:{channel_id}:{country}:{language}"
|
| 2188 |
cached = _cache_long.get(cache_key)
|
| 2189 |
if cached is not None:
|
| 2190 |
return cached
|
| 2191 |
try:
|
| 2192 |
+
data = await run(get_ytm(country, language).get_user, channel_id)
|
| 2193 |
_cache_long.set(cache_key, data)
|
| 2194 |
return data
|
| 2195 |
except Exception as e:
|
|
|
|
| 2201 |
response: Response,
|
| 2202 |
channel_id: str,
|
| 2203 |
params: Optional[str] = None,
|
| 2204 |
+
country: str = Query("ZZ"),
|
| 2205 |
language: str = Query("en"),
|
| 2206 |
):
|
| 2207 |
try:
|
| 2208 |
+
data = await run(get_ytm(country, language).get_user_playlists, channel_id, params)
|
| 2209 |
return data
|
| 2210 |
except Exception as e:
|
| 2211 |
raise HTTPException(500, detail=str(e))
|