Files
Dashboard/server/routes/qbittorrent.ts
T
Syco 5ac98d966c
ci/woodpecker/push/woodpecker Pipeline was successful
Fix qBittorrent 5.x session cookie name (SID -> QBT_SID_{port})
2026-05-20 19:28:08 +02:00

79 lines
2.6 KiB
TypeScript

import { Router } from 'express'
import axios from 'axios'
const router = Router()
let sid: string | null = null
let sidExpiry = 0
async function getCookie(): Promise<string> {
if (sid && Date.now() < sidExpiry) return sid
const host = process.env.QBT_HOST
const user = process.env.QBT_USER
const pass = process.env.QBT_PASSWORD
if (!host || !user || !pass) throw new Error('QBT_HOST / QBT_USER / QBT_PASSWORD not configured')
const res = await axios.post(
`${host}/api/v2/auth/login`,
new URLSearchParams({ username: user, password: pass }),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': host, 'Origin': host },
validateStatus: s => s < 400,
}
)
// qBittorrent 5.x renamed the cookie from SID to QBT_SID_{port}
const cookie = (res.headers['set-cookie'] ?? []).find((c: string) => /^(QBT_)?SID[_=]/.test(c))
if (!cookie) throw new Error(`qBittorrent login failed (${res.status}): "${String(res.data).trim()}"`)
sid = cookie.split(';')[0]
sidExpiry = Date.now() + 55 * 60 * 1000
return sid
}
router.get('/stats', async (_req, res) => {
try {
const host = process.env.QBT_HOST
if (!host) { res.status(503).json({ error: 'QBT_HOST not configured' }); return }
const cookie = await getCookie()
const headers = { Cookie: cookie, Referer: host, Origin: host }
const [infoRes, torrentsRes] = await Promise.all([
axios.get(`${host}/api/v2/transfer/info`, { headers }),
axios.get(`${host}/api/v2/torrents/info`, { headers }),
])
const info = infoRes.data
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const torrents: any[] = torrentsRes.data ?? []
const downloading = torrents.filter(t => ['downloading', 'stalledDL', 'metaDL', 'forcedDL'].includes(t.state))
const seeding = torrents.filter(t => ['uploading', 'stalledUP', 'forcedUP'].includes(t.state))
const paused = torrents.filter(t => ['pausedDL', 'pausedUP'].includes(t.state))
res.json({
dlSpeed: info.dl_info_speed ?? 0,
ulSpeed: info.ul_info_speed ?? 0,
downloading: downloading.length,
seeding: seeding.length,
paused: paused.length,
total: torrents.length,
active: downloading.slice(0, 5).map(t => ({
name: t.name,
progress: t.progress,
dlSpeed: t.dlspeed,
size: t.size,
state: t.state,
})),
})
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('login failed')) sid = null
const msg = err instanceof Error ? err.message : 'Unknown error'
res.status(500).json({ error: msg })
}
})
export default router