Compare commits

...
32 Commits
Author SHA1 Message Date
Quentin Robert c87b7a4c7f Merge pull request #136 from Masutayunikon/release-please--branches--main
chore(main): release 3.20.3
2026-06-26 15:24:47 +02:00
Quentin Robert a000e349c9 chore(main): release 3.20.3 2026-06-26 15:24:30 +02:00
MasutaYunikon f23e3c04ec Merge remote-tracking branch 'origin/main' 2026-06-26 15:23:45 +02:00
MasutaYunikon 63146fdc27 fix(serie): ajout du label de saisons avec les episodes 2026-06-26 15:21:55 +02:00
github-actions[bot] 7795598346 chore: update version.txt to v3.20.2 2026-06-26 12:52:07 +00:00
Quentin Robert 04252db0c4 Merge pull request #135 from Masutayunikon/release-please--branches--main
chore(main): release 3.20.2
2026-06-26 14:51:44 +02:00
Quentin Robert aeb2e938ef chore(main): release 3.20.2 2026-06-26 14:50:31 +02:00
MasutaYunikon 3799d33c62 fix(scraper): fallback on empty env for GITHUB_BASE
fix(client): add placeholder from config
fix(client): remove alldebrid
2026-06-26 14:49:53 +02:00
MasutaYunikon 98b317d493 fix(label): add season of episodes 2026-06-26 14:25:41 +02:00
Quentin Robert 4292f96645 Merge pull request #131 from Nackophilz/main
Replace favicon and add PNG logo
2026-06-09 21:44:35 +02:00
ElPouki 182c7cc807 add png logo 2026-06-09 21:42:14 +02:00
ElPouki 53be057425 replace favicon 2026-06-09 21:41:51 +02:00
github-actions[bot] ac70dba927 chore: update version.txt to v3.20.1 2026-06-08 00:23:47 +00:00
Quentin Robert f79cc244ef Merge pull request #130 from Masutayunikon/release-please--branches--main
chore(main): release 3.20.1
2026-06-08 02:23:21 +02:00
Quentin Robert 14137e1b1b chore(main): release 3.20.1 2026-06-08 02:22:48 +02:00
MasutaYunikon 43ee4597a9 Merge remote-tracking branch 'origin/main' 2026-06-08 02:16:17 +02:00
MasutaYunikon f311c8e2f3 fix(organize): import manuel ne traite que les fichiers 100% complets
L'import manuel créait un fakeTorrent sans champ files, ce qui désactivait
le filtre de complétion par fichier dans le worker. Sur Windows, tenter de
déplacer un fichier encore écrit par qBittorrent provoque EBUSY ; les fichiers
échouaient mais étaient quand même marqués importés en mémoire.

Fix :
- routes/organize.ts : appel dispatchGetFiles(hash) avant l'import pour
  récupérer la progression réelle par fichier depuis le client torrent
- organize.ts : organizeTorrent accepte un param optionnel files[] transmis
  au worker via fakeTorrent ; sans fichiers (client injoignable), le comportement
  précédent (pas de filtre) est conservé en fallback
2026-06-08 02:12:47 +02:00
github-actions[bot] 48933b4078 chore: update version.txt to v3.20.0 2026-05-29 01:12:44 +00:00
Quentin Robert 45288a130f Merge pull request #129 from Masutayunikon/release-please--branches--main
chore(main): release 3.20.0
2026-05-29 03:12:20 +02:00
Quentin Robert 73256eb15d chore(main): release 3.20.0 2026-05-29 03:05:15 +02:00
MasutaYunikon 6855c04fab Merge remote-tracking branch 'origin/main'
# Conflicts:
#	CHANGELOG.md
#	version.txt
2026-05-29 03:04:39 +02:00
MasutaYunikon f6f735ad88 fix(organize): fallback par torrent_name quand le hash n'est pas résolu
Quand un client torrent (ex: Synology avec dbid_X) ne fournit pas de vrai
infohash BT, la recherche dans le catalogue série échouait silencieusement.

Ajout de findTorrentByName() qui cherche par torrent_name et title dans les
données série. Le hash effectif (vrai infohash BT) est utilisé tout au long
du pipeline d'import (buildFileMap, organized[], organizeTorrent, message).

