checkpoint -- filler

This commit is contained in:
Christian Benincasa
2025-05-21 21:20:10 -04:00
parent 1f54a8935a
commit 89daf23296
9 changed files with 52 additions and 55 deletions

View File

@@ -36,14 +36,10 @@ import { apiRouter } from './api/index.js';
import { streamApi } from './api/streamApi.js';
import { videoApiRouter } from './api/videoApi.js';
import { FfmpegInfo } from './ffmpeg/ffmpegInfo.js';
import {
type ServerOptions,
initializeSingletons,
serverOptions,
} from './globals.js';
import { type ServerOptions, serverOptions } from './globals.js';
import { ServerContext, ServerRequestContext } from './ServerContext.js';
import { GlobalScheduler, scheduleJobs } from './services/Scheduler.ts';
import { initPersistentStreamCache } from './stream/ChannelCache.js';
import { initPersistentStreamCache } from './stream/LastPlayTimeCache.ts';
import { UpdateXmlTvTask } from './tasks/UpdateXmlTvTask.js';
import { TUNARR_ENV_VARS } from './util/env.ts';
import { fileExists } from './util/fsUtil.js';
@@ -82,8 +78,6 @@ export class Server {
this.serverOptions.databaseDirectory,
);
// TODO: Use injector
initializeSingletons(this.serverContext);
await this.serverContext.m3uService.clearCache();
await this.serverContext.channelLineupMigrator.run();

View File

@@ -24,7 +24,7 @@ import { M3uService } from './services/M3UService.ts';
import { OnDemandChannelService } from './services/OnDemandChannelService.js';
import { TVGuideService } from './services/TvGuideService.ts';
import { CacheImageService } from './services/cacheImageService.js';
import { ChannelCache } from './stream/ChannelCache.js';
import { LastPlayTimeCache } from './stream/LastPlayTimeCache.ts';
import { SessionManager } from './stream/SessionManager.js';
import { StreamProgramCalculator } from './stream/StreamProgramCalculator.js';
@@ -44,7 +44,7 @@ export class ServerContext {
@inject(TVGuideService) public guideService: TVGuideService;
@inject(HdhrService) public hdhrService: HdhrService;
@inject(CustomShowDB) public customShowDB: CustomShowDB;
@inject(ChannelCache) public channelCache: ChannelCache;
@inject(LastPlayTimeCache) public channelCache: LastPlayTimeCache;
@inject(MediaSourceDB) public mediaSourceDB: MediaSourceDB;
@inject(KEYS.ProgramDB) public programDB: IProgramDB;
@inject(TranscodeConfigDB) public transcodeConfigDB: TranscodeConfigDB;

View File

@@ -1,5 +1,5 @@
import type { IProgramDB } from '@/db/interfaces/IProgramDB.js';
import { ChannelCache } from '@/stream/ChannelCache.js';
import { LastPlayTimeCache } from '@/stream/LastPlayTimeCache.ts';
import { KEYS } from '@/types/inject.js';
import { isNonEmptyString } from '@/util/index.js';
import {
@@ -43,7 +43,7 @@ import type { ChannelFillerShowWithContent } from './schema/derivedTypes.ts';
@injectable()
export class FillerDB {
constructor(
@inject(ChannelCache) private channelCache: ChannelCache,
@inject(LastPlayTimeCache) private channelCache: LastPlayTimeCache,
@inject(KEYS.ProgramDB) private programDB: IProgramDB,
@inject(ProgramConverter) private programConverter: ProgramConverter,
@inject(KEYS.Database) private db: Kysely<DB>,

View File

@@ -56,8 +56,8 @@ export class SchemaBackedDbAdapter<T extends z.ZodTypeAny, Out = z.infer<T>>
}
this.logger.error(
`Error while parsing schema-backed JSON file ${this.path.toString()}. Returning null. This could mean the DB got corrupted somehow`,
parseResult.error,
`Error while parsing schema-backed JSON file ${this.path.toString()}. Returning null. This could mean the DB got corrupted somehow`,
);
return null;
}

View File

@@ -1,10 +1,9 @@
import { findKey, forEach, merge } from 'lodash-es';
import { findKey, merge } from 'lodash-es';
import isUndefined from 'lodash-es/isUndefined.js';
import once from 'lodash-es/once.js';
import path, { resolve } from 'node:path';
import type { ServerArgsType } from './cli/RunServerCommand.ts';
import type { GlobalArgsType } from './cli/types.ts';
import type { ServerContext } from './ServerContext.ts';
import type { LogLevels } from './util/logging/LoggerFactory.ts';
export type GlobalOptions = GlobalArgsType & {
@@ -81,22 +80,3 @@ export const dbOptions = () => {
dbName: path.join(_globalOptions.databaseDirectory, 'db.db'),
};
};
type Initializer<T> = (ctx: ServerContext) => T;
let initalized = false;
const initializers: Initializer<unknown>[] = [];
export const registerSingletonInitializer = <T>(f: Initializer<T>) => {
if (initalized) {
throw new Error(
'Attempted to register singleton after intialization. This singleton will never be initialized!!',
);
}
initializers.push(f);
};
export const initializeSingletons = once((ctx: ServerContext) => {
forEach(initializers, (f) => f(ctx));
initalized = true;
});

View File

@@ -1,5 +1,5 @@
import type { Channel } from '@/db/schema/Channel.js';
import { ChannelCache } from '@/stream/ChannelCache.js';
import { LastPlayTimeCache } from '@/stream/LastPlayTimeCache.ts';
import type { Maybe } from '@/types/util.js';
import { random } from '@/util/random.js';
import constants from '@tunarr/shared/constants';
@@ -21,9 +21,9 @@ const FiveMinutesMillis = 5 * 60 * 60 * 1000;
@injectable()
export class BestFitFillerPicker implements IFillerPicker {
#channelCache: ChannelCache;
#channelCache: LastPlayTimeCache;
constructor(channelCache: ChannelCache) {
constructor(channelCache: LastPlayTimeCache) {
this.#channelCache = channelCache;
}

View File

@@ -1,8 +1,10 @@
import type { Channel } from '@/db/schema/Channel.js';
import { ChannelCache } from '@/stream/ChannelCache.js';
import { LastPlayTimeCache } from '@/stream/LastPlayTimeCache.ts';
import type { Maybe } from '@/types/util.js';
import { random } from '@/util/random.js';
import constants from '@tunarr/shared/constants';
import dayjs from 'dayjs';
import { injectable } from 'inversify';
import { isEmpty, isNil } from 'lodash-es';
import type {
ChannelFillerShowWithContent,
@@ -11,14 +13,15 @@ import type {
import type { IFillerPicker } from './interfaces/IFillerPicker.ts';
import { EmptyFillerPickResult } from './interfaces/IFillerPicker.ts';
const DefaultFillerCooldownMillis = 30 * 60 * 1000;
const OneDayMillis = 7 * 24 * 60 * 60 * 1000;
const FiveMinutesMillis = 5 * 60 * 60 * 1000;
const DefaultFillerCooldownMillis = +dayjs.duration({ seconds: 30 });
const OneWeekMillis = +dayjs.duration({ weeks: 1 });
const FiveMinutesMillis = +dayjs.duration({ minutes: 5 });
@injectable()
export class FillerPicker implements IFillerPicker {
#channelCache: ChannelCache;
#channelCache: LastPlayTimeCache;
constructor(channelCache: ChannelCache = new ChannelCache()) {
constructor(channelCache: LastPlayTimeCache) {
this.#channelCache = channelCache;
}
@@ -42,7 +45,7 @@ export class FillerPicker implements IFillerPicker {
let fillerListId: Maybe<string>;
for (const filler of fillers) {
const fillerPrograms = filler.fillerContent;
let pickedList = false;
let pickedList = fillers.length > 1; // Always pick the first list if there's only 1
let n = 0;
for (const clip of fillerPrograms) {
@@ -52,7 +55,7 @@ export class FillerPicker implements IFillerPicker {
channel.uuid,
clip.uuid,
);
let timeSince = t1 == 0 ? OneDayMillis : t0 - t1;
let timeSince = t1 == 0 ? OneWeekMillis : t0 - t1;
if (timeSince < fillerRepeatCooldownMs - constants.SLACK) {
const w = fillerRepeatCooldownMs - timeSince;
@@ -66,7 +69,7 @@ export class FillerPicker implements IFillerPicker {
channel.uuid,
filler.fillerShow.uuid,
);
const timeSince = t1 == 0 ? OneDayMillis : t0 - t1;
const timeSince = t1 == 0 ? OneWeekMillis : t0 - t1;
if (timeSince + constants.SLACK >= filler.cooldown) {
//should we pick this list?
listM += filler.weight;

View File

@@ -28,15 +28,15 @@ const channelCacheSchema = z.object({
programPlayTimeCache: z.record(z.number()).default({}),
});
type ChannelCacheSchema = z.infer<typeof channelCacheSchema>;
type LastPlayCacheSchema = z.infer<typeof channelCacheSchema>;
class PersistentChannelCache {
class PersistentLastPlayTimeCache {
#initialized: boolean = false;
#db: Low<ChannelCacheSchema>;
#db: Low<LastPlayCacheSchema>;
async init() {
if (!this.#initialized) {
this.#db = new Low<ChannelCacheSchema>(
this.#db = new Low<LastPlayCacheSchema>(
new InMemoryCachedDbAdapter(
new SchemaBackedDbAdapter(
channelCacheSchema,
@@ -90,12 +90,31 @@ class PersistentChannelCache {
}
}
const persistentChannelCache = new PersistentChannelCache();
const persistentChannelCache = new PersistentLastPlayTimeCache();
export const initPersistentStreamCache = () => persistentChannelCache.init();
interface ILastPlayTimeCache {
getCurrentLineupItem(
channelId: string,
timeNow: number,
): StreamLineupItem | undefined;
getProgramLastPlayTime(channelId: string, programId: string): number;
getFillerLastPlayTime(channelId: string, fillerId: string): number;
recordPlayback(
channelId: string,
t0: number,
lineupItem: StreamLineupItem,
): Promise<void>;
clearPlayback(channelId: string): Promise<void>;
}
@injectable()
export class ChannelCache {
export class LastPlayTimeCache implements ILastPlayTimeCache {
getCurrentLineupItem(
channelId: string,
timeNow: number,

View File

@@ -30,7 +30,7 @@ import {
nullToUndefined,
zipWithIndex,
} from '../util/index.js';
import { ChannelCache } from './ChannelCache.js';
import { LastPlayTimeCache } from './LastPlayTimeCache.ts';
import { wereThereTooManyAttempts } from './StreamThrottler.js';
const SLACK = constants.SLACK;
@@ -79,8 +79,9 @@ export class StreamProgramCalculator {
@inject(KEYS.Logger) private logger: Logger,
@inject(FillerDB) private fillerDB: FillerDB,
@inject(KEYS.ChannelDB) private channelDB: ChannelDB,
@inject(ChannelCache) private channelCache: ChannelCache,
@inject(LastPlayTimeCache) private channelCache: LastPlayTimeCache,
@inject(KEYS.ProgramDB) private programDB: ProgramDB,
@inject(FillerPicker) private fillerPicker: FillerPicker,
) {}
async getCurrentLineupItem(
@@ -507,7 +508,7 @@ export class StreamProgramCalculator {
}
// Pick a random filler, too
const randomResult = new FillerPicker().pickFiller(
const randomResult = this.fillerPicker.pickFiller(
channel,
fillerPrograms,
streamDuration,