Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions server/src/cores/storage.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,11 @@ export class StorageCore {
}

async moveAssetVideo(asset: StorageAsset) {
const encodedVideoFile = getAssetFile(asset.files, AssetFileType.EncodedVideo, { isEdited: false });
return this.moveFile({
entityId: asset.id,
pathType: AssetPathType.EncodedVideo,
oldPath: asset.encodedVideoPath,
oldPath: encodedVideoFile?.path || null,
newPath: StorageCore.getEncodedVideoPath(asset),
});
}
Expand Down Expand Up @@ -303,21 +304,15 @@ export class StorageCore {
case AssetPathType.Original: {
return this.assetRepository.update({ id, originalPath: newPath });
}
case AssetFileType.FullSize: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
}
case AssetFileType.Preview: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath });
}
case AssetFileType.Thumbnail: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath });
}
case AssetPathType.EncodedVideo: {
return this.assetRepository.update({ id, encodedVideoPath: newPath });
}

case AssetFileType.FullSize:
case AssetFileType.EncodedVideo:
case AssetFileType.Thumbnail:
case AssetFileType.Preview:
case AssetFileType.Sidecar: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath });
return this.assetRepository.upsertFile({ assetId: id, type: pathType as AssetFileType, path: newPath });
}

case PersonPathType.Face: {
return this.personRepository.update({ id, thumbnailPath: newPath });
}
Expand Down
1 change: 0 additions & 1 deletion server/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ export type StorageAsset = {
id: string;
ownerId: string;
files: AssetFile[];
encodedVideoPath: string | null;
};

