File size: 6,126 Bytes
2a09f27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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}")