- catalog: badge ORGANISÉ/PARTIEL/EN COURS sur les affiches (download_state) - catalog: filtres Téléchargés et En cours - catalog: icône logs dans le header - catalog: polling notifications organisation (toast auto) - api: /api/series enrichi avec download_state par série - api: /api/logs + /api/logs/clear avec filtre level/source/limit - api: /api/downloads enrichi avec errorFiles depuis recentOrganized - logger: module centralisé logs.jsonl, rotation 2000 lignes, debug masqué en prod - downloads: badge erreurs avec tooltip fichiers en erreur au hover - downloads: badge organisation NON ORGANISÉ/EN COURS/ORGANISÉ avec compteur - logs: page /logs avec table, filtres niveau/source, choix 100/500/tout, taille fichier, clear - organize: callback onResult expose errorFiles - organize-worker: logs candidats absents passés en debug
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
/**
|
|
* organize.ts
|
|
* ===========
|
|
* Lance l'organizer dans un Worker Thread dédié pour ne pas bloquer Express.
|
|
* Le worker gère tout le filesystem (copy, hardlink, move).
|
|
*/
|
|
|
|
import path from 'path'
|
|
import { Worker } from 'worker_threads'
|
|
|
|
// Un seul worker à la fois
|
|
let workerRunning = false
|
|
|
|
export interface OrganizeResult {
|
|
total : number
|
|
skipped : number
|
|
done : number
|
|
errors : { file: string; error: string }[]
|
|
}
|
|
|
|
/**
|
|
* Lance l'auto-organisation dans un worker thread.
|
|
* listFn est appelée dans le thread principal (accès qBittorrent),
|
|
* puis les données sont passées au worker.
|
|
*/
|
|
export async function autoOrganizeAll(
|
|
listFn : () => Promise<any[]>,
|
|
onResult?: (r: { hash: string; name: string; done: number; skipped: number; errors: number; errorFiles: { file: string; error: string }[] }) => void
|
|
): Promise<void> {
|
|
if (workerRunning) {
|
|
console.log('[organize] Worker déjà en cours, skip')
|
|
return
|
|
}
|
|
|
|
let torrents: any[]
|
|
try {
|
|
torrents = await listFn()
|
|
} catch (err) {
|
|
console.error('[organize] Impossible de récupérer la liste des torrents:', err)
|
|
return
|
|
}
|
|
|
|
if (torrents.length === 0) return
|
|
|
|
const seedingCount = torrents.filter(t => t.state === 'seeding').length
|
|
if (seedingCount === 0) return
|
|
|
|
console.log(`[organize] Lancement worker (${seedingCount} torrents en seeding)`)
|
|
workerRunning = true
|
|
|
|
const workerPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'organize-worker.js')
|
|
|
|
const worker = new Worker(workerPath)
|
|
|
|
worker.on('message', (msg: any) => {
|
|
if (msg.type === 'log') {
|
|
if (msg.level === 'error') console.error(msg.msg)
|
|
else if (msg.level === 'warn') console.warn(msg.msg)
|
|
else console.log(msg.msg)
|
|
} else if (msg.type === 'result') {
|
|
console.log(`[organize] ${msg.name} → ${msg.done} OK, ${msg.skipped} skippés, ${msg.errors.length} erreurs`)
|
|
onResult?.({
|
|
hash : msg.hash,
|
|
name : msg.name,
|
|
done : msg.done,
|
|
skipped : msg.skipped,
|
|
errors : msg.errors.length,
|
|
errorFiles : msg.errors,
|
|
})
|
|
} else if (msg.type === 'done') {
|
|
console.log('[organize] Worker terminé')
|
|
workerRunning = false
|
|
worker.terminate()
|
|
}
|
|
})
|
|
|
|
worker.on('error', (err) => {
|
|
console.error('[organize] Worker erreur:', err)
|
|
workerRunning = false
|
|
})
|
|
|
|
worker.on('exit', (code) => {
|
|
if (code !== 0) console.error(`[organize] Worker exit code ${code}`)
|
|
workerRunning = false
|
|
})
|
|
|
|
// Envoyer les torrents au worker
|
|
worker.postMessage({ type: 'run', torrents })
|
|
}
|
|
|
|
/**
|
|
* Organisation manuelle d'un torrent (appelée depuis l'API /organize).
|
|
* Lance aussi dans un worker thread.
|
|
*/
|
|
export async function organizeTorrent(
|
|
hash : string,
|
|
name : string,
|
|
savePath: string
|
|
): Promise<OrganizeResult> {
|
|
return new Promise((resolve, reject) => {
|
|
const workerPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'organize-worker.js')
|
|
const worker = new Worker(workerPath)
|
|
|
|
// On envoie un seul torrent "fictif seeding"
|
|
const fakeTorrent = { hash, name, save_path: savePath, state: 'seeding' }
|
|
const result: OrganizeResult = { total: 0, skipped: 0, done: 0, errors: [] }
|
|
|
|
worker.on('message', (msg: any) => {
|
|
if (msg.type === 'log') {
|
|
if (msg.level === 'error') console.error(msg.msg)
|
|
else if (msg.level === 'warn') console.warn(msg.msg)
|
|
else console.log(msg.msg)
|
|
} else if (msg.type === 'result') {
|
|
result.total = msg.total
|
|
result.skipped = msg.skipped
|
|
result.done = msg.done
|
|
result.errors = msg.errors
|
|
} else if (msg.type === 'done') {
|
|
worker.terminate()
|
|
resolve(result)
|
|
}
|
|
})
|
|
|
|
worker.on('error', (err) => { worker.terminate(); reject(err) })
|
|
worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker exit ${code}`)) })
|
|
|
|
worker.postMessage({ type: 'run', torrents: [fakeTorrent] })
|
|
})
|
|
} |