mirror of
https://github.com/agregarr/agregarr.git
synced 2026-02-12 12:59:12 -06:00
fix(placeholders): trigger Plex scan and empty trash after cleanup (#332)
When placeholders are cleaned up because real content arrived, Plex wasn't notified and ghost entries remained in the library. This adds: - New emptyTrash() method to PlexAPI - Fire-and-forget scan + empty trash after placeholder cleanup - New autoEmptyTrash setting (default: true) with UI toggle - Setting in Plex settings page with "advanced" badge The scan runs in the background so it doesn't block the sync process. Co-authored-by: bitr8 <bitr8@users.noreply.github.com>
This commit is contained in:
@@ -2115,6 +2115,34 @@ class PlexAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty trash for a Plex library section
|
||||
* Removes items that Plex has detected as missing/unavailable
|
||||
* @param libraryId - The library section ID to empty trash for
|
||||
*/
|
||||
public async emptyTrash(libraryId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Emptying Plex library trash', {
|
||||
label: 'Plex API',
|
||||
libraryId,
|
||||
});
|
||||
|
||||
await this.safePutQuery(`/library/sections/${libraryId}/emptyTrash`);
|
||||
|
||||
logger.info('Plex library trash emptied', {
|
||||
label: 'Plex API',
|
||||
libraryId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to empty Plex library trash', {
|
||||
label: 'Plex API',
|
||||
libraryId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// PLEX.TV METHODS - Delegated to PlexTvAPI
|
||||
|
||||
/**
|
||||
|
||||
@@ -186,6 +186,45 @@ export class CollectionSyncService {
|
||||
cleanedUp,
|
||||
titlesFixes,
|
||||
});
|
||||
|
||||
// Trigger Plex library scan + empty trash to remove ghost entries (fire-and-forget)
|
||||
if (cleanedUp > 0 && tvLibraryId) {
|
||||
const libraryId = tvLibraryId;
|
||||
logger.info(
|
||||
'Triggering Plex scan to clean up deleted TV placeholders',
|
||||
{
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
placeholdersDeleted: cleanedUp,
|
||||
}
|
||||
);
|
||||
// Fire-and-forget: don't block sync while Plex processes
|
||||
const autoEmptyTrash = getSettings().plex.autoEmptyTrash !== false;
|
||||
void (async () => {
|
||||
try {
|
||||
await plexClient.scanLibrary(libraryId);
|
||||
if (autoEmptyTrash) {
|
||||
// Brief delay for scan to detect missing files
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
await plexClient.emptyTrash(libraryId);
|
||||
}
|
||||
logger.info('Plex placeholder cleanup complete', {
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
trashedEmptied: autoEmptyTrash,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
logger.warn('Failed to complete Plex placeholder cleanup', {
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
error:
|
||||
cleanupError instanceof Error
|
||||
? cleanupError.message
|
||||
: String(cleanupError),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to run global TV placeholder discovery', {
|
||||
label: 'Collection Sync Service',
|
||||
@@ -246,6 +285,45 @@ export class CollectionSyncService {
|
||||
label: 'Collection Sync Service',
|
||||
cleanedUp: moviesCleanedUp,
|
||||
});
|
||||
|
||||
// Trigger Plex library scan + empty trash to remove ghost entries (fire-and-forget)
|
||||
if (moviesCleanedUp > 0 && movieLibraryId) {
|
||||
const libraryId = movieLibraryId;
|
||||
logger.info(
|
||||
'Triggering Plex scan to clean up deleted movie placeholders',
|
||||
{
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
placeholdersDeleted: moviesCleanedUp,
|
||||
}
|
||||
);
|
||||
// Fire-and-forget: don't block sync while Plex processes
|
||||
const autoEmptyTrash = getSettings().plex.autoEmptyTrash !== false;
|
||||
void (async () => {
|
||||
try {
|
||||
await plexClient.scanLibrary(libraryId);
|
||||
if (autoEmptyTrash) {
|
||||
// Brief delay for scan to detect missing files
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
await plexClient.emptyTrash(libraryId);
|
||||
}
|
||||
logger.info('Plex placeholder cleanup complete', {
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
trashedEmptied: autoEmptyTrash,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
logger.warn('Failed to complete Plex placeholder cleanup', {
|
||||
label: 'Collection Sync Service',
|
||||
libraryId,
|
||||
error:
|
||||
cleanupError instanceof Error
|
||||
? cleanupError.message
|
||||
: String(cleanupError),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to run global movie placeholder discovery', {
|
||||
label: 'Collection Sync Service',
|
||||
|
||||
@@ -407,6 +407,7 @@ export interface PlexSettings {
|
||||
hubConfigs?: PlexHubConfig[]; // Plex built-in hub configurations
|
||||
preExistingCollectionConfigs?: PreExistingCollectionConfig[]; // Pre-existing Plex collections discovered by hub discovery
|
||||
usersHomeUnlocked?: boolean; // Secret unlock for Users Home collections
|
||||
autoEmptyTrash?: boolean; // Auto-empty Plex trash after placeholder cleanup (default: true)
|
||||
}
|
||||
|
||||
export interface TraktSettings {
|
||||
|
||||
@@ -62,6 +62,9 @@ const messages = defineMessages({
|
||||
webAppUrl: '<WebAppLink>Web App</WebAppLink> URL',
|
||||
webAppUrlTip:
|
||||
'Optionally direct users to the web app on your server instead of the "hosted" web app',
|
||||
autoEmptyTrash: 'Auto Empty Trash',
|
||||
autoEmptyTrashTip:
|
||||
'Automatically empty Plex library trash after placeholder cleanup to remove ghost entries',
|
||||
});
|
||||
|
||||
interface Library {
|
||||
@@ -246,6 +249,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
useSsl: data?.useSsl,
|
||||
selectedPreset: undefined,
|
||||
webAppUrl: data?.webAppUrl,
|
||||
autoEmptyTrash: data?.autoEmptyTrash !== false,
|
||||
}}
|
||||
validationSchema={PlexSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
@@ -266,6 +270,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
port: Number(values.port),
|
||||
useSsl: values.useSsl,
|
||||
webAppUrl: values.webAppUrl,
|
||||
autoEmptyTrash: values.autoEmptyTrash,
|
||||
} as PlexSettings);
|
||||
|
||||
syncLibraries();
|
||||
@@ -476,6 +481,25 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="autoEmptyTrash" className="checkbox-label">
|
||||
{intl.formatMessage(messages.autoEmptyTrash)}
|
||||
<SettingsBadge badgeType="advanced" className="ml-2" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.autoEmptyTrashTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="autoEmptyTrash"
|
||||
name="autoEmptyTrash"
|
||||
onChange={() => {
|
||||
setFieldValue('autoEmptyTrash', !values.autoEmptyTrash);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user