fix(media): verify read the library the way the servers write it #103
No files matched your search
+28
-6
@@ -27,9 +27,14 @@ beside a running scanner can even fail as "database disk image is malformed".
|
|||||||
|
|
||||||
Neither server holds a row for everything the journal renames: a `.log`, `.cue`
|
Neither server holds a row for everything the journal renames: a `.log`, `.cue`
|
||||||
or cover image has no row to land in, so its absence is not a fault. The report
|
or cover image has no row to land in, so its absence is not a fault. The report
|
||||||
therefore breaks the missing rows down by extension, and `--ignore-ext` names
|
therefore breaks the missing rows down by extension, and `--ignore-ext` names the
|
||||||
the extensions this server does not index, which are then counted separately and
|
extensions this server does not index, which are then counted separately and kept
|
||||||
kept out of the verdict.
|
out of the verdict. What is left is split by what the filesystem says, because
|
||||||
|
the database alone cannot: 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),
|
||||||
|
and a file sitting at its `dst` with no row naming it is simply something the
|
||||||
|
server does not index. A row that still names `src` is none of these: it is the
|
||||||
|
server's own stale pointer — a phantom — and it costs a re-scan, never a restore.
|
||||||
|
|
||||||
Exit codes: 0 every move landed; 1 something is stale or missing (the details are
|
Exit codes: 0 every move landed; 1 something is stale or missing (the details are
|
||||||
printed); 2 the journal or the database is not usable at all.
|
printed); 2 the journal or the database is not usable at all.
|
||||||
@@ -174,20 +179,37 @@ def main():
|
|||||||
if at_src:
|
if at_src:
|
||||||
stale.append(src)
|
stale.append(src)
|
||||||
if not at_dst:
|
if not at_dst:
|
||||||
missing.append(dst)
|
missing.append((src, dst))
|
||||||
|
|
||||||
|
# The database cannot tell a lost file from one it never indexes: both read
|
||||||
|
# as "no row at the destination". Stat both paths, so the report separates
|
||||||
|
# the classes the runbook names — `REAL` is the only one that is this pass's
|
||||||
|
# own damage, and `gone` is a later pass or the server's own rename.
|
||||||
|
on_disk, real, gone = [], [], []
|
||||||
|
for src, dst in missing:
|
||||||
|
if os.path.exists(dst):
|
||||||
|
on_disk.append(dst)
|
||||||
|
elif os.path.exists(src):
|
||||||
|
real.append(src)
|
||||||
|
else:
|
||||||
|
gone.append(dst)
|
||||||
|
|
||||||
print(f"{args.db_kind}: {len(moves)} moves checked")
|
print(f"{args.db_kind}: {len(moves)} moves checked")
|
||||||
print(f"landed: {landed}")
|
print(f"landed: {landed}")
|
||||||
print(f"still at src: {len(stale)}")
|
print(f"still at src: {len(stale)}")
|
||||||
print(f"missing from the DB: {len(missing)}")
|
print(f"missing from the DB: {len(missing)}")
|
||||||
|
print(f" on disk at dst, no row names it: {len(on_disk)}")
|
||||||
|
print(f" REAL — file still at src: {len(real)}")
|
||||||
|
print(f" gone — at neither path: {len(gone)}")
|
||||||
if ignored:
|
if ignored:
|
||||||
print(f"not indexed (extension ignored): {ignored}")
|
print(f"not indexed (extension ignored): {ignored}")
|
||||||
if missing:
|
if missing:
|
||||||
exts = Counter(os.path.splitext(d)[1].lower() or "(no extension)"
|
exts = Counter(os.path.splitext(d)[1].lower() or "(no extension)"
|
||||||
for d in missing)
|
for _, d in missing)
|
||||||
print("missing by extension: "
|
print("missing by extension: "
|
||||||
+ ", ".join(f"{e} {n}" for e, n in sorted(exts.items())))
|
+ ", ".join(f"{e} {n}" for e, n in sorted(exts.items())))
|
||||||
for label, items in (("stale", stale), ("missing", missing)):
|
for label, items in (("real", real), ("gone", gone), ("missing", on_disk),
|
||||||
|
("stale", stale)):
|
||||||
for path in items[:args.show]:
|
for path in items[:args.show]:
|
||||||
print(f" {label}: {path}")
|
print(f" {label}: {path}")
|
||||||
if len(items) > args.show:
|
if len(items) > args.show:
|
||||||
|
|||||||
+37
-2
@@ -195,7 +195,7 @@ setup() {
|
|||||||
--share "$SHARE" --container /data
|
--share "$SHARE" --container /data
|
||||||
[ "$status" -eq 1 ]
|
[ "$status" -eq 1 ]
|
||||||
[ "${lines[3]}" = "missing from the DB: 2" ]
|
[ "${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" {
|
@test "--ignore-ext keeps a file the server never indexes out of the verdict" {
|
||||||
@@ -208,7 +208,7 @@ setup() {
|
|||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[ "${lines[1]}" = "landed: 1" ]
|
[ "${lines[1]}" = "landed: 1" ]
|
||||||
[ "${lines[3]}" = "missing from the DB: 0" ]
|
[ "${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
|
# wal_case <db> — a Jellyfin database whose NEW row lives only in the
|
||||||
@@ -245,3 +245,38 @@ PY
|
|||||||
[ "${lines[1]}" = "landed: 1" ]
|
[ "${lines[1]}" = "landed: 1" ]
|
||||||
[ "${lines[3]}" = "missing from the DB: 0" ]
|
[ "${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