Compare commits

...
8 Commits
Author SHA1 Message Date
MasutaYunikon 88e2a0bc38 Merge pull request #35 from Masutayunikon/release-please--branches--main
chore(main): release 1.11.3
2026-03-20 16:08:46 +01:00
MasutaYunikon 338e3f8547 chore(main): release 1.11.3 2026-03-20 15:41:14 +01:00
MasutaYunikon d1ccc902da Merge remote-tracking branch 'origin/main' 2026-03-20 15:40:40 +01:00
MasutaYunikon 5e54e24b60 fix: entrypoint.sh config directory 2026-03-20 15:40:28 +01:00
MasutaYunikon aae2f592b5 Merge pull request #34 from Masutayunikon/release-please--branches--main
chore(main): release 1.11.2
2026-03-20 02:11:37 +01:00
MasutaYunikon bd94e6a460 chore(main): release 1.11.2 2026-03-20 02:11:01 +01:00
MasutaYunikon cbe3e1a786 Merge remote-tracking branch 'origin/main' 2026-03-20 02:10:38 +01:00
MasutaYunikon 7a0917e053 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
2026-03-20 02:10:27 +01:00
14 changed files with 154 additions and 71 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ node_modules
.git
.gitignore
*.md
data
config
.env
.env.*
!.env.example
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [1.11.3](https://github.com/Masutayunikon/FanKarr/compare/v1.11.2...v1.11.3) (2026-03-20)
### Bug Fixes
* entrypoint.sh config directory ([7e4b2fa](https://github.com/Masutayunikon/FanKarr/commit/7e4b2fa60a5946d3220618f13420a07fa57e8e1b))
## [1.11.2](https://github.com/Masutayunikon/FanKarr/compare/v1.11.1...v1.11.2) (2026-03-20)
### Bug Fixes
* port 9898, /config volume, Kaï→Kai matching, NFO pagination GitLab, saison 0 specials, JoJo/Hokuto strict season resolve ([3a278ba](https://github.com/Masutayunikon/FanKarr/commit/3a278ba33388d6ff6f33413d7696d58d0172c953))
## [1.11.1](https://github.com/Masutayunikon/FanKarr/compare/v1.11.0...v1.11.1) (2026-03-13)
+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 entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN mkdir -p /app/data
RUN mkdir -p /config
EXPOSE 3001
ENV NODE_ENV=production
+29 -27
View File
@@ -2,7 +2,6 @@
# Fankarr
![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 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)
![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
</div>
@@ -83,11 +82,10 @@ services:
- PGID=1000 # GID de votre utilisateur (id -g)
- TZ=Europe/Paris # Votre timezone
volumes:
- ./data:/app/data # Config, logs, base de données
- /votre/chemin/media:/media/Kai # Destination Jellyfin
- /votre/chemin/complete:/downloads/complete # Dossier complétés qBittorrent
- ./config:/config # Config, logs, base de données
- /votre/chemin:/media # Racine de votre médiathèque (voir note hardlinks)
ports:
- 3001:3001
- 9898:9898
restart: unless-stopped
```
@@ -95,15 +93,19 @@ services:
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)
Téléchargez l'archive correspondant à votre système depuis les [Releases GitHub](https://github.com/masutayunikon/fankarr/releases/latest) :
| Système | Archive |
|---|---|
| Windows x64 | `fankarr-windows-x64.zip` |
| Linux x64 | `fankarr-linux-x64.zip` |
| macOS (Apple Silicon) | `fankarr-macos.zip` |
| Système | Archive |
| --------------------- | ------------------------- |
| Windows x64 | `fankarr-windows-x64.zip` |
| Linux x64 | `fankarr-linux-x64.zip` |
| macOS (Apple Silicon) | `fankarr-macos.zip` |
Extrayez l'archive — elle contient le binaire **et** le dossier `public/` qui doivent rester ensemble :
@@ -120,22 +122,22 @@ chmod +x 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
| Variable | Défaut | Description |
|---|---|---|
| `PUID` | `1000` | UID utilisateur pour les permissions fichiers (Docker) |
| `PGID` | `1000` | GID utilisateur pour les permissions fichiers (Docker) |
| `TZ` | — | Timezone (ex: `Europe/Paris`) |
| `PORT` | `3001` | Port d'écoute du serveur |
| `JWT_SECRET` | auto-généré | Secret JWT — généré automatiquement dans `data/secret.key` si absent |
| `GITHUB_RAW_URL` | repo scraper | URL du `torrent_final.json` à utiliser |
| Variable | Défaut | Description |
| ---------------- | ------------ | ----------------------------------------------------------------------- |
| `PUID` | `1000` | UID utilisateur pour les permissions fichiers (Docker) |
| `PGID` | `1000` | GID utilisateur pour les permissions fichiers (Docker) |
| `TZ` | — | Timezone (ex: `Europe/Paris`) |
| `PORT` | `9898` | Port d'écoute du serveur |
| `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 |
### Premier démarrage
1. Ouvrir `http://localhost:3001`
1. Ouvrir `http://localhost:9898`
2. Créer un mot de passe à la première connexion
3. Aller dans **Paramètres** → ajouter votre client qBittorrent
4. Vérifier les chemins `completePath` et `mediaPath`
@@ -173,12 +175,12 @@ Fankarr est disponible dans l'appstore personnalisé. Pour l'installer :
## Stack technique
| Couche | Technologie |
|---|---|
| Frontend | Vue 3 + Vite + Tailwind CSS |
| Backend | Express 5 + TypeScript |
| Auth | JWT + bcrypt |
| Packaging Docker | `node:20-slim` + pnpm + gosu (PUID/PGID) |
| Couche | Technologie |
| ----------------- | ---------------------------------------- |
| Frontend | Vue 3 + Vite + Tailwind CSS |
| Backend | Express 5 + TypeScript |
| Auth | JWT + bcrypt |
| Packaging Docker | `node:20-slim` + pnpm + gosu (PUID/PGID) |
| Packaging binaire | Bun (self-contained, pas de Node requis) |
---
+14 -15
View File
@@ -1,15 +1,14 @@
services:
fankarr:
image: masutayunikon/fankarr:latest
container_name: fankarr
environment:
- PUID=1000 # UID de votre utilisateur (id -u)
- PGID=1000 # GID de votre utilisateur (id -g)
- TZ=Europe/Paris # Votre timezone
volumes:
- ./data:/app/data # Config, logs, base de données
- /votre/chemin/media:/media/Kai # Destination Jellyfin
- /votre/chemin/complete:/downloads/complete # Dossier complétés qBittorrent
ports:
- 3001:3001
restart: unless-stopped
fankarr:
image: masutayunikon/fankarr:latest
container_name: fankarr
environment:
- PUID=1000 # UID de votre utilisateur (id -u)
- PGID=1000 # GID de votre utilisateur (id -g)
- TZ=Europe/Paris # Votre timezone
volumes:
- ./config:/config # Config, logs, base de données
- /votre/chemin:/media # Racine de votre mediatheque (hardlinks compatibles)
ports:
- 9898:9898
restart: unless-stopped
+1 -1
View File
@@ -7,6 +7,6 @@ PGID=${PGID:-1000}
echo "[fankarr] Running as UID=${PUID} GID=${PGID}"
# Corriger les permissions du dossier data
chown -R "$PUID:$PGID" /app/data
chown -R "$PUID:$PGID" /config
exec gosu "$PUID:$PGID" node dist/server/index.js
+1 -1
View File
@@ -5,7 +5,7 @@ import jwt from 'jsonwebtoken'
import type { Request, Response, NextFunction } from 'express'
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
interface AuthData {
+84 -16
View File
@@ -24,10 +24,15 @@ export const BASE_DIR = _isBunBinary
? path.dirname((process as any).execPath)
: process.cwd()
// DATA_DIR : /config
export const DATA_DIR = fs.existsSync('/.dockerenv')
? '/config'
: path.join(BASE_DIR, 'config')
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 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
?? '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}`)
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[]> {
const map = new Map<number, any[]>()
for (const t of torrents) {
@@ -117,6 +131,18 @@ function indexBySerie(torrents: any[]): Map<number, any[]> {
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 ─────────────────────────────────────────────────────
function computeSerieDownloadState(
@@ -154,12 +180,13 @@ function computeSerieDownloadState(
app.get('/api/series', requireAuth, async (_req, res) => {
try {
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
let organized: Record<string, Record<string, string>> = {}
try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json')
const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {}
@@ -175,9 +202,30 @@ app.get('/api/series', requireAuth, async (_req, res) => {
}
} 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) => {
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 {
...serie,
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 ?? []) }
}))
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 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 episodeTorrentMapBySnEn: Record<string, any> = {}
const seasonTorrentMap : Record<number, any> = {}
const integraleTorrents: 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 }
for (const ep of t.resolved_episodes ?? []) {
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') {
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
let organized: Record<string, Record<string, string>> = {}
try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json')
const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {}
@@ -273,6 +335,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
// Construire un set des episode_id organisés
// On croise resolved_episodes de chaque torrent avec organized.json
const organizedEpisodeIds = new Set<number>()
const organizedBySnEn = new Set<string>()
for (const t of torrents) {
const hash = t.infohash?.toLowerCase()
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) {
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 eps = season.episodes.map((ep: any) => ({
...ep,
available : availableEpisodeIds.has(ep.id),
torrent : episodeTorrentMap[ep.id] ?? null,
organized : organizedEpisodeIds.has(ep.id),
available : availableEpisodeIds.has(ep.id)
|| availableBySnEn.has(`${season.season_number}:${ep.episode_number}`),
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
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
let organized: Record<string, Record<string, string>> = {}
try {
const orgPath = path.join(process.cwd(), 'data', 'organized.json')
const orgPath = path.join(DATA_DIR, 'organized.json')
if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {}
let torrentFinal: any[] = []
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))
torrentFinal = JSON.parse(fs.readFileSync(tfPath, 'utf-8'))
} catch {}
@@ -487,7 +555,7 @@ app.post('/api/update', requireAuth, async (_req, res) => {
app.post('/api/scan', requireAuth, async (_req, res) => {
try {
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)
logger.info('api', `Scan manuel: ${result.found} fichiers scannés, ${result.added} ajoutés`)
res.json({ ok: true, ...result })
@@ -530,7 +598,7 @@ app.listen(PORT, async () => {
console.log(`[fankarr] Serveur sur http://localhost:${PORT}`)
// 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()
scanMediaPath(mediaPath, TORRENTS_PATH, ORGANIZED_PATH).catch(err =>
logger.error('organize', `scanMediaPath échoué: ${err}`)
+1 -1
View File
@@ -19,7 +19,7 @@ export interface LogEntry {
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 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)
: process.cwd()
const TORRENTS_PATH = path.join(BASE_DIR, 'data', 'torrent_final.json')
const ORGANIZED_PATH = path.join(BASE_DIR, 'data', 'organized.json')
const TORRENTS_PATH = path.join(BASE_DIR, 'config', 'torrent_final.json')
const ORGANIZED_PATH = path.join(BASE_DIR, 'config', 'organized.json')
// ─── Utils log ────────────────────────────────────────────────
// 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 } {
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 }
return JSON.parse(fs.readFileSync(p, 'utf-8'))
} 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 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 {
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
? path.dirname((process as any).execPath)
: 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 {
mediaPath : string
+1 -1
View File
@@ -77,7 +77,7 @@ export function getAvailableClients(): TorrentClientDefinition[] {
// ─── 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[] {
try {
+2 -2
View File
@@ -14,9 +14,9 @@ export default defineConfig({
outDir: 'dist/client'
},
server: {
port: 3000,
port: 3000, // ou autre port fixe
proxy: {
'/api': 'http://localhost:3001'
'/api': 'http://localhost:9898'
}
}
})