mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Update scrobble logic
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ node_modules
|
|||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
log.txt
|
||||||
133
src/server.ts
133
src/server.ts
@@ -605,73 +605,92 @@ function server(
|
|||||||
// Sonos Reporting Endpoint for playback analytics
|
// Sonos Reporting Endpoint for playback analytics
|
||||||
app.post("/report/:version/timePlayed", async (req, res) => {
|
app.post("/report/:version/timePlayed", async (req, res) => {
|
||||||
const version = req.params["version"];
|
const version = req.params["version"];
|
||||||
logger.debug(`Received Sonos reporting event (v${version}):`, JSON.stringify(req.body));
|
logger.debug(`Received Sonos reporting event (v${version}): ${JSON.stringify(req.body)}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
// Sonos may send an array of reports
|
||||||
reportId,
|
const reports = Array.isArray(req.body) ? req.body : [req.body];
|
||||||
mediaUrl,
|
|
||||||
durationPlayedMillis,
|
|
||||||
positionMillis,
|
|
||||||
type,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// Extract track ID from mediaUrl (format: /stream/track/{id})
|
for (const report of reports) {
|
||||||
const trackIdMatch = mediaUrl?.match(/\/stream\/track\/([^?]+)/);
|
const {
|
||||||
if (!trackIdMatch) {
|
reportId,
|
||||||
logger.warn(`Could not extract track ID from mediaUrl: ${mediaUrl}`);
|
mediaUrl,
|
||||||
return res.status(200).json({ status: "ok" });
|
durationPlayedMillis,
|
||||||
}
|
positionMillis,
|
||||||
|
type,
|
||||||
|
} = report;
|
||||||
|
|
||||||
const trackId = trackIdMatch[1];
|
// Extract track ID from mediaUrl (format: /stream/track/{id} or x-sonos-http:track%3a{id}.mp3)
|
||||||
const durationPlayedSeconds = Math.floor((durationPlayedMillis || 0) / 1000);
|
let trackId: string | undefined;
|
||||||
|
|
||||||
logger.info(
|
if (mediaUrl) {
|
||||||
`Sonos reporting: type=${type}, trackId=${trackId}, reportId=${reportId}, ` +
|
// Try standard URL format first
|
||||||
`durationPlayed=${durationPlayedSeconds}s, position=${positionMillis}ms`
|
const standardMatch = mediaUrl.match(/\/stream\/track\/([^?]+)/);
|
||||||
);
|
if (standardMatch) {
|
||||||
|
trackId = standardMatch[1];
|
||||||
// For "final" reports, determine if we should scrobble
|
} else {
|
||||||
if (type === "final" && durationPlayedSeconds > 0) {
|
// Try x-sonos-http format (track%3a{id}.mp3)
|
||||||
// Extract authentication from request headers or body
|
const sonosMatch = mediaUrl.match(/track%3[aA]([^.?&]+)/);
|
||||||
// Sonos may send credentials in Authorization header or in the request
|
if (sonosMatch) {
|
||||||
const authHeader = req.headers["authorization"];
|
trackId = sonosMatch[1];
|
||||||
let serviceToken: string | undefined;
|
}
|
||||||
|
|
||||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
||||||
const token = authHeader.substring(7);
|
|
||||||
const smapiToken = serverOpts.smapiTokenStore.get(token);
|
|
||||||
if (smapiToken) {
|
|
||||||
serviceToken = pipe(
|
|
||||||
smapiAuthTokens.verify({ token, key: smapiToken.key }),
|
|
||||||
E.getOrElseW(() => undefined)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceToken) {
|
if (!trackId) {
|
||||||
await musicService.login(serviceToken).then((musicLibrary) => {
|
logger.warn(`Could not extract track ID from mediaUrl: ${mediaUrl}, full report: ${JSON.stringify(report)}`);
|
||||||
// Get track duration to determine scrobbling threshold
|
continue; // Skip this report, process next one
|
||||||
return musicLibrary.track(trackId).then((track) => {
|
}
|
||||||
const shouldScrobble =
|
|
||||||
(track.duration < 30 && durationPlayedSeconds >= 10) ||
|
|
||||||
(track.duration >= 30 && durationPlayedSeconds >= 30);
|
|
||||||
|
|
||||||
if (shouldScrobble) {
|
const durationPlayedSeconds = Math.floor((durationPlayedMillis || 0) / 1000);
|
||||||
logger.info(`Scrobbling track ${trackId} after ${durationPlayedSeconds}s playback`);
|
|
||||||
return musicLibrary.scrobble(trackId);
|
logger.info(
|
||||||
} else {
|
`Sonos reporting: type=${type}, trackId=${trackId}, reportId=${reportId}, ` +
|
||||||
logger.debug(
|
`durationPlayed=${durationPlayedSeconds}s, position=${positionMillis}ms`
|
||||||
`Not scrobbling track ${trackId}: duration=${track.duration}s, played=${durationPlayedSeconds}s`
|
);
|
||||||
);
|
|
||||||
return Promise.resolve(false);
|
// For "final" reports, determine if we should scrobble
|
||||||
}
|
if (type === "final" && durationPlayedSeconds > 0) {
|
||||||
|
// Extract authentication from request headers or body
|
||||||
|
// Sonos may send credentials in Authorization header or in the request
|
||||||
|
const authHeader = req.headers["authorization"];
|
||||||
|
let serviceToken: string | undefined;
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const smapiToken = serverOpts.smapiTokenStore.get(token);
|
||||||
|
if (smapiToken) {
|
||||||
|
serviceToken = pipe(
|
||||||
|
smapiAuthTokens.verify({ token, key: smapiToken.key }),
|
||||||
|
E.getOrElseW(() => undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceToken) {
|
||||||
|
await musicService.login(serviceToken).then((musicLibrary) => {
|
||||||
|
// Get track duration to determine scrobbling threshold
|
||||||
|
return musicLibrary.track(trackId!).then((track) => {
|
||||||
|
const shouldScrobble =
|
||||||
|
(track.duration < 30 && durationPlayedSeconds >= 10) ||
|
||||||
|
(track.duration >= 30 && durationPlayedSeconds >= 30);
|
||||||
|
|
||||||
|
if (shouldScrobble) {
|
||||||
|
logger.info(`Scrobbling track ${trackId} after ${durationPlayedSeconds}s playback`);
|
||||||
|
return musicLibrary.scrobble(trackId!);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Not scrobbling track ${trackId}: duration=${track.duration}s, played=${durationPlayedSeconds}s`
|
||||||
|
);
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
logger.error(`Failed to process scrobble for track ${trackId}`, { error: e });
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
} else {
|
||||||
logger.error(`Failed to process scrobble for track ${trackId}`, { error: e });
|
logger.debug("No authentication available for reporting endpoint scrobble");
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
logger.debug("No authentication available for reporting endpoint scrobble");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user