The Requests and Indexers cards sit side by side but read as two unrelated lists: requests led with a type badge, indexers with a status dot, and the rows drifted out of alignment once the share bar landed. Give a request row the same anatomy as an indexer row — status dot, title over a dimmed detail line, badge on the right — folding the type into the detail line and mapping request status onto the existing dot colours (a new .warn light covers pending). Both row types now share a --row-h floor so the two cards line up row for row; the share bar's top margin is trimmed so an indexer row reaches that height without overshooting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
530 lines
24 KiB
HTML
530 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Media Centre</title>
|
||
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
|
||
</head>
|
||
{#- Degraded service: human-readable cause, raw exception behind a
|
||
disclosure with any API key redacted. -#}
|
||
{% macro service_error(name, raw) %}
|
||
<div class="error" role="alert">
|
||
<div>{{ raw | friendly_error(name) }}</div>
|
||
{% if raw %}
|
||
<details class="error-detail">
|
||
<summary>Technical details</summary>
|
||
<pre>{{ raw | redact_secrets }}</pre>
|
||
</details>
|
||
{% endif %}
|
||
</div>
|
||
{% endmacro %}
|
||
|
||
{#- One service status pill in the header strip. -#}
|
||
{% macro svc_pill(key, label, url, up, stat) %}
|
||
<a class="svc-pill {{ 'up' if up else 'down' }}" id="svc-{{ key }}"
|
||
href="{{ url }}" target="_blank" rel="noopener">
|
||
<span class="status-light {{ 'up' if up else 'down' }}" id="svc-light-{{ key }}"></span>
|
||
<span class="svc-name">{{ label }}</span>
|
||
<span class="svc-stat" id="svc-stat-{{ key }}">{{ stat }}</span>
|
||
</a>
|
||
{% endmacro %}
|
||
<body data-timezone="{{ timezone }}" data-url-base="{{ request.script_root }}">
|
||
|
||
<header>
|
||
<div class="brand">
|
||
<svg width="30" height="30" viewBox="0 0 100 100" aria-hidden="true">
|
||
<rect width="100" height="100" rx="22" fill="var(--accent)"/>
|
||
<path d="M40 32 L72 50 L40 68 Z" fill="var(--bg)"/>
|
||
</svg>
|
||
<h1>Media Centre</h1>
|
||
</div>
|
||
|
||
<nav class="svc-strip" aria-label="Service status">
|
||
{{ svc_pill('sonarr', 'Sonarr', service_urls.sonarr, sonarr.status == 'ok',
|
||
(sonarr.total_series ~ ' series') if sonarr.status == 'ok' else 'offline') }}
|
||
{{ svc_pill('radarr', 'Radarr', service_urls.radarr, radarr.status == 'ok',
|
||
(radarr.total_movies ~ ' movies') if radarr.status == 'ok' else 'offline') }}
|
||
{{ svc_pill('prowlarr', 'Prowlarr', service_urls.prowlarr, prowlarr.status == 'ok',
|
||
(prowlarr.up_count ~ '/' ~ prowlarr.total ~ ' up') if prowlarr.status == 'ok' else 'offline') }}
|
||
{{ svc_pill('sabnzbd', 'SABnzbd', service_urls.sabnzbd, sabnzbd.status == 'ok',
|
||
sabnzbd.speed if sabnzbd.status == 'ok' else 'offline') }}
|
||
{{ svc_pill('tautulli', 'Tautulli', service_urls.tautulli, tautulli.status == 'ok',
|
||
((tautulli.active_streams ~ ' streaming') if tautulli.active_streams > 0 else '0 streams') if tautulli.status == 'ok' else 'offline') }}
|
||
{{ svc_pill('ombi', 'Ombi', service_urls.ombi, ombi.status == 'ok',
|
||
(ombi.total_pending ~ ' pending') if ombi.status == 'ok' else 'offline') }}
|
||
</nav>
|
||
|
||
<div class="header-right">
|
||
<div class="header-clock" id="header-clock">—</div>
|
||
<a href="{{ url_for('settings_page') }}" class="icon-btn" aria-label="Settings">
|
||
<span aria-hidden="true">⚙</span>
|
||
</a>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
|
||
<!-- ══ Right now — the time-sensitive row, above the fold ══ -->
|
||
<section class="section hero" aria-labelledby="h-now">
|
||
<h2 id="h-now">Right now</h2>
|
||
<div class="row cols-3">
|
||
|
||
<!-- Now playing -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.tautulli }}" target="_blank" rel="noopener">Now playing</a></h3>
|
||
<span class="card-meta" id="tautulli-count">
|
||
{%- if tautulli.status == 'ok' -%}
|
||
{{ tautulli.active_streams }} active
|
||
{%- endif -%}
|
||
</span>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if tautulli.status == 'ok' %}
|
||
<div id="now-playing" aria-live="polite">
|
||
{% if tautulli.sessions %}
|
||
{% for s in tautulli.sessions %}
|
||
<div class="stream">
|
||
<span class="live-dot"></span>
|
||
<span class="stream-icon" aria-hidden="true">{{ '🎬' if s.media_type == 'movie' else '📺' }}</span>
|
||
<span class="stream-info">
|
||
<span class="stream-title">{{ s.title }}</span>
|
||
<span class="stream-user">{{ s.user }}</span>
|
||
</span>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<p class="empty">Nothing streaming right now.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
{{ service_error("Tautulli", tautulli.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Downloads -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.sabnzbd }}" target="_blank" rel="noopener">Downloads</a></h3>
|
||
{% if sabnzbd.status == 'ok' %}
|
||
{% set st = sabnzbd.sab_status | lower %}
|
||
<span class="card-meta">
|
||
<span id="sab-badge" class="badge {{ 'downloading' if st == 'downloading' else ('paused' if st == 'paused' else 'idle') }}">{{ sabnzbd.sab_status }}</span>
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
{% if sabnzbd.status == 'ok' %}
|
||
<div aria-live="polite">
|
||
<div class="hero-figure" id="sab-speed">{{ sabnzbd.speed }}</div>
|
||
<div class="hero-sub">
|
||
<span id="sab-queue">{{ sabnzbd.queue_count }}</span> queued ·
|
||
<span id="sab-remaining">{{ sabnzbd.sizeleft }}</span> left
|
||
<span id="sab-timeleft-row" style="display:none;"> · ETA <span id="sab-timeleft"></span></span>
|
||
</div>
|
||
</div>
|
||
<dl class="stats" style="margin-top:0.9rem;">
|
||
<div><dt>Avg speed (7d)</dt><dd>{{ sabnzbd.avg_speed }}</dd></div>
|
||
<div><dt>Grabbed (7d)</dt><dd>{{ sabnzbd.completed_7d }}</dd></div>
|
||
<div><dt>Volume (7d)</dt><dd>{{ sabnzbd.week_size }}</dd></div>
|
||
</dl>
|
||
{% else %}
|
||
{{ service_error("SABnzbd", sabnzbd.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Storage -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.unraid }}" target="_blank" rel="noopener">Storage</a></h3>
|
||
{% if unraid.status == 'ok' and unraid.array_state %}
|
||
<span class="card-meta">{{ unraid.array_state | capitalize }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
{% if unraid.status == 'ok' %}
|
||
{% set pct = unraid.array_used_pct %}
|
||
<div class="hero-figure {% if pct > 85 %}is-danger{% elif pct > 70 %}is-warn{% endif %}"
|
||
style="color: {% if pct > 85 %}var(--danger){% elif pct > 70 %}var(--warn){% else %}var(--text){% endif %};">
|
||
{{ pct }}%
|
||
</div>
|
||
<div class="hero-sub">{{ unraid.array_free }} free of {{ unraid.array_total }}</div>
|
||
<div class="meter">
|
||
<div class="meter-fill {% if pct > 85 %}danger{% elif pct > 70 %}warn{% endif %}"
|
||
style="width: {{ pct }}%;"
|
||
role="progressbar" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"
|
||
aria-label="Array usage"></div>
|
||
</div>
|
||
<div class="meter-legend">
|
||
<span>{{ unraid.array_used }} used</span>
|
||
{% if unraid.disk_count %}<span>{{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}</span>{% endif %}
|
||
</div>
|
||
{% if unraid.source == "local" %}
|
||
<p class="empty" style="margin-bottom:0;">Container disk usage ({{ unraid.mount }}). Set <code>UNRAID_API_KEY</code> for full array stats.</p>
|
||
{% endif %}
|
||
{% else %}
|
||
{{ service_error("Unraid", unraid.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ Library ══ -->
|
||
<section class="section" aria-labelledby="h-lib">
|
||
<h2 id="h-lib">Library</h2>
|
||
<div class="row cols-2">
|
||
|
||
<!-- Sonarr -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.sonarr }}" target="_blank" rel="noopener">TV — Sonarr</a></h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="search-bar">
|
||
<label class="visually-hidden" for="sonarr-search-input">Search for a TV show</label>
|
||
<input type="text" id="sonarr-search-input" placeholder="Search and add a TV show…"
|
||
onkeydown="if(event.key==='Enter') searchSonarr()">
|
||
<button class="btn" id="sonarr-search-btn" onclick="searchSonarr()">Search</button>
|
||
</div>
|
||
<div id="sonarr-search-result" class="search-result">
|
||
<div class="search-result-info">
|
||
<div class="search-result-title" id="sonarr-result-title"></div>
|
||
<div class="search-result-meta" id="sonarr-result-meta"></div>
|
||
</div>
|
||
<button class="btn-add" id="sonarr-add-btn" onclick="confirmAddSonarr()">+ Add</button>
|
||
</div>
|
||
<div id="sonarr-search-msg" class="search-msg" style="display:none;" role="status"></div>
|
||
{% if sonarr.status == 'ok' %}
|
||
<dl class="stats">
|
||
<div><dt>Series</dt><dd>{{ sonarr.total_series }}</dd></div>
|
||
<div><dt>Missing episodes</dt><dd class="{{ 'is-warn' if sonarr.missing_episodes else '' }}">{{ sonarr.missing_episodes }}</dd></div>
|
||
</dl>
|
||
{% else %}
|
||
{{ service_error("Sonarr", sonarr.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Radarr -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.radarr }}" target="_blank" rel="noopener">Movies — Radarr</a></h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="search-bar">
|
||
<label class="visually-hidden" for="radarr-search-input">Search for a movie</label>
|
||
<input type="text" id="radarr-search-input" placeholder="Search and add a movie…"
|
||
onkeydown="if(event.key==='Enter') searchRadarr()">
|
||
<button class="btn" id="radarr-search-btn" onclick="searchRadarr()">Search</button>
|
||
</div>
|
||
<div id="radarr-search-result" class="search-result">
|
||
<div class="search-result-info">
|
||
<div class="search-result-title" id="radarr-result-title"></div>
|
||
<div class="search-result-meta" id="radarr-result-meta"></div>
|
||
</div>
|
||
<button class="btn-add" id="radarr-add-btn" onclick="confirmAddRadarr()">+ Add</button>
|
||
</div>
|
||
<div id="radarr-search-msg" class="search-msg" style="display:none;" role="status"></div>
|
||
{% if radarr.status == 'ok' %}
|
||
<dl class="stats">
|
||
<div><dt>Movies</dt><dd>{{ radarr.total_movies }}</dd></div>
|
||
<div><dt>Missing</dt><dd class="{{ 'is-warn' if radarr.missing_movies else '' }}">{{ radarr.missing_movies }}</dd></div>
|
||
</dl>
|
||
{% else %}
|
||
{{ service_error("Radarr", radarr.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ Discover ══ -->
|
||
<section class="section" aria-labelledby="h-disc">
|
||
<h2 id="h-disc">Discover — not in your library yet</h2>
|
||
<div class="row cols-2">
|
||
|
||
<!-- Popular TV -->
|
||
<div class="card">
|
||
<div class="card-header"><h3>Popular TV on Rotten Tomatoes</h3></div>
|
||
<div class="card-body">
|
||
{% if rt_tv and rt_tv[0] is defined and rt_tv[0].error is defined %}
|
||
<p class="empty">Couldn't load TV recommendations.</p>
|
||
{% elif rt_tv %}
|
||
<div class="rt-list" id="rt-tv-list">
|
||
{% for show in rt_tv %}
|
||
<div class="rt-item {{ 'is-hidden' if loop.index > 5 }}" data-rt-title="{{ show.title }}">
|
||
<span class="rt-rank">{{ loop.index }}</span>
|
||
{% if show.poster %}<img class="rt-poster" src="{{ show.poster }}" alt="" loading="lazy">
|
||
{% else %}<span class="rt-poster"></span>{% endif %}
|
||
<div class="rt-info">
|
||
<div class="rt-title">{{ show.title }}{% if show.year %} <span class="rt-year">({{ show.year }})</span>{% endif %}</div>
|
||
<div class="rt-meta">
|
||
{% if show.certified %}<span class="score certified">Certified Fresh</span>
|
||
{% elif show.critics_score %}<span class="score critics">{{ show.critics_score }}% critics</span>{% endif %}
|
||
{% if show.audience_score %}<span class="score audience">{{ show.audience_score }} audience</span>{% endif %}
|
||
</div>
|
||
<p class="rt-desc">{{ show.description[:180] ~ '…' if show.description and show.description|length > 180 else (show.description or '') }}</p>
|
||
</div>
|
||
<button class="btn-add" data-title="{{ show.title }}" data-target="sonarr" onclick="armAdd(this)">+ Add</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if rt_tv | length > 5 %}
|
||
<button class="btn btn-quiet show-more" onclick="toggleMore(this, 'rt-tv-list')"
|
||
aria-expanded="false">Show {{ rt_tv | length - 5 }} more</button>
|
||
{% endif %}
|
||
{% else %}
|
||
<p class="empty">No TV recommendations available.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Top box office -->
|
||
<div class="card">
|
||
<div class="card-header"><h3>Top box office on Rotten Tomatoes</h3></div>
|
||
<div class="card-body">
|
||
{% if rt_movies and rt_movies[0] is defined and rt_movies[0].error is defined %}
|
||
<p class="empty">Couldn't load movie recommendations.</p>
|
||
{% elif rt_movies %}
|
||
<div class="rt-list" id="rt-movie-list">
|
||
{% for movie in rt_movies %}
|
||
<div class="rt-item {{ 'is-hidden' if loop.index > 5 }}" data-rt-title="{{ movie.title }}">
|
||
<span class="rt-rank">{{ loop.index }}</span>
|
||
{% if movie.poster %}<img class="rt-poster" src="{{ movie.poster }}" alt="" loading="lazy">
|
||
{% else %}<span class="rt-poster"></span>{% endif %}
|
||
<div class="rt-info">
|
||
<div class="rt-title">{{ movie.title }}{% if movie.year %} <span class="rt-year">({{ movie.year }})</span>{% endif %}</div>
|
||
<div class="rt-meta">
|
||
{% if movie.certified %}<span class="score certified">Certified Fresh</span>
|
||
{% elif movie.critics_score %}<span class="score critics">{{ movie.critics_score }}% critics</span>{% endif %}
|
||
{% if movie.audience_score %}<span class="score audience">{{ movie.audience_score }} audience</span>{% endif %}
|
||
</div>
|
||
<p class="rt-desc">{{ movie.description[:180] ~ '…' if movie.description and movie.description|length > 180 else (movie.description or '') }}</p>
|
||
</div>
|
||
<button class="btn-add" data-title="{{ movie.title }}" data-target="radarr" onclick="armAdd(this)">+ Add</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if rt_movies | length > 5 %}
|
||
<button class="btn btn-quiet show-more" onclick="toggleMore(this, 'rt-movie-list')"
|
||
aria-expanded="false">Show {{ rt_movies | length - 5 }} more</button>
|
||
{% endif %}
|
||
{% else %}
|
||
<p class="empty">No movie recommendations available.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ Requests + Indexers ══ -->
|
||
<section class="section" aria-labelledby="h-ops">
|
||
<h2 id="h-ops">Requests & indexers</h2>
|
||
<div class="row cols-2 align-start">
|
||
|
||
<!-- Ombi -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.ombi }}" target="_blank" rel="noopener">Requests — Ombi</a></h3>
|
||
{% if ombi.status == 'ok' %}
|
||
<span class="card-meta">{{ ombi.total_pending }} pending of {{ ombi.total_requests }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
{% if ombi.status == 'ok' %}
|
||
{% if ombi.recent %}
|
||
<div class="req-list" id="req-list">
|
||
{% for req in ombi.recent %}
|
||
{# Same anatomy as an indexer row: status dot, name over a
|
||
dimmed detail line, status badge on the right. #}
|
||
{% set st = req.status | lower %}
|
||
<div class="req-item {{ 'is-hidden' if loop.index > 5 }}">
|
||
<span class="status-light {{ 'up' if st in ('available', 'approved')
|
||
else ('down' if st == 'denied' else 'warn') }}"></span>
|
||
<div class="req-info">
|
||
<div class="req-title">{{ req.title }}</div>
|
||
<div class="req-sub"><span class="req-type">{{ req.type }}</span> · {{ req.user }} · {{ req.date_display }}</div>
|
||
</div>
|
||
<span class="badge {{ st }}">{{ req.status }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if ombi.recent | length > 5 %}
|
||
<button class="btn btn-quiet show-more" onclick="toggleMore(this, 'req-list')"
|
||
aria-expanded="false">Show {{ ombi.recent | length - 5 }} more</button>
|
||
{% endif %}
|
||
{% else %}
|
||
<p class="empty">No recent requests.</p>
|
||
{% endif %}
|
||
{% else %}
|
||
{{ service_error("Ombi", ombi.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Prowlarr -->
|
||
<div class="card" id="prowlarr-card">
|
||
<div class="card-header">
|
||
<h3><a href="{{ service_urls.prowlarr }}" target="_blank" rel="noopener">Indexers — Prowlarr</a></h3>
|
||
{% if prowlarr.status == 'ok' %}
|
||
<span class="card-meta">
|
||
<span id="prowlarr-up-count">{{ prowlarr.up_count }}</span>/<span id="prowlarr-total">{{ prowlarr.total }}</span> online
|
||
{%- if prowlarr.has_stats %} · share of {{ prowlarr.share_metric }}, {{ prowlarr.stats_days }}d{% endif %}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body" id="prowlarr-body">
|
||
{% if prowlarr.status == 'ok' %}
|
||
{# Bars are scaled against the busiest indexer, not against 100%,
|
||
so a spread of single-digit shares is still readable. The label
|
||
beside each bar stays the true percentage. #}
|
||
{% set top_share = prowlarr.indexers[0].share if prowlarr.indexers else 0 %}
|
||
<div class="indexer-grid" id="indexer-grid">
|
||
{% for idx in prowlarr.indexers %}
|
||
{% set share = idx.share %}
|
||
<div class="indexer-item {{ 'up' if idx.up else 'down' }}" id="idx-{{ idx.id }}">
|
||
<span class="status-light {{ 'up' if idx.up else 'down' }}" id="idx-light-{{ idx.id }}"></span>
|
||
<div class="indexer-info">
|
||
<div class="indexer-name" title="{{ idx.name }}">{{ idx.name }}</div>
|
||
<div class="indexer-sub">
|
||
<span class="proto">{{ idx.protocol }}</span>{% if prowlarr.has_stats %} · {{ '{:,}'.format(idx.grabs) }} grabs · {{ '{:,}'.format(idx.queries) }} queries{% endif %}
|
||
</div>
|
||
{% if prowlarr.has_stats %}
|
||
<div class="share-bar" role="img"
|
||
aria-label="{{ share }}% of {{ prowlarr.share_metric }} in the last {{ prowlarr.stats_days }} days">
|
||
<span style="width: {{ (share / top_share * 100) | round(1) if top_share else 0 }}%"></span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% if prowlarr.has_stats %}
|
||
<span class="share-pct">{{ share }}%</span>
|
||
{% endif %}
|
||
<span class="badge {{ 'online' if idx.up else 'offline' }}"
|
||
id="idx-badge-{{ idx.id }}">{{ 'Online' if idx.up else 'Offline' }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
{{ service_error("Prowlarr", prowlarr.error) }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ Insights ══ -->
|
||
{% if tautulli.status == 'ok' %}
|
||
<section class="section" aria-labelledby="h-ins">
|
||
<h2 id="h-ins">Watch insights — last 30 days</h2>
|
||
<div class="row cols-2">
|
||
|
||
<div class="card">
|
||
<div class="card-header"><h3>Most played</h3></div>
|
||
<div class="card-body">
|
||
{% if tautulli.top_played %}
|
||
<div class="top-list">
|
||
{% for item in tautulli.top_played %}
|
||
<div class="top-item">
|
||
<span class="top-rank">{{ loop.index }}</span>
|
||
<div class="top-info">
|
||
<div class="top-name">{{ item.title }}</div>
|
||
<div class="top-sub">{{ item.user }}</div>
|
||
</div>
|
||
{% set mt = item.media_type | lower %}
|
||
<span class="badge {{ 'tv' if mt == 'episode' else 'movie' }}">{{ 'TV' if mt == 'episode' else ('Movie' if mt == 'movie' else mt or '?') }}</span>
|
||
<span class="top-plays">{{ item.plays }}×</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<p class="empty">No play history yet.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header"><h3>Top viewers</h3></div>
|
||
<div class="card-body">
|
||
{% if tautulli.top_users %}
|
||
<div class="top-list">
|
||
{% for u in tautulli.top_users %}
|
||
<div class="top-item">
|
||
<span class="top-rank">{{ loop.index }}</span>
|
||
<div class="top-info">
|
||
<div class="top-name">{{ u.username }}</div>
|
||
</div>
|
||
<span class="top-plays">{{ u.plays }} plays</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<p class="empty">No viewer data yet.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{% if tautulli.top_tv_list or tautulli.top_movie_list %}
|
||
<div class="row cols-2" style="margin-top: var(--gap);">
|
||
<div class="card">
|
||
<div class="card-header"><h3>Top TV</h3></div>
|
||
<div class="card-body">
|
||
{% if tautulli.top_tv_list %}
|
||
<div class="top-list">
|
||
{% for item in tautulli.top_tv_list %}
|
||
<div class="top-item">
|
||
<span class="top-rank">{{ loop.index }}</span>
|
||
<div class="top-info"><div class="top-name">{{ item.title }}</div></div>
|
||
<span class="top-plays">{{ item.plays }} plays</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}<p class="empty">No data yet.</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header"><h3>Top movies</h3></div>
|
||
<div class="card-body">
|
||
{% if tautulli.top_movie_list %}
|
||
<div class="top-list">
|
||
{% for item in tautulli.top_movie_list %}
|
||
<div class="top-item">
|
||
<span class="top-rank">{{ loop.index }}</span>
|
||
<div class="top-info"><div class="top-name">{{ item.title }}</div></div>
|
||
<span class="top-plays">{{ item.plays }} plays</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}<p class="empty">No data yet.</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</section>
|
||
{% endif %}
|
||
|
||
</main>
|
||
|
||
<footer>
|
||
Media Centre
|
||
<span class="version">v{{ app_version }} · {{ app_version_date }}</span>
|
||
</footer>
|
||
|
||
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||
|
||
</body>
|
||
</html>
|