feat(demandes): telechargement auto lors de l'aprobation

feat(demandes): ajout des demandes par épisodes
This commit is contained in:
MasutaYunikon committed 2026-05-22 20:43:30 +02:00
1 parent f3d535d577
commit 473d97e392
5 files changed
+187 -36

No files matched your search

+28 -12
View File
@@ -15,6 +15,7 @@ export interface Requester {
userId : string
username : string
seasons : number[] // numéros de saison ; [] = toutes les saisons dispo
episodes : number[] // IDs d'épisodes spécifiques ; [] = pas d'épisode ciblé
requestedAt: string
}
@@ -47,12 +48,19 @@ function writeRequests(requests: SerieRequest[]): void {
/** Union des saisons demandées sur tous les requesters ([] = toutes) */
export function mergedSeasons(req: SerieRequest): number[] {
if (req.requesters.some(r => r.seasons.length === 0)) return []
if (req.requesters.some(r => r.seasons.length === 0 && r.episodes.length === 0)) return []
const all = new Set<number>()
req.requesters.forEach(r => r.seasons.forEach(s => all.add(s)))
return [...all].sort((a, b) => a - b)
}
/** Union des IDs d'épisodes spécifiques sur tous les requesters */
export function mergedEpisodes(req: SerieRequest): number[] {
const all = new Set<number>()
req.requesters.forEach(r => (r.episodes ?? []).forEach(e => all.add(e)))
return [...all].sort((a, b) => a - b)
}
// ── CRUD ──────────────────────────────────────────────────────
/**
@@ -61,10 +69,11 @@ export function mergedSeasons(req: SerieRequest): number[] {
* - Sinon → crée une nouvelle demande
*/
export function upsertRequest(
userId : string,
serieId : number,
userId : string,
serieId : number,
serieName: string,
seasons : number[], // [] = toutes les saisons
seasons : number[], // [] = toutes les saisons
episodes : number[] = [], // IDs d'épisodes spécifiques
): SerieRequest {
const user = findById(userId)
if (!user) throw new Error('Utilisateur introuvable')
@@ -84,16 +93,23 @@ export function upsertRequest(
// Mettre à jour ou ajouter ce requester dans la demande existante
const idx = existing.requesters.findIndex(r => r.userId === userId)
if (idx >= 0) {
// Mettre à jour les saisons (union)
if (seasons.length === 0 || existing.requesters[idx].seasons.length === 0) {
existing.requesters[idx].seasons = [] // l'une ou l'autre est "toutes" → toutes
const r = existing.requesters[idx]
// Fusionner les saisons
if (seasons.length === 0 && episodes.length === 0) {
r.seasons = [] // "toutes" — efface toute granularité
r.episodes = []
} else {
const merged = new Set([...existing.requesters[idx].seasons, ...seasons])
existing.requesters[idx].seasons = [...merged].sort((a, b) => a - b)
if (seasons.length === 0 || r.seasons.length === 0) {
r.seasons = [] // l'une des deux est "toutes" → toutes
} else {
r.seasons = [...new Set([...r.seasons, ...seasons])].sort((a, b) => a - b)
}
// Fusionner les épisodes (union simple)
r.episodes = [...new Set([...(r.episodes ?? []), ...episodes])].sort((a, b) => a - b)
}
existing.requesters[idx].requestedAt = now
r.requestedAt = now
} else {
existing.requesters.push({ userId, username: user.username, seasons, requestedAt: now })
existing.requesters.push({ userId, username: user.username, seasons, episodes, requestedAt: now })
}
existing.updatedAt = now
writeRequests(requests)
@@ -106,7 +122,7 @@ export function upsertRequest(
id : crypto.randomUUID(),
serieId,
serieName,
requesters: [{ userId, username: user.username, seasons, requestedAt: now }],
requesters: [{ userId, username: user.username, seasons, episodes, requestedAt: now }],
status : 'pending',
createdAt : now,
updatedAt : now,
+94 -8
View File
@@ -3,8 +3,12 @@ import { requireAdmin } from '../auth.js'
import {
readRequests, upsertRequest, approveRequest,
rejectRequest, completeRequest, deleteRequest,
getRequestsForUser, getPendingCount, mergedSeasons,
getRequestsForUser, getPendingCount, mergedSeasons, mergedEpisodes,
type SerieRequest,
} from '../requests.js'
import { dispatchDownload } from '../torrent-clients/index.js'
import { readSerieData } from '../lib/github-cache.js'
import { logger } from '../logger.js'
const router = Router()
@@ -26,7 +30,7 @@ router.get('/requests/pending-count', (req, res) => {
// POST /api/requests — créer ou mettre à jour une demande
router.post('/requests', (req, res) => {
const { serieId, serieName, seasons } = req.body
const { serieId, serieName, seasons, episodes } = req.body
if (!serieId || !serieName) {
res.status(400).json({ error: 'serieId et serieName requis' }); return
}
@@ -35,7 +39,8 @@ router.post('/requests', (req, res) => {
req.user!.id,
Number(serieId),
String(serieName),
Array.isArray(seasons) ? seasons.map(Number) : [],
Array.isArray(seasons) ? seasons.map(Number) : [],
Array.isArray(episodes) ? episodes.map(Number) : [],
)
res.json(request)
} catch (err) {
@@ -44,14 +49,19 @@ router.post('/requests', (req, res) => {
})
// PATCH /api/requests/:id — admin : approve / reject / complete
router.patch('/requests/:id', requireAdmin, (req, res) => {
router.patch('/requests/:id', requireAdmin, async (req, res) => {
const { action, rejectionMessage } = req.body
try {
let request
let request: SerieRequest
const id = String(req.params.id)
if (action === 'approve') request = approveRequest(id)
else if (action === 'reject') request = rejectRequest(id, rejectionMessage)
else if (action === 'complete') request = completeRequest(id)
if (action === 'approve') {
request = approveRequest(id)
// Lancer le(s) téléchargement(s) correspondant à la demande
autoDownloadRequest(request).catch(err =>
logger.warn('requests', `Auto-dl échoué pour "${request.serieName}" : ${err instanceof Error ? err.message : err}`)
)
} else if (action === 'reject') request = rejectRequest(id, rejectionMessage)
else if (action === 'complete') request = completeRequest(id)
else { res.status(400).json({ error: 'Action invalide (approve | reject | complete)' }); return }
res.json(request)
} catch (err) {
@@ -59,6 +69,82 @@ router.patch('/requests/:id', requireAdmin, (req, res) => {
}
})
/** Trouve et envoie au client torrent les fichiers correspondant à la demande. */
async function autoDownloadRequest(req: SerieRequest): Promise<void> {
const serieData = await readSerieData(req.serieId)
if (!serieData) {
logger.warn('requests', `Auto-dl : aucune donnée torrent pour série ${req.serieId}`)
return
}
const seasons = mergedSeasons(req) // [] = toutes
const episodes = mergedEpisodes(req) // IDs d'épisodes spécifiques
type TorrentEntry = { url: string; magnet: string | null; infohash: string | null; file_index?: number | null; file_path?: string | null }
const toDownload: TorrentEntry[] = []
const seenHashes = new Set<string>()
function addTorrent(t: any, file_index?: number | null, file_path?: string | null) {
const key = `${t.infohash ?? t.magnet ?? t.torrent_url}:${file_index ?? ''}`
if (seenHashes.has(key)) return
seenHashes.add(key)
toDownload.push({ url: t.torrent_url, magnet: t.magnet ?? null, infohash: t.infohash ?? null, file_index: file_index ?? null, file_path: file_path ?? null })
}
if (episodes.length > 0) {
// ── Épisodes ciblés ───────────────────────────────────────
for (const season of serieData.seasons ?? []) {
for (const ep of season.episodes ?? []) {
if (!episodes.includes(ep.id)) continue
if (ep.torrents?.length > 0) {
// Torrent individuel
for (const t of ep.torrents) addTorrent(t)
} else if (ep.paths?.length > 0) {
// Épisode contenu dans un pack — télécharger le fichier spécifique
for (const p of ep.paths) {
const pack = [...(serieData.torrents ?? []), ...(serieData.seasons ?? []).flatMap((s: any) => s.torrents ?? [])]
.find((t: any) => t.infohash?.toLowerCase() === p.infohash?.toLowerCase())
if (pack) addTorrent(pack, p.file_index ?? null, p.path ?? null)
}
}
}
}
} else if (seasons.length > 0) {
// ── Saisons ciblées ───────────────────────────────────────
for (const season of serieData.seasons ?? []) {
if (!seasons.includes(season.season_number)) continue
for (const t of season.torrents ?? []) addTorrent(t)
}
} else {
// ── Toutes les saisons : intégrale en priorité ────────────
for (const t of serieData.torrents ?? []) addTorrent(t)
// Sinon packs saison
if (toDownload.length === 0) {
for (const season of serieData.seasons ?? []) {
for (const t of season.torrents ?? []) addTorrent(t)
}
}
}
if (toDownload.length === 0) {
logger.warn('requests', `Auto-dl : aucun torrent trouvé pour "${req.serieName}"`)
return
}
let sent = 0
for (const t of toDownload) {
const url = t.url ?? t.magnet
if (!url) continue
try {
await dispatchDownload(url, { infohash: t.infohash ?? undefined, magnet: t.magnet ?? undefined, file_index: t.file_index ?? null, file_path: t.file_path ?? null })
sent++
} catch (err) {
logger.warn('requests', `Auto-dl erreur torrent : ${err instanceof Error ? err.message : err}`)
}
}
logger.info('requests', `Auto-dl "${req.serieName}" — ${sent}/${toDownload.length} torrent(s) envoyé(s)`)
}
// DELETE /api/requests/:id — admin seulement
router.delete('/requests/:id', requireAdmin, (req, res) => {
try {