feat(server): visibility column (#17939)
* feat: private view * pr feedback * sql generation * feat: visibility column * fix: set visibility value as the same as the still part after unlinked live photos * fix: test * pr feedback
This commit is contained in:
@@ -9,7 +9,7 @@ import { AssetFile } from 'src/database';
|
||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
|
||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||
@@ -142,7 +142,6 @@ const createDto = Object.freeze({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
duration: '0:00:00.000000',
|
||||
}) as AssetMediaCreateDto;
|
||||
|
||||
@@ -164,7 +163,6 @@ const assetEntity = Object.freeze({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
updatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
encodedVideoPath: '',
|
||||
duration: '0:00:00.000000',
|
||||
files: [] as AssetFile[],
|
||||
@@ -437,7 +435,10 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should hide the linked motion asset', async () => {
|
||||
mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||
mocks.asset.getById.mockResolvedValueOnce({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
|
||||
await expect(
|
||||
@@ -452,7 +453,10 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: 'live-photo-motion-asset',
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a sidecar file', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
||||
import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { UploadFile } from 'src/types';
|
||||
@@ -146,7 +146,6 @@ export class AssetMediaService extends BaseService {
|
||||
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
|
||||
);
|
||||
}
|
||||
|
||||
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
|
||||
|
||||
await this.userRepository.updateUsage(auth.user.id, file.size);
|
||||
@@ -416,9 +415,8 @@ export class AssetMediaService extends BaseService {
|
||||
|
||||
type: mimeTypes.assetType(file.originalPath),
|
||||
isFavorite: dto.isFavorite,
|
||||
isArchived: dto.isArchived ?? false,
|
||||
duration: dto.duration || null,
|
||||
isVisible: dto.isVisible ?? true,
|
||||
visibility: dto.visibility ?? AssetVisibility.TIMELINE,
|
||||
livePhotoVideoId: dto.livePhotoVideoId,
|
||||
originalFileName: file.originalName,
|
||||
sidecarPath: sidecarFile?.originalPath,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@@ -46,14 +46,22 @@ describe(AssetService.name, () => {
|
||||
describe('getStatistics', () => {
|
||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
|
||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual(
|
||||
statResponse,
|
||||
);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for archived assets', async () => {
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
|
||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual(
|
||||
statResponse,
|
||||
);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for favorite assets', async () => {
|
||||
@@ -192,9 +200,9 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('update', () => {
|
||||
it('should require asset write access for the id', async () => {
|
||||
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
await expect(
|
||||
sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -242,7 +250,10 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
@@ -263,7 +274,10 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
@@ -284,7 +298,10 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
@@ -296,7 +313,7 @@ describe(AssetService.name, () => {
|
||||
mocks.asset.getById.mockResolvedValueOnce({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
ownerId: authStub.admin.user.id,
|
||||
isVisible: true,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
@@ -305,7 +322,10 @@ describe(AssetService.name, () => {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
@@ -335,7 +355,10 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: null,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: assetStub.livePhotoStillAsset.visibility,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
@@ -361,7 +384,6 @@ describe(AssetService.name, () => {
|
||||
await expect(
|
||||
sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
isArchived: false,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
@@ -369,9 +391,11 @@ describe(AssetService.name, () => {
|
||||
it('should update all assets', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE });
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||
@@ -381,7 +405,6 @@ describe(AssetService.name, () => {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
isArchived: undefined,
|
||||
isFavorite: undefined,
|
||||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
@@ -389,14 +412,14 @@ describe(AssetService.name, () => {
|
||||
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update Assets table if isArchived field is provided', async () => {
|
||||
it('should update Assets table if visibility field is provided', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
|
||||
await sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
isArchived: undefined,
|
||||
visibility: undefined,
|
||||
isFavorite: false,
|
||||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
@@ -416,7 +439,6 @@ describe(AssetService.name, () => {
|
||||
latitude: 30,
|
||||
longitude: 50,
|
||||
dateTimeOriginal,
|
||||
isArchived: undefined,
|
||||
isFavorite: false,
|
||||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
@@ -439,7 +461,6 @@ describe(AssetService.name, () => {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
isArchived: undefined,
|
||||
isFavorite: undefined,
|
||||
duplicateId: null,
|
||||
rating: undefined,
|
||||
|
||||
@@ -92,8 +92,12 @@ export class AssetService extends BaseService {
|
||||
|
||||
const asset = await this.assetRepository.update({ id, ...rest });
|
||||
|
||||
if (previousMotion) {
|
||||
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
|
||||
if (previousMotion && asset) {
|
||||
await onAfterUnlink(repos, {
|
||||
userId: auth.user.id,
|
||||
livePhotoVideoId: previousMotion.id,
|
||||
visibility: asset.visibility,
|
||||
});
|
||||
}
|
||||
|
||||
if (!asset) {
|
||||
@@ -115,7 +119,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
if (
|
||||
options.isArchived !== undefined ||
|
||||
options.visibility !== undefined ||
|
||||
options.isFavorite !== undefined ||
|
||||
options.duplicateId !== undefined ||
|
||||
options.rating !== undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetFileType, AssetType, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetFileType, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@@ -22,11 +22,11 @@ const hasEmbedding = {
|
||||
updateId: 'update-1',
|
||||
},
|
||||
],
|
||||
isVisible: true,
|
||||
stackId: null,
|
||||
type: AssetType.IMAGE,
|
||||
duplicateId: null,
|
||||
embedding: '[1, 2, 3, 4]',
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
};
|
||||
|
||||
const hasDupe = {
|
||||
@@ -207,7 +207,10 @@ describe(SearchService.name, () => {
|
||||
|
||||
it('should skip if asset is not visible', async () => {
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, isVisible: false });
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({
|
||||
...hasEmbedding,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OnJob } from 'src/decorators';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
|
||||
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { AssetFileType, AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { AssetDuplicateResult } from 'src/repositories/search.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
@@ -65,7 +65,7 @@ export class DuplicateService extends BaseService {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
if (asset.visibility == AssetVisibility.HIDDEN) {
|
||||
this.logger.debug(`Asset ${id} is not visible, skipping`);
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import {
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
BootstrapEventPriority,
|
||||
ImmichWorker,
|
||||
JobCommand,
|
||||
@@ -301,7 +302,7 @@ export class JobService extends BaseService {
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
if (asset.isVisible) {
|
||||
if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) {
|
||||
this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AssetFileType,
|
||||
AssetPathType,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
JobName,
|
||||
@@ -152,7 +153,7 @@ export class MediaService extends BaseService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
if (asset.visibility === AssetVisibility.HIDDEN) {
|
||||
this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`);
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Stats } from 'node:fs';
|
||||
import { constants } from 'node:fs/promises';
|
||||
import { defaults } from 'src/config';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
|
||||
import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@@ -504,7 +504,10 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should not apply motion photos if asset is video', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||
@@ -513,7 +516,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
|
||||
expect.objectContaining({ assetType: AssetType.VIDEO, visibility: AssetVisibility.HIDDEN }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -580,7 +583,7 @@ describe(MetadataService.name, () => {
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
isVisible: false,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
@@ -638,7 +641,7 @@ describe(MetadataService.name, () => {
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
isVisible: false,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
@@ -696,7 +699,7 @@ describe(MetadataService.name, () => {
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
isVisible: false,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
@@ -773,14 +776,17 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||
mocks.asset.getByChecksum.mockResolvedValue({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
isVisible: false,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
@@ -1301,7 +1307,9 @@ describe(MetadataService.name, () => {
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
||||
);
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1320,7 +1328,9 @@ describe(MetadataService.name, () => {
|
||||
libraryId: null,
|
||||
type: AssetType.IMAGE,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
||||
);
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1342,7 +1352,10 @@ describe(MetadataService.name, () => {
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AssetFaces, Exif, Person } from 'src/db';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import {
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
DatabaseLock,
|
||||
ExifOrientation,
|
||||
ImmichWorker,
|
||||
@@ -156,7 +157,7 @@ export class MetadataService extends BaseService {
|
||||
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
|
||||
await Promise.all([
|
||||
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
|
||||
this.assetRepository.update({ id: motionAsset.id, isVisible: false }),
|
||||
this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }),
|
||||
this.albumRepository.removeAsset(motionAsset.id),
|
||||
]);
|
||||
|
||||
@@ -527,8 +528,11 @@ export class MetadataService extends BaseService {
|
||||
});
|
||||
|
||||
// Hide the motion photo video asset if it's not already hidden to prepare for linking
|
||||
if (motionAsset.isVisible) {
|
||||
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
||||
if (motionAsset.visibility === AssetVisibility.TIMELINE) {
|
||||
await this.assetRepository.update({
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
|
||||
}
|
||||
} else {
|
||||
@@ -544,7 +548,7 @@ export class MetadataService extends BaseService {
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
|
||||
isVisible: false,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
});
|
||||
@@ -863,7 +867,7 @@ export class MetadataService extends BaseService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
|
||||
if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from 'src/dtos/person.dto';
|
||||
import {
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
CacheControl,
|
||||
ImageFormat,
|
||||
JobName,
|
||||
@@ -296,7 +297,7 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
if (asset.visibility === AssetVisibility.HIDDEN) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
@@ -484,7 +485,9 @@ export class PersonService extends BaseService {
|
||||
|
||||
this.logger.debug(`Face ${id} has ${matches.length} matches`);
|
||||
|
||||
const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived;
|
||||
const isCore =
|
||||
matches.length >= machineLearning.facialRecognition.minFaces &&
|
||||
face.asset.visibility === AssetVisibility.TIMELINE;
|
||||
if (!isCore && !deferred) {
|
||||
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
||||
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
@@ -104,7 +104,7 @@ export class SmartInfoService extends BaseService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
if (asset.visibility === AssetVisibility.HIDDEN) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SyncAckSetDto,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
import { DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { SyncAck } from 'src/types';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
@@ -262,7 +262,10 @@ export class SyncService extends BaseService {
|
||||
needsFullSync: false,
|
||||
upserted: upserted
|
||||
// do not return archived assets for partner users
|
||||
.filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived))
|
||||
.filter(
|
||||
(a) =>
|
||||
a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.TIMELINE),
|
||||
)
|
||||
.map((a) =>
|
||||
mapAsset(a, {
|
||||
auth,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||
import { TimelineService } from 'src/services/timeline.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@@ -54,7 +55,7 @@ describe(TimelineService.name, () => {
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
@@ -63,7 +64,7 @@ describe(TimelineService.name, () => {
|
||||
expect.objectContaining({
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
userIds: [authStub.admin.user.id],
|
||||
}),
|
||||
);
|
||||
@@ -77,7 +78,7 @@ describe(TimelineService.name, () => {
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: false,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
userId: authStub.admin.user.id,
|
||||
withPartners: true,
|
||||
}),
|
||||
@@ -85,7 +86,7 @@ describe(TimelineService.name, () => {
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: false,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
withPartners: true,
|
||||
userIds: [authStub.admin.user.id],
|
||||
});
|
||||
@@ -120,7 +121,7 @@ describe(TimelineService.name, () => {
|
||||
const buckets = await sut.getTimeBucket(auth, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
albumId: 'album-id',
|
||||
});
|
||||
|
||||
@@ -129,7 +130,7 @@ describe(TimelineService.name, () => {
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
albumId: 'album-id',
|
||||
});
|
||||
});
|
||||
@@ -154,12 +155,12 @@ describe(TimelineService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
||||
it('should throw an error if withParners is true and visibility true or undefined', async () => {
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
visibility: AssetVisibility.ARCHIVE,
|
||||
withPartners: true,
|
||||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
@@ -169,7 +170,7 @@ describe(TimelineService.name, () => {
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: undefined,
|
||||
visibility: undefined,
|
||||
withPartners: true,
|
||||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AssetVisibility, Permission } from 'src/enum';
|
||||
import { TimeBucketOptions } from 'src/repositories/asset.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
@@ -55,7 +55,7 @@ export class TimelineService extends BaseService {
|
||||
|
||||
if (dto.userId) {
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
||||
if (dto.isArchived !== false) {
|
||||
if (dto.visibility === AssetVisibility.ARCHIVE) {
|
||||
await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class TimelineService extends BaseService {
|
||||
}
|
||||
|
||||
if (dto.withPartners) {
|
||||
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
|
||||
const requestedArchived = dto.visibility === AssetVisibility.ARCHIVE || dto.visibility === undefined;
|
||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const requestedTrash = dto.isTrashed === true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user