tal / app.py
tecuts's picture
Update app.py
bd5b698 verified
import os
import logging
import sys
from datetime import datetime
import tidalapi
from tidalapi import Track, Quality
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger("tidalapi_app")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
def load_tokens_from_secret(session):
secret = os.getenv("TIDAL_OAUTH_TOKENS")
if not secret:
logger.error("Environment variable TIDAL_OAUTH_TOKENS not set")
return False
lines = secret.strip().splitlines()
if len(lines) != 4:
logger.error("TIDAL_OAUTH_TOKENS secret malformed: expected 4 lines")
return False
try:
expiry_time = datetime.fromisoformat(lines[3])
except Exception as e:
logger.error(f"Failed to parse expiry_time: {e}")
return False
session.load_oauth_session(lines[0], lines[1], lines[2], expiry_time)
logger.info("OAuth tokens loaded successfully from secret")
return True
# Initialize session globally
session = tidalapi.Session()
if not load_tokens_from_secret(session):
raise RuntimeError("Failed to load OAuth tokens from secret")
if not session.check_login():
raise RuntimeError("Failed to login with saved tokens")
# Map client quality strings to tidalapi Quality enums
QUALITY_MAP = {
"LOW": Quality.low_96k,
"HIGH": Quality.low_320k,
"LOSSLESS": Quality.high_lossless,
"HI_RES": Quality.hi_res_lossless,
}
@app.get("/")
async def root():
logger.info("GET /")
return {
"message": "Internal Server Error",
"author": "made by Cody from chrunos.com"
}
@app.get("/track/")
def get_track_download_url(
id: int = Query(..., description="TIDAL Track ID"),
quality: str = Query("HI_RES", description="Audio quality", regex="^(LOW|HIGH|LOSSLESS|HI_RES)$"),
):
logger.info(f"Request received for track_id: {id} with quality: {quality}")
if quality not in QUALITY_MAP:
logger.error(f"Invalid quality requested: {quality}")
raise HTTPException(status_code=400, detail=f"Invalid quality. Available: {list(QUALITY_MAP.keys())}")
session.audio_quality = QUALITY_MAP[quality]
try:
track = Track(session, id)
logger.info(f"Track found: {track.name} by {track.artist.name}")
except Exception as e:
logger.error(f"Track lookup failed: {e}")
raise HTTPException(status_code=404, detail="Track not found")
stream = track.get_stream()
manifest = stream.get_stream_manifest()
result = {
"track_id": id,
"track_name": track.name,
"artist_name": track.artist.name,
"audio_quality": str(stream.audio_quality),
"stream_type": None,
"download_urls": [],
}
if stream.is_bts:
urls = manifest.get_urls()
if not urls:
logger.error("No direct URLs found in manifest")
raise HTTPException(status_code=500, detail="No downloadable URLs found")
result["stream_type"] = "bts"
result["download_urls"] = urls
logger.info(f"Returning {len(urls)} direct download URL(s)")
elif stream.is_mpd:
mpd_manifest = stream.get_manifest_data()
result["stream_type"] = "mpd"
result["mpd_manifest"] = mpd_manifest
logger.info("Returning MPEG-DASH (MPD) manifest data")
else:
logger.error("Unsupported stream type")
raise HTTPException(status_code=500, detail="Unsupported stream type")
return JSONResponse(content=result)