fix: port 9898, /config volume, Kaï→Kai matching, NFO pagination GitLab, saison 0 specials, JoJo/Hokuto strict season resolve

- Port par défaut 3001 → 9898
- Volume /app/data → /config (entrypoint, docker-compose, README)
- Normalisation titres Kaï/Kai dans index.ts (catalogue + déduplication + serie detail)
- Fix pagination GitLab NFO (per_page=100 → loop multi-pages)
- Fix _is_bonus_filename avec word boundaries (\b) — évite faux positifs "national"→"ona"
- Fix dossier Bonus/Specials exclus : récupère flottants (7,5/19,5) et S00Exx → saison 0
- _EP_FLOAT_PATTERN supporte virgules (7,5) en plus des points (7.5)
- Ajout strict_season dans find_episode/resolve_ep — évite collision ep1 S1 vs ep1 S2 (JoJo Parties)
- Ajout SEASON_EP_OFFSET pour Hokuto No Ken (saison 02 torrent → +16 offset API)
- Fallback non-strict si strict_season échoue (Hokuto ep 5-16 S1)
- Fix collect_covered_episodes : fallback torrent.serie_id si absent dans resolved_episodes
- Fix s6_verify : get_all_specials + compteur S00 séparé, déduplication Kaï/Kai, format OK/TOTAL
- Ajout MANUAL_TORRENTS dans s4 (MHA Film 4 You're Next URL Nyaa)
- Injection torrents locaux manquants dans pipeline (Reborn! Kaï via fichier local)
- Fix s5 : résolution directe torrents manuels season_number=0
- Fix s2 : pack_saga_slash et pack_partie_slash → pack_integrale, lookahead (?<!/) sur episode_num_tiret_apres
- Fix s5 skip condition : re-résoudre si resolved_count < files_count (Fairy Tail)
- README : note hardlinks volumes multiples, port, chemins config
This commit is contained in:
MasutaYunikon committed 2026-03-20 02:10:27 +01:00
1 parent d5679ec529
commit 7a0917e053
12 files changed
+139 -70

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ node_modules
.git .git
.gitignore .gitignore
*.md *.md
data config
.env .env
.env.* .env.*
!.env.example !.env.example
+1 -1
View File
@@ -24,7 +24,7 @@ COPY --from=build /app/dist/server /app/dist/server
COPY --from=build /app/dist/client /app/public COPY --from=build /app/dist/client /app/public
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
RUN mkdir -p /app/data RUN mkdir -p /config
EXPOSE 3001 EXPOSE 3001
ENV NODE_ENV=production ENV NODE_ENV=production
+29 -27
View File
@@ -2,7 +2,6 @@
# Fankarr # Fankarr
![Version](https://img.shields.io/github/v/release/masutayunikon/fankarr?style=flat-square&color=e8513a&label=version) ![Version](https://img.shields.io/github/v/release/masutayunikon/fankarr?style=flat-square&color=e8513a&label=version)
![Docker Image Size](https://img.shields.io/docker/image-size/masutayunikon/fankarr/latest?style=flat-square&color=1e2d3d&label=image) ![Docker Image Size](https://img.shields.io/docker/image-size/masutayunikon/fankarr/latest?style=flat-square&color=1e2d3d&label=image)
![Docker Pulls](https://img.shields.io/docker/pulls/masutayunikon/fankarr?style=flat-square&color=1e2d3d) ![Docker Pulls](https://img.shields.io/docker/pulls/masutayunikon/fankarr?style=flat-square&color=1e2d3d)
@@ -11,7 +10,7 @@
![Node](https://img.shields.io/badge/node-20-5a7a94?style=flat-square) ![Node](https://img.shields.io/badge/node-20-5a7a94?style=flat-square)
![Vue](https://img.shields.io/badge/vue-3-42b883?style=flat-square) ![Vue](https://img.shields.io/badge/vue-3-42b883?style=flat-square)
**Gestionnaire de téléchargements pour le catalogue [Fankai](https://fankai.fr)** **Gestionnaire de téléchargements pour le catalogue [Fankai](https://fankai.fr)**
Inspiré de Radarr/Sonarr — interface dédiée aux éditions Kai & Yabai Inspiré de Radarr/Sonarr — interface dédiée aux éditions Kai & Yabai
</div> </div>
@@ -83,11 +82,10 @@ services:
- PGID=1000 # GID de votre utilisateur (id -g) - PGID=1000 # GID de votre utilisateur (id -g)
- TZ=Europe/Paris # Votre timezone - TZ=Europe/Paris # Votre timezone
volumes: volumes:
- ./data:/app/data # Config, logs, base de données - ./config:/config # Config, logs, base de données
- /votre/chemin/media:/media/Kai # Destination Jellyfin - /votre/chemin:/media # Racine de votre médiathèque (voir note hardlinks)
- /votre/chemin/complete:/downloads/complete # Dossier complétés qBittorrent
ports: ports:
- 3001:3001 - 9898:9898
restart: unless-stopped restart: unless-stopped
``` ```
@@ -95,15 +93,19 @@ services:
docker compose up -d docker compose up -d
``` ```
> **⚠️ Note sur les hardlinks** : Pour que le mode `hardlink` fonctionne, vos dossiers de téléchargement complets et votre médiathèque doivent être sur le **même filesystem**. La solution recommandée est de monter un seul volume racine (ex: `/votre/chemin:/media`) et de configurer vos chemins à l'intérieur — comme le font Radarr et Sonarr.
>
> Vous pouvez tout à fait monter plusieurs volumes si vous le souhaitez, mais dans ce cas les hardlinks ne seront pas possibles entre deux volumes différents et Fankarr basculera automatiquement en copie.
### Binaire autonome (Windows / Linux / macOS) ### Binaire autonome (Windows / Linux / macOS)
Téléchargez l'archive correspondant à votre système depuis les [Releases GitHub](https://github.com/masutayunikon/fankarr/releases/latest) : Téléchargez l'archive correspondant à votre système depuis les [Releases GitHub](https://github.com/masutayunikon/fankarr/releases/latest) :
| Système | Archive | | Système | Archive |
|---|---| | --------------------- | ------------------------- |
| Windows x64 | `fankarr-windows-x64.zip` | | Windows x64 | `fankarr-windows-x64.zip` |
| Linux x64 | `fankarr-linux-x64.zip` | | Linux x64 | `fankarr-linux-x64.zip` |
| macOS (Apple Silicon) | `fankarr-macos.zip` | | macOS (Apple Silicon) | `fankarr-macos.zip` |
Extrayez l'archive — elle contient le binaire **et** le dossier `public/` qui doivent rester ensemble : Extrayez l'archive — elle contient le binaire **et** le dossier `public/` qui doivent rester ensemble :
@@ -120,22 +122,22 @@ chmod +x fankarr
./fankarr ./fankarr
``` ```
Fankarr sera accessible sur `http://localhost:3001`. Les données (config, logs, base de données) sont sauvegardées dans un dossier `data/` créé automatiquement à côté du binaire. Fankarr sera accessible sur `http://localhost:9898`. Les données (config, logs, base de données) sont sauvegardées dans un dossier `data/` créé automatiquement à côté du binaire.
### Variables d'environnement ### Variables d'environnement
| Variable | Défaut | Description | | Variable | Défaut | Description |
|---|---|---| | ---------------- | ------------ | ----------------------------------------------------------------------- |
| `PUID` | `1000` | UID utilisateur pour les permissions fichiers (Docker) | | `PUID` | `1000` | UID utilisateur pour les permissions fichiers (Docker) |
| `PGID` | `1000` | GID utilisateur pour les permissions fichiers (Docker) | | `PGID` | `1000` | GID utilisateur pour les permissions fichiers (Docker) |
| `TZ` | — | Timezone (ex: `Europe/Paris`) | | `TZ` | — | Timezone (ex: `Europe/Paris`) |
| `PORT` | `3001` | Port d'écoute du serveur | | `PORT` | `9898` | Port d'écoute du serveur |
| `JWT_SECRET` | auto-généré | Secret JWT — généré automatiquement dans `data/secret.key` si absent | | `JWT_SECRET` | auto-généré | Secret JWT — généré automatiquement dans `/config/secret.key` si absent |
| `GITHUB_RAW_URL` | repo scraper | URL du `torrent_final.json` à utiliser | | `GITHUB_RAW_URL` | repo scraper | URL du `torrent_final.json` à utiliser |
### Premier démarrage ### Premier démarrage
1. Ouvrir `http://localhost:3001` 1. Ouvrir `http://localhost:9898`
2. Créer un mot de passe à la première connexion 2. Créer un mot de passe à la première connexion
3. Aller dans **Paramètres** → ajouter votre client qBittorrent 3. Aller dans **Paramètres** → ajouter votre client qBittorrent
4. Vérifier les chemins `completePath` et `mediaPath` 4. Vérifier les chemins `completePath` et `mediaPath`
@@ -173,12 +175,12 @@ Fankarr est disponible dans l'appstore personnalisé. Pour l'installer :
## Stack technique ## Stack technique
| Couche | Technologie | | Couche | Technologie |
|---|---| | ----------------- | ---------------------------------------- |
| Frontend | Vue 3 + Vite + Tailwind CSS | | Frontend | Vue 3 + Vite + Tailwind CSS |
| Backend | Express 5 + TypeScript | | Backend | Express 5 + TypeScript |
| Auth | JWT + bcrypt | | Auth | JWT + bcrypt |
| Packaging Docker | `node:20-slim` + pnpm + gosu (PUID/PGID) | | Packaging Docker | `node:20-slim` + pnpm + gosu (PUID/PGID) |
| Packaging binaire | Bun (self-contained, pas de Node requis) | | Packaging binaire | Bun (self-contained, pas de Node requis) |
--- ---
+14 -15
View File
@@ -1,15 +1,14 @@
services: fankarr:
fankarr: image: masutayunikon/fankarr:latest
image: masutayunikon/fankarr:latest container_name: fankarr
container_name: fankarr environment:
environment: - PUID=1000 # UID de votre utilisateur (id -u)
- PUID=1000 # UID de votre utilisateur (id -u) - PGID=1000 # GID de votre utilisateur (id -g)
- PGID=1000 # GID de votre utilisateur (id -g) - TZ=Europe/Paris # Votre timezone
- TZ=Europe/Paris # Votre timezone volumes:
volumes: - ./config:/config # Config, logs, base de données
- ./data:/app/data # Config, logs, base de données - /votre/chemin:/media # Racine de votre mediatheque (hardlinks compatibles)
- /votre/chemin/media:/media/Kai # Destination Jellyfin ports:
- /votre/chemin/complete:/downloads/complete # Dossier complétés qBittorrent - 9898:9898
ports: restart: unless-stopped
- 3001:3001
restart: unless-stopped
+1 -1
View File
@@ -5,7 +5,7 @@ import jwt from 'jsonwebtoken'
import type { Request, Response, NextFunction } from 'express' import type { Request, Response, NextFunction } from 'express'
import { JWT_SECRET } from './secret.js' import { JWT_SECRET } from './secret.js'
const DATA_PATH = path.join(process.cwd(), 'data', 'auth.json') const DATA_PATH = path.join(process.cwd(), 'config', 'auth.json')
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10
interface AuthData { interface AuthData {
+84 -16
View File
@@ -24,10 +24,15 @@ export const BASE_DIR = _isBunBinary
? path.dirname((process as any).execPath) ? path.dirname((process as any).execPath)
: process.cwd() : process.cwd()
// DATA_DIR : /config
export const DATA_DIR = fs.existsSync('/.dockerenv')
? '/config'
: path.join(BASE_DIR, 'config')
const app = express() const app = express()
const PORT = Number(process.env.PORT) || 3001 const PORT = Number(process.env.PORT) || 9898
const FANKAI_API = 'https://metadata.fankai.fr' const FANKAI_API = 'https://metadata.fankai.fr'
const TORRENTS_PATH = path.join(BASE_DIR, 'data', 'torrent_final.json') const TORRENTS_PATH = path.join(DATA_DIR, 'torrent_final.json')
const GITHUB_RAW_URL = process.env.GITHUB_RAW_URL const GITHUB_RAW_URL = process.env.GITHUB_RAW_URL
?? 'https://raw.githubusercontent.com/masutayunikon/fankarr-scraper/main/data/torrent_final.json' ?? 'https://raw.githubusercontent.com/masutayunikon/fankarr-scraper/main/data/torrent_final.json'
@@ -107,6 +112,15 @@ async function fankaiGet(endpoint: string): Promise<any> {
if (!res.ok) throw new Error(`Fankai API ${res.status}: ${endpoint}`) if (!res.ok) throw new Error(`Fankai API ${res.status}: ${endpoint}`)
return res.json() return res.json()
} }
function normalizeTitle(s: string): string {
return s.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // strip accents (ï→i, é→e...)
.replace(/[^a-z0-9]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function indexBySerie(torrents: any[]): Map<number, any[]> { function indexBySerie(torrents: any[]): Map<number, any[]> {
const map = new Map<number, any[]>() const map = new Map<number, any[]>()
for (const t of torrents) { for (const t of torrents) {
@@ -117,6 +131,18 @@ function indexBySerie(torrents: any[]): Map<number, any[]> {
return map return map
} }
function indexByNormalizedTitle(torrents: any[]): Map<string, any[]> {
const map = new Map<string, any[]>()
for (const t of torrents) {
const title = t.serie_title || t.show_title
if (!title) continue
const key = normalizeTitle(title)
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(t)
}
return map
}
// ── Series ───────────────────────────────────────────────────── // ── Series ─────────────────────────────────────────────────────
function computeSerieDownloadState( function computeSerieDownloadState(
@@ -154,12 +180,13 @@ function computeSerieDownloadState(
app.get('/api/series', requireAuth, async (_req, res) => { app.get('/api/series', requireAuth, async (_req, res) => {
try { try {
const [apiData, torrents] = await Promise.all([fankaiGet('/series'), Promise.resolve(readTorrents())]) const [apiData, torrents] = await Promise.all([fankaiGet('/series'), Promise.resolve(readTorrents())])
const byId = indexBySerie(torrents) const byId = indexBySerie(torrents)
const byNormTitle = indexByNormalizedTitle(torrents)
// Charger organized.json // Charger organized.json
let organized: Record<string, Record<string, string>> = {} let organized: Record<string, Record<string, string>> = {}
try { try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json') const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath)) if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {} } catch {}
@@ -175,9 +202,30 @@ app.get('/api/series', requireAuth, async (_req, res) => {
} }
} catch {} } catch {}
const seriesList = Array.isArray(apiData) ? apiData : (apiData.series ?? []) const seriesRaw = Array.isArray(apiData) ? apiData : (apiData.series ?? [])
// Dédupliquer les séries Kaï/Kai — garder celle qui a des torrents, sinon la plus récente (ID le plus grand)
const seenTitles = new Map<string, any>()
for (const serie of seriesRaw) {
const key = normalizeTitle(serie.title ?? serie.show_title ?? '')
if (!seenTitles.has(key)) {
seenTitles.set(key, serie)
} else {
const existing = seenTitles.get(key)!
const existingHasTorrents = (byId.get(existing.id) ?? byNormTitle.get(key) ?? []).length > 0
const newHasTorrents = (byId.get(serie.id) ?? byNormTitle.get(key) ?? []).length > 0
// Préférer celle qui a des torrents, sinon la plus récente
if (!existingHasTorrents && newHasTorrents) seenTitles.set(key, serie)
else if (existingHasTorrents === newHasTorrents && serie.id > existing.id) seenTitles.set(key, serie)
}
}
const seriesList = [...seenTitles.values()]
res.json({ series: seriesList.map((serie: any) => { res.json({ series: seriesList.map((serie: any) => {
const serieTorrents = byId.get(serie.id) ?? [] // Fallback par titre normalisé si l'ID ne matche pas (Kaï→Kai migration)
const serieTorrents = byId.get(serie.id)
?? byNormTitle.get(normalizeTitle(serie.title ?? serie.show_title ?? ''))
?? []
return { return {
...serie, ...serie,
torrent_count : serieTorrents.length, torrent_count : serieTorrents.length,
@@ -200,10 +248,19 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
return { ...season, episodes: Array.isArray(epsData) ? epsData : (epsData.episodes ?? []) } return { ...season, episodes: Array.isArray(epsData) ? epsData : (epsData.episodes ?? []) }
})) }))
const torrents = readTorrents().filter(t => t.serie_id === id) const allTorrents = readTorrents()
const serieMeta = allTorrents.find(t => t.serie_id === id)
const normApiTitle = normalizeTitle(serie.title ?? serie.show_title ?? '')
const torrents = allTorrents.filter(t =>
t.serie_id === id ||
normalizeTitle(t.serie_title ?? t.show_title ?? '') === normApiTitle
)
const allSeasonIds = new Set<number>(seasonsWithEpisodes.filter((s: any) => s.season_number !== 0).map((s: any) => s.id)) const allSeasonIds = new Set<number>(seasonsWithEpisodes.filter((s: any) => s.season_number !== 0).map((s: any) => s.id))
const availableEpisodeIds = new Set<number>() const availableEpisodeIds = new Set<number>()
// Fallback par season_number+episode_number pour les séries migrées Kaï→Kai
const availableBySnEn = new Set<string>() // "sn:en"
const episodeTorrentMap: Record<number, any> = {} const episodeTorrentMap: Record<number, any> = {}
const episodeTorrentMapBySnEn: Record<string, any> = {}
const seasonTorrentMap : Record<number, any> = {} const seasonTorrentMap : Record<number, any> = {}
const integraleTorrents: any[] = [] const integraleTorrents: any[] = []
const packEpisodesTorrents: any[] = [] const packEpisodesTorrents: any[] = []
@@ -212,7 +269,12 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: t.type, raw: t.raw } const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: t.type, raw: t.raw }
for (const ep of t.resolved_episodes ?? []) { for (const ep of t.resolved_episodes ?? []) {
availableEpisodeIds.add(ep.episode_id) availableEpisodeIds.add(ep.episode_id)
if (t.type === 'episode') episodeTorrentMap[ep.episode_id] = ref const snEnKey = `${ep.season_number}:${ep.episode_number}`
availableBySnEn.add(snEnKey)
if (t.type === 'episode') {
episodeTorrentMap[ep.episode_id] = ref
episodeTorrentMapBySnEn[snEnKey] = ref
}
} }
if (t.type === 'pack_saison') { if (t.type === 'pack_saison') {
const rs: any[] = t.resolved_seasons ?? [] const rs: any[] = t.resolved_seasons ?? []
@@ -265,7 +327,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
// Charger organized.json pour les badges d'état // Charger organized.json pour les badges d'état
let organized: Record<string, Record<string, string>> = {} let organized: Record<string, Record<string, string>> = {}
try { try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json') const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath)) if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {} } catch {}
@@ -273,6 +335,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
// Construire un set des episode_id organisés // Construire un set des episode_id organisés
// On croise resolved_episodes de chaque torrent avec organized.json // On croise resolved_episodes de chaque torrent avec organized.json
const organizedEpisodeIds = new Set<number>() const organizedEpisodeIds = new Set<number>()
const organizedBySnEn = new Set<string>()
for (const t of torrents) { for (const t of torrents) {
const hash = t.infohash?.toLowerCase() const hash = t.infohash?.toLowerCase()
const files = t.torrent_files ?? [] const files = t.torrent_files ?? []
@@ -286,6 +349,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
) )
if (epFile ? orgFiles[epFile.filename] : Object.keys(orgFiles).length > 0) { if (epFile ? orgFiles[epFile.filename] : Object.keys(orgFiles).length > 0) {
organizedEpisodeIds.add(ep.episode_id) organizedEpisodeIds.add(ep.episode_id)
organizedBySnEn.add(`${ep.season_number}:${ep.episode_number}`)
} }
} }
} }
@@ -293,9 +357,13 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
const enrichedSeasons = seasonsWithEpisodes.map((season: any) => { const enrichedSeasons = seasonsWithEpisodes.map((season: any) => {
const eps = season.episodes.map((ep: any) => ({ const eps = season.episodes.map((ep: any) => ({
...ep, ...ep,
available : availableEpisodeIds.has(ep.id), available : availableEpisodeIds.has(ep.id)
torrent : episodeTorrentMap[ep.id] ?? null, || availableBySnEn.has(`${season.season_number}:${ep.episode_number}`),
organized : organizedEpisodeIds.has(ep.id), torrent : episodeTorrentMap[ep.id]
?? episodeTorrentMapBySnEn[`${season.season_number}:${ep.episode_number}`]
?? null,
organized : organizedEpisodeIds.has(ep.id)
|| organizedBySnEn.has(`${season.season_number}:${ep.episode_number}`),
})) }))
// State saison : none / partial / complete // State saison : none / partial / complete
const total = eps.filter((e: any) => e.available).length const total = eps.filter((e: any) => e.available).length
@@ -341,14 +409,14 @@ app.get('/api/downloads', requireAuth, async (_req, res) => {
// Enrichir avec l'état d'organisation // Enrichir avec l'état d'organisation
let organized: Record<string, Record<string, string>> = {} let organized: Record<string, Record<string, string>> = {}
try { try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json') const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath)) if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {} } catch {}
let torrentFinal: any[] = [] let torrentFinal: any[] = []
try { try {
const tfPath = path.join(process.cwd(), 'data', 'torrent_final.json') const tfPath = path.join(process.cwd(), 'config', 'torrent_final.json')
if (fs.existsSync(tfPath)) if (fs.existsSync(tfPath))
torrentFinal = JSON.parse(fs.readFileSync(tfPath, 'utf-8')) torrentFinal = JSON.parse(fs.readFileSync(tfPath, 'utf-8'))
} catch {} } catch {}
@@ -487,7 +555,7 @@ app.post('/api/update', requireAuth, async (_req, res) => {
app.post('/api/scan', requireAuth, async (_req, res) => { app.post('/api/scan', requireAuth, async (_req, res) => {
try { try {
const { mediaPath } = readSettings() const { mediaPath } = readSettings()
const ORGANIZED_PATH = path.join(process.cwd(), 'data', 'organized.json') const ORGANIZED_PATH = path.join(DATA_DIR, 'organized.json')
const result = await scanMediaPath(mediaPath, TORRENTS_PATH, ORGANIZED_PATH) const result = await scanMediaPath(mediaPath, TORRENTS_PATH, ORGANIZED_PATH)
logger.info('api', `Scan manuel: ${result.found} fichiers scannés, ${result.added} ajoutés`) logger.info('api', `Scan manuel: ${result.found} fichiers scannés, ${result.added} ajoutés`)
res.json({ ok: true, ...result }) res.json({ ok: true, ...result })
@@ -530,7 +598,7 @@ app.listen(PORT, async () => {
console.log(`[fankarr] Serveur sur http://localhost:${PORT}`) console.log(`[fankarr] Serveur sur http://localhost:${PORT}`)
// Scan initial du mediaPath pour reconnaître les fichiers déjà présents // Scan initial du mediaPath pour reconnaître les fichiers déjà présents
const ORGANIZED_PATH = path.join(process.cwd(), 'data', 'organized.json') const ORGANIZED_PATH = path.join(DATA_DIR, 'organized.json')
const { mediaPath } = readSettings() const { mediaPath } = readSettings()
scanMediaPath(mediaPath, TORRENTS_PATH, ORGANIZED_PATH).catch(err => scanMediaPath(mediaPath, TORRENTS_PATH, ORGANIZED_PATH).catch(err =>
logger.error('organize', `scanMediaPath échoué: ${err}`) logger.error('organize', `scanMediaPath échoué: ${err}`)
+1 -1
View File
@@ -19,7 +19,7 @@ export interface LogEntry {
meta? : Record<string, any> meta? : Record<string, any>
} }
const LOGS_PATH = path.join(process.cwd(), 'data', 'logs.jsonl') const LOGS_PATH = path.join(process.cwd(), 'config', 'logs.jsonl')
const MAX_LINES = 2000 // rotation auto au-delà const MAX_LINES = 2000 // rotation auto au-delà
const IS_PROD = process.env.NODE_ENV === 'production' const IS_PROD = process.env.NODE_ENV === 'production'
+3 -3
View File
@@ -23,8 +23,8 @@ const BASE_DIR = _isBunBinary
? path.dirname((process as any).execPath) ? path.dirname((process as any).execPath)
: process.cwd() : process.cwd()
const TORRENTS_PATH = path.join(BASE_DIR, 'data', 'torrent_final.json') const TORRENTS_PATH = path.join(BASE_DIR, 'config', 'torrent_final.json')
const ORGANIZED_PATH = path.join(BASE_DIR, 'data', 'organized.json') const ORGANIZED_PATH = path.join(BASE_DIR, 'config', 'organized.json')
// ─── Utils log ──────────────────────────────────────────────── // ─── Utils log ────────────────────────────────────────────────
// Le worker tourne dans un thread séparé — il passe les logs au thread principal // Le worker tourne dans un thread séparé — il passe les logs au thread principal
@@ -39,7 +39,7 @@ function debug(msg: string) { parentPort?.postMessage({ type: 'log', level: 'deb
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(process.cwd(), 'config', '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 } }
+1 -1
View File
@@ -10,7 +10,7 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import crypto from 'crypto' import crypto from 'crypto'
const SECRET_PATH = path.join(process.cwd(), 'data', 'secret.key') const SECRET_PATH = path.join(process.cwd(), 'config', 'secret.key')
function generateSecret(): string { function generateSecret(): string {
return crypto.randomBytes(48).toString('hex') return crypto.randomBytes(48).toString('hex')
+1 -1
View File
@@ -6,7 +6,7 @@ const _isBunBinary = typeof (globalThis as any).Bun !== 'undefined'
const BASE_DIR = _isBunBinary const BASE_DIR = _isBunBinary
? path.dirname((process as any).execPath) ? path.dirname((process as any).execPath)
: process.cwd() : process.cwd()
const DATA_PATH = path.join(BASE_DIR, 'data', 'settings.json') const DATA_PATH = path.join(BASE_DIR, 'config', 'settings.json')
export interface Settings { export interface Settings {
mediaPath : string mediaPath : string
+1 -1
View File
@@ -77,7 +77,7 @@ export function getAvailableClients(): TorrentClientDefinition[] {
// ─── Persistence (clients enregistrés) ──────────────────────────────────────── // ─── Persistence (clients enregistrés) ────────────────────────────────────────
const CLIENTS_PATH = path.join(process.cwd(), 'data', 'torrent_clients.json') const CLIENTS_PATH = path.join(process.cwd(), 'config', 'torrent_clients.json')
function loadClients(): SavedClient[] { function loadClients(): SavedClient[] {
try { try {
+2 -2
View File
@@ -14,9 +14,9 @@ export default defineConfig({
outDir: 'dist/client' outDir: 'dist/client'
}, },
server: { server: {
port: 3000, port: 3000, // ou autre port fixe
proxy: { proxy: {
'/api': 'http://localhost:3001' '/api': 'http://localhost:9898'
} }
} }
}) })