Compare commits

..

No commits in common. "9129b9ad5423c57a6bcdc9d37f51e44a0a7131ac" and "20edeefeaddc406c3d76a111beb2a4b610896581" have entirely different histories.

5 changed files with 36 additions and 113 deletions

View file

@ -1,36 +1,19 @@
from fastapi import APIRouter, HTTPException, BackgroundTasks, Response from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
import json import json
from pathlib import Path from pathlib import Path
import yt_dlp import yt_dlp
import requests import requests
from backend.services.spotify import SpotifyService from backend.cache_manager import CacheManager
from backend.services.cache import CacheManager
from backend.playlist_manager import PlaylistManager from backend.playlist_manager import PlaylistManager
from backend.scheduler import update_ytdlp # Import update function
import re import re
router = APIRouter() router = APIRouter()
# Services (Assumed to be initialized elsewhere if not here, adhering to existing patterns) cache = CacheManager()
# spotify = SpotifyService() # Commented out as duplicates if already imported
if 'CacheManager' in globals():
cache = CacheManager()
else:
from backend.cache_manager import CacheManager
cache = CacheManager()
playlist_manager = PlaylistManager() playlist_manager = PlaylistManager()
@router.post("/system/update-ytdlp")
async def manual_ytdlp_update(background_tasks: BackgroundTasks):
"""
Trigger a manual update of yt-dlp in the background.
"""
background_tasks.add_task(update_ytdlp)
return {"status": "success", "message": "yt-dlp update started in background"}
def get_high_res_thumbnail(thumbnails: list) -> str: def get_high_res_thumbnail(thumbnails: list) -> str:
""" """
Selects the best thumbnail and attempts to upgrade resolution Selects the best thumbnail and attempts to upgrade resolution
@ -625,8 +608,7 @@ async def stream_audio(id: str):
""" """
try: try:
# Check Cache for stream URL # Check Cache for stream URL
# Check Cache for stream URL cache_key = f"v2:stream:{id}" # v2 cache key for new format
cache_key = f"v8:stream:{id}" # v8 cache key - deep debug mode
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
stream_url = None stream_url = None
@ -644,7 +626,6 @@ async def stream_audio(id: str):
print(f"DEBUG: Fetching new stream URL for '{id}'") print(f"DEBUG: Fetching new stream URL for '{id}'")
url = f"https://www.youtube.com/watch?v={id}" url = f"https://www.youtube.com/watch?v={id}"
ydl_opts = { ydl_opts = {
# Try standard bestaudio but prefer m4a. Removed protocol constraint to see what we actually get.
'format': 'bestaudio[ext=m4a]/bestaudio/best', 'format': 'bestaudio[ext=m4a]/bestaudio/best',
'quiet': True, 'quiet': True,
'noplaylist': True, 'noplaylist': True,
@ -653,7 +634,22 @@ async def stream_audio(id: str):
'socket_timeout': 30, 'socket_timeout': 30,
'retries': 3, 'retries': 3,
'force_ipv4': True, 'force_ipv4': True,
'extractor_args': {'youtube': {'player_client': ['ios', 'android', 'web']}}, 'extractor_args': {'youtube': {'player_client': ['ios', 'android']}},
}
if not stream_url:
print(f"DEBUG: Fetching new stream URL for '{id}'")
url = f"https://www.youtube.com/watch?v={id}"
ydl_opts = {
'format': 'bestaudio[ext=m4a][protocol^=http]/bestaudio[protocol^=http]/best[protocol^=http]', # Strictly exclude m3u8/HLS
'quiet': True,
'noplaylist': True,
'nocheckcertificate': True,
'geo_bypass': True,
'socket_timeout': 30,
'retries': 3,
'force_ipv4': True,
'extractor_args': {'youtube': {'player_client': ['android', 'web', 'ios']}}, # Android often gives good progressive streams
} }
try: try:
@ -664,58 +660,48 @@ async def stream_audio(id: str):
http_headers = info.get('http_headers', {}) # Get headers required for the URL http_headers = info.get('http_headers', {}) # Get headers required for the URL
# Determine MIME type # Determine MIME type
if ext == 'm4a' or ext == 'mp4': if ext == 'm4a':
mime_type = "audio/mp4" mime_type = "audio/mp4"
elif ext == 'webm': elif ext == 'webm':
mime_type = "audio/webm" mime_type = "audio/webm"
else: else:
mime_type = "audio/mpeg" mime_type = "audio/mpeg"
print(f"DEBUG: Got stream URL format: {info.get('format')}, ext: {ext}, mime: {mime_type}", flush=True) print(f"DEBUG: Got stream URL format: {info.get('format')}, ext: {ext}, mime: {mime_type}")
except Exception as ydl_error: except Exception as ydl_error:
print(f"DEBUG: yt-dlp extraction error: {type(ydl_error).__name__}: {str(ydl_error)}", flush=True) print(f"DEBUG: yt-dlp extraction error: {type(ydl_error).__name__}: {str(ydl_error)}")
raise ydl_error raise ydl_error
if stream_url: if stream_url:
cached_data = {"url": stream_url, "mime": mime_type, "headers": http_headers} cache_data = {"url": stream_url, "mime": mime_type, "headers": http_headers}
cache.set(cache_key, cached_data, ttl_seconds=3600) cache.set(cache_key, cache_data, ttl_seconds=3600)
if not stream_url: if not stream_url:
raise HTTPException(status_code=404, detail="Audio stream not found") raise HTTPException(status_code=404, detail="Audio stream not found")
print(f"Streaming {id} with Content-Type: {mime_type}", flush=True) print(f"Streaming {id} with Content-Type: {mime_type}")
# Pre-open the connection to verify it works and get headers # Pre-open the connection to verify it works and get headers
try: try:
# Sanitize headers: prevent Host/Cookie conflicts, but keep User-Agent and Cookies # Sanitize headers: prevent Host/Cookie conflicts, but keep User-Agent
base_headers = {} base_headers = cached_data.get('headers', {}) if 'cached_data' in locals() else http_headers
if 'http_headers' in locals():
base_headers = http_headers
elif cached_data and isinstance(cached_data, dict):
base_headers = cached_data.get('headers', {})
req_headers = { req_headers = {
'User-Agent': base_headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'), 'User-Agent': base_headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'),
'Referer': 'https://www.youtube.com/', 'Referer': 'https://www.youtube.com/',
'Accept': '*/*', 'Accept': '*/*',
'Accept-Language': base_headers.get('Accept-Language', 'en-US,en;q=0.9'),
} }
if 'Cookie' in base_headers:
req_headers['Cookie'] = base_headers['Cookie']
# Disable SSL verify to match yt-dlp 'nocheckcertificate' (fixes NAS CA issues) # Disable SSL verify to match yt-dlp 'nocheckcertificate' (fixes NAS CA issues)
external_req = requests.get(stream_url, stream=True, timeout=30, headers=req_headers, verify=False) external_req = requests.get(stream_url, stream=True, timeout=30, headers=req_headers, verify=False)
external_req.raise_for_status() external_req.raise_for_status()
except requests.exceptions.HTTPError as http_err: except requests.exceptions.HTTPError as http_err:
error_details = f"Upstream error: {http_err.response.status_code}" print(f"DEBUG: Stream Pre-flight HTTP Error: {http_err}")
print(f"Stream Error: {error_details}")
# If 403/404/410, invalidate cache # If 403/404/410, invalidate cache
if http_err.response.status_code in [403, 404, 410]: if http_err.response.status_code in [403, 404, 410]:
cache.delete(cache_key) cache.delete(cache_key)
raise HTTPException(status_code=500, detail=error_details) raise HTTPException(status_code=500, detail=f"Upstream stream error: {http_err.response.status_code}")
except Exception as e: except Exception as e:
print(f"Stream Connection Error: {e}") print(f"DEBUG: Stream Connection Error: {e}")
raise HTTPException(status_code=500, detail=f"Stream connection failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Stream connection failed: {str(e)}")
# Forward Content-Length if available # Forward Content-Length if available
@ -730,6 +716,7 @@ async def stream_audio(id: str):
yield chunk yield chunk
external_req.close() external_req.close()
except Exception as e: except Exception as e:
print(f"DEBUG: Stream Iterator Error: {e}")
pass pass
return StreamingResponse(iterfile(), media_type=mime_type, headers=headers) return StreamingResponse(iterfile(), media_type=mime_type, headers=headers)

