Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93397bee77 | ||
|
|
3f72c01911 | ||
|
|
77e6a2e38c | ||
|
|
bef99183aa |
No files matched your search
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.12.1](https://github.com/Masutayunikon/FanKarr/compare/v1.12.0...v1.12.1) (2026-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* path ([7ddd9ca](https://github.com/Masutayunikon/FanKarr/commit/7ddd9cad9271d79a258f45245bf405f13b9d764f))
|
||||||
|
|
||||||
## [1.12.0](https://github.com/Masutayunikon/FanKarr/compare/v1.11.10...v1.12.0) (2026-03-21)
|
## [1.12.0](https://github.com/Masutayunikon/FanKarr/compare/v1.11.10...v1.12.0) (2026-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+38
-58
@@ -15,31 +15,22 @@ import fs from 'fs'
|
|||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { parentPort } from 'worker_threads'
|
import { parentPort } from 'worker_threads'
|
||||||
|
import { DATA_DIR } from './config.js'
|
||||||
|
|
||||||
// Base directory : binaire Bun ou cwd (Docker/dev)
|
const TORRENTS_PATH = path.join(DATA_DIR, 'torrent_final.json')
|
||||||
const _isBunBinary = typeof (globalThis as any).Bun !== 'undefined'
|
const ORGANIZED_PATH = path.join(DATA_DIR, 'organized.json')
|
||||||
&& path.dirname((process as any).execPath) !== process.cwd()
|
|
||||||
const BASE_DIR = _isBunBinary
|
|
||||||
? path.dirname((process as any).execPath)
|
|
||||||
: process.cwd()
|
|
||||||
|
|
||||||
const TORRENTS_PATH = path.join(BASE_DIR, 'data', 'torrent_final.json')
|
|
||||||
const ORGANIZED_PATH = path.join(BASE_DIR, 'data', 'organized.json')
|
|
||||||
|
|
||||||
// ─── Utils log ────────────────────────────────────────────────
|
// ─── Utils log ────────────────────────────────────────────────
|
||||||
// Le worker tourne dans un thread séparé — il passe les logs au thread principal
|
|
||||||
// qui les écrit dans le fichier via logger.ts
|
|
||||||
|
|
||||||
function log(msg: string) { parentPort?.postMessage({ type: 'log', level: 'info', msg }) }
|
function log(msg: string) { parentPort?.postMessage({ type: 'log', level: 'info', msg }) }
|
||||||
function warn(msg: string) { parentPort?.postMessage({ type: 'log', level: 'warn', msg }) }
|
function warn(msg: string) { parentPort?.postMessage({ type: 'log', level: 'warn', msg }) }
|
||||||
function error(msg: string) { parentPort?.postMessage({ type: 'log', level: 'error', msg }) }
|
function error(msg: string) { parentPort?.postMessage({ type: 'log', level: 'error', msg }) }
|
||||||
function debug(msg: string) { parentPort?.postMessage({ type: 'log', level: 'debug', msg }) }
|
function debug(msg: string) { parentPort?.postMessage({ type: 'log', level: 'debug', msg }) }
|
||||||
|
|
||||||
// ─── Settings (lu directement, pas d'import circulaire) ──────
|
// ─── Settings ─────────────────────────────────────────────────
|
||||||
|
|
||||||
function readSettings(): { mediaPath: string; completePath: string; organizeMode: string; nfoSupport: boolean } {
|
function readSettings(): { mediaPath: string; completePath: string; organizeMode: string; nfoSupport: boolean } {
|
||||||
try {
|
try {
|
||||||
const p = path.join(process.cwd(), 'data', 'settings.json')
|
const p = path.join(DATA_DIR, 'settings.json')
|
||||||
if (!fs.existsSync(p)) return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false }
|
if (!fs.existsSync(p)) return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false }
|
||||||
return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||||
} catch { return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false } }
|
} catch { return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false } }
|
||||||
@@ -112,8 +103,6 @@ function getGitlabSerieTitle(serieTitle: string): string {
|
|||||||
return SERIE_TITLE_GITLAB_MAP[serieTitle] ?? serieTitle
|
return SERIE_TITLE_GITLAB_MAP[serieTitle] ?? serieTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ─── GitLab NFO downloader ────────────────────────────────────
|
// ─── GitLab NFO downloader ────────────────────────────────────
|
||||||
|
|
||||||
const GITLAB_API = 'https://gitlab.com/api/v4/projects/ElPouki%2Ffankai_pack/repository'
|
const GITLAB_API = 'https://gitlab.com/api/v4/projects/ElPouki%2Ffankai_pack/repository'
|
||||||
@@ -139,7 +128,6 @@ async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promi
|
|||||||
|
|
||||||
log(`[nfo] Récupération dossier GitLab: ${gitlabTitle}`)
|
log(`[nfo] Récupération dossier GitLab: ${gitlabTitle}`)
|
||||||
|
|
||||||
// Lister récursivement tous les fichiers du dossier (pagination GitLab)
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
try {
|
try {
|
||||||
let page = 1
|
let page = 1
|
||||||
@@ -166,13 +154,10 @@ async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promi
|
|||||||
log(`[nfo] ${fileEntries.length} fichiers à télécharger`)
|
log(`[nfo] ${fileEntries.length} fichiers à télécharger`)
|
||||||
|
|
||||||
for (const entry of fileEntries) {
|
for (const entry of fileEntries) {
|
||||||
// entry.path = "pack/Black Lagoon Henshū/Saison 1/fichier.nfo"
|
|
||||||
// On retire le préfixe "pack/{gitlabTitle}/" pour obtenir le chemin relatif
|
|
||||||
const relativePath = entry.path.replace(`pack/${gitlabTitle}/`, '')
|
const relativePath = entry.path.replace(`pack/${gitlabTitle}/`, '')
|
||||||
const destPath = path.join(destRoot, relativePath)
|
const destPath = path.join(destRoot, relativePath)
|
||||||
const destDir = path.dirname(destPath)
|
const destDir = path.dirname(destPath)
|
||||||
|
|
||||||
// Skip si déjà présent
|
|
||||||
if (fs.existsSync(destPath)) continue
|
if (fs.existsSync(destPath)) continue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -225,15 +210,9 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
|
|
||||||
// ── Téléchargement NFO/images depuis GitLab ───────────────
|
// ── Téléchargement NFO/images depuis GitLab ───────────────
|
||||||
if (nfoSupport) {
|
if (nfoSupport) {
|
||||||
// Collecter tous les titres de séries distincts depuis resolved_episodes
|
|
||||||
// (un pack_integrale peut mélanger plusieurs séries, ex: One Piece Yabai + Kaï)
|
|
||||||
const serieTitlesMap = new Map<number, string>()
|
const serieTitlesMap = new Map<number, string>()
|
||||||
|
|
||||||
// Titre principal du torrent
|
|
||||||
serieTitlesMap.set(torrent.serie_id ?? 0, serieTitle)
|
serieTitlesMap.set(torrent.serie_id ?? 0, serieTitle)
|
||||||
|
|
||||||
// Titres alternatifs depuis resolved_episodes
|
|
||||||
// resolved_episodes n'a pas toujours serie_title → on cherche dans torrent_final.json
|
|
||||||
const allTorrents: any[] = (() => {
|
const allTorrents: any[] = (() => {
|
||||||
try { return JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) } catch { return [] }
|
try { return JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) } catch { return [] }
|
||||||
})()
|
})()
|
||||||
@@ -249,7 +228,6 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Télécharger NFO pour chaque série distincte
|
|
||||||
for (const title of serieTitlesMap.values()) {
|
for (const title of serieTitlesMap.values()) {
|
||||||
const serieDestRoot = path.join(mediaPath, title)
|
const serieDestRoot = path.join(mediaPath, title)
|
||||||
await downloadGitlabFolder(title, serieDestRoot)
|
await downloadGitlabFolder(title, serieDestRoot)
|
||||||
@@ -289,7 +267,32 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
return { ...result, total: 1, done: 1 }
|
return { ...result, total: 1, done: 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Map filename → season ─────────────────────────────────
|
||||||
|
const filenameSeasonMap = new Map<string, number>()
|
||||||
|
|
||||||
|
// Index par filename depuis resolved_episodes (évite les collisions episode_number)
|
||||||
|
const filenameToSeason = new Map<string, number>()
|
||||||
|
for (const ep of torrent.resolved_episodes ?? []) {
|
||||||
|
if (ep.filename && ep.season_number !== undefined)
|
||||||
|
filenameToSeason.set(ep.filename, ep.season_number)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tf of torrentFiles) {
|
||||||
|
const sn = tf.season_number != null ? Number(tf.season_number) : null
|
||||||
|
if (sn !== null) {
|
||||||
|
filenameSeasonMap.set(tf.filename, sn)
|
||||||
|
} else {
|
||||||
|
const fromResolved = filenameToSeason.get(tf.filename)
|
||||||
|
if (fromResolved !== undefined) {
|
||||||
|
filenameSeasonMap.set(tf.filename, fromResolved)
|
||||||
|
} else if (torrent.season_number !== undefined) {
|
||||||
|
filenameSeasonMap.set(tf.filename, torrent.season_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Map filename → original_filename (depuis l'API) ────────
|
// ── Map filename → original_filename (depuis l'API) ────────
|
||||||
|
// Permet de renommer le fichier selon le nom standardisé Fankai
|
||||||
const filenameToOriginal = new Map<string, string>()
|
const filenameToOriginal = new Map<string, string>()
|
||||||
for (const ep of torrent.resolved_episodes ?? []) {
|
for (const ep of torrent.resolved_episodes ?? []) {
|
||||||
if (ep.filename && ep.original_filename && ep.filename !== ep.original_filename) {
|
if (ep.filename && ep.original_filename && ep.filename !== ep.original_filename) {
|
||||||
@@ -297,30 +300,6 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Map filename → season ─────────────────────────────────
|
|
||||||
// Priorité : season_number dans torrent_files (packs multi-saisons)
|
|
||||||
// Fallback : resolved_episodes matchés par episode_number (torrents individuels)
|
|
||||||
const filenameSeasonMap = new Map<string, number>()
|
|
||||||
|
|
||||||
// Index resolved_episodes par episode_number pour lookup rapide
|
|
||||||
const epByNum = new Map<number, number>() // ep_number → season_number
|
|
||||||
for (const ep of torrent.resolved_episodes ?? []) {
|
|
||||||
if (ep.episode_number !== undefined && ep.season_number !== undefined)
|
|
||||||
epByNum.set(Number(ep.episode_number), ep.season_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tf of torrentFiles) {
|
|
||||||
const sn: number | null = tf.season_number != null ? Number(tf.season_number) : null
|
|
||||||
const tfNum = tf.num !== undefined ? Number(tf.num) : undefined
|
|
||||||
if (sn !== null) {
|
|
||||||
filenameSeasonMap.set(tf.filename, sn)
|
|
||||||
} else if (tfNum !== undefined && epByNum.has(tfNum)) {
|
|
||||||
filenameSeasonMap.set(tf.filename, epByNum.get(tfNum)!)
|
|
||||||
} else if (torrent.season_number !== undefined) {
|
|
||||||
filenameSeasonMap.set(tf.filename, torrent.season_number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.total = torrentFiles.length
|
result.total = torrentFiles.length
|
||||||
|
|
||||||
for (const tf of torrentFiles) {
|
for (const tf of torrentFiles) {
|
||||||
@@ -328,7 +307,9 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
const filePath: string[] = tf.path
|
const filePath: string[] = tf.path
|
||||||
|
|
||||||
// Skip les fichiers dans des dossiers bonus/musique/images
|
// Skip les fichiers dans des dossiers bonus/musique/images
|
||||||
if (isInExcludedFolder(filePath)) {
|
// Exception : fichiers avec season_number explicite (saison 0)
|
||||||
|
const tfMeta = torrentFiles.find((tf: any) => tf.filename === filename)
|
||||||
|
if (isInExcludedFolder(filePath) && tfMeta?.season_number == null) {
|
||||||
log(`[organize] skip bonus: ${filename}`)
|
log(`[organize] skip bonus: ${filename}`)
|
||||||
result.skipped++
|
result.skipped++
|
||||||
continue
|
continue
|
||||||
@@ -358,10 +339,10 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const seasonNum = filenameSeasonMap.get(filename) ?? 1
|
const seasonNum = filenameSeasonMap.get(filename) ?? 1
|
||||||
const destFilename = filenameToOriginal.get(filename) ?? filename
|
const destFilename = filenameToOriginal.get(filename) ?? filename
|
||||||
const destDir = path.join(mediaPath, serieTitle, seasonFolder(seasonNum))
|
const destDir = path.join(mediaPath, serieTitle, seasonFolder(seasonNum))
|
||||||
const dest = path.join(destDir, destFilename)
|
const dest = path.join(destDir, destFilename)
|
||||||
|
|
||||||
if (fs.existsSync(dest)) {
|
if (fs.existsSync(dest)) {
|
||||||
markOrganized(hash, filename)
|
markOrganized(hash, filename)
|
||||||
@@ -379,7 +360,7 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
|
|||||||
}
|
}
|
||||||
markOrganized(hash, filename)
|
markOrganized(hash, filename)
|
||||||
result.done++
|
result.done++
|
||||||
log(`[organize] ✓ ${filename} → Saison ${String(seasonNum).padStart(3,'0')}`)
|
log(`[organize] ✓ ${destFilename} → Saison ${String(seasonNum).padStart(3,'0')}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
|
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
error(`[organize] ✗ ${filename}: ${msg}`)
|
error(`[organize] ✗ ${filename}: ${msg}`)
|
||||||
@@ -409,7 +390,6 @@ parentPort?.on('message', async (msg: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files: any[] = torrent.torrent_files ?? []
|
const files: any[] = torrent.torrent_files ?? []
|
||||||
// Pour fichier unique : vérifier via le name directement
|
|
||||||
const allDone = files.length > 0
|
const allDone = files.length > 0
|
||||||
? files.every((f: any) => organized[t.hash]?.[f.filename])
|
? files.every((f: any) => organized[t.hash]?.[f.filename])
|
||||||
: !!organized[t.hash]?.[t.name]
|
: !!organized[t.hash]?.[t.name]
|
||||||
|
|||||||
Reference in new issue
Block a user