Skip to content

Commit d87ae5f

Browse files
committed
feat(transfer): Add transfer/backfill system for copying scrobble history
1 parent 4b9c315 commit d87ae5f

File tree

16 files changed

+1180
-12
lines changed

16 files changed

+1180
-12
lines changed

docsite/static/aio.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docsite/static/source.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/backend/common/schema/aio-source.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/backend/common/schema/aio.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/backend/common/schema/source.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/backend/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { createVegaGenerator } from './utils/SchemaUtils.js';
2121
import ScrobbleClients from './scrobblers/ScrobbleClients.js';
2222
import ScrobbleSources from './sources/ScrobbleSources.js';
2323
import { Notifiers } from './notifier/Notifiers.js';
24+
import { TransferManager } from './transfer/TransferManager.js';
2425

2526
dayjs.extend(utc)
2627
dayjs.extend(isBetween);
@@ -105,7 +106,9 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
105106

106107
await root.items.cache().init();
107108

108-
initServer(logger, appLoggerStream, output, scrobbleSources, scrobbleClients);
109+
const transferManager = new TransferManager(scrobbleSources, scrobbleClients, logger);
110+
111+
initServer(logger, appLoggerStream, output, scrobbleSources, scrobbleClients, transferManager);
109112

110113
if(process.env.IS_LOCAL === 'true') {
111114
logger.info('multi-scrobbler can be run as a background service! See: https://foxxmd.github.io/multi-scrobbler/docs/installation/service');

src/backend/scrobblers/AbstractScrobbleClient.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,9 @@ ${closestMatch.breakdowns.join('\n')}`, {leaf: ['Dupe Check']});
792792
}
793793
const currQueuedPlay = this.queuedScrobbles.shift();
794794

795-
const [timeFrameValid, timeFrameValidLog] = this.timeFrameIsValid(currQueuedPlay.play);
795+
// Skip timeframe validation for transfers (historical data)
796+
const isTransfer = currQueuedPlay.source?.startsWith('transfer-');
797+
const [timeFrameValid, timeFrameValidLog] = isTransfer ? [true, ''] : this.timeFrameIsValid(currQueuedPlay.play);
796798
if (timeFrameValid && !(await this.alreadyScrobbled(this.transformPlay(currQueuedPlay.play, TRANSFORM_HOOK.preCompare)))) {
797799
const transformedScrobble = this.transformPlay(currQueuedPlay.play, TRANSFORM_HOOK.postCompare);
798800
try {
@@ -877,7 +879,9 @@ ${closestMatch.breakdowns.join('\n')}`, {leaf: ['Dupe Check']});
877879
if (this.getLatestQueuePlayDate() !== undefined && this.scrobblesLastCheckedAt().unix() < this.getLatestQueuePlayDate().unix()) {
878880
await this.refreshScrobbles();
879881
}
880-
const [timeFrameValid, timeFrameValidLog] = this.timeFrameIsValid(deadScrobble.play);
882+
// Skip timeframe validation for transfers (historical data)
883+
const isTransfer = deadScrobble.source?.startsWith('transfer-');
884+
const [timeFrameValid, timeFrameValidLog] = isTransfer ? [true, ''] : this.timeFrameIsValid(deadScrobble.play);
881885
if (timeFrameValid && !(await this.alreadyScrobbled(this.transformPlay(deadScrobble.play, TRANSFORM_HOOK.preCompare)))) {
882886
const transformedScrobble = this.transformPlay(deadScrobble.play, TRANSFORM_HOOK.postCompare);
883887
try {
@@ -938,6 +942,19 @@ ${closestMatch.breakdowns.join('\n')}`, {leaf: ['Dupe Check']});
938942
this.updateQueuedScrobblesCache();
939943
}
940944

945+
cancelQueuedItemsBySource = (source: string): number => {
946+
const beforeMain = this.queuedScrobbles.length;
947+
const beforeDead = this.deadLetterScrobbles.length;
948+
949+
this.queuedScrobbles = this.queuedScrobbles.filter(item => item.source !== source);
950+
this.deadLetterScrobbles = this.deadLetterScrobbles.filter(item => item.source !== source);
951+
952+
this.updateQueuedScrobblesCache();
953+
this.updateDeadLetterCache();
954+
955+
return (beforeMain + beforeDead) - (this.queuedScrobbles.length + this.deadLetterScrobbles.length);
956+
}
957+
941958
protected addDeadLetterScrobble = (data: QueuedScrobble<PlayObject>, error: (Error | string) = 'Unspecified error') => {
942959
let eString = '';
943960
if(typeof error === 'string') {

src/backend/server/api.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { setupTautulliRoutes } from "./tautulliRoutes.js";
3333
import { setupWebscrobblerRoutes } from "./webscrobblerRoutes.js";
3434
import ScrobbleSources from "../sources/ScrobbleSources.js";
3535
import ScrobbleClients from "../scrobblers/ScrobbleClients.js";
36+
import { TransferManager } from "../transfer/TransferManager.js";
37+
import { TransferOptions } from "../transfer/TransferJob.js";
3638

3739
const maxBufferSize = 300;
3840
const output: Record<number, FixedSizeList<LogDataPretty>> = {};
@@ -54,7 +56,7 @@ const getLogs = (minLevel: number, limit: number = maxBufferSize, sort: 'asc' |
5456
return allLogs.flat(1).sort((a, b) => a.time - b.time).slice(0, limit);
5557
}
5658

57-
export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream: PassThrough, initialLogOutput: LogDataPretty[] = [], scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => {
59+
export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream: PassThrough, initialLogOutput: LogDataPretty[] = [], scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients, transferManager: TransferManager) => {
5860
for(const level of Object.keys(logger.levels.labels)) {
5961
output[level] = new FixedSizeList<LeveledLogData>(maxBufferSize);
6062
}
@@ -520,6 +522,49 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream:
520522
return res.json({version: root.get('version')});
521523
});
522524

525+
app.getAsync('/api/transfer/sources-clients', async (req, res) => {
526+
try {
527+
const result = transferManager.getActiveSourcesAndClients();
528+
return res.json(result);
529+
} catch (e) {
530+
logger.error(`Error getting sources and clients: ${e.message}`);
531+
return res.status(500).json({ error: e.message });
532+
}
533+
});
534+
535+
app.postAsync('/api/transfer', bodyParser.json(), async (req, res) => {
536+
try {
537+
const options: TransferOptions = req.body;
538+
const id = await transferManager.startTransfer(options);
539+
return res.json({ id });
540+
} catch (e) {
541+
logger.error(`Error starting transfer: ${e.message}`);
542+
return res.status(400).json({ error: e.message });
543+
}
544+
});
545+
546+
app.getAsync('/api/transfer/:id?', async (req, res) => {
547+
try {
548+
const { id } = req.params;
549+
const result = transferManager.getTransferStatus(id);
550+
return res.json(result);
551+
} catch (e) {
552+
logger.error(`Error getting transfer status: ${e.message}`);
553+
return res.status(404).json({ error: e.message });
554+
}
555+
});
556+
557+
app.deleteAsync('/api/transfer/:id', async (req, res) => {
558+
try {
559+
const { id } = req.params;
560+
transferManager.cancelTransfer(id);
561+
return res.status(200).json({ message: 'Transfer cancelled' });
562+
} catch (e) {
563+
logger.error(`Error cancelling transfer: ${e.message}`);
564+
return res.status(400).json({ error: e.message });
565+
}
566+
});
567+
523568
app.useAsync('/api/*', async (req, res) => {
524569
const remote = req.connection.remoteAddress;
525570
const proxyRemote = req.headers["x-forwarded-for"];

src/backend/server/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import { getAddress } from "../utils/NetworkUtils.js";
1515
import { setupApi } from "./api.js";
1616
import ScrobbleSources from '../sources/ScrobbleSources.js';
1717
import ScrobbleClients from '../scrobblers/ScrobbleClients.js';
18+
import { TransferManager } from '../transfer/TransferManager.js';
1819

1920
const app = addAsync(express());
2021
const router = Router();
2122

22-
export const initServer = async (parentLogger: Logger, appLoggerStream: PassThrough, initialOutput: LogDataPretty[] = [], sources: ScrobbleSources, clients: ScrobbleClients) => {
23+
export const initServer = async (parentLogger: Logger, appLoggerStream: PassThrough, initialOutput: LogDataPretty[] = [], sources: ScrobbleSources, clients: ScrobbleClients, transferManager: TransferManager) => {
2324

2425
const logger = childLogger(parentLogger, 'API'); // parentLogger.child({labels: ['API']}, mergeArr);
2526

@@ -50,7 +51,7 @@ export const initServer = async (parentLogger: Logger, appLoggerStream: PassThro
5051
const local = root.get('localUrl');
5152
const localDefined = root.get('hasDefinedBaseUrl');
5253

53-
setupApi(app, logger, appLoggerStream, initialOutput, sources, clients);
54+
setupApi(app, logger, appLoggerStream, initialOutput, sources, clients, transferManager);
5455

5556
const addy = getAddress();
5657
const addresses: string[] = [];

0 commit comments

Comments
 (0)