Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dffb340250 | ||
|
|
059298f537 | ||
|
|
0ba2144f2f | ||
|
|
83a2fb9860 | ||
|
|
5445fadf94 | ||
|
|
6d110b86dd | ||
|
|
2ef6d4738a | ||
|
|
65f7d4e109 | ||
|
|
6166c370ef | ||
|
|
ad1cade6bf |
No files matched your search
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [3.1.1](https://github.com/Masutayunikon/FanKarr/compare/v3.1.0...v3.1.1) (2026-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove plex settings ([8744ade](https://github.com/Masutayunikon/FanKarr/commit/8744adeff5f2db1ab648412d50495690d9e68d0b))
|
||||
* torrent for multiple seasons is now working ([2514008](https://github.com/Masutayunikon/FanKarr/commit/25140086c13ce5a497500659bdd6cea2aeff96ed))
|
||||
|
||||
## [3.1.0](https://github.com/Masutayunikon/FanKarr/compare/v3.0.0...v3.1.0) (2026-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* use original_name and formated_name from scraper to bypass timeout ([1b693fc](https://github.com/Masutayunikon/FanKarr/commit/1b693fc53b9ca3a830c952c3f2c101bf1da7d27e))
|
||||
|
||||
## [3.0.0](https://github.com/Masutayunikon/FanKarr/compare/v2.4.1...v3.0.0) (2026-03-27)
|
||||
|
||||
|
||||
|
||||
+3
-29
@@ -41,8 +41,8 @@ app.get('/api/settings', requireAuth, (_req, res) => {
|
||||
res.json(readSettings())
|
||||
})
|
||||
app.post('/api/settings', requireAuth, (req, res) => {
|
||||
const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, usePlexTitles } = req.body
|
||||
const updated = writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, usePlexTitles })
|
||||
const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport } = req.body
|
||||
const updated = writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport })
|
||||
logger.info('api', 'Paramètres mis à jour')
|
||||
res.json(updated)
|
||||
})
|
||||
@@ -187,36 +187,10 @@ function normalizeEpisode(ep: any): any {
|
||||
return { ...ep, thumb_image: imgProxy(ep.thumb_image ?? ep.thumbnail ?? null, 400) }
|
||||
}
|
||||
|
||||
async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise<any[]> {
|
||||
return Promise.all(seriesData.map(async (sd) => {
|
||||
if (!sd) return sd
|
||||
const [serieApi, enrichedSeasons] = await Promise.all([
|
||||
fankaiGet(`/series/${sd.id}`).catch(() => null),
|
||||
Promise.all((sd.seasons ?? []).map(async (season: any) => {
|
||||
let epsData: any[] = []
|
||||
try {
|
||||
let lastErr: any
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try { const res = await fankaiGet(`/seasons/${season.id}/episodes`); epsData = Array.isArray(res) ? res : (res.episodes ?? []); break }
|
||||
catch (e) { lastErr = e; if (attempt === 0) await new Promise(r => setTimeout(r, 500)) }
|
||||
}
|
||||
if (epsData.length === 0 && lastErr) throw lastErr
|
||||
} catch { return season }
|
||||
const origMap = new Map<number, { original_filename: string; formatted_name: string }>()
|
||||
for (const ep of epsData) {
|
||||
if (ep.id) origMap.set(ep.id, { original_filename: ep.original_filename ?? '', formatted_name: ep.formatted_name ?? '' })
|
||||
}
|
||||
return { ...season, episodes: (season.episodes ?? []).map((ep: any) => ({ ...ep, original_filename: origMap.get(ep.id)?.original_filename ?? null, formatted_name: origMap.get(ep.id)?.formatted_name ?? null })) }
|
||||
}))
|
||||
])
|
||||
return { ...sd, title_for_plex: serieApi?.title_for_plex ?? null, seasons: enrichedSeasons }
|
||||
}))
|
||||
}
|
||||
|
||||
async function loadEnrichedSeriesData(): Promise<any[]> {
|
||||
const ids = await readAvailable()
|
||||
const allSd = await Promise.all(ids.map(id => readSerieData(id)))
|
||||
return enrichSeriesDataWithOriginalFilenames(allSd.filter(Boolean))
|
||||
return allSd.filter(Boolean)
|
||||
}
|
||||
|
||||
function computeSerieDownloadState(serieData: any | null, organized: Record<string, Record<string, string>>, activeTorrents: Set<string>): 'none' | 'downloading' | 'partial' | 'complete' {
|
||||
|
||||
+28
-17
@@ -20,12 +20,12 @@ function error(msg: string) { parentPort?.postMessage({ type: 'log', level: 'err
|
||||
function debug(msg: string) { parentPort?.postMessage({ type: 'log', level: 'debug', msg }) }
|
||||
|
||||
// ─── Settings ─────────────────────────────────────────────────
|
||||
function readSettings(): { mediaPath: string; completePath: string; organizeMode: string; nfoSupport: boolean; usePlexTitles: boolean } {
|
||||
function readSettings(): { mediaPath: string; completePath: string; organizeMode: string; nfoSupport: boolean } {
|
||||
try {
|
||||
const p = path.join(DATA_DIR, 'settings.json')
|
||||
if (!fs.existsSync(p)) return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false, usePlexTitles: false }
|
||||
if (!fs.existsSync(p)) return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false }
|
||||
return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch { return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false, usePlexTitles: false } }
|
||||
} catch { return { mediaPath: '', completePath: '', organizeMode: 'hardlink', nfoSupport: false } }
|
||||
}
|
||||
|
||||
// ─── Organized log ────────────────────────────────────────────
|
||||
@@ -49,26 +49,42 @@ function isOrganized(hash: string, filename: string): boolean {
|
||||
|
||||
// ─── Lookup torrent dans les seriesData ───────────────────────
|
||||
function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; serieData: any } | null {
|
||||
const h = hash.toLowerCase()
|
||||
for (const sd of seriesData) {
|
||||
// Niveau série (pack intégrale)
|
||||
for (const t of sd.torrents ?? []) {
|
||||
if (t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
if (t.infohash?.toLowerCase() === h)
|
||||
return { torrent: t, serieData: sd }
|
||||
}
|
||||
|
||||
// Niveau saison — vérifier si le même hash apparaît dans plusieurs saisons
|
||||
// Si oui, pas de _season pour que buildFileMap scanne toutes les saisons
|
||||
const seasonsWithHash = (sd.seasons ?? []).filter((s: any) =>
|
||||
s.torrents?.some((t: any) => t.infohash?.toLowerCase() === h)
|
||||
)
|
||||
if (seasonsWithHash.length === 1) {
|
||||
// Torrent propre à une seule saison → on filtre sur cette saison
|
||||
const season = seasonsWithHash[0]
|
||||
const t = season.torrents.find((t: any) => t.infohash?.toLowerCase() === h)
|
||||
return { torrent: { ...t, _season: season }, serieData: sd }
|
||||
} else if (seasonsWithHash.length > 1) {
|
||||
// Même torrent dans plusieurs saisons → pas de filtre saison
|
||||
const t = seasonsWithHash[0].torrents.find((t: any) => t.infohash?.toLowerCase() === h)
|
||||
return { torrent: t, serieData: sd }
|
||||
}
|
||||
|
||||
// Niveau épisode
|
||||
for (const season of sd.seasons ?? []) {
|
||||
for (const t of season.torrents ?? []) {
|
||||
if (t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
return { torrent: { ...t, _season: season }, serieData: sd }
|
||||
}
|
||||
const matchingEpisodes = season.episodes?.filter((ep: any) =>
|
||||
ep.torrents?.some((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
ep.torrents?.some((t: any) => t.infohash?.toLowerCase() === h)
|
||||
) ?? []
|
||||
|
||||
if (matchingEpisodes.length === 1) {
|
||||
const ep = matchingEpisodes[0]
|
||||
const t = ep.torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
const t = ep.torrents.find((t: any) => t.infohash?.toLowerCase() === h)
|
||||
return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd }
|
||||
} else if (matchingEpisodes.length > 1) {
|
||||
const t = matchingEpisodes[0].torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
const t = matchingEpisodes[0].torrents.find((t: any) => t.infohash?.toLowerCase() === h)
|
||||
return { torrent: { ...t, _season: season }, serieData: sd }
|
||||
}
|
||||
}
|
||||
@@ -255,13 +271,8 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
|
||||
}
|
||||
|
||||
const { torrent, serieData } = found
|
||||
const { usePlexTitles } = readSettings()
|
||||
const rawTitle = serieData.title ?? serieData.show_title ?? name
|
||||
const serieTitle = sanitizeDirName(
|
||||
usePlexTitles && serieData.title_for_plex
|
||||
? serieData.title_for_plex
|
||||
: rawTitle
|
||||
)
|
||||
const serieTitle = sanitizeDirName(rawTitle)
|
||||
|
||||
debug(`Série identifiée : "${serieTitle}" (mode: ${organizeMode}${nfoSupport ? ', NFO' : ''})`)
|
||||
|
||||
|
||||
+12
-14
@@ -5,23 +5,21 @@ import { DATA_DIR } from './config.js'
|
||||
const DATA_PATH = path.join(DATA_DIR, 'settings.json')
|
||||
|
||||
export interface Settings {
|
||||
mediaPath : string
|
||||
completePath : string
|
||||
organizeMode : 'hardlink' | 'move'
|
||||
category : string
|
||||
nfoSupport : boolean
|
||||
autoImport : boolean
|
||||
usePlexTitles : boolean // utilise title_for_plex pour les noms de dossiers
|
||||
mediaPath : string
|
||||
completePath : string
|
||||
organizeMode : 'hardlink' | 'move'
|
||||
category : string
|
||||
nfoSupport : boolean
|
||||
autoImport : boolean
|
||||
}
|
||||
|
||||
const defaults: Settings = {
|
||||
mediaPath : '',
|
||||
completePath : '',
|
||||
organizeMode : 'hardlink',
|
||||
category : 'fankai',
|
||||
nfoSupport : false,
|
||||
autoImport : true,
|
||||
usePlexTitles: false,
|
||||
mediaPath : '',
|
||||
completePath: '',
|
||||
organizeMode: 'hardlink',
|
||||
category : 'fankai',
|
||||
nfoSupport : false,
|
||||
autoImport : true,
|
||||
}
|
||||
|
||||
export function readSettings(): Settings {
|
||||
|
||||
@@ -72,12 +72,6 @@
|
||||
description="Importer automatiquement dès qu'un téléchargement est terminé. Vérifie toutes les 5 minutes."
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
v-model="form.usePlexTitles"
|
||||
label="Titres optimisés Plex"
|
||||
description="Utilise title_for_plex pour les noms de dossiers — évite les caractères spéciaux."
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
v-model="form.nfoSupport"
|
||||
label="NFO / Métadonnées"
|
||||
@@ -133,7 +127,6 @@ const form = ref({
|
||||
organizeMode : 'hardlink' as 'hardlink' | 'move',
|
||||
nfoSupport : false,
|
||||
autoImport : true,
|
||||
usePlexTitles: false,
|
||||
})
|
||||
|
||||
const picker = ref<{ open: boolean; field: 'completePath' | 'mediaPath'; currentPath: string }>({
|
||||
|
||||
Reference in new issue
Block a user