feat(demande): ajout des routes pour recuperer les saisons et episodes depuis le plugin jellyfin

This commit is contained in:
MasutaYunikon committed 2026-05-23 14:09:06 +02:00
1 parent 5478fa6d80
commit 1176523d61
4 files changed
+113 -42

No files matched your search

+9
View File
@@ -21,6 +21,7 @@ import { readAvailable, readInfohashMap, loadEnrichedSeriesData } from './lib/gi
import { pushNotif } from './lib/notifs.js'
import { readRequests, completeRequest } from './requests.js'
import { checkNfoUpdates } from './lib/nfo.js'
import { runJellyfinSync } from './routes/jellyfin.js'
import cors from 'cors';
import usersRouter from './routes/users.js'
@@ -237,4 +238,12 @@ server.listen(PORT, async () => {
runRssSync().catch(err => logger.error('rss-sync', `Sync périodique échoué : ${err instanceof Error ? err.message : err}`))
}, 6 * 60 * 60_000)
}, 60_000) // premier cycle 1 min après le démarrage
// Jellyfin sync — toutes les heures (no-op si Jellyfin non configuré)
setTimeout(() => {
runJellyfinSync().catch(err => logger.error('jellyfin', `Sync initiale échouée : ${err instanceof Error ? err.message : err}`))
setInterval(() => {
runJellyfinSync().catch(err => logger.error('jellyfin', `Sync périodique échouée : ${err instanceof Error ? err.message : err}`))
}, 60 * 60_000)
}, 2 * 60_000) // premier cycle 2 min après le démarrage
})
+29 -25
View File
@@ -36,34 +36,38 @@ router.post('/jellyfin/test', async (_req, res) => {
res.json(result)
})
export async function runJellyfinSync(): Promise<{ created: number; skipped: number; users: string[] }> {
const { jellyfinUrl, jellyfinAdminToken } = readSettings()
if (!jellyfinUrl || !jellyfinAdminToken) return { created: 0, skipped: 0, users: [] }
const jellyfinUsers = await fetchJellyfinUsers()
const fankarrUsers = readUsers()
const results = { created: 0, skipped: 0, users: [] as string[] }
for (const jUser of jellyfinUsers) {
if (jUser.Policy?.IsDisabled) { results.skipped++; continue }
const exists = fankarrUsers.some(u =>
u.username.toLowerCase() === jUser.Name.toLowerCase()
)
if (exists) { results.skipped++; continue }
const randomPass = Math.random().toString(36).slice(2, 10) +
Math.random().toString(36).slice(2, 10)
createUser(jUser.Name, randomPass, 'user')
results.created++
results.users.push(jUser.Name)
logger.info('jellyfin', `Compte créé pour "${jUser.Name}" (sync Jellyfin)`)
}
logger.info('jellyfin', `Sync terminée — ${results.created} créés, ${results.skipped} ignorés`)
return results
}
// POST /api/jellyfin/sync
router.post('/jellyfin/sync', async (_req, res) => {
try {
const jellyfinUsers = await fetchJellyfinUsers()
const fankarrUsers = readUsers()
const results = { created: 0, skipped: 0, users: [] as string[] }
for (const jUser of jellyfinUsers) {
// Ignorer les comptes désactivés
if (jUser.Policy?.IsDisabled) { results.skipped++; continue }
const exists = fankarrUsers.some(u =>
u.username.toLowerCase() === jUser.Name.toLowerCase()
)
if (exists) { results.skipped++; continue }
// Créer le compte avec un mot de passe aléatoire (auth via SSO uniquement)
const randomPass = Math.random().toString(36).slice(2, 10) +
Math.random().toString(36).slice(2, 10)
createUser(jUser.Name, randomPass, 'user')
results.created++
results.users.push(jUser.Name)
logger.info('jellyfin', `Compte créé pour "${jUser.Name}" (sync Jellyfin)`)
}
logger.info('jellyfin', `Sync terminée — ${results.created} créés, ${results.skipped} ignorés`)
const results = await runJellyfinSync()
res.json(results)
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur sync Jellyfin' })
+70 -5
View File
@@ -7,9 +7,11 @@ import { Router } from 'express'
import { findByApiToken } from '../users.js'
import { validateJellyfinToken } from '../lib/jellyfin.js'
import { readUsers, safeUser } from '../users.js'
import { upsertRequest, getRequestsForUser } from '../requests.js'
import { upsertRequest, approveRequest, getRequestsForUser } from '../requests.js'
import { autoDownloadRequest } from './requests.js'
import { readSettings } from '../settings.js'
import { readAvailable } from '../lib/github-cache.js'
import { fankaiGet, normalizeSerie } from '../lib/serie-helpers.js'
import { fankaiGet, normalizeSerie, normalizeSeason, normalizeEpisode } from '../lib/serie-helpers.js'
import { logger } from '../logger.js'
import type { Request, Response, NextFunction } from 'express'
@@ -96,20 +98,83 @@ router.get('/v1/series/search', requireApiToken, async (req, res) => {
}
})
// ── GET /api/v1/series/:id ────────────────────────────────────
// Détail d'une série : saisons + épisodes (proxy FanKai)
router.get('/v1/series/:id', requireApiToken, async (req, res) => {
const id = Number(req.params.id)
if (isNaN(id)) { res.status(400).json({ error: 'id invalide' }); return }
try {
const [serieRaw, seasonsData] = await Promise.all([
fankaiGet(`/series/${id}`),
fankaiGet(`/series/${id}/seasons`),
])
const serie = normalizeSerie(serieRaw)
const seasons = Array.isArray(seasonsData) ? seasonsData : (seasonsData.seasons ?? [])
const seasonsWithEpisodes = await Promise.all(
seasons.map(async (season: any) => {
const epsData = await fankaiGet(`/seasons/${season.id}/episodes`)
const episodes = (Array.isArray(epsData) ? epsData : (epsData.episodes ?? []))
.map((ep: any) => ({
id : ep.id,
episode_number: ep.episode_number,
title : ep.title ?? null,
image : normalizeEpisode(ep).thumb_image ?? null,
}))
return {
...normalizeSeason(season),
season_number: season.season_number,
episodes,
}
})
)
res.json({
id : serie.id,
title : serie.title,
original_title: serie.original_title ?? null,
image : serie.poster_image ?? null,
seasons : seasonsWithEpisodes,
})
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur' })
}
})
// ── POST /api/v1/requests ─────────────────────────────────────
// Créer ou mettre à jour une demande
router.post('/v1/requests', requireApiToken, (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
}
try {
const request = upsertRequest(
const submittedSeasons = Array.isArray(seasons) ? seasons.map(Number) : []
const submittedEpisodes = Array.isArray(episodes) ? episodes.map(Number) : []
let request = upsertRequest(
req.user!.id,
Number(serieId),
String(serieName),
Array.isArray(seasons) ? seasons.map(Number) : [],
submittedSeasons,
submittedEpisodes,
)
// Auto-approbation + téléchargement si l'utilisateur est autorisé
const { requestAutoDownloadUsers } = readSettings()
const userId = req.user!.id
const allowed = requestAutoDownloadUsers === 'all'
|| (Array.isArray(requestAutoDownloadUsers) && requestAutoDownloadUsers.includes(userId))
if (allowed) {
const isNew = request.status === 'pending'
if (isNew) request = approveRequest(request.id)
const dlOverride = isNew ? undefined : { seasons: submittedSeasons, episodes: submittedEpisodes }
autoDownloadRequest(request, dlOverride).catch(err =>
logger.warn('public-api', `Auto-dl échoué pour "${request.serieName}" : ${err instanceof Error ? err.message : err}`)
)
}
res.json(request)
} catch (err) {
res.status(400).json({ error: err instanceof Error ? err.message : 'Erreur' })
+5 -12
View File
@@ -91,17 +91,10 @@ router.patch('/requests/:id', requireAdmin, async (req, res) => {
const id = String(req.params.id)
if (action === 'approve') {
request = approveRequest(id)
// Lancer le(s) téléchargement(s) si au moins un requester est autorisé
const { requestAutoDownloadUsers } = readSettings()
const enabled = requestAutoDownloadUsers === 'all'
? true
: Array.isArray(requestAutoDownloadUsers) && requestAutoDownloadUsers.length > 0
&& request.requesters.some(r => (requestAutoDownloadUsers as string[]).includes(r.userId))
if (enabled) {
autoDownloadRequest(request).catch(err =>
logger.warn('requests', `Auto-dl échoué pour "${request.serieName}" : ${err instanceof Error ? err.message : err}`)
)
}
// Approbation manuelle par l'admin → toujours lancer le téléchargement
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 }
@@ -116,7 +109,7 @@ router.patch('/requests/:id', requireAdmin, async (req, res) => {
* @param override - si fourni, utilise ces saisons/épisodes au lieu du merged de la demande
* (utile pour ne télécharger que ce qui vient d'être ajouté)
*/
async function autoDownloadRequest(req: SerieRequest, override?: { seasons: number[]; episodes: number[] }): Promise<void> {
export async function autoDownloadRequest(req: SerieRequest, override?: { seasons: number[]; episodes: number[] }): Promise<void> {
const serieData = await readSerieData(req.serieId)
if (!serieData) {
logger.warn('requests', `Auto-dl : aucune donnée torrent pour série ${req.serieId}`)