9b12d06160
The Jinja2 namespace update was missing the _key mapping, causing 'str object has no attribute providers' error when rendering HTML search results.
165 lines
11 KiB
HTML
165 lines
11 KiB
HTML
{% set default_lang = settings.default_lang if settings else 'vostfr' %}
|
|
|
|
{% set _groups = namespace(items={}) %}
|
|
{% for pid, items in (results or {}).items() %}
|
|
{% for item in items %}
|
|
{% set _key = item.title | lower | trim %}
|
|
{% if _key not in _groups.items %}
|
|
{% set _ = _groups.items.update({_key: {
|
|
"title": item.title,
|
|
"cover": item.cover_image or (item.metadata.poster_image if item.metadata else "") or "",
|
|
"synopsis": (item.metadata.synopsis if item.metadata and item.metadata.synopsis else ""),
|
|
"rating": item.metadata.rating if item.metadata and item.metadata.rating else "",
|
|
"genres": item.metadata.genres if item.metadata and item.metadata.genres else [],
|
|
"providers": [{ "id": item.provider_id or pid, "url": item.url }]
|
|
}}) %}
|
|
{% else %}
|
|
{% set _existing = _groups.items[_key] %}
|
|
{% if not _existing.cover and item.cover_image %}
|
|
{% set _ = _existing.update({"cover": item.cover_image}) %}
|
|
{% endif %}
|
|
{% if not _existing.synopsis and item.metadata and item.metadata.synopsis %}
|
|
{% set _ = _existing.update({"synopsis": item.metadata.synopsis}) %}
|
|
{% endif %}
|
|
{% if not _existing.rating and item.metadata and item.metadata.rating %}
|
|
{% set _ = _existing.update({"rating": item.metadata.rating}) %}
|
|
{% endif %}
|
|
{% set _ = _existing["providers"].append({"id": item.provider_id or pid, "url": item.url}) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
|
|
<div class="flex flex-col gap-4" x-data="{ openDropdown: null }">
|
|
{% if _groups.items.values() | list | length > 0 %}
|
|
{% for group in _groups.items.values() | list %}
|
|
{% set first_url = group.providers[0].url %}
|
|
<div class="card bg-base-200 border border-base-300 hover:border-primary transition-colors">
|
|
<div class="card-body p-5 flex-row gap-5">
|
|
<!-- Poster -->
|
|
<figure class="w-28 shrink-0">
|
|
<a href="{{ first_url }}" target="_blank" rel="noopener">
|
|
<img src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}"
|
|
alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer"
|
|
class="rounded-lg w-full aspect-[2/3] object-cover"
|
|
onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;">
|
|
</a>
|
|
</figure>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 min-w-0 flex flex-col gap-2">
|
|
<!-- Title + rating -->
|
|
<div class="flex items-baseline gap-3">
|
|
<h3 class="card-title text-base truncate">{{ group.title }}</h3>
|
|
{% if group.rating %}
|
|
<span class="badge badge-warning badge-sm shrink-0"><i class="fas fa-star"></i> {{ group.rating }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if group.synopsis %}
|
|
<p class="text-sm text-base-content/60 line-clamp-3">{{ group.synopsis }}</p>
|
|
{% endif %}
|
|
|
|
{% if group.genres %}
|
|
<div class="flex flex-wrap gap-1">
|
|
{% for g in group.genres[:5] %}
|
|
<span class="badge badge-ghost badge-sm">{{ g }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Provider badges -->
|
|
<div class="flex flex-wrap gap-1.5">
|
|
{% for p in group.providers %}
|
|
<a href="{{ p.url }}" target="_blank" rel="noopener"
|
|
class="badge badge-primary badge-outline text-xs uppercase tracking-wider hover:bg-primary hover:text-primary-content hover:border-primary transition-colors cursor-pointer">
|
|
{{ p.id | upper }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="flex flex-wrap gap-2 mt-1">
|
|
<!-- Watch -->
|
|
<a href="{{ first_url }}" target="_blank" rel="noopener"
|
|
class="btn btn-sm btn-primary">
|
|
<i class="fas fa-play"></i> Regarder
|
|
</a>
|
|
|
|
<!-- Download dropdown -->
|
|
<div class="dropdown dropdown-end" @click.outside="openDropdown = null">
|
|
<div tabindex="0" role="button"
|
|
@click.stop="openDropdown = (openDropdown === '{{ first_url | urlencode }}') ? null : '{{ first_url | urlencode }}'"
|
|
x-ref="dlToggle-{{ loop.index0 }}">
|
|
<span class="btn btn-sm btn-success">
|
|
<i class="fas fa-arrow-down"></i> Télécharger <i class="fas fa-chevron-down text-[0.6rem] ml-1"></i>
|
|
</span>
|
|
</div>
|
|
<ul tabindex="0"
|
|
class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-300 rounded-xl w-64 border border-base-300 mt-2"
|
|
x-show="openDropdown === '{{ first_url | urlencode }}'"
|
|
x-transition:enter="transition ease-out duration-100"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95">
|
|
<!-- Full season -->
|
|
<li>
|
|
<button class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-base-200 transition-colors"
|
|
hx-post="/api/anime/download-season?url={{ first_url | urlencode }}&lang={{ default_lang }}"
|
|
hx-swap="none"
|
|
hx-on::before-request="this.querySelector('.dl-icon').classList.add('hidden'); this.querySelector('.dl-spinner').classList.remove('hidden')"
|
|
hx-on::after-request="if(event.detail.successful){showToast('Saison complète mise en file'); this.querySelector('.dl-icon').classList.remove('hidden'); this.querySelector('.dl-spinner').classList.add('hidden'); openDropdown = null}">
|
|
<span class="w-8 h-8 rounded-lg bg-success/20 text-success flex items-center justify-center shrink-0">
|
|
<i class="fas fa-layer-group dl-icon text-sm"></i>
|
|
<span class="loading loading-spinner loading-xs dl-spinner hidden"></span>
|
|
</span>
|
|
<div class="flex flex-col text-left">
|
|
<span class="text-sm font-medium">Saison complète</span>
|
|
<span class="text-xs text-base-content/50">Tous les épisodes d'un coup</span>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<div class="divider my-1 before:bg-base-content/10 after:bg-base-content/10"></div>
|
|
</li>
|
|
<!-- Choose episodes -->
|
|
<li>
|
|
<button class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-base-200 transition-colors"
|
|
hx-get="/api/anime/episodes?url={{ first_url | urlencode }}&lang={{ default_lang }}&html=1"
|
|
hx-target="#player-container"
|
|
hx-swap="innerHTML"
|
|
hx-on::after-request="openDropdown = null; document.getElementById('player-container').scrollIntoView({behavior: 'smooth'})">
|
|
<span class="w-8 h-8 rounded-lg bg-primary/20 text-primary flex items-center justify-center shrink-0">
|
|
<i class="fas fa-list-ol text-sm"></i>
|
|
</span>
|
|
<div class="flex flex-col text-left">
|
|
<span class="text-sm font-medium">Choisir des épisodes</span>
|
|
<span class="text-xs text-base-content/50">Sélectionnez ce que vous voulez</span>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Follow -->
|
|
<button class="btn btn-sm btn-accent btn-outline"
|
|
hx-post="/api/watchlist"
|
|
hx-vals='{"anime_url": "{{ first_url }}", "anime_title": "{{ group.title }}", "provider_id": "{{ group.providers[0].id }}", "lang": "{{ default_lang }}", "poster_image": "{{ group.cover }}"}'
|
|
hx-swap="none"
|
|
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i> Suivi';this.disabled=true;this.classList.remove('btn-accent','btn-outline');this.classList.add('btn-accent','btn-ghost','pointer-events-none')}">
|
|
<i class="fas fa-plus"></i> Suivre
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-20 text-base-content/40">
|
|
<i class="fas fa-search text-6xl mb-5 block opacity-20"></i>
|
|
<p>Aucun anime trouve pour votre recherche.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|