View file

@ -1,20 +1,9 @@
```python
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager from backend.api.routes import router as api_router
from backend.api import routes
from backend.scheduler import start_scheduler
import os import os
@asynccontextmanager app = FastAPI(title="Spotify Clone Backend")
async def lifespan(app: FastAPI):
# Startup: Start scheduler
scheduler = start_scheduler()
yield
# Shutdown: Scheduler shuts down automatically with process, or we can explicit shutdown if needed
scheduler.shutdown()
app = FastAPI(title="Spotify Clone Backend", lifespan=lifespan)
# CORS setup # CORS setup
origins = [ origins = [

View file

@ -3,7 +3,6 @@ uvicorn==0.34.0
spotdl spotdl
pydantic==2.10.4 pydantic==2.10.4
python-multipart==0.0.20 python-multipart==0.0.20
APScheduler==0.0.20
requests==2.32.3 requests==2.32.3
yt-dlp==2024.12.23 yt-dlp==2024.12.23
ytmusicapi==1.9.1 ytmusicapi==1.9.1

View file

@ -1,51 +0,0 @@
import subprocess
import logging
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def update_ytdlp():
"""
Check for and install the latest version of yt-dlp.
"""
logger.info("Scheduler: Checking for yt-dlp updates...")
try:
# Run pip install --upgrade yt-dlp
result = subprocess.run(
["pip", "install", "--upgrade", "yt-dlp"],
capture_output=True,
text=True,
check=True
)
logger.info(f"Scheduler: yt-dlp update completed.\n{result.stdout}")
except subprocess.CalledProcessError as e:
logger.error(f"Scheduler: Failed to update yt-dlp.\nError: {e.stderr}")
except Exception as e:
logger.error(f"Scheduler: Unexpected error during update: {str(e)}")
def start_scheduler():
"""
Initialize and start the background scheduler.
"""
scheduler = BackgroundScheduler()
# Schedule yt-dlp update every 24 hours
trigger = IntervalTrigger(days=1)
scheduler.add_job(
update_ytdlp,
trigger=trigger,
id="update_ytdlp_job",
name="Update yt-dlp daily",
replace_existing=True
)
scheduler.start()
logger.info("Scheduler: Started background scheduler. yt-dlp will update every 24 hours.")
# Run once on startup to ensure we are up to date immediately
# update_ytdlp() # Optional: Uncomment if we want strict enforcement on boot
return scheduler

View file

@ -16,8 +16,7 @@
"next": "^14.2.0", "next": "^14.2.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1"
"sharp": "^0.33.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",