Fix APNG thumbnail handling and improve animated format detection (#5113) * check png for apng headers * isAnimated flag * refactor: centralize background animation extension checks * refactor: scope animated extension dedupe to backgrounds * remove precompute from startup * Fix animation preference not being loaded, fix type-check of customInputs * Fix eslint * Fix sort on removal type * Update metadata before returning from endpoint on CRUD operations * Remove race condition in metadata load * Load metadata after loading the list --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -41,6 +41,8 @@ const THUMBNAIL_CONFIG = { | |||
| 41 | height: 90, | 41 | height: 90, |
| 42 | }; | 42 | }; |
| 43 | 43 | ||
| 44 | const ANIMATED_BACKGROUND_EXTENSIONS = ['mp4', 'webp', 'gif', 'apng']; | ||
| 45 | |||
| 44 | /** | 46 | /** |
| 45 | * Cache for image metadata. | 47 | * Cache for image metadata. |
| 46 | * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>} | 48 | * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>} |
| @@ -88,7 +90,7 @@ let lazyLoadObserver = null; | |||
| 88 | /** | 90 | /** |
| 89 | * Cache for the current list of system background filenames. | 91 | * Cache for the current list of system background filenames. |
| 90 | * Used to re-sort backgrounds without refetching from the server. | 92 | * Used to re-sort backgrounds without refetching from the server. |
| 91 | * @type {string[]} | 93 | * @type {Array<{filename: string, isAnimated: boolean}>} |
| 92 | */ | 94 | */ |
| 93 | let cachedSystemBackgrounds = []; | 95 | let cachedSystemBackgrounds = []; |
| 94 | 96 | ||
| @@ -136,12 +138,13 @@ function sortBackgrounds(backgrounds, isCustom = false) { | |||
| 136 | 138 | ||
| 137 | /** | 139 | /** |
| 138 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. | 140 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. |
| 139 | * @param {object} imageData - Data for the image (filename, isCustom). | 141 | * @param {object} imageData - Data for the image (filename, isCustom, isAnimated). |
| 140 | * @returns {HTMLElement} The created thumbnail element. | 142 | * @returns {HTMLElement} The created thumbnail element. |
| 141 | */ | 143 | */ |
| 142 | function createThumbnailElement(imageData) { | 144 | function createThumbnailElement(imageData) { |
| 143 | const bg = imageData.filename; | 145 | const bg = imageData.filename; |
| 144 | const isCustom = imageData.isCustom; | 146 | const isCustom = imageData.isCustom; |
| 147 | const isAnimated = imageData.isAnimated ?? false; | ||
| 145 | 148 | ||
| 146 | const thumbnail = $('#background_template .bg_example').clone(); | 149 | const thumbnail = $('#background_template .bg_example').clone(); |
| 147 | 150 | ||
| @@ -172,6 +175,7 @@ function createThumbnailElement(imageData) { | |||
| 172 | thumbnail.attr('title', title); | 175 | thumbnail.attr('title', title); |
| 173 | thumbnail.attr('bgfile', bg); | 176 | thumbnail.attr('bgfile', bg); |
| 174 | thumbnail.attr('custom', String(isCustom)); | 177 | thumbnail.attr('custom', String(isCustom)); |
| 178 | thumbnail.attr('animated', String(isAnimated)); | ||
| 175 | thumbnail.data('url', url); | 179 | thumbnail.data('url', url); |
| 176 | titleElement.text(friendlyTitle); | 180 | titleElement.text(friendlyTitle); |
| 177 | 181 | ||
| @@ -216,6 +220,7 @@ export function loadBackgroundSettings(settings) { | |||
| 216 | } | 220 | } |
| 217 | background_settings.thumbnailColumns = columns; | 221 | background_settings.thumbnailColumns = columns; |
| 218 | background_settings.sortOrder = backgroundSettings.sortOrder; | 222 | background_settings.sortOrder = backgroundSettings.sortOrder; |
| 223 | background_settings.animation = backgroundSettings.animation; | ||
| 219 | applyThumbnailColumns(background_settings.thumbnailColumns); | 224 | applyThumbnailColumns(background_settings.thumbnailColumns); |
| 220 | 225 | ||
| 221 | setBackground(backgroundSettings.name, backgroundSettings.url); | 226 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| @@ -498,14 +503,13 @@ async function onDeleteBackgroundClick(e) { | |||
| 498 | const url = bgToDelete.data('url'); | 503 | const url = bgToDelete.data('url'); |
| 499 | const isCustom = bgToDelete.attr('custom') === 'true'; | 504 | const isCustom = bgToDelete.attr('custom') === 'true'; |
| 500 | const deleteFromServerId = 'delete_bg_from_server'; | 505 | const deleteFromServerId = 'delete_bg_from_server'; |
| 501 | const customInputs = [ | 506 | /** @type {import('./popup.js').CustomPopupInput[]} */ |
| 502 | { | 507 | const customInputs = [{ |
| 503 | type: 'checkbox', | 508 | type: 'checkbox', |
| 504 | label: t`Also delete file from server`, | 509 | label: t`Also delete file from server`, |
| 505 | id: deleteFromServerId, | 510 | id: deleteFromServerId, |
| 506 | defaultState: true, | 511 | defaultState: true, |
| 507 | }, | 512 | }]; |
| 508 | ]; | ||
| 509 | let deleteFromServer = false; | 513 | let deleteFromServer = false; |
| 510 | const confirm = await Popup.show.confirm(t`Delete the background?`, null, { | 514 | const confirm = await Popup.show.confirm(t`Delete the background?`, null, { |
| 511 | customInputs: isCustom ? customInputs : [], | 515 | customInputs: isCustom ? customInputs : [], |
| @@ -522,7 +526,7 @@ async function onDeleteBackgroundClick(e) { | |||
| 522 | if (!isCustom) { | 526 | if (!isCustom) { |
| 523 | await delBackground(bg); | 527 | await delBackground(bg); |
| 524 | // Remove from cache to prevent reappearing on sort change | 528 | // Remove from cache to prevent reappearing on sort change |
| 525 | const cacheIndex = cachedSystemBackgrounds.indexOf(bg); | 529 | const cacheIndex = cachedSystemBackgrounds.findIndex(s => s.filename === bg); |
| 526 | if (cacheIndex !== -1) { | 530 | if (cacheIndex !== -1) { |
| 527 | cachedSystemBackgrounds.splice(cacheIndex, 1); | 531 | cachedSystemBackgrounds.splice(cacheIndex, 1); |
| 528 | } | 532 | } |
| @@ -605,7 +609,7 @@ async function autoBackgroundCommand() { | |||
| 605 | 609 | ||
| 606 | /** | 610 | /** |
| 607 | * Renders the system backgrounds gallery. | 611 | * Renders the system backgrounds gallery. |
| 608 | * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | 612 | * @param {Array<{filename: string, isAnimated: boolean}>} [backgrounds] - Optional filtered list of backgrounds with metadata. |
| 609 | */ | 613 | */ |
| 610 | function renderSystemBackgrounds(backgrounds) { | 614 | function renderSystemBackgrounds(backgrounds) { |
| 611 | const sourceList = backgrounds || []; | 615 | const sourceList = backgrounds || []; |
| @@ -614,9 +618,11 @@ function renderSystemBackgrounds(backgrounds) { | |||
| 614 | 618 | ||
| 615 | if (sourceList.length === 0) return; | 619 | if (sourceList.length === 0) return; |
| 616 | 620 | ||
| 617 | const sortedList = sortBackgrounds(sourceList, false); | 621 | const sortedList = sortBackgrounds(sourceList.map(bg => bg.filename), false); |
| 618 | sortedList.forEach(bg => { | 622 | const metadataByFilename = new Map(sourceList.map(bg => [bg.filename, bg])); |
| 619 | const imageData = { filename: bg, isCustom: false }; | 623 | sortedList.forEach(filename => { |
| 624 | const bg = metadataByFilename.get(filename); | ||
| 625 | const imageData = { filename, isCustom: false, isAnimated: bg?.isAnimated ?? false }; | ||
| 620 | const thumbnail = createThumbnailElement(imageData); | 626 | const thumbnail = createThumbnailElement(imageData); |
| 621 | container.append(thumbnail); | 627 | container.append(thumbnail); |
| 622 | }); | 628 | }); |
| @@ -638,7 +644,9 @@ function renderChatBackgrounds(backgrounds) { | |||
| 638 | 644 | ||
| 639 | const sortedList = sortBackgrounds(sourceList, true); | 645 | const sortedList = sortBackgrounds(sourceList, true); |
| 640 | sortedList.forEach(bg => { | 646 | sortedList.forEach(bg => { |
| 641 | const imageData = { filename: bg, isCustom: true }; | 647 | // For custom backgrounds, infer isAnimated from extension since we don't have server metadata |
| 648 | const isAnimated = isAnimatedBackgroundExtension(bg); | ||
| 649 | const imageData = { filename: bg, isCustom: true, isAnimated }; | ||
| 642 | const thumbnail = createThumbnailElement(imageData); | 650 | const thumbnail = createThumbnailElement(imageData); |
| 643 | container.append(thumbnail); | 651 | container.append(thumbnail); |
| 644 | }); | 652 | }); |
| @@ -647,8 +655,6 @@ function renderChatBackgrounds(backgrounds) { | |||
| 647 | } | 655 | } |
| 648 | 656 | ||
| 649 | export async function getBackgrounds() { | 657 | export async function getBackgrounds() { |
| 650 | const metadataPromise = preloadImageMetadata(); | ||
| 651 | |||
| 652 | const response = await fetch('/api/backgrounds/all', { | 658 | const response = await fetch('/api/backgrounds/all', { |
| 653 | method: 'POST', | 659 | method: 'POST', |
| 654 | headers: getRequestHeaders(), | 660 | headers: getRequestHeaders(), |
| @@ -657,10 +663,8 @@ export async function getBackgrounds() { | |||
| 657 | if (response.ok) { | 663 | if (response.ok) { |
| 658 | const { images, config } = await response.json(); | 664 | const { images, config } = await response.json(); |
| 659 | Object.assign(THUMBNAIL_CONFIG, config); | 665 | Object.assign(THUMBNAIL_CONFIG, config); |
| 660 | |||
| 661 | cachedSystemBackgrounds = images; | 666 | cachedSystemBackgrounds = images; |
| 662 | 667 | await preloadImageMetadata(); | |
| 663 | await metadataPromise; | ||
| 664 | 668 | ||
| 665 | renderSystemBackgrounds(images); | 669 | renderSystemBackgrounds(images); |
| 666 | highlightSelectedBackground(); | 670 | highlightSelectedBackground(); |
| @@ -716,7 +720,8 @@ function activateLazyLoader() { | |||
| 716 | if (parentThumbnail) { | 720 | if (parentThumbnail) { |
| 717 | const bg = parentThumbnail.getAttribute('bgfile'); | 721 | const bg = parentThumbnail.getAttribute('bgfile'); |
| 718 | const isCustom = parentThumbnail.getAttribute('custom') === 'true'; | 722 | const isCustom = parentThumbnail.getAttribute('custom') === 'true'; |
| 719 | resolveImageUrl(bg, isCustom) | 723 | const isAnimated = parentThumbnail.getAttribute('animated') === 'true'; |
| 724 | resolveImageUrl(bg, isCustom, isAnimated) | ||
| 720 | .then(url => { clipper.style.backgroundImage = url; }) | 725 | .then(url => { clipper.style.backgroundImage = url; }) |
| 721 | .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); | 726 | .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); |
| 722 | } | 727 | } |
| @@ -745,16 +750,26 @@ function generateUrlParameter(bg, isCustom) { | |||
| 745 | return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`; | 750 | return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`; |
| 746 | } | 751 | } |
| 747 | 752 | ||
| 753 | function isAnimatedBackgroundExtension(fileName) { | ||
| 754 | const fileExtension = fileName.split('.').pop().toLowerCase(); | ||
| 755 | return ANIMATED_BACKGROUND_EXTENSIONS.includes(fileExtension); | ||
| 756 | } | ||
| 757 | |||
| 748 | /** | 758 | /** |
| 749 | * Resolves the image URL for the background. | 759 | * Resolves the image URL for the background. |
| 750 | * @param {string} bg Background file name | 760 | * @param {string} bg Background file name |
| 751 | * @param {boolean} isCustom Is a custom background | 761 | * @param {boolean} isCustom Is a custom background |
| 762 | * @param {boolean|null} [isAnimated=null] Is the background animated (from metadata). If null, infers from extension. | ||
| 752 | * @returns {Promise<string>} CSS URL of the background | 763 | * @returns {Promise<string>} CSS URL of the background |
| 753 | */ | 764 | */ |
| 754 | async function resolveImageUrl(bg, isCustom) { | 765 | async function resolveImageUrl(bg, isCustom, isAnimated = null) { |
| 755 | const fileExtension = bg.split('.').pop().toLowerCase(); | 766 | // If isAnimated is not provided (null), fall back to extension-based heuristic |
| 756 | const isAnimated = ['mp4', 'webp'].includes(fileExtension); | 767 | let animated = isAnimated; |
| 757 | const thumbnailUrl = isAnimated && !background_settings.animation | 768 | if (animated === null) { |
| 769 | animated = isAnimatedBackgroundExtension(bg); | ||
| 770 | } | ||
| 771 | |||
| 772 | const thumbnailUrl = animated && !background_settings.animation | ||
| 758 | ? await getThumbnailFromStorage(bg, isCustom) | 773 | ? await getThumbnailFromStorage(bg, isCustom) |
| 759 | : isCustom | 774 | : isCustom |
| 760 | ? bg | 775 | ? bg |
| @@ -12,12 +12,33 @@ import { getFileNameValidationFunction } from '../middleware/validateFileName.js | |||
| 12 | export const router = express.Router(); | 12 | export const router = express.Router(); |
| 13 | 13 | ||
| 14 | router.post('/all', async function (request, response) { | 14 | router.post('/all', async function (request, response) { |
| 15 | try { | ||
| 15 | const images = getImages(request.user.directories.backgrounds); | 16 | const images = getImages(request.user.directories.backgrounds); |
| 16 | const config = { width: thumbnailDimensions.bg[0], height: thumbnailDimensions.bg[1] }; | 17 | const config = { width: thumbnailDimensions.bg[0], height: thumbnailDimensions.bg[1] }; |
| 17 | response.json({ images, config }); | 18 | |
| 19 | // Get metadata for all images to provide isAnimated flag to client | ||
| 20 | const relativePaths = images.map(img => path.join('backgrounds', img)); | ||
| 21 | const { results: metadataMap } = await getOrGenerateMetadataBatch(request.user.directories.root, relativePaths, 'bg'); | ||
| 22 | |||
| 23 | // Build response with metadata for each image | ||
| 24 | const imagesWithMetadata = images.map(img => { | ||
| 25 | const relativePath = path.join('backgrounds', img); | ||
| 26 | const metadata = metadataMap[relativePath]; | ||
| 27 | return { | ||
| 28 | filename: img, | ||
| 29 | isAnimated: metadata?.isAnimated ?? false, | ||
| 30 | }; | ||
| 31 | }); | ||
| 32 | |||
| 33 | response.json({ images: imagesWithMetadata, config }); | ||
| 34 | } catch (error) { | ||
| 35 | console.error('[Backgrounds] Error fetching backgrounds:', error); | ||
| 36 | response.status(500).json({ error: 'Failed to fetch backgrounds' }); | ||
| 37 | } | ||
| 18 | }); | 38 | }); |
| 19 | 39 | ||
| 20 | router.post('/delete', getFileNameValidationFunction('bg'), function (request, response) { | 40 | router.post('/delete', getFileNameValidationFunction('bg'), async function (request, response) { |
| 41 | try { | ||
| 21 | if (!request.body) return response.sendStatus(400); | 42 | if (!request.body) return response.sendStatus(400); |
| 22 | 43 | ||
| 23 | if (request.body.bg !== sanitize(request.body.bg)) { | 44 | if (request.body.bg !== sanitize(request.body.bg)) { |
| @@ -37,14 +58,19 @@ router.post('/delete', getFileNameValidationFunction('bg'), function (request, r | |||
| 37 | 58 | ||
| 38 | // Remove metadata for deleted image | 59 | // Remove metadata for deleted image |
| 39 | const relativePath = path.join('backgrounds', request.body.bg); | 60 | const relativePath = path.join('backgrounds', request.body.bg); |
| 40 | removeMetadata(request.user.directories.root, relativePath).catch(err => { | 61 | await removeMetadata(request.user.directories.root, relativePath).catch(err => { |
| 41 | console.warn('[Backgrounds] Failed to remove metadata:', err.message); | 62 | console.warn('[Backgrounds] Failed to remove metadata:', err.message); |
| 42 | }); | 63 | }); |
| 43 | 64 | ||
| 44 | return response.send('ok'); | 65 | return response.send('ok'); |
| 66 | } catch (err) { | ||
| 67 | console.error(err); | ||
| 68 | response.sendStatus(500); | ||
| 69 | } | ||
| 45 | }); | 70 | }); |
| 46 | 71 | ||
| 47 | router.post('/rename', function (request, response) { | 72 | router.post('/rename', async function (request, response) { |
| 73 | try { | ||
| 48 | if (!request.body) return response.sendStatus(400); | 74 | if (!request.body) return response.sendStatus(400); |
| 49 | 75 | ||
| 50 | const oldFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.old_bg)); | 76 | const oldFileName = path.join(request.user.directories.backgrounds, sanitize(request.body.old_bg)); |
| @@ -67,27 +93,31 @@ router.post('/rename', function (request, response) { | |||
| 67 | // Update metadata for renamed image | 93 | // Update metadata for renamed image |
| 68 | const oldRelativePath = path.join('backgrounds', request.body.old_bg); | 94 | const oldRelativePath = path.join('backgrounds', request.body.old_bg); |
| 69 | const newRelativePath = path.join('backgrounds', request.body.new_bg); | 95 | const newRelativePath = path.join('backgrounds', request.body.new_bg); |
| 70 | renameMetadata(request.user.directories.root, oldRelativePath, newRelativePath).catch(err => { | 96 | await renameMetadata(request.user.directories.root, oldRelativePath, newRelativePath).catch(err => { |
| 71 | console.warn('[Backgrounds] Failed to rename metadata:', err.message); | 97 | console.warn('[Backgrounds] Failed to rename metadata:', err.message); |
| 72 | }); | 98 | }); |
| 73 | 99 | ||
| 74 | return response.send('ok'); | 100 | return response.send('ok'); |
| 101 | } catch (err) { | ||
| 102 | console.error(err); | ||
| 103 | response.sendStatus(500); | ||
| 104 | } | ||
| 75 | }); | 105 | }); |
| 76 | 106 | ||
| 77 | router.post('/upload', function (request, response) { | 107 | router.post('/upload', async function (request, response) { |
| 108 | try { | ||
| 78 | if (!request.body || !request.file) return response.sendStatus(400); | 109 | if (!request.body || !request.file) return response.sendStatus(400); |
| 79 | 110 | ||
| 80 | const img_path = path.join(request.file.destination, request.file.filename); | 111 | const img_path = path.join(request.file.destination, request.file.filename); |
| 81 | const filename = sanitize(request.file.originalname); | 112 | const filename = sanitize(request.file.originalname); |
| 82 | 113 | ||
| 83 | try { | ||
| 84 | fs.copyFileSync(img_path, path.join(request.user.directories.backgrounds, filename)); | 114 | fs.copyFileSync(img_path, path.join(request.user.directories.backgrounds, filename)); |
| 85 | fs.unlinkSync(img_path); | 115 | fs.unlinkSync(img_path); |
| 86 | invalidateThumbnail(request.user.directories, 'bg', filename); | 116 | invalidateThumbnail(request.user.directories, 'bg', filename); |
| 87 | 117 | ||
| 88 | // Generate metadata for the new image | 118 | // Generate metadata for the new image |
| 89 | const relativePath = path.join('backgrounds', filename); | 119 | const relativePath = path.join('backgrounds', filename); |
| 90 | getOrGenerateMetadataBatch(request.user.directories.root, [relativePath], 'bg').catch(err => { | 120 | await getOrGenerateMetadataBatch(request.user.directories.root, [relativePath], 'bg').catch(err => { |
| 91 | console.warn('[Backgrounds] Failed to generate metadata for upload:', err.message); | 121 | console.warn('[Backgrounds] Failed to generate metadata for upload:', err.message); |
| 92 | }); | 122 | }); |
| 93 | 123 | ||
| @@ -10,7 +10,7 @@ import { imageSize } from 'image-size'; | |||
| 10 | import writeFileAtomic from 'write-file-atomic'; | 10 | import writeFileAtomic from 'write-file-atomic'; |
| 11 | import express from 'express'; | 11 | import express from 'express'; |
| 12 | import { Jimp } from '../jimp.js'; | 12 | import { Jimp } from '../jimp.js'; |
| 13 | import { getConfigValue, getImages, isPathUnderParent } from '../util.js'; | 13 | import { getConfigValue, isPathUnderParent } from '../util.js'; |
| 14 | 14 | ||
| 15 | export const METADATA_FILE = 'image-metadata.json'; | 15 | export const METADATA_FILE = 'image-metadata.json'; |
| 16 | 16 | ||
| @@ -319,51 +319,6 @@ export async function cleanupOrphanedMetadata(userDataRoot) { | |||
| 319 | return orphanedPaths; | 319 | return orphanedPaths; |
| 320 | } | 320 | } |
| 321 | 321 | ||
| 322 | /** | ||
| 323 | * Initializes metadata for all images in a directory. | ||
| 324 | * @param {string} userDataRoot - Path to the user data directory root | ||
| 325 | * @param {string} subDirectory - Subdirectory relative to userDataRoot | ||
| 326 | * @param {ThumbnailType} type - The thumbnail type for resolution calculation | ||
| 327 | * @returns {Promise<number>} Number of images processed | ||
| 328 | */ | ||
| 329 | export async function initializeMetadataForDirectory(userDataRoot, subDirectory, type) { | ||
| 330 | const fullDir = path.join(userDataRoot, subDirectory); | ||
| 331 | |||
| 332 | let images; | ||
| 333 | try { | ||
| 334 | images = getImages(fullDir); | ||
| 335 | } catch { | ||
| 336 | // Directory doesn't exist or can't be read | ||
| 337 | return 0; | ||
| 338 | } | ||
| 339 | |||
| 340 | // Convert to relative paths from userDataRoot | ||
| 341 | const relativePaths = images.map(img => path.join(subDirectory, img)); | ||
| 342 | |||
| 343 | // Generate metadata for new images | ||
| 344 | const { results, generatedCount } = await getOrGenerateMetadataBatch(userDataRoot, relativePaths, type); | ||
| 345 | |||
| 346 | if (generatedCount > 0) { | ||
| 347 | console.log(`[ImageMetadata] Generated metadata for ${generatedCount} new images in ${subDirectory}`); | ||
| 348 | } | ||
| 349 | |||
| 350 | return Object.keys(results).length; | ||
| 351 | } | ||
| 352 | |||
| 353 | /** | ||
| 354 | * Initializes image metadata for all users' background directories. | ||
| 355 | * @param {Array<{root: string}>} userDirectories - List of user directory objects | ||
| 356 | * @returns {Promise<void>} | ||
| 357 | */ | ||
| 358 | export async function initializeAllUserMetadata(userDirectories) { | ||
| 359 | try { | ||
| 360 | for (const userDir of userDirectories) { | ||
| 361 | await initializeMetadataForDirectory(userDir.root, 'backgrounds', 'bg'); | ||
| 362 | } | ||
| 363 | } catch (error) { | ||
| 364 | console.error('[ImageMetadata] Failed to initialize background metadata:', error.message); | ||
| 365 | } | ||
| 366 | } | ||
| 367 | 322 | ||
| 368 | export const router = express.Router(); | 323 | export const router = express.Router(); |
| 369 | 324 | ||
| @@ -8,7 +8,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 8 | import { imageSize as sizeOf } from 'image-size'; | 8 | import { imageSize as sizeOf } from 'image-size'; |
| 9 | 9 | ||
| 10 | import { getConfigValue, invalidateFirefoxCache } from '../util.js'; | 10 | import { getConfigValue, invalidateFirefoxCache } from '../util.js'; |
| 11 | import { getThumbnailResolution, isAnimatedWebP, thumbnailDimensions as dimensions, isAnimatedApng } from './image-metadata.js'; | 11 | import { getThumbnailResolution, isAnimatedWebP, isAnimatedApng, thumbnailDimensions as dimensions } from './image-metadata.js'; |
| 12 | import { ResizeStrategy } from '@jimp/plugin-resize'; | 12 | import { ResizeStrategy } from '@jimp/plugin-resize'; |
| 13 | 13 | ||
| 14 | export const publicRouter = express.Router(); | 14 | export const publicRouter = express.Router(); |
| @@ -69,7 +69,6 @@ import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } fro | |||
| 69 | import { diskCache } from './endpoints/characters.js'; | 69 | import { diskCache } from './endpoints/characters.js'; |
| 70 | import { migrateFlatSecrets } from './endpoints/secrets.js'; | 70 | import { migrateFlatSecrets } from './endpoints/secrets.js'; |
| 71 | import { migrateGroupChatsMetadataFormat } from './endpoints/groups.js'; | 71 | import { migrateGroupChatsMetadataFormat } from './endpoints/groups.js'; |
| 72 | import { initializeAllUserMetadata } from './endpoints/image-metadata.js'; | ||
| 73 | 72 | ||
| 74 | // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | 73 | // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. |
| 75 | // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 | 74 | // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 |
| @@ -300,9 +299,6 @@ async function preSetupTasks() { | |||
| 300 | await settingsInit(); | 299 | await settingsInit(); |
| 301 | await statsInit(); | 300 | await statsInit(); |
| 302 | 301 | ||
| 303 | // Initialize image metadata | ||
| 304 | await initializeAllUserMetadata(directories); | ||
| 305 | |||
| 306 | const pluginsDirectory = path.join(serverDirectory, 'plugins'); | 302 | const pluginsDirectory = path.join(serverDirectory, 'plugins'); |
| 307 | const cleanupPlugins = await loadPlugins(app, pluginsDirectory); | 303 | const cleanupPlugins = await loadPlugins(app, pluginsDirectory); |
| 308 | const consoleTitle = process.title; | 304 | const consoleTitle = process.title; |