mirror of
https://github.com/immich-app/immich.git
synced 2025-01-23 12:12:45 +01:00
wip
This commit is contained in:
parent
987f8796a8
commit
8bc9b668a0
17 changed files with 186 additions and 170 deletions
e2e/src/api/specs
mobile/openapi
open-api
server
web/src/routes/admin/library-management
|
@ -716,7 +716,7 @@ describe('/libraries', () => {
|
|||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets).toEqual(assetsBefore);
|
||||
expect(assets.items.map((asset) => asset.id)).toEqual(assetsBefore.items.map((asset) => asset.id));
|
||||
});
|
||||
|
||||
describe('xmp metadata', async () => {
|
||||
|
@ -735,12 +735,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2000-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -761,12 +766,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2000-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -788,12 +798,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2000-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -824,12 +839,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2010-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -858,12 +878,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2000-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -892,12 +917,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2000-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -928,12 +958,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2010-09-27T12:35:33+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -963,12 +998,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2010-07-20T17:27:12+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -998,12 +1038,17 @@ describe('/libraries', () => {
|
|||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
withExif: true,
|
||||
});
|
||||
|
||||
expect(newAssets.items).toEqual([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'glarus.nef',
|
||||
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2010-07-20T17:27:12+00:00',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/libraries_api.dart
generated
BIN
mobile/openapi/lib/api/libraries_api.dart
generated
Binary file not shown.
|
@ -2853,9 +2853,44 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/libraries/{id}/count": {
|
||||
"/libraries/{id}/scan": {
|
||||
"post": {
|
||||
"operationId": "scanLibrary",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Libraries"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/libraries/{id}/statistics": {
|
||||
"get": {
|
||||
"operationId": "getAssetCount",
|
||||
"operationId": "getLibraryStatistics",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -2895,41 +2930,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/libraries/{id}/scan": {
|
||||
"post": {
|
||||
"operationId": "scanLibrary",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Libraries"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/libraries/{id}/validate": {
|
||||
"post": {
|
||||
"operationId": "validate",
|
||||
|
@ -8450,12 +8450,10 @@
|
|||
},
|
||||
"fileCreatedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"fileModifiedAt": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"hasMetadata": {
|
||||
|
@ -8488,7 +8486,6 @@
|
|||
},
|
||||
"localDateTime": {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"originalFileName": {
|
||||
|
|
|
@ -2093,16 +2093,6 @@ export function updateLibrary({ id, updateLibraryDto }: {
|
|||
body: updateLibraryDto
|
||||
})));
|
||||
}
|
||||
export function getAssetCount({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: number;
|
||||
}>(`/libraries/${encodeURIComponent(id)}/count`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function scanLibrary({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
@ -2111,6 +2101,16 @@ export function scanLibrary({ id }: {
|
|||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getLibraryStatistics({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: number;
|
||||
}>(`/libraries/${encodeURIComponent(id)}/statistics`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function validate({ id, validateLibraryDto }: {
|
||||
id: string;
|
||||
validateLibraryDto: ValidateLibraryDto;
|
||||
|
|
|
@ -56,9 +56,9 @@ export class LibraryController {
|
|||
return this.service.validate(id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/count')
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true })
|
||||
getAssetCount(@Param() { id }: UUIDParamDto): Promise<number> {
|
||||
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<number> {
|
||||
return this.service.getAssetCount(id);
|
||||
}
|
||||
|
||||
|
|
6
server/src/db.d.ts
vendored
6
server/src/db.d.ts
vendored
|
@ -121,8 +121,8 @@ export interface Assets {
|
|||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
encodedVideoPath: Generated<string | null>;
|
||||
fileCreatedAt: Timestamp | null;
|
||||
fileModifiedAt: Timestamp | null;
|
||||
fileCreatedAt: Timestamp;
|
||||
fileModifiedAt: Timestamp;
|
||||
id: Generated<string>;
|
||||
isArchived: Generated<boolean>;
|
||||
isExternal: Generated<boolean>;
|
||||
|
@ -131,7 +131,7 @@ export interface Assets {
|
|||
isVisible: Generated<boolean>;
|
||||
libraryId: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
localDateTime: Timestamp | null;
|
||||
localDateTime: Timestamp;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
|
|
|
@ -165,8 +165,8 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
|||
const hasSharedLink = entity.sharedLinks?.length > 0;
|
||||
const hasSharedUser = sharedUsers.length > 0;
|
||||
|
||||
let startDate = getAssetDateTime(assets.at(0)) ?? undefined;
|
||||
let endDate = getAssetDateTime(assets.at(-1)) ?? undefined;
|
||||
let startDate = getAssetDateTime(assets.at(0));
|
||||
let endDate = getAssetDateTime(assets.at(-1));
|
||||
// Swap dates if start date is greater than end date.
|
||||
if (startDate && endDate && startDate > endDate) {
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
|
|
|
@ -21,7 +21,7 @@ export class SanitizedAssetResponseDto {
|
|||
type!: AssetType;
|
||||
thumbhash!: string | null;
|
||||
originalMimeType?: string;
|
||||
localDateTime!: Date | null;
|
||||
localDateTime!: Date;
|
||||
duration!: string;
|
||||
livePhotoVideoId?: string | null;
|
||||
hasMetadata!: boolean;
|
||||
|
@ -36,8 +36,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
libraryId?: string | null;
|
||||
originalPath!: string;
|
||||
originalFileName!: string;
|
||||
fileCreatedAt!: Date | null;
|
||||
fileModifiedAt!: Date | null;
|
||||
fileCreatedAt!: Date;
|
||||
fileModifiedAt!: Date;
|
||||
updatedAt!: Date;
|
||||
isFavorite!: boolean;
|
||||
isArchived!: boolean;
|
||||
|
|
|
@ -100,14 +100,14 @@ export class AssetEntity {
|
|||
deletedAt!: Date | null;
|
||||
|
||||
@Index('idx_asset_file_created_at')
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
fileCreatedAt!: Date | null;
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
localDateTime!: Date | null;
|
||||
@Column({ type: 'timestamptz' })
|
||||
localDateTime!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
fileModifiedAt!: Date | null;
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileModifiedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: boolean;
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class NullableDates1736718596137 implements MigrationInterface {
|
||||
name = 'NullableDates1736718596137'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { CompiledQuery, Insertable, Kysely, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { Insertable, Kysely, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { ASSET_FILE_CONFLICT_KEYS, EXIF_CONFLICT_KEYS, JOB_STATUS_CONFLICT_KEYS } from 'src/constants';
|
||||
|
|
|
@ -546,7 +546,7 @@ describe(AssetMediaService.name, () => {
|
|||
files: [
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
createdAt: assetStub.image.fileCreatedAt,
|
||||
createdAt: assetStub.image.fileCreatedAt ?? new Date(),
|
||||
id: '42',
|
||||
path: '/path/to/preview',
|
||||
type: AssetFileType.THUMBNAIL,
|
||||
|
@ -566,7 +566,7 @@ describe(AssetMediaService.name, () => {
|
|||
files: [
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
createdAt: assetStub.image.fileCreatedAt,
|
||||
createdAt: assetStub.image.fileCreatedAt ?? new Date(),
|
||||
id: '42',
|
||||
path: '/path/to/preview.jpg',
|
||||
type: AssetFileType.PREVIEW,
|
||||
|
|
|
@ -229,11 +229,10 @@ export class LibraryService extends BaseService {
|
|||
|
||||
let progressMessage = '';
|
||||
|
||||
if (job.progressCounter && job.totalAssets) {
|
||||
progressMessage = `(${job.progressCounter} of ${job.totalAssets})`;
|
||||
} else {
|
||||
progressMessage = `(${job.progressCounter} done so far)`;
|
||||
}
|
||||
progressMessage =
|
||||
job.progressCounter && job.totalAssets
|
||||
? `(${job.progressCounter} of ${job.totalAssets})`
|
||||
: `(${job.progressCounter} done so far)`;
|
||||
|
||||
this.logger.log(`Imported ${assetIds.length} ${progressMessage} file(s) into library ${job.libraryId}`);
|
||||
|
||||
|
@ -362,10 +361,9 @@ export class LibraryService extends BaseService {
|
|||
checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`),
|
||||
originalPath: assetPath,
|
||||
|
||||
// These dates are placeholders and will be read from disk during metadata extraction
|
||||
fileCreatedAt: null,
|
||||
fileModifiedAt: null,
|
||||
localDateTime: null,
|
||||
fileCreatedAt: new Date(),
|
||||
fileModifiedAt: new Date(),
|
||||
localDateTime: new Date(),
|
||||
// TODO: device asset id is deprecated, remove it
|
||||
deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''),
|
||||
deviceId: 'Library Import',
|
||||
|
@ -480,8 +478,8 @@ export class LibraryService extends BaseService {
|
|||
});
|
||||
await this.queuePostSyncJobs(assetIdsToOnline);
|
||||
|
||||
if (progressMessage !== '') {
|
||||
progressMessage + ', ';
|
||||
if (progressMessage) {
|
||||
progressMessage += ', ';
|
||||
}
|
||||
|
||||
progressMessage += `${assetIdsToOnline.length} onlined`;
|
||||
|
@ -491,8 +489,8 @@ export class LibraryService extends BaseService {
|
|||
//TODO: When we have asset status, we need to leave deletedAt as is when status is trashed
|
||||
await this.queuePostSyncJobs(assetIdsToUpdate);
|
||||
|
||||
if (progressMessage !== '') {
|
||||
progressMessage + ', ';
|
||||
if (progressMessage) {
|
||||
progressMessage += ', ';
|
||||
}
|
||||
|
||||
progressMessage += `${assetIdsToUpdate.length} updated`;
|
||||
|
@ -501,8 +499,8 @@ export class LibraryService extends BaseService {
|
|||
const remainingCount = assets.length - assetIdsToOffline.length - assetIdsToUpdate.length - assetIdsToOnline.length;
|
||||
|
||||
if (remainingCount) {
|
||||
if (progressMessage !== '') {
|
||||
progressMessage + ', ';
|
||||
if (progressMessage) {
|
||||
progressMessage += ', ';
|
||||
}
|
||||
|
||||
progressMessage += `${remainingCount} unchanged`;
|
||||
|
@ -523,7 +521,7 @@ export class LibraryService extends BaseService {
|
|||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
private async checkOfflineAsset(asset: AssetEntity, library: LibraryEntity): Promise<boolean> {
|
||||
private checkOfflineAsset(asset: AssetEntity, library: LibraryEntity): boolean {
|
||||
if (!asset.libraryId) {
|
||||
return false;
|
||||
}
|
||||
|
@ -567,7 +565,7 @@ export class LibraryService extends BaseService {
|
|||
}
|
||||
|
||||
const mtime = stat.mtime;
|
||||
const isAssetTimeUpdated = asset.fileModifiedAt ? mtime.toISOString() !== asset.fileModifiedAt.toISOString() : true;
|
||||
const isAssetTimeUpdated = mtime.toISOString() !== asset.fileModifiedAt.toISOString();
|
||||
|
||||
let shouldAssetGoOnline = false;
|
||||
|
||||
|
@ -575,7 +573,7 @@ export class LibraryService extends BaseService {
|
|||
// Only perform the expensive check if the asset is offline
|
||||
|
||||
// TODO: give more feedback on why asset was onlined
|
||||
shouldAssetGoOnline = await this.checkOfflineAsset(asset, library);
|
||||
shouldAssetGoOnline = this.checkOfflineAsset(asset, library);
|
||||
|
||||
if (shouldAssetGoOnline) {
|
||||
this.logger.debug(`Asset is back online: ${asset.originalPath}`);
|
||||
|
@ -590,7 +588,7 @@ export class LibraryService extends BaseService {
|
|||
|
||||
if (isAssetTimeUpdated) {
|
||||
this.logger.verbose(
|
||||
`Asset ${asset.originalPath} modification time changed from ${asset.fileModifiedAt?.toISOString()} to ${mtime.toISOString()}, queuing re-import`,
|
||||
`Asset ${asset.originalPath} modification time changed from ${asset.fileModifiedAt?.toISOString()} to ${mtime.toISOString()}, queuing re-import. Creation time is ${asset.fileCreatedAt?.toISOString()}`,
|
||||
);
|
||||
|
||||
return AssetSyncResult.UPDATE;
|
||||
|
@ -626,8 +624,6 @@ export class LibraryService extends BaseService {
|
|||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
let assetsOnDiskCount = 0;
|
||||
|
||||
const pathsOnDisk = this.storageRepository.walk({
|
||||
pathsToCrawl: validImportPaths,
|
||||
includeHidden: false,
|
||||
|
@ -654,7 +650,6 @@ export class LibraryService extends BaseService {
|
|||
ownerId: library.ownerId,
|
||||
assetPaths: newPaths,
|
||||
progressCounter: crawlCount,
|
||||
totalAssets: assetsOnDiskCount,
|
||||
},
|
||||
});
|
||||
this.logger.log(
|
||||
|
|
|
@ -4,7 +4,7 @@ import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
|||
import { Insertable } from 'kysely';
|
||||
import _ from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
import { file } from 'mock-fs/lib/filesystem';
|
||||
import { Stats } from 'node:fs';
|
||||
import { constants } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { SystemConfig } from 'src/config';
|
||||
|
@ -163,18 +163,30 @@ export class MetadataService extends BaseService {
|
|||
|
||||
this.logger.verbose('Exif Tags', exifTags);
|
||||
|
||||
const dates = await this.getDates(asset, exifTags);
|
||||
const { dateTimeOriginal, localDateTime, timeZone, modifyDate, fileCreatedAt, fileModifiedAt } = this.getDates(
|
||||
asset,
|
||||
exifTags,
|
||||
stats,
|
||||
);
|
||||
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||
|
||||
const { width, height } = this.getImageDimensions(exifTags);
|
||||
|
||||
let fileCreatedAtDate = dateTimeOriginal;
|
||||
let fileModifiedAtDate = modifyDate;
|
||||
|
||||
if (asset.isExternal) {
|
||||
fileCreatedAtDate = fileCreatedAt;
|
||||
fileModifiedAtDate = fileModifiedAt;
|
||||
}
|
||||
|
||||
const exifData: Insertable<Exif> = {
|
||||
assetId: asset.id,
|
||||
|
||||
// dates
|
||||
dateTimeOriginal: dates.dateTimeOriginal,
|
||||
modifyDate: dates.modifyDate,
|
||||
timeZone: dates.timeZone,
|
||||
dateTimeOriginal,
|
||||
modifyDate,
|
||||
timeZone,
|
||||
|
||||
// gps
|
||||
latitude,
|
||||
|
@ -220,9 +232,9 @@ export class MetadataService extends BaseService {
|
|||
await this.assetRepository.update({
|
||||
id: asset.id,
|
||||
duration: exifTags.Duration?.toString() ?? null,
|
||||
localDateTime: dates.localDateTime,
|
||||
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
|
||||
fileModifiedAt: exifData.dateTimeOriginal ?? undefined,
|
||||
localDateTime,
|
||||
fileCreatedAt: fileCreatedAtDate,
|
||||
fileModifiedAt: fileModifiedAtDate,
|
||||
});
|
||||
|
||||
await this.assetRepository.upsertJobStatus({
|
||||
|
@ -453,7 +465,7 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
} else {
|
||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||
const dates = await this.getDates(asset, tags);
|
||||
const dates = this.getDates(asset, tags, stat);
|
||||
motionAsset = await this.assetRepository.create({
|
||||
id: motionAssetId,
|
||||
libraryId: asset.libraryId,
|
||||
|
@ -571,7 +583,7 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
private async getDates(asset: AssetEntity, exifTags: ImmichTags) {
|
||||
private getDates(asset: AssetEntity, exifTags: ImmichTags, stat: Stats) {
|
||||
const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
|
||||
this.logger.verbose(`Asset ${asset.id} date time is ${dateTime}`);
|
||||
|
||||
|
@ -592,52 +604,39 @@ export class MetadataService extends BaseService {
|
|||
let fileCreatedAt = asset.fileCreatedAt;
|
||||
let fileModifiedAt = asset.fileModifiedAt;
|
||||
|
||||
if (!fileCreatedAt || !fileModifiedAt) {
|
||||
let stat;
|
||||
if (asset.isExternal) {
|
||||
// With external assets we need to extract dates from the filesystem, this can't be done with uploades assets as that information is lost on upload
|
||||
fileCreatedAt = stat.mtime;
|
||||
fileModifiedAt = stat.mtime;
|
||||
|
||||
// Throw error if the file does not exist
|
||||
stat = await this.storageRepository.stat(asset.originalPath);
|
||||
|
||||
if (!fileCreatedAt) {
|
||||
fileCreatedAt = stat.mtime;
|
||||
this.logger.debug(
|
||||
`No valid fileCreatedAt date found for asset ${asset.id}, read file creation date from filesystem: ${fileCreatedAt.toISOString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!fileModifiedAt) {
|
||||
fileModifiedAt = stat.mtime;
|
||||
this.logger.debug(
|
||||
`No valid fileModifiedAt date found for asset ${asset.id}, read file modification date from filesystem: ${fileModifiedAt.toISOString()}`,
|
||||
);
|
||||
}
|
||||
this.logger.verbose(`External asset ${asset.id} has a file modification time of ${fileCreatedAt.toISOString()}`);
|
||||
}
|
||||
|
||||
let dateTimeOriginal = dateTime?.toDate();
|
||||
let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
|
||||
if (!localDateTime || !dateTimeOriginal) {
|
||||
this.logger.debug(
|
||||
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification`,
|
||||
);
|
||||
const earliestDate = this.earliestDate(fileModifiedAt, fileCreatedAt);
|
||||
this.logger.debug(
|
||||
`No valid date found in exif tags from asset ${asset.id}, falling back to earliest timestamp between file creation and file modification: ${earliestDate.toISOString()}`,
|
||||
);
|
||||
dateTimeOriginal = earliestDate;
|
||||
localDateTime = earliestDate;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime?.toISOString()}`);
|
||||
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);
|
||||
|
||||
let modifyDate = fileModifiedAt;
|
||||
let modifyDate = asset.fileModifiedAt;
|
||||
try {
|
||||
modifyDate = (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? modifyDate;
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
fileCreatedAt,
|
||||
fileModifiedAt,
|
||||
dateTimeOriginal,
|
||||
timeZone,
|
||||
localDateTime,
|
||||
modifyDate,
|
||||
fileCreatedAt,
|
||||
fileModifiedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -758,6 +757,8 @@ export class MetadataService extends BaseService {
|
|||
|
||||
if (asset.isExternal) {
|
||||
if (sidecarPath !== asset.sidecarPath) {
|
||||
this.logger.verbose(`External asset ${asset.id} has sidecar path ${sidecarPath}`);
|
||||
|
||||
await this.assetRepository.update({ id: asset.id, sidecarPath });
|
||||
}
|
||||
return JobStatus.SUCCESS;
|
||||
|
|
6
server/test/fixtures/shared-link.stub.ts
vendored
6
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -311,11 +311,7 @@ export const sharedLinkResponseStub = {
|
|||
allowUpload: false,
|
||||
allowDownload: false,
|
||||
showMetadata: false,
|
||||
album: {
|
||||
...albumResponse,
|
||||
startDate: assetResponse.fileCreatedAt ?? undefined,
|
||||
endDate: assetResponse.fileCreatedAt ?? undefined,
|
||||
},
|
||||
album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt },
|
||||
assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }],
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
createLibrary,
|
||||
deleteLibrary,
|
||||
getAllLibraries,
|
||||
getAssetCount,
|
||||
getLibraryStatistics,
|
||||
getUserAdmin,
|
||||
scanLibrary,
|
||||
updateLibrary,
|
||||
|
@ -67,7 +67,7 @@
|
|||
};
|
||||
|
||||
const refreshStats = async (listIndex: number) => {
|
||||
assetCount[listIndex] = await getAssetCount({ id: libraries[listIndex].id });
|
||||
assetCount[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
||||
owner[listIndex] = await getUserAdmin({ id: libraries[listIndex].ownerId });
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue