Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c4422b4f | ||
|
|
6410fcee1f | ||
|
|
3b281b60ee | ||
|
|
0e6e13332f |
No files matched your search
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## [1.2.0](https://github.com/Masutayunikon/FanKarr/compare/v1.1.0...v1.2.0) (2026-03-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+47
-7
@@ -253,15 +253,55 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
|
|||||||
}).join(' · ')
|
}).join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrichedSeasons = seasonsWithEpisodes.map((season: any) => ({
|
// Charger organized.json pour les badges d'état
|
||||||
...season,
|
let organized: Record<string, Record<string, string>> = {}
|
||||||
torrent: seasonTorrentMap[season.id] ?? null,
|
try {
|
||||||
episodes: season.episodes.map((ep: any) => ({
|
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,
|
...ep,
|
||||||
available: availableEpisodeIds.has(ep.id),
|
available : availableEpisodeIds.has(ep.id),
|
||||||
torrent : episodeTorrentMap[ep.id] ?? null
|
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[] = [
|
const heroBundles: any[] = [
|
||||||
...integraleTorrents.map(t => ({ label: 'Intégrale', torrent_url: t.torrent_url, magnet: t.magnet, raw: t.raw })),
|
...integraleTorrents.map(t => ({ label: 'Intégrale', torrent_url: t.torrent_url, magnet: t.magnet, raw: t.raw })),
|
||||||
|
|||||||
+20
-5
@@ -27,14 +27,14 @@
|
|||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div v-if="data.serie.fanart_image" class="absolute inset-0 h-72 overflow-hidden pointer-events-none">
|
<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 class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative max-w-7xl mx-auto px-6 pt-8 pb-10 flex gap-8">
|
<div class="relative max-w-7xl mx-auto px-6 pt-8 pb-10 flex gap-8">
|
||||||
<!-- Poster -->
|
<!-- Poster -->
|
||||||
<div class="shrink-0 w-36 rounded-xl overflow-hidden border border-white/10 shadow-2xl hidden sm:block">
|
<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">
|
<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">
|
<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"/>
|
<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">
|
<p class="text-xs text-zinc-600 mt-0.5">
|
||||||
{{ season.episodes.length }} épisode{{ season.episodes.length > 1 ? 's' : '' }}
|
{{ season.episodes.length }} épisode{{ season.episodes.length > 1 ? 's' : '' }}
|
||||||
· {{ seasonAvailableCount(season) }}/{{ season.episodes.length }} dispo
|
· {{ 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +144,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Thumbnail -->
|
<!-- Thumbnail -->
|
||||||
<div class="shrink-0 w-24 aspect-video rounded-lg overflow-hidden bg-zinc-800 hidden sm:block">
|
<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">
|
<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}` }}
|
{{ season.season_number === 0 ? 'SP' : `E${ep.episode_number}` }}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,10 +165,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bouton épisode (type episode uniquement) -->
|
<!-- 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) -->
|
<!-- Badge dispo/indispo basé sur available (episode résolu dans un torrent) -->
|
||||||
<span
|
<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
|
:class="ep.available
|
||||||
? 'bg-green-500/10 text-green-400 border-green-500/20'
|
? 'bg-green-500/10 text-green-400 border-green-500/20'
|
||||||
: 'bg-white/5 text-zinc-600 border-white/8'"
|
: 'bg-white/5 text-zinc-600 border-white/8'"
|
||||||
|
|||||||
Reference in new issue
Block a user