import os from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel from qbittorrent import Client from typing import List, Optional # --- Configuration --- # The qBittorrent-nox daemon is started on port 8080 inside the container. # The default credentials for qbittorrent-nox are admin/adminadmin. # In a real-world scenario, these should be securely managed. QB_HOST = os.getenv("QB_HOST", "localhost") QB_PORT = os.getenv("QB_PORT", "8080") QB_USER = os.getenv("QB_USER", "admin") QB_PASS = os.getenv("QB_PASS", "adminadmin") # --- FastAPI App Initialization --- app = FastAPI( title="qBittorrent Magnet Link API", description="A simple FastAPI service to add magnet links to a running qBittorrent instance with download tracking.", version="2.0.0" ) # Mount static files (frontend) app.mount("/static", StaticFiles(directory="app/static"), name="static") # --- Pydantic Models --- class MagnetLink(BaseModel): magnet_link: str class StatusResponse(BaseModel): status: str message: str class TorrentInfo(BaseModel): name: str hash: str state: str progress: float downloaded: int total_size: int upload_speed: int download_speed: int eta: int num_seeds: int num_leechs: int class TorrentListResponse(BaseModel): torrents: List[TorrentInfo] total_count: int # --- Utility Function for qBittorrent Client --- def get_qb_client(): """Initializes and logs into the qBittorrent client.""" try: qb = Client(f'http://{QB_HOST}:{QB_PORT}') qb.login(QB_USER, QB_PASS) return qb except Exception as e: raise HTTPException(status_code=503, detail=f"Could not connect or log in to qBittorrent: {e}") # --- Endpoints --- @app.get("/", response_class=FileResponse) async def serve_frontend(): """Serves the main HTML frontend.""" return FileResponse("app/static/index.html") @app.get("/health", response_model=StatusResponse) async def health_check(): """Checks the health of the FastAPI service and qBittorrent connection.""" try: qb = get_qb_client() # A simple call to verify connection and authentication version = qb.app.version return StatusResponse(status="ok", message=f"FastAPI is running and connected to qBittorrent v{version}") except HTTPException as e: raise e except Exception as e: raise HTTPException(status_code=500, detail=f"Internal server error during health check: {e}") @app.post("/api/add_torrent", response_model=StatusResponse) async def add_torrent(link: MagnetLink): """Adds a magnet link to the qBittorrent download queue.""" qb = get_qb_client() try: # The add_torrent method handles both magnet links and .torrent files qb.torrents_add(urls=link.magnet_link) return StatusResponse( status="success", message=f"Successfully added magnet link to qBittorrent" ) except Exception as e: # Log the error and return a user-friendly message print(f"Error adding torrent: {e}") raise HTTPException(status_code=500, detail=f"Failed to add torrent: {e}") @app.get("/api/torrents", response_model=TorrentListResponse) async def get_torrents(): """Fetches the list of all torrents with their current status.""" qb = get_qb_client() try: torrents = qb.torrents() torrent_list = [] for torrent in torrents: torrent_info = TorrentInfo( name=torrent.get('name', 'Unknown'), hash=torrent.get('hash', ''), state=torrent.get('state', 'unknown'), progress=torrent.get('progress', 0) * 100, # Convert to percentage downloaded=torrent.get('downloaded', 0), total_size=torrent.get('total_size', 0), upload_speed=torrent.get('upspeed', 0), download_speed=torrent.get('dlspeed', 0), eta=torrent.get('eta', 0), num_seeds=torrent.get('num_seeds', 0), num_leechs=torrent.get('num_leechs', 0), ) torrent_list.append(torrent_info) return TorrentListResponse(torrents=torrent_list, total_count=len(torrent_list)) except Exception as e: print(f"Error fetching torrents: {e}") raise HTTPException(status_code=500, detail=f"Failed to fetch torrents: {e}") @app.delete("/api/torrents/{torrent_hash}") async def delete_torrent(torrent_hash: str, delete_files: bool = False): """Deletes a torrent from the qBittorrent instance.""" qb = get_qb_client() try: qb.torrents_delete(torrent_hashes=torrent_hash, delete_files=delete_files) return StatusResponse( status="success", message=f"Successfully deleted torrent" ) except Exception as e: print(f"Error deleting torrent: {e}") raise HTTPException(status_code=500, detail=f"Failed to delete torrent: {e}") @app.post("/api/torrents/{torrent_hash}/pause") async def pause_torrent(torrent_hash: str): """Pauses a torrent.""" qb = get_qb_client() try: qb.torrents_pause(torrent_hashes=torrent_hash) return StatusResponse( status="success", message=f"Successfully paused torrent" ) except Exception as e: print(f"Error pausing torrent: {e}") raise HTTPException(status_code=500, detail=f"Failed to pause torrent: {e}") @app.post("/api/torrents/{torrent_hash}/resume") async def resume_torrent(torrent_hash: str): """Resumes a torrent.""" qb = get_qb_client() try: qb.torrents_resume(torrent_hashes=torrent_hash) return StatusResponse( status="success", message=f"Successfully resumed torrent" ) except Exception as e: print(f"Error resuming torrent: {e}") raise HTTPException(status_code=500, detail=f"Failed to resume torrent: {e}")