feat(demande): ajout des routes pour recuperer les saisons et episodes depuis le plugin jellyfin
This commit is contained in:
1 parent
5478fa6d80
commit
1176523d61
4 files changed
+113
-42
No files matched your search
@@ -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
@@ -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' })
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
Reference in new issue
Block a user