diff options
Diffstat (limited to 'src/gen-thumbs.js')
-rw-r--r-- | src/gen-thumbs.js | 269 |
1 files changed, 117 insertions, 152 deletions
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index c5c5ee4f..40505189 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -91,7 +91,8 @@ const WARNING_DELAY_TIME = 10000; // this particular thumbtack will be regenerated, but any others (whose // `tackbust` listed below is equal or below the cache-recorded bust) will be // reused. (Zero is a special value that means this tack's spec is still the -// same as it would've been generated prior to thumbtack versioning.) +// same as it would've been generated prior to thumbtack versioning; any new +// kinds of thumbnails should start counting up from one.) // // * `size` is the maximum length of the image. It will be scaled down, // keeping aspect ratio, to fit in this dimension. @@ -132,6 +133,12 @@ const thumbnailSpec = { quality: 85, }, + 'adorb': { + tackbust: 1, + size: 64, + quality: 90, + }, + 'mini': { tackbust: 2, size: 8, @@ -155,7 +162,7 @@ import { import dimensionsOf from 'image-size'; -import CacheableObject from '#cacheable-object'; +import {stringifyCache} from '#cli'; import {commandExists, isMain, promisifyProcess, traverse} from '#node-utils'; import {sortByName} from '#sort'; @@ -339,28 +346,6 @@ export function getThumbnailsAvailableForDimensions([width, height]) { ]; } -function stringifyCache(cache) { - if (Object.keys(cache).length === 0) { - return `{}`; - } - - const entries = Object.entries(cache); - sortByName(entries, {getName: entry => entry[0]}); - - return [ - `{`, - entries - .map(([key, value]) => [JSON.stringify(key), JSON.stringify(value)]) - .map(([key, value]) => `${key}: ${value}`) - .map((line, index, array) => - (index < array.length - 1 - ? `${line},` - : line)) - .map(line => ` ${line}`), - `}`, - ].flat().join('\n'); -} - getThumbnailsAvailableForDimensions.all = Object.entries(thumbnailSpec) .map(([name, {size}]) => [name, size]) @@ -461,7 +446,7 @@ async function getImageMagickVersion(binary) { try { await promisifyProcess(proc, false); - } catch (error) { + } catch { return null; } @@ -570,42 +555,56 @@ async function determineThumbtacksNeededForFile({ return mismatchedWithinRightSize; } -async function generateImageThumbnail(imagePath, thumbtack, { +// Write all requested thumbtacks for a source image in one pass +// This saves a lot of disk reads which are probably the main bottleneck +function prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks) { + const args = [filePathInMedia, '-strip']; + + const basename = + path.basename(filePathInMedia, path.extname(filePathInMedia)); + + // do larger sizes first + thumbtacks.sort((a, b) => thumbnailSpec[b].size - thumbnailSpec[a].size); + + for (const tack of thumbtacks) { + const {size, quality} = thumbnailSpec[tack]; + const filename = `${basename}.${tack}.jpg`; + const filePathInCache = path.join(dirnameInCache, filename); + args.push( + '(', '+clone', + '-resize', `${size}x${size}>`, + '-interlace', 'Plane', + '-quality', `${quality}%`, + '-write', filePathInCache, + '+delete', ')', + ); + } + + // throw away the (already written) image stream + args.push('null:'); + + return args; +} + +async function generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }) { + if (empty(thumbtacks)) return; + const filePathInMedia = path.join(mediaPath, imagePath); const dirnameInCache = path.join(mediaCachePath, path.dirname(imagePath)); - const filename = - path.basename(imagePath, path.extname(imagePath)) + - `.${thumbtack}.jpg`; - - const filePathInCache = - path.join(dirnameInCache, filename); - await mkdir(dirnameInCache, {recursive: true}); - const specEntry = thumbnailSpec[thumbtack]; - const {size, quality} = specEntry; - - const convertProcess = spawnConvert([ - filePathInMedia, - '-strip', - '-resize', - `${size}x${size}>`, - '-interlace', - 'Plane', - '-quality', - `${quality}%`, - filePathInCache, - ]); - - await promisifyProcess(convertProcess, false); + const convertArgs = + prepareConvertArgs(filePathInMedia, dirnameInCache, thumbtacks); + + await promisifyProcess(spawnConvert(convertArgs), false); } export async function determineMediaCachePath({ @@ -630,12 +629,19 @@ export async function determineMediaCachePath({ }; } + if (!wikiCachePath) { + return { + annotation: 'wiki cache path not provided', + mediaCachePath: null, + }; + } + let mediaIncludesThumbnailCache; try { const files = await readdir(mediaPath); mediaIncludesThumbnailCache = files.includes(CACHE_FILE); - } catch (error) { + } catch { mediaIncludesThumbnailCache = false; } @@ -648,24 +654,33 @@ export async function determineMediaCachePath({ // Two inferred paths are possible - "adjacent" and "contained". // "Contained" is the preferred format and we'll create it if - // wikiCachePath is provided, but if it *isn't* we won't know - // where to create it. Since "adjacent" isn't preferred we don't - // ever generate it, and we'd prefer not to *newly* generate - // thumbs in-place with mediaPath, so give up - we've already - // determined mediaPath doesn't include in-place thumbs. - - const adjacentInferredPath = - path.join( - path.dirname(mediaPath), - path.basename(mediaPath) + '-cache'); + // neither of the inferred paths exists. (Of course, by this + // point we've already determined that the media path itself + // isn't doubling as the thumbnail cache.) const containedInferredPath = (wikiCachePath ? path.join(wikiCachePath, 'media-cache') : null); - let adjacentIncludesThumbnailCache; + const adjacentInferredPath = + path.join( + path.dirname(mediaPath), + path.basename(mediaPath) + '-cache'); + let containedIncludesThumbnailCache; + let adjacentIncludesThumbnailCache; + + try { + const files = await readdir(containedInferredPath); + containedIncludesThumbnailCache = files.includes(CACHE_FILE); + } catch (error) { + if (error.code === 'ENOENT') { + containedIncludesThumbnailCache = null; + } else { + containedIncludesThumbnailCache = undefined; + } + } try { const files = await readdir(adjacentInferredPath); @@ -678,19 +693,6 @@ export async function determineMediaCachePath({ } } - if (wikiCachePath) { - try { - const files = await readdir(containedInferredPath); - containedIncludesThumbnailCache = files.includes(CACHE_FILE); - } catch (error) { - if (error.code === 'ENOENT') { - containedIncludesThumbnailCache = null; - } else { - containedIncludesThumbnailCache = undefined; - } - } - } - // Go ahead with the contained path if it exists and contains a cache - // no other conditions matter. if (containedIncludesThumbnailCache === true) { @@ -712,7 +714,7 @@ export async function determineMediaCachePath({ // Throw a very high-priority tantrum if the contained cache exists but // isn't readable. It's the preferred cache and we can't tell if it's // available for use or not! - if (wikiCachePath && containedIncludesThumbnailCache === undefined) { + if (containedIncludesThumbnailCache === undefined) { return { annotation: `contained path not readable`, mediaCachePath: null, @@ -764,28 +766,12 @@ export async function determineMediaCachePath({ } } - // If wikiCachePath was provided and the contained cache just doesn't - // exist yet, we'll create it during this run. - if (wikiCachePath && containedIncludesThumbnailCache === null) { - return { - annotation: `contained path will be created`, - mediaCachePath: containedInferredPath, - }; - } - - // If the adjacent cache doesn't exist, too dang bad! - // We aren't interested in newly creating it, so - // don't count it as an option. - - // Similarly, we've already established mediaPath isn't - // currently doubling as the thumbnail cache, and we won't - // newly start generating thumbnails here either. - - // All options aside struck out, there's no way to continue. - + // If we haven't found any information about either inferred + // location (and so have fallen back to this base case), we'll + // create the contained cache during this run. return { - annotation: `missing wiki cache to create media cache inside`, - mediaCachePath: null, + annotation: `contained path will be created`, + mediaCachePath: containedInferredPath, }; } @@ -888,7 +874,7 @@ export async function migrateThumbsIntoDedicatedCacheDirectory({ path.join(mediaPath, CACHE_FILE), path.join(mediaCachePath, CACHE_FILE)); logInfo`Moved thumbnail cache file.`; - } catch (error) { + } catch { logWarn`Failed to move cache file. (${CACHE_FILE})`; logWarn`Check its permissions, or try copying/pasting.`; } @@ -1127,33 +1113,23 @@ export default async function genThumbs({ const writeMessageFn = () => `Writing image thumbnails. [failed: ${numFailed}]`; - const generateCallImageIndices = - imageThumbtacksNeeded - .flatMap(({length}, index) => - Array.from({length}, () => index)); - - const generateCallImagePaths = - generateCallImageIndices - .map(index => imagePaths[index]); - - const generateCallThumbtacks = - imageThumbtacksNeeded.flat(); - const generateCallFns = stitchArrays({ - imagePath: generateCallImagePaths, - thumbtack: generateCallThumbtacks, - }).map(({imagePath, thumbtack}) => () => - generateImageThumbnail(imagePath, thumbtack, { + imagePath: imagePaths, + thumbtacks: imageThumbtacksNeeded, + }).map(({imagePath, thumbtacks}) => () => + generateImageThumbnails(imagePath, thumbtacks, { mediaPath, mediaCachePath, spawnConvert, }).catch(error => { numFailed++; - return ({error}); + return {error}; })); - logInfo`Generating ${generateCallFns.length} thumbnails for ${imagePaths.length} media files.`; + const totalThumbs = imageThumbtacksNeeded.reduce((sum, tacks) => sum + tacks.length, 0); + + logInfo`Generating ${totalThumbs} thumbnails for ${imagePaths.length} media files.`; if (generateCallFns.length > 500) { logInfo`Go get a latte - this could take a while!`; } @@ -1162,37 +1138,30 @@ export default async function genThumbs({ await progressPromiseAll(writeMessageFn, queue(generateCallFns, magickThreads)); - let successfulIndices; + let successfulPaths; { - const erroredIndices = generateCallImageIndices.slice(); - const erroredPaths = generateCallImagePaths.slice(); - const erroredThumbtacks = generateCallThumbtacks.slice(); + const erroredPaths = imagePaths.slice(); const errors = generateCallResults.map(result => result?.error); const {removed} = filterMultipleArrays( - erroredIndices, erroredPaths, - erroredThumbtacks, errors, - (_index, _imagePath, _thumbtack, error) => error); - - successfulIndices = new Set(removed[0]); + (_imagePath, error) => error); - const chunks = - chunkMultipleArrays(erroredPaths, erroredThumbtacks, errors, - (imagePath, lastImagePath) => imagePath !== lastImagePath); + ([successfulPaths] = removed); // TODO: This should obviously be an aggregate error. // ...Just like every other error report here, and those dang aggregates // should be constructable from within the queue, rather than after. - for (const [[imagePath], thumbtacks, errors] of chunks) { - logError`Failed to generate thumbnails for ${imagePath}:`; - for (const {thumbtack, error} of stitchArrays({thumbtack: thumbtacks, error: errors})) { - logError`- ${thumbtack}: ${error}`; - } - } + stitchArrays({ + imagePath: erroredPaths, + error: errors, + }).forEach(({imagePath, error}) => { + logError`Failed to generate thumbnails for ${imagePath}:`; + logError`- ${error}`; + }); if (empty(errors)) { logInfo`All needed thumbnails generated successfully - nice!`; @@ -1206,8 +1175,8 @@ export default async function genThumbs({ imagePaths, imageThumbtacksNeeded, imageDimensions, - (_imagePath, _thumbtacksNeeded, _dimensions, index) => - successfulIndices.has(index)); + (imagePath, _thumbtacksNeeded, _dimensions) => + successfulPaths.includes(imagePath)); for (const { imagePath, @@ -1269,24 +1238,20 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { const fromRoot = urls.from('media.root'); const paths = [ + wikiData.artworkData + .filter(artwork => artwork.path) + .map(artwork => fromRoot.to(...artwork.path)), + wikiData.albumData - .flatMap(album => [ - album.hasCoverArt && fromRoot.to('media.albumCover', album.directory, album.coverArtFileExtension), - !empty(CacheableObject.getUpdateValue(album, 'bannerArtistContribs')) && fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension), - !empty(CacheableObject.getUpdateValue(album, 'wallpaperArtistContribs')) && fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension), - ]) - .filter(Boolean), - - wikiData.artistData - .filter(artist => artist.hasAvatar) - .map(artist => fromRoot.to('media.artistAvatar', artist.directory, artist.avatarFileExtension)), - - wikiData.flashData - .map(flash => fromRoot.to('media.flashArt', flash.directory, flash.coverArtFileExtension)), - - wikiData.trackData - .filter(track => track.hasUniqueCoverArt) - .map(track => fromRoot.to('media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension)), + .flatMap(album => album.wallpaperParts + .filter(part => part.asset) + .map(part => + fromRoot.to('media.albumWallpaperPart', album.directory, part.asset))), + + wikiData.wikiInfo.wikiWallpaperParts + .filter(part => part.asset) + .map(part => + fromRoot.to('media.path', part.asset)), ].flat(); sortByName(paths, {getName: path => path}); |