Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6624843b9 | ||
|
|
8bd30913cd | ||
|
|
d7eb932ed3 | ||
|
|
4594dc5189 | ||
|
|
894a621e85 | ||
|
|
23dbf86e70 | ||
|
|
d86138ef1c | ||
|
|
4aa6f99676 | ||
|
|
10432e0f99 | ||
|
|
4f69f8bf92 | ||
|
|
1511dd3ca3 | ||
|
|
23967a81d6 |
No files matched your search
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [2.1.3](https://github.com/Masutayunikon/FanKarr/compare/v2.1.2...v2.1.3) (2026-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use magnet before torrent ([ed4665a](https://github.com/Masutayunikon/FanKarr/commit/ed4665a378d15c0b45a2be32d612e41082a43df1))
|
||||
|
||||
## [2.1.2](https://github.com/Masutayunikon/FanKarr/compare/v2.1.1...v2.1.2) (2026-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* path now work on every device ([cfc3440](https://github.com/Masutayunikon/FanKarr/commit/cfc3440a95cb73802778e01d5f256f473e0cc952))
|
||||
|
||||
## [2.1.1](https://github.com/Masutayunikon/FanKarr/compare/v2.1.0...v2.1.1) (2026-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* torrent path to hash ([d9e453b](https://github.com/Masutayunikon/FanKarr/commit/d9e453b486b9fd85b69ee134bee2bd6a1a6d3702))
|
||||
|
||||
## [2.1.0](https://github.com/Masutayunikon/FanKarr/compare/v2.0.0...v2.1.0) (2026-03-23)
|
||||
|
||||
|
||||
|
||||
+34
-20
@@ -192,22 +192,36 @@ function extractTorrentsFromSerieData(sd: any): any[] {
|
||||
}
|
||||
|
||||
// Construit resolved_episodes pour un torrent à partir des paths[torrentIdx]
|
||||
// paths[i] = chemin du fichier dans le torrent i → on extrait le filename
|
||||
function buildResolvedEpisodes(sd: any, torrentIdx: number, seasonFilter?: number): any[] {
|
||||
// Construit les épisodes résolus pour un torrent donné par son infohash
|
||||
function buildResolvedEpisodes(sd: any, hash: string, seasonFilter?: number): any[] {
|
||||
const resolved: any[] = []
|
||||
const h = hash.toLowerCase()
|
||||
|
||||
for (const season of sd.seasons ?? []) {
|
||||
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
||||
for (const ep of season.episodes ?? []) {
|
||||
const filePath = (ep.paths ?? [])[torrentIdx]
|
||||
const paths: any[] = ep.paths ?? []
|
||||
|
||||
// Nouveau format { infohash, path } ou legacy string
|
||||
let filePath: string | null = null
|
||||
for (const p of paths) {
|
||||
if (typeof p === 'string') {
|
||||
if (!filePath) filePath = p.replace(/\\/g, '/') // legacy fallback
|
||||
} else if (p?.infohash?.toLowerCase() === h) {
|
||||
filePath = p.path.replace(/\\/g, '/')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!filePath) continue
|
||||
|
||||
const filename = filePath.split('/').pop() ?? filePath
|
||||
resolved.push({
|
||||
episode_id : ep.id,
|
||||
episode_number: ep.episode_number,
|
||||
season_number : season.season_number,
|
||||
season_id : season.id,
|
||||
episode_id : ep.id,
|
||||
episode_number : ep.episode_number,
|
||||
season_number : season.season_number,
|
||||
season_id : season.id,
|
||||
filename,
|
||||
original_filename: filename, // sera enrichi depuis l'API si besoin
|
||||
original_filename: filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -331,7 +345,7 @@ function computeSerieDownloadState(
|
||||
if (!hash) continue
|
||||
const orgFiles = organized[hash] ?? {}
|
||||
if (Object.keys(orgFiles).length === 0) continue
|
||||
const resolved = buildResolvedEpisodes(serieData, t._torrentIdx, t.season_number)
|
||||
const resolved = buildResolvedEpisodes(serieData, hash, t.season_number)
|
||||
for (const ep of resolved) {
|
||||
if (orgFiles[ep.filename]) organizedEpisodeIds.add(ep.episode_id)
|
||||
}
|
||||
@@ -433,15 +447,13 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
||||
|
||||
if (serieData) {
|
||||
// ── Torrents niveau série (pack_integrale) ──────────────────
|
||||
for (const [i, t] of (serieData.torrents ?? []).entries()) {
|
||||
for (const t of (serieData.torrents ?? [])) {
|
||||
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: 'pack_integrale', raw: t.title, manual: t.manual ?? false }
|
||||
integraleTorrents.push({ ...t, raw: t.title, _torrentIdx: i })
|
||||
integraleTorrents.push({ ...t, raw: t.title })
|
||||
|
||||
// Marquer les épisodes disponibles via paths[i]
|
||||
const resolved = buildResolvedEpisodes(serieData, i)
|
||||
const resolved = buildResolvedEpisodes(serieData, t.infohash)
|
||||
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
|
||||
|
||||
// Vérifier organisés
|
||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||
for (const ep of resolved) {
|
||||
if (orgFiles[ep.filename]) organizedEpisodeIds.add(ep.episode_id)
|
||||
@@ -450,11 +462,11 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
||||
|
||||
// ── Torrents niveau saison (pack_saison) ────────────────────
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
for (const [i, t] of (season.torrents ?? []).entries()) {
|
||||
for (const t of (season.torrents ?? [])) {
|
||||
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: 'pack_saison', raw: t.title, manual: t.manual ?? false }
|
||||
seasonTorrentMapBySn[season.season_number] = ref
|
||||
|
||||
const resolved = buildResolvedEpisodes(serieData, i, season.season_number)
|
||||
const resolved = buildResolvedEpisodes(serieData, t.infohash, season.season_number)
|
||||
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
|
||||
|
||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||
@@ -465,13 +477,15 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
||||
|
||||
// ── Torrents niveau épisode ─────────────────────────────
|
||||
for (const ep of season.episodes ?? []) {
|
||||
for (const [i, t] of (ep.torrents ?? []).entries()) {
|
||||
for (const t of (ep.torrents ?? [])) {
|
||||
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: 'episode', raw: t.title, manual: t.manual ?? false }
|
||||
episodeTorrentMap[ep.id] = ref
|
||||
availableEpisodeIds.add(ep.id)
|
||||
|
||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||
const filename = (ep.paths ?? [])[i]?.split('/').pop()
|
||||
const paths: any[] = ep.paths ?? []
|
||||
const match = paths.find((p: any) => typeof p === 'object' && p.infohash?.toLowerCase() === t.infohash?.toLowerCase())
|
||||
const filename = match ? match.path.split('/').pop() : null
|
||||
if (filename && orgFiles[filename]) organizedEpisodeIds.add(ep.id)
|
||||
}
|
||||
}
|
||||
@@ -531,7 +545,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
||||
|
||||
// ── Download ───────────────────────────────────────────────────
|
||||
app.post('/api/download', requireAuth, async (req, res) => {
|
||||
const url = req.body.torrent_url ?? req.body.magnet
|
||||
const url = req.body.magnet ?? req.body.torrent_url
|
||||
if (!url) { res.status(400).json({ error: 'torrent_url ou magnet requis' }); return }
|
||||
const results = await dispatchDownload(url)
|
||||
res.status(results.every((r: any) => r.ok) ? 200 : 207).json({ results })
|
||||
@@ -562,7 +576,7 @@ app.get('/api/downloads', requireAuth, async (_req, res) => {
|
||||
const allT = extractTorrentsFromSerieData(sd)
|
||||
const match = allT.find((st: any) => st.infohash?.toLowerCase() === t.hash?.toLowerCase())
|
||||
if (match) {
|
||||
const resolved = buildResolvedEpisodes(sd, match._torrentIdx, match.season_number)
|
||||
const resolved = buildResolvedEpisodes(sd, match.infohash, match.season_number)
|
||||
totalFiles = resolved.length || 1
|
||||
break
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser
|
||||
}
|
||||
|
||||
// Construit la map filename → { season_number, original_filename, fullPath }
|
||||
// en matchant par infohash dans ep.paths[]
|
||||
// Supporte les deux formats : { infohash, path } et string (legacy)
|
||||
function buildFileMap(
|
||||
serieData : any,
|
||||
hash : string,
|
||||
@@ -92,14 +92,25 @@ function buildFileMap(
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
||||
for (const ep of season.episodes ?? []) {
|
||||
const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === h)
|
||||
if (!match) continue
|
||||
const filename = match.path.split('/').pop() ?? match.path
|
||||
const paths: any[] = ep.paths ?? []
|
||||
let matchedPath: string | null = null
|
||||
|
||||
for (const p of paths) {
|
||||
if (typeof p === 'string') {
|
||||
if (!matchedPath) matchedPath = p.replace(/\\/g, '/')
|
||||
} else if (p?.infohash?.toLowerCase() === h) {
|
||||
matchedPath = p.path.replace(/\\/g, '/')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedPath) continue
|
||||
const filename = matchedPath.split('/').pop() ?? matchedPath
|
||||
const original = ep.original_filename ?? filename
|
||||
map.set(filename, {
|
||||
season_number : season.season_number,
|
||||
original_filename: original,
|
||||
fullPath : match.path,
|
||||
fullPath : matchedPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -224,8 +235,12 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
|
||||
if (torrent._episode) {
|
||||
const ep = torrent._episode
|
||||
const season = torrent._season
|
||||
const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === hash.toLowerCase())
|
||||
const filePath = match?.path ?? null
|
||||
const paths: any[] = ep.paths ?? []
|
||||
let filePath: string | null = null
|
||||
for (const p of paths) {
|
||||
if (typeof p === 'string') { if (!filePath) filePath = p.replace(/\\/g, '/') }
|
||||
else if (p?.infohash?.toLowerCase() === hash.toLowerCase()) { filePath = p.path.replace(/\\/g, '/'); break }
|
||||
}
|
||||
const filename = filePath?.split('/').pop() ?? name
|
||||
const original = ep.original_filename ?? filename
|
||||
const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1))
|
||||
|
||||
+5
-1
@@ -54,9 +54,13 @@ export async function scanMediaPath(
|
||||
for (const season of sd.seasons ?? []) {
|
||||
for (const ep of season.episodes ?? []) {
|
||||
for (const p of ep.paths ?? []) {
|
||||
if (typeof p === 'string') {
|
||||
// Legacy : pas d'infohash disponible, on skip pour le scan
|
||||
continue
|
||||
}
|
||||
const hash = p.infohash?.toLowerCase()
|
||||
if (!hash || !p.path) continue
|
||||
const filename = p.path.split('/').pop()
|
||||
const filename = p.path.replace(/\\/g, '/').split('/').pop()
|
||||
if (filename) filenameIndex.set(filename, hash)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user