Compare commits

...
9 Commits
Author SHA1 Message Date
FoxxMD 246e9ad7c5 chore(release): Bump version for release 2026-05-06 12:12:52 +00:00
FoxxMD 569c5a8690 fix(spotify): Fix stale state updated at timestamp 2026-05-06 02:56:21 +00:00
FoxxMD 11f7299c0e fix(transform): Fix using cache to return transformed play
Should be caching transformed data instead of play so that "old" plays aren't returned for repeated tracks
2026-05-06 02:40:58 +00:00
Matt Foxx 8a6ae698a1 Merge pull request #587 from FoxxMD/lastfmConnectTimeout
fix(lastfm): Increase connection test timeout for slow networks
2026-05-05 17:49:39 -04:00
FoxxMD d6ed03028d fix(lastfm): Increase connection test timeout for slow networks 2026-05-05 20:59:05 +00:00
FoxxMD 2818d1d275 chore: add lib folder and future db files to gitignore 2026-05-05 16:54:33 +00:00
FoxxMD 8127e4b6ce fix: Use correct gh pages link for replacement 2026-05-05 14:44:00 +00:00
FoxxMD 8e829a53b5 ci: Replace canonical bash command with js script that is more readable and maintainable 2026-05-05 14:39:09 +00:00
FoxxMD 1562f8bffc chore: Fix contributing link in PR template 2026-05-05 12:19:57 +00:00
11 changed files with 79 additions and 15 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
## Checklist before requesting a review
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
- [ ] I have read the [contributing guidelines.](../blob/master/CONTRIBUTING.md)
## Type of change
+2 -3
View File
@@ -39,9 +39,8 @@ jobs:
working-directory: ./docsite
- name: Replace canonical
working-directory: ./docsite/build
# https://stackoverflow.com/a/74211242/1469797
run: grep '<link data-rh="true" rel="canonical" href="https://foxxmd.github.io' . -lr | xargs sed -i 's/<link data-rh="true" rel="canonical" href="https:\/\/foxxmd.github.io\/docs/<link data-rh="true" rel="canonical" href="https:\/\/docs.multi-scrobbler.app/g'
working-directory: ./docsite
run: node --experimental-strip-types canonical-replace.ts ./build
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
+4 -1
View File
@@ -151,4 +151,7 @@ tmp-*
*storybook.log
storybook-static
lib
lib
*.db
*.db.*
*.db-*lib
+13
View File
@@ -1,6 +1,19 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Canonical Replace",
"program": "${workspaceFolder}/docsite/canonical-replace.ts",
"runtimeArgs": ["--experimental-strip-types"],
"args": [
"${workspaceFolder}/docsite/build"
],
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
{
"name": "dev",
"type": "node",
+1
View File
@@ -18,3 +18,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lib
+42
View File
@@ -0,0 +1,42 @@
import fs from 'node:fs/promises';
import path from 'node:path';
var args = process.argv.slice(2);
const linkRegex = new RegExp(/<link data-rh="true" rel="\w+"\shref="(https:\/\/foxxmd\.github\.io\/multi-scrobbler)/g,);
const replacement = process.env.CANONICAL_HREF ?? 'https://docs.multi-scrobbler.app';
/**
* Replace canonical and alternative <link> nodes with the "real" domain of the site
* so that SEO (google) chooses the correct domain when showing search results
*
*/
(async function () {
if(replacement === undefined || replacement === '') {
console.warn('No replacement value found in process.env.CANONICAL_HREF');
return;
}
const buildDir = path.resolve(args[0]);
console.log(`Reading dir recursively ${buildDir}`);
const files = (await fs.readdir(buildDir, { recursive: true })).filter(x => x.includes('.html'));
console.log(`Found ${files.length} files with .html extensions`);
let modifications = 0;
await Promise.all(files.map(async (x) => {
const filePath = path.resolve(path.join(buildDir, x));
//console.log(`Replacing at ${filePath}`);
await fs.writeFile(filePath, (await fs.readFile(filePath)).toString().replace(linkRegex, (match, capture, offset, string, groups) => {
// this may not be fully accurate since we're mutating concurrently/async
// but its a good enough signal for logging to tell if replacements happened
modifications++;
return match.replace(capture, replacement);
}));
}));
console.log(`Done with ${modifications} replacements`);
}());
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "multi-scrobbler",
"version": "0.13.2",
"version": "0.13.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-scrobbler",
"version": "0.13.2",
"version": "0.13.3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "multi-scrobbler",
"version": "0.13.2",
"version": "0.13.3",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
@@ -61,12 +61,13 @@ export default abstract class AbstractTransformer<T = any, Y extends StageConfig
protected abstract doParseConfig(data: StageConfig): Y;
public async handle(data: Y, play: PlayObject): Promise<PlayObject> {
const cacheKey = `${this.configHash}-${hashObject(data)}-${hashObject(playContentInvariantTransform(play))}`
const cacheKey = `transformResult-${this.configHash}-${hashObject(data)}-${hashObject(playContentInvariantTransform(play))}`
try {
const cachedTransform = await this.cache.get<PlayObject>(cacheKey);
if(cachedTransform !== undefined) {
const cachedTransformData = await this.cache.get<T>(cacheKey);
if(cachedTransformData !== undefined) {
this.logger.debug('Transform cache hit');
return cachedTransform;
const transformed = await this.doHandle(data, play, cachedTransformData);
return transformed;
}
} catch (e) {
this.logger.warn(new Error(`Could not fetch cache key ${cacheKey}`, {cause: e}));
@@ -106,7 +107,7 @@ export default abstract class AbstractTransformer<T = any, Y extends StageConfig
}
const transformed = await this.doHandle(data, play, transformData);
await this.cache.set(cacheKey, transformed, this.config.options?.ttl ?? '15s');
await this.cache.set(cacheKey, transformData, this.config.options?.ttl ?? '15s');
return transformed;
}
+6 -1
View File
@@ -18,6 +18,7 @@ import { IncomingMessage } from "http";
import { baseFormatPlayObj } from "../../utils/PlayTransformUtils.js";
import { ScrobbleSubmitError, SimpleError } from "../errors/MSErrors.js";
import { redactString } from "@foxxmd/redact-string";
import dns from 'node:dns/promises';
const badErrors = [
'api key suspended',
@@ -226,7 +227,11 @@ export default class LastfmApiClient extends AbstractApiClient implements Pagina
testConnection = async() => {
try {
await isPortReachableConnect(this.url.port, { host: this.url.url.hostname });
this.logger.trace(`Looking up IP for ${this.url.url.hostname}`);
const resolved = await dns.lookup(this.url.url.hostname);
this.logger.trace(`${this.url.url.hostname} resolved to ${resolved.address}`);
this.logger.trace(`Checking if ${this.url.url.hostname}:${this.url.port} is reachable...`);
await isPortReachableConnect(this.url.port, { host: this.url.url.hostname, timeout: 2000 });
this.logger.verbose(`${this.url.url.hostname}:${this.url.port} is reachable.`);
return true;
} catch (e) {
+1 -1
View File
@@ -520,7 +520,7 @@ export default class SpotifySource extends MemoryPositionalSource implements Pag
platformId: [combinePartsToString([shortDeviceId(device.id), device.name]), NO_USER],
status,
play: item !== null && item !== undefined ? SpotifySource.formatPlayObj(res.body, {newFromSource: true}) : undefined,
stateUpdatedAt: dayjs(timestamp),
stateUpdatedAt: dayjs(),
position: progress_ms !== null && progress_ms !== undefined ? progress_ms / 1000 : undefined,
}
}