Nouvelle version réécrite de zéro : recherche multi-sources (Vostfree, French-Manga), extraction 2 niveaux, proxy vidéo intégré, streaming/téléchargement HLS, métadonnées Kitsu, bibliothèque locale, comptes JWT + administration, découverte fusionnée, indexeur Torznab (Sonarr/Prowlarr).
67 lines
2.5 KiB
HTML
67 lines
2.5 KiB
HTML
{% extends "base.html" %}
|
|
{% set active = 'search' %}
|
|
{% block title %}Recherche — Ohm Stream{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1 class="page-title">Recherche unifiée</h1>
|
|
<p class="page-sub">Une seule requête interroge toutes tes sources activées.</p>
|
|
|
|
<div x-data="searchPage()">
|
|
<form class="search-bar" @submit.prevent="run">
|
|
<input type="search" x-model="query" placeholder="Naruto, One Piece, Frieren…" autofocus>
|
|
<button class="btn" type="submit" :disabled="loading">Rechercher</button>
|
|
</form>
|
|
|
|
<div class="spinner" x-show="loading"></div>
|
|
|
|
<template x-if="error">
|
|
<div class="login-error" x-text="error"></div>
|
|
</template>
|
|
|
|
<template x-if="!loading && searched && results.length === 0">
|
|
<div class="empty-state"><div class="big">🏜️</div>Aucun résultat pour cette recherche.</div>
|
|
</template>
|
|
|
|
<div class="grid" x-show="results.length > 0">
|
|
<template x-for="r in results" :key="r.source + r.source_id">
|
|
<a class="card" :href="`/title/${r.source}/${encodeURIComponent(r.source_id)}`">
|
|
<img class="card-poster" :src="r.image_url || '/static/img/placeholder.svg'" :alt="r.title" loading="lazy"
|
|
onerror="this.src='/static/img/placeholder.svg'">
|
|
<span class="card-chip" x-text="r.source"></span>
|
|
<div class="card-overlay">
|
|
<div class="card-title" x-text="r.title"></div>
|
|
<div class="card-meta">
|
|
<span class="badge badge-type" x-text="r.media_type"></span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function searchPage() {
|
|
return {
|
|
query: new URLSearchParams(location.search).get('q') || '',
|
|
results: [], loading: false, searched: false, error: null,
|
|
init() { if (this.query) this.run(); },
|
|
async run() {
|
|
if (this.query.trim().length < 2) return;
|
|
this.loading = true; this.error = null; this.searched = false;
|
|
history.replaceState(null, '', '?q=' + encodeURIComponent(this.query));
|
|
try {
|
|
const res = await fetch('/api/search?q=' + encodeURIComponent(this.query.trim()));
|
|
if (!res.ok) throw new Error('Erreur ' + res.status);
|
|
const data = await res.json();
|
|
this.results = data.results;
|
|
if (data.failed_sources.length) this.error = 'Sources en échec : ' + data.failed_sources.join(', ');
|
|
} catch (e) { this.error = e.message; }
|
|
finally { this.loading = false; this.searched = true; }
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|