diff --git a/CHANGELOG.md b/CHANGELOG.md index 5410322..cdd7b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## 2.21.5 - 2026-10-27 + +### Fixed + +- Generated image crop and playback copies no longer appear as separate Media Library assets. + ## 2.21.4 - 2026-10-27 ### Added diff --git a/build/package.player.json b/build/package.player.json index 46cb46c..1e8395f 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.21.4", + "version": "2.21.5", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 8bf8f47..6e458fc 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.21.4", + "version": "2.21.5", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 2b34e10..882e924 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.21.4", + "version": "2.21.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.21.4", + "version": "2.21.5", "dependencies": { "@better-auth/passkey": "^1.7.13", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index 1d268c2..cfa21e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.21.4", + "version": "2.21.5", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/lib/media/library.js b/src/web/lib/media/library.js index b87ec1a..090f5ce 100644 --- a/src/web/lib/media/library.js +++ b/src/web/lib/media/library.js @@ -26,6 +26,10 @@ const IMAGE_VARIANT_DIMENSIONS = [2048, 4096, 8192, 16384]; const IMAGE_THUMBNAIL_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg']); const IMAGE_THUMBNAIL_MAX_DIMENSION = 480; +function isGeneratedImageCopyName(name) { + return /^image-(?:crop|playback)-[a-f0-9]{64}\.(?:jpe?g|png|webp|gif)$/i.test(String(name || '')); +} + function getImageVariantPath(filePath, dimension) { const extension = path.extname(filePath); const baseName = path.basename(filePath, extension); @@ -352,7 +356,7 @@ async function fetchMediaAssets(pool) { WHERE is_published = 1 ORDER BY created_at DESC, id DESC` ); - return rows || []; + return (rows || []).filter(asset => !isGeneratedImageCopyName(path.basename(String(asset.media_path || '')))); } async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) { @@ -377,7 +381,7 @@ async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) { continue; } const originalName = String(entry.name || '').trim(); - if (/\.variant-\d+\.(?:jpe?g|png|webp)$/i.test(originalName) || /\.(?:thumb|poster)\.webp$/i.test(originalName)) { + if (isGeneratedImageCopyName(originalName) || /\.variant-\d+\.(?:jpe?g|png|webp)$/i.test(originalName) || /\.(?:thumb|poster)\.webp$/i.test(originalName)) { continue; } const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()]; diff --git a/test/media-library.test.js b/test/media-library.test.js index 6c8b09e..8e200d5 100644 --- a/test/media-library.test.js +++ b/test/media-library.test.js @@ -737,6 +737,7 @@ test('media library backfills existing image and video uploads', async () => { const uploadDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-media-library-')); await fs.promises.writeFile(path.join(uploadDir, 'legacy.png'), Buffer.from('image')); await fs.promises.writeFile(path.join(uploadDir, 'legacy.thumb.webp'), Buffer.from('thumbnail')); + await fs.promises.writeFile(path.join(uploadDir, 'image-playback-' + 'a'.repeat(64) + '.png'), Buffer.from('generated')); await fs.promises.writeFile(path.join(uploadDir, 'legacy.mp4'), Buffer.from('video')); await fs.promises.writeFile(path.join(uploadDir, 'ignore.txt'), Buffer.from('other')); const queries = []; @@ -760,6 +761,20 @@ test('media library backfills existing image and video uploads', async () => { } }); +test('media library omits generated image copies from published asset results', async () => { + const generated = 'image-crop-' + 'b'.repeat(64) + '.png'; + const pool = { + async query() { + return [[ + { id: 1, media_path: '/media/uploads/source.png' }, + { id: 2, media_path: '/media/uploads/' + generated } + ]]; + } + }; + + assert.deepEqual((await mediaLibrary.fetchMediaAssets(pool)).map(asset => asset.id), [1]); +}); + test('media library counts slide, template background, and region default references', async () => { const queries = []; const pool = {