Release 2.21.5: hide generated image copies from media library
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 2m1s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-27 18:44:23 +00:00
1 parent b80d4aa9c7
commit 9e988c307e
7 files changed
+32 -7

No files matched your search

+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. 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 ## 2.21.4 - 2026-10-27
### Added ### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-player", "name": "pulse-signage-player",
"version": "2.21.4", "version": "2.21.5",
"private": false, "private": false,
"description": "Pulse Signage player application bundle", "description": "Pulse Signage player application bundle",
"engines": { "engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-web", "name": "pulse-signage-web",
"version": "2.21.4", "version": "2.21.5",
"private": false, "private": false,
"description": "Pulse Signage web and bridge application bundle", "description": "Pulse Signage web and bridge application bundle",
"engines": { "engines": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.4", "version": "2.21.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.4", "version": "2.21.5",
"dependencies": { "dependencies": {
"@better-auth/passkey": "^1.7.13", "@better-auth/passkey": "^1.7.13",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.4", "version": "2.21.5",
"private": false, "private": false,
"description": "Pulse Signage application with MySQL and media storage", "description": "Pulse Signage application with MySQL and media storage",
"engines": { "engines": {
+6 -2
View File
@@ -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_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg']);
const IMAGE_THUMBNAIL_MAX_DIMENSION = 480; 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) { function getImageVariantPath(filePath, dimension) {
const extension = path.extname(filePath); const extension = path.extname(filePath);
const baseName = path.basename(filePath, extension); const baseName = path.basename(filePath, extension);
@@ -352,7 +356,7 @@ async function fetchMediaAssets(pool) {
WHERE is_published = 1 WHERE is_published = 1
ORDER BY created_at DESC, id DESC` 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) { async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) {
@@ -377,7 +381,7 @@ async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) {
continue; continue;
} }
const originalName = String(entry.name || '').trim(); 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; continue;
} }
const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()]; const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()];
+15
View File
@@ -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-')); 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.png'), Buffer.from('image'));
await fs.promises.writeFile(path.join(uploadDir, 'legacy.thumb.webp'), Buffer.from('thumbnail')); 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, 'legacy.mp4'), Buffer.from('video'));
await fs.promises.writeFile(path.join(uploadDir, 'ignore.txt'), Buffer.from('other')); await fs.promises.writeFile(path.join(uploadDir, 'ignore.txt'), Buffer.from('other'));
const queries = []; 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 () => { test('media library counts slide, template background, and region default references', async () => {
const queries = []; const queries = [];
const pool = { const pool = {