alphanet06/processor
0
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")