Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A successful fetch returning zero items made existing.Except([]) flag the ENTIRE library FileNotFound in one scan — feeding EmptyTrashHandler's permanent delete and emptying every affected collection (dead channels). Add a shared MediaServerReconciliationGuard that skips (and logs a Warning) the sweep when incoming==0 while items exist, wired into the three library-level sweeps (Television shows / Movie / OtherVideo). An empty incoming set is indistinguishable at scan time from a mid-restore / emptied-upstream error (both report a zero total), so this deliberately overrides #476's degenerate "last item removed => empty incoming => flag" case. #476's cascade still fires for partial deletions (survivors present); its characterization test moves from an empty incoming to a survivor+removed partial-deletion case. Tests: policy table (MediaServerReconciliationGuardTests) + per-scanner integration proving the wiring (empty incoming + non-empty existing flags/reindexes nothing). Proven non-vacuous by neutralizing the guard. Nested TV season/episode sweeps left unguarded (bounded blast radius); ratio-threshold + projection-failure detection deferred to a follow-up. docs/decisions.md updated. Fixes #477 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 lines
2.1 KiB
C#
41 lines
2.1 KiB
C#
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ErsatzTV.Scanner.Core.Metadata;
|
|
|
|
// #477: a media-server library sweep computes "gone upstream" as existing.Except(incoming) and flags the
|
|
// result FileNotFound. If a successful fetch returns ZERO items (the server is up but mid-restore /
|
|
// mid-rebuild, or the library was emptied upstream) then existing.Except([]) is EVERY existing item, so
|
|
// the whole library is flagged FileNotFound in one pass. That is data-loss-adjacent: EmptyTrashHandler
|
|
// deletes state:FileNotFound rows permanently, and PlayoutSkipMissingItems empties every affected
|
|
// collection. An empty incoming set is indistinguishable at scan time from a transient error (both report
|
|
// zero), so the safe policy is to refuse the sweep — logged loudly — rather than nuke the library.
|
|
//
|
|
// This deliberately overrides the degenerate "last item removed => empty incoming => flag" case that a
|
|
// partial-deletion sweep would otherwise handle (see #476's cascade, which still fires for the common
|
|
// case where survivors are present and only some items are gone). The cost of not flagging a genuinely
|
|
// emptied library (stale rows persist until an item returns or the library is removed) is far smaller
|
|
// than a one-scan permanent wipe. Ratio-thresholds and projection-failure detection are deferred — see
|
|
// docs/decisions.md and the #477 follow-up.
|
|
internal static class MediaServerReconciliationGuard
|
|
{
|
|
public static bool ShouldFlagMissing(
|
|
ILogger logger,
|
|
string libraryName,
|
|
int incomingCount,
|
|
int existingCount)
|
|
{
|
|
if (incomingCount == 0 && existingCount > 0)
|
|
{
|
|
logger.LogWarning(
|
|
"Media server library {Library} returned zero items but {ExistingCount} exist locally; "
|
|
+ "skipping the file-not-found sweep to avoid flagging the entire library as missing "
|
|
+ "(expected if the server is mid-restore or the library was emptied upstream)",
|
|
libraryName,
|
|
existingCount);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|