checkpoint

This commit is contained in:
Christian Benincasa
2024-12-20 12:54:04 -05:00
parent aad1ecfe1a
commit 125c54ac53
10 changed files with 76 additions and 45 deletions

View File

@@ -144,6 +144,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
.selectFrom('program')
.orderBy((ob) => ob.fn('random'))
.where('type', '=', ProgramType.Episode)
.select(withProgramExternalIds)
.limit(1)
.selectAll()
.executeTakeFirstOrThrow();
@@ -276,7 +277,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
);
async function initStream(
program: ProgramDao,
program: ProgramWithExternalIds,
channel: Channel,
transcodeConfig: TranscodeConfig,
startTime: number = 0,

View File

@@ -11,10 +11,13 @@ import type {
} from '@tunarr/types/plex';
import type { ContentProgramOriginalProgram } from '@tunarr/types/schemas';
import dayjs from 'dayjs';
import { find, first, isError } from 'lodash-es';
import { find, isError } from 'lodash-es';
import { P, match } from 'ts-pattern';
import { v4 } from 'uuid';
import type { NewProgramDao as NewRawProgram } from '../schema/Program.ts';
import type {
NewProgramDao,
NewProgramDao as NewRawProgram,
} from '../schema/Program.ts';
import { ProgramType } from '../schema/Program.ts';
/**
@@ -51,7 +54,7 @@ class ProgramDaoMinter {
mint(
serverName: string,
program: ContentProgramOriginalProgram,
): NewRawProgram {
): NewProgramDao {
const ret = match(program)
.with(
{ sourceType: 'plex', program: { type: 'movie' } },
@@ -92,18 +95,17 @@ class ProgramDaoMinter {
private mintProgramForPlexMovie(
serverName: string,
plexMovie: PlexMovie,
): NewRawProgram {
const file = first(first(plexMovie.Media)?.Part ?? []);
): NewProgramDao {
return {
uuid: v4(),
sourceType: ProgramSourceType.PLEX,
originalAirDate: plexMovie.originallyAvailableAt ?? null,
duration: plexMovie.duration ?? 0,
filePath: file?.file ?? null,
// filePath: file?.file ?? null,
externalSourceId: serverName,
externalKey: plexMovie.ratingKey,
plexRatingKey: plexMovie.ratingKey,
plexFilePath: file?.key ?? null,
// plexRatingKey: plexMovie.ratingKey,
// plexFilePath: file?.key ?? null,
rating: plexMovie.contentRating ?? null,
summary: plexMovie.summary ?? null,
title: plexMovie.title,
@@ -119,7 +121,7 @@ class ProgramDaoMinter {
item: Omit<JellyfinItem, 'Type'> & {
Type: 'Movie' | 'Episode' | 'Audio' | 'Video' | 'MusicVideo' | 'Trailer';
},
): NewRawProgram {
): NewProgramDao {
return {
uuid: v4(),
createdAt: +dayjs(),
@@ -155,8 +157,7 @@ class ProgramDaoMinter {
private mintProgramForPlexEpisode(
serverName: string,
plexEpisode: PlexEpisode,
): NewRawProgram {
const file = first(first(plexEpisode.Media)?.Part ?? []);
): NewProgramDao {
return {
uuid: v4(),
createdAt: +dayjs(),
@@ -164,11 +165,11 @@ class ProgramDaoMinter {
sourceType: ProgramSourceType.PLEX,
originalAirDate: plexEpisode.originallyAvailableAt,
duration: plexEpisode.duration ?? 0,
filePath: file?.file,
// filePath: file?.file,
externalSourceId: serverName,
externalKey: plexEpisode.ratingKey,
plexRatingKey: plexEpisode.ratingKey,
plexFilePath: file?.key,
// plexRatingKey: plexEpisode.ratingKey,
// plexFilePath: file?.key,
rating: plexEpisode.contentRating,
summary: plexEpisode.summary,
title: plexEpisode.title,
@@ -186,19 +187,18 @@ class ProgramDaoMinter {
private mintProgramForPlexTrack(
serverName: string,
plexTrack: PlexMusicTrack,
): NewRawProgram {
const file = first(first(plexTrack.Media)?.Part ?? []);
): NewProgramDao {
return {
uuid: v4(),
createdAt: +dayjs(),
updatedAt: +dayjs(),
sourceType: ProgramSourceType.PLEX,
duration: plexTrack.duration ?? 0,
filePath: file?.file,
// filePath: file?.file,
externalSourceId: serverName,
externalKey: plexTrack.ratingKey,
plexRatingKey: plexTrack.ratingKey,
plexFilePath: file?.key,
// plexRatingKey: plexTrack.ratingKey,
// plexFilePath: file?.key,
summary: plexTrack.summary,
title: plexTrack.title,
type: ProgramType.Track,

View File

@@ -79,9 +79,11 @@ const BaseContentBackedStreamLineupItemSchema =
// ID in the program DB table
programId: z.string().uuid(),
// These are taken from the Program DB entity
plexFilePath: z.string().optional(),
externalSourceId: z.string(),
filePath: z.string().optional(),
// Path to fetch the raw stream from the server
serverPath: z.string().optional(),
// The file path of the underlying media as seen from the media server
serverFilePath: z.string().optional(),
externalKey: z.string(),
programType: ProgramTypeEnum,
externalSource: z.nativeEnum(MediaSourceType),

View File

@@ -22,12 +22,15 @@ export interface ProgramTable extends WithCreatedAt, WithUpdatedAt, WithUuid {
episodeIcon: string | null;
externalKey: string;
externalSourceId: string;
// Deprecated, use program_external_id.direct_file_path
filePath: string | null;
grandparentExternalKey: string | null;
icon: string | null;
originalAirDate: string | null;
parentExternalKey: string | null;
// Deprecated, use program_external_id.external_file_path
plexFilePath: string | null;
// Deprecated, use external_key
plexRatingKey: string | null;
rating: string | null;
seasonIcon: string | null;

View File

@@ -0,0 +1 @@
export abstract class MediaSourceScanner {}

View File

@@ -4,7 +4,10 @@ import { ProgramDB } from '@/db/ProgramDB.js';
import { ProgramExternalIdType } from '@/db/custom_types/ProgramExternalIdType.js';
import { Channel } from '@/db/schema/Channel.js';
import { MediaSourceType } from '@/db/schema/MediaSource.js';
import type { ProgramWithRelations as RawProgramEntity } from '@/db/schema/derivedTypes.js';
import type {
ProgramWithExternalIds,
ProgramWithRelations as RawProgramEntity,
} from '@/db/schema/derivedTypes.js';
import { FillerPicker } from '@/services/FillerPicker.js';
import { KEYS } from '@/types/inject.js';
import { Result } from '@/types/result.js';
@@ -12,6 +15,7 @@ import { Maybe, Nullable } from '@/types/util.js';
import { binarySearchRange } from '@/util/binarySearch.js';
import { type Logger } from '@/util/logging/LoggerFactory.js';
import constants from '@tunarr/shared/constants';
import { nullToUndefined } from '@tunarr/shared/util';
import dayjs from 'dayjs';
import { inject, injectable } from 'inversify';
import { first, isEmpty, isNil, isNull, isUndefined, nth } from 'lodash-es';
@@ -22,16 +26,14 @@ import {
isOfflineItem,
} from '../db/derived_types/Lineup.ts';
import {
CommercialStreamLineupItem,
EnrichedLineupItem,
ProgramStreamLineupItem,
RedirectStreamLineupItem,
StreamLineupItem,
createOfflineStreamLineupItem,
} from '../db/derived_types/StreamLineup.ts';
import {
isNonEmptyString,
nullToUndefined,
zipWithIndex,
} from '../util/index.js';
import { isNonEmptyString, zipWithIndex } from '../util/index.js';
import { ChannelCache } from './ChannelCache.js';
import { wereThereTooManyAttempts } from './StreamThrottler.js';
@@ -402,9 +404,9 @@ export class StreamProgramCalculator {
externalInfo.sourceType === ProgramExternalIdType.JELLYFIN
? MediaSourceType.Jellyfin
: MediaSourceType.Plex,
plexFilePath: nullToUndefined(externalInfo.externalFilePath),
externalKey: externalInfo.externalKey,
filePath: nullToUndefined(externalInfo.directFilePath),
serverPath: nullToUndefined(externalInfo.externalFilePath),
serverFilePath: nullToUndefined(externalInfo.directFilePath),
externalSourceId: externalInfo.externalSourceId,
duration: backingItem.duration,
programId: backingItem.uuid,
@@ -412,7 +414,7 @@ export class StreamProgramCalculator {
id: backingItem.uuid,
programType: backingItem.type,
programBeginMs: timestamp - timeElapsed,
};
} satisfies ProgramStreamLineupItem;
}
}
} else if (isOfflineItem(lineupItem)) {
@@ -527,7 +529,6 @@ export class StreamProgramCalculator {
// just add the video, starting at 0, playing the entire duration
type: 'commercial',
title: filler.title,
filePath: nullToUndefined(externalInfo.directFilePath),
externalKey: externalInfo.externalKey,
externalSource:
externalInfo.sourceType === ProgramExternalIdType.JELLYFIN
@@ -542,10 +543,11 @@ export class StreamProgramCalculator {
programId: filler.uuid,
beginningOffset: beginningOffset,
externalSourceId: externalInfo.externalSourceId!,
plexFilePath: nullToUndefined(externalInfo.externalFilePath),
serverFilePath: nullToUndefined(externalInfo.directFilePath),
serverPath: nullToUndefined(externalInfo.externalFilePath),
programType: filler.type,
programBeginMs: activeProgram.programBeginMs,
};
} satisfies CommercialStreamLineupItem;
}
}
@@ -572,6 +574,26 @@ export class StreamProgramCalculator {
streamDuration: activeProgram.duration - timeElapsed,
beginningOffset: timeElapsed,
id: activeProgram.id,
} satisfies ProgramStreamLineupItem;
}
createStreamItemFromProgram(
program: ProgramWithExternalIds,
): ProgramStreamLineupItem {
return {
...program,
type: 'program',
programType: program.type,
programId: program.uuid,
id: program.uuid,
// HACK
externalSource: program.sourceType,
serverPath: nullToUndefined(
find(program.externalIds, { sourceType: 'plex' })?.externalFilePath,
),
serverFilePath: nullToUndefined(
find(program.externalIds, { sourceType: 'plex' })?.directFilePath,
),
};
}
}

View File

@@ -49,7 +49,7 @@ import {
// TODO: See if we need separate types for JF and Plex and what is really necessary here
type JellyfinItemStreamDetailsQuery = Pick<
ContentBackedStreamLineupItem,
'programType' | 'externalKey' | 'plexFilePath' | 'filePath' | 'programId'
'programType' | 'externalKey' | 'serverPath' | 'serverFilePath' | 'programId'
>;
@injectable()
@@ -166,7 +166,7 @@ export class JellyfinStreamDetails {
path: filePath,
};
} else {
const path = details.serverPath ?? item.plexFilePath;
const path = details.serverPath ?? item.serverPath;
if (isNonEmptyString(path)) {
streamSource = new HttpStreamSource(
`${trimEnd(server.uri, '/')}/Videos/${trimStart(

View File

@@ -50,7 +50,7 @@ import {
// The minimum fields we need to get stream details about an item
type PlexItemStreamDetailsQuery = Pick<
ContentBackedStreamLineupItem,
'programType' | 'externalKey' | 'plexFilePath' | 'filePath' | 'programId'
'programType' | 'externalKey' | 'programId' | 'serverPath' | 'serverFilePath'
>;
/**
@@ -179,7 +179,7 @@ export class PlexStreamDetails {
if (
isNonEmptyString(details.serverPath) &&
details.serverPath !== item.plexFilePath
details.serverPath !== item.serverPath
) {
this.programDB
.updateProgramPlexRatingKey(item.programId, server.name, {
@@ -229,7 +229,7 @@ export class PlexStreamDetails {
path: filePath,
};
} else {
let path = details.serverPath ?? item.plexFilePath;
let path = details.serverPath ?? item.serverPath;
this.logger.debug(
'Did not find Plex file on disk relative to Tunarr. Using network path: %s',
path,
@@ -299,8 +299,8 @@ export class PlexStreamDetails {
videoStream.scanType === 'interlaced'
? 'interlaced'
: videoStream.scanType === 'progressive'
? 'progressive'
: 'unknown',
? 'progressive'
: 'unknown',
width: videoStream.width,
height: videoStream.height,
framerate: videoStream.frameRate,

View File

@@ -19,6 +19,7 @@ import {
first,
forEach,
groupBy,
isEmpty,
isNil,
isNull,
isUndefined,
@@ -67,6 +68,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
.$if(!isNull(lastId), (eb) => eb.where('uuid', '>', lastId!))
.where('seasonNumber', 'is', null)
.where('type', '=', ProgramType.Episode)
.where('sourceType', '=', ProgramSourceType.PLEX)
.orderBy('uuid asc')
.limit(100)
.execute();
@@ -95,7 +97,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
if (parentId === 'unset') {
for (const program of programs) {
if (!program.plexRatingKey) {
if (isEmpty(program.externalKey)) {
this.logger.debug(
`Uh-oh, we're missing a plex rating key for %s`,
program.uuid,
@@ -104,7 +106,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
}
const seasonNum = await this.findSeasonNumberUsingEpisode(
program.plexRatingKey,
program.externalKey,
plexByName[server],
);
@@ -130,7 +132,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
});
} else {
for (const program of programs) {
if (!program.plexRatingKey) {
if (isEmpty(program.externalKey)) {
this.logger.warn(
`Uh-oh, we're missing a plex rating key for %s`,
program.uuid,
@@ -139,7 +141,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
}
const seasonNum = await this.findSeasonNumberUsingEpisode(
program.plexRatingKey,
program.externalKey,
plexByName[server],
);