84 lines
2.8 KiB
TypeScript
84 lines
2.8 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 loginUrl = `${host}/api/v2/auth/login`
|
|
console.log(`[qbt] POST ${loginUrl} user=${user}`)
|
|
|
|
const res = await axios.post(
|
|
loginUrl,
|
|
new URLSearchParams({ username: user, password: pass }),
|
|
{
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': host, 'Origin': host },
|
|
maxRedirects: 0,
|
|
validateStatus: s => s < 400,
|
|
}
|
|
)
|
|
|
|
console.log(`[qbt] login status=${res.status} body=${JSON.stringify(res.data)} cookies=${JSON.stringify(res.headers['set-cookie'])}`)
|
|
|
|
const cookie = (res.headers['set-cookie'] ?? []).find((c: string) => c.startsWith('SID='))
|
|
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
|