diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index 51aa5a03..a1f19189 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -13,7 +13,8 @@ import { childLogger, Logger } from '@foxxmd/logging'; import { projectDir } from './index.js'; import path from 'path'; import { cacheFunctions } from "@foxxmd/regex-buddy-core"; -import { fileOrDirectoryIsWriteable } from '../utils/FSUtils.js'; +import { fileExists, fileOrDirectoryIsWriteable } from '../utils/FSUtils.js'; +import { copyFile } from 'fs/promises'; import { asCacheAuthProvider, asCacheConfig, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js'; import { Typeson } from 'typeson'; import { builtin } from 'typeson-registry'; @@ -384,6 +385,12 @@ export const flatCacheLoad = async (flatCache: FlatCache, logger: Logger = logge throw new Error(`Unable to use path for file cache at ${cachePath}`, { cause: e }) } + if(fileExists(cachePath) && !fileExists(`${cachePath}.bak`)) { + logger.info(`Backing up ${cachePath} in preparation for migration to database...`); + await copyFile(cachePath, `${cachePath}.bak`); + logger.info(`Done! Backed up to ${cachePath}.bak\nAll data has been loaded into cache. It will be deleted (from cache) after migrating to database.\nIf there are migration issues or you wish to downgrade then overwrite ${cachePath} with the .bak backup copy`); + } + const streamPromise = new Promise((resolve, reject) => { flatCache.loadFileStream(cachePath, (progress: number, total: number) => { logger.trace(`Loading ${progress}/${total} chunks...`); diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 5a31e444..ec4d5e87 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -449,14 +449,95 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i protected async doParseCache(): Promise { const cachedQueue = (await this.cache.cacheScrobble.get(`${this.getMachineId()}-queue`) as QueuedScrobble[] ?? []); - const cachedQLength = cachedQueue.length; - //this.queuedScrobbles = cachedQueue.map(x => ({...x, play: rehydratePlay(x.play)})); + if (cachedQueue.length > 0) { + this.logger.info('Migrating cached scrobbles to database...'); + let allGood = true; + for (const cachedQueuedScrobble of cachedQueue) { + const play = asPlay(cachedQueuedScrobble.play); + const { + meta: { + lifecycle, + ...metaRest + }, + } = play; + try { + const res = await this.playRepo.createPlays([ + playToRepositoryCreatePlayOpts({ + play: { + ...play, + meta: { + ...metaRest, + lifecycle: { + steps: [] + } + } + }, + componentId: this.dbComponent.id, + state: 'queued', + parentId: play.id + }) + ]); + this.logger.verbose(`Migrated Play ${res[0].uid} => ${buildTrackString(play)}`); + } catch (e) { + allGood = false; + this.logger.verbose(new Error(`Failed to migrate Play ${buildTrackString(play)}`, {cause: e})); + } + } + this.logger[allGood ? 'info' : 'warn'](allGood ? 'Finished migrating all queued scrobbles.' : 'Migrated queued scrobbles with errors'); + await this.cache.cacheScrobble.delete(`${this.getMachineId()}-queue`); + this.logger.info('Deleted legacy cached queued scrobbles'); + } const cachedDead = (await this.cache.cacheScrobble.get(`${this.getMachineId()}-dead`) as DeadLetterScrobble[] ?? []); - const cachedDLength = cachedDead.length; - //this.deadLetterScrobbles = cachedDead.map(x => ({...x, play: rehydratePlay(x.play), lastRetry: x.lastRetry !== undefined ? dayjs(x.lastRetry) : undefined})); + if(cachedDead.length > 0) { + this.logger.info('Migrating failed scrobbles to database...'); + let allGood = true; + for(const cDeadScrobble of cachedDead) { + const play = asPlay(cDeadScrobble.play); + const { + meta: { + lifecycle, + ...metaRest + }, + } = play; + try { + const res = await this.playRepo.createPlays([ + playToRepositoryCreatePlayOpts({ + play: { + ...play, + meta: { + ...metaRest, + lifecycle: { + steps: [] + } + } + }, + componentId: this.dbComponent.id, + state: 'failed', + parentId: play.id + }) + ]); + this.logger.verbose(`Added Play ${res[0].uid} to database => ${buildTrackString(play)}`); + await this.queueRepo.create({ + componentId: this.dbComponent.id, + playId: res[0].id, + queueName: CLIENT_DEAD_QUEUE, + queueStatus: 'queued', + retries: cDeadScrobble.retries, + error: cDeadScrobble.error !== undefined ? {message: cDeadScrobble.error } : undefined + }); + this.logger.verbose(`Added Play ${res[0].uid} to Failed Queue`); + } catch (e) { + allGood = false; + this.logger.verbose(new Error(`Failed to migrate Play to failed queued ${buildTrackString(play)}`, {cause: e})); + } + this.logger[allGood ? 'info' : 'warn'](allGood ? 'Finished migrating all failed scrobbles.' : 'Migrated failed scrobbles with errors'); + await this.cache.cacheScrobble.delete(`${this.getMachineId()}-dead`); + this.logger.info('Deleted legacy cached failed scrobbles'); + } + } - return `Scrobbles from Cache: ${cachedQLength} Queue | ${cachedDLength} Dead Letter`; + return; } protected async postInitialize(): Promise {