Fix: l'import manuel de torrents Synology dont le hash est un dbid_X natif
(ex: "[Shalouf] Mob Psycho 100 Henshū (Fan-Kaï)" → dbid_79) échouait avec
"non trouvé dans les données série" alors que le torrent_name correspondait.
2026-05-29 03:03:47 +02:00
MasutaYunikon a7250a5f80 feat(torrent): système générique de suivi des tâches gérées
- index: ajout de managed_tasks.json (clientUuid → [infohash])
  · trackManagedHash() appelé dans dispatchDownload après add() réussi
  · untrackManagedHash() appelé dans dispatchRemove après remove() réussi
  · clearManagedHashes() appelé dans removeClient
  · dispatchList filtre par les hashes suivis pour les drivers avec
    filterByManagedHashes: true (clients sans catégories natives)
- TorrentClientDefinition: nouveau champ filterByManagedHashes (optionnel)
- synology-ds: filterByManagedHashes: true — la liste ne retourne que
  les torrents ajoutés via FanKarr
- synology-ds add(): snapshot avant/après création pour capturer le dbid_X
  et peupler _hashToSynoId immédiatement (sans attendre le prochain list)
2026-05-29 03:03:06 +02:00
MasutaYunikon d326e08834 fix(synology): filtrage par savePath et retour au GET pour add()
- list(): filtre les tâches par detail.destination === savePath (Synology
  n'ayant pas de labels/catégories, c'est le seul moyen de n'afficher que
  les torrents FanKarr) ; sans savePath configuré, tous les torrents sont
  retournés comme avant
