Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d49125e09 | ||
|
|
721047c95f | ||
|
|
74662de2b6 | ||
|
|
84ef0333f3 | ||
|
|
c6624843b9 | ||
|
|
8bd30913cd | ||
|
|
d7eb932ed3 | ||
|
|
4594dc5189 | ||
|
|
894a621e85 | ||
|
|
23dbf86e70 | ||
|
|
d86138ef1c | ||
|
|
4aa6f99676 | ||
|
|
10432e0f99 | ||
|
|
4f69f8bf92 | ||
|
|
1511dd3ca3 | ||
|
|
23967a81d6 |
No files matched your search
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [2.2.0](https://github.com/Masutayunikon/FanKarr/compare/v2.1.3...v2.2.0) (2026-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add organize all button ([bbaccfd](https://github.com/Masutayunikon/FanKarr/commit/bbaccfd97b51724932f2ebf22cad81be1cde61c0))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* organizer reuse the wrong name for gitlab path ([bbaccfd](https://github.com/Masutayunikon/FanKarr/commit/bbaccfd97b51724932f2ebf22cad81be1cde61c0))
|
||||||
|
|
||||||
|
## [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)
|
## [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]
|
// Construit resolved_episodes pour un torrent à partir des paths[torrentIdx]
|
||||||
// paths[i] = chemin du fichier dans le torrent i → on extrait le filename
|
// Construit les épisodes résolus pour un torrent donné par son infohash
|
||||||
function buildResolvedEpisodes(sd: any, torrentIdx: number, seasonFilter?: number): any[] {
|
function buildResolvedEpisodes(sd: any, hash: string, seasonFilter?: number): any[] {
|
||||||
const resolved: any[] = []
|
const resolved: any[] = []
|
||||||
|
const h = hash.toLowerCase()
|
||||||
|
|
||||||
for (const season of sd.seasons ?? []) {
|
for (const season of sd.seasons ?? []) {
|
||||||
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
||||||
for (const ep of season.episodes ?? []) {
|
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
|
if (!filePath) continue
|
||||||
|
|
||||||
const filename = filePath.split('/').pop() ?? filePath
|
const filename = filePath.split('/').pop() ?? filePath
|
||||||
resolved.push({
|
resolved.push({
|
||||||
episode_id : ep.id,
|
episode_id : ep.id,
|
||||||
episode_number: ep.episode_number,
|
episode_number : ep.episode_number,
|
||||||
season_number : season.season_number,
|
season_number : season.season_number,
|
||||||
season_id : season.id,
|
season_id : season.id,
|
||||||
filename,
|
filename,
|
||||||
original_filename: filename, // sera enrichi depuis l'API si besoin
|
original_filename: filename,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +345,7 @@ function computeSerieDownloadState(
|
|||||||
if (!hash) continue
|
if (!hash) continue
|
||||||
const orgFiles = organized[hash] ?? {}
|
const orgFiles = organized[hash] ?? {}
|
||||||
if (Object.keys(orgFiles).length === 0) continue
|
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) {
|
for (const ep of resolved) {
|
||||||
if (orgFiles[ep.filename]) organizedEpisodeIds.add(ep.episode_id)
|
if (orgFiles[ep.filename]) organizedEpisodeIds.add(ep.episode_id)
|
||||||
}
|
}
|
||||||
@@ -433,15 +447,13 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
if (serieData) {
|
if (serieData) {
|
||||||
// ── Torrents niveau série (pack_integrale) ──────────────────
|
// ── 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 }
|
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, t.infohash)
|
||||||
const resolved = buildResolvedEpisodes(serieData, i)
|
|
||||||
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
|
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
|
||||||
|
|
||||||
// Vérifier organisés
|
|
||||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||||
for (const ep of resolved) {
|
for (const ep of resolved) {
|
||||||
if (orgFiles[ep.filename]) organizedEpisodeIds.add(ep.episode_id)
|
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) ────────────────────
|
// ── Torrents niveau saison (pack_saison) ────────────────────
|
||||||
for (const season of serieData.seasons ?? []) {
|
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 }
|
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
|
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)
|
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
|
||||||
|
|
||||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||||
@@ -465,13 +477,15 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
// ── Torrents niveau épisode ─────────────────────────────
|
// ── Torrents niveau épisode ─────────────────────────────
|
||||||
for (const ep of season.episodes ?? []) {
|
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 }
|
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: 'episode', raw: t.title, manual: t.manual ?? false }
|
||||||
episodeTorrentMap[ep.id] = ref
|
episodeTorrentMap[ep.id] = ref
|
||||||
availableEpisodeIds.add(ep.id)
|
availableEpisodeIds.add(ep.id)
|
||||||
|
|
||||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
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)
|
if (filename && orgFiles[filename]) organizedEpisodeIds.add(ep.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,7 +545,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
// ── Download ───────────────────────────────────────────────────
|
// ── Download ───────────────────────────────────────────────────
|
||||||
app.post('/api/download', requireAuth, async (req, res) => {
|
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 }
|
if (!url) { res.status(400).json({ error: 'torrent_url ou magnet requis' }); return }
|
||||||
const results = await dispatchDownload(url)
|
const results = await dispatchDownload(url)
|
||||||
res.status(results.every((r: any) => r.ok) ? 200 : 207).json({ results })
|
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 allT = extractTorrentsFromSerieData(sd)
|
||||||
const match = allT.find((st: any) => st.infohash?.toLowerCase() === t.hash?.toLowerCase())
|
const match = allT.find((st: any) => st.infohash?.toLowerCase() === t.hash?.toLowerCase())
|
||||||
if (match) {
|
if (match) {
|
||||||
const resolved = buildResolvedEpisodes(sd, match._torrentIdx, match.season_number)
|
const resolved = buildResolvedEpisodes(sd, match.infohash, match.season_number)
|
||||||
totalFiles = resolved.length || 1
|
totalFiles = resolved.length || 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-18
@@ -67,12 +67,20 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser
|
|||||||
if (t.infohash?.toLowerCase() === hash.toLowerCase())
|
if (t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||||
return { torrent: { ...t, _season: season }, serieData: sd }
|
return { torrent: { ...t, _season: season }, serieData: sd }
|
||||||
}
|
}
|
||||||
// Niveau épisode
|
// Niveau épisode — compter combien d'épisodes partagent ce torrent
|
||||||
for (const ep of season.episodes ?? []) {
|
const matchingEpisodes = season.episodes?.filter((ep: any) =>
|
||||||
for (const t of ep.torrents ?? []) {
|
ep.torrents?.some((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||||
if (t.infohash?.toLowerCase() === hash.toLowerCase())
|
) ?? []
|
||||||
return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd }
|
|
||||||
}
|
if (matchingEpisodes.length === 1) {
|
||||||
|
// Torrent propre à un seul épisode → cas épisode unique
|
||||||
|
const ep = matchingEpisodes[0]
|
||||||
|
const t = ep.torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||||
|
return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd }
|
||||||
|
} else if (matchingEpisodes.length > 1) {
|
||||||
|
// Même torrent partagé entre plusieurs épisodes → traiter comme pack_saison
|
||||||
|
const t = matchingEpisodes[0].torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase())
|
||||||
|
return { torrent: { ...t, _season: season }, serieData: sd }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +88,7 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construit la map filename → { season_number, original_filename, fullPath }
|
// 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(
|
function buildFileMap(
|
||||||
serieData : any,
|
serieData : any,
|
||||||
hash : string,
|
hash : string,
|
||||||
@@ -92,14 +100,25 @@ function buildFileMap(
|
|||||||
for (const season of serieData.seasons ?? []) {
|
for (const season of serieData.seasons ?? []) {
|
||||||
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
||||||
for (const ep of season.episodes ?? []) {
|
for (const ep of season.episodes ?? []) {
|
||||||
const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === h)
|
const paths: any[] = ep.paths ?? []
|
||||||
if (!match) continue
|
let matchedPath: string | null = null
|
||||||
const filename = match.path.split('/').pop() ?? match.path
|
|
||||||
|
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
|
const original = ep.original_filename ?? filename
|
||||||
map.set(filename, {
|
map.set(filename, {
|
||||||
season_number : season.season_number,
|
season_number : season.season_number,
|
||||||
original_filename: original,
|
original_filename: original,
|
||||||
fullPath : match.path,
|
fullPath : matchedPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +143,23 @@ function isInExcludedFolder(filePath: string[]): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Map correction titres API → titres GitLab ───────────────
|
||||||
|
const SERIE_TITLE_GITLAB_MAP: Record<string, string> = {
|
||||||
|
'Enfer Et Paradis Henshū' : 'Enfer et Paradis Henshū',
|
||||||
|
'Hajime No Ippo Henshū' : 'Hajime no Ippo Henshū',
|
||||||
|
'Hikaru No Go Henshū' : 'Hikaru no Go Henshū',
|
||||||
|
'Hokuto No Ken Kaï' : 'Hokuto no Ken Kaï',
|
||||||
|
'Kaguya-sama : Love is War Henshū' : 'Kaguya-sama - Love is War Henshū',
|
||||||
|
'Kenshin le Vagabond Henshū' : 'Kenshin le vagabond Henshū',
|
||||||
|
'Kuroko No Basket Henshū' : 'Kuroko no Basket Henshū',
|
||||||
|
'Shingeki No Kyojin Henshū' : 'Shingeki no Kyojin Henshū',
|
||||||
|
'Tower Of God Henshū' : 'Tower of God Henshū',
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGitlabSerieTitle(serieTitle: string): string {
|
||||||
|
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'
|
||||||
const GITLAB_RAW_BASE = 'https://gitlab.com/ElPouki/fankai_pack/-/raw/main/pack'
|
const GITLAB_RAW_BASE = 'https://gitlab.com/ElPouki/fankai_pack/-/raw/main/pack'
|
||||||
@@ -143,8 +179,9 @@ async function fetchBinary(url: string): Promise<Buffer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promise<void> {
|
async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promise<void> {
|
||||||
const folderPath = `pack/${serieTitle}`
|
const gitlabTitle = getGitlabSerieTitle(serieTitle)
|
||||||
log(`[nfo] Récupération dossier GitLab: ${serieTitle}`)
|
const folderPath = `pack/${gitlabTitle}`
|
||||||
|
log(`[nfo] Récupération dossier GitLab: ${gitlabTitle}`)
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
try {
|
try {
|
||||||
@@ -169,12 +206,12 @@ 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) {
|
||||||
const relativePath = entry.path.replace(`pack/${serieTitle}/`, '')
|
const relativePath = entry.path.replace(`pack/${gitlabTitle}/`, '')
|
||||||
const destPath = path.join(destRoot, relativePath)
|
const destPath = path.join(destRoot, relativePath)
|
||||||
if (fs.existsSync(destPath)) continue
|
if (fs.existsSync(destPath)) continue
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
||||||
const rawUrl = `${GITLAB_RAW_BASE}/${encodeURIComponent(serieTitle)}/${relativePath.split('/').map(encodeURIComponent).join('/')}`
|
const rawUrl = `${GITLAB_RAW_BASE}/${encodeURIComponent(gitlabTitle)}/${relativePath.split('/').map(encodeURIComponent).join('/')}`
|
||||||
const data = await fetchBinary(rawUrl)
|
const data = await fetchBinary(rawUrl)
|
||||||
fs.writeFileSync(destPath, data)
|
fs.writeFileSync(destPath, data)
|
||||||
log(`[nfo] ✓ ${relativePath}`)
|
log(`[nfo] ✓ ${relativePath}`)
|
||||||
@@ -182,7 +219,7 @@ async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promi
|
|||||||
warn(`[nfo] ✗ ${relativePath}: ${err instanceof Error ? err.message : err}`)
|
warn(`[nfo] ✗ ${relativePath}: ${err instanceof Error ? err.message : err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log(`[nfo] GitLab synchronisé pour "${serieTitle}"`)
|
log(`[nfo] GitLab synchronisé pour "${gitlabTitle}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Filesystem ops ───────────────────────────────────────────
|
// ─── Filesystem ops ───────────────────────────────────────────
|
||||||
@@ -224,8 +261,12 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
|
|||||||
if (torrent._episode) {
|
if (torrent._episode) {
|
||||||
const ep = torrent._episode
|
const ep = torrent._episode
|
||||||
const season = torrent._season
|
const season = torrent._season
|
||||||
const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === hash.toLowerCase())
|
const paths: any[] = ep.paths ?? []
|
||||||
const filePath = match?.path ?? null
|
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 filename = filePath?.split('/').pop() ?? name
|
||||||
const original = ep.original_filename ?? filename
|
const original = ep.original_filename ?? filename
|
||||||
const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1))
|
const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1))
|
||||||
|
|||||||
+18
-2
@@ -54,9 +54,13 @@ export async function scanMediaPath(
|
|||||||
for (const season of sd.seasons ?? []) {
|
for (const season of sd.seasons ?? []) {
|
||||||
for (const ep of season.episodes ?? []) {
|
for (const ep of season.episodes ?? []) {
|
||||||
for (const p of ep.paths ?? []) {
|
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()
|
const hash = p.infohash?.toLowerCase()
|
||||||
if (!hash || !p.path) continue
|
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)
|
if (filename) filenameIndex.set(filename, hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +154,19 @@ export async function autoOrganizeAll(
|
|||||||
|
|
||||||
for (const t of torrents) _prevStates.set(t.hash, t.state)
|
for (const t of torrents) _prevStates.set(t.hash, t.state)
|
||||||
|
|
||||||
if (!isFirstRun && newlySeeding.length === 0) return
|
// Charger organized.json pour vérifier s'il reste des torrents non organisés
|
||||||
|
let organized: Record<string, Record<string, string>> = {}
|
||||||
|
try {
|
||||||
|
const orgPath = path.join(DATA_DIR, 'organized.json')
|
||||||
|
if (fs.existsSync(orgPath))
|
||||||
|
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const hasUnorganized = torrents.some(t =>
|
||||||
|
t.state === 'seeding' && !organized[t.hash?.toLowerCase()]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isFirstRun && newlySeeding.length === 0 && !hasUnorganized) return
|
||||||
if (seedingCount === 0) return
|
if (seedingCount === 0) return
|
||||||
|
|
||||||
if (newlySeeding.length > 0 && !isFirstRun) {
|
if (newlySeeding.length > 0 && !isFirstRun) {
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
<h1 class="text-xl font-black text-white font-sans">Téléchargements</h1>
|
<h1 class="text-xl font-black text-white font-sans">Téléchargements</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
@click="organizeAll"
|
||||||
|
:disabled="organizingAll"
|
||||||
|
class="border border-[#1e2d3d] text-[#5a7a94] px-4 py-1.5 text-xs tracking-widest hover:border-green-500 hover:text-green-500 transition-colors cursor-pointer disabled:opacity-50">
|
||||||
|
{{ organizingAll ? '...' : 'ORGANISER TOUT' }}
|
||||||
|
</button>
|
||||||
<div class="flex items-center gap-2 text-[10px] text-[#5a7a94] tracking-widest">
|
<div class="flex items-center gap-2 text-[10px] text-[#5a7a94] tracking-widest">
|
||||||
<div class="w-1.5 h-1.5 rounded-full" :class="polling ? 'bg-green-500 animate-pulse' : 'bg-[#1e2d3d]'"/>
|
<div class="w-1.5 h-1.5 rounded-full" :class="polling ? 'bg-green-500 animate-pulse' : 'bg-[#1e2d3d]'"/>
|
||||||
{{ polling ? 'EN DIRECT' : 'PAUSE' }}
|
{{ polling ? 'EN DIRECT' : 'PAUSE' }}
|
||||||
@@ -190,6 +196,7 @@ const loading = ref(true)
|
|||||||
const polling = ref(true)
|
const polling = ref(true)
|
||||||
const noClients = ref(false)
|
const noClients = ref(false)
|
||||||
const organizing = ref<Record<string, boolean>>({})
|
const organizing = ref<Record<string, boolean>>({})
|
||||||
|
const organizingAll = ref(false)
|
||||||
const activeTab = ref<'active' | 'done'>('active')
|
const activeTab = ref<'active' | 'done'>('active')
|
||||||
const hideOrganized = ref(false)
|
const hideOrganized = ref(false)
|
||||||
|
|
||||||
@@ -251,7 +258,7 @@ function togglePolling() {
|
|||||||
onMounted(startPolling)
|
onMounted(startPolling)
|
||||||
onUnmounted(stopPolling)
|
onUnmounted(stopPolling)
|
||||||
|
|
||||||
// ── Organiser ──────────────────────────────────────────────────
|
// ── Organiser un torrent ───────────────────────────────────────
|
||||||
async function organize(torrent: any) {
|
async function organize(torrent: any) {
|
||||||
organizing.value[torrent.hash] = true
|
organizing.value[torrent.hash] = true
|
||||||
try {
|
try {
|
||||||
@@ -263,6 +270,7 @@ async function organize(torrent: any) {
|
|||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast(`${torrent.name} organisé ✓`, 'success')
|
toast(`${torrent.name} organisé ✓`, 'success')
|
||||||
|
await fetchTorrents()
|
||||||
} else {
|
} else {
|
||||||
const { error } = await res.json()
|
const { error } = await res.json()
|
||||||
toast(error ?? 'Erreur lors de l\'organisation', 'error')
|
toast(error ?? 'Erreur lors de l\'organisation', 'error')
|
||||||
@@ -274,6 +282,40 @@ async function organize(torrent: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Organiser tout ─────────────────────────────────────────────
|
||||||
|
async function organizeAll() {
|
||||||
|
const toOrganize = doneTorrents.value.filter(t => t.organizeState !== 'done')
|
||||||
|
if (toOrganize.length === 0) {
|
||||||
|
toast('Tous les torrents sont déjà organisés', 'success')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
organizingAll.value = true
|
||||||
|
let done = 0
|
||||||
|
let errors = 0
|
||||||
|
|
||||||
|
for (const t of toOrganize) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/organize', {
|
||||||
|
method : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ hash: t.hash, save_path: t.save_path, name: t.name })
|
||||||
|
})
|
||||||
|
if (res.ok) done++
|
||||||
|
else errors++
|
||||||
|
} catch {
|
||||||
|
errors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organizingAll.value = false
|
||||||
|
await fetchTorrents()
|
||||||
|
|
||||||
|
if (errors === 0) toast(`${done} torrent(s) organisé(s) ✓`, 'success')
|
||||||
|
else toast(`${done} OK, ${errors} erreur(s)`, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers affichage ──────────────────────────────────────────
|
// ── Helpers affichage ──────────────────────────────────────────
|
||||||
function stateBadge(state: string) {
|
function stateBadge(state: string) {
|
||||||
const map: Record<string, { label: string; class: string }> = {
|
const map: Record<string, { label: string; class: string }> = {
|
||||||
|
|||||||
Reference in new issue
Block a user