Merge pull request #754 from FoxxMD/deezerExtraInfo
Some checks are pending
Publish Docker image to Dockerhub / test (push) Waiting to run
Publish Docker image to Dockerhub / Build OCI Images (push) Blocked by required conditions
Publish Docker image to Dockerhub / Build OCI Images-1 (push) Blocked by required conditions
Publish Docker image to Dockerhub / Merge OCI Images and Push (push) Blocked by required conditions

feat(deezer): Add additional data to deezer plays
This commit is contained in:
Matt Foxx 2026-10-15 08:11:29 -04:00 • committed by GitHub
commit aab24819a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 8 deletions

View file

@ -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 |
| `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 | |

View file

@ -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<typeof deezerInternalDataSchema>;
export const deezerInternalSourceOptionsSchema = z.object({
...commonSourceOptionsSchema.shape,
variant: z.boolean().optional().meta({
description: 'Add the reported title variant, like `(Remastered)`, to the title name.',
}),
/**
* 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<typeof deezerInternalSourceOptionsSchema>;
@ -116,7 +120,8 @@ export type DeezerCompatConfig = z.infer<typeof deezerCompatConfigSchema>;
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()))
});
export const envSchemas: EnvSourceSchema<typeof envDataSchema, DeezerInternalSourceConfig> = {
@ -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
}

View file

@ -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),
},
@ -244,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);