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 from fastapi.responses import FileResponse
import os import os
from backend.core.config import settings # Renamed to settings_config to avoid conflict 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 # Aliased settings router 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 # CORS setup
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS, # Used settings_config allow_origins=settings_config.BACKEND_CORS_ORIGINS, # Used settings_config
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], 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(search.router, tags=["search"])
api_router.include_router(stream.router, tags=["stream"]) api_router.include_router(stream.router, tags=["stream"])
api_router.include_router(lyrics.router, tags=["lyrics"]) 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 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) # Serve Static Frontend (Production Mode)
if settings.CACHE_DIR.parent.name == "backend": if settings_config.CACHE_DIR.parent.name == "backend":
# assuming running from root # assuming running from root
STATIC_DIR = "static" STATIC_DIR = "static"
else: else:

View file

@ -159,36 +159,51 @@ class YouTubeService:
cached = self.cache.get(cache_key) cached = self.cache.get(cache_key)
if cached: return cached if cached: return cached
try: # Strategy: Try versatile clients in order
url = f"https://www.youtube.com/watch?v={id}" clients_to_try = [
ydl_opts = { # 1. iOS (often best for audio)
'format': 'bestaudio[ext=m4a]/best[ext=mp4]/best', {'extractor_args': {'youtube': {'player_client': ['ios']}}},
'quiet': True, # 2. Android (robust)
'noplaylist': True, {'extractor_args': {'youtube': {'player_client': ['android']}}},
'force_ipv4': True, # 3. Web (standard, prone to 403)
# Use mobile clients to avoid web scraping blocks {'extractor_args': {'youtube': {'player_client': ['web']}}},
'extractor_args': { # 4. TV (sometimes works for age-gated)
'youtube': { {'extractor_args': {'youtube': {'player_client': ['tv']}}},
'player_client': ['ios', 'android', 'web'] ]
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,
}
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
with yt_dlp.YoutubeDL(ydl_opts) as ydl: except Exception as e:
info = ydl.extract_info(url, download=False) last_error = e
stream_url = info.get('url') print(f"Fetch failed with client {client_config}: {e}")
continue
if stream_url:
# Extract headers that yt-dlp used/recommends # If all fail
headers = info.get('http_headers', {}) print(f"All clients failed for {id}. Last error: {last_error}")
result = { raise ExternalAPIError(str(last_error))
"url": stream_url,
"headers": headers
}
self.cache.set(cache_key, result, ttl_seconds=3600)
return result
raise ResourceNotFound("Stream not found")
except Exception as e:
raise ExternalAPIError(str(e))
def invalidate_stream_cache(self, id: str): def invalidate_stream_cache(self, id: str):
cache_key = f"stream:{id}" cache_key = f"stream:{id}"
@ -228,6 +243,59 @@ class YouTubeService:
response = {"tracks": tracks} response = {"tracks": tracks}
self.cache.set(cache_key, response, ttl_seconds=3600) self.cache.set(cache_key, response, ttl_seconds=3600)
return response return response
except Exception as e:
print(f"Rec Error: {e}") print(f"Rec Error: {e}")
return {"tracks": []} 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": []}