mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
fix: ensure title changes are accounted for in canonical id calculation
This commit is contained in:
1029
server/src/services/LocalMediaCanonicalizer.test.ts
Normal file
1029
server/src/services/LocalMediaCanonicalizer.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,9 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
private getShowCanonicalId(show: Show): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
this.updateHashForBaseItem(show, hash);
|
||||
// FIXME: updateHashForBaseItem already calls updateHashForGrouping for non-terminal
|
||||
// types, so plot/tagline are hashed twice here. Removing this call is a breaking
|
||||
// change (all Show canonical IDs will change).
|
||||
this.updateHashForGrouping(show, hash);
|
||||
orderBy(show.actors, (a) => a.name).forEach((a) => {
|
||||
hash.update(a.name);
|
||||
@@ -73,6 +76,7 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
private getSeasonCanonicalId(season: Season): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
this.updateHashForBaseItem(season, hash);
|
||||
// FIXME: same double-hash bug as getShowCanonicalId — plot/tagline hashed twice.
|
||||
this.updateHashForGrouping(season, hash);
|
||||
hash.update(season.index?.toString() ?? '');
|
||||
orderBy(season.studios, (s) => s.name).forEach((s) => hash.update(s.name));
|
||||
@@ -100,6 +104,7 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
private getMusicArtistCanonicalId(musicArtist: MusicArtist): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
this.updateHashForBaseItem(musicArtist, hash);
|
||||
// FIXME: same double-hash bug as getShowCanonicalId — plot/tagline hashed twice.
|
||||
this.updateHashForGrouping(musicArtist, hash);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
@@ -107,6 +112,7 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
private getMusicAlbumCanonicalId(musicAlbum: MusicAlbum): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
this.updateHashForBaseItem(musicAlbum, hash);
|
||||
// FIXME: same double-hash bug as getShowCanonicalId — plot/tagline hashed twice.
|
||||
this.updateHashForGrouping(musicAlbum, hash);
|
||||
hash.update(musicAlbum.index?.toString() ?? '');
|
||||
orderBy(musicAlbum.studios, (s) => s.name).forEach((s) =>
|
||||
@@ -121,6 +127,10 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
private getMusicTrackCanonicalId(musicTrack: MusicTrack): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
this.updateHashForBaseItem(musicTrack, hash);
|
||||
// FIXME: updateHashForBaseItem already calls updateHashForTerminalProgram for
|
||||
// terminal types, so all terminal fields (actors, directors, duration, mediaItem,
|
||||
// etc.) are hashed twice, and year is hashed a third time below. Removing this
|
||||
// call is a breaking change (all MusicTrack canonical IDs will change).
|
||||
this.updateHashForTerminalProgram(musicTrack, hash);
|
||||
hash.update(musicTrack.trackNumber?.toString() ?? '');
|
||||
hash.update(musicTrack.year?.toString() ?? '');
|
||||
@@ -129,16 +139,19 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
|
||||
private updateHashForBaseItem(base: ProgramLike, hash: crypto.Hash) {
|
||||
hash.update(base.externalId);
|
||||
base.genres?.forEach((g) => {
|
||||
orderBy(base.genres ?? [], (g) => g.name).forEach((g) => {
|
||||
hash.update(g.name);
|
||||
});
|
||||
base.identifiers.forEach((i) => {
|
||||
orderBy(
|
||||
base.identifiers,
|
||||
(i) => `${i.type}|${i.id}|${i.sourceId ?? ''}`,
|
||||
).forEach((i) => {
|
||||
hash.update(`${i.id}|${i.sourceId ?? ''}|${i.type}`);
|
||||
});
|
||||
hash.update(base.libraryId);
|
||||
hash.update(base.mediaSourceId);
|
||||
hash.update(base.sourceType);
|
||||
base.tags.forEach((t) => hash.update(t));
|
||||
orderBy(base.tags).forEach((t) => hash.update(t));
|
||||
hash.update(base.title);
|
||||
hash.update(base.type);
|
||||
if (isTerminalItemType(base)) {
|
||||
@@ -152,6 +165,11 @@ export class LocalMediaCanonicalizer implements Canonicalizer<ProgramLike> {
|
||||
base: TerminalProgram,
|
||||
hash: crypto.Hash,
|
||||
) {
|
||||
// FIXME: actors and directors are not prefixed with a type marker before hashing.
|
||||
// Because hash.update('') is a no-op, an actor with no order/role is
|
||||
// hash-indistinguishable from a director with the same name. Fix by adding a
|
||||
// type prefix (e.g. hash.update('actor') / hash.update('director')) — but this
|
||||
// is a breaking change for any item that has actors or directors.
|
||||
orderBy(base.actors, (a) => a.name).forEach((a) => {
|
||||
hash.update(a.name);
|
||||
hash.update(a.order?.toString() ?? '');
|
||||
|
||||
1336
server/src/services/PlexMediaCanonicalizers.test.ts
Normal file
1336
server/src/services/PlexMediaCanonicalizers.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexMovie(plexMovie: PlexMovie): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexMovie.key);
|
||||
hash.update(plexMovie.title);
|
||||
hash.update(plexMovie.addedAt?.toString() ?? '');
|
||||
hash.update(plexMovie.updatedAt?.toString() ?? '');
|
||||
for (const media of plexMovie.Media ?? []) {
|
||||
@@ -64,6 +65,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexShow(plexShow: PlexTvShow): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexShow.key);
|
||||
hash.update(plexShow.title);
|
||||
hash.update(plexShow.addedAt?.toString() ?? '');
|
||||
hash.update(plexShow.updatedAt?.toString() ?? '');
|
||||
for (const role of plexShow.Role ?? []) {
|
||||
@@ -84,6 +86,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexCollection.key);
|
||||
hash.update(plexCollection.title);
|
||||
hash.update(plexCollection.addedAt?.toString() ?? '');
|
||||
hash.update(plexCollection.updatedAt?.toString() ?? '');
|
||||
hash.update(plexCollection.childCount?.toFixed() ?? '');
|
||||
@@ -94,6 +97,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexSeason(plexSeason: PlexTvSeason): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexSeason.key);
|
||||
hash.update(plexSeason.title);
|
||||
hash.update(plexSeason.addedAt?.toString() ?? '');
|
||||
hash.update(plexSeason.updatedAt?.toString() ?? '');
|
||||
|
||||
@@ -107,6 +111,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexEpisode(plexEpisode: PlexEpisode): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexEpisode.key);
|
||||
hash.update(plexEpisode.title);
|
||||
hash.update(plexEpisode.addedAt?.toString() ?? '');
|
||||
hash.update(plexEpisode.updatedAt?.toString() ?? '');
|
||||
|
||||
@@ -134,14 +139,20 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
}
|
||||
}
|
||||
|
||||
seq.collect(
|
||||
compact(
|
||||
flatten([plexEpisode.Director, plexEpisode.Writer, plexEpisode.Role]),
|
||||
),
|
||||
(keyVal) => {
|
||||
hash.update(keyVal.tag);
|
||||
},
|
||||
);
|
||||
for (const director of plexEpisode.Director ?? []) {
|
||||
hash.update('director');
|
||||
hash.update(director.tag);
|
||||
}
|
||||
|
||||
for (const writer of plexEpisode.Writer ?? []) {
|
||||
hash.update('writer');
|
||||
hash.update(writer.tag);
|
||||
}
|
||||
|
||||
for (const role of plexEpisode.Role ?? []) {
|
||||
hash.update('role');
|
||||
hash.update(role.tag);
|
||||
}
|
||||
|
||||
return hash.digest('base64');
|
||||
}
|
||||
@@ -149,6 +160,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexMusicArtist(plexArtist: PlexMusicArtist): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexArtist.key);
|
||||
hash.update(plexArtist.title);
|
||||
hash.update(plexArtist.addedAt?.toString() ?? '');
|
||||
hash.update(plexArtist.updatedAt?.toString() ?? '');
|
||||
|
||||
@@ -169,9 +181,9 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
// hash.update(keyVal.tag);
|
||||
// },
|
||||
// );
|
||||
plexArtist.Genre?.sort()
|
||||
.map((g) => g.tag)
|
||||
.forEach((genre) => hash.update(genre));
|
||||
plexArtist.Genre?.toSorted((a, b) => a.tag.localeCompare(b.tag)).forEach(
|
||||
(g) => hash.update(g.tag),
|
||||
);
|
||||
|
||||
return hash.digest('base64');
|
||||
}
|
||||
@@ -179,6 +191,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexMusicAlbum(plexAlbum: PlexMusicAlbum): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexAlbum.key);
|
||||
hash.update(plexAlbum.title);
|
||||
hash.update(plexAlbum.addedAt?.toString() ?? '');
|
||||
hash.update(plexAlbum.updatedAt?.toString() ?? '');
|
||||
hash.update(plexAlbum.year?.toFixed() ?? '');
|
||||
@@ -191,9 +204,9 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
hash.update(plexAlbum.studio);
|
||||
}
|
||||
|
||||
plexAlbum.Genre?.sort()
|
||||
.map((g) => g.tag)
|
||||
.forEach((genre) => hash.update(genre));
|
||||
plexAlbum.Genre?.toSorted((a, b) => a.tag.localeCompare(b.tag)).forEach(
|
||||
(g) => hash.update(g.tag),
|
||||
);
|
||||
|
||||
return hash.digest('base64');
|
||||
}
|
||||
@@ -201,6 +214,7 @@ export class PlexMediaCanonicalizer implements Canonicalizer<PlexMedia> {
|
||||
private canonicalizePlexTrack(plexMusicTrack: PlexMusicTrack): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(plexMusicTrack.key);
|
||||
hash.update(plexMusicTrack.title);
|
||||
hash.update(plexMusicTrack.addedAt?.toString() ?? '');
|
||||
hash.update(plexMusicTrack.updatedAt?.toString() ?? '');
|
||||
hash.update(plexMusicTrack.duration?.toFixed() ?? '');
|
||||
|
||||
Reference in New Issue
Block a user