[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(listenbrainz): Improved slug and normal lz endpoint detection

FoxxMD (Mar 13, 2025, 4:53 PM UTC) e74ba52c d3cd07c5

+69 -81
+3 -3
src/backend/server/endpointListenbrainzRoutes.ts
··· 2 2 import { ExpressWithAsync } from "@awaitjs/express"; 3 3 import { childLogger, Logger } from "@foxxmd/logging"; 4 4 import bodyParser from "body-parser"; 5 - import { EndpointListenbrainzSource, playStateFromRequest } from "../sources/EndpointListenbrainzSource.js"; 5 + import { EndpointListenbrainzSource, playStateFromRequest, parseDisplayIdentifiersFromRequest } from "../sources/EndpointListenbrainzSource.js"; 6 6 import { LZEndpointNotifier } from "../sources/ingressNotifiers/LZEndpointNotifier.js"; 7 7 import ScrobbleSources from "../sources/ScrobbleSources.js"; 8 8 import { nonEmptyBody } from "./middleware.js"; ··· 17 17 const nonEmptyCheck = nonEmptyBody(logger, 'LZ Endpoint'); 18 18 19 19 const webhookIngress = new LZEndpointNotifier(logger); 20 - app.useAsync(/\/api\/listenbrainz.*/, 20 + app.useAsync(/(\/api\/listenbrainz.*)|(\/1\/submit-listens)/, 21 21 async function (req, res, next) { 22 22 // track request before parsing body to ensure we at least log that something is happening 23 23 // (in the event body parsing does not work or request is not POST/PATCH) ··· 40 40 41 41 const validSources = sources.filter(x => x.matchRequest(req)); 42 42 if (validSources.length === 0) { 43 - const [slug, token] = EndpointListenbrainzSource.parseDisplayIdentifiersFromRequest(req); 43 + const [slug, token] = parseDisplayIdentifiersFromRequest(req); 44 44 logger.warn(`No Listenbrainz endpoint config matched => Slug: ${slug} | Token: ${token}`); 45 45 } 46 46
+63 -75
src/backend/sources/EndpointListenbrainzSource.ts
··· 15 15 import { parseRegexSingleOrFail } from "../utils.js"; 16 16 import MemorySource from "./MemorySource.js"; 17 17 18 - const noSlugMatch = new RegExp(/\/api\/listenbrainz(?:\/?|\/1\/?|\/1\/submit-listens\/?)$/i); 19 - const slugMatch = new RegExp(/\/api\/listenbrainz\/([^\/]+)(?:\/?|\/1\/?|\/1\/submit-listens\/?)$/i); 18 + const noSlugMatch = new RegExp(/(?:\/api\/listenbrainz\/?)$|(?:\/1\/?|\/1\/submit-listens\/?)$/i); 19 + const slugMatch = new RegExp(/\/api\/listenbrainz\/([^\/]+)$/i); 20 20 21 21 export const authHeaderRegex = new RegExp(/Token (.+)$/i); 22 22 ··· 42 42 }; 43 43 } 44 44 45 - static parseSlugFromString(path: string): string | false | undefined { 46 - const noSlug = parseRegexSingleOrFail(noSlugMatch, path); 47 - if (noSlug !== undefined) { 48 - return undefined; 49 - } 50 - const slugResult = parseRegexSingleOrFail(slugMatch, path); 51 - if (slugResult !== undefined) { 52 - return slugResult.groups[0]; 53 - } 54 - return false; 55 - } 56 - 57 - static parseSlugFromRequest(req: ExpressRequest): string | false | undefined { 58 - return EndpointListenbrainzSource.parseSlugFromString(req.baseUrl); 59 - } 60 - 61 - static parseTokenFromString(str: string): string | undefined { 62 - const tokenMatch = parseRegexSingleOrFail(authHeaderRegex, str); 63 - if(tokenMatch !== undefined) { 64 - return tokenMatch.groups[0]; 65 - } 66 - return undefined; 67 - } 68 - 69 - static parseTokenFromRequest(req: ExpressRequest): string | false | undefined { 70 - const auth = req.header('Authorization'); 71 - if(typeof auth === 'string' && auth !== '') { 72 - const matchedToken = EndpointListenbrainzSource.parseTokenFromString(auth); 73 - if(matchedToken === undefined) { 74 - return false; 75 - } 76 - return matchedToken; 77 - } 78 - return undefined; 79 - } 80 - 81 - static parseIdentifiersFromRequest(req: ExpressRequest): [string | false | undefined, false | string | undefined] { 82 - const slug = EndpointListenbrainzSource.parseSlugFromRequest(req); 83 - const token = EndpointListenbrainzSource.parseTokenFromRequest(req); 84 - 85 - return [slug, token]; 86 - } 87 - 88 - static parseDisplayIdentifiersFromRequest(req: ExpressRequest): [string, string] { 89 - const [slug, token] = EndpointListenbrainzSource.parseIdentifiersFromRequest(req); 90 - let slugStr = '(no slug)'; 91 - if (slug === false) { 92 - slugStr = '(invalid slug)'; 93 - } else if (slug !== undefined) { 94 - slugStr = slug; 95 - } 96 - 97 - let tokenStr = '(no token)'; 98 - if (token === false) { 99 - tokenStr = '(invalid token)'; 100 - } else if (token !== undefined) { 101 - tokenStr = `${token.substring(0,3)}****` 102 - } 103 - return [slugStr, tokenStr]; 104 - } 105 - 106 45 matchRequest(req: ExpressRequest): boolean { 107 46 let matchesToken = this.config.data.token === undefined; 108 - const reqToken = EndpointListenbrainzSource.parseTokenFromRequest(req); 47 + const reqToken = parseTokenFromRequest(req); 109 48 if (reqToken === false) { 110 49 return false; 111 50 } ··· 118 57 } 119 58 120 59 let matchesPath = false; 121 - const slug = EndpointListenbrainzSource.parseSlugFromRequest(req); 60 + const slug = parseSlugFromRequest(req); 122 61 if (slug === false) { 123 62 return false; 124 63 } else { ··· 128 67 return matchesToken && matchesPath; 129 68 } 130 69 131 - static listenTypeAsPlayerStatus(event: string): ReportedPlayerStatus { 132 - switch (event) { 133 - case 'single': 134 - case 'playing_now': 135 - return REPORTED_PLAYER_STATUSES.playing; 136 - default: 137 - return REPORTED_PLAYER_STATUSES.unknown; 138 - } 139 - } 140 - 141 70 static formatPlayObj(obj: ListenPayload, options: FormatPlayObjectOptions & { 142 71 nowPlaying?: boolean 143 72 } = {}): PlayObject { ··· 188 117 default: 189 118 return REPORTED_PLAYER_STATUSES.unknown; 190 119 } 120 + } 121 + 122 + export const parseTokenFromString = (str: string): string | undefined => { 123 + const tokenMatch = parseRegexSingleOrFail(authHeaderRegex, str); 124 + if(tokenMatch !== undefined) { 125 + return tokenMatch.groups[0]; 126 + } 127 + return undefined; 128 + } 129 + 130 + export const parseTokenFromRequest = (req: ExpressRequest): string | false | undefined => { 131 + const auth = req.header('Authorization'); 132 + if(typeof auth === 'string' && auth !== '') { 133 + const matchedToken = parseTokenFromString(auth); 134 + if(matchedToken === undefined) { 135 + return false; 136 + } 137 + return matchedToken; 138 + } 139 + return undefined; 140 + } 141 + 142 + export const parseSlugFromString = (path: string): string | false | undefined => { 143 + const noSlug = parseRegexSingleOrFail(noSlugMatch, path); 144 + if (noSlug !== undefined) { 145 + return undefined; 146 + } 147 + const slugResult = parseRegexSingleOrFail(slugMatch, path); 148 + if (slugResult !== undefined) { 149 + return slugResult.groups[0]; 150 + } 151 + return false; 152 + } 153 + 154 + export const parseSlugFromRequest = (req: ExpressRequest): string | false | undefined => parseSlugFromString(req.baseUrl); 155 + 156 + export const parseIdentifiersFromRequest = (req: ExpressRequest): [string | false | undefined, false | string | undefined] => { 157 + const slug = parseSlugFromRequest(req); 158 + const token = parseTokenFromRequest(req); 159 + 160 + return [slug, token]; 161 + } 162 + 163 + export const parseDisplayIdentifiersFromRequest = (req: ExpressRequest): [string, string] => { 164 + const [slug, token] = parseIdentifiersFromRequest(req); 165 + let slugStr = '(no slug)'; 166 + if (slug === false) { 167 + slugStr = '(invalid slug)'; 168 + } else if (slug !== undefined) { 169 + slugStr = slug; 170 + } 171 + 172 + let tokenStr = '(no token)'; 173 + if (token === false) { 174 + tokenStr = '(invalid token)'; 175 + } else if (token !== undefined) { 176 + tokenStr = `${token.substring(0,3)}****` 177 + } 178 + return [slugStr, tokenStr]; 191 179 }
+3 -3
src/backend/sources/ingressNotifiers/LZEndpointNotifier.ts
··· 1 1 import { Logger } from "@foxxmd/logging"; 2 2 import { Request } from "express"; 3 - import { EndpointListenbrainzSource } from "../EndpointListenbrainzSource.js"; 3 + import { EndpointListenbrainzSource, parseIdentifiersFromRequest } from "../EndpointListenbrainzSource.js"; 4 4 import { IngressNotifier } from "./IngressNotifier.js"; 5 5 6 6 export class LZEndpointNotifier extends IngressNotifier { ··· 14 14 15 15 if(!isRaw) { 16 16 17 - const [slug, token] = EndpointListenbrainzSource.parseIdentifiersFromRequest(req); 17 + const [slug, token] = parseIdentifiersFromRequest(req); 18 18 if(slug === false) { 19 - return [false, `Request URL was not a valid: ${req.baseUrl}`]; 19 + return [false, `Request URL was not valid: ${req.baseUrl}`]; 20 20 } 21 21 const slugStr = slug ?? '(no slug)'; 22 22