feat(e-pub): e-bog/lydbog fra et indsat link, med live-log, kø, timeout og /admin/epub
Claude-Session: https://claude.ai/code/session_01AUj1LSMmrowcomkn1ciAgj
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — E-bøger & lydbøger{% endblock %}
|
||||
{% block body %}
|
||||
<p><a href="/admin" class="wt-muted">← Admin</a></p>
|
||||
<h1>E-bøger & lydbøger</h1>
|
||||
<p class="wt-lead">Indstillinger for <a href="/e-pub">/e-pub</a>. Proxy-nøglen styres i <code>/etc/portal.env</code> og vises ikke her.</p>
|
||||
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
|
||||
|
||||
{% if tjek.mangler_filer or tjek.mangler_programmer or (settings.rp_required and not tjek.rp_noegle) %}
|
||||
<div class="err">
|
||||
{% if tjek.mangler_filer %}<p>Mangler i <code>{{ settings.tools_dir }}</code>: {{ tjek.mangler_filer | join(", ") }}</p>{% endif %}
|
||||
{% if tjek.mangler_programmer %}<p>Mangler på serveren: {{ tjek.mangler_programmer | join(", ") }}</p>{% endif %}
|
||||
{% if settings.rp_required and not tjek.rp_noegle %}<p>Proxy er påkrævet, men <code>RP_API_KEY</code> er ikke sat: alle jobs fejler.</p>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="wt-muted">✅ Værktøjer, programmer og proxy-nøgle er på plads.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/epub" style="margin-top:1rem;max-width:40rem;display:grid;gap:.8rem">
|
||||
<label>Hvem må bruge siden (Authentik-gruppe; tom = kun admins)
|
||||
<input class="wt-input" name="allowed_group" value="{{ settings.allowed_group }}"></label>
|
||||
<label>Samtidige jobs
|
||||
<input class="wt-input" type="number" name="max_concurrent_jobs" min="1" max="8" value="{{ settings.max_concurrent_jobs }}"></label>
|
||||
<label>Timeout pr. job (sekunder)
|
||||
<input class="wt-input" type="number" name="job_timeout_seconds" min="60" max="3600" value="{{ settings.job_timeout_seconds }}"></label>
|
||||
<label>Færdige filer slettes efter (sekunder, hvis ikke hentet)
|
||||
<input class="wt-input" type="number" name="output_ttl_seconds" min="60" max="86400" value="{{ settings.output_ttl_seconds }}"></label>
|
||||
<label>Værktøjsmappe
|
||||
<input class="wt-input" name="tools_dir" value="{{ settings.tools_dir }}" required></label>
|
||||
<label><input type="checkbox" name="rp_required" {% if settings.rp_required %}checked{% endif %}>
|
||||
Proxy påkrævet (job fejler i stedet for at gå direkte ud)</label>
|
||||
<p><button class="wt-btn wt-btn--primary" type="submit">Gem</button></p>
|
||||
</form>
|
||||
|
||||
<h2>Jobs lige nu</h2>
|
||||
{% if jobs %}
|
||||
<table>
|
||||
<thead><tr><th>Bruger</th><th>Tilstand</th><th>Fil</th><th>Besked</th></tr></thead>
|
||||
<tbody>
|
||||
{% for j in jobs %}
|
||||
<tr><td>{{ j.ejer }}</td><td>{{ j.tilstand }}</td>
|
||||
<td>{% if j.filnavn %}{{ j.filnavn }} ({{ human(j.stoerrelse) }}){% endif %}</td><td>{{ j.besked }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="wt-muted">Ingen.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}E-bog / lydbog{% endblock %}
|
||||
{% block body %}
|
||||
<h1>E-bog / lydbog</h1>
|
||||
<p class="wt-lead">Indsæt et link, så laver serveren en fil du kan hente: en e-bog (.epub) eller en lydbog (.m4b).
|
||||
Det kan tage fra få sekunder til et kvarter. Filen kan hentes én gang og slettes ellers af sig selv efter et stykke tid.</p>
|
||||
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
|
||||
|
||||
<form id="ep-form" method="post" action="/e-pub/start" autocomplete="off"
|
||||
style="margin-top:1.2rem;display:flex;gap:.6rem;flex-wrap:wrap;align-items:center">
|
||||
<input class="wt-input" name="link" id="ep-link" required maxlength="2048" placeholder="Indsæt link"
|
||||
autocapitalize="off" autocorrect="off" spellcheck="false" style="flex:1 1 22rem;min-width:0">
|
||||
<button class="wt-btn wt-btn--primary" type="submit" id="ep-knap">Hent</button>
|
||||
</form>
|
||||
<p id="ep-fejl" class="err" hidden></p>
|
||||
|
||||
<div id="ep-jobs" style="margin-top:1.4rem;display:grid;gap:1rem"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var TEKST = { koe: "⏳ I kø", koerer: "⚙️ Arbejder…", faerdig: "✅ Færdig", fejl: "⚠️ Fejlede",
|
||||
hentet: "Hentet (slettet fra serveren)", udloebet: "Udløbet (slettet fra serveren)" };
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
var timere = {};
|
||||
|
||||
function str(n) {
|
||||
var e = ["B", "KB", "MB", "GB"], i = 0;
|
||||
while (n >= 1024 && i < e.length - 1) { n /= 1024; i++; }
|
||||
return (i ? n.toFixed(1) : n) + " " + e[i];
|
||||
}
|
||||
|
||||
function kort(id) {
|
||||
var k = $("ep-" + id);
|
||||
if (k) return k;
|
||||
k = document.createElement("section");
|
||||
k.className = "wt-card"; k.id = "ep-" + id; k.style.padding = "1rem 1.2rem";
|
||||
k.innerHTML = '<p class="ep-tilstand" style="margin:0 0 .4rem;font-weight:600"></p>' +
|
||||
'<p class="ep-besked wt-muted" style="margin:0 0 .4rem"></p>' +
|
||||
'<p class="ep-hent" hidden style="margin:.4rem 0"><a class="wt-btn wt-btn--primary" href="/e-pub/hent/' + id + '">Hent fil</a></p>' +
|
||||
'<details><summary class="wt-muted" style="cursor:pointer">Log</summary>' +
|
||||
'<pre class="ep-log" style="max-height:16rem;overflow:auto;white-space:pre-wrap;font-size:.85em;margin:.4rem 0 0"></pre></details>';
|
||||
$("ep-jobs").insertBefore(k, $("ep-jobs").firstChild);
|
||||
k.querySelector(".ep-hent a").addEventListener("click", function () {
|
||||
// Filen slettes efter én hentning: knappen forsvinder, og status hentes igen om lidt.
|
||||
setTimeout(function () { hent(id); }, 4000);
|
||||
});
|
||||
return k;
|
||||
}
|
||||
|
||||
function vis(st) {
|
||||
var k = kort(st.id);
|
||||
var t = TEKST[st.tilstand] || st.tilstand;
|
||||
if (st.tilstand === "faerdig" && st.filnavn) t += " · " + st.filnavn + " (" + str(st.stoerrelse) + ")";
|
||||
k.querySelector(".ep-tilstand").textContent = t;
|
||||
k.querySelector(".ep-besked").textContent = st.besked || "";
|
||||
k.querySelector(".ep-hent").hidden = st.tilstand !== "faerdig";
|
||||
var log = k.querySelector(".ep-log"), nederst = log.scrollTop + log.clientHeight >= log.scrollHeight - 4;
|
||||
log.textContent = (st.log || []).join("\n");
|
||||
if (nederst) log.scrollTop = log.scrollHeight;
|
||||
if (st.tilstand === "koerer" || st.tilstand === "koe") k.querySelector("details").open = true;
|
||||
planlaeg(st.id, st.tilstand === "koe" ? 3000 : st.tilstand === "koerer" ? 1500 : st.tilstand === "faerdig" ? 30000 : 0);
|
||||
}
|
||||
|
||||
function planlaeg(id, ms) {
|
||||
clearTimeout(timere[id]);
|
||||
if (ms) timere[id] = setTimeout(function () { hent(id); }, ms);
|
||||
}
|
||||
|
||||
function hent(id) {
|
||||
fetch("/e-pub/status/" + id, { credentials: "same-origin", cache: "no-store" })
|
||||
.then(function (r) {
|
||||
if (r.status === 404) { var k = $("ep-" + id); if (k) k.remove(); return null; }
|
||||
return r.json();
|
||||
})
|
||||
.then(function (st) { if (st) vis(st); })
|
||||
.catch(function () { planlaeg(id, 5000); });
|
||||
}
|
||||
|
||||
$("ep-form").addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
$("ep-knap").disabled = true;
|
||||
$("ep-fejl").hidden = true;
|
||||
fetch("/e-pub/start", { method: "POST", credentials: "same-origin", headers: { Accept: "application/json" },
|
||||
body: new FormData($("ep-form")) })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (st) {
|
||||
if (st.error) { $("ep-fejl").textContent = st.error; $("ep-fejl").hidden = false; return; }
|
||||
$("ep-link").value = "";
|
||||
vis(st);
|
||||
})
|
||||
.catch(function () { $("ep-fejl").textContent = "Kunne ikke nå serveren."; $("ep-fejl").hidden = false; })
|
||||
.finally(function () { $("ep-knap").disabled = false; });
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden) Object.keys(timere).forEach(hent);
|
||||
});
|
||||
({{ jobs | tojson }}).reverse().forEach(vis);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user