Fix: Add fallback logic for stream fetching and implement browse endpoint
This commit is contained in:
parent
701b146dc2
commit
74748ec86f
3 changed files with 120 additions and 36 deletions
15
backend/api/endpoints/browse.py
Normal file
15
backend/api/endpoints/browse.py
Normal 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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
# 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']}}},
|
||||
]
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(url, download=False)
|
||||
stream_url = info.get('url')
|
||||
|
||||
if stream_url:
|
||||
# Extract headers that yt-dlp used/recommends
|
||||
headers = info.get('http_headers', {})
|
||||
result = {
|
||||
"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))
|
||||
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": []}
|
||||
|
|
|
|||
Loading…
Reference in a new issue