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)