Team Ai
Apppublic

alphanet06/processor

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
app.py895 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3N_m3u8DL-RE DRM Processor with API-triggered Processing4Allows triggering file processing via HTTP requests5"""6 7from fastapi import FastAPI, HTTPException, BackgroundTasks8from fastapi.responses import FileResponse, JSONResponse9from fastapi.staticfiles import StaticFiles10from fastapi.middleware.cors import CORSMiddleware11from pydantic import BaseModel, Field12import os13from pathlib import Path14import subprocess15import asyncio16from datetime import datetime17from typing import Optional, Dict, List18import uuid19import uvicorn20import json21from pydrive2.auth import GoogleAuth22from pydrive2.drive import GoogleDrive23 24app = FastAPI(title="N_m3u8DL-RE DRM Processor")25 26# Enable CORS for all origins27app.add_middleware(28    CORSMiddleware,29    allow_origins=["*"],30    allow_credentials=True,31    allow_methods=["*"],32    allow_headers=["*"],33)34 35# Paths36BASE_DIR = Path("/app")37STREAM_DIR = BASE_DIR / "stream"38STREAM_DIR.mkdir(parents=True, exist_ok=True)39 40# Job tracking41active_jobs: Dict[str, dict] = {}42completed_jobs: Dict[str, dict] = {}43 44# Mount static files directory45app.mount("/stream", StaticFiles(directory=str(STREAM_DIR)), name="stream")46 47# Request models48class ProcessRequest(BaseModel):49    url: str = Field(..., description="MPD/M3U8 stream URL")50    save_name: str = Field(..., description="Output filename (without extension)")51    key: Optional[str] = Field(default=None, description="DRM decryption key (single key, deprecated - use keys instead)")52    keys: Optional[List[str]] = Field(default=None, description="DRM decryption keys (list of keys for multi-key content)")53    select_video: str = Field(default="best", description="Video track selection")54    select_audio: str = Field(default="all", description="Audio track selection")55    select_subtitle: str = Field(default="all", description="Subtitle track selection")56    format: str = Field(default="mkv", description="Output format")57    log_level: str = Field(default="Debug", description="Log level")58    binary_merge: bool = Field(default=False, description="Enable binary merge mode")59    additional_args: Optional[List[str]] = Field(default=None, description="Additional N_m3u8DL-RE arguments")60 61class JobStatus(BaseModel):62    job_id: str63    status: str64    filename: Optional[str]65    url: Optional[str]66    started_at: str67    completed_at: Optional[str]68    error: Optional[str]69 70@app.get("/")71async def root():72    """Service information."""73    return {74        "service": "N_m3u8DL-RE DRM Processor",75        "version": "2.0",76        "status": "running",77        "endpoints": {78            "process": "POST /process - Trigger file processing",79            "jobs": "GET /jobs - List all jobs",80            "job_status": "GET /jobs/{job_id} - Get job status",81            "files": "GET /files - List processed files",82            "stream": "GET /stream/{filename} - Stream file (playback)",83            "download": "GET /download/{filename} - Download file",84            "health": "GET /health - Health check",85            "check_gdrive": "GET /check_gdrive - Verify Google Drive credentials",86            "debug": "GET /debug - Show detailed system info"87        }88    }89 90@app.get("/health")91async def health_check():92    """Health check endpoint."""93    tools_status = {}94 95    try:96        result = subprocess.run(["/usr/local/bin/N_m3u8DL-RE", "--version"], 97                              capture_output=True, text=True, timeout=5)98        tools_status["N_m3u8DL-RE"] = "available" if result.returncode == 0 else "error"99    except:100        tools_status["N_m3u8DL-RE"] = "unavailable"101 102    try:103        result = subprocess.run(["/usr/bin/ffmpeg", "-version"], 104                              capture_output=True, text=True, timeout=5)105        tools_status["ffmpeg"] = "available" if result.returncode == 0 else "error"106    except:107        tools_status["ffmpeg"] = "unavailable"108 109    try:110        subprocess.run(["/usr/local/bin/mp4decrypt"], 111                      capture_output=True, text=True, timeout=5)112        tools_status["mp4decrypt"] = "available"113    except:114        tools_status["mp4decrypt"] = "unavailable"115 116    return {117        "status": "healthy",118        "timestamp": datetime.now().isoformat(),119        "tools": tools_status,120        "active_jobs": len(active_jobs),121        "completed_jobs": len(completed_jobs),122        "files_available": len(list(STREAM_DIR.glob("*.mkv"))) + len(list(STREAM_DIR.glob("*.mp4")))123    }124 125def upload_to_google_drive(file_path: Path) -> Optional[str]:126    """127    Upload file to Google Drive and return shareable link.128    Uses settings.yaml for authentication configuration.129    Requires credentials.json to be pre-generated locally.130    """131    try:132        # Debug: Print current working directory and check files133        print(f"๐Ÿ” Current working directory: {os.getcwd()}")134        print(f"๐Ÿ” Environment: HuggingFace Space")135        print(f"๐Ÿ” Checking for credentials.json...")136 137        # List all files in current directory first138        try:139            all_files = os.listdir('.')140            print(f"๐Ÿ” ALL files in directory: {sorted(all_files)}")141            print(f"   Total files: {len(all_files)}")142 143            # Check for our specific files144            json_files = [f for f in all_files if f.endswith('.json')]145            yaml_files = [f for f in all_files if f.endswith('.yaml')]146            py_files = [f for f in all_files if f.endswith('.py')]147            print(f"   JSON files found: {json_files}")148            print(f"   YAML files found: {yaml_files}")149            print(f"   Python files found: {py_files}")150 151            # Check file permissions and details152            for filename in all_files:153                try:154                    file_path = Path(filename)155                    if file_path.exists():156                        size = file_path.stat().st_size157                        print(f"   ๐Ÿ“„ {filename}: {size} bytes")158                    else:159                        print(f"   โŒ {filename}: not accessible")160                except Exception as e:161                    print(f"   โš ๏ธ  {filename}: error checking - {e}")162 163        except Exception as e:164            print(f"   โŒ Error listing directory: {e}")165            import traceback166            traceback.print_exc()167 168        # Check multiple possible locations for credentials file169        possible_paths = [170            Path("credentials.json"),171            Path("/app/credentials.json"),172            Path(os.getcwd()) / "credentials.json"173        ]174 175        credentials_file = None176        for path in possible_paths:177            exists = path.exists()178            print(f"   Checking: {path} -> exists: {exists}")179            if exists:180                try:181                    size = path.stat().st_size182                    print(f"     โœ… File exists! Size: {size} bytes")183                except Exception as e:184                    print(f"     โš ๏ธ  File exists but error getting size: {e}")185                credentials_file = path186                break187            else:188                print(f"     โŒ File not found")189 190        if not credentials_file:191            print("โš ๏ธ  credentials.json not found in any expected location.")192            print("   Please ensure credentials.json is uploaded to the same directory as app.py")193            print("   Expected locations:")194            for path in possible_paths:195                print(f"     - {path}")196            return None197 198        print(f"โœ… Found credentials.json at: {credentials_file}")199 200        # Check file size to ensure it's not empty201        file_size = credentials_file.stat().st_size202        print(f"๐Ÿ“„ credentials.json size: {file_size} bytes")203 204        if file_size == 0:205            print("โŒ credentials.json is empty!")206            return None207 208        # Initialize GoogleAuth with settings - try multiple paths209        settings_paths = ['settings.yaml', '/app/settings.yaml', Path(os.getcwd()) / 'settings.yaml']210        settings_file = None211 212        for settings_path in settings_paths:213            if Path(settings_path).exists():214                settings_file = settings_path215                break216 217        if not settings_file:218            print("โš ๏ธ  settings.yaml not found!")219            return None220 221        print(f"โœ… Found settings.yaml at: {settings_file}")222        gauth = GoogleAuth(settings_file=str(settings_file))223 224        # Load saved credentials with absolute path225        print(f"๐Ÿ”„ Loading credentials from: {credentials_file}")226        try:227            gauth.LoadCredentialsFile(str(credentials_file))228 229            if gauth.credentials is None:230                print("โŒ No valid credentials found in credentials.json")231                print("   This usually means the credentials file is corrupted or expired")232                print("   Please regenerate credentials.json locally and upload again")233                print()234                print("๐Ÿ”ง TROUBLESHOOTING:")235                print("   1. Run 'python authenticate_gdrive.py' locally")236                print("   2. Upload the generated credentials.json to HuggingFace")237                print("   3. Make sure credentials.json is in the same directory as app.py")238                print(f"   4. Current directory: {os.getcwd()}")239                print(f"   5. Expected location: {credentials_file.absolute()}")240                return None241 242            print("โœ… Credentials loaded successfully!")243 244        except Exception as e:245            print(f"โŒ Error loading credentials: {e}")246            print("   This suggests the credentials.json file is corrupted")247            print("   Please regenerate it locally using: python authenticate_gdrive.py")248            return None249 250        if gauth.access_token_expired:251            # Refresh them if expired252            print("๐Ÿ”„ Refreshing expired access token...")253            gauth.Refresh()254            # Save refreshed credentials255            gauth.SaveCredentialsFile(str(credentials_file))256        else:257            # Initialize the saved creds258            gauth.Authorize()259        260        # Create GoogleDrive instance261        drive = GoogleDrive(gauth)262        263        # Upload file264        file_drive = drive.CreateFile({'title': file_path.name})265        file_drive.SetContentFile(str(file_path))266        file_drive.Upload()267        268        # Make file shareable (anyone with link can view)269        file_drive.InsertPermission({270            'type': 'anyone',271            'value': 'anyone',272            'role': 'reader'273        })274        275        # Get shareable link276        shareable_link = file_drive['alternateLink']277        return shareable_link278        279    except Exception as e:280        print(f"Error uploading to Google Drive: {e}")281        return None282 283async def run_n_m3u8dl_process(job_id: str, request: ProcessRequest):284    """Run N_m3u8DL-RE process in background."""285 286    # Validate that at least one key is provided287    if not request.keys and not request.key:288        active_jobs[job_id]["status"] = "error"289        active_jobs[job_id]["error"] = "No decryption key(s) provided"290        return291 292    # Build command293    cmd = [294        "/usr/local/bin/N_m3u8DL-RE",295        request.url,296        "--save-name", request.save_name,297        "--select-video", request.select_video,298        "--select-audio", request.select_audio,299        "--select-subtitle", request.select_subtitle,300        "-mt",301        "-M", f"format={request.format}",302        "--log-level", request.log_level303    ]304 305    # Add key(s) - handle both single key and multiple keys306    if request.keys:307        # Multiple keys provided - add each with its own --key flag308        for key in request.keys:309            cmd.extend(["--key", key])310    elif request.key:311        # Single key provided (backward compatibility)312        cmd.extend(["--key", request.key])313 314    # Add binary merge argument if enabled315    if request.binary_merge:316        cmd.append("--binary-merge")317 318    # Add additional arguments if provided319    if request.additional_args:320        cmd.extend(request.additional_args)321 322    # Update job status323    active_jobs[job_id]["status"] = "processing"324    active_jobs[job_id]["command"] = " ".join(cmd)325 326    try:327        # Change to output directory328        os.chdir(STREAM_DIR)329 330        # Run the process331        process = await asyncio.create_subprocess_exec(332            *cmd,333            stdout=asyncio.subprocess.PIPE,334            stderr=asyncio.subprocess.PIPE335        )336 337        stdout, stderr = await process.communicate()338 339        # Check if successful340        if process.returncode == 0:341            output_file = STREAM_DIR / f"{request.save_name}.{request.format}"342 343            if output_file.exists():344                # Convert MKV to MP4 using ffmpeg345                try:346                    mkv_file = output_file347                    mp4_file = STREAM_DIR / f"{request.save_name}.mp4"348 349                    ffmpeg_cmd = [350                        "/usr/bin/ffmpeg",351                        "-fflags", "+genpts",352                        "-i", str(mkv_file),353                        "-map", "0:v",  # Map all video streams354                        "-map", "0:a",  # Map all audio streams355                        "-c", "copy",   # Copy streams (no re-encoding)356                        "-movflags", "+faststart",357                        "-max_interleave_delta", "0",358                        "-avoid_negative_ts", "make_zero",359                        "-y",  # Overwrite output file without asking360                        str(mp4_file)361                    ]362 363                    active_jobs[job_id]["status"] = "converting"364 365                    # Run ffmpeg conversion366                    ffmpeg_process = await asyncio.create_subprocess_exec(367                        *ffmpeg_cmd,368                        stdout=asyncio.subprocess.PIPE,369                        stderr=asyncio.subprocess.PIPE370                    )371 372                    ffmpeg_stdout, ffmpeg_stderr = await ffmpeg_process.communicate()373 374                    if ffmpeg_process.returncode == 0 and mp4_file.exists():375                        # Conversion successful, delete original MKV376                        try:377                            mkv_file.unlink()378                            final_file = mp4_file379                            final_filename = mp4_file.name380                        except Exception as e:381                            # If deletion fails, keep MKV and use it as final file382                            print(f"Warning: Failed to delete MKV file: {e}")383                            final_file = mkv_file384                            final_filename = mkv_file.name385                    else:386                        # Conversion failed, keep original MKV387                        final_file = mkv_file388                        final_filename = mkv_file.name389                        active_jobs[job_id]["conversion_error"] = ffmpeg_stderr.decode() if ffmpeg_stderr else "FFmpeg conversion failed"390 391                    # Update job with final file info392                    active_jobs[job_id]["status"] = "completed"393                    active_jobs[job_id]["filename"] = final_filename394                    active_jobs[job_id]["url"] = f"/stream/{final_filename}"395                    active_jobs[job_id]["completed_at"] = datetime.now().isoformat()396                    active_jobs[job_id]["file_size_mb"] = round(final_file.stat().st_size / (1024 * 1024), 2)397                    active_jobs[job_id]["converted_to_mp4"] = final_filename.endswith('.mp4')398 399                    # Upload to Google Drive if MP4400                    if final_filename.endswith('.mp4'):401                        active_jobs[job_id]["status"] = "uploading_to_gdrive"402                        403                        gdrive_link = upload_to_google_drive(final_file)404                        if gdrive_link:405                            active_jobs[job_id]["gdrive_link"] = gdrive_link406                            active_jobs[job_id]["status"] = "completed"407                            print(f"โœ… Job {job_id}: Google Drive upload completed!")408                        else:409                            active_jobs[job_id]["gdrive_error"] = "Failed to upload to Google Drive"410                            active_jobs[job_id]["status"] = "completed"411                            print(f"โŒ Job {job_id}: Google Drive upload failed")412 413                    # Move to completed jobs414                    completed_jobs[job_id] = active_jobs[job_id]415                    del active_jobs[job_id]416 417                except Exception as e:418                    # If conversion fails, fall back to original MKV419                    active_jobs[job_id]["status"] = "completed"420                    active_jobs[job_id]["filename"] = output_file.name421                    active_jobs[job_id]["url"] = f"/stream/{output_file.name}"422                    active_jobs[job_id]["completed_at"] = datetime.now().isoformat()423                    active_jobs[job_id]["file_size_mb"] = round(output_file.stat().st_size / (1024 * 1024), 2)424                    active_jobs[job_id]["conversion_error"] = str(e)425 426                    # Move to completed jobs427                    completed_jobs[job_id] = active_jobs[job_id]428                    del active_jobs[job_id]429            else:430                active_jobs[job_id]["status"] = "error"431                active_jobs[job_id]["error"] = "Output file not found"432                active_jobs[job_id]["stderr"] = stderr.decode() if stderr else ""433        else:434            active_jobs[job_id]["status"] = "error"435            active_jobs[job_id]["error"] = f"Process exited with code {process.returncode}"436            active_jobs[job_id]["stderr"] = stderr.decode() if stderr else ""437            active_jobs[job_id]["stdout"] = stdout.decode() if stdout else ""438 439    except Exception as e:440        active_jobs[job_id]["status"] = "error"441        active_jobs[job_id]["error"] = str(e)442        active_jobs[job_id]["completed_at"] = datetime.now().isoformat()443 444@app.post("/process")445async def process_file(request: ProcessRequest, background_tasks: BackgroundTasks):446    """447    Trigger N_m3u8DL-RE processing via API.448 449    Example curl commands:450    451    Single key (backward compatible):452    ```453    curl -X POST "https://your-space.hf.space/process" \454      -H "Content-Type: application/json" \455      -d '{456        "url": "https://example.com/stream.mpd",457        "save_name": "clip_13141002",458        "key": "f01b16c0f7ca5aa0b843645a44b4210a",459        "binary_merge": true460      }'461    ```462    463    Multiple keys (for multi-key DRM content):464    ```465    curl -X POST "https://your-space.hf.space/process" \466      -H "Content-Type: application/json" \467      -d '{468        "url": "https://example.com/stream.mpd",469        "save_name": "clip_13141002",470        "keys": [471          "f01b16c0f7ca5aa0b843645a44b4210a",472          "a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7",473          "b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8"474        ],475        "binary_merge": true476      }'477    ```478    """479    # Validate that at least one key is provided480    if not request.keys and not request.key:481        raise HTTPException(482            status_code=400,483            detail="At least one decryption key must be provided. Use 'key' for single key or 'keys' for multiple keys."484        )485    486    # Generate job ID487    job_id = str(uuid.uuid4())488 489    # Create job entry490    active_jobs[job_id] = {491        "job_id": job_id,492        "status": "queued",493        "request": request.model_dump(),494        "started_at": datetime.now().isoformat(),495        "filename": None,496        "url": None,497        "completed_at": None,498        "error": None499    }500 501    # Start processing in background502    background_tasks.add_task(run_n_m3u8dl_process, job_id, request)503 504    return {505        "job_id": job_id,506        "status": "queued",507        "message": "Processing started",508        "check_status": f"/jobs/{job_id}",509        "estimated_filename": f"{request.save_name}.{request.format}"510    }511 512@app.get("/jobs")513async def list_jobs():514    """List all jobs (active and completed)."""515    return {516        "active": list(active_jobs.values()),517        "completed": list(completed_jobs.values())[-20:]  # Last 20 completed jobs518    }519 520@app.get("/jobs/{job_id}")521async def get_job_status(job_id: str):522    """Get status of a specific job."""523 524    # Check active jobs525    if job_id in active_jobs:526        return active_jobs[job_id]527 528    # Check completed jobs529    if job_id in completed_jobs:530        return completed_jobs[job_id]531 532    raise HTTPException(status_code=404, detail=f"Job {job_id} not found")533 534@app.get("/files")535async def list_files():536    """List all available processed files (MKV and MP4)."""537    try:538        files = []539        # Look for both MKV and MP4 files540        for file_path in STREAM_DIR.glob("*.mkv"):541            stat = file_path.stat()542            files.append({543                "filename": file_path.name,544                "format": "mkv",545                "size_mb": round(stat.st_size / (1024 * 1024), 2),546                "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),547                "stream_url": f"/stream/{file_path.name}",548                "download_url": f"/download/{file_path.name}"549            })550 551        for file_path in STREAM_DIR.glob("*.mp4"):552            stat = file_path.stat()553            files.append({554                "filename": file_path.name,555                "format": "mp4",556                "size_mb": round(stat.st_size / (1024 * 1024), 2),557                "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),558                "stream_url": f"/stream/{file_path.name}",559                "download_url": f"/download/{file_path.name}"560            })561 562        return {563            "count": len(files),564            "files": sorted(files, key=lambda x: x["modified"], reverse=True)565        }566    except Exception as e:567        raise HTTPException(status_code=500, detail=f"Error listing files: {str(e)}")568 569 570@app.get("/download/{filename}")571async def download_file(filename: str):572    """573    Download a processed file with proper download headers.574    575    Example:576    ```577    curl -O "https://your-space.hf.space/download/clip_13141002.mp4"578    wget "https://your-space.hf.space/download/clip_13141002.mkv"579    ```580    """581    # Security: prevent directory traversal582    if ".." in filename or "/" in filename or "\\" in filename:583        raise HTTPException(status_code=400, detail="Invalid filename")584    585    file_path = STREAM_DIR / filename586    587    # Check if file exists588    if not file_path.exists() or not file_path.is_file():589        raise HTTPException(status_code=404, detail=f"File '{filename}' not found")590    591    # Return file with download headers592    return FileResponse(593        path=str(file_path),594        filename=filename,595        media_type="application/octet-stream",596        headers={597            "Content-Disposition": f'attachment; filename="{filename}"',598            "Content-Length": str(file_path.stat().st_size)599        }600    )601 602 603@app.get("/debug")604async def debug_info():605    """Show detailed system and file information for debugging."""606    import platform607    import sys608 609    debug_data = {610        "system": {611            "platform": platform.platform(),612            "python_version": sys.version,613            "working_directory": os.getcwd(),614            "user": os.environ.get('USER', 'unknown'),615            "home": os.environ.get('HOME', 'unknown')616        },617        "files": {},618        "environment": {}619    }620 621    # Check all files in current directory622    try:623        files = os.listdir('.')624        debug_data["files"]["directory_listing"] = sorted(files)625        debug_data["files"]["total_count"] = len(files)626 627        # Check specific file types628        debug_data["files"]["json_files"] = [f for f in files if f.endswith('.json')]629        debug_data["files"]["yaml_files"] = [f for f in files if f.endswith('.yaml')]630        debug_data["files"]["py_files"] = [f for f in files if f.endswith('.py')]631 632        # Get details for specific files we're looking for633        important_files = ['credentials.json', 'settings.yaml', 'client_secrets.json', 'app.py', 'requirements.txt']634        debug_data["files"]["important_files"] = {}635 636        for filename in important_files:637            if filename in files:638                try:639                    file_path = Path(filename)640                    stat = file_path.stat()641                    debug_data["files"]["important_files"][filename] = {642                        "exists": True,643                        "size": stat.st_size,644                        "modified": stat.st_mtime,645                        "permissions": oct(stat.st_mode)[-3:]646                    }647                except Exception as e:648                    debug_data["files"]["important_files"][filename] = {649                        "exists": True,650                        "error": str(e)651                    }652            else:653                debug_data["files"]["important_files"][filename] = {654                    "exists": False655                }656 657    except Exception as e:658        debug_data["files"]["error"] = str(e)659 660    # Check environment variables661    env_vars = ['SPACE_ID', 'SPACE_NAME', 'SPACE_HOST', 'HF_SPACE', 'HF_TOKEN']662    for var in env_vars:663        debug_data["environment"][var] = os.environ.get(var, 'not_set')664 665    # Try to check git status if git is available666    try:667        import subprocess668        result = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True, timeout=5)669        if result.returncode == 0:670            debug_data["git"] = {671                "status": "available",672                "changes": result.stdout.strip(),673                "has_changes": len(result.stdout.strip()) > 0674            }675        else:676            debug_data["git"] = {677                "status": "error",678                "error": result.stderr679            }680    except Exception as e:681        debug_data["git"] = {682            "status": "not_available",683            "error": str(e)684        }685 686    return debug_data687 688@app.get("/check_gdrive")689async def check_google_drive_credentials():690    """Check if Google Drive credentials are working properly."""691    try:692        # Check if credentials file exists693        print(f"๐Ÿ” Current working directory: {os.getcwd()}")694 695        # List all files in current directory696        try:697            files = os.listdir('.')698            json_files = [f for f in files if f.endswith('.json')]699            yaml_files = [f for f in files if f.endswith('.yaml')]700            py_files = [f for f in files if f.endswith('.py')]701 702            # Detailed file info for debugging703            file_details = {}704            for filename in files:705                try:706                    file_path = Path(filename)707                    if file_path.exists():708                        stat = file_path.stat()709                        file_details[filename] = {710                            "size": stat.st_size,711                            "modified": stat.st_mtime,712                            "permissions": oct(stat.st_mode)[-3:]713                        }714                except Exception as e:715                    file_details[filename] = {"error": str(e)}716 717        except Exception as e:718            return {719                "error": f"Cannot list directory: {e}",720                "current_directory": os.getcwd(),721                "expected_locations": [str(p) for p in possible_paths]722            }723 724        credentials_file = None725        possible_paths = [726            Path("credentials.json"),727            Path("/app/credentials.json"),728            Path(os.getcwd()) / "credentials.json"729        ]730 731        for path in possible_paths:732            if path.exists():733                credentials_file = path734                break735 736        if not credentials_file:737            # Print debug info to server logs738            print("โš ๏ธ  credentials.json not found in any expected location.")739            print("   Please ensure credentials.json is uploaded to the same directory as app.py")740            print("   Expected locations:")741            for path in possible_paths:742                print(f"     - {path}")743 744            return {745                "status": "error",746                "message": "credentials.json not found",747                "current_directory": os.getcwd(),748                "files_in_directory": files,749                "total_files": len(files),750                "json_files": json_files,751                "yaml_files": yaml_files,752                "py_files": py_files,753                "file_details": file_details,754                "expected_locations": [str(p) for p in possible_paths],755                "debug_info": "Check server logs for detailed file listing",756                "troubleshooting": {757                    "check_git_status": "Run 'git status' to see if files are committed",758                    "check_gitignore": "Ensure credentials.json is not in .gitignore",759                    "force_rebuild": "Try rebuilding the HuggingFace space",760                    "verify_upload": "Go to Files tab and confirm all files are uploaded"761                }762            }763 764        # Check file size765        file_size = credentials_file.stat().st_size766        if file_size == 0:767            return {768                "status": "error",769                "message": "credentials.json is empty",770                "file_size": file_size771            }772 773        # Find settings file774        settings_file = None775        settings_paths = ['settings.yaml', '/app/settings.yaml', Path(os.getcwd()) / 'settings.yaml']776 777        for settings_path in settings_paths:778            if Path(settings_path).exists():779                settings_file = settings_path780                break781 782        if not settings_file:783            return {784                "status": "error",785                "message": "settings.yaml not found",786                "credentials_found": True,787                "credentials_size": file_size,788                "settings_paths_checked": settings_paths789            }790 791        # Try to load credentials792        gauth = GoogleAuth(settings_file=str(settings_file))793        gauth.LoadCredentialsFile(str(credentials_file))794 795        if gauth.credentials is None:796            return {797                "status": "error",798                "message": "No valid credentials loaded",799                "credentials_file": str(credentials_file),800                "settings_file": str(settings_file),801                "file_size": file_size802            }803 804        # Try to authorize805        try:806            if gauth.access_token_expired:807                gauth.Refresh()808                gauth.SaveCredentialsFile(str(credentials_file))809 810            gauth.Authorize()811 812            # Try to create a drive instance (this will test if auth actually works)813            drive = GoogleDrive(gauth)814 815            # Get user info as a test816            about = drive.GetAbout()817            user_email = about['user']['emailAddress']818 819            return {820                "status": "success",821                "message": "Google Drive credentials are working!",822                "user_email": user_email,823                "credentials_file": str(credentials_file),824                "settings_file": str(settings_file),825                "file_size": file_size,826                "token_expired": gauth.access_token_expired,827                "current_directory": os.getcwd(),828                "files_in_directory": files829            }830 831        except Exception as e:832            return {833                "status": "error",834                "message": f"Authentication failed: {str(e)}",835                "credentials_file": str(credentials_file),836                "settings_file": str(settings_file),837                "file_size": file_size,838                "current_directory": os.getcwd()839            }840 841    except Exception as e:842        return {843            "status": "error",844            "message": f"Unexpected error: {str(e)}",845            "current_directory": os.getcwd()846        }847 848@app.delete("/jobs/{job_id}")849async def cancel_job(job_id: str):850    """Cancel an active job (if possible)."""851    if job_id in active_jobs:852        # Note: Actual process cancellation would require tracking the subprocess PID853        active_jobs[job_id]["status"] = "cancelled"854        completed_jobs[job_id] = active_jobs[job_id]855        del active_jobs[job_id]856        return {"message": f"Job {job_id} cancelled"}857 858    raise HTTPException(status_code=404, detail=f"Job {job_id} not found or already completed")859 860if __name__ == "__main__":861    print("๐ŸŽฅ N_m3u8DL-RE DRM Processor - Enhanced FastAPI Server")862    print("=" * 60)863    print(f"Stream directory: {STREAM_DIR}")864    print("Server starting on port 7860...")865    print()866    print("๐Ÿ“ก API Endpoints:")867    print("  โ€ข POST /process           - Trigger file processing via API")868    print("  โ€ข GET  /jobs              - List all jobs")869    print("  โ€ข GET  /jobs/{job_id}     - Check job status")870    print("  โ€ข GET  /files             - List all processed files")871    print("  โ€ข GET  /stream/{filename} - Stream/access file (playback)")872    print("  โ€ข GET  /download/{filename} - Download file")873    print("  โ€ข GET  /health            - Health check")874    print()875    print("๐Ÿ”„ Conversion Info:")876    print("  โ€ข MKV files are automatically converted to MP4 after processing")877    print("  โ€ข Video and audio streams are copied (no re-encoding) for speed")878    print("  โ€ข Subtitle streams are excluded (MP4 compatibility)")879    print("  โ€ข Optimized for streaming with faststart flag")880    print("  โš ๏ธ  Note: Subtitles are not included in MP4 output files")881    print()882    print("๐Ÿ’ก Example API calls:")883    print()884    print("  Single key:")885    print('    curl -X POST "http://localhost:7860/process" \\')886    print('      -H "Content-Type: application/json" \\')887    print('      -d \'{"url":"https://...", "save_name":"clip", "key":"...", "binary_merge":true}\'')888    print()889    print("  Multiple keys:")890    print('    curl -X POST "http://localhost:7860/process" \\')891    print('      -H "Content-Type: application/json" \\')892    print('      -d \'{"url":"https://...", "save_name":"clip", "keys":["key1","key2","key3"], "binary_merge":true}\'')893    print()894 895    uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")