Compare commits

...
4 Commits
Author SHA1 Message Date
MasutaYunikon 43c4422b4f Merge pull request #10 from Masutayunikon/release-please--branches--main
chore(main): release 1.3.0
2026-03-11 14:25:31 +01:00
MasutaYunikon 6410fcee1f chore(main): release 1.3.0 2026-03-11 14:25:03 +01:00
MasutaYunikon 3b281b60ee Merge remote-tracking branch 'origin/main' 2026-03-11 14:24:42 +01:00
MasutaYunikon 0e6e13332f feat(serie): badges état organisation sur épisodes et saisons
- api: /api/series/:id enrichi avec organized + organized_state/count par saison
- ui: badge ✓ organisé vert sur chaque épisode organisé
- ui: compteur organisés dans le header de saison (partial/complete)
- fix: suppression fill="none" restants dans SerieView.vue
2026-03-11 14:24:33 +01:00
3 changed files with 74 additions and 12 deletions

No files matched your search

+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [1.3.0](https://github.com/Masutayunikon/FanKarr/compare/v1.2.0...v1.3.0) (2026-03-11)
### Features
* **serie:** badges état organisation sur épisodes et saisons ([4d3a6bd](https://github.com/Masutayunikon/FanKarr/commit/4d3a6bd276907f793452429d62693fafc1d804db))
## [1.2.0](https://github.com/Masutayunikon/FanKarr/compare/v1.1.0...v1.2.0) (2026-03-11)
+47 -7
View File
@@ -253,15 +253,55 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
}).join(' · ')
}
const enrichedSeasons = seasonsWithEpisodes.map((season: any) => ({
...season,
torrent: seasonTorrentMap[season.id] ?? null,
episodes: season.episodes.map((ep: any) => ({
// 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')
if (fs.existsSync(orgPath))
organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8'))
} catch {}
// Construire un set des episode_id organisés
// On croise resolved_episodes de chaque torrent avec organized.json
const organizedEpisodeIds = new Set<number>()
for (const t of torrents) {
const hash = t.infohash?.toLowerCase()
const files = t.torrent_files ?? []
const orgFiles = organized[hash] ?? {}
if (Object.keys(orgFiles).length === 0) continue
// Au moins un fichier organisé pour ce torrent → on marque les épisodes résolus
for (const ep of t.resolved_episodes ?? []) {
// Vérifier si le fichier de cet épisode est organisé
const epFile = files.find((f: any) =>
f.episode_ids?.includes(ep.episode_id) || f.episode_number === ep.episode_number
)
if (epFile ? orgFiles[epFile.filename] : Object.keys(orgFiles).length > 0) {
organizedEpisodeIds.add(ep.episode_id)
}
}
}
const enrichedSeasons = seasonsWithEpisodes.map((season: any) => {
const eps = season.episodes.map((ep: any) => ({
...ep,
available: availableEpisodeIds.has(ep.id),
torrent : episodeTorrentMap[ep.id] ?? null
available : availableEpisodeIds.has(ep.id),
torrent : episodeTorrentMap[ep.id] ?? null,
organized : organizedEpisodeIds.has(ep.id),
}))
}))
// State saison : none / partial / complete
const total = eps.filter((e: any) => e.available).length
const orgCount = eps.filter((e: any) => e.organized).length
const seasonOrganizedState =
orgCount === 0 ? 'none' :
orgCount >= total && total > 0 ? 'complete' : 'partial'
return {
...season,
torrent : seasonTorrentMap[season.id] ?? null,
organized_state : seasonOrganizedState,
organized_count : orgCount,
episodes : eps,
}
})
const heroBundles: any[] = [
...integraleTorrents.map(t => ({ label: 'Intégrale', torrent_url: t.torrent_url, magnet: t.magnet, raw: t.raw })),
+20 -5
View File
@@ -27,14 +27,14 @@
<!-- Hero -->
<div class="relative">
<div v-if="data.serie.fanart_image" class="absolute inset-0 h-72 overflow-hidden pointer-events-none">
<img :src="data.serie.fanart_image" class="w-full h-full object-cover opacity-20" alt="fanart image"/>
<img :src="data.serie.fanart_image" class="w-full h-full object-cover opacity-20" />
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950" />
</div>
<div class="relative max-w-7xl mx-auto px-6 pt-8 pb-10 flex gap-8">
<!-- Poster -->
<div class="shrink-0 w-36 rounded-xl overflow-hidden border border-white/10 shadow-2xl hidden sm:block">
<img v-if="data.serie.poster_image" :src="data.serie.poster_image" class="w-full h-full object-cover" alt="poster image"/>
<img v-if="data.serie.poster_image" :src="data.serie.poster_image" class="w-full h-full object-cover" />
<div v-else class="w-full aspect-[2/3] bg-zinc-900 flex items-center justify-center text-zinc-700">
<svg width="28" height="28" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<rect x="2" y="2" width="20" height="20" rx="2"/><path d="m9 9 6 6M15 9l-6 6"/>
@@ -108,6 +108,15 @@
<p class="text-xs text-zinc-600 mt-0.5">
{{ season.episodes.length }} épisode{{ season.episodes.length > 1 ? 's' : '' }}
· {{ seasonAvailableCount(season) }}/{{ season.episodes.length }} dispo
<span v-if="season.organized_state !== 'none'"
class="ml-1.5 px-1.5 py-0.5 rounded text-[10px] font-medium"
:class="season.organized_state === 'complete'
? 'bg-green-500/10 text-green-400'
: 'bg-yellow-500/10 text-yellow-500'">
{{ season.organized_state === 'complete'
? `✓ ${season.organized_count} organisé${season.organized_count > 1 ? 's' : ''}`
: `${season.organized_count}/${seasonAvailableCount(season)} organisés` }}
</span>
</p>
</div>
</div>
@@ -135,7 +144,7 @@
>
<!-- Thumbnail -->
<div class="shrink-0 w-24 aspect-video rounded-lg overflow-hidden bg-zinc-800 hidden sm:block">
<img v-if="ep.thumb_image" :src="ep.thumb_image" class="w-full h-full object-cover" loading="lazy" alt="thumb image"/>
<img v-if="ep.thumb_image" :src="ep.thumb_image" class="w-full h-full object-cover" loading="lazy" />
<div v-else class="w-full h-full flex items-center justify-center text-zinc-700 text-xs font-mono">
{{ season.season_number === 0 ? 'SP' : `E${ep.episode_number}` }}
</div>
@@ -156,10 +165,16 @@
</div>
<!-- Bouton épisode (type episode uniquement) -->
<div class="shrink-0">
<div class="shrink-0 flex items-center gap-2">
<!-- Badge organisé -->
<span v-if="ep.organized"
class="text-[10px] px-2 py-0.5 rounded-full border bg-green-500/10 text-green-400 border-green-500/20">
✓ organisé
</span>
<!-- Badge dispo/indispo basé sur available (episode résolu dans un torrent) -->
<span
class="inline-block text-[10px] px-2 py-0.5 rounded-full border mr-2"
class="inline-block text-[10px] px-2 py-0.5 rounded-full border"
:class="ep.available
? 'bg-green-500/10 text-green-400 border-green-500/20'
: 'bg-white/5 text-zinc-600 border-white/8'"