mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
checkpoint
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
server/src/services/scanner/MediaSourceScanner.ts
Normal file
1
server/src/services/scanner/MediaSourceScanner.ts
Normal file
@@ -0,0 +1 @@
|
||||
export abstract class MediaSourceScanner {}
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user