diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index 90fc2984..1e491d87 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -4,25 +4,6 @@ import { CollectionType } from "@jellyfin/sdk/lib/generated-client/index.js"; -export interface JellyData extends CommonSourceData { - /** - * optional list of users to scrobble tracks from - * - * If none are provided tracks from all users will be scrobbled - * - * @examples [["MyUser1","MyUser2"]] - * */ - users?: string | string[] - /** - * optional list of servers to scrobble tracks from - * - * If none are provided tracks from all servers will be scrobbled - * - * @examples [["MyServerName1"]] - * */ - servers?: string | string[] -} - export interface JellyApiData extends CommonSourceData { /** * HOST:PORT of the Jellyfin server to connect to @@ -101,14 +82,6 @@ export interface JellyApiData extends CommonSourceData { export interface JellyApiOptions extends CommonSourceOptions { } -export interface JellySourceConfig extends CommonSourceConfig { - data: JellyData -} - -export interface JellySourceAIOConfig extends JellySourceConfig { - type: 'jellyfin' -} - export interface JellyApiSourceConfig extends CommonSourceConfig { data: JellyApiData options: JellyApiOptions @@ -119,4 +92,4 @@ export interface JellyApiSourceAIOConfig extends JellyApiSourceConfig { } -export type JellyfinCompatConfig = JellyApiSourceConfig | JellySourceConfig; \ No newline at end of file +export type JellyfinCompatConfig = JellyApiSourceConfig; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/sources.ts b/src/backend/common/infrastructure/config/source/sources.ts index 305c9389..61bc6736 100644 --- a/src/backend/common/infrastructure/config/source/sources.ts +++ b/src/backend/common/infrastructure/config/source/sources.ts @@ -3,7 +3,7 @@ import { ChromecastSourceAIOConfig, ChromecastSourceConfig } from "./chromecast. import { ListenbrainzEndpointSourceAIOConfig, ListenbrainzEndpointSourceConfig } from "./endpointlz.js"; import { LastFMEndpointSourceAIOConfig, LastFMEndpointSourceConfig } from "./endpointlfm.js"; import { DeezerInternalSourceConfig, DeezerInternalAIOConfig, DeezerSourceAIOConfig, DeezerSourceConfig } from "./deezer.js"; -import { JellyApiSourceAIOConfig, JellyApiSourceConfig, JellySourceAIOConfig, JellySourceConfig } from "./jellyfin.js"; +import { JellyApiSourceAIOConfig, JellyApiSourceConfig } from "./jellyfin.js"; import { JRiverSourceAIOConfig, JRiverSourceConfig } from "./jriver.js"; import { KodiSourceAIOConfig, KodiSourceConfig } from "./kodi.js"; import { LastFmSouceAIOConfig, LastfmSourceConfig } from "./lastfm.js"; @@ -37,7 +37,6 @@ export type SourceConfig = | ListenbrainzEndpointSourceConfig | LastFMEndpointSourceConfig | SubSonicSourceConfig - | JellySourceConfig | JellyApiSourceConfig | LastfmSourceConfig | YTMusicSourceConfig @@ -69,7 +68,6 @@ export type SourceAIOConfig = | ListenbrainzEndpointSourceAIOConfig | LastFMEndpointSourceAIOConfig | SubsonicSourceAIOConfig - | JellySourceAIOConfig | JellyApiSourceAIOConfig | LastFmSouceAIOConfig | YTMusicSourceAIOConfig diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index 0612c18e..e8defad7 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -24,7 +24,6 @@ import MemorySource from "../sources/MemorySource.js"; import { parseBool, sortByNewestPlayDate } from "../utils.js"; import { setupAuthRoutes } from "./auth.js"; import { setupDeezerRoutes } from "./deezerRoutes.js"; -import { setupJellyfinRoutes } from "./jellyfinRoutes.js"; import {setupLZEndpointRoutes} from "./endpointListenbrainzRoutes.js"; import {setupLastfmEndpointRoutes} from "./endpointLastfmRoutes.js"; import { makeClientCheckMiddle, makeSourceCheckMiddle } from "./middleware.js"; @@ -150,7 +149,6 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream: setupTautulliRoutes(app, logger, scrobbleSources); setupPlexRoutes(app, logger, scrobbleSources); - setupJellyfinRoutes(app, logger, scrobbleSources); setupDeezerRoutes(app, logger, scrobbleSources); setupWebscrobblerRoutes(app, logger, scrobbleSources); setupLZEndpointRoutes(app, logger, scrobbleSources); diff --git a/src/backend/server/jellyfinRoutes.ts b/src/backend/server/jellyfinRoutes.ts deleted file mode 100644 index 4a7529e0..00000000 --- a/src/backend/server/jellyfinRoutes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ExpressWithAsync } from "@awaitjs/express"; -import { Logger } from "@foxxmd/logging"; -import bodyParser from "body-parser"; -import { JellyfinNotifier } from "../sources/ingressNotifiers/JellyfinNotifier.js"; -import JellyfinSource from "../sources/JellyfinSource.js"; -import ScrobbleSources from "../sources/ScrobbleSources.js"; -import { isDebugMode, parseBool, remoteHostIdentifiers } from "../utils.js"; - -export const setupJellyfinRoutes = (app: ExpressWithAsync, logger: Logger, scrobbleSources: ScrobbleSources) => { - - // webhook plugin sends json with context type text/utf-8 so we need to parse it differently - const jellyfinJsonParser = bodyParser.json({ - type: ['text/*', 'application/json'], - // verify: function(req, res, buf, encoding) { - // // get rawBody - // // @ts-ignore - // req.rawBody = buf.toString(); - // } - }); - const jellyIngress = new JellyfinNotifier(logger); - app.postAsync('/jellyfin', async (req, res) => { - res.redirect(307, '/api/jellyfin/ingress'); - }); - app.postAsync('/api/jellyfin/ingress', - async (req, res, next) => { - // track request before parsing body to ensure we at least log that something is happening - // (in the event body parsing does not work or request is not POST/PATCH) - jellyIngress.trackIngress(req, true); - next(); - }, - jellyfinJsonParser, async (req, res) => { - jellyIngress.trackIngress(req, false); - - res.send('OK'); - - const bodyEmpty = req.body === undefined || req.body === null || (typeof req.body === 'object' && Object.keys(req.body).length === 0); - if(bodyEmpty) { - const length = req.header('content-length') !== undefined ? Number.parseInt(req.header('content-length')) : undefined; - // can't think of a way a user would send an empty body for a webhook payload but if they meant to do it don't spam them with errors... - if(length === 0) { - return; - } - if(length === undefined) { - logger.warn(`Jellyfin is not sending a well-formatted request. It does not have valid headers (application/json - text/*) OR it is missing content-length header: Content-Type => '${req.header('content-type')}' | Length => ${length}`); - } else { - logger.warn(`Jellyfin is not sending a request with valid headers. Content-Type must be either application/json or a text/* wildcard (like text/plain) -- given: Content-Type => '${req.header('content-type')}'`); - } - res.status(400).send('Invalid Content-Type. Must be either application/json or a text wildcard (like text/plain)'); - return; - } - - const parts = remoteHostIdentifiers(req); - const connectionId = `${parts.host}-${parts.proxy ?? ''}`; - - const playObj = JellyfinSource.formatPlayObj({...req.body, connectionId}, {newFromSource: true}); - const pSources = scrobbleSources.getByType('jellyfin') as JellyfinSource[]; - if(pSources.length === 0) { - logger.warn('Received Jellyfin connection but no Jellyfin sources are configured'); - } - const logPayload = pSources.some(x => { - const { - options: { - logPayload = isDebugMode() - } = {} - } = x.config; - return logPayload; - }); - if(logPayload) { - logger.debug(`[Jellyfin] Logging payload due to at least one Jellyfin source having 'logPayload: true`, req.body); - } - for (const source of pSources) { - await source.handle(playObj); - } - }); -} - diff --git a/src/backend/sources/JellyfinSource.ts b/src/backend/sources/JellyfinSource.ts deleted file mode 100644 index 21e5b03a..00000000 --- a/src/backend/sources/JellyfinSource.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { Logger } from "@foxxmd/logging"; -import dayjs from "dayjs"; -import EventEmitter from "events"; -import { PlayObject, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT } from "../../core/Atomic.js"; -import { - buildTrackString, - combinePartsToString, - splitByFirstFound, - truncateStringToLength -} from "../../core/StringUtils.js"; -import { FormatPlayObjectOptions, InternalConfig, PlayPlatformId } from "../common/infrastructure/Atomic.js"; -import { JellySourceConfig } from "../common/infrastructure/config/source/jellyfin.js"; -import { - doubleReturnNewline, - isDebugMode, - parseBool, - playObjDataMatch, -} from "../utils.js"; -import { parseDurationFromTimestamp } from '../utils/TimeUtils.js'; -import { - comparePlayTemporally, - hasAcceptableTemporalAccuracy, - temporalPlayComparisonSummary, -} from "../utils/TimeUtils.js"; -import MemorySource from "./MemorySource.js"; -import { PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js"; -import { JellyfinPlayerState } from "./PlayerState/JellyfinPlayerState.js"; - -const shortDeviceId = truncateStringToLength(10, ''); - -const TZ_OFFSET_PRESENT_THRESHOLD = 12; - -export default class JellyfinSource extends MemorySource { - users; - servers; - - multiPlatform: boolean = true; - - declare config: JellySourceConfig; - userDataWarn: boolean = false; - - constructor(name: any, config: JellySourceConfig, internal: InternalConfig, emitter: EventEmitter) { - super('jellyfin', name, config, internal, emitter); - const { - data: { - users, - servers, - } = {}, - options: { - logFilterFailure = (isDebugMode() ? 'debug' : 'warn') - } = {} - } = this.config; - - if(logFilterFailure !== false && !['debug', 'warn'].includes(logFilterFailure)) { - this.logger.warn(`logFilterFailure value of '${logFilterFailure.toString()}' is NOT VALID. Logging will not occur if filters fail. You should fix this.`); - } - - if (users === undefined || users === null) { - this.users = undefined; - } else { - if (!Array.isArray(users)) { - if(users.trim() === '') { - this.users = undefined; - } else { - this.users = users.split(',').map(x => x.trim()); - } - } else { - this.users = users; - } - if(this.users !== undefined) { - this.users = this.users.map((x: any) => x.toLocaleLowerCase()) - } - } - - if (servers === undefined || servers === null) { - this.servers = undefined; - } else { - if (!Array.isArray(servers)) { - if(servers.trim() === '') { - this.servers = undefined; - } else { - this.servers = servers.split(',').map(x => x.trim()); - } - } else { - this.servers = servers; - } - if(this.servers !== undefined) { - this.servers = this.servers.map((x: any) => x.toLocaleLowerCase()); - } - } - - if (users === undefined && servers === undefined) { - this.logger.warn('Initializing, but with no filters! All tracks from all users on all servers will be scrobbled.'); - } else { - this.logger.info(`Initializing with the following filters => Users: ${this.users === undefined ? 'N/A' : this.users.join(', ')} | Servers: ${this.servers === undefined ? 'N/A' : this.servers.join(', ')}`); - } - - this.logger.warn('Jellyfin source using Webhook plugin is DEPRECATED and will be removed in an upcoming release! Please switch to Jellyfin source using API integration as soon as possible.') - } - - static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject { - const {newFromSource = false} = options; - let nfs = newFromSource; - const { - ServerId, - ServerName, - ServerVersion, - NotificationUsername, - UserId, - NotificationType, - SaveReason, - Played, - UtcTimestamp, - LastPlayedDate, - Album, - Artist, - Name, - RunTime, - ItemId, - ItemType, - PlaybackPosition, - connectionId, - DeviceId = '', - DeviceName, - ClientName, - Provider_musicbrainzalbumartist, - Provider_musicbrainzartist, - Provider_musicbrainzalbum, - Provider_musicbrainztrack, - Provider_musicbrainzreleasegroup - } = obj; - - const dur = parseDurationFromTimestamp(RunTime); - - let server = ServerName; - if(server === undefined || server === '') { - server = ServerId; - } - if(server === undefined || server === '') { - server = connectionId; - } - - let artists = []; - if(Artist !== undefined) { - artists = [Artist]; - } - - let eventReason = SaveReason; - let playDate = dayjs(); - // JELLYFIN WEB -> LastPlayedDate = time when track stopped being played - if(NotificationType === 'UserDataSaved' && SaveReason === 'PlaybackFinished' && LastPlayedDate !== undefined) { - nfs = false; - - playDate = dayjs(LastPlayedDate); - if(Played !== true) { - eventReason = 'PlaybackFinished-NOTPLAYED' - } - } - - return { - data: { - artists, - album: Album, - track: Name, - duration: dur !== undefined ? dur.as('seconds') : undefined, - playDate, - meta: { - brainz: { - artist: splitByFirstFound(Provider_musicbrainzartist, [';'], undefined), - album: Provider_musicbrainzalbum, - albumArtist: [Provider_musicbrainzalbumartist], - track: Provider_musicbrainztrack, - releaseGroup: Provider_musicbrainzreleasegroup - } - } - }, - meta: { - event: NotificationType, - eventReason, - mediaType: ItemType, - trackId: ItemId, - user: NotificationUsername ?? UserId, - server, - source: 'Jellyfin', - newFromSource: nfs, - trackProgressPosition: PlaybackPosition !== undefined ? parseDurationFromTimestamp(PlaybackPosition).asSeconds() : undefined, - sourceVersion: ServerVersion, - deviceId: combinePartsToString([shortDeviceId(DeviceId), DeviceName]) - } - } - } - - protected logFilterFailure = (str: string, meta?: any) => { - const { - options: { - logFilterFailure = (isDebugMode() ? 'debug' : 'warn') - } = {} - } = this.config; - - if(logFilterFailure === false || !['warn','debug'].includes(logFilterFailure)) { - return false; - } - - this.logger[logFilterFailure](str, meta); - } - - isValidEvent = (playObj: PlayObject) => { - const { - meta: { - mediaType, event, user, server, eventReason - }, - data: { - artists, - track, - } = {} - } = playObj; - - if (mediaType !== 'Audio') { - this.logger.debug(`Will not scrobble event because media type was not 'Audio', found type: ${mediaType}`, { - track - }); - return false; - } - - if (event !== undefined && !['PlaybackProgress','PlaybackStarted', 'UserDataSaved'].includes(event)) { - this.logger.debug(`Will not scrobble event because event type is not PlaybackProgress, PlaybackStarted or UserDataSaved - found event: ${event}`) - return false; - } - - if (event !== undefined && event === 'UserDataSaved' && eventReason !== 'PlaybackFinished') { - this.logger.debug(`Will not scrobble event because event type of 'UserDataSaved' did not have a valid SaveReason of 'PlaybackFinished' - found reason: ${eventReason}`) - return false; - } - - if (this.servers !== undefined && !this.servers.includes(server.toLocaleLowerCase())) { - this.logFilterFailure(`Will not scrobble event because server was not an allowed server. Expected: ${this.servers.map(x => `'${x}'`).join(' or ')} | Found: '${server.toLocaleLowerCase()}'`, { - track - }) - return false; - } - - if (this.users !== undefined) { - if (user === undefined) { - this.logFilterFailure(`Will not scrobble event because config defined users but payload contained no user info`); - return false; - } else if (!this.users.includes(user.toLocaleLowerCase())) { - this.logFilterFailure(`Will not scrobble event because author was not an allowed user. Expected: ${this.users.map(x => `'${x}'`).join(' or ')} | Found: '${user.toLocaleLowerCase()}'`, { - artists, - track - }) - return false; - } - } - - return true; - } - - getRecentlyPlayed = async (options = {}) => this.getFlatRecentlyDiscoveredPlays() - - handle = async (playObj: PlayObject) => { - if (!this.isValidEvent(playObj)) { - return; - } - - const scrobbleOpts = {checkAll: false}; - let newPlays: PlayObject[] = []; - - //https://github.com/FoxxMD/multi-scrobbler/issues/87 - if(playObj.meta.event === 'UserDataSaved' && playObj.meta.eventReason === 'PlaybackFinished') { - - if(this.userDataWarn === false) { - this.logger.warn('!!!!!!!!!!WARNING!!!!!!!!!!'); - this.logger.warn(`You have enabled 'UserDataSaved' notification type for the Jellyfin webhook plugin.`); - this.logger.warn(`This notification's behavior is EXTREMELY bugged and you are likely to see duplicate scrobbles, missed scrobbles, and scrobbles with the wrong timestamp.`); - this.logger.warn(`This is caused by Jellyfin/plugin behavior that CANNOT be fixed by multi-scrobbler. See here for more context: https://github.com/FoxxMD/multi-scrobbler/issues/87`); - this.logger.warn(`You WILL NOT receive any support for issues/behavior caused by using the 'UserDataSaved' notification type. USE AT YOUR OWN RISK.`); - this.logger.warn('!!!!!!!!!!WARNING!!!!!!!!!!'); - this.userDataWarn = true; - } - - const trackId = buildTrackString(playObj, {include: ['artist', 'track']}); - - // sometimes jellyfin sends UserDataSaved payload with a LastPlayedDate that uses local time but accidentally includes a UTC offset (Z) - const now = dayjs(); - - const tz = dayjs.tz.guess(); - // so convert to UTC (corrects hour offset) - // and then convert BACK to local but keep corrected time - // then parse as new date from ISO string so dayjs has no knowledge of tz conversion - const normalizedDate = dayjs(playObj.data.playDate.utc().tz('Etc/UTC').tz(tz, true).toISOString()); - const oneHour = now.add(1, 'hour'); - - // if timestamp was from the future (tz offset is positive) we know the new one is correct - if(playObj.data.playDate.isSameOrAfter(oneHour)) { - playObj.data.playDate = normalizedDate; - this.logger.warn(`Play with event UserDataSaved-PlaybackFinished has a playDate that is from the future (${playObj.data.playDate.diff(now, 'minutes')} minutes from now). This is likely a local time with incorrect UTC offset and has been corrected. => ${trackId}`); - } - // if the timestamp is super close to the current time its likely the jellyfin client is online and this is the current track that just finished (not offline) - else if(Math.abs(now.diff(normalizedDate, 'seconds')) < TZ_OFFSET_PRESENT_THRESHOLD) { - playObj.data.playDate = normalizedDate; - this.logger.warn(`Play with event UserDataSaved-PlaybackFinished has a playDate that when timezone offset adjusted is only ${Math.abs(now.diff(normalizedDate, 'seconds'))}s from now. This is likely a local time with incorrect UTC offset and has been corrected. => ${trackId}`); - } - // unfortunately there's nothing(?) that can be done accurately detect and correct real offline play dates when the timezone offset is negative (in the past) - - let existingTracked: PlayObject; - for(const [platformIdStr, player] of this.players) { - const currPlay = player.getPlayedObject(); - if(currPlay !== undefined && playObjDataMatch(currPlay, playObj)) { - const temporalResult = comparePlayTemporally(currPlay, playObj); - if(isDebugMode()) { - player.logger.debug(doubleReturnNewline` - Play with event UserDataSaved-PlaybackFinished matched => ${trackId} - - Temporal Comparison => ${temporalPlayComparisonSummary(temporalResult, currPlay, playObj)}`); - } - if(hasAcceptableTemporalAccuracy(temporalResult.match)) { - existingTracked = currPlay; - } - break; - } - } - if(existingTracked !== undefined) { - this.logger.debug(`Will not scrobble Play with event UserDataSaved-PlaybackFinished because it has already been tracked => ${trackId}`); - return; - } - - // if last played ts was less than 30 seconds ago it's too early to scrobble (skipped track, essentially) - const finishedToNow = now.diff(playObj.data.playDate, 'seconds'); - if(Math.abs(finishedToNow) < 30) { - this.logger.debug(`Will not scrobble Play with event UserDataSaved-PlaybackFinished because it took place too recently (${finishedToNow}s) => ${trackId}`); - return; - } - - if(playObj.data.duration !== undefined) { - // since we want to use UserDataSaved for offline *only* a reasonable assumption to make is that - // the last played date + track duration = AT LEAST a little before NOW - // IE offline for 15 minutes -> played 2:00 track -> played ts is either 15 or 13 minutes ago, but not like 2 seconds ago - // -- so if we see a diff of only a few (10 for some buffer) seconds it's likely either Jellyfin or a Jellyfin client is reporting - // last played date while ONLINE and from the start of the track instead of the last (when PlaybackFinished actually occurred) - const lastPlayedAndDur = playObj.data.playDate.add(playObj.data.duration, 'seconds'); - const diffPlayedDuration = now.diff(lastPlayedAndDur, 'seconds'); - // TODO at least one PlayBackFinished SaveReason from symphonium is returning local time but with Z (UTC) timezone - // which makes this filtering not work at all - if(diffPlayedDuration < 10) { - this.logger.debug(`Will not scrobble Play with event UserDataSaved-PlaybackFinished because it is likely from online client (play date + track duration = ~now) => ${trackId}`); - return; - } - } - - - // good confidence this play object is from an offline event in which case we want to immediately scrobble it - // or we have already discovered this play (so it won't be re-scrobbled) - newPlays = [playObj]; - scrobbleOpts.checkAll = true; - - } else { - newPlays = await this.processRecentPlays([playObj]); - } - - if(newPlays.length > 0) { - try { - const discovered = await this.discover(newPlays, scrobbleOpts); - await this.scrobble(discovered); - } catch (e) { - this.logger.error('Encountered error while scrobbling') - this.logger.error(e) - } - } - } - - getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new JellyfinPlayerState(logger, id, opts) -} diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 9a4c92a5..80fc5c5a 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -11,8 +11,6 @@ import { LastFMEndpointSourceConfig, LastFMEndpointData } from "../common/infras import { JellyApiData, JellyApiSourceConfig, - JellyData, - JellySourceConfig } from "../common/infrastructure/config/source/jellyfin.js"; import { JRiverData, JRiverSourceConfig } from "../common/infrastructure/config/source/jriver.js"; import { KodiData, KodiSourceConfig } from "../common/infrastructure/config/source/kodi.js"; @@ -44,7 +42,6 @@ import DeezerSource from "./DeezerSource.js"; import { EndpointListenbrainzSource } from "./EndpointListenbrainzSource.js"; import { EndpointLastfmSource } from "./EndpointLastfmSource.js"; import JellyfinApiSource from "./JellyfinApiSource.js"; -import JellyfinSource from "./JellyfinSource.js"; import { JRiverSource } from "./JRiverSource.js"; import { KodiSource } from "./KodiSource.js"; import LastfmSource from "./LastfmSource.js"; @@ -409,9 +406,7 @@ export default class ScrobbleSources { } break; case 'jellyfin': - const j: (JellyData | JellyApiData) = { - users: process.env.JELLYFIN_USER, - servers: process.env.JELLYFIN_SERVER, + const j: JellyApiData = { user: process.env.JELLYFIN_USER, password: process.env.JELLYFIN_PASSWORD, apiKey: process.env.JELLYFIN_APIKEY, @@ -857,12 +852,7 @@ export default class ScrobbleSources { newSource = new SubsonicSource(name, compositeConfig as SubSonicSourceConfig, this.internalConfig, this.emitter); break; case 'jellyfin': - const jfConfig = compositeConfig as (JellySourceConfig | JellyApiSourceConfig); - if(jfConfig.data.user !== undefined) { - newSource = await new JellyfinApiSource(name, compositeConfig as JellyApiSourceConfig, this.internalConfig, this.emitter); - } else { - newSource = await new JellyfinSource(name, compositeConfig as JellySourceConfig, this.internalConfig, this.emitter); - } + newSource = await new JellyfinApiSource(name, compositeConfig as JellyApiSourceConfig, this.internalConfig, this.emitter); break; case 'lastfm': newSource = await new LastfmSource(name, compositeConfig as LastfmSourceConfig, this.internalConfig, this.emitter); diff --git a/src/backend/sources/ingressNotifiers/JellyfinNotifier.ts b/src/backend/sources/ingressNotifiers/JellyfinNotifier.ts deleted file mode 100644 index e7d0cfa6..00000000 --- a/src/backend/sources/ingressNotifiers/JellyfinNotifier.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Logger } from "@foxxmd/logging"; -import { Request } from "express"; -import { remoteHostIdentifiers, remoteHostStr } from "../../utils.js"; -import JellyfinSource from "../JellyfinSource.js"; -import { IngressNotifier } from "./IngressNotifier.js"; - -export class JellyfinNotifier extends IngressNotifier { - - constructor(logger: Logger) { - super('Jellyfin', logger); - } - - seenServers: Record = {}; - notifyBySource(req: Request, isRaw: boolean): [boolean, (string | undefined)] { - if(!isRaw) { - const parts = remoteHostIdentifiers(req); - let serverIdentifier = `${parts.host}-${parts.proxy ?? ''}`; - - const playObj = JellyfinSource.formatPlayObj({...req.body, connectionId: serverIdentifier}); - - const warnings = []; - - if(req.body.ServerName === undefined || req.body.ServerName === '') { - if(req.body.ServerId !== undefined && req.body.ServerId !== '') { - warnings.push(`Webhook payload did not contain ServerName, will use ServerId (${req.body.ServerId}) instead`); - serverIdentifier = req.body.ServerId; - } else { - warnings.push(`Webhook payload did not contain ServerName OR ServerId will use connection ID (${serverIdentifier}) instead`); - } - } else { - serverIdentifier = playObj.meta.server; - } - - if(this.seenServers[serverIdentifier] === undefined) { - let version = req.body.ServerVersion; - if(version === undefined || version === '') { - warnings.push('Webhook payload did not contain ServerVersion'); - version = 'Unknown'; - } - - if(warnings.length > 0) { - this.logger.warn(`${remoteHostStr(req)} There is information missing from the Jellyfin webhook payload. Make sure 'Send all Properties' is checked in webhook: ${warnings.join(' | ')}`); - } - - this.seenServers[serverIdentifier] = version; - return [true, `Received valid data from server ${serverIdentifier} (Version ${version}) for the first time.`]; - } - } - return [true, undefined]; - } - - notifyByRequest(req: Request, isRaw: boolean): string | undefined { - if(req.method !== 'POST') { - return `Expected POST request (webhook payload) but received ${req.method}`; - } - return; - } - -} diff --git a/src/backend/tests/jellyfin/jellyfin.test.ts b/src/backend/tests/jellyfin/jellyfin.test.ts index a26e3aa4..cdc23520 100644 --- a/src/backend/tests/jellyfin/jellyfin.test.ts +++ b/src/backend/tests/jellyfin/jellyfin.test.ts @@ -4,9 +4,7 @@ import EventEmitter from "events"; import { describe, it } from 'mocha'; import { JsonPlayObject, PlayMeta, PlayObject } from "../../../core/Atomic.js"; -import JellyfinSource from "../../sources/JellyfinSource.js"; import JellyfinApiSource from "../../sources/JellyfinApiSource.js"; -import samplePayload from './playbackProgressSample.json'; import validSession from './validSession.json'; import { JellyApiData } from "../../common/infrastructure/config/source/jellyfin.js"; import { generatePlay } from "../utils/PlayTestUtils.js"; @@ -60,30 +58,6 @@ const playWithMeta = (meta: PlayMeta): PlayerStateDataMaybePlay => { const nowPlayingSession = (data: object): SessionInfo => ({...validSession, NowPlayingItem: {...validSession.NowPlayingItem, ...data}}); -describe('Jellyfin Legacy Source', function() { - describe('Jellyfin Payload Parsing', function () { - - it('Should parse PlayProgress payload as PlayObject', async function () { - const fixture = dataAsFixture(samplePayload[0]); - const play = JellyfinSource.formatPlayObj(fixture.data); - - assert.equal(play.data.track, fixture.expected.data.track); - assert.equal(play.meta.mediaType, 'Audio'); - }); - }); - - describe('Correctly detects events as valid/invalid', function () { - const jfSource = new JellyfinSource('Test', {data: {}}, {localUrl: new URL('http://test'), configDir: 'test', logger: loggerTest, version: 'test'}, new EventEmitter()); - it('Should parse PlayProgress with Audio ItemType as valid event', async function () { - const fixture = dataAsFixture(samplePayload[0]); - const play = JellyfinSource.formatPlayObj(fixture.data); - - assert.isTrue(jfSource.isValidEvent(play)) - await jfSource.destroy(); - }); - }); -}); - describe("Jellyfin API Source", function() { describe('Parses config allow/block correctly', function () {