|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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" |
|
|
) |
|
|
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory="app/static"), name="static") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
|
|
|
@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() |
|
|
|
|
|
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: |
|
|
|
|
|
qb.torrents_add(urls=link.magnet_link) |
|
|
return StatusResponse( |
|
|
status="success", |
|
|
message=f"Successfully added magnet link to qBittorrent" |
|
|
) |
|
|
except Exception as e: |
|
|
|
|
|
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, |
|
|
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}") |
|
|
|