Skip to content

Commit f2fc76b

Browse files
committed
fix: self-healing for stuck placeholder DB records
The previous commit removed the unconditional stale deletion branch, which also served as a recovery path for active placeholders whose files were deleted externally. Without it, a missing file + existing DB record would permanently block re-creation (creation flow skips items with DB records). Add a targeted check: for items still in the source list, verify the placeholder file exists on disk. If missing, remove the DB record so the next sync recreates the placeholder cleanly. This preserves the self-healing behaviour without the churn caused by the old stale branch.
1 parent 2cbdb13 commit f2fc76b

1 file changed

Lines changed: 50 additions & 1 deletion

File tree

server/lib/placeholders/services/PlaceholderCleanup.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function cleanupPlaceholderForRealContent(
6565

6666
/**
6767
* Handle placeholder operations based on createPlaceholdersForMissing setting
68-
* - If enabled: runs cleanup (released items, orphaned items)
68+
* - If enabled: runs cleanup (released items, orphaned items, stuck records)
6969
* - If disabled: deletes all placeholder records for the config
7070
* Files will be cleaned up later by orphaned file cleanup
7171
*/
@@ -701,6 +701,7 @@ async function deletePlexPlaceholderEpisode(
701701
* Clean up placeholders for a collection:
702702
* 1. Items with real content detected in Plex (via discovery system)
703703
* 2. Items no longer in source data (orphaned items)
704+
* 3. Active items with missing placeholder files (self-healing — clears DB record for re-creation)
704705
*
705706
* Released items are tracked for configured window (placeholderReleasedDays, default: 7 days),
706707
* then database records are removed and overlay system automatically updates posters.
@@ -768,6 +769,54 @@ export async function cleanupPlaceholdersForConfig(
768769

769770
const isOrphaned = !sourceTmdbIds.has(placeholder.tmdbId);
770771

772+
// Self-healing: if item is still in source but placeholder file is
773+
// missing (external deletion, disk issue), remove the DB record so the
774+
// creation flow can recreate it next sync.
775+
if (!isOrphaned && placeholder.placeholderPath) {
776+
const { getPlaceholderRootFolder } = await import(
777+
'@server/lib/placeholders/helpers/placeholderPathHelpers'
778+
);
779+
const libraryPath = getPlaceholderRootFolder(
780+
config.libraryId,
781+
placeholder.mediaType
782+
);
783+
784+
if (libraryPath) {
785+
const fullPath = path.join(
786+
libraryPath,
787+
placeholder.placeholderPath
788+
);
789+
try {
790+
await fs.access(fullPath);
791+
} catch {
792+
// File missing — clear DB record to unblock recreation
793+
logger.info(
794+
'Placeholder file missing for active item — removing DB record for re-creation',
795+
{
796+
label: 'PlaceholderService',
797+
title: placeholder.title,
798+
tmdbId: placeholder.tmdbId,
799+
path: fullPath,
800+
}
801+
);
802+
803+
if (
804+
placeholder.mediaType === 'tv' &&
805+
placeholder.plexRatingKey
806+
) {
807+
await deletePlexPlaceholderEpisode(
808+
plexClient,
809+
placeholder.plexRatingKey,
810+
placeholder.title
811+
);
812+
}
813+
814+
await repository.remove(placeholder);
815+
removedCount++;
816+
}
817+
}
818+
}
819+
771820
// For orphaned items, check if past configured window
772821
if (isOrphaned) {
773822
// This handles items that fall off source lists (e.g., Trakt Trending)

0 commit comments

Comments
 (0)