import { childLogger, type Logger } from "@foxxmd/logging"; import type EventEmitter from "events"; import normalizeUrl from "normalize-url"; import {COMPONENT_AUTH_TYPE, type ComponentAuthType, type PlayObject} from "../../core/Atomic.ts"; import { buildTrackString, capitalize } from "../../core/StringUtils.ts"; import { isNodeNetworkException } from "../common/errors/NodeErrors.ts"; import type {FormatPlayObjectOptions, TimeRangeListensFetcher} from "../common/infrastructure/Atomic.ts"; import type {MalojaClientConfig} from "../common/infrastructure/config/client/maloja.ts"; import type {MalojaScrobbleRequestData} from "../common/vendor/maloja/interfaces.ts"; import AbstractScrobbleClient from "./AbstractScrobbleClient.ts"; import { MalojaApiClient, formatPlayObj as formatMalojaScrobbleToPlay, playToScrobblePayload } from "../common/vendor/maloja/MalojaApiClient.ts"; import { createGetScrobblesForTimeRangeFunc } from "../utils/ListenFetchUtils.ts"; const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"]; export default class MalojaScrobbler extends AbstractScrobbleClient { override authType: ComponentAuthType = COMPONENT_AUTH_TYPE.unattended; requiresAuth = true; serverVersion: any; webUrl: string; api: MalojaApiClient; getScrobblesForTimeRange: TimeRangeListensFetcher declare config: MalojaClientConfig constructor(name: any, config: MalojaClientConfig, emitter: EventEmitter, logger: Logger) { super('maloja', name, config, emitter, logger); this.api = new MalojaApiClient(name, this.config.data, { logger: childLogger(this.logger, 'API') }); this.MAX_INITIAL_SCROBBLES_FETCH = 100; this.getScrobblesForTimeRange = createGetScrobblesForTimeRangeFunc(this.api, this.api.logger); } formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => formatMalojaScrobbleToPlay(obj, { url: this.webUrl }); protected async doBuildInitData(): Promise { const { data: { url, apiKey } = {} } = this.config; if (apiKey === undefined) { throw new Error("'apiKey' not found in config!"); } if (url === undefined) { throw new Error("Missing 'url' for Maloja config"); } this.webUrl = normalizeUrl(url); return true; } protected async doCheckConnection(): Promise { try { await this.api.testConnection(); await this.api.testHealth(); return true; } catch (e) { throw e; } } doAuthentication = async () => { const { data: { url, apiKey } = {} } = this.config; if (apiKey === undefined) { throw new Error("'apiKey' not found in config!"); } try { await this.api.testAuth(); return true; } catch (e) { if (isNodeNetworkException(e)) { this.logger.error('Could not communicate with Maloja API'); } throw e; } } public playToClientPayload(playObj: PlayObject): MalojaScrobbleRequestData { const { apiKey } = this.config.data; return playToScrobblePayload(playObj); } doScrobble = async (playObj: PlayObject) => { const { meta: { source, newFromSource = false, } = {} } = playObj; const scrobbleData = playToScrobblePayload(playObj); try { const result = await this.api.scrobble(playObj); const scrobbleInfo = `Scrobbled (${newFromSource ? 'New' : 'Backlog'}) => (${source}) ${buildTrackString(playObj)}`; if (result.warnings?.length > 0) { this.logger.warn(`${scrobbleInfo} | ${result.warnings.join(' | ')}`); } else { this.logger.info(scrobbleInfo); } return result; } catch (e) { await this.notify({ title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error' }); throw e; } } }