fix(media-verify): read the WAL the server commits through
`mode=ro&immutable=1` claims the file never changes, so sqlite skips the write-ahead log entirely: every row the server has committed but not yet checkpointed reads as MISSING, and a read beside a running scanner can fail outright as "database disk image is malformed". Both servers keep their libraries in WAL, so plain `mode=ro` is now the first door and `immutable=1` only the fallback for a database whose `-shm` cannot be read. The verdict also stopped guessing from the database alone. A file still at its `src` is REAL (this pass's own damage); a file at neither path is `gone` (a later pass or the server renamed it); a file sitting at `dst` with no row naming it is something the server does not index. A row that still names `src` is none of those — it is the server's stale pointer, and it costs a re-scan, never a restore.
This commit is contained in:
1 parent
f430e2264d
commit
649bbafa17
3 files changed
+131
-16
No files matched your search
+72
-2
@@ -195,7 +195,7 @@ setup() {
|
||||
--share "$SHARE" --container /data
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[3]}" = "missing from the DB: 2" ]
|
||||
[ "${lines[4]}" = "missing by extension: .log 1, .mkv 1" ]
|
||||
[[ "$output" == *"missing by extension: .log 1, .mkv 1"* ]]
|
||||
}
|
||||
|
||||
@test "--ignore-ext keeps a file the server never indexes out of the verdict" {
|
||||
@@ -208,5 +208,75 @@ setup() {
|
||||
[ "$status" -eq 0 ]
|
||||
[ "${lines[1]}" = "landed: 1" ]
|
||||
[ "${lines[3]}" = "missing from the DB: 0" ]
|
||||
[ "${lines[4]}" = "not indexed (extension ignored): 1" ]
|
||||
[[ "$output" == *"not indexed (extension ignored): 1"* ]]
|
||||
}
|
||||
|
||||
# wal_case <db> — a Jellyfin database whose NEW row lives only in the
|
||||
# write-ahead log: the server has committed the rename but not checkpointed it,
|
||||
# which is the normal state of a library a scan is writing. The holder
|
||||
# connection keeps the WAL from folding into the main file, and the verifier
|
||||
# runs as a real subprocess so it faces the same files Plex and Jellyfin leave.
|
||||
wal_case() {
|
||||
python3 - "$JFDB" "$JOURNAL" "$SHARE" "$VERIFY" <<'PY'
|
||||
import sqlite3, subprocess, sys
|
||||
db, journal, share, verify = sys.argv[1:5]
|
||||
w = sqlite3.connect(db)
|
||||
w.execute("pragma journal_mode=wal")
|
||||
w.execute("pragma wal_autocheckpoint=0")
|
||||
w.execute("insert into BaseItems (Path, Type) values ('/data/Movies/Old.mkv', 'x')")
|
||||
w.commit()
|
||||
w.execute("pragma wal_checkpoint(truncate)")
|
||||
holder = sqlite3.connect(db) # last-connection close would checkpoint
|
||||
w.execute("update BaseItems set Path='/data/Movies/Old/Old.mkv'")
|
||||
w.commit()
|
||||
r = subprocess.run([sys.executable, verify, "--journal", journal,
|
||||
"--db-kind", "jellyfin", "--db", db,
|
||||
"--share", share, "--container", "/data"],
|
||||
capture_output=True, text=True)
|
||||
print(r.stdout, end="")
|
||||
sys.exit(r.returncode)
|
||||
PY
|
||||
}
|
||||
|
||||
@test "a row the server committed but has not checkpointed is proof, not a miss" {
|
||||
move_row "$SHARE/Movies/Old.mkv" "$SHARE/Movies/Old/Old.mkv"
|
||||
run wal_case
|
||||
[ "$status" -eq 0 ]
|
||||
[ "${lines[1]}" = "landed: 1" ]
|
||||
[ "${lines[3]}" = "missing from the DB: 0" ]
|
||||
}
|
||||
|
||||
# The DB alone cannot tell a lost file from a sidecar: both are "no row at the
|
||||
# destination". The tool therefore stats the two paths and separates the classes
|
||||
# the runbook names, so an operator never reads a `.pdf` as a lost film or,
|
||||
# worse, reads a real loss as a sidecar.
|
||||
@test "a file the database does not name that still sits at src is the pass's own damage" {
|
||||
mkdir -p "$SHARE/Movies"
|
||||
echo film > "$SHARE/Movies/Old.mkv"
|
||||
move_row "$SHARE/Movies/Old.mkv" "$SHARE/Movies/Old/Old.mkv"
|
||||
|
||||
run python3 "$VERIFY" --journal "$JOURNAL" --db-kind jellyfin --db "$JFDB" \
|
||||
--share "$SHARE" --container /data
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[3]}" = "missing from the DB: 1" ]
|
||||
[ "${lines[4]}" = " on disk at dst, no row names it: 0" ]
|
||||
[ "${lines[5]}" = " REAL — file still at src: 1" ]
|
||||
[ "${lines[6]}" = " gone — at neither path: 0" ]
|
||||
[[ "$output" == *"real: $SHARE/Movies/Old.mkv"* ]]
|
||||
}
|
||||
|
||||
@test "a file at neither path is gone, and a file at dst the server ignores is not a loss" {
|
||||
move_row "$SHARE/Movies/Gone.mkv" "$SHARE/Movies/Gone/Gone.mkv"
|
||||
mkdir -p "$SHARE/Movies/Sidecar"
|
||||
echo cover > "$SHARE/Movies/Sidecar/cover.jpg"
|
||||
move_row "$SHARE/Movies/Album/cover.jpg" "$SHARE/Movies/Sidecar/cover.jpg"
|
||||
|
||||
run python3 "$VERIFY" --journal "$JOURNAL" --db-kind jellyfin --db "$JFDB" \
|
||||
--share "$SHARE" --container /data
|
||||
[ "$status" -eq 1 ]
|
||||
[ "${lines[3]}" = "missing from the DB: 2" ]
|
||||
[ "${lines[4]}" = " on disk at dst, no row names it: 1" ]
|
||||
[ "${lines[5]}" = " REAL — file still at src: 0" ]
|
||||
[ "${lines[6]}" = " gone — at neither path: 1" ]
|
||||
[[ "$output" == *"gone: $SHARE/Movies/Gone/Gone.mkv"* ]]
|
||||
}
|
||||
Reference in new issue
Block a user