Merge 44d0453ba3 into 96d521e149
commit
a8ccabb068
|
|
@ -27,6 +27,7 @@ class AssetMediaSize {
|
|||
static const fullsize = AssetMediaSize._(r'fullsize');
|
||||
static const preview = AssetMediaSize._(r'preview');
|
||||
static const thumbnail = AssetMediaSize._(r'thumbnail');
|
||||
static const micro = AssetMediaSize._(r'micro');
|
||||
|
||||
/// List of all possible values in this [enum][AssetMediaSize].
|
||||
static const values = <AssetMediaSize>[
|
||||
|
|
@ -34,6 +35,7 @@ class AssetMediaSize {
|
|||
fullsize,
|
||||
preview,
|
||||
thumbnail,
|
||||
micro,
|
||||
];
|
||||
|
||||
static AssetMediaSize? fromJson(dynamic value) => AssetMediaSizeTypeTransformer().decode(value);
|
||||
|
|
@ -76,6 +78,7 @@ class AssetMediaSizeTypeTransformer {
|
|||
case r'fullsize': return AssetMediaSize.fullsize;
|
||||
case r'preview': return AssetMediaSize.preview;
|
||||
case r'thumbnail': return AssetMediaSize.thumbnail;
|
||||
case r'micro': return AssetMediaSize.micro;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
|
|
|||
|
|
@ -16932,7 +16932,8 @@
|
|||
"original",
|
||||
"fullsize",
|
||||
"preview",
|
||||
"thumbnail"
|
||||
"thumbnail",
|
||||
"micro"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7123,7 +7123,8 @@ export enum AssetMediaSize {
|
|||
Original = "original",
|
||||
Fullsize = "fullsize",
|
||||
Preview = "preview",
|
||||
Thumbnail = "thumbnail"
|
||||
Thumbnail = "thumbnail",
|
||||
Micro = "micro"
|
||||
}
|
||||
export enum SourceType {
|
||||
MachineLearning = "machine-learning",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export enum AssetMediaSize {
|
|||
FULLSIZE = 'fullsize',
|
||||
PREVIEW = 'preview',
|
||||
THUMBNAIL = 'thumbnail',
|
||||
MICRO = 'micro',
|
||||
}
|
||||
|
||||
const AssetMediaSizeSchema = z.enum(AssetMediaSize).describe('Asset media size').meta({ id: 'AssetMediaSize' });
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ export enum AssetFileType {
|
|||
FullSize = 'fullsize',
|
||||
Preview = 'preview',
|
||||
Thumbnail = 'thumbnail',
|
||||
/**
|
||||
* A very small thumbnail for dense, zoomed-out grids where the regular
|
||||
* thumbnail would be needlessly large to fetch and decode.
|
||||
*/
|
||||
Micro = 'micro',
|
||||
Sidecar = 'sidecar',
|
||||
EncodedVideo = 'encoded_video',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,6 +277,12 @@ export class AssetMediaService extends BaseService {
|
|||
return { targetSize: AssetMediaSize.PREVIEW };
|
||||
}
|
||||
|
||||
if (dto.size === AssetMediaSize.MICRO && !path) {
|
||||
// downgrade to the regular thumbnail when the micro size has not been
|
||||
// generated yet (e.g. an existing library not yet re-thumbnailed).
|
||||
return { targetSize: AssetMediaSize.THUMBNAIL };
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new NotFoundException('Asset media not found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ describe(MediaService.name, () => {
|
|||
size: 1440,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -490,6 +490,14 @@ describe(MediaService.name, () => {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
{
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Micro,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
]);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer });
|
||||
});
|
||||
|
|
@ -540,6 +548,14 @@ describe(MediaService.name, () => {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
{
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Micro,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -589,6 +605,14 @@ describe(MediaService.name, () => {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
{
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Micro,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -707,7 +731,7 @@ describe(MediaService.name, () => {
|
|||
size: 1440,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -757,7 +781,7 @@ describe(MediaService.name, () => {
|
|||
size: 1440,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -824,6 +848,11 @@ describe(MediaService.name, () => {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: AssetFileType.Micro,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -863,6 +892,11 @@ describe(MediaService.name, () => {
|
|||
isProgressive: true,
|
||||
isTransparent: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: AssetFileType.Micro,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -889,6 +923,11 @@ describe(MediaService.name, () => {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: AssetFileType.Micro,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -1022,7 +1061,7 @@ describe(MediaService.name, () => {
|
|||
expect.objectContaining({ processInvalidImages: true }),
|
||||
);
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.objectContaining({ processInvalidImages: false }),
|
||||
|
|
@ -1064,7 +1103,7 @@ describe(MediaService.name, () => {
|
|||
size: 1440, // capped to preview size as fullsize conversion is skipped
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
fullsizeBuffer,
|
||||
{
|
||||
|
|
@ -1101,7 +1140,7 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
fullsizeBuffer,
|
||||
{
|
||||
|
|
@ -1149,7 +1188,7 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -1201,7 +1240,7 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -1233,7 +1272,7 @@ describe(MediaService.name, () => {
|
|||
size: 1440,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
|
|
@ -1266,7 +1305,7 @@ describe(MediaService.name, () => {
|
|||
size: undefined,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -1309,7 +1348,7 @@ describe(MediaService.name, () => {
|
|||
processInvalidImages: false,
|
||||
});
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
{
|
||||
|
|
@ -1342,7 +1381,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.objectContaining({
|
||||
|
|
@ -1464,7 +1503,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(4);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.anything(),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ interface UpsertFileOptions {
|
|||
|
||||
type ThumbnailAsset = NonNullable<Awaited<ReturnType<AssetJobRepository['getForGenerateThumbnailJob']>>>;
|
||||
|
||||
/// Fixed edge (px) for the micro thumbnail used by dense, zoomed-out grids. Small
|
||||
/// enough that fetching/decoding thousands of them stays cheap; it reuses the
|
||||
/// thumbnail's configured format and quality, only smaller.
|
||||
const MICRO_THUMBNAIL_SIZE = 128;
|
||||
|
||||
@Injectable()
|
||||
export class MediaService extends BaseService {
|
||||
videoInterfaces: VideoInterfaces = { dri: [], mali: false };
|
||||
|
|
@ -330,16 +335,27 @@ export class MediaService extends BaseService {
|
|||
isProgressive: !!image.thumbnail.progressive && thumbnailFormat !== ImageFormat.Webp,
|
||||
isTransparent,
|
||||
});
|
||||
// A very small thumbnail for dense zoomed-out grids; a smaller variant of the
|
||||
// thumbnail (same format/quality), so it stays tiny to fetch and decode.
|
||||
const microFile = this.getImageFile(asset, {
|
||||
fileType: AssetFileType.Micro,
|
||||
format: thumbnailFormat,
|
||||
isEdited: useEdits,
|
||||
isProgressive: false,
|
||||
isTransparent,
|
||||
});
|
||||
this.storageCore.ensureFolders(previewFile.path);
|
||||
|
||||
// generate final images
|
||||
const baseOptions = { colorspace, processInvalidImages: false, raw: info, edits: useEdits ? asset.edits : [] };
|
||||
const thumbnailOptions = { ...image.thumbnail, ...baseOptions, format: thumbnailFormat };
|
||||
const microOptions = { ...image.thumbnail, ...baseOptions, format: thumbnailFormat, size: MICRO_THUMBNAIL_SIZE };
|
||||
const previewOptions = { ...image.preview, ...baseOptions, format: previewFormat };
|
||||
const promises = [
|
||||
this.mediaRepository.generateThumbhash(data, baseOptions),
|
||||
this.mediaRepository.generateThumbnail(data, thumbnailOptions, thumbnailFile.path),
|
||||
this.mediaRepository.generateThumbnail(data, previewOptions, previewFile.path),
|
||||
this.mediaRepository.generateThumbnail(data, microOptions, microFile.path),
|
||||
];
|
||||
|
||||
let fullsizeFile: UpsertFileOptions | undefined;
|
||||
|
|
@ -398,7 +414,9 @@ export class MediaService extends BaseService {
|
|||
const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions;
|
||||
|
||||
return {
|
||||
files: fullsizeFile ? [previewFile, thumbnailFile, fullsizeFile] : [previewFile, thumbnailFile],
|
||||
files: fullsizeFile
|
||||
? [previewFile, thumbnailFile, microFile, fullsizeFile]
|
||||
: [previewFile, thumbnailFile, microFile],
|
||||
thumbhash: outputs[0] as Buffer,
|
||||
fullsizeDimensions,
|
||||
};
|
||||
|
|
@ -520,6 +538,13 @@ export class MediaService extends BaseService {
|
|||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
});
|
||||
const microFile = this.getImageFile(asset, {
|
||||
fileType: AssetFileType.Micro,
|
||||
format: image.thumbnail.format,
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
});
|
||||
this.storageCore.ensureFolders(previewFile.path);
|
||||
|
||||
const { videoStream, format } = asset;
|
||||
|
|
@ -529,11 +554,14 @@ export class MediaService extends BaseService {
|
|||
|
||||
const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() });
|
||||
const thumbConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() });
|
||||
const microConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: MICRO_THUMBNAIL_SIZE.toString() });
|
||||
const previewOptions = previewConfig.getCommand(TranscodeTarget.Video, videoStream, undefined, format ?? undefined);
|
||||
const thumbnailOptions = thumbConfig.getCommand(TranscodeTarget.Video, videoStream, undefined, format ?? undefined);
|
||||
const microOptions = microConfig.getCommand(TranscodeTarget.Video, videoStream, undefined, format ?? undefined);
|
||||
|
||||
await this.mediaRepository.transcode(asset.originalPath, previewFile.path, previewOptions);
|
||||
await this.mediaRepository.transcode(asset.originalPath, thumbnailFile.path, thumbnailOptions);
|
||||
await this.mediaRepository.transcode(asset.originalPath, microFile.path, microOptions);
|
||||
|
||||
const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path, {
|
||||
colorspace: image.colorspace,
|
||||
|
|
@ -541,7 +569,7 @@ export class MediaService extends BaseService {
|
|||
});
|
||||
|
||||
return {
|
||||
files: [previewFile, thumbnailFile],
|
||||
files: [previewFile, thumbnailFile, microFile],
|
||||
thumbhash,
|
||||
fullsizeDimensions: { width: videoStream.width, height: videoStream.height },
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue