mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore!: duration in milliseconds (#28003)
* server changes * openapi * web changes * mobile changes * assume 3.0 client * deprecate * review feedback * update medium tests * linting
This commit is contained in:
@@ -101,7 +101,7 @@ export class JobService extends BaseService {
|
||||
const edits = await this.assetEditRepository.getWithSyncInfo(item.data.id);
|
||||
|
||||
if (asset) {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV2', asset.ownerId, {
|
||||
asset: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
@@ -156,7 +156,7 @@ export class JobService extends BaseService {
|
||||
this.websocketRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
|
||||
if (asset.exifInfo) {
|
||||
const exif = asset.exifInfo;
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV2', asset.ownerId, {
|
||||
// TODO remove `on_upload_success` and then modify the query to select only the required fields)
|
||||
asset: {
|
||||
id: asset.id,
|
||||
|
||||
@@ -999,7 +999,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '00:00:06.210',
|
||||
duration: 6210,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1067,7 +1067,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '168:00:00.000',
|
||||
duration: 604_800_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1080,7 +1080,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
@@ -1092,7 +1092,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
@@ -1121,7 +1121,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 456_000 }));
|
||||
});
|
||||
|
||||
it('should trim whitespace from description', async () => {
|
||||
|
||||
@@ -1001,18 +1001,10 @@ export class MetadataService extends BaseService {
|
||||
return bitsPerSample;
|
||||
}
|
||||
|
||||
private getDuration(tags: ImmichTags): string | null {
|
||||
private getDuration(tags: ImmichTags): number | null {
|
||||
const duration = tags.Duration;
|
||||
|
||||
if (typeof duration === 'string') {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (typeof duration === 'number') {
|
||||
return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS');
|
||||
}
|
||||
|
||||
return null;
|
||||
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
|
||||
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
|
||||
}
|
||||
|
||||
private async getVideoTags(originalPath: string) {
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
SyncAckDeleteDto,
|
||||
SyncAckSetDto,
|
||||
syncAlbumV2ToV1,
|
||||
syncAssetFaceV2ToV1,
|
||||
SyncAssetV1,
|
||||
SyncAssetV2,
|
||||
SyncItem,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
@@ -22,7 +21,7 @@ import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||
|
||||
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
||||
type AssetLike = Omit<SyncAssetV2, 'checksum' | 'thumbhash'> & {
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||
};
|
||||
@@ -31,7 +30,7 @@ const COMPLETE_ID = 'complete';
|
||||
const MAX_DAYS = 30;
|
||||
const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS });
|
||||
|
||||
const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({
|
||||
const mapSyncAssetV2 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV2 => ({
|
||||
...data,
|
||||
checksum: hexOrBufferToBase64(checksum),
|
||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||
@@ -56,10 +55,13 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
SyncRequestType.AssetsV2,
|
||||
SyncRequestType.StacksV1,
|
||||
SyncRequestType.PartnerAssetsV1,
|
||||
SyncRequestType.PartnerAssetsV2,
|
||||
SyncRequestType.PartnerStacksV1,
|
||||
SyncRequestType.AlbumAssetsV1,
|
||||
SyncRequestType.AlbumAssetsV2,
|
||||
SyncRequestType.AlbumsV1,
|
||||
SyncRequestType.AlbumsV2,
|
||||
SyncRequestType.AlbumUsersV1,
|
||||
@@ -156,20 +158,26 @@ export class SyncService extends BaseService {
|
||||
const options: SyncQueryOptions = { nowId, userId: auth.user.id };
|
||||
|
||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||
// deprecated handlers
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(),
|
||||
[SyncRequestType.AssetFacesV1]: () => this.syncAssetFacesV1(),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(),
|
||||
|
||||
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV2]: () => this.syncAssetsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.PartnerAssetsV2]: () => this.syncPartnerAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumsV2]: () => this.syncAlbumsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV2]: () => this.syncAlbumAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () =>
|
||||
this.syncAlbumAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
@@ -178,13 +186,12 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV2]: () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||
};
|
||||
} as const;
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
const handler = handlers[type];
|
||||
const handler = handlers[type as keyof typeof handlers];
|
||||
await handler();
|
||||
}
|
||||
|
||||
@@ -260,21 +267,31 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
private syncAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException('SyncRequestType.AssetsV1 is deprecated, use SyncRequestType.AssetsV2 instead');
|
||||
}
|
||||
|
||||
private async syncAssetsV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AssetDeleteV1;
|
||||
const deletes = this.syncRepository.asset.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetV1;
|
||||
const upsertType = SyncEntityType.AssetV2;
|
||||
const upserts = this.syncRepository.asset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV1(
|
||||
private syncPartnerAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.PartnerAssetsV1 is deprecated, use SyncRequestType.PartnerAssetsV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
@@ -286,13 +303,13 @@ export class SyncService extends BaseService {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const partners = await this.syncRepository.partner.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const upsertType = SyncEntityType.PartnerAssetV1;
|
||||
const upsertType = SyncEntityType.PartnerAssetV2;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
@@ -313,7 +330,7 @@ export class SyncService extends BaseService {
|
||||
send(response, {
|
||||
type: backfillType,
|
||||
ids: [createId, updateId],
|
||||
data: mapSyncAssetV1(data),
|
||||
data: mapSyncAssetV2(data),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,7 +346,7 @@ export class SyncService extends BaseService {
|
||||
|
||||
const upserts = this.syncRepository.partnerAsset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,20 +507,26 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV1(
|
||||
private syncAlbumAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.AlbumAssetsV1 is deprecated, use SyncRequestType.AlbumAssetsV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const updateType = SyncEntityType.AlbumAssetUpdateV1;
|
||||
const createType = SyncEntityType.AlbumAssetCreateV1;
|
||||
const updateType = SyncEntityType.AlbumAssetUpdateV2;
|
||||
const createType = SyncEntityType.AlbumAssetCreateV2;
|
||||
const updateCheckpoint = checkpointMap[updateType];
|
||||
const createCheckpoint = checkpointMap[createType];
|
||||
if (createCheckpoint) {
|
||||
@@ -522,7 +545,7 @@ export class SyncService extends BaseService {
|
||||
);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
@@ -541,7 +564,7 @@ export class SyncService extends BaseService {
|
||||
createCheckpoint,
|
||||
);
|
||||
for await (const { updateId, ...data } of updates) {
|
||||
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,12 +575,12 @@ export class SyncService extends BaseService {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV1,
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV2,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,19 +825,10 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetFacesV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
||||
const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetFaceV1;
|
||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
const v1 = syncAssetFaceV2ToV1(data);
|
||||
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
||||
}
|
||||
private syncAssetFacesV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.AssetFacesV1 is deprecated, use SyncRequestType.AssetFacesV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAssetFacesV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
|
||||
Reference in New Issue
Block a user