mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-15 09:52:06 +08:00
404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
import { getRepository } from '@server/datasource';
|
|
import { CollectionMetadata } from '@server/entity/CollectionMetadata';
|
|
import { MediaItemMetadata } from '@server/entity/MediaItemMetadata';
|
|
import logger from '@server/logger';
|
|
|
|
/**
|
|
* Service for managing collection and media item metadata tracking
|
|
* Prevents redundant uploads by tracking input hashes and Plex upload URLs
|
|
*/
|
|
class MetadataTrackingService {
|
|
// === COLLECTION POSTER METHODS ===
|
|
|
|
async shouldRegeneratePoster(
|
|
collectionRatingKey: string,
|
|
newInputHash: string
|
|
): Promise<boolean> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata?.lastPosterInputHash) {
|
|
logger.debug('No poster metadata found, regeneration needed', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const needsRegeneration = metadata.lastPosterInputHash !== newInputHash;
|
|
|
|
logger.debug('Poster regeneration check', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
needsRegeneration,
|
|
lastHash: metadata.lastPosterInputHash.substring(0, 8),
|
|
newHash: newInputHash.substring(0, 8),
|
|
});
|
|
|
|
return needsRegeneration;
|
|
}
|
|
|
|
async shouldReapplyPoster(
|
|
collectionRatingKey: string,
|
|
currentPlexUrl: string | null
|
|
): Promise<boolean> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata?.lastPosterUploadUrl) {
|
|
logger.debug('No poster URL tracked, reapplication needed', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (!currentPlexUrl) {
|
|
logger.debug('No current Plex poster, reapplication needed', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Use normalized URL comparison to handle different URL formats
|
|
// (upload://posters/123, /library/metadata/456/thumb/123, http://...?token=xyz)
|
|
const { posterUrlsMatch } = await import('@server/utils/posterUrlHelpers');
|
|
const urlsMatch = posterUrlsMatch(
|
|
metadata.lastPosterUploadUrl,
|
|
currentPlexUrl
|
|
);
|
|
const needsReapplication = !urlsMatch;
|
|
|
|
logger.debug('Poster reapplication check', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
needsReapplication,
|
|
expectedUrl: metadata.lastPosterUploadUrl,
|
|
currentUrl: currentPlexUrl,
|
|
});
|
|
|
|
return needsReapplication;
|
|
}
|
|
|
|
async recordPosterApplication(
|
|
collectionRatingKey: string,
|
|
inputHash: string,
|
|
uploadUrl: string,
|
|
options?: {
|
|
configId?: string;
|
|
libraryKey?: string;
|
|
posterLocalPath?: string;
|
|
}
|
|
): Promise<void> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new CollectionMetadata({
|
|
plexCollectionRatingKey: collectionRatingKey,
|
|
collectionConfigId: options?.configId,
|
|
libraryKey: options?.libraryKey,
|
|
});
|
|
}
|
|
|
|
metadata.lastPosterInputHash = inputHash;
|
|
metadata.lastPosterUploadUrl = uploadUrl;
|
|
metadata.lastPosterAppliedAt = new Date();
|
|
if (options?.posterLocalPath !== undefined) {
|
|
metadata.posterLocalPath = options.posterLocalPath;
|
|
}
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.info('Recorded poster application', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
inputHash: inputHash.substring(0, 8),
|
|
uploadUrl,
|
|
posterLocalPath: options?.posterLocalPath,
|
|
});
|
|
}
|
|
|
|
async getPosterLocalPath(
|
|
collectionRatingKey: string
|
|
): Promise<string | null> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
return metadata?.posterLocalPath || null;
|
|
}
|
|
|
|
async updatePosterLocalPath(
|
|
collectionRatingKey: string,
|
|
posterLocalPath: string | null,
|
|
options?: { configId?: string; libraryKey?: string }
|
|
): Promise<void> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new CollectionMetadata({
|
|
plexCollectionRatingKey: collectionRatingKey,
|
|
collectionConfigId: options?.configId,
|
|
libraryKey: options?.libraryKey,
|
|
});
|
|
}
|
|
|
|
metadata.posterLocalPath = posterLocalPath || undefined;
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.debug('Updated poster local path', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
posterLocalPath,
|
|
});
|
|
}
|
|
|
|
// === WALLPAPER METHODS ===
|
|
|
|
async shouldReapplyWallpaper(
|
|
collectionRatingKey: string,
|
|
newFilename: string,
|
|
currentPlexUrl: string | null
|
|
): Promise<boolean> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
// Check if filename changed (acts as input hash)
|
|
if (!metadata || metadata.lastWallpaperFilename !== newFilename) {
|
|
return true;
|
|
}
|
|
|
|
// Check if Plex URL matches
|
|
if (!currentPlexUrl || metadata.lastWallpaperUploadUrl !== currentPlexUrl) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async recordWallpaperApplication(
|
|
collectionRatingKey: string,
|
|
filename: string,
|
|
uploadUrl: string,
|
|
options?: { configId?: string; libraryKey?: string }
|
|
): Promise<void> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new CollectionMetadata({
|
|
plexCollectionRatingKey: collectionRatingKey,
|
|
collectionConfigId: options?.configId,
|
|
libraryKey: options?.libraryKey,
|
|
});
|
|
}
|
|
|
|
metadata.lastWallpaperFilename = filename;
|
|
metadata.lastWallpaperUploadUrl = uploadUrl;
|
|
metadata.lastWallpaperAppliedAt = new Date();
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.info('Recorded wallpaper application', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
filename,
|
|
uploadUrl,
|
|
});
|
|
}
|
|
|
|
// === THEME METHODS ===
|
|
|
|
async shouldReapplyTheme(
|
|
collectionRatingKey: string,
|
|
newFilename: string,
|
|
currentPlexUrl: string | null
|
|
): Promise<boolean> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata || metadata.lastThemeFilename !== newFilename) {
|
|
return true;
|
|
}
|
|
|
|
if (!currentPlexUrl || metadata.lastThemeUploadUrl !== currentPlexUrl) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async recordThemeApplication(
|
|
collectionRatingKey: string,
|
|
filename: string,
|
|
uploadUrl: string,
|
|
options?: { configId?: string; libraryKey?: string }
|
|
): Promise<void> {
|
|
const repo = getRepository(CollectionMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexCollectionRatingKey: collectionRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new CollectionMetadata({
|
|
plexCollectionRatingKey: collectionRatingKey,
|
|
collectionConfigId: options?.configId,
|
|
libraryKey: options?.libraryKey,
|
|
});
|
|
}
|
|
|
|
metadata.lastThemeFilename = filename;
|
|
metadata.lastThemeUploadUrl = uploadUrl;
|
|
metadata.lastThemeAppliedAt = new Date();
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.info('Recorded theme application', {
|
|
label: 'MetadataTracking',
|
|
collectionRatingKey,
|
|
filename,
|
|
uploadUrl,
|
|
});
|
|
}
|
|
|
|
// === OVERLAY METHODS (for individual items) ===
|
|
|
|
async shouldReapplyOverlay(
|
|
itemRatingKey: string,
|
|
newInputHash: string,
|
|
currentPlexUrl: string | null
|
|
): Promise<boolean> {
|
|
const repo = getRepository(MediaItemMetadata);
|
|
const metadata = await repo.findOne({
|
|
where: { plexItemRatingKey: itemRatingKey },
|
|
});
|
|
|
|
// Check if input hash changed
|
|
if (!metadata || metadata.lastOverlayInputHash !== newInputHash) {
|
|
return true;
|
|
}
|
|
|
|
// Check if Plex URL matches
|
|
if (!currentPlexUrl || metadata.lastPosterUploadUrl !== currentPlexUrl) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async recordOverlayApplication(
|
|
itemRatingKey: string,
|
|
libraryKey: string,
|
|
inputHash: string,
|
|
uploadUrl: string
|
|
): Promise<void> {
|
|
const repo = getRepository(MediaItemMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexItemRatingKey: itemRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new MediaItemMetadata({
|
|
plexItemRatingKey: itemRatingKey,
|
|
libraryKey: libraryKey,
|
|
});
|
|
}
|
|
|
|
metadata.lastOverlayInputHash = inputHash;
|
|
metadata.lastPosterUploadUrl = uploadUrl;
|
|
metadata.lastOverlayAppliedAt = new Date();
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.info('Recorded overlay application', {
|
|
label: 'MetadataTracking',
|
|
itemRatingKey,
|
|
inputHash: inputHash.substring(0, 8),
|
|
uploadUrl,
|
|
});
|
|
}
|
|
|
|
async recordOverlayApplicationWithBasePoster(
|
|
itemRatingKey: string,
|
|
libraryKey: string,
|
|
overlayInputHash: string,
|
|
ourOverlayPosterUrl: string,
|
|
basePosterInfo: {
|
|
basePosterSource: 'tmdb' | 'plex' | 'local';
|
|
originalPlexPosterUrl: string;
|
|
basePosterFilename: string;
|
|
localPosterModifiedTime?: number | null;
|
|
}
|
|
): Promise<void> {
|
|
const repo = getRepository(MediaItemMetadata);
|
|
|
|
let metadata = await repo.findOne({
|
|
where: { plexItemRatingKey: itemRatingKey },
|
|
});
|
|
|
|
if (!metadata) {
|
|
metadata = new MediaItemMetadata({
|
|
plexItemRatingKey: itemRatingKey,
|
|
libraryKey: libraryKey,
|
|
});
|
|
}
|
|
|
|
// Update overlay tracking
|
|
metadata.lastOverlayInputHash = overlayInputHash;
|
|
metadata.lastPosterUploadUrl = ourOverlayPosterUrl;
|
|
metadata.lastOverlayAppliedAt = new Date();
|
|
|
|
// Update base poster tracking
|
|
metadata.basePosterSource = basePosterInfo.basePosterSource;
|
|
metadata.originalPlexPosterUrl = basePosterInfo.originalPlexPosterUrl;
|
|
metadata.ourOverlayPosterUrl = ourOverlayPosterUrl;
|
|
metadata.basePosterFilename = basePosterInfo.basePosterFilename;
|
|
metadata.localPosterModifiedTime =
|
|
basePosterInfo.localPosterModifiedTime || undefined;
|
|
|
|
await repo.save(metadata);
|
|
|
|
logger.info('Recorded overlay application with base poster tracking', {
|
|
label: 'MetadataTracking',
|
|
itemRatingKey,
|
|
overlayInputHash: overlayInputHash.substring(0, 8),
|
|
basePosterSource: basePosterInfo.basePosterSource,
|
|
});
|
|
}
|
|
|
|
async getItemMetadata(
|
|
itemRatingKey: string
|
|
): Promise<MediaItemMetadata | null> {
|
|
const repo = getRepository(MediaItemMetadata);
|
|
return await repo.findOne({
|
|
where: { plexItemRatingKey: itemRatingKey },
|
|
});
|
|
}
|
|
}
|
|
|
|
export default new MetadataTrackingService();
|