Compare commits

...
12 Commits
5 changed files with 69 additions and 21 deletions

No files matched your search

+21
View File
@@ -1,5 +1,26 @@
# Changelog # Changelog
## [1.8.2](https://github.com/Masutayunikon/FanKarr/compare/v1.8.1...v1.8.2) (2026-03-13)
### Bug Fixes
* season fallback ([e9b5091](https://github.com/Masutayunikon/FanKarr/commit/e9b5091242d73be77bf6fb525ce2865554e336cc))
## [1.8.1](https://github.com/Masutayunikon/FanKarr/compare/v1.8.0...v1.8.1) (2026-03-13)
### Bug Fixes
* nfo settings and add json file when not existing ([0b6bfa6](https://github.com/Masutayunikon/FanKarr/commit/0b6bfa6a81d99353be877ed6d99687ad3d55adbc))
## [1.8.0](https://github.com/Masutayunikon/FanKarr/compare/v1.7.3...v1.8.0) (2026-03-12)
### Features
* cache images ([da460de](https://github.com/Masutayunikon/FanKarr/commit/da460de60c4eec6851ea58ce0ae646a1208f6018))
## [1.7.3](https://github.com/Masutayunikon/FanKarr/compare/v1.7.2...v1.7.3) (2026-03-12) ## [1.7.3](https://github.com/Masutayunikon/FanKarr/compare/v1.7.2...v1.7.3) (2026-03-12)
+19 -3
View File
@@ -42,8 +42,8 @@ app.get('/api/settings', requireAuth, (_req, res) => {
res.json(readSettings()) res.json(readSettings())
}) })
app.post('/api/settings', requireAuth, (req, res) => { app.post('/api/settings', requireAuth, (req, res) => {
const { mediaPath, completePath, organizeMode, category } = req.body const { mediaPath, completePath, organizeMode, category, nfoSupport } = req.body
res.json(writeSettings({ mediaPath, completePath, organizeMode, category })) res.json(writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport }))
}) })
// ── Torrent clients ──────────────────────────────────────────── // ── Torrent clients ────────────────────────────────────────────
@@ -495,7 +495,23 @@ if (fs.existsSync(PUBLIC_PATH)) {
} }
// ── Start ────────────────────────────────────────────────────── // ── Start ──────────────────────────────────────────────────────
app.listen(PORT, () => { app.listen(PORT, async () => {
// Télécharger torrent_final.json si absent
if (!fs.existsSync(TORRENTS_PATH)) {
try {
console.log('[fankarr] torrent_final.json absent → téléchargement...')
const response = await fetch(GITHUB_RAW_URL)
if (!response.ok) throw new Error(`GitHub a répondu ${response.status}`)
const data = await response.json() as unknown[]
if (!Array.isArray(data)) throw new Error('Format invalide')
fs.mkdirSync(path.dirname(TORRENTS_PATH), { recursive: true })
fs.writeFileSync(TORRENTS_PATH, JSON.stringify(data, null, 2), 'utf-8')
console.log(`[fankarr] ${data.length} torrents téléchargés`)
} catch (err) {
console.warn(`[fankarr] ⚠ Téléchargement échoué: ${err instanceof Error ? err.message : err}`)
}
}
try { try {
const data = JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) const data = JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8'))
const count = Array.isArray(data) ? data.length : 0 const count = Array.isArray(data) ? data.length : 0
+18 -2
View File
@@ -249,10 +249,26 @@ async function organizeTorrent(hash: string, name: string, savePath: string) {
} }
// ── Map filename → season ───────────────────────────────── // ── Map filename → season ─────────────────────────────────
// Priorité : season_number dans torrent_files (packs multi-saisons)
// Fallback : resolved_episodes matchés par episode_number (torrents individuels)
const filenameSeasonMap = new Map<string, number>() const filenameSeasonMap = new Map<string, number>()
// Index resolved_episodes par episode_number pour lookup rapide
const epByNum = new Map<number, number>() // ep_number → season_number
for (const ep of torrent.resolved_episodes ?? []) { for (const ep of torrent.resolved_episodes ?? []) {
if (ep.filename && ep.season_number !== undefined) if (ep.episode_number !== undefined && ep.season_number !== undefined)
filenameSeasonMap.set(ep.filename, ep.season_number) epByNum.set(ep.episode_number, ep.season_number)
}
for (const tf of torrentFiles) {
const sn: number | null = tf.season_number ?? null
if (sn !== null) {
filenameSeasonMap.set(tf.filename, sn)
} else if (tf.num !== undefined && epByNum.has(tf.num)) {
filenameSeasonMap.set(tf.filename, epByNum.get(tf.num)!)
} else if (torrent.season_number !== undefined) {
filenameSeasonMap.set(tf.filename, torrent.season_number)
}
} }
result.total = torrentFiles.length result.total = torrentFiles.length
+9
View File
@@ -61,6 +61,8 @@ export const useSeriesStore = defineStore('series', () => {
const loadingDetail = ref(false) const loadingDetail = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const _imageCache = new Set<string>()
async function fetchSeries() { async function fetchSeries() {
loadingSeries.value = true loadingSeries.value = true
error.value = null error.value = null
@@ -69,6 +71,13 @@ export const useSeriesStore = defineStore('series', () => {
if (!res.ok) throw new Error(`Erreur ${res.status}`) if (!res.ok) throw new Error(`Erreur ${res.status}`)
const data = await res.json() const data = await res.json()
series.value = data.series series.value = data.series
// Précache des posters en arrière-plan
for (const s of data.series as Serie[]) {
if (s.poster_image && !_imageCache.has(s.poster_image)) {
_imageCache.add(s.poster_image)
new Image().src = s.poster_image
}
}
} catch (err) { } catch (err) {
error.value = err instanceof Error ? err.message : 'Erreur inconnue' error.value = err instanceof Error ? err.message : 'Erreur inconnue'
} finally { } finally {
+2 -16
View File
@@ -276,23 +276,10 @@ async function fetchOrganizeNotifs() {
} }
// ── Cache images ─────────────────────────────────────────────── // ── Cache images ───────────────────────────────────────────────
const imageCache = new Set<string>() // Géré directement dans le store series.ts
function precacheImages(series: any[]) {
for (const s of series) {
if (s.poster_image && !imageCache.has(s.poster_image)) {
imageCache.add(s.poster_image)
const img = new Image()
img.src = s.poster_image
}
}
}
onMounted(async () => { onMounted(async () => {
if (store.series.length === 0) { if (store.series.length === 0) await store.fetchSeries()
await store.fetchSeries()
precacheImages(store.series)
}
fetchActiveDownloads() fetchActiveDownloads()
fetchOrganizeNotifs() fetchOrganizeNotifs()
dlInterval = setInterval(() => { dlInterval = setInterval(() => {
@@ -304,7 +291,6 @@ onMounted(async () => {
// Refresh au retour sur le catalogue (après update ou scan) // Refresh au retour sur le catalogue (après update ou scan)
onActivated(async () => { onActivated(async () => {
await store.fetchSeries() await store.fetchSeries()
precacheImages(store.series)
fetchActiveDownloads() fetchActiveDownloads()
}) })