- add(): retour à la requête GET (comme l'ancien driver) — le POST avec
  destination dans le body ne fonctionnait pas correctement sur certains
  DSM, alors que les paramètres en query string fonctionnaient
2026-05-29 03:03:05 +02:00
github-actions[bot] 0e2d513cf7 chore: update version.txt to v3.19.0 2026-05-29 03:03:05 +02:00
Quentin Robert 5cf530ba78 chore(main): release 3.19.0 2026-05-29 03:03:05 +02:00
github-actions[bot] 3bc2e08ca5 chore: update version.txt to v3.19.1 2026-05-28 13:42:18 +00:00
Quentin Robert 38996b4095 Merge pull request #128 from Masutayunikon/release-please--branches--main
chore(main): release 3.19.1
2026-05-28 15:41:40 +02:00
Quentin Robert 4063a1b8ae chore(main): release 3.19.1 2026-05-28 15:40:43 +02:00
MasutaYunikon e9e0b487f4 Merge branch 'main' of github.com:Masutayunikon/FanKarr 2026-05-28 15:40:01 +02:00
MasutaYunikon 099b0889b0 fix(ci): rerun deployment 2026-05-28 15:39:53 +02:00
github-actions[bot] 33be4ce687 chore: update version.txt to v3.19.0 2026-05-28 13:38:50 +00:00
12 changed files with 289 additions and 56 deletions

No files matched your search

+44
View File
@@ -1,5 +1,49 @@
# Changelog
## [3.20.3](https://github.com/Masutayunikon/FanKarr/compare/v3.20.2...v3.20.3) (2026-06-26)
### Bug Fixes
* **serie:** ajout du label de saisons avec les episodes ([63146fd](https://github.com/Masutayunikon/FanKarr/commit/63146fdc27d98acb70cf86c7890ea1236388d717))
## [3.20.2](https://github.com/Masutayunikon/FanKarr/compare/v3.20.1...v3.20.2) (2026-06-26)
### Bug Fixes
* **client:** add placeholder from config ([3799d33](https://github.com/Masutayunikon/FanKarr/commit/3799d33c625fe28eab4b46917ff32b1266cda1e3))
* **client:** remove alldebrid ([3799d33](https://github.com/Masutayunikon/FanKarr/commit/3799d33c625fe28eab4b46917ff32b1266cda1e3))
* **label:** add season of episodes ([98b317d](https://github.com/Masutayunikon/FanKarr/commit/98b317d493b7742c330d49ca816c0fecd983e82a))
* **scraper:** fallback on empty env for GITHUB_BASE ([3799d33](https://github.com/Masutayunikon/FanKarr/commit/3799d33c625fe28eab4b46917ff32b1266cda1e3))
## [3.20.1](https://github.com/Masutayunikon/FanKarr/compare/v3.20.0...v3.20.1) (2026-06-08)
### Bug Fixes
* **organize:** import manuel ne traite que les fichiers 100% complets ([f311c8e](https://github.com/Masutayunikon/FanKarr/commit/f311c8e2f3b9f53b6d03a327cf1b5bfd69cc6424))
## [3.20.0](https://github.com/Masutayunikon/FanKarr/compare/v3.19.1...v3.20.0) (2026-05-29)
### Features
* **torrent:** système générique de suivi des tâches gérées ([a7250a5](https://github.com/Masutayunikon/FanKarr/commit/a7250a5f804189a7ddecef44193436cf6b324c31))
### Bug Fixes
* **organize:** fallback par torrent_name quand le hash n'est pas résolu ([f6f735a](https://github.com/Masutayunikon/FanKarr/commit/f6f735ad8838d0755472f739571684d75f9dc1b5))
* **synology:** filtrage par savePath et retour au GET pour add() ([d326e08](https://github.com/Masutayunikon/FanKarr/commit/d326e0883477a7dbf453ab28a3fc619f759a8604))
## [3.19.1](https://github.com/Masutayunikon/FanKarr/compare/v3.19.0...v3.19.1) (2026-05-28)
### Bug Fixes
* **ci:** rerun deployment ([099b088](https://github.com/Masutayunikon/FanKarr/commit/099b0889b069f0226b7cb2ec4fa3b1ca535a82a6))
## [3.19.0](https://github.com/Masutayunikon/FanKarr/compare/v3.18.3...v3.19.0) (2026-05-28)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 231 KiB

+3 -2
View File
@@ -2,8 +2,9 @@ import { DATA_DIR } from '../config.js'
import path from 'path'
import fs from 'fs'
const GITHUB_BASE = process.env.GITHUB_BASE
?? 'https://raw.githubusercontent.com/masutayunikon/fankarr-scraper/main'
const GITHUB_BASE =
process.env.GITHUB_BASE?.trim() ||
'https://raw.githubusercontent.com/masutayunikon/fankarr-scraper/main'
const CACHE_TTL_MS = 1 * 60 * 60 * 1000
+71 -5
View File
@@ -63,6 +63,10 @@ function isOrganized(hash: string, episodeId: number): boolean {
}
// ─── Lookup torrent dans les seriesData ───────────────────────
/**
* Recherche par infohash (méthode principale).
*/
function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; serieData: any } | null {
const h = hash.toLowerCase()
for (const sd of seriesData) {
@@ -101,6 +105,53 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser
return null
}
/**
* Fallback quand le hash n'est pas résolu (ex: dbid_X Synology).
* Cherche par torrent_name (nom réel du dossier torrent) ou title dans le catalogue.
* Retourne aussi le vrai infohash BT pour que l'import soit correctement indexé.
*/
function findTorrentByName(
name : string,
seriesData: any[],
): { torrent: any; serieData: any; infohash: string } | null {
const n = name.toLowerCase().trim()
if (!n) return null
for (const sd of seriesData) {
// Pack intégral (torrents au niveau série)
for (const t of sd.torrents ?? []) {
if (!t.infohash) continue
if (
t.torrent_name?.toLowerCase().trim() === n ||
t.title?.toLowerCase().trim() === n
) return { torrent: t, serieData: sd, infohash: t.infohash.toLowerCase() }
}
for (const season of sd.seasons ?? []) {
// Pack saison
for (const t of season.torrents ?? []) {
if (!t.infohash) continue
if (
t.torrent_name?.toLowerCase().trim() === n ||
t.title?.toLowerCase().trim() === n
) return { torrent: { ...t, _season: season }, serieData: sd, infohash: t.infohash.toLowerCase() }
}
// Torrent épisode unique
for (const ep of season.episodes ?? []) {
for (const t of ep.torrents ?? []) {
if (!t.infohash) continue
if (
t.torrent_name?.toLowerCase().trim() === n ||
t.title?.toLowerCase().trim() === n
) return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd, infohash: t.infohash.toLowerCase() }
}
}
}
}
return null
}
function buildFileMap(
serieData : any,
hash : string,
@@ -504,7 +555,22 @@ parentPort?.on('message', async (msg: any) => {
for (const t of torrents) {
if (t.state !== 'seeding') continue
const found = findTorrentByHash(t.hash, seriesData)
// Résolution principale : par infohash
let found = findTorrentByHash(t.hash, seriesData)
let effectiveHash = t.hash
// Fallback : si le hash n'est pas résolu (ex: dbid_X Synology), on cherche
// par torrent_name — le nom du dossier racine tel que rapporté par le client,
// qui correspond au champ torrent_name dans les données série.
if (!found && t.name) {
const byName = findTorrentByName(t.name, seriesData)
if (byName) {
found = byName
effectiveHash = byName.infohash
debug(`Hash résolu par torrent_name : "${t.name}" → ${effectiveHash.slice(0, 8)}…`)
}
}
if (!found) {
debug(`Torrent "${t.name}" (${t.hash.slice(0, 8)}…) non trouvé dans les données série — catalogue peut-être pas à jour`)
continue
@@ -512,8 +578,8 @@ parentPort?.on('message', async (msg: any) => {
const { torrent, serieData } = found
const seasonFilter = torrent._season?.season_number
const fileMap = buildFileMap(serieData, t.hash, nfoSupport, seasonFilter)
const orgHash = organized[t.hash] ?? {}
const fileMap = buildFileMap(serieData, effectiveHash, nfoSupport, seasonFilter)
const orgHash = organized[effectiveHash] ?? {}
const allDone = fileMap.size > 0
? [...fileMap.values()].every(f => orgHash[String(f.episode_id)])
: Object.keys(orgHash).length > 0
@@ -546,8 +612,8 @@ parentPort?.on('message', async (msg: any) => {
}
}
const result = await organizeTorrent(t.hash, t.name, t.save_path, seriesData, completedFileNames)
parentPort?.postMessage({ type: 'result', hash: t.hash, name: t.name, serieId: serieData.id ?? null, ...result })
const result = await organizeTorrent(effectiveHash, t.name, t.save_path, seriesData, completedFileNames)
parentPort?.postMessage({ type: 'result', hash: effectiveHash, name: t.name, serieId: serieData.id ?? null, ...result })
} catch (err) {
error(`Erreur lors de l'import de "${t.name}" : ${err instanceof Error ? err.message : err}`)
}
+6 -2
View File
@@ -520,13 +520,17 @@ export async function organizeTorrent(
hash : string,
name : string,
savePath : string,
seriesData: any[]
seriesData: any[],
files? : { name?: string; progress: number; priority?: number }[],
): Promise<OrganizeResult> {
logger.info('organize', `Import manuel lancé pour "${name}" (${hash})`)
return new Promise((resolve, reject) => {
const worker = new Worker(resolveWorkerPath())
const fakeTorrent = { hash, name, save_path: savePath, state: 'seeding' }
// files : progression par fichier récupérée depuis le client torrent.
// Permet au worker de ne traiter que les fichiers à 100% et d'éviter
// les EBUSY sur Windows quand qBit est encore en train d'écrire les autres.
const fakeTorrent = { hash, name, save_path: savePath, state: 'seeding', files: files ?? [] }
const result: OrganizeResult = { total: 0, skipped: 0, done: 0, errors: [] }
worker.on('message', (msg: any) => {
+8 -1
View File
@@ -4,6 +4,7 @@ import { logger } from '../logger.js'
import { organizeTorrent, migrateOrganizedEpisodeIds } from '../organize.js'
import { loadEnrichedSeriesData } from '../lib/github-cache.js'
import { recentOrganized, pushNotif } from '../lib/notifs.js'
import { dispatchGetFiles } from '../torrent-clients/index.js'
import { DATA_DIR } from '../config.js'
import path from 'path'
@@ -36,7 +37,13 @@ router.post('/organize', requireAuth, async (req, res) => {
if (!hash || !name || !save_path) { res.status(400).json({ error: 'hash, name et save_path requis' }); return }
try {
const seriesData = await loadEnrichedSeriesData()
const result = await organizeTorrent(hash, name, save_path, seriesData)
// Récupère la progression par fichier depuis le client torrent.
// Permet au worker de sauter les fichiers encore en téléchargement (EBUSY sur Windows).
// En cas d'échec (client injoignable, hash non trouvé), on continue sans filtre.
const files = await dispatchGetFiles(hash).catch(() => [])
const result = await organizeTorrent(hash, name, save_path, seriesData, files)
if (result.done > 0 || result.errors.length > 0) {
pushNotif({ hash, name, done: result.done, skipped: result.skipped, errors: result.errors.length, errorFiles: result.errors, at: new Date().toISOString() })
}
+78 -4
View File
@@ -22,9 +22,12 @@ export interface FieldDef {
}
export interface TorrentClientDefinition {
id : string
label : string
fields : FieldDef[]
id : string
label : string
fields : FieldDef[]
/** Si true, dispatchList filtre les résultats pour n'inclure que les
* torrents ajoutés via FanKarr (clients sans catégories natives). */
filterByManagedHashes?: boolean
}
export interface SavedClient {
@@ -94,7 +97,64 @@ export function getAvailableClients(): TorrentClientDefinition[] {
// ─── Persistence ──────────────────────────────────────────────────────────────
const CLIENTS_PATH = path.join(DATA_DIR, 'torrent_clients.json')
const CLIENTS_PATH = path.join(DATA_DIR, 'torrent_clients.json')
// ─── Managed tasks ─────────────────────────────────────────────────────────────
// Stocke les infohashes des torrents ajoutés via FanKarr, par UUID client.
// Utilisé par dispatchList pour filtrer les résultats des clients sans
// catégories natives (ex: Synology Download Station).
// Structure : { clientUuid: [infohash, ...] }
const MANAGED_PATH = path.join(DATA_DIR, 'managed_tasks.json')
function loadManagedMap(): Record<string, string[]> {
try {
if (!fs.existsSync(MANAGED_PATH)) return {}
return JSON.parse(fs.readFileSync(MANAGED_PATH, 'utf-8'))
} catch { return {} }
}
function saveManagedMap(data: Record<string, string[]>) {
try {
fs.mkdirSync(path.dirname(MANAGED_PATH), { recursive: true })
fs.writeFileSync(MANAGED_PATH, JSON.stringify(data, null, 2), 'utf-8')
} catch {}
}
/** Retourne le Set des infohashes gérés pour un client, ou null si vide. */
function getManagedSet(clientUuid: string): Set<string> | null {
const list = loadManagedMap()[clientUuid]
if (!list || list.length === 0) return null
return new Set(list)
}
/** Enregistre un infohash comme géré pour un client (idempotent). */
export function trackManagedHash(clientUuid: string, infohash: string) {
const data = loadManagedMap()
if (!data[clientUuid]) data[clientUuid] = []
const h = infohash.toLowerCase()
if (!data[clientUuid].includes(h)) {
data[clientUuid].push(h)
saveManagedMap(data)
}
}
/** Retire un infohash de la liste gérée pour un client. */
export function untrackManagedHash(clientUuid: string, infohash: string) {
const data = loadManagedMap()
if (!data[clientUuid]) return
data[clientUuid] = data[clientUuid].filter(h => h !== infohash.toLowerCase())
if (data[clientUuid].length === 0) delete data[clientUuid]
saveManagedMap(data)
}
/** Supprime toutes les tâches gérées d'un client (appelé lors de la suppression du client). */
function clearManagedHashes(clientUuid: string) {
const data = loadManagedMap()
if (!data[clientUuid]) return
delete data[clientUuid]
saveManagedMap(data)
}
function loadClients(): SavedClient[] {
try {
@@ -129,6 +189,7 @@ export function removeClient(uuid: string): boolean {
const filtered = clients.filter(c => c.uuid !== uuid)
if (filtered.length === clients.length) return false
saveClients(filtered)
clearManagedHashes(uuid)
logger.info('torrent-clients', `Client supprimé : "${target?.name}" (${target?.type})`)
return true
}
@@ -243,6 +304,10 @@ export async function dispatchDownload(url: string, options?: DownloadOptions):
}
try {
await driver.add(client.config, url, options)
if (options?.infohash) {
trackManagedHash(client.uuid, options.infohash)
logger.debug('torrent-clients', `Tâche ${options.infohash.slice(0, 8)}… suivie pour "${client.name}"`)
}
logger.info('torrent-clients', `Téléchargement envoyé à "${client.name}"`)
results.push({ uuid: client.uuid, name: client.name, ok: true })
} catch (err) {
@@ -266,6 +331,7 @@ export async function dispatchRemove(hash: string, deleteFiles = false): Promise
try {
await driver.remove(client.config, hash, deleteFiles)
logger.info('torrent-clients', `Torrent ${hash.slice(0, 8)}… supprimé de "${client.name}"${deleteFiles ? ' (avec fichiers)' : ''}`)
untrackManagedHash(client.uuid, hash)
found = true
} catch (err) {
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
@@ -328,6 +394,14 @@ export async function dispatchList(
}
}
// Filtre par hashes gérés pour les clients sans catégories natives.
// Si le Set de tâches suivies est non-vide et que ce hash n'y figure pas,
// on ignore ce torrent (il n'a pas été ajouté via FanKarr).
if (driver.definition.filterByManagedHashes) {
const managed = getManagedSet(client.uuid)
if (managed && !managed.has(hash)) continue
}
results.push({ ...t, hash, save_path, client_uuid: client.uuid, client_name: client.name })
}
} catch (err) {
+44 -19
View File
@@ -120,9 +120,10 @@ async function dsRequest(
const DS: TorrentClientDriver = {
definition: {
id : 'synology-ds',
label : 'Synology Download Station',
fields: [
id : 'synology-ds',
label : 'Synology Download Station',
filterByManagedHashes: true,
fields : [
{ key: 'url', label: 'URL DSM', type: 'url', placeholder: 'http://192.168.1.x:5000', required: true },
{ key: 'username', label: 'Identifiant', type: 'text', placeholder: 'admin', required: true },
{ key: 'password', label: 'Mot de passe', type: 'password', placeholder: '••••••••', required: true },
@@ -172,7 +173,17 @@ const DS: TorrentClientDriver = {
const tasks: any[] = data?.tasks ?? []
return tasks.map(t => {
// Filtrage par dossier cible — Synology n'a pas de système de labels/catégories.
// Si savePath est configuré, on ne garde que les tâches dont la destination correspond.
const normalizedSavePath = String(config.savePath ?? '').replace(/\/+$/, '').toLowerCase()
const filtered = normalizedSavePath
? tasks.filter(t => {
const dest = String(t.additional?.detail?.destination ?? '').replace(/\/+$/, '').toLowerCase()
return dest === normalizedSavePath || dest.startsWith(normalizedSavePath + '/')
})
: tasks
return filtered.map(t => {
const transfer = t.additional?.transfer ?? {}
const detail = t.additional?.detail ?? {}
const fileList: any[] = t.additional?.file ?? []
@@ -235,37 +246,51 @@ const DS: TorrentClientDriver = {
const sid = await dsLogin(config)
const makeBody = (withDestination: boolean) => {
const body = new URLSearchParams({
// Snapshot des IDs existants avant l'ajout — pour identifier la nouvelle tâche Synology
const beforeIds = new Set<string>()
try {
const before = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', {}, sid)
for (const t of before?.tasks ?? []) beforeIds.add(String(t.id))
} catch {}
const doAdd = async (withDestination: boolean) => {
const params = new URLSearchParams({
api : 'SYNO.DownloadStation.Task',
version: '1',
method : 'create',
_sid : sid,
uri : url,
})
if (withDestination && config.savePath) body.append('destination', String(config.savePath))
return body
}
const doRequest = async (body: URLSearchParams) => {
const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi`, {
method : 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body : body.toString(),
})
if (withDestination && config.savePath) params.append('destination', String(config.savePath))
const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
let data = await doRequest(makeBody(true))
let data = await doAdd(true)
// Code 403 = destination inexistante sur le NAS → réessai sans destination
// Code 403 = dossier cible introuvable sur le NAS → réessai sans destination
if (!data.success && data.error?.code === 403 && config.savePath) {
logger.warn('synology-ds', `Dossier cible "${config.savePath}" introuvable (code 403) — ajout sans destination`)
data = await doRequest(makeBody(false))
data = await doAdd(false)
}
if (!data.success) throw new Error(`Ajout échoué : ${JSON.stringify(data.error)}`)
// Snapshot après ajout — identifier l'ID natif Synology (dbid_X) de la tâche créée
// et le stocker dans _hashToSynoId pour remove/getFiles sans attendre le prochain list()
if (options?.infohash) {
try {
const after = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', {}, sid)
const newTask = (after?.tasks ?? []).find((t: any) => !beforeIds.has(String(t.id)))
if (newTask) {
const synoId = String(newTask.id).toLowerCase()
_hashToSynoId.set(options.infohash.toLowerCase(), synoId)
logger.debug('synology-ds', `ID Synology capturé : ${synoId} → ${options.infohash.slice(0, 8)}…`)
}
} catch {}
}
logger.info('synology-ds', `Torrent ajouté avec succès${config.savePath ? ` (dossier: ${config.savePath})` : ''}`)
},
+32 -16
View File
@@ -606,33 +606,49 @@ const _epChapCache = new Map<string | null | undefined, ReturnType<typeof _parse
*/
function _parseEpChap(plot: string | null | undefined): {
episodes: string | null
chapters: string | null
season: number | null
chapters: string |null
cleanPlot: string
} {
const text = plot ?? ''
const epMatch = text.match(/[éeÉE]pisodes?\s+(\d+)(?:\s+[àa]\s+(\d+))?/i)
const chapMatch = text.match(/chapitres?\s+(\d+)(?:\s+[àa]\s+(\d+))?/i)
const epMatch = text.match(
/[éeÉE]pisodes?\s+(\d+)(?:\s+[àa]\s+(\d+))?(?:\s+de\s+la\s+saison\s+(\d+))?/i
)
const seasonMatch = text.match(/saison\s+(\d+)/i)
const season = epMatch?.[3] ?? seasonMatch?.[1] ?? null
const chapMatch = text.match(
/chapitres?\s+(\d+)(?:\s+[àa]\s+(\d+))?/i
)
const episodes = epMatch
? (epMatch[2] ? `Ép. ${epMatch[1]}–${epMatch[2]}` : `Ép. ${epMatch[1]}`)
: null
const chapters = chapMatch
? (chapMatch[2] ? `Ch. ${chapMatch[1]}–${chapMatch[2]}` : `Ch. ${chapMatch[1]}`)
: null
? `${season ? `S${season} · ` : ''}${
epMatch[2]
? `Ép. ${epMatch[1]}–${epMatch[2]}`
: `Ép. ${epMatch[1]}`
}`
: null
const chapters = chapMatch
? (chapMatch[2] ? `Ch. ${chapMatch[1]}–${chapMatch[2]}` : `Ch. ${chapMatch[1]}`)
: null
// Retirer les lignes contenant les références d'épisodes ou chapitres
let cleanPlot = text
if (episodes || chapters) {
if (episodes || chapters || season) {
cleanPlot = text
.split(/\r?\n/)
.filter(line => !/[éeÉE]pisodes?\s+\d+|chapitres?\s+\d+/i.test(line))
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
.split(/\r?\n/)
.filter(line => !/(?:[éeÉE]pisodes?\s+\d+|chapitres?\s+\d+|saison\s+\d+)/i.test(line))
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
return { episodes, chapters, cleanPlot }
return { episodes, season, chapters, cleanPlot }
}
function parseEpChap(plot: string | null | undefined) {
if (_epChapCache.has(plot)) return _epChapCache.get(plot)!
+2 -6
View File
@@ -182,7 +182,7 @@
<input
v-model="modal.config[field.key]"
:type="field.type === 'password' ? 'password' : 'text'"
placeholder=""
:placeholder="field.placeholder ?? ''"
class="settings-input"
/>
</div>
@@ -254,11 +254,7 @@ const clientMatrix = [
{
id: 'real-debrid', label: 'Real-Debrid', version: 'API v1',
auth: 'Clé API', fileSelect: true, fileProgress: true, torrentUpload: 'proxy', categories: false, deleteFiles: true,
},
{
id: 'alldebrid', label: 'AllDebrid', version: 'API v4',
auth: 'Clé API', fileSelect: false, fileProgress: true, torrentUpload: 'proxy', categories: false, deleteFiles: true,
},
}
]
const modal = ref({
+1 -1
View File
@@ -1 +1 @@
3.19.0
3.20.3