From 7cee9f04cfa4c97837b17dd55df68d72401e9da4 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 15 Sep 2025 17:37:30 +0000 Subject: [PATCH] fix: Update object hashing function Might be a solution for #358 --- package-lock.json | 11 +++++++++++ package.json | 1 + .../common/vendor/chromecast/ChromecastClientUtils.ts | 4 ++-- src/backend/sources/JellyfinApiSource.ts | 5 ++--- src/backend/sources/MemorySource.ts | 6 +++--- src/backend/sources/PlexApiSource.ts | 5 ++--- src/backend/utils/StringUtils.ts | 8 +++++++- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42cb2270..d2c48e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "mopidy": "^1.3.0", "mpd-api": "^1.1.2", "nanoid": "^3.3.1", + "node-object-hash": "^3.1.1", "normalize-url": "^8.0.1", "ntfy": "^1.7.6", "object-hash": "^3.0.0", @@ -7861,6 +7862,16 @@ } } }, + "node_modules/node-object-hash": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-3.1.1.tgz", + "integrity": "sha512-A32kRGjXtwQ+uSa3GrXiCl8HVFY0Jy6IiKFO7UjagAKSaOOrruxB2Qf/w7TP5QtNfB3uOiHTu3cjhp8k/C0PCg==", + "license": "MIT", + "engines": { + "node": ">=16", + "pnpm": ">=8" + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", diff --git a/package.json b/package.json index d51be5b5..803888be 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "mopidy": "^1.3.0", "mpd-api": "^1.1.2", "nanoid": "^3.3.1", + "node-object-hash": "^3.1.1", "normalize-url": "^8.0.1", "ntfy": "^1.7.6", "object-hash": "^3.0.0", diff --git a/src/backend/common/vendor/chromecast/ChromecastClientUtils.ts b/src/backend/common/vendor/chromecast/ChromecastClientUtils.ts index ad59291d..fb3517ff 100644 --- a/src/backend/common/vendor/chromecast/ChromecastClientUtils.ts +++ b/src/backend/common/vendor/chromecast/ChromecastClientUtils.ts @@ -1,8 +1,8 @@ import { Media, MediaController, Result } from "@foxxmd/chromecast-client"; -import objectHash from "object-hash"; import { PlayObject } from "../../../../core/Atomic.js"; import { REPORTED_PLAYER_STATUSES, ReportedPlayerStatus } from "../../infrastructure/Atomic.js"; import { PlatformApplication, PlatformType } from "./interfaces.js"; +import { hashObject } from "../../../utils/StringUtils.js"; export const chromePlayerStateToReported = (state: string): ReportedPlayerStatus => { switch (state) { @@ -71,5 +71,5 @@ export const genPlayHash = (play: PlayObject) => { } } = play; - return objectHash.sha1({artists, track, album, albumArtists, mediaType}); + return hashObject({artists, track, album, albumArtists, mediaType}); } diff --git a/src/backend/sources/JellyfinApiSource.ts b/src/backend/sources/JellyfinApiSource.ts index 002a6ffd..947a6970 100644 --- a/src/backend/sources/JellyfinApiSource.ts +++ b/src/backend/sources/JellyfinApiSource.ts @@ -1,6 +1,5 @@ import { Logger } from "@foxxmd/logging"; import { WS } from "iso-websocket"; -import objectHash from 'object-hash'; // @ts-expect-error weird typings? import { Api, Jellyfin } from "@jellyfin/sdk"; import { @@ -58,7 +57,7 @@ import { import { JellyApiSourceConfig } from "../common/infrastructure/config/source/jellyfin.js"; import { genGroupIdStr, getPlatformIdFromData, isDebugMode, parseBool, } from "../utils.js"; import { joinedUrl } from "../utils/NetworkUtils.js"; -import { parseArrayFromMaybeString } from "../utils/StringUtils.js"; +import { hashObject, parseArrayFromMaybeString } from "../utils/StringUtils.js"; import { MemoryPositionalSource } from "./MemoryPositionalSource.js"; import { FixedSizeList } from "fixed-size-list"; @@ -98,7 +97,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource { this.canPoll = true; this.multiPlatform = true; this.requiresAuth = true; - this.deviceId = `${name}-ms${internal.version}-${truncateStringToLength(10, '')(objectHash.sha1(config))}`; + this.deviceId = `${name}-ms${internal.version}-${truncateStringToLength(10, '')(hashObject(config))}`; this.client = new Jellyfin({ clientInfo: { diff --git a/src/backend/sources/MemorySource.ts b/src/backend/sources/MemorySource.ts index f6443933..2ff594fe 100644 --- a/src/backend/sources/MemorySource.ts +++ b/src/backend/sources/MemorySource.ts @@ -1,7 +1,6 @@ import { Logger } from "@foxxmd/logging"; import dayjs, { Dayjs } from "dayjs"; import { EventEmitter } from "events"; -import objectHash from 'object-hash'; import { SimpleIntervalJob, Task, ToadScheduler } from "toad-scheduler"; import { PlayObject, SOURCE_SOT, SOURCE_SOT_TYPES, SourcePlayerObj } from "../../core/Atomic.js"; import { buildTrackString } from "../../core/StringUtils.js"; @@ -28,6 +27,7 @@ import { timePassesScrobbleThreshold, timeToHumanTimestamp } from "../utils/Time import AbstractSource from "./AbstractSource.js"; import { AbstractPlayerState, createPlayerOptions, PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js"; import { GenericPlayerState } from "./PlayerState/GenericPlayerState.js"; +import { hashObject } from "../utils/StringUtils.js"; const EXPECTED_NON_DISCOVERED_REASON = 'not added because an identical play with the same timestamp was already discovered.'; @@ -86,7 +86,7 @@ export default class MemorySource extends AbstractSource { } else if (isStale) { label = 'Stale Player Cleanup'; const state = player.getApiState(); - const stateHash = objectHash.sha1(state); + const stateHash = hashObject(state); if(stateHash !== this.playerState.get(key)) { this.playerState.set(key, stateHash); this.emitEvent('playerUpdate', { @@ -265,7 +265,7 @@ export default class MemorySource extends AbstractSource { player.logSummary(); } const apiState = player.getApiState(); - this.playerState.set(key, objectHash.sha1(apiState)) + this.playerState.set(key, hashObject(apiState)) this.emitEvent('playerUpdate', { ...apiState, options: { diff --git a/src/backend/sources/PlexApiSource.ts b/src/backend/sources/PlexApiSource.ts index bad51fbe..db90a8eb 100644 --- a/src/backend/sources/PlexApiSource.ts +++ b/src/backend/sources/PlexApiSource.ts @@ -1,4 +1,3 @@ -import objectHash from 'object-hash'; import EventEmitter from "events"; import { PlayObject } from "../../core/Atomic.js"; import { buildTrackString, combinePartsToString, truncateStringToLength } from "../../core/StringUtils.js"; @@ -12,7 +11,7 @@ import { PlayPlatformId, REPORTED_PLAYER_STATUSES } from "../common/infrastructure/Atomic.js"; import { genGroupIdStr, getFirstNonEmptyString, getPlatformIdFromData, isDebugMode, parseBool, } from "../utils.js"; -import { buildStatePlayerPlayIdententifyingInfo, parseArrayFromMaybeString } from "../utils/StringUtils.js"; +import { buildStatePlayerPlayIdententifyingInfo, hashObject, parseArrayFromMaybeString } from "../utils/StringUtils.js"; import { GetSessionsMetadata } from "@lukehagar/plexjs/sdk/models/operations/getsessions.js"; import { PlexAPI } from "@lukehagar/plexjs"; import { PlexApiSourceConfig } from "../common/infrastructure/config/source/plex.js"; @@ -66,7 +65,7 @@ export default class PlexApiSource extends MemoryPositionalSource { this.multiPlatform = true; this.requiresAuth = true; this.requiresAuthInteraction = false; - this.deviceId = `${name}-ms${internal.version}-${truncateStringToLength(10, '')(objectHash.sha1(config))}`; + this.deviceId = `${name}-ms${internal.version}-${truncateStringToLength(10, '')(hashObject(config))}`; this.uniqueDropReasons = new FixedSizeList(100); this.mediaIdsSeen = new FixedSizeList(100); } diff --git a/src/backend/utils/StringUtils.ts b/src/backend/utils/StringUtils.ts index 588af50d..95002ab6 100644 --- a/src/backend/utils/StringUtils.ts +++ b/src/backend/utils/StringUtils.ts @@ -1,4 +1,5 @@ import { strategies, stringSameness, StringSamenessResult } from "@foxxmd/string-sameness"; +import { hasher } from 'node-object-hash'; import { PlayObject } from "../../core/Atomic.js"; import { asPlayerStateData, DELIMITERS, PlayerStateDataMaybePlay } from "../common/infrastructure/Atomic.js"; import { genGroupIdStr, getPlatformIdFromData, intersect, parseRegexSingleOrFail } from "../utils.js"; @@ -419,4 +420,9 @@ export const normalizeListenbrainzUrl = (urlVal: string): string | undefined => return urlVal.replace(LZ_VERSION_PATH, ''); } return undefined; -} \ No newline at end of file +} + +type HashFunction = (obj: object) => string; +const defaultHasher = hasher(); +const defaultHashFunc: HashFunction = (obj) => defaultHasher.hash(obj); +export const hashObject = (obj: object, h: HashFunction = defaultHashFunc): string => h(obj); \ No newline at end of file -- 2.51.2