Compare commits

...
6 Commits
Author SHA1 Message Date
FoxxMD 8be08a5c29 chore: Bump for release 2025-06-20 20:09:21 +00:00
Matt Foxx 9fe5b9b671 Merge pull request #312 from FoxxMD/lzEnrich
feat: Enrich Listenbrainz submit payload
2025-06-20 16:07:43 -04:00
FoxxMD 720c07b008 feat(listenbrainz): Include more ids/fields in submit additional_info #311 2025-06-20 16:43:01 +00:00
FoxxMD 7fdf93c5d3 feat(spotify): Add spotify meta ids to play 2025-06-20 16:42:04 +00:00
FoxxMD a0fd466bcf dev: Add ENV only launch config 2025-06-19 18:15:45 +00:00
FoxxMD 35d62c913a fix: Guard against undefined type in config.json #310 2025-06-19 18:12:57 +00:00
8 changed files with 97 additions and 10 deletions
+19
View File
@@ -16,6 +16,25 @@
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "dev (ENV Only)",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/index.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"env": {
"CONFIG_DIR": "./not-real"
},
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "tsx",
"type": "node",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "multi-scrobbler",
"version": "0.9.5",
"version": "0.9.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-scrobbler",
"version": "0.9.5",
"version": "0.9.6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "multi-scrobbler",
"version": "0.9.5",
"version": "0.9.6",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
+44 -4
View File
@@ -74,8 +74,21 @@ export interface AdditionalTrackInfoResponse extends AdditionalTrackInfo {
recording_msid?: string
}
// using submit-listens example from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens
// which is documented in official docs https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#openapi-specification
// and based on this LZ developer comment https://github.com/lyarenei/jellyfin-plugin-listenbrainz/issues/10#issuecomment-1253867941
export interface SubmitListenAdditionalTrackInfo extends AdditionalTrackInfo {
artist_names?: string[]
release_artist_name?: string
release_artist_names?: string[]
spotify_album_id?: string
spotify_album_artist_ids?: string[]
spotify_artist_ids?: string[]
albumartist?: string
}
export interface TrackPayload extends MinimumTrack {
additional_info?: AdditionalTrackInfo
additional_info?: SubmitListenAdditionalTrackInfo
}
export interface ListenPayload {
@@ -339,14 +352,40 @@ export class ListenbrainzApiClient extends AbstractApiClient {
track,
duration,
meta: {
brainz = {}
brainz = {},
spotify = {}
} = {}
}
} = play;
// using submit-listens exmaple from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens
// which is documented in official docs https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#openapi-specification
// and based on this LZ developer comment https://github.com/lyarenei/jellyfin-plugin-listenbrainz/issues/10#issuecomment-1253867941
const addInfo: SubmitListenAdditionalTrackInfo = {
// all artists
artist_names: Array.from(new Set([...artists, ...albumArtists])),
// primary artist
release_artist_name: artists[0],
release_artist_names: [artists[0]],
};
if(spotify.track !== undefined) {
addInfo.spotify_id = spotify.track;
}
if(spotify.album !== undefined) {
addInfo.spotify_album_id = spotify.album;
}
if(spotify.albumArtist !== undefined && spotify.albumArtist.length > 0) {
addInfo.spotify_album_artist_ids = spotify.albumArtist;
}
if(spotify.artist !== undefined) {
addInfo.spotify_artist_ids = spotify.artist;
}
return {
listened_at: getScrobbleTsSOCDate(play).unix(),
track_metadata: {
artist_name: artists[0],
artist_name: Array.from(new Set([...artists, ...albumArtists])).join(', '),
track_name: track,
release_name: album,
additional_info: {
@@ -354,7 +393,8 @@ export class ListenbrainzApiClient extends AbstractApiClient {
track_mbid: brainz.track,
artist_mbids: brainz.artist,
release_mbid: brainz.album,
release_group_mbid: brainz.releaseGroup
release_group_mbid: brainz.releaseGroup,
...addInfo
}
}
}
@@ -121,6 +121,11 @@ export default class ScrobbleClients {
clientDefaults = cd;
for (const [index, c] of mainConfigClientConfigs.entries()) {
const {name = 'unnamed'} = c;
if(c.type === undefined) {
const invalidMsgType = `Client config ${index + 1} (${name}) in config.json does not have a "type" property! "type": "[clientType]" must be one of ${clientTypes.join(' | ')}`;
this.logger.error(invalidMsgType);
continue;
}
if(!isClientType(c.type.toLocaleLowerCase())) {
const invalidTypeMsg = `Client config ${index + 1} (${name}) in config.json has an invalid client type of '${c.type}'. Must be one of ${clientTypes.join(' | ')}`;
//this.emitter.emit('error', new Error(invalidTypeMsg));
+7 -1
View File
@@ -265,8 +265,14 @@ export default class ScrobbleSources {
sourceDefaults = this.buildSourceDefaults(sd);
for (const [index, c] of mainConfigSourcesConfigs.entries()) {
const {name = 'unnamed'} = c;
if(c.type === undefined) {
const invalidMsgType = `Source config ${index + 1} (${name}) in config.json does not have a "type" property! "type": "[sourceType]" must be one of ${sourceTypes.join(' | ')}`;
this.emitter.emit('error', new Error(invalidMsgType));
this.logger.error(invalidMsgType);
continue;
}
if(!isSourceType(c.type.toLocaleLowerCase())) {
const invalidMsgType = `Source config ${index + 1} (${name}) in config.json has an invalid source type of '${c.type}'. Must be one of ${sourceTypes.join(' | ')}`;
const invalidMsgType = `Source config ${index + 1} (${name}) in config.json has an invalid source "type" of "${c.type}". Must be one of ${sourceTypes.join(' | ')}`;
this.emitter.emit('error', new Error(invalidMsgType));
this.logger.error(invalidMsgType);
continue;
+11 -2
View File
@@ -2,7 +2,7 @@ import dayjs, { Dayjs } from "dayjs";
import EventEmitter from "events";
import SpotifyWebApi from "spotify-web-api-node";
import request from 'superagent';
import { PlayObject, SCROBBLE_TS_SOC_END, SCROBBLE_TS_SOC_START, ScrobbleTsSOC } from "../../core/Atomic.js";
import { PlayObject, SCROBBLE_TS_SOC_END, SCROBBLE_TS_SOC_START, ScrobbleTsSOC, SpotifyMeta } from "../../core/Atomic.js";
import { combinePartsToString, truncateStringToLength } from "../../core/StringUtils.js";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
import { hasUpstreamError, UpstreamError } from "../common/errors/UpstreamError.js";
@@ -155,6 +155,7 @@ export default class SpotifySource extends MemoryPositionalSource {
}
const {
id: albumId,
name: albumName,
artists: albumArtists = [],
images = []
@@ -183,7 +184,15 @@ export default class SpotifySource extends MemoryPositionalSource {
track: name,
duration: duration_ms / 1000,
playDate: played_at,
playDateCompleted
playDateCompleted,
meta: {
spotify: {
track: id,
artist: artists.map(x => x.id),
albumArtist: actualAlbumArtists.map(x => x.id),
album: albumId
}
}
},
meta: {
deviceId: deviceId ?? `${NO_DEVICE}-${NO_USER}`,
+8
View File
@@ -74,6 +74,13 @@ export interface BrainzMeta {
releaseGroup?: string
}
export interface SpotifyMeta {
artist?: string[]
albumArtist?: string[]
album?: string
track?: string
}
export interface TrackData {
artists?: string[]
albumArtists?: string[]
@@ -86,6 +93,7 @@ export interface TrackData {
meta?: {
brainz?: BrainzMeta
spotify?: SpotifyMeta
}
}