Compare commits

...

2 Commits

Author SHA1 Message Date
mertalev
87e0e9c6ce linting 2026-04-27 20:10:59 -04:00
mertalev
2f75d323e3 make timeBase non-nullable 2026-04-27 19:24:43 -04:00
9 changed files with 107 additions and 102 deletions

View File

@@ -627,9 +627,12 @@ select
"asset_audio"."profile",
"asset_audio"."bitrate"
from
"asset_audio"
(
select
1
) as "dummy"
where
"asset_audio"."assetId" = "asset"."id"
"asset_audio"."assetId" is not null
) as obj
) as "audioStream",
(
@@ -695,7 +698,8 @@ select
from
"asset"
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
left join "asset_video" on "asset_video"."assetId" = "asset"."id"
inner join "asset_video" on "asset_video"."assetId" = "asset"."id"
left join "asset_audio" on "asset_audio"."assetId" = "asset"."id"
where
"asset"."id" = $1
and "asset"."type" = 'VIDEO'

View File

@@ -9,7 +9,7 @@ import { DB } from 'src/schema';
import {
anyUuid,
asUuid,
withAudioVideo,
withAudioStream,
withDefaultVisibility,
withEdits,
withExif,
@@ -17,6 +17,8 @@ import {
withFaces,
withFilePath,
withFiles,
withVideoFormat,
withVideoStream,
} from 'src/utils/database';
import { mimeTypes } from 'src/utils/mime-types';
@@ -135,7 +137,9 @@ export class AssetJobRepository {
)
.select(withEdits)
.$call(withExifInner)
.$call(withAudioVideo)
.leftJoin('asset_video', 'asset_video.assetId', 'asset.id')
.select((eb) => withVideoStream(eb).as('videoStream'))
.select((eb) => withVideoFormat(eb).as('format'))
.where('asset.id', '=', id)
.executeTakeFirst();
}
@@ -336,9 +340,13 @@ export class AssetJobRepository {
return this.db
.selectFrom('asset')
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
.innerJoin('asset_video', 'asset_video.assetId', 'asset.id')
.leftJoin('asset_audio', 'asset_audio.assetId', 'asset.id')
.select(['asset.id', 'asset.ownerId', 'asset.originalPath'])
.select(withFiles)
.$call((qb) => withAudioVideo(qb, true))
.select((eb) => withAudioStream(eb).as('audioStream'))
.select((eb) => withVideoStream(eb).$notNull().as('videoStream'))
.select((eb) => withVideoFormat(eb).$notNull().as('format'))
.where('asset.id', '=', id)
.where('asset.type', '=', sql.lit(AssetType.Video))
.executeTakeFirst();

View File

@@ -14,7 +14,7 @@ export async function up(db: Kysely<any>): Promise<void> {
"assetId" uuid NOT NULL,
"bitrate" integer NOT NULL,
"frameCount" integer NOT NULL,
"timeBase" integer,
"timeBase" integer NOT NULL,
"index" smallint NOT NULL,
"profile" smallint,
"level" smallint,

View File

@@ -32,8 +32,8 @@ export class AssetVideoTable {
@Column({ type: 'integer' })
frameCount!: number;
@Column({ type: 'integer', nullable: true })
timeBase!: number | null;
@Column({ type: 'integer' })
timeBase!: number;
@Column({ type: smallint })
index!: number;

View File

@@ -1,4 +1,4 @@
import { ShallowDehydrateObject } from 'kysely';
import { NotNull, ShallowDehydrateObject } from 'kysely';
import { OutputInfo } from 'sharp';
import { SystemConfig } from 'src/config';
import { Exif } from 'src/database';
@@ -508,7 +508,7 @@ describe(MediaService.name, () => {
expect.any(String),
expect.objectContaining({
inputOptions: ['-skip_frame', 'nointra', '-sws_flags', 'accurate_rnd+full_chroma_int'],
outputOptions: [
outputOptions: expect.arrayContaining([
'-fps_mode',
'vfr',
'-frames:v',
@@ -519,7 +519,7 @@ describe(MediaService.name, () => {
'verbose',
'-vf',
String.raw`fps=12:start_time=0:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc`,
],
]),
twoPass: false,
}),
);
@@ -557,7 +557,7 @@ describe(MediaService.name, () => {
expect.any(String),
expect.objectContaining({
inputOptions: ['-skip_frame', 'nointra', '-sws_flags', 'accurate_rnd+full_chroma_int'],
outputOptions: [
outputOptions: expect.arrayContaining([
'-fps_mode',
'vfr',
'-frames:v',
@@ -567,8 +567,8 @@ describe(MediaService.name, () => {
'-v',
'verbose',
'-vf',
String.raw`fps=12:start_time=0:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
],
String.raw`fps=12:start_time=0:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:250:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
]),
twoPass: false,
}),
);
@@ -608,7 +608,7 @@ describe(MediaService.name, () => {
expect.any(String),
expect.objectContaining({
inputOptions: ['-skip_frame', 'nointra', '-sws_flags', 'accurate_rnd+full_chroma_int'],
outputOptions: [
outputOptions: expect.arrayContaining([
'-fps_mode',
'vfr',
'-frames:v',
@@ -618,8 +618,8 @@ describe(MediaService.name, () => {
'-v',
'verbose',
'-vf',
String.raw`fps=12:start_time=0:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
],
String.raw`fps=12:start_time=0:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:250:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
]),
twoPass: false,
}),
);
@@ -1937,16 +1937,16 @@ describe(MediaService.name, () => {
describe('handleVideoConversion', () => {
let asset: ReturnType<typeof AssetFactory.create> & {
videoStream: VideoStreamInfo | null;
videoStream: VideoStreamInfo & { timeBase: NotNull };
audioStream: AudioStreamInfo | null;
format: VideoFormat | null;
format: VideoFormat;
};
beforeEach(() => {
asset = {
...AssetFactory.create({ id: 'video-id', type: AssetType.Video, originalPath: '/original/path.ext' }),
videoStream: null,
videoStream: probeStub.videoStreamH264.videoStream,
audioStream: null,
format: null,
format: probeStub.videoStreamH264.format,
};
mocks.assetJob.getForVideoConversion.mockResolvedValue(asset);
sut.videoInterfaces = { dri: ['renderD128'], mali: true };

View File

@@ -326,7 +326,7 @@ export class MetadataService extends BaseService {
: undefined;
const videoData =
format?.formatName && format?.formatLongName && video?.codecName
format?.formatName && format?.formatLongName && video?.codecName && video?.timeBase
? {
assetId: asset.id,
bitrate: video.bitrate,

View File

@@ -101,26 +101,20 @@ export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
export const dummy = sql`(select 1)`.as('dummy');
export function withAudioVideo<O>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', O>, withAudio = false) {
return qb
.$if(withAudio, (qb) =>
qb.select((eb) =>
jsonObjectFrom(
eb
.selectFrom('asset_audio')
.select(['asset_audio.index', 'asset_audio.codecName', 'asset_audio.profile', 'asset_audio.bitrate'])
.whereRef('asset_audio.assetId', '=', 'asset.id'),
)
.$castTo<AudioStreamInfo | null>()
.as('audioStream'),
),
)
.leftJoin('asset_video', 'asset_video.assetId', 'asset.id')
.select((eb) =>
jsonObjectFrom(
export function withAudioStream(eb: ExpressionBuilder<DB, 'asset_exif' | 'asset_audio'>) {
return jsonObjectFrom(
eb
.selectFrom(dummy)
.select(['asset_audio.index', 'asset_audio.codecName', 'asset_audio.profile', 'asset_audio.bitrate'])
.where('asset_audio.assetId', 'is not', sql.lit(null))
.$castTo<AudioStreamInfo | null>(),
);
}
export function withVideoStream(eb: ExpressionBuilder<DB, 'asset_exif' | 'asset_video'>) {
return jsonObjectFrom(
eb
.selectFrom(dummy)
.where('asset_video.assetId', 'is not', sql.lit(null))
.select((eb) => [
'asset_video.index',
'asset_video.codecName',
@@ -151,15 +145,15 @@ export function withAudioVideo<O>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_ex
'asset_video.dvLevel',
'asset_video.dvBlSignalCompatibilityId',
])
.$castTo<VideoStreamInfo | null>(),
).as('videoStream'),
)
.select((eb) =>
jsonObjectFrom(
.where('asset_video.assetId', 'is not', sql.lit(null)),
).$castTo<(VideoStreamInfo & { timeBase: NotNull }) | null>();
}
export function withVideoFormat(eb: ExpressionBuilder<DB, 'asset' | 'asset_video'>) {
return jsonObjectFrom(
eb
.selectFrom(dummy)
.where('asset_video.assetId', 'is not', sql.lit(null))
.select((eb) => [
.select([
'asset_video.formatName',
'asset_video.formatLongName',
// TODO: simplify after https://github.com/immich-app/immich/pull/28003
@@ -173,11 +167,9 @@ export function withAudioVideo<O>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_ex
.end()
.as('duration'),
'asset_video.bitrate',
]),
)
.$castTo<VideoFormat | null>()
.as('format'),
);
])
.where('asset_video.assetId', 'is not', sql.lit(null)),
).$castTo<VideoFormat | null>();
}
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {

View File

@@ -1,3 +1,4 @@
import { NotNull } from 'kysely';
import { ColorMatrix, ColorPrimaries, ColorTransfer, DvProfile, DvSignalCompatibility } from 'src/enum';
import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/types';
@@ -392,9 +393,9 @@ export const videoInfoStub = {
};
interface SelectedStreams {
videoStream: VideoStreamInfo | null;
videoStream: VideoStreamInfo & { timeBase: NotNull };
audioStream: AudioStreamInfo | null;
format: VideoFormat | null;
format: VideoFormat;
}
const toSelectedStreams = (info: VideoInfo) => ({

View File

@@ -1,4 +1,4 @@
import { Selectable, ShallowDehydrateObject } from 'kysely';
import { NotNull, Selectable, ShallowDehydrateObject } from 'kysely';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { ActivityTable } from 'src/schema/tables/activity.table';
@@ -156,7 +156,7 @@ export const getForGenerateThumbnail = (asset: ReturnType<AssetFactory['build']>
files: asset.files.map((file) => getDehydrated(file)),
exifInfo: getDehydrated(asset.exifInfo),
edits: asset.edits.map(({ action, parameters }) => ({ action, parameters })) as AssetEditActionItem[],
videoStream: null as VideoStreamInfo | null,
videoStream: null as (VideoStreamInfo & { timeBase: NotNull }) | null,
audioStream: null as AudioStreamInfo | null,
format: null as VideoFormat | null,
});