From edee34e9d50b898414abcddcc8e365a62522f478 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 14 Oct 2026 13:12:57 +0000 Subject: [PATCH 1/3] feat(deezer): Add additional data to deezer plays #752 --- .../sources/_env_configs/_deezer.md | 3 ++- .../infrastructure/config/source/deezer.ts | 15 +++++++++++++-- src/backend/sources/DeezerInternalSource.ts | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docsite/docs/configuration/sources/_env_configs/_deezer.md b/docsite/docs/configuration/sources/_env_configs/_deezer.md index 309e30b8..7295900e 100644 --- a/docsite/docs/configuration/sources/_env_configs/_deezer.md +++ b/docsite/docs/configuration/sources/_env_configs/_deezer.md @@ -5,4 +5,5 @@ | `DEEZER_ENABLE` | boolean | true | Should this component be used? | | _**`DEEZER_ARL`**_ | string | | ARL retrieved from Deezer response header | | `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. | -| `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent | \ No newline at end of file +| `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent | +| `DEEZER_TRACK_VARIANT` | boolean | false | | \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index 42b3020d..ec8967f9 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -2,6 +2,7 @@ import * as z from "zod"; import type {Second} from "../../../../../core/Atomic.ts"; import {pollingOptionsSchema} from "../common.ts"; import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { envMetaNormalize } from "../../../../utils/ZodUtils.ts"; export const deezerDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -80,7 +81,9 @@ export const deezerInternalDataSchema = z.object({ export type DeezerInternalData = z.infer; export const deezerInternalSourceOptionsSchema = z.object({ - ...commonSourceOptionsSchema.shape, + variant: z.boolean().optional().meta({ + description: 'Add the reported title variant, like `(Remastered)`, to the title name.', + }).default(false), /** * Set the heuristic level of duplicate/repeated track pruning from listening history. * @@ -90,6 +93,7 @@ export const deezerInternalSourceOptionsSchema = z.object({ * * `aggressive` => above AND check for plays during previous play times that occurred during the track (50% +/- duration plus playedAt date) */ fuzzyDiscoveryIgnore: z.union([z.boolean(), z.literal('aggressive')]).optional(), + ...commonSourceOptionsSchema.shape, }); export type DeezerInternalSourceOptions = z.infer; @@ -116,7 +120,8 @@ export type DeezerCompatConfig = z.infer; const envDataSchema = z.object({ DEEZER_ARL: deezerInternalDataSchema.shape.arl, DEEZER_ACCOUNT_ID: deezerInternalDataSchema.shape.accountId, - DEEZER_USER_AGENT: deezerInternalDataSchema.shape.userAgent + DEEZER_USER_AGENT: deezerInternalDataSchema.shape.userAgent, + DEEZER_TRACK_VARIANT: z.stringbool().optional().meta(envMetaNormalize(deezerInternalSourceOptionsSchema.shape.variant.meta())).default(false) }); export const envSchemas: EnvSourceSchema = { @@ -154,6 +159,12 @@ export interface DeezerInternalTrackData { SNG_TITLE: string /** Time listened to track in seconds */ DURATION: Second + /** The variant of the Song Title + * + * @example (Remastered) + */ + VERSION?: string + ISRC?: string __TYPE__: 'song' | string } diff --git a/src/backend/sources/DeezerInternalSource.ts b/src/backend/sources/DeezerInternalSource.ts index 609f2c0a..7069b9ca 100644 --- a/src/backend/sources/DeezerInternalSource.ts +++ b/src/backend/sources/DeezerInternalSource.ts @@ -1,4 +1,4 @@ -import { withAlbumArt } from "../../core/MusicMetadata.ts"; +import { isrcMeta, withAlbumArt } from "../../core/MusicMetadata.ts"; import dayjs from "dayjs"; import type EventEmitter from "events"; import type { Request } from 'superagent'; @@ -7,7 +7,7 @@ import { COMPONENT_AUTH_TYPE, type ComponentAuthType, PARSED_FROM, type PlayMatc import { DEFAULT_RETRY_MULTIPLIER, type FormatPlayObjectOptions, type InternalConfig } from "../common/infrastructure/Atomic.ts"; import type {DeezerInternalSourceConfig, DeezerInternalTrackData} from "../common/infrastructure/config/source/deezer.ts"; import { TRANSFORM_HOOK } from "../../core/Transform.ts"; -import { parseRetryAfterSecsFromObj, playObjDataMatch, sleep, sortByOldestPlayDate} from "../utils.ts"; +import { getNonEmptyString, parseRetryAfterSecsFromObj, playObjDataMatch, sleep, sortByOldestPlayDate} from "../utils.ts"; import type {RecentlyPlayedOptions} from "./AbstractSource.ts"; import { CookieJar } from 'tough-cookie'; import { MixedCookieAgent } from 'http-cookie-agent/http'; @@ -114,13 +114,21 @@ export default class DeezerInternalSource extends MemorySource { this.agent = request.agent().use((req) => req.agent(mixedAgent)); } - static formatPlayObj(obj: DeezerInternalTrackData, options: FormatPlayObjectOptions = {}): PlayObject { + static formatPlayObj(obj: DeezerInternalTrackData, options: FormatPlayObjectOptions & {variant?: boolean} = {}): PlayObject { const {newFromSource = false} = options; + const trackParts: string[] = [obj.SNG_TITLE]; + if(options.variant === true) { + const v = getNonEmptyString(obj.VERSION); + if(v !== undefined) { + trackParts.push(v); + } + } + const play: PlayObjectMinimal = { data: { artists: namesToCredits([obj.ART_NAME]), album: nameToCredit(obj.ALB_TITLE), - track: nameToCredit(obj.SNG_TITLE), + track: nameToCredit(trackParts.join(' '), isrcMeta(getNonEmptyString(obj.ISRC))), duration: obj.DURATION, playDate: dayjs(obj.TS * 1000), }, From 31293eca2cba85a107a298d7dfe7955f04c5521b Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 14 Oct 2026 13:18:18 +0000 Subject: [PATCH 2/3] fix: don't use default for deezer variant --- src/backend/common/infrastructure/config/source/deezer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index ec8967f9..adc04b7b 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -83,7 +83,7 @@ export type DeezerInternalData = z.infer; export const deezerInternalSourceOptionsSchema = z.object({ variant: z.boolean().optional().meta({ description: 'Add the reported title variant, like `(Remastered)`, to the title name.', - }).default(false), + }), /** * Set the heuristic level of duplicate/repeated track pruning from listening history. * @@ -121,7 +121,7 @@ const envDataSchema = z.object({ DEEZER_ARL: deezerInternalDataSchema.shape.arl, DEEZER_ACCOUNT_ID: deezerInternalDataSchema.shape.accountId, DEEZER_USER_AGENT: deezerInternalDataSchema.shape.userAgent, - DEEZER_TRACK_VARIANT: z.stringbool().optional().meta(envMetaNormalize(deezerInternalSourceOptionsSchema.shape.variant.meta())).default(false) + DEEZER_TRACK_VARIANT: z.stringbool().optional().meta(envMetaNormalize(deezerInternalSourceOptionsSchema.shape.variant.meta())) }); export const envSchemas: EnvSourceSchema = { From 0045728a5d771c23de8d0d20c18b7a38863339de Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Thu, 15 Oct 2026 00:47:47 +0000 Subject: [PATCH 3/3] fix: missing variant arg --- src/backend/sources/DeezerInternalSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/sources/DeezerInternalSource.ts b/src/backend/sources/DeezerInternalSource.ts index 7069b9ca..4054a75f 100644 --- a/src/backend/sources/DeezerInternalSource.ts +++ b/src/backend/sources/DeezerInternalSource.ts @@ -252,7 +252,7 @@ export default class DeezerInternalSource extends MemorySource { this.logger.debug(`Ignoring ${nonSong.length} entries in history with types of ${nonSongTypes.join(',')}`); } return resp.results.data.filter(x => x.__TYPE__ === 'song').map(x => { - const play = DeezerInternalSource.formatPlayObj(x); + const play = DeezerInternalSource.formatPlayObj(x, {variant: this.config.options?.variant}); play.meta.parsedFrom = PARSED_FROM.history; return play; }).sort(sortByOldestPlayDate);