- Port par défaut 3001 → 9898 - Volume /app/data → /config (entrypoint, docker-compose, README) - Normalisation titres Kaï/Kai dans index.ts (catalogue + déduplication + serie detail) - Fix pagination GitLab NFO (per_page=100 → loop multi-pages) - Fix _is_bonus_filename avec word boundaries (\b) — évite faux positifs "national"→"ona" - Fix dossier Bonus/Specials exclus : récupère flottants (7,5/19,5) et S00Exx → saison 0 - _EP_FLOAT_PATTERN supporte virgules (7,5) en plus des points (7.5) - Ajout strict_season dans find_episode/resolve_ep — évite collision ep1 S1 vs ep1 S2 (JoJo Parties) - Ajout SEASON_EP_OFFSET pour Hokuto No Ken (saison 02 torrent → +16 offset API) - Fallback non-strict si strict_season échoue (Hokuto ep 5-16 S1) - Fix collect_covered_episodes : fallback torrent.serie_id si absent dans resolved_episodes - Fix s6_verify : get_all_specials + compteur S00 séparé, déduplication Kaï/Kai, format OK/TOTAL - Ajout MANUAL_TORRENTS dans s4 (MHA Film 4 You're Next URL Nyaa) - Injection torrents locaux manquants dans pipeline (Reborn! Kaï via fichier local) - Fix s5 : résolution directe torrents manuels season_number=0 - Fix s2 : pack_saga_slash et pack_partie_slash → pack_integrale, lookahead (?<!/) sur episode_num_tiret_apres - Fix s5 skip condition : re-résoudre si resolved_count < files_count (Fairy Tail) - README : note hardlinks volumes multiples, port, chemins config
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
/**
|
|
* logger.ts
|
|
* =========
|
|
* Logger centralisé — écrit dans data/logs.jsonl (une ligne JSON par event)
|
|
* Niveaux : debug | info | warn | error
|
|
* En prod (NODE_ENV=production) les lignes debug ne sont pas écrites sur disque.
|
|
*/
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
|
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
|
|
export interface LogEntry {
|
|
at : string // ISO timestamp
|
|
level : LogLevel
|
|
source : string // 'organize' | 'api' | 'scraper' | ...
|
|
msg : string
|
|
meta? : Record<string, any>
|
|
}
|
|
|
|
const LOGS_PATH = path.join(process.cwd(), 'config', 'logs.jsonl')
|
|
const MAX_LINES = 2000 // rotation auto au-delà
|
|
const IS_PROD = process.env.NODE_ENV === 'production'
|
|
|
|
// ── Écriture ──────────────────────────────────────────────────
|
|
|
|
function writeLine(entry: LogEntry) {
|
|
try {
|
|
fs.appendFileSync(LOGS_PATH, JSON.stringify(entry) + '\n', 'utf-8')
|
|
// Rotation si trop grand
|
|
const content = fs.readFileSync(LOGS_PATH, 'utf-8')
|
|
const lines = content.split('\n').filter(Boolean)
|
|
if (lines.length > MAX_LINES) {
|
|
fs.writeFileSync(LOGS_PATH, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8')
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// ── API publique ──────────────────────────────────────────────
|
|
|
|
export function log(level: LogLevel, source: string, msg: string, meta?: Record<string, any>) {
|
|
const entry: LogEntry = { at: new Date().toISOString(), level, source, msg, meta }
|
|
|
|
// Console
|
|
const prefix = `[${source}]`
|
|
if (level === 'error') console.error(prefix, msg, meta ?? '')
|
|
else if (level === 'warn') console.warn(prefix, msg, meta ?? '')
|
|
else if (level === 'debug' && !IS_PROD) console.log(prefix, msg, meta ?? '')
|
|
else if (level === 'info') console.log(prefix, msg, meta ?? '')
|
|
|
|
// Disque — pas les debug en prod
|
|
if (level === 'debug' && IS_PROD) return
|
|
writeLine(entry)
|
|
}
|
|
|
|
export const logger = {
|
|
debug: (source: string, msg: string, meta?: Record<string, any>) => log('debug', source, msg, meta),
|
|
info : (source: string, msg: string, meta?: Record<string, any>) => log('info', source, msg, meta),
|
|
warn : (source: string, msg: string, meta?: Record<string, any>) => log('warn', source, msg, meta),
|
|
error: (source: string, msg: string, meta?: Record<string, any>) => log('error', source, msg, meta),
|
|
}
|
|
|
|
// ── Lecture pour l'API ────────────────────────────────────────
|
|
|
|
export interface LogsReadOptions {
|
|
limit? : number // nb de lignes (défaut 100)
|
|
level? : LogLevel | 'all' // filtre niveau
|
|
source? : string // filtre source
|
|
}
|
|
|
|
export function readLogs(opts: LogsReadOptions = {}): LogEntry[] {
|
|
try {
|
|
if (!fs.existsSync(LOGS_PATH)) return []
|
|
const content = fs.readFileSync(LOGS_PATH, 'utf-8')
|
|
let lines = content.split('\n').filter(Boolean)
|
|
|
|
// Parse
|
|
let entries: LogEntry[] = []
|
|
for (const line of lines) {
|
|
try { entries.push(JSON.parse(line)) } catch {}
|
|
}
|
|
|
|
// Filtres
|
|
if (opts.level && opts.level !== 'all')
|
|
entries = entries.filter(e => e.level === opts.level)
|
|
if (opts.source)
|
|
entries = entries.filter(e => e.source === opts.source)
|
|
|
|
// Plus récents en premier
|
|
entries.reverse()
|
|
|
|
// Limite
|
|
const limit = opts.limit ?? 100
|
|
return entries.slice(0, limit)
|
|
} catch { return [] }
|
|
}
|
|
|
|
export function clearLogs(): void {
|
|
try { fs.writeFileSync(LOGS_PATH, '', 'utf-8') } catch {}
|
|
}
|
|
|
|
export function logsFileSize(): number {
|
|
try { return fs.statSync(LOGS_PATH).size } catch { return 0 }
|
|
} |