export type Stack = {
Expand Down
1 change: 0 additions & 1 deletion server/src/dtos/asset-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ export type MapAsset = {
duplicateId: string | null;
duration: string | null;
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
encodedVideoPath: string | null;
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
faces?: ShallowDehydrateObject<AssetFace>[];
fileCreatedAt: Date;
Expand Down
1 change: 1 addition & 0 deletions server/src/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum AssetFileType {
Preview = 'preview',
Thumbnail = 'thumbnail',
Sidecar = 'sidecar',
EncodedVideo = 'encoded_video',
}

export enum AlbumUserRole {
Expand Down
36 changes: 27 additions & 9 deletions server/src/queries/asset.job.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ where
select
"asset"."id",
"asset"."ownerId",
"asset"."encodedVideoPath",
(
select
coalesce(json_agg(agg), '[]')
Expand Down Expand Up @@ -463,7 +462,6 @@ select
"asset"."libraryId",
"asset"."ownerId",
"asset"."livePhotoVideoId",
"asset"."encodedVideoPath",
"asset"."originalPath",
"asset"."isOffline",
to_json("asset_exif") as "exifInfo",
Expand Down Expand Up @@ -521,25 +519,45 @@ select
from
"asset"
where
"asset"."type" = $1
and (
"asset"."encodedVideoPath" is null
or "asset"."encodedVideoPath" = $2
"asset"."type" = 'VIDEO'
and not exists (
select
"asset_file"."id"
from
"asset_file"
where
"asset_file"."assetId" = "asset"."id"
and "asset_file"."type" = 'encoded_video'
)
and "asset"."visibility" != $3
and "asset"."visibility" != 'hidden'
and "asset"."deletedAt" is null

-- AssetJobRepository.getForVideoConversion
select
"asset"."id",
"asset"."ownerId",
"asset"."originalPath",
"asset"."encodedVideoPath"
(
select
coalesce(json_agg(agg), '[]')
from
(
select
"asset_file"."id",
"asset_file"."path",
"asset_file"."type",
"asset_file"."isEdited"
from
"asset_file"
where
"asset_file"."assetId" = "asset"."id"
) as agg
) as "files"
from
"asset"
where
"asset"."id" = $1
and "asset"."type" = $2
and "asset"."type" = 'VIDEO'

-- AssetJobRepository.streamForMetadataExtraction
select
Expand Down
16 changes: 12 additions & 4 deletions server/src/queries/asset.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,21 @@ order by

-- AssetRepository.getForVideo
select
"asset"."encodedVideoPath",
"asset"."originalPath"
"asset"."originalPath",
(
select
"asset_file"."path"
from
"asset_file"
where
"asset_file"."assetId" = "asset"."id"
and "asset_file"."type" = $1
) as "encodedVideoPath"
from
"asset"
where
"asset"."id" = $1
and "asset"."type" = $2
"asset"."id" = $2
and "asset"."type" = $3

-- AssetRepository.getForOcr
select
Expand Down
24 changes: 17 additions & 7 deletions server/src/repositories/asset-job.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class AssetJobRepository {
getForMigrationJob(id: string) {
return this.db
.selectFrom('asset')
.select(['asset.id', 'asset.ownerId', 'asset.encodedVideoPath'])
.select(['asset.id', 'asset.ownerId'])
.select(withFiles)
.where('asset.id', '=', id)
.executeTakeFirst();
Expand Down Expand Up @@ -268,7 +268,6 @@ export class AssetJobRepository {
'asset.libraryId',
'asset.ownerId',
'asset.livePhotoVideoId',
'asset.encodedVideoPath',
'asset.originalPath',
'asset.isOffline',
])
Expand Down Expand Up @@ -310,11 +309,21 @@ export class AssetJobRepository {
return this.db
.selectFrom('asset')
.select(['asset.id'])
.where('asset.type', '=', AssetType.Video)
.where('asset.type', '=', sql.lit(AssetType.Video))
.$if(!force, (qb) =>
qb
.where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')]))
.where('asset.visibility', '!=', AssetVisibility.Hidden),
.where((eb) =>
eb.not(
eb.exists(
eb
.selectFrom('asset_file')
.select('asset_file.id')
.whereRef('asset_file.assetId', '=', 'asset.id')
.where('asset_file.type', '=', sql.lit(AssetFileType.EncodedVideo)),
),
),
)
.where('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)),
)
.where('asset.deletedAt', 'is', null)
.stream();
Expand All @@ -324,9 +333,10 @@ export class AssetJobRepository {
getForVideoConversion(id: string) {
return this.db
.selectFrom('asset')
.select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath'])
.select(['asset.id', 'asset.ownerId', 'asset.originalPath'])
.select(withFiles)
.where('asset.id', '=', id)
.where('asset.type', '=', AssetType.Video)
.where('asset.type', '=', sql.lit(AssetType.Video))
.executeTakeFirst();
}

Expand Down
21 changes: 18 additions & 3 deletions server/src/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
withExif,
withFaces,
withFacesAndPeople,
withFilePath,
withFiles,
withLibrary,
withOwner,
Expand Down Expand Up @@ -1019,8 +1020,21 @@ export class AssetRepository {
.execute();
}

async deleteFile({ assetId, type }: { assetId: string; type: AssetFileType }): Promise<void> {
await this.db.deleteFrom('asset_file').where('assetId', '=', asUuid(assetId)).where('type', '=', type).execute();
async deleteFile({
assetId,
type,
edited,
}: {
assetId: string;
type: AssetFileType;
edited?: boolean;
}): Promise<void> {
await this.db
.deleteFrom('asset_file')
.where('assetId', '=', asUuid(assetId))
.where('type', '=', type)
.$if(edited !== undefined, (qb) => qb.where('isEdited', '=', edited!))
.execute();
}

async deleteFiles(files: Pick<Selectable<AssetFileTable>, 'id'>[]): Promise<void> {
Expand Down Expand Up @@ -1139,7 +1153,8 @@ export class AssetRepository {
async getForVideo(id: string) {
return this.db
.selectFrom('asset')
.select(['asset.encodedVideoPath', 'asset.originalPath'])
.select(['asset.originalPath'])
.select((eb) => withFilePath(eb, AssetFileType.EncodedVideo).as('encodedVideoPath'))
.where('asset.id', '=', id)
.where('asset.type', '=', AssetType.Video)
.executeTakeFirst();
Expand Down
1 change: 0 additions & 1 deletion server/src/repositories/database.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,6 @@ export class DatabaseRepository {
.updateTable('asset')
.set((eb) => ({
originalPath: eb.fn('REGEXP_REPLACE', ['originalPath', source, target]),
encodedVideoPath: eb.fn('REGEXP_REPLACE', ['encodedVideoPath', source, target]),
}))
.execute();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Kysely, sql } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
await sql`
INSERT INTO "asset_file" ("assetId", "type", "path")
SELECT "id", 'encoded_video', "encodedVideoPath"
FROM "asset"
WHERE "encodedVideoPath" IS NOT NULL AND "encodedVideoPath" != '';
`.execute(db);

await sql`ALTER TABLE "asset" DROP COLUMN "encodedVideoPath";`.execute(db);
}

export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset" ADD "encodedVideoPath" character varying DEFAULT '';`.execute(db);

await sql`
UPDATE "asset"
SET "encodedVideoPath" = af."path"
FROM "asset_file" af
WHERE "asset"."id" = af."assetId"
AND af."type" = 'encoded_video'
AND af."isEdited" = false;
`.execute(db);
}
3 changes: 0 additions & 3 deletions server/src/schema/tables/asset.table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ export class AssetTable {
@Column({ type: 'character varying', nullable: true })
duration!: string | null;

@Column({ type: 'character varying', nullable: true, default: '' })
encodedVideoPath!: string | null;

@Column({ type: 'bytea', index: true })
checksum!: Buffer; // sha1 checksum

Expand Down
17 changes: 12 additions & 5 deletions server/src/services/asset-media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,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,
encodedVideoPath: '',
duration: '0:00:00.000000',
files: [] as AssetFile[],
exifInfo: {
Expand Down Expand Up @@ -711,13 +710,18 @@ describe(AssetMediaService.name, () => {
});

it('should return the encoded video path if available', async () => {
const asset = AssetFactory.create({ encodedVideoPath: '/path/to/encoded/video.mp4' });
const asset = AssetFactory.from()
.file({ type: AssetFileType.EncodedVideo, path: '/path/to/encoded/video.mp4' })
.build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getForVideo.mockResolvedValue(asset);
mocks.asset.getForVideo.mockResolvedValue({
originalPath: asset.originalPath,
encodedVideoPath: asset.files[0].path,
});

await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual(
new ImmichFileResponse({
path: asset.encodedVideoPath!,
path: '/path/to/encoded/video.mp4',
cacheControl: CacheControl.PrivateWithCache,
contentType: 'video/mp4',
}),
Expand All @@ -727,7 +731,10 @@ describe(AssetMediaService.name, () => {
it('should fall back to the original path', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getForVideo.mockResolvedValue(asset);
mocks.asset.getForVideo.mockResolvedValue({
originalPath: asset.originalPath,
encodedVideoPath: null,
});

await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual(
new ImmichFileResponse({
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export class AssetService extends BaseService {
assetFiles.editedFullsizeFile?.path,
assetFiles.editedPreviewFile?.path,
assetFiles.editedThumbnailFile?.path,
asset.encodedVideoPath,
assetFiles.encodedVideoFile?.path,
];

if (deleteOnDisk && !asset.isOffline) {
Expand Down
6 changes: 4 additions & 2 deletions server/src/services/media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2254,7 +2254,9 @@ describe(MediaService.name, () => {
});

it('should delete existing transcode if current policy does not require transcoding', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, encodedVideoPath: '/encoded/video/path.mp4' });
const asset = AssetFactory.from({ type: AssetType.Video })
.file({ type: AssetFileType.EncodedVideo, path: '/encoded/video/path.mp4' })
.build();
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } });
mocks.assetJob.getForVideoConversion.mockResolvedValue(asset);
Expand All @@ -2264,7 +2266,7 @@ describe(MediaService.name, () => {
expect(mocks.media.transcode).not.toHaveBeenCalled();
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.FileDelete,
data: { files: [asset.encodedVideoPath] },
data: { files: ['/encoded/video/path.mp4'] },
});
});

Expand Down
Loading
Loading