mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-05-06 08:10:19 +00:00
feat(tealfm): Add tealfm Source
This commit is contained in:
parent
8110e8e9fa
commit
fa6ed323ff
10 changed files with 179 additions and 25 deletions
|
|
@ -6,5 +6,13 @@
|
|||
"identifier": "myemail@gmail.com",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "myemail@gmail.com",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -32,7 +32,8 @@ export type SourceType =
|
|||
| 'vlc'
|
||||
| 'icecast'
|
||||
| 'azuracast'
|
||||
| 'koito';
|
||||
| 'koito'
|
||||
| 'tealfm';
|
||||
|
||||
export const sourceTypes: SourceType[] = [
|
||||
'spotify',
|
||||
|
|
@ -59,7 +60,8 @@ export const sourceTypes: SourceType[] = [
|
|||
'vlc',
|
||||
'icecast',
|
||||
'azuracast',
|
||||
'koito'
|
||||
'koito',
|
||||
'tealfm'
|
||||
];
|
||||
|
||||
export const isSourceType = (data: string): data is SourceType => {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { RequestRetryOptions } from "../common.js"
|
|||
import { CommonClientConfig, CommonClientData, CommonClientOptions } from "./index.js"
|
||||
|
||||
export interface TealData extends RequestRetryOptions {
|
||||
}
|
||||
|
||||
export interface TealClientData extends TealData, CommonClientData {
|
||||
/**
|
||||
/**
|
||||
* The base URI of the Multi-Scrobbler to use for ATProto OAuth
|
||||
*
|
||||
* Only include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol.
|
||||
|
|
@ -28,9 +25,12 @@ export interface TealClientData extends TealData, CommonClientData {
|
|||
appPassword?: string
|
||||
}
|
||||
|
||||
export interface TealClientData extends TealData, CommonClientData {
|
||||
|
||||
}
|
||||
export interface TealClientConfig extends CommonClientConfig {
|
||||
/**
|
||||
* Should always be `client` when using Koito as a client
|
||||
* Should always be `client` when using Tealfm as a client
|
||||
*
|
||||
* @default client
|
||||
* @examples ["client"]
|
||||
|
|
@ -40,7 +40,7 @@ export interface TealClientConfig extends CommonClientConfig {
|
|||
options?: TealClientOptions
|
||||
}
|
||||
|
||||
export interface TealClientOptions extends CommonClientOptions {
|
||||
export interface TealOptions {
|
||||
/** The [PDS (Personal Data Server)](https://github.com/bluesky-social/pds) to use
|
||||
*
|
||||
* @default "https://bsky.social"
|
||||
|
|
@ -49,6 +49,11 @@ export interface TealClientOptions extends CommonClientOptions {
|
|||
pds?: string
|
||||
}
|
||||
|
||||
|
||||
export interface TealClientOptions extends TealOptions,CommonClientOptions {
|
||||
|
||||
}
|
||||
|
||||
export interface TealClientAIOConfig extends TealClientConfig {
|
||||
type: 'tealfm'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { YTMusicSourceAIOConfig, YTMusicSourceConfig } from "./ytmusic.js";
|
|||
import { IcecastSourceAIOConfig, IcecastSourceConfig } from "./icecast.js";
|
||||
import { KoitoSourceAIOConfig, KoitoSourceConfig } from "./koito.js";
|
||||
import { MalojaSourceAIOConfig, MalojaSourceConfig } from "./maloja.js";
|
||||
import { TealSourceAIOConfig, TealSourceConfig } from "./tealfm.js";
|
||||
|
||||
|
||||
export type SourceConfig =
|
||||
|
|
@ -53,7 +54,8 @@ export type SourceConfig =
|
|||
| VLCSourceConfig
|
||||
| IcecastSourceConfig
|
||||
| AzuracastSourceConfig
|
||||
| KoitoSourceConfig;
|
||||
| KoitoSourceConfig
|
||||
| TealSourceConfig;
|
||||
|
||||
export type SourceAIOConfig =
|
||||
SpotifySourceAIOConfig
|
||||
|
|
@ -83,4 +85,5 @@ export type SourceAIOConfig =
|
|||
| VLCSourceAIOConfig
|
||||
| IcecastSourceAIOConfig
|
||||
| AzuracastSourceAIOConfig
|
||||
| KoitoSourceAIOConfig;
|
||||
| KoitoSourceAIOConfig
|
||||
| TealSourceAIOConfig;
|
||||
|
|
|
|||
27
src/backend/common/infrastructure/config/source/tealfm.ts
Normal file
27
src/backend/common/infrastructure/config/source/tealfm.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { TealData, TealOptions } from "../client/tealfm.js"
|
||||
import { PollingOptions } from "../common.js"
|
||||
import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./index.js"
|
||||
|
||||
|
||||
export interface TealSourceData extends TealData, CommonSourceData, PollingOptions {
|
||||
|
||||
}
|
||||
|
||||
export interface TealSourceConfig extends CommonSourceConfig {
|
||||
/**
|
||||
* Should always be `souce` when using Tealfm as a Source
|
||||
*
|
||||
* @default source
|
||||
* @examples ["source"]
|
||||
* */
|
||||
configureAs?: 'source'
|
||||
data: TealSourceData
|
||||
options?: TealSourceOptions
|
||||
}
|
||||
|
||||
export interface TealSourceOptions extends CommonSourceOptions, TealOptions {
|
||||
}
|
||||
|
||||
export interface TealSourceAIOConfig extends TealSourceConfig {
|
||||
type: 'tealfm'
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ type ParsedConfig = ClientAIOConfig & ConfigMeta;
|
|||
export default class ScrobbleClients {
|
||||
|
||||
/** @type AbstractScrobbleClient[] */
|
||||
clients: (MalojaScrobbler | LastfmScrobbler | TealScrobbler)[] = [];
|
||||
clients: (MalojaScrobbler | LastfmScrobbler | KoitoScrobbler | TealScrobbler)[] = [];
|
||||
logger: Logger;
|
||||
configDir: string;
|
||||
localUrl: URL;
|
||||
|
|
@ -148,7 +148,7 @@ export default class ScrobbleClients {
|
|||
this.logger.error(invalidTypeMsg);
|
||||
continue;
|
||||
}
|
||||
if(['lastfm','listenbrainz','koito','tealfm'].includes(c.type.toLocaleLowerCase()) && ((c as LastfmClientConfig | ListenBrainzClientConfig).configureAs === 'source')) {
|
||||
if(['lastfm','listenbrainz','koito','tealfm'].includes(c.type.toLocaleLowerCase()) && ((c as LastfmClientConfig | ListenBrainzClientConfig | KoitoClientConfig | TealClientConfig).configureAs === 'source')) {
|
||||
this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a source.`);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -287,8 +287,8 @@ export default class ScrobbleClients {
|
|||
continue;
|
||||
}
|
||||
for(const [i,rawConf] of rawClientConfigs.entries()) {
|
||||
if(['lastfm','listenbrainz','koito'].includes(clientType) &&
|
||||
((rawConf as LastfmClientConfig | ListenBrainzClientConfig | KoitoClientConfig).configureAs === 'source'))
|
||||
if(['lastfm','listenbrainz','koito','tealfm'].includes(clientType) &&
|
||||
((rawConf as LastfmClientConfig | ListenBrainzClientConfig | KoitoClientConfig | TealClientConfig).configureAs === 'source'))
|
||||
{
|
||||
this.logger.debug(`Skipping config ${i + 1} from ${clientType}.json because it is configured as a source.`);
|
||||
continue;
|
||||
|
|
@ -381,7 +381,7 @@ ${sources.join('\n')}`);
|
|||
newClient = new KoitoScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as KoitoClientConfig, {}, notifier, this.emitter, this.logger);
|
||||
break;
|
||||
case 'tealfm':
|
||||
newClient = new TealScrobbler(name, {...clientConfig, data: {baseUri: `${this.localUrl}/api/tealfm/${name}`, ...data}} as unknown as TealClientConfig, {}, notifier, this.emitter, this.logger);
|
||||
newClient = new TealScrobbler(name, {...clientConfig, data: {...data}} as unknown as TealClientConfig, {}, notifier, this.emitter, this.logger);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { AbstractBlueSkyApiClient, listRecordToPlay, playToRecord, recordToPlay
|
|||
export default class TealScrobbler extends AbstractScrobbleClient {
|
||||
|
||||
requiresAuth = true;
|
||||
requiresAuthInteraction = true;
|
||||
requiresAuthInteraction = false;
|
||||
|
||||
declare config: TealClientConfig;
|
||||
|
||||
|
|
@ -29,10 +29,10 @@ export default class TealScrobbler extends AbstractScrobbleClient {
|
|||
this.scrobbleDelay = 1500;
|
||||
this.supportsNowPlaying = false;
|
||||
if(config.data.appPassword !== undefined) {
|
||||
this.client = new BlueSkyAppApiClient(name, config.data, {...options, logger});
|
||||
this.client = new BlueSkyAppApiClient(name, {...config.data, pds: config.options?.pds}, {...options, logger});
|
||||
this.requiresAuthInteraction = false;
|
||||
} else if(config.data.baseUri !== undefined) {
|
||||
this.client = new BlueSkyOauthApiClient(name, config.data, {...options, logger});
|
||||
this.client = new BlueSkyOauthApiClient(name, {...config.data, pds: config.options?.pds}, {...options, logger});
|
||||
} else {
|
||||
throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import EventEmitter from "events";
|
||||
import request from "superagent";
|
||||
import { PlayObject, SOURCE_SOT } from "../../core/Atomic.js";
|
||||
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
|
||||
import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js";
|
||||
import { InternalConfig } from "../common/infrastructure/Atomic.js";
|
||||
import { RecentlyPlayedOptions } from "./AbstractSource.js";
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import { KoitoApiClient, listenObjectResponseToPlay } from "../common/vendor/koito/KoitoApiClient.js";
|
||||
import { KoitoSourceConfig } from "../common/infrastructure/config/source/koito.js";
|
||||
import { MalojaApiClient } from "../common/vendor/maloja/MalojaApiClient.js";
|
||||
import { MalojaSourceConfig } from "../common/infrastructure/config/source/maloja.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ import DeezerInternalSource from './DeezerInternalSource.js';
|
|||
import KoitoSource from './KoitoSource.js';
|
||||
import { KoitoSourceConfig } from '../common/infrastructure/config/source/koito.js';
|
||||
import MalojaSource from './MalojaSource.js';
|
||||
import TealfmSource from './TealfmSource.js';
|
||||
import { TealSourceConfig } from '../common/infrastructure/config/source/tealfm.js';
|
||||
|
||||
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
|
||||
|
||||
|
|
@ -209,6 +211,9 @@ export default class ScrobbleSources {
|
|||
case 'koito':
|
||||
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("KoitoSourceConfig");
|
||||
break;
|
||||
case 'tealfm':
|
||||
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("TealSourceConfig");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this.schemaDefinitions[type];
|
||||
|
|
@ -287,7 +292,7 @@ export default class ScrobbleSources {
|
|||
this.logger.error(invalidMsgType);
|
||||
continue;
|
||||
}
|
||||
if(['lastfm','listenbrainz','koito'].includes(c.type.toLocaleLowerCase()) && ((c as LastfmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig).configureAs !== 'source'))
|
||||
if(['lastfm','listenbrainz','koito','tealfm'].includes(c.type.toLocaleLowerCase()) && ((c as LastfmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig | TealSourceConfig).configureAs !== 'source'))
|
||||
{
|
||||
this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a client.`);
|
||||
continue;
|
||||
|
|
@ -703,8 +708,8 @@ export default class ScrobbleSources {
|
|||
continue;
|
||||
}
|
||||
for (const [i,rawConf] of sourceConfigs.entries()) {
|
||||
if(['lastfm','listenbrainz','koito','maloja'].includes(sourceType) &&
|
||||
((rawConf as LastfmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig | MalojaSourceConfig).configureAs !== 'source'))
|
||||
if(['lastfm','listenbrainz','koito','maloja','tealfm'].includes(sourceType) &&
|
||||
((rawConf as LastfmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig | MalojaSourceConfig | TealSourceConfig).configureAs !== 'source'))
|
||||
{
|
||||
this.logger.debug(`Skipping config ${i + 1} from ${sourceType}.json because it is configured as a client.`);
|
||||
continue;
|
||||
|
|
@ -882,6 +887,9 @@ export default class ScrobbleSources {
|
|||
case 'maloja':
|
||||
newSource = await new MalojaSource(name, compositeConfig as MalojaSourceConfig, this.internalConfig, this.emitter);
|
||||
break;
|
||||
case 'tealfm':
|
||||
newSource = await new TealfmSource(name, compositeConfig as TealSourceConfig, this.internalConfig, this.emitter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
104
src/backend/sources/TealfmSource.ts
Normal file
104
src/backend/sources/TealfmSource.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import EventEmitter from "events";
|
||||
import { PlayObject, SOURCE_SOT } from "../../core/Atomic.js";
|
||||
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
|
||||
import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js";
|
||||
import { RecentlyPlayedOptions } from "./AbstractSource.js";
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import { AbstractBlueSkyApiClient, listRecordToPlay, recordToPlay } from "../common/vendor/bluesky/AbstractBlueSkyApiClient.js";
|
||||
import { TealSourceConfig } from "../common/infrastructure/config/source/tealfm.js";
|
||||
import { BlueSkyAppApiClient } from "../common/vendor/bluesky/BlueSkyAppApiClient.js";
|
||||
import { BlueSkyOauthApiClient } from "../common/vendor/bluesky/BlueSkyOauthApiClient.js";
|
||||
import { ListRecord, ScrobbleRecord } from "../common/infrastructure/config/client/tealfm.js";
|
||||
|
||||
export default class TealfmSource extends MemorySource {
|
||||
|
||||
client: AbstractBlueSkyApiClient;
|
||||
requiresAuth = true;
|
||||
requiresAuthInteraction = false;
|
||||
|
||||
declare config: TealSourceConfig;
|
||||
|
||||
constructor(name: any, config: TealSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
|
||||
const {
|
||||
data: {
|
||||
interval = 15,
|
||||
maxInterval = 60,
|
||||
...restData
|
||||
} = {}
|
||||
} = config;
|
||||
super('tealfm', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter);
|
||||
this.canPoll = true;
|
||||
this.canBacklog = true;
|
||||
if(config.data.appPassword !== undefined) {
|
||||
this.client = new BlueSkyAppApiClient(name, {...config.data, pds: config.options?.pds}, {...internal, logger: internal.logger});
|
||||
this.requiresAuthInteraction = false;
|
||||
} else if(config.data.baseUri !== undefined) {
|
||||
this.client = new BlueSkyOauthApiClient(name, {...config.data, pds: config.options?.pds}, {...internal, logger: internal.logger});
|
||||
} else {
|
||||
throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`);
|
||||
}
|
||||
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
|
||||
this.supportsUpstreamRecentlyPlayed = true
|
||||
this.SCROBBLE_BACKLOG_COUNT = 20;
|
||||
}
|
||||
|
||||
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}){ return recordToPlay(obj); }
|
||||
|
||||
protected async doBuildInitData(): Promise<true | string | undefined> {
|
||||
const {
|
||||
data: {
|
||||
identifier,
|
||||
} = {}
|
||||
} = this.config;
|
||||
if (identifier === undefined) {
|
||||
throw new Error('Must provide an identifier');
|
||||
}
|
||||
this.client.initClient();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async doCheckConnection(): Promise<true | string | undefined> {
|
||||
return true;
|
||||
}
|
||||
|
||||
doAuthentication = async () => {
|
||||
try {
|
||||
const sessionRes = await this.client.restoreSession();
|
||||
if(sessionRes) {
|
||||
return true;
|
||||
}
|
||||
if(this.client instanceof BlueSkyAppApiClient) {
|
||||
return await this.client.appLogin();
|
||||
}
|
||||
} catch (e) {
|
||||
if(isNodeNetworkException(e)) {
|
||||
this.logger.error('Could not communicate with ATProto API');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}) => {
|
||||
const {limit = 20} = options;
|
||||
let list: ListRecord<ScrobbleRecord>[];
|
||||
try {
|
||||
list = await this.client.listScrobbleRecord(limit)
|
||||
} catch (e) {
|
||||
throw new Error('Error occurred while trying to fetch records', {cause: e});
|
||||
}
|
||||
this.processRecentPlays([]);
|
||||
const plays = list.map(x => listRecordToPlay(x));
|
||||
return plays;
|
||||
}
|
||||
|
||||
getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
|
||||
try {
|
||||
return await this.getRecentlyPlayed(options);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected getBackloggedPlays = async (options: RecentlyPlayedOptions = {}) => await this.getRecentlyPlayed({formatted: true, ...options})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue