Shaurya commited on
Commit
a3f95cd
Β·
verified Β·
1 Parent(s): 4ff942f

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -47
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- YouTube Music API Wrapper β€” v6 (Production / Render 512 MB)
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
- # /docs Basic Auth middleware (registered after app = FastAPI() below)
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 StarletteResponse(
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 v6 …")
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="6.0.0",
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", "POST", "DELETE"], # POST required for /download/start + /done
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
- return await loop.run_in_executor(_executor, lambda: fn(*args, **kwargs))
 
 
 
 
 
 
 
 
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": "6.0.0",
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("ZZ", language).get_search_suggestions, query, detailed_runs
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("ZZ", language).get_artist, artist_id)
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("ZZ", language)
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("ZZ", language)
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("ZZ", language).get_album, album_id)
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(request: Request, response: Response, video_id: str):
 
 
 
 
 
 
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(request: Request, response: Response, video_id: str):
 
 
 
 
 
 
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("ZZ", language).get_playlist,
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("ZZ", language)
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(response: Response, browse_id: str):
1718
- cache_key = f"lyrics:{browse_id}"
 
 
 
 
 
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(response: Response, video_id: str):
1734
- cache_key = f"lyrics_vid:{video_id}"
 
 
 
 
 
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("ZZ", language).get_user, channel_id)
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("ZZ", language).get_user_playlists, channel_id, params)
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))