Files
ohm_streaming/app/templates/discover.html
T
Roman d747c574b8 Découvrir v2 : rails par type, exploration par genre
- Rails Nouveautés séparés : 🆕 animés (tri date Kitsu) et 🆕 séries & films
  (French-Stream) — chaque type garde son rail, plus d'écrasement mutuel
- Section 🎭 Explorer : chips type × genre — animés via catégories Kitsu,
  séries (/<genre>-series-/) et films (/films/<genre>/) via French-Stream ;
  état dans l'URL (/discover?t=serie&g=medical), partageable
- Scraper : browse(media_type, genre) + catalogue YAML surchargeable ;
  parsing div.short mutualisé avec les nouveautés
- Films réels retirés du mode « Animés » (ils suivent le rail séries)
- Endpoints /api/discover (latest_anime/latest_serie), /api/discover/genres,
  /api/discover/browse — parcours filtré par la préférence du compte
- Warmup démarrage : latest_by_type()
2026-09-25 14:48:29 +00:00

283 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% set active = 'discover' %}
{% block title %}Découvrir — Ohm Stream{% endblock %}
{% block content %}
<h1 class="page-title">Découvrir</h1>
<p class="page-sub">Explore par genre, nouveautés de tes sources, incontournables et suggestions basées sur tes téléchargements.</p>
<div x-data="discoverPage()" x-init="load()" x-cloak>
<!-- ------------------------------------------------ Explorer par genre -->
<section class="rail-section explore-section" x-show="!loading && types.length > 0">
<h2 class="rail-title">🎭 Explorer
<span class="rail-source" x-text="exploreSubtitle"></span>
</h2>
<div class="explore-filters">
<div class="chip-row" role="tablist" aria-label="Type de contenu">
<template x-for="t in types" :key="t">
<button class="genre-chip" :class="{ active: exploreType === t }" role="tab"
:aria-selected="exploreType === t" @click="setType(t)"
x-text="typeLabels[t]"></button>
</template>
</div>
<div class="chip-row" role="tablist" aria-label="Genre" x-show="currentGenres.length > 0">
<template x-for="g in currentGenres" :key="g.key">
<button class="genre-chip" :class="{ active: exploreGenre === g.key }" role="tab"
:aria-selected="exploreGenre === g.key" @click="setGenre(g.key)"
x-text="g.label"></button>
</template>
</div>
</div>
<div class="rail-wrap" x-show="!exploreLoading">
<button class="rail-nav rail-prev" type="button" aria-label="Défiler à gauche"
@click="scrollRail($el, -1)">‹</button>
<div class="rail">
<template x-for="(item, i) in exploreItems" :key="'ex' + i + (item.source_id || item.kitsu_id || item.title)">
<a class="card rail-card" :style="`--i:${i}`" :href="exploreHref(item)"
:title="item.source ? item.title : `Rechercher « ${item.title} »`">
<img class="card-poster" :src="item.image_url || '/static/img/placeholder.svg'" :alt="item.title"
loading="lazy" onerror="this.src='/static/img/placeholder.svg'">
<span class="card-chip" x-show="item.label" x-text="item.label"></span>
<span class="card-chip" x-show="!item.label && item.rating" x-text="item.rating ? '★ ' + item.rating : ''"></span>
<div class="card-overlay">
<div class="card-title" x-text="item.title"></div>
<div class="card-meta">
<span class="badge badge-type" x-text="item.start_date ? item.start_date.slice(0, 4) : (item.year || '')"></span>
</div>
</div>
</a>
</template>
</div>
<button class="rail-nav rail-next" type="button" aria-label="Défiler à droite"
@click="scrollRail($el, 1)">›</button>
</div>
<div class="explore-skel" x-show="exploreLoading">
<div class="rail rail-skel">
<template x-for="i in 8" :key="'sk' + i"><div class="rail-card skel skel-card"></div></template>
</div>
</div>
</section>
<!-- ------------------------------------------------ Skeletons de chargement -->
<div x-show="loading">
<section class="rail-section" x-data="{ n: 8 }">
<div class="skel skel-title"></div>
<div class="rail rail-skel">
<template x-for="i in n"><div class="rail-card skel skel-card"></div></template>
</div>
</section>
<section class="rail-section" x-data="{ n: 8 }">
<div class="skel skel-title"></div>
<div class="rail rail-skel">
<template x-for="i in n"><div class="rail-card skel skel-card"></div></template>
</div>
</section>
</div>
<template x-if="error">
<div class="login-error" x-text="error"></div>
</template>
<!-- ------------------------------------------------ Nouveautés animés -->
<section class="rail-section" x-show="!loading && latestAnime.length > 0">
<h2 class="rail-title">🆕 Nouveautés animés <span class="rail-source">triées par date de sortie</span></h2>
<div class="rail-wrap">
<button class="rail-nav rail-prev" type="button" aria-label="Défiler à gauche"
@click="scrollRail($el, -1)">‹</button>
<div class="rail">
<template x-for="(item, i) in latestAnime" :key="item.source + item.source_id">
<a class="card rail-card" :style="`--i:${i}`" :href="`/title/${item.source}/${encodeURIComponent(item.source_id)}`">
<img class="card-poster" :src="item.image_url || '/static/img/placeholder.svg'" :alt="item.title"
loading="lazy" onerror="this.src='/static/img/placeholder.svg'">
<span class="card-chip" x-text="item.label"></span>
<span class="card-year" x-text="item.start_date ? item.start_date.slice(0, 4) : ''"></span>
<span class="card-airing" x-show="item.status === 'current'" title="En cours de diffusion">● EN COURS</span>
<div class="card-overlay">
<div class="card-title" x-text="item.title"></div>
<div class="card-meta">
<span class="badge badge-type" x-show="item.rating" x-text="item.rating ? '★ ' + item.rating : ''"></span>
</div>
</div>
</a>
</template>
</div>
<button class="rail-nav rail-next" type="button" aria-label="Défiler à droite"
@click="scrollRail($el, 1)">›</button>
</div>
</section>
<!-- ------------------------------------------------ Nouveautés séries & films -->
<section class="rail-section" x-show="!loading && latestSerie.length > 0">
<h2 class="rail-title">🆕 Nouveautés séries &amp; films <span class="rail-source">ajouts récents de French-Stream</span></h2>
<div class="rail-wrap">
<button class="rail-nav rail-prev" type="button" aria-label="Défiler à gauche"
@click="scrollRail($el, -1)">‹</button>
<div class="rail">
<template x-for="(item, i) in latestSerie" :key="item.source + item.source_id">
<a class="card rail-card" :style="`--i:${i}`" :href="`/title/${item.source}/${encodeURIComponent(item.source_id)}`">
<img class="card-poster" :src="item.image_url || '/static/img/placeholder.svg'" :alt="item.title"
loading="lazy" onerror="this.src='/static/img/placeholder.svg'">
<span class="card-chip" x-text="item.label"></span>
<span class="card-year" x-text="item.media_type === 'film' ? 'Film' : ''"></span>
<div class="card-overlay">
<div class="card-title" x-text="item.title"></div>
</div>
</a>
</template>
</div>
<button class="rail-nav rail-next" type="button" aria-label="Défiler à droite"
@click="scrollRail($el, 1)">›</button>
</div>
</section>
<!-- ------------------------------------------------ Incontournables -->
<section class="rail-section" x-show="!loading && mustWatch.length > 0">
<h2 class="rail-title">🔥 Incontournables animés <span class="rail-source">les classiques les mieux notés</span></h2>
<div class="rail-wrap">
<button class="rail-nav rail-prev" type="button" aria-label="Défiler à gauche"
@click="scrollRail($el, -1)">‹</button>
<div class="rail">
<template x-for="(item, i) in mustWatch" :key="'mw' + item.kitsu_id">
<a class="card rail-card" :style="`--i:${i}`" :href="`/?q=${encodeURIComponent(item.title)}`" :title="`Rechercher « ${item.title} »`">
<img class="card-poster" :src="item.image_url || '/static/img/placeholder.svg'" :alt="item.title"
loading="lazy" onerror="this.src='/static/img/placeholder.svg'">
<span class="card-chip" x-show="item.rating" x-text="item.rating ? '★ ' + item.rating : ''"></span>
<div class="card-overlay">
<div class="card-title" x-text="item.title"></div>
<div class="card-meta"><span class="badge badge-type" x-text="item.year || ''"></span></div>
</div>
</a>
</template>
</div>
<button class="rail-nav rail-next" type="button" aria-label="Défiler à droite"
@click="scrollRail($el, 1)">›</button>
</div>
</section>
<!-- ------------------------------------------------ Pour toi -->
<section class="rail-section" x-show="!loading && forYou.items.length > 0">
<h2 class="rail-title">✨ Pour toi
<span class="rail-source" x-show="forYou.based_on.length"
x-text="forYou.based_on.length ? 'parce que tu aimes : ' + forYou.based_on.join(', ') : ''"></span>
</h2>
<div class="rail-wrap">
<button class="rail-nav rail-prev" type="button" aria-label="Défiler à gauche"
@click="scrollRail($el, -1)">‹</button>
<div class="rail">
<template x-for="(item, i) in forYou.items" :key="'fy' + item.kitsu_id">
<a class="card rail-card" :style="`--i:${i}`" :href="`/?q=${encodeURIComponent(item.title)}`" :title="`Rechercher « ${item.title} »`">
<img class="card-poster" :src="item.image_url || '/static/img/placeholder.svg'" :alt="item.title"
loading="lazy" onerror="this.src='/static/img/placeholder.svg'">
<span class="card-chip" x-show="item.rating" x-text="item.rating ? '★ ' + item.rating : ''"></span>
<div class="card-overlay">
<div class="card-title" x-text="item.title"></div>
<div class="card-meta"><span class="badge badge-type" x-text="item.year || ''"></span></div>
</div>
</a>
</template>
</div>
<button class="rail-nav rail-next" type="button" aria-label="Défiler à droite"
@click="scrollRail($el, 1)">›</button>
</div>
</section>
<template x-if="!loading && !error && latestAnime.length === 0 && latestSerie.length === 0 && mustWatch.length === 0 && forYou.items.length === 0 && exploreItems.length === 0">
<div class="empty-state"><div class="big">🏜️</div>Rien à afficher pour le moment — réessaie plus tard.</div>
</template>
</div>
{% endblock %}
{% block scripts %}
<script>
function discoverPage() {
return {
latestAnime: [], latestSerie: [], mustWatch: [], forYou: { based_on: [], items: [] },
genres: {}, exploreType: null, exploreGenre: null,
exploreItems: [], exploreLoading: false,
loading: true, error: null,
typeLabels: { anime: '⛩ Animés', serie: '📺 Séries', film: '🎬 Films' },
get types() { return Object.keys(this.genres); },
get currentGenres() { return this.genres[this.exploreType] || []; },
get exploreSubtitle() {
const genre = (this.currentGenres.find(g => g.key === this.exploreGenre) || {}).label;
return genre ? `${this.typeLabels[this.exploreType] || ''} · ${genre}` : 'choisis un genre';
},
scrollRail(el, dir) {
const rail = el.closest('.rail-wrap').querySelector('.rail');
rail.scrollBy({ left: dir * rail.clientWidth * 0.85, behavior: 'smooth' });
},
exploreHref(item) {
return item.source
? `/title/${item.source}/${encodeURIComponent(item.source_id)}`
: `/?q=${encodeURIComponent(item.title)}`;
},
setType(type) {
this.exploreType = type;
this.exploreGenre = (this.genres[type][0] || {}).key || null;
this.browse();
},
setGenre(genre) {
this.exploreGenre = genre;
this.browse();
},
async browse() {
if (!this.exploreType || !this.exploreGenre) return;
this.exploreLoading = true;
const params = new URLSearchParams({ type: this.exploreType, genre: this.exploreGenre });
const url = `${location.pathname}?${params}`;
history.replaceState(null, '', url); // état partageable
try {
const res = await fetch(`/api/discover/browse?${params}`);
if (!res.ok) throw new Error('Erreur ' + res.status);
this.exploreItems = (await res.json()).items;
} catch (e) {
this.exploreItems = [];
console.error('Explorer :', e);
} finally {
this.exploreLoading = false;
}
},
async load() {
this.loading = true; this.error = null;
try {
const [discoverRes, genresRes] = await Promise.all([
fetch('/api/discover'), fetch('/api/discover/genres'),
]);
if (!discoverRes.ok) throw new Error('Erreur ' + discoverRes.status);
const data = await discoverRes.json();
this.latestAnime = data.latest_anime;
this.latestSerie = data.latest_serie;
this.mustWatch = data.must_watch;
this.forYou = data.for_you;
this.genres = await genresRes.json();
// État initial : paramètres d'URL (?t=&g=) sinon premier type/genre dispo
const params = new URLSearchParams(location.search);
let type = params.get('t');
if (!this.genres[type]) type = this.types[0];
let genre = params.get('g');
if (!(this.genres[type] || []).some(g => g.key === genre)) {
genre = ((this.genres[type] || [])[0] || {}).key;
}
this.exploreType = type || null;
this.exploreGenre = genre || null;
await this.browse();
} catch (e) {
this.error = e.message;
} finally {
this.loading = false;
}
},
};
}
</script>
{% endblock %}