feat(ui): refonte de l'interface
This commit is contained in:
1 parent
3ede3e808c
commit
09eb1fa3aa
79 files changed
+5323
-3514
No files matched your search
@@ -8,7 +8,7 @@ const GITHUB_BASE =
|
||||
|
||||
const CACHE_TTL_MS = 1 * 60 * 60 * 1000
|
||||
|
||||
interface CacheEntry<T> { data: T; expiresAt: number }
|
||||
interface CacheEntry<T> { data: T; expiresAt: number; fetchedAt: number }
|
||||
const _cache = new Map<string, CacheEntry<any>>()
|
||||
|
||||
export function cacheGet<T>(key: string): T | null {
|
||||
@@ -19,7 +19,13 @@ export function cacheGet<T>(key: string): T | null {
|
||||
}
|
||||
|
||||
export function cacheSet<T>(key: string, data: T): void {
|
||||
_cache.set(key, { data, expiresAt: Date.now() + CACHE_TTL_MS })
|
||||
_cache.set(key, { data, expiresAt: Date.now() + CACHE_TTL_MS, fetchedAt: Date.now() })
|
||||
}
|
||||
|
||||
// Date de la dernière lecture d'une clé
|
||||
export function cacheFetchedAt(key: string): string | null {
|
||||
const entry = _cache.get(key)
|
||||
return entry && Date.now() <= entry.expiresAt ? new Date(entry.fetchedAt).toISOString() : null
|
||||
}
|
||||
|
||||
export function cacheClear(): void { _cache.clear() }
|
||||
|
||||
+46
-4
@@ -1,12 +1,54 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { DATA_DIR } from '../config.js'
|
||||
|
||||
export interface OrganizeNotif {
|
||||
hash: string; name: string; done: number; skipped: number; errors: number
|
||||
hash: string; name: string; serieId?: number | null; done: number; skipped: number; errors: number
|
||||
errorFiles: { file: string; error: string }[]; at: string
|
||||
}
|
||||
|
||||
export const recentOrganized: OrganizeNotif[] = []
|
||||
const MAX_NOTIFS = 20
|
||||
export interface RecentSerieImport { serieId: number; episodes: number; at: string }
|
||||
|
||||
const HISTORY_PATH = path.join(DATA_DIR, 'import-history.json')
|
||||
const MAX_HISTORY = 200
|
||||
const RECENT_WINDOW_MS = 7 * 24 * 60 * 60_000
|
||||
|
||||
function loadHistory(): OrganizeNotif[] {
|
||||
try {
|
||||
if (!fs.existsSync(HISTORY_PATH)) return []
|
||||
const data = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'))
|
||||
return Array.isArray(data) ? data.slice(0, MAX_HISTORY) : []
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
function saveHistory(): void {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(HISTORY_PATH), { recursive: true })
|
||||
fs.writeFileSync(HISTORY_PATH, JSON.stringify(recentOrganized, null, 2))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Du plus récent au plus ancien
|
||||
export const recentOrganized: OrganizeNotif[] = loadHistory()
|
||||
|
||||
export function pushNotif(n: OrganizeNotif) {
|
||||
recentOrganized.unshift(n)
|
||||
if (recentOrganized.length > MAX_NOTIFS) recentOrganized.pop()
|
||||
if (recentOrganized.length > MAX_HISTORY) recentOrganized.length = MAX_HISTORY
|
||||
saveHistory()
|
||||
}
|
||||
|
||||
export function clearNotifs() {
|
||||
recentOrganized.length = 0
|
||||
saveHistory()
|
||||
}
|
||||
|
||||
export function recentImportsBySerie(): RecentSerieImport[] {
|
||||
const bySerie = new Map<number, RecentSerieImport>()
|
||||
for (const n of recentOrganized) {
|
||||
if (n.serieId == null || n.done <= 0) continue
|
||||
const current = bySerie.get(n.serieId)
|
||||
if (!current) { bySerie.set(n.serieId, { serieId: n.serieId, episodes: n.done, at: n.at }); continue }
|
||||
if (Date.parse(current.at) - Date.parse(n.at) <= RECENT_WINDOW_MS) current.episodes += n.done
|
||||
}
|
||||
return [...bySerie.values()]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
export type RequestTorrent = { url: string; magnet: string | null; infohash: string | null; file_index?: number | null; file_path?: string | null }
|
||||
|
||||
/**
|
||||
* Torrents à envoyer pour une portée de demande.
|
||||
* episodes non vide → épisodes ciblés ; sinon seasons non vide → saisons ciblées ; sinon toute la série puis voila
|
||||
*/
|
||||
export function resolveRequestTorrents(serieData: any, seasons: number[], episodes: number[]): RequestTorrent[] {
|
||||
const toDownload: RequestTorrent[] = []
|
||||
const seenHashes = new Set<string>()
|
||||
|
||||
const allPackTorrents: any[] = [
|
||||
...(serieData.torrents ?? []),
|
||||
...(serieData.seasons ?? []).flatMap((s: any) => s.torrents ?? []),
|
||||
]
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute le premier torrent disponible par épisode
|
||||
*/
|
||||
function addEpisodesByIndex(epList: any[]) {
|
||||
for (const ep of epList) {
|
||||
if (ep.torrents?.length > 0) {
|
||||
// Premier torrent individuel disponible
|
||||
addTorrent(ep.torrents[0])
|
||||
} else if (ep.paths?.length > 0) {
|
||||
// Premier path dans un pack
|
||||
const p = ep.paths[0]
|
||||
const pack = allPackTorrents.find((t: any) => t.infohash?.toLowerCase() === p.infohash?.toLowerCase())
|
||||
if (pack) addTorrent(pack, p.file_index ?? null, p.path ?? null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (episodes.length > 0) {
|
||||
// ── Épisodes ciblés ───────────────────────────────────────
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
const targets = (season.episodes ?? []).filter((ep: any) => episodes.includes(ep.id))
|
||||
addEpisodesByIndex(targets)
|
||||
}
|
||||
} else if (seasons.length > 0) {
|
||||
// ── Saisons ciblées ───────────────────────────────────────
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
if (!seasons.includes(season.season_number)) continue
|
||||
if ((season.torrents ?? []).length > 0) {
|
||||
// Premier pack saison disponible → télécharger en bloc
|
||||
addTorrent(season.torrents[0])
|
||||
} else {
|
||||
// Pas de pack → épisode par épisode avec file_index
|
||||
addEpisodesByIndex(season.episodes ?? [])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ── Toutes les saisons : intégrale en priorité ────────────
|
||||
if ((serieData.torrents ?? []).length > 0) {
|
||||
addTorrent(serieData.torrents[0])
|
||||
} else if ((serieData.seasons ?? []).some((s: any) => s.torrents?.length > 0)) {
|
||||
// Packs saison (premier par saison)
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
if (season.torrents?.length > 0) addTorrent(season.torrents[0])
|
||||
}
|
||||
} else {
|
||||
// Fallback : épisode par épisode avec file_index
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
addEpisodesByIndex(season.episodes ?? [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toDownload
|
||||
}
|
||||
|
||||
// Épisodes uniques concernés par la même portée
|
||||
export function countRequestedEpisodes(serieData: any, seasons: number[], episodes: number[]): number {
|
||||
const keys = new Set<string>()
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
for (const ep of season.episodes ?? []) {
|
||||
const inScope = episodes.length > 0
|
||||
? episodes.includes(ep.id)
|
||||
: seasons.length === 0 || seasons.includes(season.season_number)
|
||||
if (inScope) keys.add(`${season.season_number}:${ep.episode_number}`)
|
||||
}
|
||||
}
|
||||
return keys.size
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Cadence de l'import automatique
|
||||
export const AUTO_IMPORT_INTERVAL_MS = 5 * 60_000
|
||||
|
||||
export const autoImportSchedule = {
|
||||
lastRunAt: null as string | null,
|
||||
nextRunAt: null as string | null,
|
||||
}
|
||||
|
||||
export function planNextAutoImport(delayMs = AUTO_IMPORT_INTERVAL_MS) {
|
||||
autoImportSchedule.nextRunAt = new Date(Date.now() + delayMs).toISOString()
|
||||
}
|
||||
@@ -212,12 +212,20 @@ export function buildResolvedEpisodes(sd: any, hash: string, seasonFilter?: numb
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function computeSerieDownloadState(serieData: any | null, organized: Record<string, Record<string, any>>, activeTorrents: Set<string>): 'none' | 'downloading' | 'partial' | 'complete' {
|
||||
export function computeSerieDownloadState(
|
||||
serieData : any | null,
|
||||
organized : Record<string, Record<string, any>>,
|
||||
activeTorrents: Set<string>,
|
||||
counts = serieData ? countOrganizedEpisodes(serieData, organized) : { total: 0, organized: 0 },
|
||||
): 'none' | 'downloading' | 'partial' | 'complete' {
|
||||
if (!serieData) return 'none'
|
||||
const allTorrents = extractTorrentsFromSerieData(serieData)
|
||||
|
||||
if (allTorrents.some(t => t.infohash && activeTorrents.has(t.infohash.toLowerCase()))) return 'downloading'
|
||||
if (extractTorrentsFromSerieData(serieData).some(t => t.infohash && activeTorrents.has(t.infohash.toLowerCase()))) return 'downloading'
|
||||
if (counts.total === 0 || counts.organized === 0) return 'none'
|
||||
if (counts.organized >= counts.total) return 'complete'
|
||||
return 'partial'
|
||||
}
|
||||
|
||||
export function countOrganizedEpisodes(serieData: any, organized: Record<string, Record<string, any>>): { total: number; organized: number } {
|
||||
// Dédoublonnage : épisodes uniques par (season_number, episode_number)
|
||||
// pour éviter de compter x264 + x265 comme deux épisodes distincts
|
||||
const uniqueEpKeys = new Set<string>()
|
||||
@@ -232,7 +240,7 @@ export function computeSerieDownloadState(serieData: any | null, organized: Reco
|
||||
|
||||
const organizedEpKeys = new Set<string>()
|
||||
|
||||
for (const t of allTorrents) {
|
||||
for (const t of extractTorrentsFromSerieData(serieData)) {
|
||||
const hash = t.infohash?.toLowerCase()
|
||||
if (!hash) continue
|
||||
const orgFiles = organized[hash] ?? {}
|
||||
@@ -250,7 +258,5 @@ export function computeSerieDownloadState(serieData: any | null, organized: Reco
|
||||
for (const [id, key] of epIdToKey)
|
||||
if (trackedIds.has(String(id))) organizedEpKeys.add(key)
|
||||
|
||||
if (uniqueEpKeys.size === 0 || organizedEpKeys.size === 0) return 'none'
|
||||
if (organizedEpKeys.size >= uniqueEpKeys.size) return 'complete'
|
||||
return 'partial'
|
||||
return { total: uniqueEpKeys.size, organized: organizedEpKeys.size }
|
||||
}
|
||||
Reference in new issue
Block a user