[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.

cache init cleanup

FoxxMD (Mar 30, 2026, 6:50 PM UTC) 972eccd0 fe03bcd6

+54 -19
+34 -19
src/backend/common/Cache.ts
··· 14 14 import path from 'path'; 15 15 import { cacheFunctions } from "@foxxmd/regex-buddy-core"; 16 16 import { fileOrDirectoryIsWriteable } from '../utils/FSUtils.js'; 17 - import { asCacheAuthProvider, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js'; 17 + import { asCacheAuthProvider, asCacheConfig, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js'; 18 18 import { Typeson } from 'typeson'; 19 19 import { builtin } from 'typeson-registry'; 20 20 import { loggerNoop } from './MaybeLogger.js'; ··· 120 120 await this.initMetadataCache(); 121 121 await this.initScrobbleCache(); 122 122 await this.initAuthCache(); 123 - 124 - this.cacheClientScrobbles = await this.initCacheable('Historical Scrobbles', {provider: 'memory', ttl: '2m', lruSize: 50}, {...this.config.metadata, ttl: '10m'}); 125 - this.cacheClientScrobbles.stats.enabled = true; 126 - this.cacheTransform = await this.initCacheable('Historical Scrobbles', {provider: 'memory', ttl: '2m', lruSize: 100}, {...this.config.metadata, ttl: '5m'}); 127 - this.cacheTransform.stats.enabled = true; 128 - this.cacheApi = await this.initCacheable('External API Responses', {provider: 'memory', ttl: '30s', lruSize: 100}, {...this.config.metadata, ttl: '5m'}); 129 - this.cacheApi.stats.enabled = true; 130 123 131 124 if(enableCollectors) { 132 125 this.enableCollectors(); ··· 291 284 292 285 initScrobbleCache = async () => { 293 286 if (!this.hasInit) { 294 - if (!asCacheScrobbleProvider(this.config.scrobble.provider)) { 295 - throw new Error(`Cache Scrobble provider '${this.config.scrobble.provider}' must be one of: memory, valkey, file`); 287 + let scrobbleConfig: CacheConfig | undefined; 288 + try { 289 + if(asCacheConfig(this.config.scrobble)) { 290 + scrobbleConfig = this.config.scrobble; 291 + this.cacheScrobble = await this.initCacheable('Scrobble', this.config.scrobble); 292 + } 293 + } catch (e) { 294 + this.logger.warn(new Error('Could not validate scrobble config! No fallback is possible', {cause: e})); 295 + this.cacheScrobble = await this.initCacheable('Scrobble', {provider: false}); 296 296 } 297 - 298 - this.cacheScrobble = await this.initCacheable('Scrobble', this.config.scrobble); 299 297 } 300 298 } 301 299 302 300 initMetadataCache = async () => { 303 301 if (!this.hasInit) { 304 - if (!asCacheMetadataProvider(this.config.metadata.provider)) { 305 - throw new Error(`Cache Metadata provider '${this.config.metadata.provider}' must be one of: memory, valkey`); 302 + let metadataConfig: CacheConfig | undefined; 303 + try { 304 + if(asCacheConfig(this.config.metadata)) { 305 + metadataConfig = this.config.metadata; 306 + } 307 + } catch (e) { 308 + this.logger.warn(new Error('Could not validate metadata config, will fallback to memory cache only', {cause: e})); 306 309 } 307 - 308 - this.cacheMetadata = await this.initCacheable('Metadata', {provider: 'memory', lruSize: 100, ttl: '3m'}, {...this.config.metadata, ttl: '15m'}); 310 + this.cacheMetadata = await this.initCacheable('Metadata', {provider: 'memory', ttl: '3m', lruSize: 100}, metadataConfig === undefined ? undefined : {...this.config.metadata, ttl: '15m'}); 311 + this.cacheMetadata.stats.enabled = true; 312 + this.cacheClientScrobbles = await this.initCacheable('Historical Scrobbles', {provider: 'memory', ttl: '2m', lruSize: 50}, metadataConfig === undefined ? undefined : {...this.config.metadata, ttl: '10m'}); 313 + this.cacheClientScrobbles.stats.enabled = true; 314 + this.cacheTransform = await this.initCacheable('Transform Data', {provider: 'memory', ttl: '2m', lruSize: 100}, metadataConfig === undefined ? undefined : {...this.config.metadata, ttl: '5m'}); 315 + this.cacheTransform.stats.enabled = true; 316 + this.cacheApi = await this.initCacheable('External API Responses', {provider: 'memory', ttl: '30s', lruSize: 100}, metadataConfig === undefined ? undefined : {...this.config.metadata, ttl: '20m'}); 317 + this.cacheApi.stats.enabled = true; 309 318 } 310 319 } 311 320 312 321 initAuthCache = async () => { 313 322 if (!this.hasInit) { 314 - if (!asCacheAuthProvider(this.config.auth.provider)) { 315 - throw new Error(`Cache Auth provider '${this.config.auth.provider}' must be one of: memory, valkey, file`); 323 + let authConfig: CacheConfig | undefined; 324 + try { 325 + if(asCacheConfig(this.config.scrobble)) { 326 + authConfig = this.config.auth; 327 + } 328 + } catch (e) { 329 + this.logger.warn(new Error('Could not validate auth config! will fallback to memory cache only', {cause: e})); 330 + this.cacheScrobble = await this.initCacheable('Scrobble', {provider: false}); 316 331 } 317 332 318 - this.cacheAuth = await this.initCacheable('Auth', {provider: 'memory', ttl: '3m'}, this.config.auth); 333 + this.cacheAuth = await this.initCacheable('Auth', {provider: 'memory', ttl: '3m'}, authConfig); 319 334 } 320 335 } 321 336
+20
src/backend/common/infrastructure/Atomic.ts
··· 8 8 import { MusicBrainzApi } from 'musicbrainz-api'; 9 9 import { SourceType } from './config/source/sources.js'; 10 10 import { ClientType, clientTypes } from './config/client/clients.js'; 11 + import assert, { AssertionError } from 'assert'; 12 + import { SimpleError } from '../errors/MSErrors.js'; 11 13 12 14 export const lowGranularitySources: SourceType[] = ['subsonic', 'ytmusic']; 13 15 ··· 250 252 return ['memory', 'valkey', 'file'].includes(val); 251 253 } 252 254 return val === false; 255 + } 256 + export const asCacheEphemeralConfig = (val: Record<string, any>): val is (CacheMemoryConfig | CacheNoopConfig) => { 257 + return val.provider === false || val.provider === 'memory'; 258 + } 259 + export const asCacheConnectableConfig = (val: Record<string, any>): val is (CacheValkeyConfig | CacheFileConfig) => { 260 + if(!['valkey', 'file'].includes(val.provider)) { 261 + return false; 262 + } 263 + return typeof val.connection === 'string'; 264 + } 265 + export const asCacheConfig = (val: Record<string, any>): val is CacheConfigType => { 266 + if(!asCacheProvider(val.provider)) { 267 + assert(asCacheProvider(val.provider), `Cache provider must be one of: 'memory', 'valkey', 'file', or false`); 268 + } 269 + if(asCacheConnectableConfig(val) || asCacheEphemeralConfig(val)) { 270 + return true; 271 + } 272 + throw new SimpleError(`${val.provider} must have a connection property that is a string`); 253 273 } 254 274 export const asCacheMetadataProvider = (val: any): val is CacheScrobbleProvider => asCacheProvider(val); 255 275 export type CacheScrobbleProvider = CacheProvider;