Fix: Add fallback logic for stream fetching and implement browse endpoint

This commit is contained in:
Your Name 2026-01-01 13:43:26 +07:00
parent 701b146dc2
commit 74748ec86f
3 changed files with 120 additions and 36 deletions

View file

@ -0,0 +1,15 @@
from fastapi import APIRouter, Depends
from backend.services.youtube import YouTubeService
router = APIRouter()
def get_youtube_service():
return YouTubeService()
@router.get("/browse")
async def get_browse_content(yt: YouTubeService = Depends(get_youtube_service)):
return yt.get_home()
@router.get("/trending")
async def get_trending(yt: YouTubeService = Depends(get_youtube_service)):
return yt.get_trending()

View file

@ -5,15 +5,15 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import os
from backend.core.config import settings # Renamed to settings_config to avoid conflict
from backend.api.endpoints import playlists, search, stream, lyrics, settings as settings_router # Aliased settings router
from backend.core.config import settings as settings_config # Renamed to settings_config to avoid conflict
from backend.api.endpoints import playlists, search, stream, lyrics, settings as settings_router, browse # Aliased settings router
app = FastAPI(title=settings.APP_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json") # Used settings_config
app = FastAPI(title=settings_config.APP_NAME, openapi_url=f"{settings_config.API_V1_STR}/openapi.json") # Used settings_config
# CORS setup
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS, # Used settings_config
allow_origins=settings_config.BACKEND_CORS_ORIGINS, # Used settings_config
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -25,12 +25,13 @@ api_router.include_router(playlists.router, prefix="/playlists", tags=["playlist
api_router.include_router(search.router, tags=["search"])
api_router.include_router(stream.router, tags=["stream"])
api_router.include_router(lyrics.router, tags=["lyrics"])
api_router.include_router(browse.router, tags=["browse"])
api_router.include_router(settings_router.router, prefix="/settings", tags=["settings"]) # Included settings_router
app.include_router(api_router, prefix=settings.API_V1_STR) # Corrected prefix and removed extra tags
app.include_router(api_router, prefix=settings_config.API_V1_STR) # Corrected prefix and removed extra tags
# Serve Static Frontend (Production Mode)
if settings.CACHE_DIR.parent.name == "backend":
if settings_config.CACHE_DIR.parent.name == "backend":
# assuming running from root
STATIC_DIR = "static"
else:

View file

@ -159,36 +159,51 @@ class YouTubeService:
cached = self.cache.get(cache_key)
if cached: return cached
try:
url = f"https://www.youtube.com/watch?v={id}"
ydl_opts = {
'format': 'bestaudio[ext=m4a]/best[ext=mp4]/best',
'quiet': True,
'noplaylist': True,
'force_ipv4': True,
# Use mobile clients to avoid web scraping blocks
'extractor_args': {
'youtube': {
'player_client': ['ios', 'android', 'web']
}
}
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
stream_url = info.get('url')
# Strategy: Try versatile clients in order
clients_to_try = [
# 1. iOS (often best for audio)
{'extractor_args': {'youtube': {'player_client': ['ios']}}},
# 2. Android (robust)
{'extractor_args': {'youtube': {'player_client': ['android']}}},
# 3. Web (standard, prone to 403)
{'extractor_args': {'youtube': {'player_client': ['web']}}},
# 4. TV (sometimes works for age-gated)
{'extractor_args': {'youtube': {'player_client': ['tv']}}},
]
if stream_url:
# Extract headers that yt-dlp used/recommends
headers = info.get('http_headers', {})
result = {
"url": stream_url,
"headers": headers
last_error = None
for client_config in clients_to_try:
try:
url = f"https://www.youtube.com/watch?v={id}"
ydl_opts = {
'format': 'bestaudio[ext=m4a]/best[ext=mp4]/best',
'quiet': True,
'noplaylist': True,
'force_ipv4': True,
}
self.cache.set(cache_key, result, ttl_seconds=3600)
return result
raise ResourceNotFound("Stream not found")
except Exception as e:
raise ExternalAPIError(str(e))
ydl_opts.update(client_config)
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
stream_url = info.get('url')
if stream_url:
headers = info.get('http_headers', {})
result = {
"url": stream_url,
"headers": headers
}
self.cache.set(cache_key, result, ttl_seconds=3600)
return result
except Exception as e:
last_error = e
print(f"Fetch failed with client {client_config}: {e}")
continue
# If all fail
print(f"All clients failed for {id}. Last error: {last_error}")
raise ExternalAPIError(str(last_error))
def invalidate_stream_cache(self, id: str):
cache_key = f"stream:{id}"
@ -228,6 +243,59 @@ class YouTubeService:
response = {"tracks": tracks}
self.cache.set(cache_key, response, ttl_seconds=3600)
return response
except Exception as e:
print(f"Rec Error: {e}")
return {"tracks": []}
def get_home(self):
cache_key = "home:browse"
cached = self.cache.get(cache_key)
if cached: return cached
try:
# ytmusicapi `get_home` returns complex Sections
# For simplicity, we'll fetch charts and new releases as "Browse" content
charts = self.yt.get_charts(country="US")
# Formating Charts
trending_songs = []
if 'videos' in charts and 'items' in charts['videos']:
for track in charts['videos']['items']:
trending_songs.append({
"title": track.get('title', 'Unknown'),
"artist": self._extract_artist_names(track),
"album": "Trending",
"duration": 0, # Charts often lack duration
"cover_url": self._get_high_res_thumbnail(track.get('thumbnails', [])),
"id": track.get('videoId'),
"url": f"https://music.youtube.com/watch?v={track.get('videoId')}"
})
# New Releases (using search for "New Songs" as proxy or actual new releases if supported)
# Actually ytmusicapi has get_new_releases usually under get_charts or specific calls
# We'll use get_charts "trending" for "Trending" category
# And maybe "Top Songs" for "Top Hits"
response = {
"Trending": [{
"id": "trending",
"title": "Trending Now",
"description": "Top music videos right now",
"cover_url": trending_songs[0]['cover_url'] if trending_songs else "",
"tracks": trending_songs,
"type": "Playlist",
"creator": "YouTube Charts"
}]
}
self.cache.set(cache_key, response, ttl_seconds=3600)
return response
except Exception as e:
print(f"Home Error: {e}")
return {}
def get_trending(self):
# Dedicated trending endpoint
home = self.get_home()
if "Trending" in home and home["Trending"]:
return {"tracks": home["Trending"][0]["tracks"]}
return {"tracks": []}