1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(server): add originalFileName to asset table (#2231)

This commit is contained in:
Alex 2023-04-11 05:23:39 -05:00 committed by GitHub
parent db628cec11
commit a1a62b00a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 127 additions and 41 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,6 +2,7 @@ import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
import { AssetEntity, UserEntity } from '@app/infra/entities';
import { IAssetRepository } from './asset-repository';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
import { parse } from 'node:path';
export class AssetCore {
constructor(private repository: IAssetRepository, private jobRepository: IJobRepository) {}
@ -35,6 +36,7 @@ export class AssetCore {
encodedVideoPath: null,
tags: [],
sharedLinks: [],
originalFileName: parse(file.originalName).name,
});
await this.jobRepository.queue({ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } });

View File

@ -10,9 +10,6 @@ export class CreateExifDto {
@IsOptional()
model?: string;
@IsOptional()
imageName?: string;
@IsOptional()
exifImageWidth?: number;

View File

@ -28,8 +28,8 @@ export class DownloadService {
let fileCount = 0;
let complete = true;
for (const { id, originalPath, exifInfo } of assets) {
const name = `${exifInfo?.imageName || id}${extname(originalPath)}`;
for (const { originalPath, exifInfo, originalFileName } of assets) {
const name = `${originalFileName}${extname(originalPath)}`;
archive.file(originalPath, { name });
totalSize += Number(exifInfo?.fileSizeInByte || 0);
fileCount++;

View File

@ -1,6 +1,5 @@
import {
AssetCore,
getFileNameWithoutExtension,
IAssetRepository,
IAssetUploadedJob,
IBaseJob,
@ -21,7 +20,6 @@ import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
import { Duration } from 'luxon';
import fs from 'node:fs';
import path from 'path';
import sharp from 'sharp';
import { Repository } from 'typeorm/repository/Repository';
import { promisify } from 'util';
@ -79,7 +77,7 @@ export class MetadataExtractionProcessor {
: await this.assetRepository.getWithout(WithoutProperty.EXIF);
for (const asset of assets) {
const fileName = asset.exifInfo?.imageName ?? getFileNameWithoutExtension(asset.originalPath);
const fileName = asset.originalFileName;
const name = asset.type === AssetType.VIDEO ? JobName.EXTRACT_VIDEO_METADATA : JobName.EXIF_EXTRACTION;
await this.jobRepository.queue({ name, data: { asset, fileName } });
}
@ -92,7 +90,6 @@ export class MetadataExtractionProcessor {
async extractExifInfo(job: Job<IAssetUploadedJob>) {
try {
let asset = job.data.asset;
const fileName = job.data.fileName;
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
return null;
@ -126,7 +123,6 @@ export class MetadataExtractionProcessor {
const newExif = new ExifEntity();
newExif.assetId = asset.id;
newExif.imageName = path.parse(fileName).name;
newExif.fileSizeInByte = fileSizeInBytes;
newExif.make = exifData?.Make || null;
newExif.model = exifData?.Model || null;
@ -191,7 +187,6 @@ export class MetadataExtractionProcessor {
@Process({ name: JobName.EXTRACT_VIDEO_METADATA, concurrency: 2 })
async extractVideoMetadata(job: Job<IAssetUploadedJob>) {
let asset = job.data.asset;
const fileName = job.data.fileName;
if (!asset.isVisible) {
return;
@ -219,7 +214,6 @@ export class MetadataExtractionProcessor {
const newExif = new ExifEntity();
newExif.assetId = asset.id;
newExif.description = '';
newExif.imageName = path.parse(fileName).name || null;
newExif.fileSizeInByte = data.format.size || null;
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
newExif.modifyDate = null;
@ -242,7 +236,6 @@ export class MetadataExtractionProcessor {
if (photoAsset) {
await this.assetCore.save({ id: photoAsset.id, livePhotoVideoId: asset.id });
await this.assetCore.save({ id: asset.id, isVisible: false });
newExif.imageName = (photoAsset.exifInfo as ExifEntity).imageName;
}
}

View File

@ -3548,11 +3548,6 @@
"nullable": true,
"default": null
},
"imageName": {
"type": "string",
"nullable": true,
"default": null
},
"exifImageWidth": {
"type": "number",
"nullable": true,
@ -3712,6 +3707,9 @@
"originalPath": {
"type": "string"
},
"originalFileName": {
"type": "string"
},
"resizePath": {
"type": "string",
"nullable": true
@ -3767,6 +3765,7 @@
"ownerId",
"deviceId",
"originalPath",
"originalFileName",
"resizePath",
"fileCreatedAt",
"fileModifiedAt",

View File

@ -13,6 +13,7 @@ export class AssetResponseDto {
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType;
originalPath!: string;
originalFileName!: string;
resizePath!: string | null;
fileCreatedAt!: string;
fileModifiedAt!: string;
@ -36,6 +37,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
originalFileName: entity.originalFileName,
resizePath: entity.resizePath,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
@ -60,6 +62,7 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
originalFileName: entity.originalFileName,
resizePath: entity.resizePath,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,

View File

@ -4,7 +4,6 @@ import { ApiProperty } from '@nestjs/swagger';
export class ExifResponseDto {
make?: string | null = null;
model?: string | null = null;
imageName?: string | null = null;
exifImageWidth?: number | null = null;
exifImageHeight?: number | null = null;
@ -30,7 +29,6 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
return {
make: entity.make,
model: entity.model,
imageName: entity.imageName,
exifImageWidth: entity.exifImageWidth,
exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,

View File

@ -26,7 +26,7 @@ export class StorageTemplateService {
const { asset } = data;
try {
const filename = asset.exifInfo?.imageName || asset.id;
const filename = asset.originalFileName || asset.id;
await this.moveAsset(asset, filename);
// move motion part of live photo
@ -56,7 +56,7 @@ export class StorageTemplateService {
for (const asset of assets) {
const livePhotoParentAsset = livePhotoMap[asset.id];
// TODO: remove livePhoto specific stuff once upload is fixed
const filename = asset.exifInfo?.imageName || livePhotoParentAsset?.exifInfo?.imageName || asset.id;
const filename = asset.originalFileName || livePhotoParentAsset?.originalFileName || asset.id;
await this.moveAsset(asset, filename);
}

View File

@ -118,6 +118,7 @@ export const fileStub = {
export const assetEntityStub = {
noResizePath: Object.freeze<AssetEntity>({
id: 'asset-id',
originalFileName: 'asset_1.jpeg',
deviceAssetId: 'device-asset-id',
fileModifiedAt: '2023-02-23T05:06:29.716Z',
fileCreatedAt: '2023-02-23T05:06:29.716Z',
@ -163,9 +164,11 @@ export const assetEntityStub = {
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.ext',
}),
video: Object.freeze<AssetEntity>({
id: 'asset-id',
originalFileName: 'asset-id.ext',
deviceAssetId: 'device-asset-id',
fileModifiedAt: '2023-02-23T05:06:29.716Z',
fileCreatedAt: '2023-02-23T05:06:29.716Z',
@ -320,7 +323,6 @@ export const albumStub = {
const assetInfo: ExifResponseDto = {
make: 'camera-make',
model: 'camera-model',
imageName: 'fancy-image',
exifImageWidth: 500,
exifImageHeight: 500,
fileSizeInByte: 100,
@ -347,6 +349,7 @@ const assetResponse: AssetResponseDto = {
deviceId: 'device_id_1',
type: AssetType.VIDEO,
originalPath: 'fake_path/jpeg',
originalFileName: 'asset_1.jpeg',
resizePath: '',
fileModifiedAt: today.toISOString(),
fileCreatedAt: today.toISOString(),
@ -602,6 +605,7 @@ export const sharedLinkStub = {
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
originalFileName: 'asset_1.jpeg',
exifInfo: {
livePhotoCID: null,
assetId: 'id_1',
@ -620,7 +624,6 @@ export const sharedLinkStub = {
country: 'country',
make: 'camera-make',
model: 'camera-model',
imageName: 'fancy-image',
lensModel: 'fancy',
fNumber: 100,
focalLength: 100,

View File

@ -87,6 +87,9 @@ export class AssetEntity {
@Column({ nullable: true })
livePhotoVideoId!: string | null;
@Column({ type: 'varchar' })
originalFileName!: string;
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
exifInfo?: ExifEntity;

View File

@ -63,9 +63,6 @@ export class ExifEntity {
@Column({ type: 'varchar', nullable: true })
model!: string | null;
@Column({ type: 'varchar', nullable: true })
imageName!: string | null;
@Column({ type: 'varchar', nullable: true })
lensModel!: string | null;
@ -94,7 +91,6 @@ export class ExifEntity {
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))`,

View File

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddOriginalFileNameToAssetTable1681144628393 implements MigrationInterface {
name = 'AddOriginalFileNameToAssetTable1681144628393';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "originalFileName" character varying`);
await queryRunner.query(`
UPDATE assets a
SET "originalFileName" = (
select e."imageName"
from exif e
where e."assetId" = a.id
)
`);
await queryRunner.query(`
UPDATE assets a
SET "originalFileName" = a.id
where a."originalFileName" IS NULL or a."originalFileName" = ''
`);
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "originalFileName" SET NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "originalFileName"`);
}
}

View File

@ -0,0 +1,62 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveImageNameFromEXIFTable1681159594469 implements MigrationInterface {
name = 'RemoveImageNameFromEXIFTable1681159594469';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN IF EXISTS "exifTextSearchableColumn"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))) STORED NOT NULL`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
[
'immich',
'public',
'exif',
'GENERATED_COLUMN',
'exifTextSearchableColumn',
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
],
);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "imageName"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
[
'immich',
'public',
'exif',
'GENERATED_COLUMN',
'exifTextSearchableColumn',
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"imageName\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))) STORED NOT NULL`);
await queryRunner.query(`ALTER TABLE "exif" ADD "imageName" character varying`);
}
}

View File

@ -144,7 +144,7 @@ export class TypesenseRepository implements ISearchRepository {
const { facet_counts: facets } = await asset$.search({
...common,
query_by: 'exifInfo.imageName',
query_by: 'originalFileName',
facet_by: 'exifInfo.city,smartInfo.objects',
max_facet_values: 12,
});
@ -157,7 +157,7 @@ export class TypesenseRepository implements ISearchRepository {
mergeMap((count) => {
const config = {
...common,
query_by: 'exifInfo.imageName',
query_by: 'originalFileName',
filter_by: [
this.buildFilterBy('ownerId', userId, true),
this.buildFilterBy(facet.field_name, count.value, true),
@ -230,7 +230,7 @@ export class TypesenseRepository implements ISearchRepository {
.search({
q: query,
query_by: [
'exifInfo.imageName',
'originalFileName',
'exifInfo.country',
'exifInfo.state',
'exifInfo.city',

View File

@ -1,6 +1,6 @@
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
export const assetSchemaVersion = 3;
export const assetSchemaVersion = 4;
export const assetSchema: CollectionCreateSchema = {
name: `assets-v${assetSchemaVersion}`,
fields: [
@ -13,6 +13,7 @@ export const assetSchema: CollectionCreateSchema = {
{ name: 'fileCreatedAt', type: 'string', facet: false, sort: true },
{ name: 'fileModifiedAt', type: 'string', facet: false, sort: true },
{ name: 'isFavorite', type: 'bool', facet: true },
{ name: 'originalFileName', type: 'string', facet: false, optional: true },
// { name: 'checksum', type: 'string', facet: true },
// { name: 'tags', type: 'string[]', facet: true, optional: true },
@ -21,7 +22,6 @@ export const assetSchema: CollectionCreateSchema = {
{ name: 'exifInfo.country', type: 'string', facet: true, optional: true },
{ name: 'exifInfo.state', type: 'string', facet: true, optional: true },
{ name: 'exifInfo.description', type: 'string', facet: false, optional: true },
{ name: 'exifInfo.imageName', type: 'string', facet: false, optional: true },
{ name: 'exifInfo.make', type: 'string', facet: true, optional: true },
{ name: 'exifInfo.model', type: 'string', facet: true, optional: true },
{ name: 'exifInfo.orientation', type: 'string', optional: true },

View File

@ -476,6 +476,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'originalPath': string;
/**
*
* @type {string}
* @memberof AssetResponseDto
*/
'originalFileName': string;
/**
*
* @type {string}
@ -1100,12 +1106,6 @@ export interface ExifResponseDto {
* @memberof ExifResponseDto
*/
'model'?: string | null;
/**
*
* @type {string}
* @memberof ExifResponseDto
*/
'imageName'?: string | null;
/**
*
* @type {number}

View File

@ -96,7 +96,7 @@
<div><ImageOutline size="24" /></div>
<div>
<p>{`${asset.exifInfo.imageName}.${asset.originalPath.split('.')[1]}` || ''}</p>
<p>{`${asset.originalFileName}.${asset.originalPath.split('.')[1]}` || ''}</p>
<div class="flex text-sm gap-2">
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}

View File

@ -116,7 +116,7 @@
<ImageThumbnail
url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
altText={asset.exifInfo?.imageName ?? asset.id}
altText={asset.originalFileName}
widthStyle="{width}px"
heightStyle="{height}px"
/>