filesystem-based statuscheck, merged state fixes

feature/pipeline-finalization
peter.fong 3 days ago
parent 3a7cc7687c
commit 65842505b0

@ -358,28 +358,14 @@ from db.repository import get_book_state
@app.route("/inspect/statuscheck/<book_idx>", methods=["POST"]) @app.route("/inspect/statuscheck/<book_idx>", methods=["POST"])
@logcall
def inspect_statuscheck(book_idx): def inspect_statuscheck(book_idx):
try: try:
statuscheck_result = StatusCheckService.run(book_idx) StatusCheckService.run(book_idx)
return ("", 204) # background action, geen UI
repo_state = get_book_state(book_idx)
return render_template(
"inspect/statuscheck_result.html",
result=statuscheck_result,
repo_state=repo_state,
)
except Exception as e: except Exception as e:
log(f"[STATUSCHECK] ERROR book_idx={book_idx}: {e}") log(f"[STATUSCHECK] ERROR book_idx={book_idx}: {e}")
return ( return jsonify({"error": str(e)}), 500
render_template(
"inspect/statuscheck_result.html",
error=str(e),
book_idx=book_idx,
),
500,
)
# ===================================================== # =====================================================

@ -373,5 +373,18 @@ def get_book_state(book_idx):
state["downloaded"] = merged_downloaded state["downloaded"] = merged_downloaded
state["audio_done"] = merged_audio_done state["audio_done"] = merged_audio_done
state["chapters_total"] = chapters_total state["chapters_total"] = chapters_total
# ----------------------------------------------------
# 4b. Derive status (READ-ONLY)
# ----------------------------------------------------
derived_status = sqlite_row.get("status") or "unknown"
if chapters_total > 0:
if merged_downloaded < chapters_total:
derived_status = "downloading"
elif merged_downloaded == chapters_total and merged_audio_done < chapters_total:
derived_status = "audio"
elif merged_audio_done == chapters_total:
derived_status = "done"
state["status"] = derived_status
return state return state

@ -5,7 +5,6 @@
import os import os
import stat import stat
from logbus.publisher import log from logbus.publisher import log
from scraper.logger_decorators import logcall from scraper.logger_decorators import logcall
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates") TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
@ -45,26 +44,40 @@ def detect_volumes(book_base: str):
# ------------------------------------------------------------ # ------------------------------------------------------------
def build_merge_block(title: str, author: str, volumes): def build_merge_block(title: str, author: str, volumes):
lines = [] lines = []
# --------------------------------------------------------
# Normalize input (defensive)
# --------------------------------------------------------
title = (title or "").strip()
author = (author or "").strip()
total_vols = len(volumes) total_vols = len(volumes)
# Padding-regel:
# - altijd minimaal 2 (01, 02)
# - 3 bij >=100
if total_vols >= 100: if total_vols >= 100:
pad = 3 pad = 3
elif total_vols >= 10:
pad = 2
else: else:
pad = 0 pad = 2
for num, dirname in volumes: for num, dirname in volumes:
if pad > 0: vol_num = f"{num:0{pad}d}" # voor filename
vol_num = f"{num:0{pad}d}" series_part = f"{num:0{pad}d}" # voor series-part (string!)
else:
vol_num = str(num)
line = ( line = (
f'm4b-tool merge --jobs=4 --writer="{author}" ' f"m4b-tool merge --jobs=4 "
f'--albumartist="{author}" --album="{title}" ' f'--writer="{author}" '
f'--name="{title}" --output-file="{title}-{vol_num}.m4b" ' f'--sortalbum="{title}" '
f'--albumartist="{author}" '
f'--album="{title}" '
f'--name="{title}" '
f'--series="{title}" '
f'--series-part="{series_part}" '
f'--output-file="{title}-{vol_num}.m4b" '
f'"{dirname}" -vvv' f'"{dirname}" -vvv'
) )
lines.append(line) lines.append(line)
if not lines: if not lines:
@ -76,7 +89,14 @@ def build_merge_block(title: str, author: str, volumes):
# ------------------------------------------------------------ # ------------------------------------------------------------
# Main generator # Main generator
# ------------------------------------------------------------ # ------------------------------------------------------------
@logcall
def generate_all_scripts(book_base: str, title: str, author: str): def generate_all_scripts(book_base: str, title: str, author: str):
# --------------------------------------------------------
# Defensive normalize
# --------------------------------------------------------
title = (title or "").strip()
author = (author or "").strip()
log(f"[SCRIPTGEN] Generating scripts in {book_base}") log(f"[SCRIPTGEN] Generating scripts in {book_base}")
# Load templates # Load templates

@ -1,8 +1,11 @@
/* ======================================================================= /* =======================================================================
File: static/css/bookcard.css File: static/css/bookcard.css
Purpose: Purpose:
All styling for registered book cards (book-card) + Styling voor registered book cards:
status colors + start/abort buttons + progress bars - status kleuren
- badges
- start/abort/statuscheck
- progress bars
======================================================================= */ ======================================================================= */
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
@ -17,7 +20,7 @@
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
BOOK CARD BOOK CARD BASE
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.book-card { .book-card {
@ -36,34 +39,28 @@
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
STATUS COLORS STATUS COLORS (BOOK CARD BORDER)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.book-card.processing { /* Downloading / actief bezig */
border-color: #007aff;
box-shadow: 0 0 6px rgba(0, 122, 255, 0.35);
}
.book-card.downloading { .book-card.downloading {
border-color: #ff9500; border-color: #ff9500;
box-shadow: 0 0 6px rgba(255, 149, 0, 0.35); box-shadow: 0 0 6px rgba(255, 149, 0, 0.35);
} }
.book-card.parsing { /* Audio fase */
border-color: #ffcc00;
box-shadow: 0 0 6px rgba(255, 204, 0, 0.35);
}
.book-card.audio { .book-card.audio {
border-color: #34c759; border-color: #ffca28;
box-shadow: 0 0 6px rgba(52, 199, 89, 0.35); box-shadow: 0 0 6px rgba(255, 202, 40, 0.35);
} }
.book-card.completed { /* Volledig klaar */
border-color: #34c759; .book-card.done {
box-shadow: 0 0 6px rgba(52, 199, 89, 0.35); border: 2px solid #4caf50;
box-shadow: 0 0 6px rgba(76, 175, 80, 0.35);
} }
/* Afgebroken */
.book-card.aborted { .book-card.aborted {
border-color: #ff3b30; border-color: #ff3b30;
box-shadow: 0 0 6px rgba(255, 59, 48, 0.35); box-shadow: 0 0 6px rgba(255, 59, 48, 0.35);
@ -188,6 +185,21 @@
background: #555; background: #555;
} }
/* Statuscheck */
.statuscheck-btn {
background-color: #444;
color: #fff;
border: 1px solid #666;
margin-left: 4px;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
}
.statuscheck-btn:hover {
background-color: #333;
}
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
PROGRESS (FULL WIDTH) PROGRESS (FULL WIDTH)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
@ -201,7 +213,7 @@
} }
.progress-row { .progress-row {
margin-bottom: 2px; margin-bottom: 4px;
} }
.progress-label { .progress-label {
@ -225,12 +237,12 @@
transition: width 0.4s ease; transition: width 0.4s ease;
} }
/* Download = blauw */ /* Download */
.progressbar-fill.download { .progressbar-fill.download {
background: #2196f3; background: #2196f3;
} }
/* Audio = groen */ /* Audio */
.progressbar-fill.audio { .progressbar-fill.audio {
background: #4caf50; background: #4caf50;
} }
@ -249,9 +261,50 @@
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
pointer-events: none; pointer-events: none;
} }
.statuscheck-btn {
background-color: #444; /* -----------------------------------------------------------------------
color: #fff; STATUS BADGE
border: 1px solid #666; ----------------------------------------------------------------------- */
margin-left: 4px;
.status-badge {
display: inline-block;
margin-bottom: 6px;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: default;
}
/* DONE */
.status-badge.status-done {
background-color: #e6f4ea;
color: #2e7d32;
border: 1px solid #4caf50;
}
/* AUDIO */
.status-badge.status-audio {
background-color: #fff8e1;
color: #8d6e00;
border: 1px solid #ffca28;
}
/* DOWNLOADING */
.status-badge.status-downloading {
background-color: #e3f2fd;
color: #1565c0;
border: 1px solid #42a5f5;
}
/* Statuscheck */
.icon-statuscheck {
background: #444;
}
.icon-statuscheck:hover {
background: #333;
transform: scale(1.05);
} }

