diff --git a/config/tealfm.json.example b/config/tealfm.json.example
index 352667d2..1413b8c5 100644
--- a/config/tealfm.json.example
+++ b/config/tealfm.json.example
@@ -12,7 +12,11 @@
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
- "appPassword": "twog-phu7-4dhe-y4j3"
+ "appPassword": "twog-phu7-4dhe-y4j3",
+
+ // everything below is optional
+ "serviceAllow": ["plyr"],
+ "serviceDeny": ["last.fm"]
}
}
]
\ No newline at end of file
diff --git a/docsite/docs/configuration/sources/tealfm-source.mdx b/docsite/docs/configuration/sources/tealfm-source.mdx
index 16561696..5b252e07 100644
--- a/docsite/docs/configuration/sources/tealfm-source.mdx
+++ b/docsite/docs/configuration/sources/tealfm-source.mdx
@@ -27,11 +27,82 @@ Follow the **Setup** instructions in the [teal.fm (Client)](/configuration/clien
## Configuration
-See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The only difference for **Source** configuration:
+See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The main differences for **Source** configuration:
* Cannot be setup with ENV config
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
+### Service Filtering
+
+`serviceAllow` and `serviceDeny` are lists that can be used to filter what Plays are scrobbles from teal.fm. These correspond to the base domain of the music service where the Play is from, originally. The values in the list can be a subset of the actual service name (they do not need to be exact matches).
+
+Only one filter can be used at a time. If `serviceAllow` is configured then `serviceDeny` will be ignored.
+
+If a Play does not have an associated music service it can be allowed/denied using the special keyword `unknown`.
+
+If you are unsure what services are being used for your scrobbles you can check this by:
+
+* use [atproto.at](https://atproto.at/) to lookup your DID/handle
+* View the **Raw Data** for the PDS Collection `fm.teal.alpha.feed.play`
+* Check the `musicServiceBaseDomain` property for each record
+
+
+
+Examples
+
+Allowing only scrobbles from [plyr.fm](https://plyr.fm/):
+
+```json title="tealfm.json"
+[
+ {
+ "name": "myTealSource",
+ "configureAs": "source",
+ "data": {
+ "identifier": "alice.bsky.social",
+ "appPassword": "twog-phu7-4dhe-y4j3",
+
+ "serviceAllow": ["plyr"],
+ }
+ }
+]
+```
+
+Deny scrobbles from last.fm:
+
+```json title="tealfm.json"
+[
+ {
+ "name": "myTealSource",
+ "configureAs": "source",
+ "data": {
+ "identifier": "alice.bsky.social",
+ "appPassword": "twog-phu7-4dhe-y4j3",
+
+ "serviceDeny": ["last"],
+ }
+ }
+]
+```
+
+Allow scrobbles from plyr.fm and unknown services:
+
+```json title="tealfm.json"
+[
+ {
+ "name": "myTealSource",
+ "configureAs": "source",
+ "data": {
+ "identifier": "alice.bsky.social",
+ "appPassword": "twog-phu7-4dhe-y4j3",
+
+ "serviceAllow": ["plyr", "unknown"],
+ }
+ }
+]
+```
+
+
+
:::note
You cannot use ENV variables shown in the [teal.fm Client config](/configuration/clients/tealfm#configuration) -- multi-scrobbler assumes teal.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup teal.fm as a Source.
diff --git a/src/backend/common/infrastructure/config/client/tealfm.ts b/src/backend/common/infrastructure/config/client/tealfm.ts
index e33598ff..e2e87662 100644
--- a/src/backend/common/infrastructure/config/client/tealfm.ts
+++ b/src/backend/common/infrastructure/config/client/tealfm.ts
@@ -51,7 +51,10 @@ export interface TealClientOptions extends TealOptions,CommonClientOptions {
export interface TealClientAIOConfig extends TealClientConfig {
type: 'tealfm'
}
-/** https://github.com/teal-fm/piper/blob/main/service/atproto/submission.go */
+/**
+ * https://github.com/teal-fm/teal/blob/main/lexicons/fm.teal.alpha/feed/play.json
+ * https://github.com/teal-fm/teal/blob/main/lexicons/fm.teal.alpha/feed/defs.json
+ * */
export interface ScrobbleRecord {
$type: "fm.teal.alpha.feed.play",
trackName: string,
@@ -60,12 +63,15 @@ export interface ScrobbleRecord {
artists?: {artistName?: string, artistMbId?: string}[]
/** Album name */
releaseName?: string
+ /** A metadata string specifying the user agent where the format is `/ (; ; )` */
submissionClientAgent: string,
musicServiceBaseDomain?: string
// musicbrainz
recordingMbId?: string
releaseMbId?: string
isrc?: string,
+ /** The URL associated with this track */
+ originUrl?: string
[x: string]: unknown
}
diff --git a/src/backend/common/infrastructure/config/source/tealfm.ts b/src/backend/common/infrastructure/config/source/tealfm.ts
index 34701de3..0bf9ab03 100644
--- a/src/backend/common/infrastructure/config/source/tealfm.ts
+++ b/src/backend/common/infrastructure/config/source/tealfm.ts
@@ -4,7 +4,8 @@ import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./ind
export interface TealSourceData extends TealData, CommonSourceData, PollingOptions {
-
+ serviceAllow?: string[]
+ serviceDeny?: string[]
}
export interface TealSourceConfig extends CommonSourceConfig {
diff --git a/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts
index 5eb320db..824258d2 100644
--- a/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts
+++ b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts
@@ -66,7 +66,8 @@ export const playToRecord = (play: PlayObject): ScrobbleRecord => {
playedTime: getScrobbleTsSOCDateWithContext(play)[0].toISOString(),
releaseName: play.data.album,
submissionClientAgent: `multi-scrobbler/${getRoot().items.version}`,
- musicServiceBaseDomain: play.meta.musicService !== undefined ? musicServiceToCononical(play.meta.musicService) : undefined,
+ musicServiceBaseDomain: musicServiceToCononical(play.meta.musicService) ?? play.meta.musicService,
+ isrc: play.data.isrc,
recordingMbId: play.data.meta?.brainz?.track,
releaseMbId: play.data.meta?.brainz?.album
};
@@ -91,6 +92,7 @@ export const recordToPlay = (record: ScrobbleRecord, options: RecordOptions = {}
duration: record.duration,
playDate: dayjs(record.playedTime),
album: record.releaseName,
+ isrc: record.isrc,
meta: {
brainz: {
track: record.recordingMbId,
@@ -102,6 +104,7 @@ export const recordToPlay = (record: ScrobbleRecord, options: RecordOptions = {}
meta: {
source: 'tealfm',
parsedFrom: 'history',
+ musicService: record.musicServiceBaseDomain,
playId: options.playId,
url: {
web: options.web
diff --git a/src/backend/sources/TealfmSource.ts b/src/backend/sources/TealfmSource.ts
index 830c6130..a8f46f44 100644
--- a/src/backend/sources/TealfmSource.ts
+++ b/src/backend/sources/TealfmSource.ts
@@ -9,6 +9,7 @@ import { TealSourceConfig } from "../common/infrastructure/config/source/tealfm.
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";
+import { parseArrayFromMaybeString } from "../utils/StringUtils.js";
export default class TealfmSource extends MemorySource {
@@ -16,6 +17,9 @@ export default class TealfmSource extends MemorySource {
requiresAuth = true;
requiresAuthInteraction = false;
+ serviceAllow: string[]
+ serviceDeny: string[]
+
declare config: TealSourceConfig;
constructor(name: any, config: TealSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
@@ -48,12 +52,26 @@ export default class TealfmSource extends MemorySource {
const {
data: {
identifier,
+ serviceAllow = [],
+ serviceDeny = []
} = {}
} = this.config;
if (identifier === undefined) {
throw new Error('Must provide an identifier');
}
+
await this.client.initClient();
+
+ this.serviceAllow = parseArrayFromMaybeString(serviceAllow, {lower: true});
+ if(this.serviceAllow.length > 0) {
+ this.logger.debug(`Discover Plays from these services only: ${this.serviceAllow.join(',')}`);
+ } else {
+ this.serviceDeny = parseArrayFromMaybeString(serviceDeny, {lower: true});
+ if(this.serviceDeny.length > 0) {
+ this.logger.debug(`Do not discover Plays from these services: ${this.serviceDeny.join(',')}`);
+ }
+ }
+
return true;
}
@@ -96,7 +114,18 @@ export default class TealfmSource extends MemorySource {
throw new Error('Error occurred while trying to fetch records', {cause: e});
}
await this.processRecentPlays([]);
- const plays = list.map(x => listRecordToPlay(x));
+ let plays = list.map(x => listRecordToPlay(x));
+ if(this.serviceAllow.length > 0) {
+ plays = plays.filter(x =>
+ (x.meta.musicService !== undefined && this.serviceAllow.some(y => x.meta.musicService.toLocaleLowerCase().includes(y)))
+ || (x.meta.musicService === undefined && this.serviceAllow.includes('unknown'))
+ );
+ } else if(this.serviceDeny.length > 0) {
+ plays = plays.filter(x =>
+ (x.meta.musicService !== undefined && !this.serviceDeny.some(y => x.meta.musicService.toLocaleLowerCase().includes(y)))
+ || (x.meta.musicService === undefined && !this.serviceAllow.includes('unknown'))
+ );
+ }
return plays;
}