mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
@@ -55,7 +55,7 @@ import { MaterializeLineupCommand } from '../commands/MaterializeLineupCommand.t
|
||||
import { MaterializeProgramGroupings } from '../commands/MaterializeProgramGroupings.ts';
|
||||
import { MaterializeProgramsCommand } from '../commands/MaterializeProgramsCommand.ts';
|
||||
import { container } from '../container.ts';
|
||||
import { dbTranscodeConfigToApiSchema } from '../db/converters/transcodeConfigConverters.ts';
|
||||
import { transcodeConfigOrmToDto } from '../db/converters/transcodeConfigConverters.ts';
|
||||
import type { LegacyChannelAndLineup } from '../db/interfaces/IChannelDB.ts';
|
||||
import type { SessionType } from '../stream/Session.ts';
|
||||
import { Result } from '../types/result.ts';
|
||||
@@ -680,7 +680,7 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
const config = await req.serverCtx.transcodeConfigDB.getChannelConfig(
|
||||
req.params.id,
|
||||
);
|
||||
return res.send(dbTranscodeConfigToApiSchema(config));
|
||||
return res.send(transcodeConfigOrmToDto(config));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { container } from '@/container.js';
|
||||
import type { StreamLineupItem } from '@/db/derived_types/StreamLineup.js';
|
||||
import { createOfflineStreamLineupItem } from '@/db/derived_types/StreamLineup.js';
|
||||
import type { Channel } from '@/db/schema/Channel.js';
|
||||
import { AllChannelTableKeys } from '@/db/schema/Channel.js';
|
||||
import type { ChannelOrm } from '@/db/schema/Channel.js';
|
||||
import { ProgramType } from '@/db/schema/Program.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import { AllTranscodeConfigColumns } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import { MpegTsOutputFormat } from '@/ffmpeg/builder/constants.js';
|
||||
import { PlayerContext } from '@/stream/PlayerStreamContext.js';
|
||||
import type { OfflineStreamFactoryType } from '@/stream/StreamModule.js';
|
||||
import { KEYS } from '@/types/inject.js';
|
||||
import type { RouterPluginAsyncCallback } from '@/types/serverType.js';
|
||||
import dayjs from '@/util/dayjs.js';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/sqlite';
|
||||
import { isNumber, isUndefined, nth, random } from 'lodash-es';
|
||||
import { isNumber, isUndefined, random } from 'lodash-es';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import type { MarkRequired } from 'ts-essentials';
|
||||
import { z } from 'zod/v4';
|
||||
import type { ProgramWithRelationsOrm } from '../../db/schema/derivedTypes.ts';
|
||||
import type {
|
||||
ChannelOrmWithTranscodeConfig,
|
||||
ProgramWithRelationsOrm,
|
||||
} from '../../db/schema/derivedTypes.ts';
|
||||
import type { ProgramStreamFactory } from '../../stream/ProgramStreamFactory.ts';
|
||||
import { ChannelNotFoundError } from '../../types/errors.ts';
|
||||
import type { Maybe } from '../../types/util.ts';
|
||||
import { isNonEmptyString } from '../../util/index.ts';
|
||||
|
||||
export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
@@ -37,23 +39,15 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
},
|
||||
async (req, res) => {
|
||||
const channel = await req.serverCtx
|
||||
.databaseFactory()
|
||||
.selectFrom('channel')
|
||||
.selectAll()
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('transcodeConfig')
|
||||
.whereRef(
|
||||
'transcodeConfig.uuid',
|
||||
'=',
|
||||
'channel.transcodeConfigId',
|
||||
)
|
||||
.select(AllTranscodeConfigColumns),
|
||||
).as('transcodeConfig'),
|
||||
)
|
||||
.$narrowType<{ transcodeConfig: TranscodeConfig }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
.drizzleFactory()
|
||||
.query.channels.findFirst({
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channel)
|
||||
throw new Error('This debug endpoint requires at least one channel');
|
||||
|
||||
const stream = container.getNamed<OfflineStreamFactoryType>(
|
||||
KEYS.ProgramStreamFactory,
|
||||
@@ -93,29 +87,23 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
},
|
||||
async (req, res) => {
|
||||
const channel = await req.serverCtx
|
||||
.databaseFactory()
|
||||
.selectFrom('channel')
|
||||
.selectAll()
|
||||
.$if(isNonEmptyString(req.query.channelId), (eb) =>
|
||||
eb.where('channel.uuid', '=', req.query.channelId as string),
|
||||
)
|
||||
.$if(isNumber(req.query.channelId), (eb) =>
|
||||
eb.where('channel.number', '=', req.query.channelId as number),
|
||||
)
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('transcodeConfig')
|
||||
.whereRef(
|
||||
'transcodeConfig.uuid',
|
||||
'=',
|
||||
'channel.transcodeConfigId',
|
||||
)
|
||||
.select(AllTranscodeConfigColumns),
|
||||
).as('transcodeConfig'),
|
||||
)
|
||||
.$narrowType<{ transcodeConfig: TranscodeConfig }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
.drizzleFactory()
|
||||
.query.channels.findFirst({
|
||||
where: (fields, { eq }) => {
|
||||
if (isNonEmptyString(req.query.channelId)) {
|
||||
return eq(fields.uuid, req.query.channelId);
|
||||
} else if (isNumber(req.query.channelId)) {
|
||||
return eq(fields.number, req.query.channelId);
|
||||
}
|
||||
return;
|
||||
},
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!channel)
|
||||
throw new ChannelNotFoundError(req.query.channelId ?? 'unknown');
|
||||
|
||||
const stream = container.getNamed<OfflineStreamFactoryType>(
|
||||
KEYS.ProgramStreamFactory,
|
||||
@@ -155,30 +143,18 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
}
|
||||
|
||||
const channels = await req.serverCtx
|
||||
.databaseFactory()
|
||||
.selectFrom('channelPrograms')
|
||||
.where('programUuid', '=', program.uuid)
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('channel')
|
||||
.whereRef('channel.uuid', '=', 'channelPrograms.channelUuid')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('transcodeConfig')
|
||||
.whereRef(
|
||||
'transcodeConfig.uuid',
|
||||
'=',
|
||||
'channel.transcodeConfigId',
|
||||
)
|
||||
.select(AllTranscodeConfigColumns),
|
||||
).as('transcodeConfig'),
|
||||
)
|
||||
.select(AllChannelTableKeys),
|
||||
).as('channel'),
|
||||
)
|
||||
.execute();
|
||||
.drizzleFactory()
|
||||
.query.channelPrograms.findMany({
|
||||
where: (fields, { eq }) => eq(fields.programUuid, program.uuid),
|
||||
columns: {},
|
||||
with: {
|
||||
channel: {
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstChannel = channels?.[0]!.channel;
|
||||
|
||||
@@ -189,7 +165,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
const out = await initStream(
|
||||
program,
|
||||
firstChannel,
|
||||
firstChannel.transcodeConfig!,
|
||||
firstChannel.transcodeConfig,
|
||||
);
|
||||
return res.header('Content-Type', 'video/mp2t').send(out);
|
||||
});
|
||||
@@ -224,58 +200,42 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
: req.query.start;
|
||||
|
||||
const channels = await req.serverCtx
|
||||
.databaseFactory()
|
||||
.selectFrom('channelPrograms')
|
||||
.where('programUuid', '=', program.uuid)
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('channel')
|
||||
.whereRef('channel.uuid', '=', 'channelPrograms.channelUuid')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('transcodeConfig')
|
||||
.whereRef(
|
||||
'transcodeConfig.uuid',
|
||||
'=',
|
||||
'channel.transcodeConfigId',
|
||||
)
|
||||
.select(AllTranscodeConfigColumns),
|
||||
).as('transcodeConfig'),
|
||||
)
|
||||
.select(AllChannelTableKeys),
|
||||
).as('channel'),
|
||||
)
|
||||
.execute();
|
||||
.drizzleFactory()
|
||||
.query.channelPrograms.findMany({
|
||||
where: (fields, { eq }) => eq(fields.programUuid, program.uuid),
|
||||
columns: {},
|
||||
with: {
|
||||
channel: {
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let firstChannel = nth(channels, 0)?.channel;
|
||||
let firstChannel = channels[0]
|
||||
?.channel as Maybe<ChannelOrmWithTranscodeConfig>;
|
||||
|
||||
if (!firstChannel) {
|
||||
firstChannel = await req.serverCtx
|
||||
.databaseFactory()
|
||||
.selectFrom('channel')
|
||||
.selectAll()
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('transcodeConfig')
|
||||
.whereRef(
|
||||
'transcodeConfig.uuid',
|
||||
'=',
|
||||
'channel.transcodeConfigId',
|
||||
)
|
||||
.select(AllTranscodeConfigColumns),
|
||||
).as('transcodeConfig'),
|
||||
)
|
||||
.$narrowType<{ transcodeConfig: TranscodeConfig }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
.drizzleFactory()
|
||||
.query.channels.findFirst({
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!firstChannel) {
|
||||
throw new Error(
|
||||
'Cannot find a channel with a transcode config to use!',
|
||||
);
|
||||
}
|
||||
|
||||
const outStream = await initStream(
|
||||
program,
|
||||
firstChannel,
|
||||
firstChannel.transcodeConfig!,
|
||||
firstChannel.transcodeConfig,
|
||||
startTime * 1000,
|
||||
);
|
||||
return res.header('Content-Type', 'video/mp2t').send(outStream);
|
||||
@@ -284,8 +244,8 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
|
||||
async function initStream(
|
||||
program: MarkRequired<ProgramWithRelationsOrm, 'externalIds'>,
|
||||
channel: Channel,
|
||||
transcodeConfig: TranscodeConfig,
|
||||
channel: ChannelOrm,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
startTime: number = 0,
|
||||
) {
|
||||
if (!program.mediaSourceId) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { isError, map } from 'lodash-es';
|
||||
import { match, P } from 'ts-pattern';
|
||||
import { z } from 'zod/v4';
|
||||
import { dbTranscodeConfigToApiSchema } from '../db/converters/transcodeConfigConverters.ts';
|
||||
import { transcodeConfigOrmToDto } from '../db/converters/transcodeConfigConverters.ts';
|
||||
import { GlobalScheduler } from '../services/Scheduler.ts';
|
||||
import { SubtitleExtractorTask } from '../tasks/SubtitleExtractorTask.ts';
|
||||
import { TranscodeConfigNotFoundError } from '../types/errors.ts';
|
||||
@@ -176,7 +176,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
},
|
||||
async (req, res) => {
|
||||
const configs = await req.serverCtx.transcodeConfigDB.getAll();
|
||||
const apiConfigs = map(configs, dbTranscodeConfigToApiSchema);
|
||||
const apiConfigs = map(configs, transcodeConfigOrmToDto);
|
||||
return res.send(apiConfigs);
|
||||
},
|
||||
);
|
||||
@@ -203,7 +203,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
return res.status(404).send();
|
||||
}
|
||||
|
||||
return res.send(dbTranscodeConfigToApiSchema(config));
|
||||
return res.send(transcodeConfigOrmToDto(config));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -236,7 +236,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
return res.send(dbTranscodeConfigToApiSchema(copyResult.get()));
|
||||
return res.send(transcodeConfigOrmToDto(copyResult.get()));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -257,7 +257,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
const newConfig = await req.serverCtx.transcodeConfigDB.insertConfig(
|
||||
req.body,
|
||||
);
|
||||
return res.status(201).send(dbTranscodeConfigToApiSchema(newConfig));
|
||||
return res.status(201).send(transcodeConfigOrmToDto(newConfig));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ import { DB } from './schema/db.ts';
|
||||
import {
|
||||
ChannelOrmWithPrograms,
|
||||
ChannelOrmWithRelations,
|
||||
ChannelOrmWithTranscodeConfig,
|
||||
ChannelWithPrograms,
|
||||
ChannelWithRelations,
|
||||
MusicAlbumOrm,
|
||||
@@ -264,11 +265,16 @@ export class ChannelDB implements IChannelDB {
|
||||
return !isNil(channel);
|
||||
}
|
||||
|
||||
getChannelOrm(id: string | number): Promise<Maybe<ChannelOrm>> {
|
||||
getChannelOrm(
|
||||
id: string | number,
|
||||
): Promise<Maybe<ChannelOrmWithTranscodeConfig>> {
|
||||
return this.drizzleDB.query.channels.findFirst({
|
||||
where: (channel, { eq }) => {
|
||||
return isString(id) ? eq(channel.uuid, id) : eq(channel.number, id);
|
||||
},
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1567,6 +1573,20 @@ export class ChannelDB implements IChannelDB {
|
||||
};
|
||||
}
|
||||
|
||||
async loadChannelAndLineupOrm(
|
||||
channelId: string,
|
||||
): Promise<ChannelAndLineup<ChannelOrm> | null> {
|
||||
const channel = await this.getChannelOrm(channelId);
|
||||
if (isNil(channel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
channel,
|
||||
lineup: await this.loadLineup(channelId),
|
||||
};
|
||||
}
|
||||
|
||||
async loadChannelWithProgamsAndLineup(
|
||||
channelId: string,
|
||||
): Promise<{ channel: ChannelOrmWithPrograms; lineup: Lineup } | null> {
|
||||
|
||||
25
server/src/db/ITranscodeConfigDB.ts
Normal file
25
server/src/db/ITranscodeConfigDB.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { TranscodeConfig } from '@tunarr/types';
|
||||
import type {
|
||||
TranscodeConfigNotFoundError,
|
||||
WrappedError,
|
||||
} from '../types/errors.ts';
|
||||
import type { Result } from '../types/result.ts';
|
||||
import type { Maybe } from '../types/util.ts';
|
||||
import type { TranscodeConfigOrm } from './schema/TranscodeConfig.ts';
|
||||
|
||||
export interface ITranscodeConfigDB {
|
||||
getAll(): Promise<TranscodeConfigOrm[]>;
|
||||
getById(id: string): Promise<Maybe<TranscodeConfigOrm>>;
|
||||
getDefaultConfig(): Promise<Maybe<TranscodeConfigOrm>>;
|
||||
getChannelConfig(channelId: string): Promise<TranscodeConfigOrm>;
|
||||
insertConfig(
|
||||
config: Omit<TranscodeConfig, 'id'>,
|
||||
): Promise<TranscodeConfigOrm>;
|
||||
duplicateConfig(
|
||||
id: string,
|
||||
): Promise<
|
||||
Result<TranscodeConfigOrm, TranscodeConfigNotFoundError | WrappedError>
|
||||
>;
|
||||
updateConfig(id: string, updatedConfig: TranscodeConfig): Promise<void>;
|
||||
deleteConfig(id: string): Promise<void>;
|
||||
}
|
||||
@@ -1,95 +1,83 @@
|
||||
import { booleanToNumber } from '@/util/sqliteUtil.js';
|
||||
import { Resolution, TranscodeConfig } from '@tunarr/types';
|
||||
import { TranscodeConfig } from '@tunarr/types';
|
||||
import { count, eq } from 'drizzle-orm';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Kysely } from 'kysely';
|
||||
import { omit } from 'lodash-es';
|
||||
import { head, omit, sumBy } from 'lodash-es';
|
||||
import { v4 } from 'uuid';
|
||||
import { TranscodeConfigNotFoundError, WrappedError } from '../types/errors.ts';
|
||||
import { KEYS } from '../types/inject.ts';
|
||||
import { Result } from '../types/result.ts';
|
||||
import { Maybe } from '../types/util.ts';
|
||||
import { ITranscodeConfigDB } from './ITranscodeConfigDB.ts';
|
||||
import { Channel } from './schema/Channel.ts';
|
||||
import {
|
||||
NewTranscodeConfig,
|
||||
TranscodeConfig as TranscodeConfigDAO,
|
||||
TranscodeConfigUpdate,
|
||||
defaultTranscodeConfig,
|
||||
NewTranscodeConfigOrm,
|
||||
TranscodeConfig as TranscodeConfigTable,
|
||||
type TranscodeConfigOrm,
|
||||
} from './schema/TranscodeConfig.ts';
|
||||
import { DB } from './schema/db.ts';
|
||||
import { DrizzleDBAccess } from './schema/index.ts';
|
||||
|
||||
@injectable()
|
||||
export class TranscodeConfigDB {
|
||||
constructor(@inject(KEYS.Database) private db: Kysely<DB>) {}
|
||||
export class TranscodeConfigDB implements ITranscodeConfigDB {
|
||||
constructor(@inject(KEYS.DrizzleDB) private drizzle: DrizzleDBAccess) {}
|
||||
|
||||
getAll() {
|
||||
return this.db.selectFrom('transcodeConfig').selectAll().execute();
|
||||
async getAll(): Promise<TranscodeConfigOrm[]> {
|
||||
return await this.drizzle.query.transcodeConfigs.findMany();
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.db
|
||||
.selectFrom('transcodeConfig')
|
||||
.where('uuid', '=', id)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
async getById(id: string): Promise<Maybe<TranscodeConfigOrm>> {
|
||||
return await this.drizzle.query.transcodeConfigs.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.uuid, id),
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return this.db
|
||||
.selectFrom('transcodeConfig')
|
||||
.where('isDefault', '=', 1)
|
||||
.limit(1)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
async getDefaultConfig(): Promise<Maybe<TranscodeConfigOrm>> {
|
||||
return await this.drizzle.query.transcodeConfigs.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.isDefault, true),
|
||||
});
|
||||
}
|
||||
|
||||
async getChannelConfig(channelId: string) {
|
||||
const channelConfig = await this.db
|
||||
.selectFrom('channel')
|
||||
.where('channel.uuid', '=', channelId)
|
||||
.innerJoin(
|
||||
'transcodeConfig',
|
||||
'channel.transcodeConfigId',
|
||||
'transcodeConfig.uuid',
|
||||
)
|
||||
.selectAll('transcodeConfig')
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
async getChannelConfig(channelId: string): Promise<TranscodeConfigOrm> {
|
||||
const channelConfig = await this.drizzle.query.channels.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.uuid, channelId),
|
||||
with: {
|
||||
transcodeConfig: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (channelConfig) {
|
||||
return channelConfig;
|
||||
return channelConfig.transcodeConfig;
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('transcodeConfig')
|
||||
.where('isDefault', '=', 1)
|
||||
.selectAll()
|
||||
.limit(1)
|
||||
.executeTakeFirstOrThrow();
|
||||
const defaultConfig = await this.getDefaultConfig();
|
||||
if (!defaultConfig) {
|
||||
throw new Error('Bad state - no default transcode config');
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
insertConfig(config: Omit<TranscodeConfig, 'id'>) {
|
||||
async insertConfig(
|
||||
config: Omit<TranscodeConfig, 'id'>,
|
||||
): Promise<TranscodeConfigOrm> {
|
||||
const id = v4();
|
||||
const newConfig: NewTranscodeConfig = {
|
||||
const newConfig: NewTranscodeConfigOrm = {
|
||||
...omit(config, 'id'),
|
||||
uuid: id,
|
||||
resolution: JSON.stringify(config.resolution),
|
||||
normalizeFrameRate: booleanToNumber(config.normalizeFrameRate),
|
||||
deinterlaceVideo: booleanToNumber(config.deinterlaceVideo),
|
||||
disableChannelOverlay: booleanToNumber(config.disableChannelOverlay),
|
||||
isDefault: booleanToNumber(config.isDefault),
|
||||
disableHardwareDecoder: booleanToNumber(config.disableHardwareDecoder),
|
||||
disableHardwareEncoding: booleanToNumber(config.disableHardwareEncoding),
|
||||
disableHardwareFilters: booleanToNumber(config.disableHardwareFilters),
|
||||
};
|
||||
|
||||
return this.db
|
||||
.insertInto('transcodeConfig')
|
||||
.values(newConfig)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
return head(
|
||||
await this.drizzle
|
||||
.insert(TranscodeConfigTable)
|
||||
.values(newConfig)
|
||||
.returning(),
|
||||
)!;
|
||||
}
|
||||
|
||||
async duplicateConfig(
|
||||
id: string,
|
||||
): Promise<
|
||||
Result<TranscodeConfigDAO, TranscodeConfigNotFoundError | WrappedError>
|
||||
Result<TranscodeConfigOrm, TranscodeConfigNotFoundError | WrappedError>
|
||||
> {
|
||||
const baseConfig = await this.getById(id);
|
||||
if (!baseConfig) {
|
||||
@@ -98,79 +86,59 @@ export class TranscodeConfigDB {
|
||||
|
||||
const newId = v4();
|
||||
baseConfig.uuid = newId;
|
||||
baseConfig.isDefault = booleanToNumber(false);
|
||||
baseConfig.isDefault = false;
|
||||
baseConfig.name = `${baseConfig.name} (copy)`;
|
||||
|
||||
return Result.attemptAsync(() => {
|
||||
return this.db
|
||||
.insertInto('transcodeConfig')
|
||||
.values({
|
||||
...baseConfig,
|
||||
resolution: JSON.stringify(baseConfig.resolution),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
return Result.attemptAsync(async () => {
|
||||
return head(
|
||||
await this.drizzle
|
||||
.insert(TranscodeConfigTable)
|
||||
.values(baseConfig)
|
||||
.returning()
|
||||
.execute(),
|
||||
)!;
|
||||
});
|
||||
}
|
||||
|
||||
updateConfig(id: string, updatedConfig: TranscodeConfig) {
|
||||
const update: TranscodeConfigUpdate = {
|
||||
...omit(updatedConfig, 'id'),
|
||||
resolution: JSON.stringify(updatedConfig.resolution),
|
||||
normalizeFrameRate: booleanToNumber(updatedConfig.normalizeFrameRate),
|
||||
deinterlaceVideo: booleanToNumber(updatedConfig.deinterlaceVideo),
|
||||
disableChannelOverlay: booleanToNumber(
|
||||
updatedConfig.disableChannelOverlay,
|
||||
),
|
||||
isDefault: booleanToNumber(updatedConfig.isDefault),
|
||||
disableHardwareDecoder: booleanToNumber(
|
||||
updatedConfig.disableHardwareDecoder,
|
||||
),
|
||||
disableHardwareEncoding: booleanToNumber(
|
||||
updatedConfig.disableHardwareEncoding,
|
||||
),
|
||||
disableHardwareFilters: booleanToNumber(
|
||||
updatedConfig.disableHardwareFilters,
|
||||
),
|
||||
};
|
||||
|
||||
return this.db
|
||||
.updateTable('transcodeConfig')
|
||||
.where('uuid', '=', id)
|
||||
.set(update)
|
||||
.execute();
|
||||
async updateConfig(
|
||||
id: string,
|
||||
updatedConfig: TranscodeConfig,
|
||||
): Promise<void> {
|
||||
await this.drizzle
|
||||
.update(TranscodeConfigTable)
|
||||
.set({
|
||||
...omit(updatedConfig, 'id'),
|
||||
})
|
||||
.where(eq(TranscodeConfigTable.uuid, id));
|
||||
}
|
||||
|
||||
deleteConfig(id: string) {
|
||||
async deleteConfig(id: string) {
|
||||
// A few cases to handle:
|
||||
// 1. if we are deleting the default configuration, we have to pick a new one.
|
||||
// 2. If we are deleting the last configuration, we have to create a default configuration
|
||||
// 3. We have to update all related channels.
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
await this.drizzle.transaction(async (tx) => {
|
||||
const numConfigs = await tx
|
||||
.selectFrom('transcodeConfig')
|
||||
.select((eb) => eb.fn.count<number>('uuid').as('count'))
|
||||
.executeTakeFirst()
|
||||
.then((res) => res?.count ?? 0);
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(TranscodeConfigTable)
|
||||
.then((results) => sumBy(results, (r) => r.count));
|
||||
|
||||
// If there are no configs (should be impossible) create a default, assign it to all channels
|
||||
// and move on.
|
||||
if (numConfigs === 0) {
|
||||
const { uuid: newDefaultConfigId } =
|
||||
await this.insertDefaultConfiguration(tx);
|
||||
const newDefaultConfigId = await this.insertDefaultConfiguration(tx);
|
||||
await tx
|
||||
.updateTable('channel')
|
||||
.set('transcodeConfigId', newDefaultConfigId)
|
||||
.update(Channel)
|
||||
.set({ transcodeConfigId: newDefaultConfigId })
|
||||
.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
const configToDelete = await tx
|
||||
.selectFrom('transcodeConfig')
|
||||
.where('uuid', '=', id)
|
||||
.selectAll()
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
const configToDelete = await tx.query.transcodeConfigs.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.uuid, id),
|
||||
});
|
||||
|
||||
if (!configToDelete) {
|
||||
return;
|
||||
@@ -178,79 +146,136 @@ export class TranscodeConfigDB {
|
||||
|
||||
// If this is the last config, we'll need a new one and will have to assign it
|
||||
if (numConfigs === 1) {
|
||||
const { uuid: newDefaultConfigId } =
|
||||
await this.insertDefaultConfiguration(tx);
|
||||
const newDefaultConfigId = await this.insertDefaultConfiguration(tx);
|
||||
await tx
|
||||
.updateTable('channel')
|
||||
.set('transcodeConfigId', newDefaultConfigId)
|
||||
.update(Channel)
|
||||
.set({ transcodeConfigId: newDefaultConfigId })
|
||||
.execute();
|
||||
await tx
|
||||
.deleteFrom('transcodeConfig')
|
||||
.where('uuid', '=', id)
|
||||
.delete(TranscodeConfigTable)
|
||||
.where(eq(TranscodeConfigTable.uuid, id))
|
||||
.limit(1)
|
||||
.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
// We're deleting the default config. Pick a random one to make the new default. Not great!
|
||||
let replacementId: string;
|
||||
if (configToDelete.isDefault) {
|
||||
const newDefaultConfig = await tx
|
||||
.selectFrom('transcodeConfig')
|
||||
.where('uuid', '!=', id)
|
||||
.where('isDefault', '=', 0)
|
||||
.select('uuid')
|
||||
.limit(1)
|
||||
.executeTakeFirstOrThrow();
|
||||
const newDefaultConfig = (
|
||||
await tx
|
||||
.select({ uuid: TranscodeConfigTable.uuid })
|
||||
.from(TranscodeConfigTable)
|
||||
.where(eq(TranscodeConfigTable.isDefault, false))
|
||||
.limit(1)
|
||||
)[0]!;
|
||||
await tx
|
||||
.updateTable('transcodeConfig')
|
||||
.set('isDefault', 1)
|
||||
.where('uuid', '=', newDefaultConfig.uuid)
|
||||
.limit(1)
|
||||
.execute();
|
||||
await tx
|
||||
.updateTable('channel')
|
||||
.set('transcodeConfigId', newDefaultConfig.uuid)
|
||||
.execute();
|
||||
.update(TranscodeConfigTable)
|
||||
.set({ isDefault: true })
|
||||
.where(eq(TranscodeConfigTable.uuid, newDefaultConfig.uuid));
|
||||
replacementId = newDefaultConfig.uuid;
|
||||
} else {
|
||||
const defaultId = (
|
||||
await tx
|
||||
.select({ uuid: TranscodeConfigTable.uuid })
|
||||
.from(TranscodeConfigTable)
|
||||
.where(eq(TranscodeConfigTable.isDefault, true))
|
||||
.limit(1)
|
||||
)[0]!;
|
||||
replacementId = defaultId.uuid;
|
||||
}
|
||||
|
||||
await tx
|
||||
.deleteFrom('transcodeConfig')
|
||||
.where('uuid', '=', id)
|
||||
.update(Channel)
|
||||
.set({ transcodeConfigId: replacementId })
|
||||
.where(eq(Channel.transcodeConfigId, configToDelete.uuid))
|
||||
.execute();
|
||||
|
||||
await tx
|
||||
.delete(TranscodeConfigTable)
|
||||
.where(eq(TranscodeConfigTable.uuid, id))
|
||||
.limit(1)
|
||||
.execute();
|
||||
});
|
||||
// const numConfigs = await tx
|
||||
// .selectFrom('transcodeConfig')
|
||||
// .select((eb) => eb.fn.count<number>('uuid').as('count'))
|
||||
// .executeTakeFirst()
|
||||
// .then((res) => res?.count ?? 0);
|
||||
|
||||
// // If there are no configs (should be impossible) create a default, assign it to all channels
|
||||
// // and move on.
|
||||
// if (numConfigs === 0) {
|
||||
// const { uuid: newDefaultConfigId } =
|
||||
// await this.insertDefaultConfiguration(tx);
|
||||
// await tx
|
||||
// .updateTable('channel')
|
||||
// .set('transcodeConfigId', newDefaultConfigId)
|
||||
// .execute();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const configToDelete = await tx
|
||||
// .selectFrom('transcodeConfig')
|
||||
// .where('uuid', '=', id)
|
||||
// .selectAll()
|
||||
// .limit(1)
|
||||
// .executeTakeFirst();
|
||||
|
||||
// if (!configToDelete) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // If this is the last config, we'll need a new one and will have to assign it
|
||||
// if (numConfigs === 1) {
|
||||
// const { uuid: newDefaultConfigId } =
|
||||
// await this.insertDefaultConfiguration(tx);
|
||||
// await tx
|
||||
// .updateTable('channel')
|
||||
// .set('transcodeConfigId', newDefaultConfigId)
|
||||
// .execute();
|
||||
// await tx
|
||||
// .deleteFrom('transcodeConfig')
|
||||
// .where('uuid', '=', id)
|
||||
// .limit(1)
|
||||
// .execute();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // We're deleting the default config. Pick a random one to make the new default. Not great!
|
||||
// if (configToDelete.isDefault) {
|
||||
// const newDefaultConfig = await tx
|
||||
// .selectFrom('transcodeConfig')
|
||||
// .where('uuid', '!=', id)
|
||||
// .where('isDefault', '=', 0)
|
||||
// .select('uuid')
|
||||
// .limit(1)
|
||||
// .executeTakeFirstOrThrow();
|
||||
// await tx
|
||||
// .updateTable('transcodeConfig')
|
||||
// .set('isDefault', 1)
|
||||
// .where('uuid', '=', newDefaultConfig.uuid)
|
||||
// .limit(1)
|
||||
// .execute();
|
||||
// await tx
|
||||
// .updateTable('channel')
|
||||
// .set('transcodeConfigId', newDefaultConfig.uuid)
|
||||
// .execute();
|
||||
// }
|
||||
|
||||
// await tx
|
||||
// .deleteFrom('transcodeConfig')
|
||||
// .where('uuid', '=', id)
|
||||
// .limit(1)
|
||||
// .execute();
|
||||
// });
|
||||
}
|
||||
|
||||
private async insertDefaultConfiguration(db: Kysely<DB> = this.db) {
|
||||
return db
|
||||
.insertInto('transcodeConfig')
|
||||
.values(TranscodeConfigDB.createDefaultConfiguration())
|
||||
.returning('uuid as uuid')
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
private static createDefaultConfiguration(): NewTranscodeConfig {
|
||||
const id = v4();
|
||||
return {
|
||||
uuid: id,
|
||||
name: 'Default Config',
|
||||
threadCount: 0,
|
||||
resolution: JSON.stringify({
|
||||
widthPx: 1920,
|
||||
heightPx: 1080,
|
||||
} satisfies Resolution),
|
||||
audioBitRate: 192,
|
||||
audioBufferSize: 192 * 3,
|
||||
audioChannels: 2,
|
||||
audioSampleRate: 48,
|
||||
audioFormat: 'aac',
|
||||
hardwareAccelerationMode: 'none',
|
||||
normalizeFrameRate: booleanToNumber(false),
|
||||
deinterlaceVideo: booleanToNumber(true),
|
||||
videoBitRate: 3500,
|
||||
videoBufferSize: 3500 * 2,
|
||||
videoFormat: 'h264',
|
||||
isDefault: booleanToNumber(true),
|
||||
};
|
||||
private async insertDefaultConfiguration(db: DrizzleDBAccess = this.drizzle) {
|
||||
return head(
|
||||
await db
|
||||
.insert(TranscodeConfigTable)
|
||||
.values(defaultTranscodeConfig(true))
|
||||
.returning({ uuid: TranscodeConfigTable.uuid }),
|
||||
)!.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
import type { TranscodeConfig } from '@tunarr/types';
|
||||
import { numberToBoolean } from '../../util/sqliteUtil.ts';
|
||||
import type { TranscodeConfig as TrannscodeConfigDao } from '../schema/TranscodeConfig.ts';
|
||||
import type {
|
||||
TranscodeConfig as TranscodeConfigDAO,
|
||||
TranscodeConfigOrm,
|
||||
} from '../schema/TranscodeConfig.ts';
|
||||
|
||||
export function dbTranscodeConfigToApiSchema(
|
||||
config: TrannscodeConfigDao,
|
||||
export function transcodeConfigOrmToDto(
|
||||
config: TranscodeConfigOrm,
|
||||
): TranscodeConfig {
|
||||
return {
|
||||
...config,
|
||||
id: config.uuid,
|
||||
// disableChannelOverlay: numberToBoolean(config.disableChannelOverlay),
|
||||
// normalizeFrameRate: numberToBoolean(config.normalizeFrameRate),
|
||||
// deinterlaceVideo: numberToBoolean(config.deinterlaceVideo),
|
||||
// isDefault: numberToBoolean(config.isDefault),
|
||||
// disableHardwareDecoder: numberToBoolean(config.disableHardwareDecoder),
|
||||
// disableHardwareEncoding: numberToBoolean(config.disableHardwareEncoding),
|
||||
// disableHardwareFilters: numberToBoolean(config.disableHardwareFilters),
|
||||
disableChannelOverlay: config.disableChannelOverlay ?? false,
|
||||
normalizeFrameRate: config.normalizeFrameRate ?? false,
|
||||
deinterlaceVideo: config.deinterlaceVideo ?? false,
|
||||
isDefault: config.isDefault ?? false,
|
||||
disableHardwareDecoder: config.disableHardwareDecoder ?? false,
|
||||
disableHardwareEncoding: config.disableHardwareEncoding ?? false,
|
||||
disableHardwareFilters: config.disableHardwareFilters ?? false,
|
||||
} satisfies TranscodeConfig;
|
||||
}
|
||||
|
||||
export function legacyTranscodeConfigToDto(
|
||||
config: TranscodeConfigDAO,
|
||||
): TranscodeConfig {
|
||||
return {
|
||||
...config,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ProgramDao } from '@/db/schema/Program.js';
|
||||
import type { ProgramExternalId } from '@/db/schema/ProgramExternalId.js';
|
||||
import type {
|
||||
ChannelOrmWithRelations,
|
||||
ChannelOrmWithTranscodeConfig,
|
||||
ChannelWithRelations,
|
||||
MusicArtistOrm,
|
||||
ProgramWithRelationsOrm,
|
||||
@@ -42,7 +43,9 @@ export type ChannelAndRawLineup = { channel: ChannelOrm; lineup: Json };
|
||||
export interface IChannelDB {
|
||||
channelExists(channelId: string): Promise<boolean>;
|
||||
|
||||
getChannelOrm(id: string | number): Promise<Maybe<ChannelOrm>>;
|
||||
getChannelOrm(
|
||||
id: string | number,
|
||||
): Promise<Maybe<ChannelOrmWithTranscodeConfig>>;
|
||||
|
||||
getChannel(id: string | number): Promise<Maybe<ChannelWithRelations>>;
|
||||
getChannel(
|
||||
@@ -147,6 +150,10 @@ export interface IChannelDB {
|
||||
channelId: string,
|
||||
): Promise<ChannelAndLineup<Channel> | null>;
|
||||
|
||||
loadChannelAndLineupOrm(
|
||||
channelId: string,
|
||||
): Promise<ChannelAndLineup<ChannelOrm> | null>;
|
||||
|
||||
addPendingPrograms(
|
||||
channelId: string,
|
||||
pendingPrograms: PendingProgram[],
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ChannelFillerShow } from './ChannelFillerShow.ts';
|
||||
import { ChannelPrograms } from './ChannelPrograms.ts';
|
||||
import type { KyselifyBetter } from './KyselifyBetter.ts';
|
||||
import { ProgramPlayHistory } from './ProgramPlayHistory.ts';
|
||||
import { TranscodeConfig } from './TranscodeConfig.ts';
|
||||
|
||||
export const Channel = sqliteTable(
|
||||
'channel',
|
||||
@@ -73,9 +74,13 @@ export type NewChannel = Insertable<ChannelTable>;
|
||||
export type ChannelUpdate = Updateable<ChannelTable>;
|
||||
export type ChannelOrm = InferSelectModel<typeof Channel>;
|
||||
|
||||
export const ChannelRelations = relations(Channel, ({ many }) => ({
|
||||
export const ChannelRelations = relations(Channel, ({ many, one }) => ({
|
||||
channelPrograms: many(ChannelPrograms),
|
||||
channelCustomShows: many(ChannelCustomShow),
|
||||
channelFillerShow: many(ChannelFillerShow),
|
||||
playHistory: many(ProgramPlayHistory),
|
||||
transcodeConfig: one(TranscodeConfig, {
|
||||
fields: [Channel.transcodeConfigId],
|
||||
references: [TranscodeConfig.uuid],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Resolution, TupleToUnion } from '@tunarr/types';
|
||||
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { check, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import type { Insertable, Selectable, Updateable } from 'kysely';
|
||||
@@ -200,8 +201,38 @@ export type TranscodeConfig = Selectable<TranscodeConfigTable>;
|
||||
export type NewTranscodeConfig = Insertable<TranscodeConfigTable>;
|
||||
export type TranscodeConfigUpdate = Updateable<TranscodeConfigTable>;
|
||||
|
||||
export type TranscodeConfigOrm = InferSelectModel<typeof TranscodeConfig>;
|
||||
export type NewTranscodeConfigOrm = InferInsertModel<typeof TranscodeConfig>;
|
||||
|
||||
export const defaultTranscodeConfig = (
|
||||
isDefault?: boolean,
|
||||
): NewTranscodeConfigOrm => {
|
||||
return {
|
||||
threadCount: 0,
|
||||
audioBitRate: 192,
|
||||
audioBufferSize: 192 * 2,
|
||||
audioChannels: 2,
|
||||
audioFormat: 'aac',
|
||||
audioSampleRate: 48,
|
||||
hardwareAccelerationMode: 'none',
|
||||
name: isDefault ? 'Default' : `h264 @ 1920x1080`,
|
||||
resolution: {
|
||||
widthPx: 1920,
|
||||
heightPx: 1080,
|
||||
} satisfies Resolution,
|
||||
uuid: v4(),
|
||||
videoBitRate: 2000,
|
||||
videoBufferSize: 4000,
|
||||
videoFormat: 'h264',
|
||||
disableChannelOverlay: false,
|
||||
normalizeFrameRate: false,
|
||||
videoBitDepth: 8,
|
||||
isDefault: !!isDefault,
|
||||
};
|
||||
};
|
||||
|
||||
export const defaultTranscodeConfigLegacy = (
|
||||
isDefault?: boolean,
|
||||
): NewTranscodeConfig => {
|
||||
return {
|
||||
threadCount: 0,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type {
|
||||
TranscodeConfig,
|
||||
TranscodeConfigOrm,
|
||||
} from '@/db/schema/TranscodeConfig.js';
|
||||
import type { MarkNonNullable, Nullable } from '@/types/util.js';
|
||||
import type { Insertable } from 'kysely';
|
||||
import type { DeepNullable, MarkRequired, StrictOmit } from 'ts-essentials';
|
||||
@@ -182,7 +185,7 @@ export type ChannelOrmWithRelations = ChannelOrm & {
|
||||
programs?: ProgramWithRelationsOrm[];
|
||||
fillerContent?: ProgramWithRelationsOrm[];
|
||||
fillerShows?: ChannelFillerShow[];
|
||||
transcodeConfig?: TranscodeConfig;
|
||||
transcodeConfig?: TranscodeConfigOrm;
|
||||
subtitlePreferences?: ChannelSubtitlePreferences[];
|
||||
};
|
||||
|
||||
@@ -204,6 +207,11 @@ export type ChannelOrmWithPrograms = MarkRequired<
|
||||
'programs'
|
||||
>;
|
||||
|
||||
export type ChannelOrmWithTranscodeConfig = MarkRequired<
|
||||
ChannelOrmWithRelations,
|
||||
'transcodeConfig'
|
||||
>;
|
||||
|
||||
export type ChannelFillerShowWithRelations = ChannelFillerShow & {
|
||||
fillerShow: MarkNonNullable<DeepNullable<FillerShow>, 'uuid'>;
|
||||
fillerContent?: ProgramWithRelations[];
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
TagRelations,
|
||||
TagRelationSchema,
|
||||
} from './Tag.ts';
|
||||
import { TranscodeConfig } from './TranscodeConfig.ts';
|
||||
|
||||
// export { Program } from './Program.ts';
|
||||
|
||||
@@ -157,6 +158,7 @@ export const schema = {
|
||||
tagRelations: TagJoinRelationSchema,
|
||||
tagJoin: TagRelations,
|
||||
tagJoinRelations: TagRelationSchema,
|
||||
transcodeConfigs: TranscodeConfig,
|
||||
};
|
||||
|
||||
export type DrizzleDBAccess = BetterSQLite3Database<typeof schema>;
|
||||
|
||||
28
server/src/external/plex/PlexApiClient.ts
vendored
28
server/src/external/plex/PlexApiClient.ts
vendored
@@ -646,19 +646,21 @@ export class PlexApiClient extends MediaSourceApiClient<PlexTypes> {
|
||||
);
|
||||
|
||||
return result.mapPure((data) => {
|
||||
const playlists = (data.MediaContainer.Metadata ?? []).map(
|
||||
(playlist) =>
|
||||
({
|
||||
externalId: playlist.ratingKey,
|
||||
libraryId: '',
|
||||
mediaSourceId: this.options.mediaSource.uuid,
|
||||
sourceType: this.options.mediaSource.type,
|
||||
title: playlist.title,
|
||||
type: 'playlist',
|
||||
uuid: v4(),
|
||||
childCount: playlist.leafCount,
|
||||
}) satisfies Playlist,
|
||||
);
|
||||
const playlists = (data.MediaContainer.Metadata ?? [])
|
||||
.filter((playlist) => playlist.playlistType !== 'photo')
|
||||
.map(
|
||||
(playlist) =>
|
||||
({
|
||||
externalId: playlist.ratingKey,
|
||||
libraryId: '',
|
||||
mediaSourceId: this.options.mediaSource.uuid,
|
||||
sourceType: this.options.mediaSource.type,
|
||||
title: playlist.title,
|
||||
type: 'playlist',
|
||||
uuid: v4(),
|
||||
childCount: playlist.leafCount,
|
||||
}) satisfies Playlist,
|
||||
);
|
||||
|
||||
return {
|
||||
result: playlists,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { ISettingsDB } from '@/db/interfaces/ISettingsDB.js';
|
||||
import type { Channel } from '@/db/schema/Channel.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import { FfmpegStreamFactory } from '@/ffmpeg/FfmpegStreamFactory.js';
|
||||
import type { IFFMPEG } from '@/ffmpeg/ffmpegBase.js';
|
||||
import { KEYS } from '@/types/inject.js';
|
||||
import type { ChannelStreamMode } from '@tunarr/types';
|
||||
import { ContainerModule } from 'inversify';
|
||||
import type { IChannelDB } from '../db/interfaces/IChannelDB.ts';
|
||||
import type { ChannelOrm } from '../db/schema/Channel.ts';
|
||||
import { bindFactoryFunc } from '../util/inject.ts';
|
||||
import type { PipelineBuilderFactory } from './builder/pipeline/PipelineBuilderFactory.ts';
|
||||
import { FfmpegInfo } from './ffmpegInfo.ts';
|
||||
|
||||
export type FFmpegFactory = (
|
||||
transcodeConfig: TranscodeConfig,
|
||||
channel: Channel,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
channel: ChannelOrm,
|
||||
streamMode: ChannelStreamMode,
|
||||
) => IFFMPEG;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import {
|
||||
HardwareAccelerationMode,
|
||||
TranscodeAudioOutputFormat,
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import type { ChannelStreamMode } from '@/db/schema/base.js';
|
||||
import type { StreamDetails, VideoStreamDetails } from '@/stream/types.js';
|
||||
import { gcd } from '@/util/index.js';
|
||||
import { numberToBoolean } from '@/util/sqliteUtil.js';
|
||||
import type { Resolution } from '@tunarr/types';
|
||||
import { ChannelStreamModes } from '@tunarr/types';
|
||||
import type { OutputFormat, VideoFormat } from './builder/constants.ts';
|
||||
@@ -16,7 +15,7 @@ import { FrameSize } from './builder/types.ts';
|
||||
|
||||
export class FfmpegPlaybackParamsCalculator {
|
||||
constructor(
|
||||
private transcodeConfig: TranscodeConfig,
|
||||
private transcodeConfig: TranscodeConfigOrm,
|
||||
private streamMode: ChannelStreamMode,
|
||||
) {}
|
||||
|
||||
@@ -95,7 +94,7 @@ export class FfmpegPlaybackParamsCalculator {
|
||||
params.pixelFormat = new PixelFormatYuv420P();
|
||||
|
||||
params.deinterlace =
|
||||
numberToBoolean(this.transcodeConfig.deinterlaceVideo) &&
|
||||
!!this.transcodeConfig.deinterlaceVideo &&
|
||||
videoStream.scanType === 'interlaced';
|
||||
}
|
||||
|
||||
@@ -165,7 +164,7 @@ export type FfmpegPlaybackParams = {
|
||||
};
|
||||
|
||||
function needsToScale(
|
||||
transcodeConfig: TranscodeConfig,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
videoStreamDetails: VideoStreamDetails,
|
||||
) {
|
||||
return (
|
||||
@@ -213,7 +212,7 @@ function isAnamorphic(videoStreamDetails: VideoStreamDetails) {
|
||||
}
|
||||
|
||||
function calculateScaledSize(
|
||||
config: TranscodeConfig,
|
||||
config: TranscodeConfigOrm,
|
||||
videoStream: VideoStreamDetails,
|
||||
) {
|
||||
const { widthPx: targetW, heightPx: targetH } = config.resolution;
|
||||
|
||||
@@ -2,8 +2,8 @@ import type {
|
||||
ISettingsDB,
|
||||
ReadableFfmpegSettings,
|
||||
} from '@/db/interfaces/ISettingsDB.js';
|
||||
import type { Channel } from '@/db/schema/Channel.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { ChannelOrm } from '@/db/schema/Channel.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import { InfiniteLoopInputOption } from '@/ffmpeg/builder/options/input/InfiniteLoopInputOption.js';
|
||||
import type { AudioStreamDetails } from '@/stream/types.js';
|
||||
import { FileStreamSource, HttpStreamSource } from '@/stream/types.js';
|
||||
@@ -18,7 +18,6 @@ import { isUndefined } from 'lodash-es';
|
||||
import type { DeepReadonly, NonEmptyArray } from 'ts-essentials';
|
||||
import { match, P } from 'ts-pattern';
|
||||
import type { IChannelDB } from '../db/interfaces/IChannelDB.ts';
|
||||
import { numberToBoolean } from '../util/sqliteUtil.ts';
|
||||
import { FfmpegPlaybackParamsCalculator } from './FfmpegPlaybackParamsCalculator.ts';
|
||||
import { FfmpegProcess } from './FfmpegProcess.ts';
|
||||
import { FfmpegTranscodeSession } from './FfmpegTrancodeSession.ts';
|
||||
@@ -72,8 +71,8 @@ export class FfmpegStreamFactory extends IFFMPEG {
|
||||
|
||||
constructor(
|
||||
private ffmpegSettings: ReadableFfmpegSettings,
|
||||
private transcodeConfig: TranscodeConfig,
|
||||
private channel: Channel,
|
||||
private transcodeConfig: TranscodeConfigOrm,
|
||||
private channel: ChannelOrm,
|
||||
private ffmpegInfo: FfmpegInfo,
|
||||
private settingsDB: ISettingsDB,
|
||||
private pipelineBuilderFactory: PipelineBuilderFactory,
|
||||
@@ -504,11 +503,11 @@ export class FfmpegStreamFactory extends IFFMPEG {
|
||||
vaapiDevice: this.getVaapiDevice(),
|
||||
vaapiDriver: this.getVaapiDriver(),
|
||||
disableHardwareDecoding:
|
||||
numberToBoolean(this.transcodeConfig.disableHardwareDecoder) ?? false,
|
||||
this.transcodeConfig.disableHardwareDecoder ?? false,
|
||||
disableHardwareEncoding:
|
||||
numberToBoolean(this.transcodeConfig.disableHardwareEncoding) ?? false,
|
||||
this.transcodeConfig.disableHardwareEncoding ?? false,
|
||||
disableHardwareFilters:
|
||||
numberToBoolean(this.transcodeConfig.disableHardwareFilters) ?? false,
|
||||
this.transcodeConfig.disableHardwareFilters ?? false,
|
||||
};
|
||||
|
||||
const pipeline = builder.build(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReadableFfmpegSettings } from '@/db/interfaces/ISettingsDB.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import { HardwareAccelerationMode } from '@/db/schema/TranscodeConfig.js';
|
||||
import type {
|
||||
BaseFfmpegHardwareCapabilities,
|
||||
@@ -16,7 +16,7 @@ export class HardwareCapabilitiesFactory
|
||||
{
|
||||
constructor(
|
||||
private ffmpegSettings: ReadableFfmpegSettings,
|
||||
private transcodeConfig: TranscodeConfig,
|
||||
private transcodeConfig: TranscodeConfigOrm,
|
||||
) {}
|
||||
|
||||
async getCapabilities(): Promise<BaseFfmpegHardwareCapabilities> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { seq } from '@tunarr/shared/util';
|
||||
import { drop, isEmpty, map, reject, split, trim } from 'lodash-es';
|
||||
import type { ReadableFfmpegSettings } from '../../../db/interfaces/ISettingsDB.ts';
|
||||
import type { TranscodeConfig } from '../../../db/schema/TranscodeConfig.ts';
|
||||
import type { TranscodeConfigOrm } from '../../../db/schema/TranscodeConfig.ts';
|
||||
import { ChildProcessHelper } from '../../../util/ChildProcessHelper.ts';
|
||||
import { LoggerFactory } from '../../../util/logging/LoggerFactory.ts';
|
||||
import { FFmpegOptionsExtractionPattern } from '../../ffmpegInfo.ts';
|
||||
@@ -21,7 +21,7 @@ export class QsvHardwareCapabilitiesFactory
|
||||
});
|
||||
|
||||
constructor(
|
||||
private transcodeConfig: TranscodeConfig,
|
||||
private transcodeConfig: TranscodeConfigOrm,
|
||||
private ffmpegSettings: ReadableFfmpegSettings,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { FfmpegHardwareCapabilitiesFactory } from '@/ffmpeg/builder/capabilities/BaseFfmpegHardwareCapabilities.js';
|
||||
import { DefaultHardwareCapabilities } from '@/ffmpeg/builder/capabilities/DefaultHardwareCapabilities.js';
|
||||
import { NoHardwareCapabilities } from '@/ffmpeg/builder/capabilities/NoHardwareCapabilities.js';
|
||||
@@ -26,7 +26,7 @@ export class VaapiHardwareCapabilitiesFactory
|
||||
return `vainfo_${driver}_${device}`;
|
||||
}
|
||||
|
||||
constructor(private transcodeConfig: TranscodeConfig) {}
|
||||
constructor(private transcodeConfig: TranscodeConfigOrm) {}
|
||||
|
||||
async getCapabilities() {
|
||||
// windows check bail!
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
ISettingsDB,
|
||||
ReadableFfmpegSettings,
|
||||
} from '@/db/interfaces/ISettingsDB.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import { HardwareAccelerationMode } from '@/db/schema/TranscodeConfig.js';
|
||||
import { HardwareCapabilitiesFactory } from '@/ffmpeg/builder/capabilities/HardwareCapabilitiesFactory.js';
|
||||
import type { AudioInputSource } from '@/ffmpeg/builder/input/AudioInputSource.js';
|
||||
@@ -24,7 +24,7 @@ import { NvidiaPipelineBuilder } from './nvidia/NvidiaPipelineBuilder.ts';
|
||||
import { SoftwarePipelineBuilder } from './software/SoftwarePipelineBuilder.ts';
|
||||
|
||||
export type PipelineBuilderFactory = (
|
||||
transcodeConfig: TranscodeConfig,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
) => PipelineBuilderFactory$Builder;
|
||||
|
||||
export const FfmpegPipelineBuilderModule = new ContainerModule((bind) => {
|
||||
@@ -56,7 +56,7 @@ class PipelineBuilderFactory$Builder {
|
||||
constructor(
|
||||
private ffmpegSettings: ReadableFfmpegSettings,
|
||||
private ffmpegInfo: FfmpegInfo,
|
||||
private transcodeConfig: TranscodeConfig,
|
||||
private transcodeConfig: TranscodeConfigOrm,
|
||||
) {}
|
||||
|
||||
setVideoInputSource(
|
||||
|
||||
@@ -3,14 +3,14 @@ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import events from 'node:events';
|
||||
import type { ReadableFfmpegSettings } from '../db/interfaces/ISettingsDB.ts';
|
||||
import type { TranscodeConfig } from '../db/schema/TranscodeConfig.ts';
|
||||
import type { TranscodeConfigOrm } from '../db/schema/TranscodeConfig.ts';
|
||||
|
||||
export class FfmpegText extends events.EventEmitter {
|
||||
private args: string[];
|
||||
private ffmpeg: ChildProcessWithoutNullStreams;
|
||||
|
||||
constructor(
|
||||
transcodeConfig: TranscodeConfig,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
opts: ReadableFfmpegSettings,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NewTranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import { defaultTranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import { defaultTranscodeConfigLegacy } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { DB } from '@/db/schema/db.js';
|
||||
import { booleanToNumber } from '@/util/sqliteUtil.js';
|
||||
import type { Resolution } from '@tunarr/types';
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
)
|
||||
.execute();
|
||||
|
||||
const defaultConfig = defaultTranscodeConfig(true);
|
||||
const defaultConfig = defaultTranscodeConfigLegacy(true);
|
||||
const transcodeConfigId = (
|
||||
await db
|
||||
.insertInto('transcodeConfig')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Channel } from '@/db/schema/Channel.js';
|
||||
import type { ChannelOrm } from '@/db/schema/Channel.js';
|
||||
import { ChannelCache } from '@/stream/ChannelCache.js';
|
||||
import type { Maybe } from '@/types/util.js';
|
||||
import { random } from '@/util/random.js';
|
||||
@@ -31,7 +31,7 @@ export class FillerPicker implements IFillerPicker {
|
||||
}
|
||||
|
||||
pickFiller(
|
||||
channel: Channel,
|
||||
channel: ChannelOrm,
|
||||
fillers: ChannelFillerShowWithContent[],
|
||||
maxDuration: number,
|
||||
): Promise<FillerPickResult> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Channel } from '../../db/schema/Channel.ts';
|
||||
import type { ChannelOrm } from '../../db/schema/Channel.ts';
|
||||
import type {
|
||||
ChannelFillerShowWithContent,
|
||||
ProgramWithRelations,
|
||||
@@ -19,7 +19,7 @@ export const EmptyFillerPickResult: FillerPickResult = {
|
||||
|
||||
export interface IFillerPicker {
|
||||
pickFiller(
|
||||
channel: Channel,
|
||||
channel: ChannelOrm,
|
||||
fillers: ChannelFillerShowWithContent[],
|
||||
maxDuration: number,
|
||||
now?: number,
|
||||
|
||||
@@ -2,7 +2,7 @@ import dayjs from 'dayjs';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { groupBy, isEmpty, maxBy, sumBy } from 'lodash-es';
|
||||
import { ProgramPlayHistoryDB } from '../../db/ProgramPlayHistoryDB.ts';
|
||||
import { Channel } from '../../db/schema/Channel.ts';
|
||||
import { ChannelOrm } from '../../db/schema/Channel.ts';
|
||||
import { ChannelFillerShowWithContent } from '../../db/schema/derivedTypes.ts';
|
||||
import {
|
||||
FiveMinutesMillis,
|
||||
@@ -34,7 +34,7 @@ export class FillerPickerV2 implements IFillerPicker {
|
||||
) {}
|
||||
|
||||
async pickFiller(
|
||||
channel: Channel,
|
||||
channel: ChannelOrm,
|
||||
fillers: ChannelFillerShowWithContent[],
|
||||
maxDuration: number,
|
||||
now = +dayjs(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { FfmpegTranscodeSession } from '@/ffmpeg/FfmpegTrancodeSession.js';
|
||||
import type { ChannelConcatStreamMode } from '@tunarr/types/schemas';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -12,7 +12,7 @@ export type ConcatSessionOptions = SessionOptions & {
|
||||
};
|
||||
|
||||
export type ConcatSessionFactory = (
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: ConcatSessionOptions,
|
||||
) => ConcatSession;
|
||||
|
||||
@@ -20,7 +20,7 @@ export class ConcatSession extends DirectStreamSession<ConcatSessionOptions> {
|
||||
#transcodeSession: FfmpegTranscodeSession;
|
||||
|
||||
constructor(
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: ConcatSessionOptions,
|
||||
private concatStreamFactory: ConcatStreamFactory,
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { FFmpegFactory } from '@/ffmpeg/FFmpegModule.js';
|
||||
import type { FfmpegTranscodeSession } from '@/ffmpeg/FfmpegTrancodeSession.js';
|
||||
import { MpegTsOutputFormat } from '@/ffmpeg/builder/constants.js';
|
||||
@@ -7,12 +7,12 @@ import { makeFfmpegPlaylistUrl, makeLocalUrl } from '@/util/serverUtil.js';
|
||||
import type { ChannelConcatStreamMode } from '@tunarr/types/schemas';
|
||||
|
||||
export type ConcatStreamFactory = (
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
streamMode: ChannelConcatStreamMode,
|
||||
) => ConcatStream;
|
||||
export class ConcatStream {
|
||||
constructor(
|
||||
private channel: ChannelWithTranscodeConfig,
|
||||
private channel: ChannelOrmWithTranscodeConfig,
|
||||
private streamMode: ChannelConcatStreamMode,
|
||||
private ffmpegFactory: FFmpegFactory,
|
||||
) {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { FfmpegTranscodeSession } from '@/ffmpeg/FfmpegTrancodeSession.js';
|
||||
import { once, round } from 'lodash-es';
|
||||
import type { Readable } from 'node:stream';
|
||||
@@ -15,7 +15,7 @@ export abstract class DirectStreamSession<
|
||||
> extends Session<TOpts> {
|
||||
#stream: Readable;
|
||||
|
||||
protected constructor(channel: ChannelWithTranscodeConfig, opts: TOpts) {
|
||||
protected constructor(channel: ChannelOrmWithTranscodeConfig, opts: TOpts) {
|
||||
super(channel, opts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StreamLineupItem } from '@/db/derived_types/StreamLineup.js';
|
||||
import type { Channel } from '@/db/schema/Channel.js';
|
||||
import type { TranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { ChannelOrm } from '@/db/schema/Channel.js';
|
||||
import type { TranscodeConfigOrm } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { ChannelStreamMode } from '@tunarr/types';
|
||||
import dayjs from 'dayjs';
|
||||
import type { GetCurrentLineupItemRequest } from './StreamProgramCalculator.ts';
|
||||
@@ -20,21 +20,21 @@ export class PlayerContext {
|
||||
*/
|
||||
constructor(
|
||||
public lineupItem: StreamLineupItem,
|
||||
public targetChannel: Channel,
|
||||
public sourceChannel: Channel,
|
||||
public targetChannel: ChannelOrm,
|
||||
public sourceChannel: ChannelOrm,
|
||||
public audioOnly: boolean,
|
||||
public realtime: boolean,
|
||||
public transcodeConfig: TranscodeConfig,
|
||||
public transcodeConfig: TranscodeConfigOrm,
|
||||
public streamMode: ChannelStreamMode,
|
||||
) {}
|
||||
|
||||
static error(
|
||||
duration: number,
|
||||
error: string | boolean | Error,
|
||||
targetChannel: Channel,
|
||||
sourceChannel: Channel,
|
||||
targetChannel: ChannelOrm,
|
||||
sourceChannel: ChannelOrm,
|
||||
realtime: boolean,
|
||||
transcodeConfig: TranscodeConfig,
|
||||
transcodeConfig: TranscodeConfigOrm,
|
||||
streamMode: ChannelStreamMode,
|
||||
): PlayerContext {
|
||||
return new PlayerContext(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { TypedEventEmitter } from '@/types/eventEmitter.js';
|
||||
import { Result } from '@/types/result.js';
|
||||
import type { Maybe } from '@/types/util.js';
|
||||
@@ -58,7 +58,7 @@ export abstract class Session<
|
||||
protected lock = new Mutex();
|
||||
protected logger: Logger;
|
||||
protected sessionOptions: TOpts;
|
||||
protected channel: ChannelWithTranscodeConfig;
|
||||
protected channel: ChannelOrmWithTranscodeConfig;
|
||||
protected connectionTracker: ConnectionTracker<StreamConnectionDetails>;
|
||||
|
||||
#state: SessionState = 'init';
|
||||
@@ -66,7 +66,7 @@ export abstract class Session<
|
||||
|
||||
error: Maybe<Error>;
|
||||
|
||||
constructor(channel: ChannelWithTranscodeConfig, opts: TOpts) {
|
||||
constructor(channel: ChannelOrmWithTranscodeConfig, opts: TOpts) {
|
||||
super();
|
||||
this.#uniqueId = v4();
|
||||
this.logger = LoggerFactory.child({
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { HlsSlowerSession } from './hls/HlsSlowerSession.js';
|
||||
|
||||
import { type IChannelDB } from '@/db/interfaces/IChannelDB.js';
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import { OnDemandChannelService } from '@/services/OnDemandChannelService.js';
|
||||
import { KEYS } from '@/types/inject.js';
|
||||
import { ifDefined } from '@/util/index.js';
|
||||
@@ -258,7 +258,7 @@ export class SessionManager {
|
||||
token: string,
|
||||
connection: StreamConnectionDetails,
|
||||
sessionType: SessionType,
|
||||
sessionFactory: (channel: ChannelWithTranscodeConfig) => TSession,
|
||||
sessionFactory: (channel: ChannelOrmWithTranscodeConfig) => TSession,
|
||||
): Promise<Result<TSession, TypedError>> {
|
||||
const lock = await this.#sessionLocker.getOrCreateLock(channelId);
|
||||
try {
|
||||
@@ -269,12 +269,9 @@ export class SessionManager {
|
||||
) as Maybe<TSession>;
|
||||
|
||||
if (isNil(session)) {
|
||||
const channel = await this.channelDB
|
||||
.getChannelBuilder(channelId)
|
||||
.withTranscodeConfig()
|
||||
.executeTakeFirst();
|
||||
const channel = await this.channelDB.getChannelOrm(channelId);
|
||||
|
||||
if (isNil(channel)) {
|
||||
if (!channel?.transcodeConfig) {
|
||||
throw new ChannelNotFoundError(channelId);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { MediaSourceId } from '../db/schema/base.ts';
|
||||
import { IStreamLineupCache } from '../interfaces/IStreamLineupCache.ts';
|
||||
import { IFillerPicker } from '../services/interfaces/IFillerPicker.ts';
|
||||
import {
|
||||
createChannel,
|
||||
createChannelOrm,
|
||||
createFakeProgram,
|
||||
} from '../testing/fakes/entityCreators.ts';
|
||||
import { LoggerFactory } from '../util/logging/LoggerFactory.ts';
|
||||
@@ -72,14 +72,14 @@ describe('StreamProgramCalculator', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const channel = createChannel({
|
||||
const channel = createChannelOrm({
|
||||
uuid: channelId,
|
||||
number: 1,
|
||||
startTime: +startTime.subtract(1, 'hour'),
|
||||
duration: sumBy(lineup, ({ durationMs }) => durationMs),
|
||||
});
|
||||
|
||||
when(channelDB.getChannel(1)).thenReturn(Promise.resolve(channel));
|
||||
when(channelDB.getChannelOrm(1)).thenReturn(Promise.resolve(channel));
|
||||
|
||||
when(channelDB.loadLineup(channelId)).thenReturn(
|
||||
Promise.resolve({
|
||||
@@ -183,14 +183,14 @@ describe('StreamProgramCalculator', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const channel = createChannel({
|
||||
const channel = createChannelOrm({
|
||||
uuid: channelId,
|
||||
number: 1,
|
||||
startTime: +startTime.subtract(1, 'hour'),
|
||||
duration: sumBy(lineup, ({ durationMs }) => durationMs),
|
||||
});
|
||||
|
||||
when(channelDB.getChannel(1)).thenReturn(Promise.resolve(channel));
|
||||
when(channelDB.getChannelOrm(1)).thenReturn(Promise.resolve(channel));
|
||||
|
||||
when(channelDB.loadLineup(channelId)).thenReturn(
|
||||
Promise.resolve({
|
||||
@@ -296,14 +296,14 @@ describe('StreamProgramCalculator', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const channel = createChannel({
|
||||
const channel = createChannelOrm({
|
||||
uuid: channelId,
|
||||
number: 1,
|
||||
startTime: +startTime.subtract(1, 'hour'),
|
||||
duration: sumBy(lineup, ({ durationMs }) => durationMs),
|
||||
});
|
||||
|
||||
when(channelDB.getChannel(1)).thenReturn(Promise.resolve(channel));
|
||||
when(channelDB.getChannelOrm(1)).thenReturn(Promise.resolve(channel));
|
||||
|
||||
when(channelDB.loadLineup(channelId)).thenReturn(
|
||||
Promise.resolve({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Channel } from '@/db/schema/Channel.js';
|
||||
import { ChannelOrm } from '@/db/schema/Channel.js';
|
||||
import type { ProgramWithRelations as RawProgramEntity } from '@/db/schema/derivedTypes.js';
|
||||
import { KEYS } from '@/types/inject.js';
|
||||
import { Result } from '@/types/result.js';
|
||||
@@ -67,8 +67,8 @@ export type CurrentLineupItemResult = {
|
||||
lineupItem: StreamLineupItem;
|
||||
// Either the source channel or the target channel
|
||||
// if the current program is a redirect
|
||||
channelContext: Channel;
|
||||
sourceChannel: Channel;
|
||||
channelContext: ChannelOrm;
|
||||
sourceChannel: ChannelOrm;
|
||||
};
|
||||
|
||||
@injectable()
|
||||
@@ -90,7 +90,7 @@ export class StreamProgramCalculator {
|
||||
req: GetCurrentLineupItemRequest,
|
||||
): Promise<Result<CurrentLineupItemResult>> {
|
||||
const startTime = req.startTime;
|
||||
const channel = await this.channelDB.getChannel(req.channelId);
|
||||
const channel = await this.channelDB.getChannelOrm(req.channelId);
|
||||
|
||||
if (isNil(channel)) {
|
||||
return Result.failure(
|
||||
@@ -111,7 +111,7 @@ export class StreamProgramCalculator {
|
||||
}
|
||||
|
||||
let lineupItem: Maybe<StreamLineupItem>;
|
||||
let channelContext: Channel = channel;
|
||||
let channelContext: ChannelOrm = channel;
|
||||
const redirectChannels: string[] = [];
|
||||
const upperBounds: number[] = [];
|
||||
|
||||
@@ -148,7 +148,7 @@ export class StreamProgramCalculator {
|
||||
|
||||
const nextChannelId = currentProgram.program.channel;
|
||||
const newChannelAndLineup =
|
||||
await this.channelDB.loadChannelAndLineup(nextChannelId);
|
||||
await this.channelDB.loadChannelAndLineupOrm(nextChannelId);
|
||||
|
||||
if (isNil(newChannelAndLineup)) {
|
||||
const msg = "Invalid redirect to a channel that doesn't exist";
|
||||
@@ -418,7 +418,7 @@ export class StreamProgramCalculator {
|
||||
async createLineupItem(
|
||||
{ program, timeElapsed }: ProgramAndTimeElapsed,
|
||||
streamDuration: number,
|
||||
channel: Channel,
|
||||
channel: ChannelOrm,
|
||||
): Promise<StreamLineupItem> {
|
||||
if (program.type === 'redirect') {
|
||||
throw new Error(
|
||||
|
||||
@@ -76,10 +76,7 @@ export class VideoStream {
|
||||
// const serverCtx = getServerContext();
|
||||
const outStream = new PassThrough();
|
||||
|
||||
const channel = await this.channelDB
|
||||
.getChannelBuilder(channelIdOrNumber)
|
||||
.withTranscodeConfig()
|
||||
.executeTakeFirst();
|
||||
const channel = await this.channelDB.getChannelOrm(channelIdOrNumber);
|
||||
|
||||
if (isNil(channel)) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { SessionOptions } from '@/stream/Session.js';
|
||||
import { Session } from '@/stream/Session.js';
|
||||
import { Result } from '@/types/result.js';
|
||||
@@ -41,7 +41,10 @@ export abstract class BaseHlsSession<
|
||||
return minBy([...this._minByIp.entries()], ([_, seg]) => seg)?.[1] ?? 0;
|
||||
}
|
||||
|
||||
constructor(channel: ChannelWithTranscodeConfig, options: HlsSessionOptsT) {
|
||||
constructor(
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: HlsSessionOptsT,
|
||||
) {
|
||||
super(channel, options);
|
||||
|
||||
this._workingDirectory = path.join(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ISettingsDB } from '@/db/interfaces/ISettingsDB.js';
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { FfmpegTranscodeSession } from '@/ffmpeg/FfmpegTrancodeSession.js';
|
||||
import { GetLastPtsDurationTask } from '@/ffmpeg/GetLastPtsDuration.js';
|
||||
import type { HlsOptions, OutputFormat } from '@/ffmpeg/builder/constants.js';
|
||||
@@ -30,12 +30,12 @@ import type { HlsPlaylistFilterOptions } from './HlsPlaylistMutator.js';
|
||||
import { HlsPlaylistMutator } from './HlsPlaylistMutator.js';
|
||||
|
||||
export type HlsSessionProvider = (
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: HlsSessionOptions,
|
||||
) => HlsSession;
|
||||
|
||||
export type HlsSlowerSessionProvider = (
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: BaseHlsSessionOptions,
|
||||
) => HlsSlowerSession;
|
||||
|
||||
@@ -55,7 +55,7 @@ export class HlsSession extends BaseHlsSession<HlsSessionOptions> {
|
||||
#isFirstTranscode = true;
|
||||
|
||||
constructor(
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: HlsSessionOptions,
|
||||
private programCalculator: StreamProgramCalculator,
|
||||
private settingsDB: ISettingsDB,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { ChannelOrmWithTranscodeConfig } from '@/db/schema/derivedTypes.js';
|
||||
import type { FfmpegTranscodeSession } from '@/ffmpeg/FfmpegTrancodeSession.js';
|
||||
import type { ProgramStream } from '@/stream/ProgramStream.js';
|
||||
import type { StreamProgramCalculator } from '@/stream/StreamProgramCalculator.js';
|
||||
@@ -33,7 +33,7 @@ export class HlsSlowerSession extends BaseHlsSession {
|
||||
#concatSession: FfmpegTranscodeSession;
|
||||
|
||||
constructor(
|
||||
channel: ChannelWithTranscodeConfig,
|
||||
channel: ChannelOrmWithTranscodeConfig,
|
||||
options: BaseHlsSessionOptions,
|
||||
programCalculator: StreamProgramCalculator,
|
||||
private programStreamFactory: ProgramStreamFactory,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultTranscodeConfig } from '@/db/schema/TranscodeConfig.js';
|
||||
import { defaultTranscodeConfigLegacy } from '@/db/schema/TranscodeConfig.js';
|
||||
import Fixer from '@/tasks/fixers/fixer.js';
|
||||
import { KEYS } from '@/types/inject.js';
|
||||
import { type Logger } from '@/util/logging/LoggerFactory.js';
|
||||
@@ -71,7 +71,7 @@ export class EnsureTranscodeConfigIds extends Fixer {
|
||||
return (
|
||||
await this.db
|
||||
.insertInto('transcodeConfig')
|
||||
.values(defaultTranscodeConfig(true))
|
||||
.values(defaultTranscodeConfigLegacy(true))
|
||||
.returning('uuid')
|
||||
.executeTakeFirstOrThrow()
|
||||
).uuid;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { tag } from '@tunarr/types';
|
||||
import { MultiExternalIdType } from '@tunarr/types/schemas';
|
||||
import type { Channel } from '../../db/schema/Channel.ts';
|
||||
import type { Channel, ChannelOrm } from '../../db/schema/Channel.ts';
|
||||
import type { ProgramDao } from '../../db/schema/Program.ts';
|
||||
import { ProgramTypes } from '../../db/schema/Program.ts';
|
||||
import type { MinimalProgramExternalId } from '../../db/schema/ProgramExternalId.ts';
|
||||
@@ -24,6 +24,18 @@ export function createChannel(overrides?: Partial<Channel>): Channel {
|
||||
} satisfies Channel;
|
||||
}
|
||||
|
||||
export function createChannelOrm(overrides?: Partial<ChannelOrm>): ChannelOrm {
|
||||
return {
|
||||
uuid: faker.string.uuid(),
|
||||
duration: faker.number.int({ min: 0 }),
|
||||
startTime: faker.date.past().getTime(),
|
||||
createdAt: null,
|
||||
disableFillerOverlay: faker.datatype.boolean(),
|
||||
name: faker.music.artist(),
|
||||
...(overrides ?? {}),
|
||||
} satisfies ChannelOrm;
|
||||
}
|
||||
|
||||
export function createFakeMultiExternalId(): MinimalProgramExternalId {
|
||||
const typ = faker.helpers.arrayElement(MultiExternalIdType);
|
||||
return {
|
||||
|
||||
@@ -232,7 +232,7 @@ export const PlexPlaylistSchema = z.object({
|
||||
titleSort: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
smart: z.boolean().optional(),
|
||||
playlistType: z.union([z.literal('video'), z.literal('audio')]).optional(), // Add new known types here
|
||||
playlistType: z.enum(['video', 'audio', 'photo']).optional(), // Add new known types here
|
||||
composite: z.string().optional(), // Thumb path
|
||||
icon: z.string().optional(),
|
||||
viewCount: z.number().optional(),
|
||||
|
||||
Reference in New Issue
Block a user