@ -52,6 +52,7 @@ function updateSingleBookCard(card, state) {
console.log("[BOOKCARD] updateSingleBookCard", state.book_idx); console.log("[BOOKCARD] updateSingleBookCard", state.book_idx);
updateStatus(card, state); updateStatus(card, state);
updateStatusBadge(card, state);
updateButtons(card, state); updateButtons(card, state);
updateProgress(card, state); updateProgress(card, state);
} }
@ -64,6 +65,21 @@ function updateStatus(card, state) {
console.log("[BOOKCARD][STATUS]", state.book_idx, "→", state.status); console.log("[BOOKCARD][STATUS]", state.book_idx, "→", state.status);
card.className = `book-card ${state.status || ""}`; card.className = `book-card ${state.status || ""}`;
} }
function updateStatusBadge(card, state) {
const badge = card.querySelector(".status-badge");
if (!badge) return;
const status = (state.status || "").toLowerCase();
badge.textContent = status.toUpperCase();
badge.className = `status-badge status-${status}`;
badge.title =
{
downloading: "Bezig met downloaden",
audio: "Downloads compleet, audio wordt gegenereerd",
done: "Alle chapters en audio zijn compleet",
}[status] || "";
}
/* ============================================================ /* ============================================================
BUTTONS BUTTONS

@ -31,6 +31,20 @@ component) ============================================================ #}
<!-- META --> <!-- META -->
<div class="book-meta"> <div class="book-meta">
<!-- STATUS BADGE -->
{% if b.status %}
<span
class="status-badge status-{{ b.status }}"
title="
{% if b.status == 'done' %}Alle chapters en audio zijn compleet{% endif %}
{% if b.status == 'audio' %}Downloads compleet, audio wordt nog gegenereerd{% endif %}
{% if b.status == 'downloading' %}Bezig met downloaden{% endif %}
"
>
{{ b.status | upper }}
</span>
{% endif %}
<div class="book-title" data-field="title">{{ b.title }}</div> <div class="book-title" data-field="title">{{ b.title }}</div>
<div class="book-author" data-field="author">{{ b.author }}</div> <div class="book-author" data-field="author">{{ b.author }}</div>
<div class="book-created"> <div class="book-created">
@ -62,10 +76,10 @@ component) ============================================================ #}
> >
<button <button
type="submit" type="submit"
class="statuscheck-btn" class="icon-btn icon-statuscheck"
title="Herbereken status op basis van bestanden" title="Herbereken status op basis van bestanden"
> >
Statuscheck <i class="fa-solid fa-magnifying-glass-chart"></i>
</button> </button>
</form> </form>
</div> </div>

@ -22,25 +22,11 @@
<hr /> <hr />
{% include "components/registered_books.html" %}
<hr />
<!-- =========================================================== <!-- ===========================================================
BOOK LIST BOOK LIST
=========================================================== --> =========================================================== -->
<section class="dashboard-section">
<h2>Actieve boeken</h2>
{% if books and books|length > 0 %}
<div id="book-list" class="book-list">
{% for book in books %} {% include "components/book_list_item.html" %} {%
endfor %}
</div>
{% else %}
<div id="book-list" class="book-list-empty">Geen actieve boeken.</div>
{% endif %}
</section>
{% include "components/registered_books.html" %}
<hr /> <hr />
<!-- =========================================================== <!-- ===========================================================

Loading…
Cancel
Save