Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use stable ID when reporting Plex playback session status #993

Merged
merged 1 commit into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions server/src/api/mediaSourceApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { MediaSourceType } from '@/db/schema/MediaSource.ts';
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js';
import { JellyfinApiClient } from '@/external/jellyfin/JellyfinApiClient.js';
import { PlexApiClient } from '@/external/plex/PlexApiClient.js';
import { GlobalScheduler } from '@/services/Scheduler.ts';
import { UpdateXmlTvTask } from '@/tasks/UpdateXmlTvTask.js';
import { RouterPluginAsyncCallback } from '@/types/serverType.js';
Expand Down Expand Up @@ -91,14 +90,16 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (

const healthyPromise = match(server)
.with({ type: 'plex' }, (server) => {
return new PlexApiClient(server).checkServerStatus();
return MediaSourceApiFactory().get(server).checkServerStatus();
})
.with({ type: 'jellyfin' }, (server) => {
return new JellyfinApiClient({
url: server.uri,
apiKey: server.accessToken,
name: server.name,
})
.with({ type: 'jellyfin' }, async (server) => {
return (
await MediaSourceApiFactory().getJellyfinClient({
url: server.uri,
apiKey: server.accessToken,
name: server.name,
})
)
.getSystemInfo()
.then(() => true)
.catch(() => false);
Expand Down Expand Up @@ -148,7 +149,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
let healthyPromise: Promise<boolean>;
switch (req.body.type) {
case 'plex': {
const plex = new PlexApiClient({
const plex = MediaSourceApiFactory().get({
...req.body,
name: req.body.name ?? 'unknown',
clientIdentifier: null,
Expand Down
12 changes: 6 additions & 6 deletions server/src/external/MediaSourceApiFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChannelDB } from '@/db/ChannelDB.ts';
import { SettingsDB, getSettings } from '@/db/SettingsDB.ts';
import { MediaSourceDB } from '@/db/mediaSourceDB.ts';
import { MediaSourceType } from '@/db/schema/MediaSource.ts';
import { registerSingletonInitializer } from '@/globals.ts';
import { Maybe } from '@/types/util.js';
import { isDefined } from '@/util/index.js';
import { LoggerFactory } from '@/util/logging/LoggerFactory.js';
Expand Down Expand Up @@ -165,11 +165,11 @@ export class MediaSourceApiFactoryImpl {
}
}

export const MediaSourceApiFactory = (
mediaSourceDB: MediaSourceDB = new MediaSourceDB(new ChannelDB()),
) => {
registerSingletonInitializer((ctx) => {
if (!instance) {
instance = new MediaSourceApiFactoryImpl(mediaSourceDB);
instance = new MediaSourceApiFactoryImpl(ctx.mediaSourceDB);
}
return instance;
};
});

export const MediaSourceApiFactory = () => instance;
9 changes: 9 additions & 0 deletions server/src/external/plex/PlexApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Maybe, Nilable } from '@/types/util.js';
import { getChannelId } from '@/util/channels.js';
import { isSuccess } from '@/util/index.js';
import { getTunarrVersion } from '@/util/version.ts';
import { PlexClientIdentifier } from '@tunarr/shared/constants';
import {
PlexDvr,
PlexDvrsResponse,
Expand Down Expand Up @@ -50,6 +52,11 @@ export type PlexApiOptions = {

const PlexCache = new PlexQueryCache();

const PlexHeaders = {
'X-Plex-Product': 'Tunarr',
'X-Plex-Client-Identifier': PlexClientIdentifier,
};

export class PlexApiClient extends BaseApiClient {
private opts: PlexApiOptions;
private accessToken: string;
Expand All @@ -59,6 +66,8 @@ export class PlexApiClient extends BaseApiClient {
url: opts.uri,
name: opts.name,
extraHeaders: {
...PlexHeaders,
'X-Plex-Version': getTunarrVersion(),
'X-Plex-Token': opts.accessToken,
},
});
Expand Down
11 changes: 6 additions & 5 deletions server/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import once from 'lodash-es/once.js';
import path, { resolve } from 'node:path';
import { ServerArgsType } from './cli/RunServerCommand.ts';
import { GlobalArgsType } from './cli/types.ts';
import { ServerContext } from './serverContext.ts';
import { LogLevels } from './util/logging/LoggerFactory.ts';

export type GlobalOptions = GlobalArgsType & {
Expand Down Expand Up @@ -81,11 +82,11 @@ export const dbOptions = () => {
};
};

type Initializer = () => unknown;
type Initializer<T> = (ctx: ServerContext) => T;
let initalized = false;
const initializers: Initializer[] = [];
const initializers: Initializer<unknown>[] = [];

export const registerSingletonInitializer = <T>(f: () => T) => {
export const registerSingletonInitializer = <T>(f: Initializer<T>) => {
if (initalized) {
throw new Error(
'Attempted to register singleton after intialization. This singleton will never be initialized!!',
Expand All @@ -95,7 +96,7 @@ export const registerSingletonInitializer = <T>(f: () => T) => {
initializers.push(f);
};

export const initializeSingletons = once(() => {
forEach(initializers, (f) => f());
export const initializeSingletons = once((ctx: ServerContext) => {
forEach(initializers, (f) => f(ctx));
initalized = true;
});
3 changes: 1 addition & 2 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ export async function initServer(opts: ServerOptions) {

logger.info('Using Tunarr database directory: %s', opts.databaseDirectory);

initializeSingletons();

const ctx = serverContext();
initializeSingletons(ctx);
registerHealthChecks(ctx);
await ctx.m3uService.clearCache();
await new ChannelLineupMigrator(ctx.channelDB).run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProgramDB } from '@/db/ProgramDB.ts';
import { PendingProgram } from '@/db/derived_types/Lineup.ts';
import { MediaSourceDB } from '@/db/mediaSourceDB.ts';
import { Channel } from '@/db/schema/Channel.ts';
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts';
import { PlexApiClient } from '@/external/plex/PlexApiClient.js';
import { Logger, LoggerFactory } from '@/util/logging/LoggerFactory.js';
import { Timer } from '@/util/perf.js';
Expand Down Expand Up @@ -44,7 +45,7 @@ export class PlexContentSourceUpdater extends ContentSourceUpdater<DynamicConten
throw new Error('media source not found');
}

this.#plex = new PlexApiClient(server);
this.#plex = MediaSourceApiFactory().get(server);
}

protected async run() {
Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/UpdateXmlTvTask.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChannelDB } from '@/db/ChannelDB.ts';
import { SettingsDB, defaultXmlTvSettings } from '@/db/SettingsDB.ts';
import { MediaSourceDB } from '@/db/mediaSourceDB.ts';
import { PlexApiClient } from '@/external/plex/PlexApiClient.js';
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts';
import { globalOptions } from '@/globals.js';
import { ServerContext } from '@/serverContext.js';
import { TVGuideService } from '@/services/TvGuideService.ts';
Expand Down Expand Up @@ -92,7 +92,7 @@ export class UpdateXmlTvTask extends Task<void> {
const allMediaSources = await this.mediaSourceDB.findByType('plex');

await mapAsyncSeq(allMediaSources, async (plexServer) => {
const plex = new PlexApiClient(plexServer);
const plex = MediaSourceApiFactory().get(plexServer);
let dvrs: PlexDvr[] = [];

if (!plexServer.sendGuideUpdates && !plexServer.sendChannelUpdates) {
Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/fixers/addPlexServerIds.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getDatabase } from '@/db/DBAccess.ts';
import { MediaSourceType } from '@/db/schema/MediaSource.ts';
import { PlexApiClient } from '@/external/plex/PlexApiClient.js';
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts';
import { find, isNil } from 'lodash-es';
import Fixer from './fixer.js';

Expand All @@ -13,7 +13,7 @@ export class AddPlexServerIdsFixer extends Fixer {
.where('type', '=', MediaSourceType.Plex)
.execute();
for (const server of plexServers) {
const api = new PlexApiClient(server);
const api = MediaSourceApiFactory().get(server);
const devices = await api.getDevices();
if (!isNil(devices) && devices.MediaContainer.Device) {
const matchingServer = find(
Expand Down
6 changes: 2 additions & 4 deletions server/src/tasks/fixers/missingSeasonNumbersFixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,8 @@ export class MissingSeasonNumbersFixer extends Fixer {
return;
}

const plexByName = groupByUniqPropAndMap(
allPlexServers,
'name',
(server) => new PlexApiClient(server),
const plexByName = groupByUniqPropAndMap(allPlexServers, 'name', (server) =>
MediaSourceApiFactory().get(server),
);

const updatedPrograms: RawProgram[] = [];
Expand Down
14 changes: 10 additions & 4 deletions server/src/tasks/plex/UpdatePlexPlayStatusTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { GlobalScheduler } from '@/services/Scheduler.ts';
import { ScheduledTask } from '@/tasks/ScheduledTask.ts';
import { Task } from '@/tasks/Task.ts';
import { run } from '@/util/index.ts';
import { getTunarrVersion } from '@/util/version.ts';
import { PlexClientIdentifier } from '@tunarr/shared/constants';
import dayjs from 'dayjs';
import { RecurrenceRule } from 'node-schedule';
import { v4 } from 'uuid';
Expand All @@ -13,6 +15,7 @@ type UpdatePlexPlayStatusScheduleRequest = {
startTime: number;
duration: number;
channelNumber: number;
updateIntervalSeconds?: number;
};

type UpdatePlexPlayStatusInvocation = UpdatePlexPlayStatusScheduleRequest & {
Expand Down Expand Up @@ -41,7 +44,7 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask {
UpdatePlexPlayStatusScheduledTask.name,
run(() => {
const rule = new RecurrenceRule();
rule.second = 30;
rule.second = request.updateIntervalSeconds ?? 10;
return rule;
}),
() => this.getNextTask(),
Expand All @@ -64,7 +67,7 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask {
this.playState = 'stopped';
GlobalScheduler.scheduleOneOffTask(
UpdatePlexPlayStatusTask.name,
dayjs().add(30, 'seconds').toDate(),
dayjs().add(5, 'seconds').toDate(),
this.getNextTask(),
);
}
Expand All @@ -79,7 +82,8 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask {
this.request = {
...this.request,
startTime: Math.min(
this.request.startTime + 30000,
this.request.startTime +
(this.request.updateIntervalSeconds ?? 10) * 1000,
this.request.duration,
),
};
Expand Down Expand Up @@ -113,9 +117,11 @@ class UpdatePlexPlayStatusTask extends Task {
key: `/library/metadata/${this.request.ratingKey}`,
time: this.request.startTime,
duration: this.request.duration,
'X-Plex-Product': 'Tunarr',
'X-Plex-Version': getTunarrVersion(),
'X-Plex-Device-Name': deviceName,
'X-Plex-Device': deviceName,
'X-Plex-Client-Identifier': this.request.sessionId,
'X-Plex-Client-Identifier': PlexClientIdentifier,
};

try {
Expand Down
4 changes: 2 additions & 2 deletions shared/src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const constants = {
DEFAULT_DATA_DIR: '.tunarr',
};

const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';
export const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';

export const DefaultPlexHeaders = {
Accept: 'application/json',
Expand All @@ -17,7 +17,7 @@ export const DefaultPlexHeaders = {
'X-Plex-Version': '0.1',
'X-Plex-Client-Identifier': PlexClientIdentifier,
'X-Plex-Platform': 'Chrome',
'X-Plex-Platform-Version': '80.0',
'X-Plex-Platform-Version': '130.0',
};

export default constants;
5 changes: 4 additions & 1 deletion web/src/helpers/plexLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { ApiClient } from '../external/api.ts';
import { AsyncInterval } from './AsyncInterval.ts';
import { sequentialPromises } from './util.ts';

// From Plex: The Client Identifier identifies the specific instance of your app. A random string or UUID is sufficient here. There are no hard requirements for Client Identifier length or format, but once one is generated the client should store and re-use this identifier for subsequent requests.
// From Plex: The Client Identifier identifies the specific instance of your app.
// A random string or UUID is sufficient here. There are no hard requirements for
// Client Identifier length or format, but once one is generated the client should store
// and re-use this identifier for subsequent requests.
const ClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';

const PlexLoginHeaders = {
Expand Down
Loading