mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
fix: queue assets missing fullsize files for thumbnail regeneration
This commit is contained in:
@@ -89,6 +89,14 @@ export class AssetJobRepository {
|
||||
.where('asset_file.type', '=', AssetFileType.Thumbnail),
|
||||
),
|
||||
),
|
||||
eb.not((eb) =>
|
||||
eb.exists((qb) =>
|
||||
qb
|
||||
.selectFrom('asset_file')
|
||||
.whereRef('assetId', '=', 'asset.id')
|
||||
.where('asset_file.type', '=', AssetFileType.FullSize),
|
||||
),
|
||||
),
|
||||
eb('asset.thumbhash', 'is', null),
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -165,6 +165,35 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue all assets with missing fullsize when feature is enabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noFullsize]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should not queue assets with missing fullsize when feature is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noFullsize]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue assets with edits but missing edited thumbnails', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
@@ -181,6 +210,35 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should not queue assets with missing edited fullsize when feature is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue assets with missing fullsize when force is true, regardless of setting', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noFullsize]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue both regular and edited thumbnails for assets with edits when force is true', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
|
||||
@@ -68,6 +68,7 @@ export class MediaService extends BaseService {
|
||||
|
||||
@OnJob({ name: JobName.AssetGenerateThumbnailsQueueAll, queue: QueueName.ThumbnailGeneration })
|
||||
async handleQueueGenerateThumbnails({ force }: JobOf<JobName.AssetGenerateThumbnailsQueueAll>): Promise<JobStatus> {
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
let jobs: JobItem[] = [];
|
||||
|
||||
const queueAll = async () => {
|
||||
@@ -78,13 +79,22 @@ export class MediaService extends BaseService {
|
||||
for await (const asset of this.assetJobRepository.streamForThumbnailJob(!!force)) {
|
||||
const assetFiles = getAssetFiles(asset.files);
|
||||
|
||||
if (!assetFiles.previewFile || !assetFiles.thumbnailFile || !asset.thumbhash || force) {
|
||||
if (
|
||||
!assetFiles.previewFile ||
|
||||
!assetFiles.thumbnailFile ||
|
||||
(config.image.fullsize.enabled && !assetFiles.fullsizeFile) ||
|
||||
!asset.thumbhash ||
|
||||
force
|
||||
) {
|
||||
jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } });
|
||||
}
|
||||
|
||||
if (
|
||||
asset.edits.length > 0 &&
|
||||
(!assetFiles.editedPreviewFile || !assetFiles.editedThumbnailFile || !assetFiles.editedFullsizeFile || force)
|
||||
(!assetFiles.editedPreviewFile ||
|
||||
!assetFiles.editedThumbnailFile ||
|
||||
(config.image.fullsize.enabled && !assetFiles.editedFullsizeFile) ||
|
||||
force)
|
||||
) {
|
||||
jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } });
|
||||
}
|
||||
|
||||
42
server/test/fixtures/asset.stub.ts
vendored
42
server/test/fixtures/asset.stub.ts
vendored
@@ -50,6 +50,8 @@ const editedFullsizeFile = factory.assetFile({
|
||||
|
||||
const files = [fullsizeFile, previewFile, thumbnailFile];
|
||||
|
||||
const filesWithoutFullsize = [previewFile, thumbnailFile];
|
||||
|
||||
const editedFiles = [
|
||||
fullsizeFile,
|
||||
previewFile,
|
||||
@@ -213,6 +215,46 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
noFullsize: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/data/library/IMG_789.jpg',
|
||||
files: filesWithoutFullsize,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'IMG_789.jpg',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
exifInfo: {} as Exif,
|
||||
}),
|
||||
|
||||
primaryImage: Object.freeze({
|
||||
id: 'primary-asset-id',
|
||||
status: AssetStatus.Active,
|
||||
|
||||
Reference in New Issue
Block a user