mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
checkpoint -- filler
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user