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

refactor: reorganized and consolidate source/client types

FoxxMD (Mar 13, 2026, 2:52 PM EDT) c472b4e3 f6c3fc6b

+169 -148
+1 -1
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 22 22 ARTIST_WEIGHT, 23 23 Authenticatable, 24 24 CALCULATED_PLAYER_STATUSES, 25 - ClientType, 26 25 DEFAULT_RETRY_MULTIPLIER, 27 26 DUP_SCORE_THRESHOLD, 28 27 FormatPlayObjectOptions, ··· 34 33 TimeRangeListensFetcher, 35 34 TITLE_WEIGHT, 36 35 } from "../common/infrastructure/Atomic.js"; 36 + import { ClientType } from '../common/infrastructure/config/client/clients.js'; 37 37 import { CommonClientConfig, NowPlayingOptions, UpstreamRefreshOptions } from "../common/infrastructure/config/client/index.js"; 38 38 import { TRANSFORM_HOOK } from "../common/infrastructure/Transform.js"; 39 39 import { Notifiers } from "../notifier/Notifiers.js";
+2 -1
src/backend/scrobblers/ScrobbleClients.ts
··· 2 2 import { childLogger, Logger } from '@foxxmd/logging'; 3 3 import dayjs, { Dayjs } from "dayjs"; 4 4 import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js"; 5 - import { ClientType, clientTypes, ConfigMeta, InternalConfig, InternalConfigOptional, isClientType, REPORTED_PLAYER_STATUSES, SourceIdentifier } from "../common/infrastructure/Atomic.js"; 5 + import { ConfigMeta, InternalConfig, InternalConfigOptional, isClientType, SourceIdentifier } from "../common/infrastructure/Atomic.js"; 6 + import { clientTypes, ClientType } from '../common/infrastructure/config/client/clients.js'; 6 7 import { AIOConfig } from "../common/infrastructure/config/aioConfig.js"; 7 8 import { ClientAIOConfig, ClientConfig } from "../common/infrastructure/config/client/clients.js"; 8 9 import { LastfmClientConfig } from "../common/infrastructure/config/client/lastfm.js";
+2 -3
src/backend/sources/AbstractSource.ts
··· 2 2 import dayjs, { Dayjs } from "dayjs"; 3 3 import { EventEmitter } from "events"; 4 4 import { FixedSizeList } from "fixed-size-list"; 5 - import { PlayObject, TA_CLOSE, TA_DEFAULT_ACCURACY } from "../../core/Atomic.js"; 5 + import { PlayObject } from "../../core/Atomic.js"; 6 6 import { buildTrackString, capitalize, truncateStringToLength } from "../../core/StringUtils.js"; 7 7 import AbstractComponent from "../common/AbstractComponent.js"; 8 8 import { ··· 18 18 PlayUserId, 19 19 ProgressAwarePlayObject, 20 20 SINGLE_USER_PLATFORM_ID, 21 - SourceType, 22 21 } from "../common/infrastructure/Atomic.js"; 23 - import { SourceConfig } from "../common/infrastructure/config/source/sources.js"; 22 + import { SourceType, SourceConfig } from '../common/infrastructure/config/source/sources.js'; 24 23 import { TRANSFORM_HOOK } from "../common/infrastructure/Transform.js"; 25 24 import TupleMap from "../common/TupleMap.js"; 26 25 import {
+2 -1
src/backend/sources/LastfmSource.ts
··· 1 1 import EventEmitter from "events"; 2 2 import { PlayObject, SOURCE_SOT } from "../../core/Atomic.js"; 3 - import { FormatPlayObjectOptions, InternalConfig, PlayPlatformId, SourceType, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js"; 3 + import { FormatPlayObjectOptions, InternalConfig, PlayPlatformId, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js"; 4 + import { SourceType } from '../common/infrastructure/config/source/sources.js'; 4 5 import { LastfmSourceConfig } from "../common/infrastructure/config/source/lastfm.js"; 5 6 import LastfmApiClient, { formatPlayObj } from "../common/vendor/LastfmApiClient.js"; 6 7 import { sortByOldestPlayDate } from "../utils.js";
+2 -3
src/backend/sources/MemoryPositionalSource.ts
··· 1 1 import { Logger } from "@foxxmd/logging"; 2 - import { InternalConfig, PlayerStateDataMaybePlay, PlayPlatformId, SourceType } from "../common/infrastructure/Atomic.js"; 2 + import { InternalConfig, PlayPlatformId } from "../common/infrastructure/Atomic.js"; 3 3 import MemorySource from "./MemorySource.js"; 4 4 import { PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js"; 5 5 import { PositionalPlayerState } from "./PlayerState/PositionalPlayerState.js"; 6 - import { PlayObject } from "../../core/Atomic.js"; 7 - import { SourceConfig } from "../common/infrastructure/config/source/sources.js"; 6 + import { SourceConfig, SourceType } from "../common/infrastructure/config/source/sources.js"; 8 7 import EventEmitter from "events"; 9 8 10 9 export class MemoryPositionalSource extends MemorySource {
+2 -4
src/backend/sources/MemorySource.ts
··· 9 9 CALCULATED_PLAYER_STATUSES, 10 10 InternalConfig, PlayerStateDataMaybePlay, 11 11 PlayPlatformId, 12 - ProgressAwarePlayObject, 13 - SourceType 14 - } from "../common/infrastructure/Atomic.js"; 12 + ProgressAwarePlayObject} from "../common/infrastructure/Atomic.js"; 13 + import { SourceType, SourceConfig } from '../common/infrastructure/config/source/sources.js'; 15 14 import { PollingOptions } from "../common/infrastructure/config/common.js"; 16 - import { SourceConfig } from "../common/infrastructure/config/source/sources.js"; 17 15 import { 18 16 formatNumber, 19 17 genGroupId,
+4 -1
src/backend/sources/ScrobbleSources.ts
··· 1 1 /* eslint-disable no-case-declarations */ 2 2 import { childLogger, Logger } from '@foxxmd/logging'; 3 3 import EventEmitter from "events"; 4 - import { ConfigMeta, InternalConfig, InternalConfigOptional, isSourceType, SourceType, sourceTypes } from "../common/infrastructure/Atomic.js"; 4 + import { ConfigMeta, InternalConfig, InternalConfigOptional } from "../common/infrastructure/Atomic.js"; 5 + import { isSourceType } from '../common/infrastructure/config/source/sources.js'; 6 + import { sourceTypes } from '../common/infrastructure/config/source/sources.js'; 7 + import { SourceType } from '../common/infrastructure/config/source/sources.js'; 5 8 import { AIOConfig, SourceDefaults } from "../common/infrastructure/config/aioConfig.js"; 6 9 import { AzuracastData, AzuracastSourceConfig } from "../common/infrastructure/config/source/azuracast.js"; 7 10 import { ChromecastSourceConfig } from "../common/infrastructure/config/source/chromecast.js";
+2 -1
src/backend/utils/SchemaAppStaticUtils.ts
··· 2 2 import { resolve } from "path"; 3 3 import { projectDir } from "../common/index.js"; 4 4 import {getTypeSchemaFromConfigGenerator} from "./SchemaUtils.js"; 5 - import { clientInterfaces, sourceInterfaces } from "../common/infrastructure/Atomic.js"; 5 + import { clientInterfaces } from '../common/infrastructure/config/client/clients.js'; 6 + import { sourceInterfaces } from '../common/infrastructure/config/source/sources.js'; 6 7 7 8 for(const inter of sourceInterfaces) { 8 9 const schema = getTypeSchemaFromConfigGenerator(inter);
+2 -129
src/backend/common/infrastructure/Atomic.ts
··· 7 7 import { isPlayObject, PlayMeta, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless, UnixTimestamp } from "../../../core/Atomic.js"; 8 8 import TupleMap from "../TupleMap.js"; 9 9 import { MusicBrainzApi } from 'musicbrainz-api'; 10 - 11 - export type SourceType = 12 - 'spotify' 13 - | 'plex' 14 - | 'subsonic' 15 - | 'jellyfin' 16 - | 'lastfm' 17 - | 'librefm' 18 - | 'deezer' 19 - | 'endpointlz' 20 - | 'endpointlfm' 21 - | 'ytmusic' 22 - | 'mpris' 23 - | 'mopidy' 24 - | 'musiccast' 25 - | 'listenbrainz' 26 - | 'jriver' 27 - | 'kodi' 28 - | 'webscrobbler' 29 - | 'chromecast' 30 - | 'maloja' 31 - | 'musikcube' 32 - | 'mpd' 33 - | 'vlc' 34 - | 'icecast' 35 - | 'azuracast' 36 - | 'koito' 37 - | 'tealfm' 38 - | 'rocksky' 39 - | 'sonos'; 40 - 41 - export const sourceTypes: SourceType[] = [ 42 - 'spotify', 43 - 'plex', 44 - 'subsonic', 45 - 'jellyfin', 46 - 'lastfm', 47 - 'librefm', 48 - 'deezer', 49 - 'endpointlz', 50 - 'endpointlfm', 51 - 'ytmusic', 52 - 'mpris', 53 - 'mopidy', 54 - 'musiccast', 55 - 'listenbrainz', 56 - 'jriver', 57 - 'kodi', 58 - 'webscrobbler', 59 - 'chromecast', 60 - 'maloja', 61 - 'musikcube', 62 - 'mpd', 63 - 'vlc', 64 - 'icecast', 65 - 'azuracast', 66 - 'koito', 67 - 'tealfm', 68 - 'rocksky', 69 - 'sonos' 70 - ]; 71 - 72 - export const sourceInterfaces = [ 73 - 'AIOSourceRelaxedConfig', 74 - 'SpotifySourceConfig', 75 - 'PlexCompatConfig', 76 - 'DeezerCompatConfig', 77 - 'ListenbrainzEndpointSourceConfig', 78 - 'LastFMEndpointSourceConfig', 79 - 'IcecastSourceConfig', 80 - 'SubSonicSourceConfig', 81 - 'JellyfinCompatConfig', 82 - 'LastfmSourceConfig', 83 - 'LibrefmSourceConfig', 84 - 'YTMusicSourceConfig', 85 - 'MalojaSourceConfig', 86 - 'MPRISSourceConfig', 87 - 'MopidySourceConfig', 88 - 'ListenBrainzSourceConfig', 89 - 'JRiverSourceConfig', 90 - 'KodiSourceConfig', 91 - 'ChromecastSourceConfig', 92 - 'WebScrobblerSourceConfig', 93 - 'MusikcubeSourceConfig', 94 - 'MusicCastSourceConfig', 95 - 'MPDSourceConfig', 96 - 'VLCSourceConfig', 97 - 'AzuracastSourceConfig', 98 - 'KoitoSourceConfig', 99 - 'TealSourceConfig', 100 - 'RockskySourceConfig', 101 - 'SonosSourceConfig' 102 - ] 103 - 104 - export const isSourceType = (data: string): data is SourceType => { 105 - return sourceTypes.includes(data as SourceType); 106 - } 10 + import { SourceType } from './config/source/sources.js'; 11 + import { ClientType, clientTypes } from './config/client/clients.js'; 107 12 108 13 export const lowGranularitySources: SourceType[] = ['subsonic', 'ytmusic']; 109 - 110 - export type ClientType = 111 - 'maloja' 112 - | 'lastfm' 113 - | 'librefm' 114 - | 'listenbrainz' 115 - | 'koito' 116 - | 'tealfm' 117 - | 'rocksky' 118 - | 'discord'; 119 - export const clientTypes: ClientType[] = [ 120 - 'maloja', 121 - 'lastfm', 122 - 'librefm', 123 - 'listenbrainz', 124 - 'koito', 125 - 'tealfm', 126 - 'rocksky', 127 - 'discord' 128 - ]; 129 - 130 - export const clientInterfaces = [ 131 - 'AIOClientRelaxedConfig', 132 - 'MalojaClientConfig', 133 - 'LastfmClientConfig', 134 - 'LibrefmClientConfig', 135 - 'ListenBrainzClientConfig', 136 - 'KoitoClientConfig', 137 - 'TealClientConfig', 138 - 'RockSkyClientConfig', 139 - 'DiscordClientConfig' 140 - ]; 141 14 142 15 export const isClientType = (data: string): data is ClientType => { 143 16 return clientTypes.includes(data as ClientType);
+2 -1
src/backend/tests/config/config.test.ts
··· 9 9 import ScrobbleSources from '../../sources/ScrobbleSources.js'; 10 10 import EventEmitter from "events"; 11 11 import {loggerTest, loggerDebug} from '@foxxmd/logging'; 12 - import { clientTypes, SourceType, sourceTypes } from '../../common/infrastructure/Atomic.js'; 12 + import { clientTypes } from '../../common/infrastructure/config/client/clients.js'; 13 + import { sourceTypes } from '../../common/infrastructure/config/source/sources.js'; 13 14 import { Notifiers } from '../../notifier/Notifiers.js'; 14 15 import { difference, intersect } from '../../utils.js'; 15 16
+1 -1
src/backend/common/infrastructure/config/aioConfig.ts
··· 5 5 import { WebhookConfig } from "./health/webhooks.js"; 6 6 import { CommonSourceOptions, SourceRetryOptions } from "./source/index.js"; 7 7 import { SourceAIOConfig } from "./source/sources.js"; 8 - import { CacheConfigOptions, ClientType, SourceType } from "../Atomic.js"; 8 + import { CacheConfigOptions } from "../Atomic.js"; 9 9 import { TransformerCommonConfig } from "../../../../core/Atomic.js"; 10 10 11 11
+49 -2
src/backend/common/infrastructure/config/client/clients.ts
··· 7 7 import { LibrefmClientConfig, LibrefmClientAIOConfig } from "./librefm.js"; 8 8 import { DiscordClientAIOConfig, DiscordClientConfig } from "./discord.js"; 9 9 10 - export type ClientConfig = MalojaClientConfig | LastfmClientConfig | LibrefmClientConfig | ListenBrainzClientConfig | KoitoClientConfig | TealClientConfig | RockSkyClientConfig | DiscordClientConfig; 10 + export type ClientConfig = MalojaClientConfig 11 + | LastfmClientConfig 12 + | LibrefmClientConfig 13 + | ListenBrainzClientConfig 14 + | KoitoClientConfig 15 + | TealClientConfig 16 + | RockSkyClientConfig 17 + | DiscordClientConfig; 11 18 12 - export type ClientAIOConfig = MalojaClientAIOConfig | LastfmClientAIOConfig | LibrefmClientAIOConfig | ListenBrainzClientAIOConfig | KoitoClientAIOConfig | TealClientAIOConfig | RockSkyClientAIOConfig | DiscordClientAIOConfig; 19 + export type ClientAIOConfig = MalojaClientAIOConfig 20 + | LastfmClientAIOConfig 21 + | LibrefmClientAIOConfig 22 + | ListenBrainzClientAIOConfig 23 + | KoitoClientAIOConfig 24 + | TealClientAIOConfig 25 + | RockSkyClientAIOConfig 26 + | DiscordClientAIOConfig; 27 + 28 + export const clientInterfaces = [ 29 + 'AIOClientRelaxedConfig', 30 + 'MalojaClientConfig', 31 + 'LastfmClientConfig', 32 + 'LibrefmClientConfig', 33 + 'ListenBrainzClientConfig', 34 + 'KoitoClientConfig', 35 + 'TealClientConfig', 36 + 'RockSkyClientConfig', 37 + 'DiscordClientConfig' 38 + ]; 39 + 40 + export type ClientType = 41 + 'maloja' 42 + | 'lastfm' 43 + | 'librefm' 44 + | 'listenbrainz' 45 + | 'koito' 46 + | 'tealfm' 47 + | 'rocksky' 48 + | 'discord'; 49 + 50 + export const clientTypes: ClientType[] = [ 51 + 'maloja', 52 + 'lastfm', 53 + 'librefm', 54 + 'listenbrainz', 55 + 'koito', 56 + 'tealfm', 57 + 'rocksky', 58 + 'discord' 59 + ];
+98
src/backend/common/infrastructure/config/source/sources.ts
··· 89 89 | TealSourceAIOConfig 90 90 | RockskySourceAIOConfig 91 91 | SonosSourceAIOConfig; 92 + 93 + export type SourceType = 94 + 'spotify' 95 + | 'plex' 96 + | 'subsonic' 97 + | 'jellyfin' 98 + | 'lastfm' 99 + | 'librefm' 100 + | 'deezer' 101 + | 'endpointlz' 102 + | 'endpointlfm' 103 + | 'ytmusic' 104 + | 'mpris' 105 + | 'mopidy' 106 + | 'musiccast' 107 + | 'listenbrainz' 108 + | 'jriver' 109 + | 'kodi' 110 + | 'webscrobbler' 111 + | 'chromecast' 112 + | 'maloja' 113 + | 'musikcube' 114 + | 'mpd' 115 + | 'vlc' 116 + | 'icecast' 117 + | 'azuracast' 118 + | 'koito' 119 + | 'tealfm' 120 + | 'rocksky' 121 + | 'sonos'; 122 + 123 + export const sourceTypes: SourceType[] = [ 124 + 'spotify', 125 + 'plex', 126 + 'subsonic', 127 + 'jellyfin', 128 + 'lastfm', 129 + 'librefm', 130 + 'deezer', 131 + 'endpointlz', 132 + 'endpointlfm', 133 + 'ytmusic', 134 + 'mpris', 135 + 'mopidy', 136 + 'musiccast', 137 + 'listenbrainz', 138 + 'jriver', 139 + 'kodi', 140 + 'webscrobbler', 141 + 'chromecast', 142 + 'maloja', 143 + 'musikcube', 144 + 'mpd', 145 + 'vlc', 146 + 'icecast', 147 + 'azuracast', 148 + 'koito', 149 + 'tealfm', 150 + 'rocksky', 151 + 'sonos' 152 + ]; 153 + 154 + export const sourceInterfaces = [ 155 + 'AIOSourceRelaxedConfig', 156 + 'SpotifySourceConfig', 157 + 'PlexCompatConfig', 158 + 'DeezerCompatConfig', 159 + 'ListenbrainzEndpointSourceConfig', 160 + 'LastFMEndpointSourceConfig', 161 + 'IcecastSourceConfig', 162 + 'SubSonicSourceConfig', 163 + 'JellyfinCompatConfig', 164 + 'LastfmSourceConfig', 165 + 'LibrefmSourceConfig', 166 + 'YTMusicSourceConfig', 167 + 'MalojaSourceConfig', 168 + 'MPRISSourceConfig', 169 + 'MopidySourceConfig', 170 + 'ListenBrainzSourceConfig', 171 + 'JRiverSourceConfig', 172 + 'KodiSourceConfig', 173 + 'ChromecastSourceConfig', 174 + 'WebScrobblerSourceConfig', 175 + 'MusikcubeSourceConfig', 176 + 'MusicCastSourceConfig', 177 + 'MPDSourceConfig', 178 + 'VLCSourceConfig', 179 + 'AzuracastSourceConfig', 180 + 'KoitoSourceConfig', 181 + 'TealSourceConfig', 182 + 'RockskySourceConfig', 183 + 'SonosSourceConfig' 184 + ]; 185 + 186 + export const isSourceType = (data: string): data is SourceType => { 187 + return sourceTypes.includes(data as SourceType); 188 + }; 189 +