@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<title>RadioSync Hub</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<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=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ink: #e9eef1;
|
||||
--muted: #9ca9b2;
|
||||
--canvas: #151a1e;
|
||||
--surface: #20272c;
|
||||
--surface-strong: #293238;
|
||||
--line: #3b474e;
|
||||
--cyan: #29d3e8;
|
||||
--yellow: #ffd166;
|
||||
--red: #ff6b6b;
|
||||
--green: #5ee0a0;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { background: var(--canvas); color: var(--ink); font-family: "DM Sans", sans-serif; letter-spacing: 0; }
|
||||
h1, h2, h3, h4, h5, .navbar-brand { font-family: "Space Grotesk", sans-serif; letter-spacing: 0; }
|
||||
.navbar { background: rgba(21, 26, 30, .96) !important; border-color: var(--line) !important; }
|
||||
.navbar-brand { color: var(--cyan) !important; font-weight: 700; }
|
||||
.navbar-brand-mark { color: var(--yellow); margin-right: .5rem; }
|
||||
.nav-link { color: var(--muted) !important; font-weight: 600; }
|
||||
.nav-link:hover, .nav-link.active { color: var(--cyan) !important; }
|
||||
.btn { border-radius: 7px; font-weight: 600; }
|
||||
.btn-primary { background: var(--cyan); border-color: var(--cyan); color: #102126; }
|
||||
.btn-primary:hover { background: #7ae7f2; border-color: #7ae7f2; color: #102126; }
|
||||
.btn-outline-info { color: var(--cyan); border-color: var(--cyan); }
|
||||
.btn-outline-warning { color: var(--yellow); border-color: var(--yellow); }
|
||||
.btn-light { background: var(--ink); border-color: var(--ink); color: #182025; }
|
||||
.btn-danger { background: transparent; color: var(--red); border-color: var(--red); }
|
||||
.surface { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.section-label { color: var(--muted); font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.eyebrow { color: var(--cyan); font-size: .8rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.page-title { font-size: clamp(1.65rem, 3vw, 2.35rem); margin: 0; }
|
||||
.log-box { height: 250px; overflow-y: auto; background: #111519; color: #b9c7ce; font-family: monospace; padding: 14px; font-size: 12px; border: 1px solid var(--line); border-radius: 8px; }
|
||||
.podcast-img { width: 64px; height: 64px; object-fit: cover; border-radius: 8px; }
|
||||
.form-control, .form-select { background: #181e22; border-color: var(--line); color: var(--ink); border-radius: 7px; }
|
||||
.form-control:focus, .form-select:focus { background: #181e22; border-color: var(--cyan); color: var(--ink); box-shadow: 0 0 0 .2rem rgba(41, 211, 232, .14); }
|
||||
.form-label { color: var(--muted); font-size: .82rem; font-weight: 600; }
|
||||
.text-muted { color: var(--muted) !important; }
|
||||
.table { --bs-table-bg: transparent; --bs-table-color: var(--ink); --bs-table-border-color: var(--line); }
|
||||
.table thead th { color: var(--muted); font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.badge { font-weight: 700; }
|
||||
.station-card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; height: 100%; }
|
||||
.station-card .card-header { background: var(--surface-strong); border-bottom: 1px solid var(--line); border-radius: 11px 11px 0 0; }
|
||||
.station-card .card-body { padding: 1rem 1.1rem; }
|
||||
.station-card .card-footer { border-top: 1px solid var(--line) !important; }
|
||||
.metric-label { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; white-space: nowrap; }
|
||||
.metric-value { font-size: 1.1rem; font-weight: 700; }
|
||||
.config-surface { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.config-surface .card-header { background: var(--surface-strong); border-color: var(--line); }
|
||||
.error-item { background: var(--surface) !important; border-width: 1px !important; }
|
||||
.error-item strong { font-family: "Space Grotesk", sans-serif; }
|
||||
.program-card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; }
|
||||
.program-card .card-body { padding: 1rem; }
|
||||
.program-title { font-size: 1.05rem; line-height: 1.2; }
|
||||
.program-card .card-footer { border-color: var(--line) !important; background: #1a2024 !important; }
|
||||
.episode-item { background: var(--surface) !important; border: 1px solid var(--line) !important; border-radius: 10px !important; }
|
||||
.episode-title { font-size: 1.05rem; }
|
||||
.page-toolbar { border-bottom: 1px solid var(--line); padding-bottom: 1rem; margin-bottom: 1.25rem; }
|
||||
.empty-state { color: var(--muted); padding: 2rem 0; }
|
||||
@media (max-width: 767.98px) {
|
||||
.container { padding-left: 18px; padding-right: 18px; }
|
||||
.navbar .container { padding-left: 18px; padding-right: 18px; }
|
||||
.page-title { font-size: 1.7rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/"><span class="navbar-brand-mark">●</span>RadioSync Hub</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="/programs/radio357">357</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/programs/rns">Nowy Świat</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/programs/jazz">Jazz FM</a></li>
|
||||
</ul>
|
||||
<div class="d-flex">
|
||||
<a href="/feeds/Podcasts.opml" class="btn btn-outline-warning btn-sm" target="_blank">Master OPML</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4 mb-5">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/programs/{{ program.station }}">{{ program.station }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ program.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% if msg %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<strong>Info:</strong> {{ msg }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="page-toolbar d-flex justify-content-between align-items-center gap-3">
|
||||
<div><div class="eyebrow mb-2">Odcinki</div><h1 class="page-title">{{ program.name }}</h1></div>
|
||||
<a href="/feeds/{{ program.station }}/{{ program.slug }}.xml" class="btn btn-warning" target="_blank">Skopiuj URL Feedu (RSS)</a>
|
||||
</div>
|
||||
|
||||
<div class="list-group">
|
||||
{% for ep in episodes %}
|
||||
<div class="list-group-item episode-item list-group-item-dark text-white mb-2">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="episode-title mb-1">{{ ep.title }}</h5>
|
||||
<small>{{ ep.pub_date }}</small>
|
||||
</div>
|
||||
<p class="mb-1 small">{{ (ep.description or '')[:200] }}{% if ep.description and ep.description|length > 200 %}...{% endif %}</p>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div>
|
||||
{% if ep.url %}
|
||||
<audio controls preload="none" style="height: 35px; width: 300px;">
|
||||
<source src="{{ ep.url }}" type="audio/mpeg">
|
||||
Twoja przeglądarka nie obsługuje tagu audio.
|
||||
</audio>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Brak pliku audio (lub oznaczony jako błąd)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if ep.url %}
|
||||
<form method="POST" action="/episodes/delete-url" style="margin:0;">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="ep_id" value="{{ ep.id }}">
|
||||
<button class="btn btn-danger btn-sm" onclick="return confirm('Czy na pewno chcesz usunąć ten link z bazy? Zostanie podjęta próba ponownego pobrania przy kolejnej synchronizacji.')">Usuń link audio</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,233 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% set is_running = (status.radio357.status == 'W trakcie') or (status.rns.status == 'W trakcie') or (status.jazz.status == 'W trakcie') %}
|
||||
{% if is_running %}
|
||||
<script>
|
||||
setTimeout(function() { window.location.reload(); }, 5000);
|
||||
</script>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if msg %}
|
||||
<div class="col-12 mb-3">
|
||||
<div class="alert alert-info alert-dismissible fade show m-0" role="alert">
|
||||
<strong>Info:</strong> {{ msg }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
|
||||
<div class="mb-4">
|
||||
<div class="eyebrow mb-2">Panel operacyjny</div>
|
||||
<h1 class="page-title">Status systemu</h1>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
{% for st, name in [('radio357', 'Radio 357'), ('rns', 'Radio Nowy Świat'), ('jazz', 'Radio Jazz')] %}
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card station-card text-white mb-3 h-100">
|
||||
<div class="card-header fw-bold">
|
||||
{{ name }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<span class="metric-label d-block mb-1">Status</span>
|
||||
{% if status[st].status == 'OK' %}<span class="badge bg-success">OK</span>
|
||||
{% elif status[st].status == 'Błąd' %}<span class="badge bg-danger">Błąd</span>
|
||||
{% elif status[st].status == 'W trakcie' %}
|
||||
<span class="badge bg-primary">W trakcie <span class="spinner-border spinner-border-sm" style="width: 10px; height: 10px;" role="status" aria-hidden="true"></span></span>
|
||||
{% if status[st].progress %}<br><small class="text-info mt-1 d-block">Zapytania: {{ status[st].progress }}</small>{% endif %}
|
||||
{% else %}<span class="badge bg-warning text-dark">{{ status[st].status }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><span class="metric-label d-block">Ostatni sync</span><span class="metric-value">{% if status[st].last_run %}{{ status[st].last_run.strftime('%d.%m.%Y %H:%M') }}{% else %}Nigdy{% endif %}</span></div>
|
||||
{% if st in ['radio357', 'rns'] %}
|
||||
<div class="col-6"><span class="metric-label d-block">Programy</span><span class="metric-value">{{ stats['r357_progs' if st == 'radio357' else 'rns_progs'] }}</span></div>
|
||||
<div class="col-6"><span class="metric-label d-block">Audio / odcinki</span><span class="metric-value">{{ stats['r357_audio_eps' if st == 'radio357' else 'rns_audio_eps'] }} / {{ stats['r357_eps' if st == 'radio357' else 'rns_eps'] }}</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer border-0 bg-transparent d-grid gap-2 pb-3">
|
||||
{% if st != 'jazz' %}
|
||||
<form method="POST" action="/test-auth" class="m-0">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="station" value="{{ st }}">
|
||||
<button class="btn btn-sm btn-outline-info w-100" title="Sprawdź czy sesja jest aktywna">Sprawdź status</button>
|
||||
</form>
|
||||
<form method="POST" action="/force-relogin" class="m-0">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="station" value="{{ st }}">
|
||||
<button class="btn btn-sm btn-outline-warning w-100" title="Wymuś nowe logowanie z API" onclick="return confirm('Czy na pewno chcesz wymusić nowe logowanie?')">Relogin</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="/force-sync" class="m-0">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="station" value="{{ st }}">
|
||||
<button class="btn btn-sm btn-light w-100" {% if status[st].status == 'W trakcie' %}disabled{% endif %}>
|
||||
{% if status[st].status == 'W trakcie' %}
|
||||
Pobieranie...
|
||||
{% else %}
|
||||
Sync
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% if status[st].status == 'W trakcie' %}
|
||||
<form method="POST" action="/stop-sync" class="m-0">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="station" value="{{ st }}">
|
||||
<button class="btn btn-sm btn-outline-danger w-100" onclick="return confirm('Zatrzymać synchronizację?')">
|
||||
⏹ Stop
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-5 mb-3">
|
||||
<h3 class="m-0">Błędy wymagające uwagi</h3>
|
||||
<span class="badge {% if pending_errors %}bg-danger{% else %}bg-success{% endif %}">
|
||||
{{ pending_errors|length }}
|
||||
</span>
|
||||
</div>
|
||||
{% if pending_errors %}
|
||||
<div class="list-group mb-4">
|
||||
{% for error in pending_errors %}
|
||||
<div class="list-group-item list-group-item-dark bg-secondary text-white {% if error.severity == 'warning' %}border-warning{% else %}border-danger{% endif %} mb-2 rounded">
|
||||
<div class="d-flex justify-content-between gap-3">
|
||||
<strong>{% if error.severity == 'warning' %}Ostrzeżenie{% else %}Błąd{% endif %}: {{ error.station }}</strong>
|
||||
<small>{{ error.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||
</div>
|
||||
<div class="mt-1">{{ error.message }}</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<small class="text-warning">Powtórzenia: {{ error.occurrences }}</small>
|
||||
<form method="POST" action="/errors/{{ error.id }}/acknowledge" class="m-0">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button class="btn btn-sm btn-outline-light">Potwierdź odczytanie</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success mb-4">Brak błędów wymagających odczytania.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if acknowledged_errors %}
|
||||
<details class="mb-4">
|
||||
<summary class="text-muted">Ostatnio odczytane błędy ({{ acknowledged_errors|length }})</summary>
|
||||
<div class="list-group mt-2">
|
||||
{% for error in acknowledged_errors %}
|
||||
<div class="list-group-item list-group-item-dark bg-secondary text-white border-secondary mb-1">
|
||||
<div class="d-flex justify-content-between gap-3">
|
||||
<strong>{{ error.station }}</strong>
|
||||
<small>{{ error.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||
</div>
|
||||
<div>{{ error.message }}</div>
|
||||
<small class="text-muted">Powtórzenia: {{ error.occurrences }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-5 mb-3">Ostatnie logi (z pamięci podręcznej)</h3>
|
||||
<div class="card bg-dark text-light border-secondary">
|
||||
<div class="card-body">
|
||||
<div id="log-container" class="log-box" style="max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
|
||||
{% for line in status.logs %}
|
||||
<div>{{ line }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card config-surface text-white h-100">
|
||||
<div class="card-header border-secondary">Konfiguracja Systemu</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="/config">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<h5 class="mb-3 text-info">Radio 357</h5>
|
||||
<div class="mb-2">
|
||||
<input type="email" name="r357_email" class="form-control form-control-sm" placeholder="Email (logowanie)" value="{{ config.r357_email or '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input type="password" name="r357_password" class="form-control form-control-sm" placeholder="Hasło (zostaw puste aby nie zmieniać)">
|
||||
</div>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Sync co (godzin)</label>
|
||||
<input type="number" name="r357_interval" class="form-control form-control-sm" value="{{ config.r357_interval or '6' }}">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Max odcinków backfill</label>
|
||||
<input type="number" name="r357_limit" class="form-control form-control-sm" value="{{ config.r357_limit or '600' }}">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Hard limit (all)</label>
|
||||
<input type="number" name="r357_hard_limit" class="form-control form-control-sm" value="{{ config.r357_hard_limit or '1000' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 text-info">Radio Nowy Świat</h5>
|
||||
<div class="mb-2">
|
||||
<input type="email" name="rns_email" class="form-control form-control-sm" placeholder="Email (logowanie)" value="{{ config.rns_email or '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input type="password" name="rns_password" class="form-control form-control-sm" placeholder="Hasło (zostaw puste aby nie zmieniać)">
|
||||
</div>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Sync co (godzin)</label>
|
||||
<input type="number" name="rns_interval" class="form-control form-control-sm" value="{{ config.rns_interval or '6' }}">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Max stron backfill</label>
|
||||
<input type="number" name="rns_limit" class="form-control form-control-sm" value="{{ config.rns_limit or '50' }}">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small text-muted m-0">Hard limit (all)</label>
|
||||
<input type="number" name="rns_hard_limit" class="form-control form-control-sm" value="{{ config.rns_hard_limit or '500' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 text-info">Radio Jazz</h5>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-12">
|
||||
<label class="form-label small text-muted m-0">Sync co (godzin)</label>
|
||||
<input type="number" name="jazz_interval" class="form-control form-control-sm" value="{{ config.jazz_interval or '24' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 mt-4 text-info">Powiadomienia ntfy.sh</h5>
|
||||
<div class="mb-2">
|
||||
<input type="text" name="ntfy_url" class="form-control form-control-sm" placeholder="Adres serwera (np. https://ntfy.sh)" value="{{ config.ntfy_url or '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input type="text" name="ntfy_topic" class="form-control form-control-sm" placeholder="Temat (Topic)" value="{{ config.ntfy_topic or '' }}">
|
||||
</div>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-6">
|
||||
<input type="text" name="ntfy_user" class="form-control form-control-sm" placeholder="Użytkownik (opcj.)" value="{{ config.ntfy_user or '' }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="password" name="ntfy_password" class="form-control form-control-sm" placeholder="Hasło (opcj.)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mt-4">
|
||||
<button type="submit" class="btn btn-primary">Zapisz konfigurację</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="POST" action="/test-ntfy" class="mt-2">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-outline-info btn-sm w-100">Testuj powiadomienie ntfy</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-toolbar d-flex justify-content-between align-items-center mt-2 gap-3">
|
||||
<div><div class="eyebrow mb-2">Biblioteka</div><h1 class="page-title">Programy: {{ station }}</h1></div>
|
||||
<a href="/feeds/{{ station }}.opml" class="btn btn-warning" target="_blank">OPML dla tej stacji</a>
|
||||
</div>
|
||||
|
||||
{% if msg %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<strong>Info:</strong> {{ msg }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3">
|
||||
{% for prog in programs %}
|
||||
<div class="col">
|
||||
<div class="card program-card h-100 text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
{% if prog.image %}
|
||||
<img src="{{ prog.image }}" class="podcast-img me-3" alt="Okładka">
|
||||
{% endif %}
|
||||
<h5 class="card-title program-title m-0">{{ prog.name }}</h5>
|
||||
</div>
|
||||
<p class="card-text small text-muted text-truncate mb-3">{{ prog.description or 'Brak opisu programu.' }}</p>
|
||||
<div class="mt-2 small">
|
||||
<div><strong>Ostatni sync:</strong> {{ prog.last_catchup_str }}</div>
|
||||
{% if station == 'rns' %}
|
||||
<div><strong>Status backfill:</strong>
|
||||
{% if prog.backfill_complete or (prog.total_pages > 0 and prog.backfill_page > prog.total_pages) %}
|
||||
<span class="text-success">✅ Zakończony</span>
|
||||
{% elif prog.backfill_page <= 2 %}
|
||||
{% if prog.total_eps > 0 %}
|
||||
<span class="text-info">Catch-up wykonany; od strony 2</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Nie rozpoczęty</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-warning">Strona {{ prog.backfill_page - 1 }} z {{ prog.total_pages if prog.total_pages > 0 else '?' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if station != 'jazz' %}
|
||||
<div><strong>Odcinki:</strong> {{ prog.total_eps }} (brak audio: {% if prog.missing_urls_count > 0 %}<span class="text-danger fw-bold">{{ prog.missing_urls_count }}</span>{% else %}0{% endif %})</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between bg-dark">
|
||||
{% if station != 'jazz' %}
|
||||
<a href="/episodes/{{ station }}/{{ prog.slug }}" class="btn btn-outline-light btn-sm">Odcinki</a>
|
||||
<form method="POST" action="/force-sync-program" style="margin:0;">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="station" value="{{ station }}">
|
||||
<input type="hidden" name="slug" value="{{ prog.slug }}">
|
||||
<button class="btn btn-outline-info btn-sm" onclick="this.disabled=true; this.form.submit();">Wymuś Sync</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="https://podkasty.radiojazz.fm/@{{ prog.slug }}/feed.xml" target="_blank" class="btn btn-outline-light btn-sm">Oryginalny RSS</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user