@deadair/plugin-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1062 -0
- package/dist/article.parse.d.ts +82 -0
- package/dist/article.parse.d.ts.map +1 -0
- package/dist/boundary.json.safe.d.ts +191 -0
- package/dist/boundary.json.safe.d.ts.map +1 -0
- package/dist/capabilities/analysis.d.ts +330 -0
- package/dist/capabilities/analysis.d.ts.map +1 -0
- package/dist/capabilities/charts.d.ts +134 -0
- package/dist/capabilities/charts.d.ts.map +1 -0
- package/dist/capabilities/enrichment.d.ts +255 -0
- package/dist/capabilities/enrichment.d.ts.map +1 -0
- package/dist/capabilities/llm.d.ts +318 -0
- package/dist/capabilities/llm.d.ts.map +1 -0
- package/dist/capabilities/mixer.d.ts +183 -0
- package/dist/capabilities/mixer.d.ts.map +1 -0
- package/dist/capabilities/music.provider.d.ts +245 -0
- package/dist/capabilities/music.provider.d.ts.map +1 -0
- package/dist/capabilities/news.d.ts +171 -0
- package/dist/capabilities/news.d.ts.map +1 -0
- package/dist/capabilities/scrobble.d.ts +133 -0
- package/dist/capabilities/scrobble.d.ts.map +1 -0
- package/dist/capabilities/search.d.ts +122 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/similarity.d.ts +101 -0
- package/dist/capabilities/similarity.d.ts.map +1 -0
- package/dist/capabilities/speech.d.ts +211 -0
- package/dist/capabilities/speech.d.ts.map +1 -0
- package/dist/capabilities/weather.d.ts +192 -0
- package/dist/capabilities/weather.d.ts.map +1 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/define.plugin.d.ts +56 -0
- package/dist/define.plugin.d.ts.map +1 -0
- package/dist/feed.parse.d.ts +97 -0
- package/dist/feed.parse.d.ts.map +1 -0
- package/dist/html.text.d.ts +71 -0
- package/dist/html.text.d.ts.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1153 -0
- package/dist/index.js.map +1 -0
- package/dist/match.text.d.ts +28 -0
- package/dist/match.text.d.ts.map +1 -0
- package/dist/plugin.api.version.d.ts +9 -0
- package/dist/plugin.api.version.d.ts.map +1 -0
- package/dist/plugin.base.d.ts +80 -0
- package/dist/plugin.base.d.ts.map +1 -0
- package/dist/plugin.config.fields.d.ts +524 -0
- package/dist/plugin.config.fields.d.ts.map +1 -0
- package/dist/plugin.config.read.d.ts +52 -0
- package/dist/plugin.config.read.d.ts.map +1 -0
- package/dist/plugin.error.d.ts +177 -0
- package/dist/plugin.error.d.ts.map +1 -0
- package/dist/plugin.host.d.ts +220 -0
- package/dist/plugin.host.d.ts.map +1 -0
- package/dist/plugin.host.response.d.ts +42 -0
- package/dist/plugin.host.response.d.ts.map +1 -0
- package/dist/plugin.http.d.ts +80 -0
- package/dist/plugin.http.d.ts.map +1 -0
- package/dist/plugin.lifecycle.d.ts +68 -0
- package/dist/plugin.lifecycle.d.ts.map +1 -0
- package/dist/plugin.manifest.d.ts +282 -0
- package/dist/plugin.manifest.d.ts.map +1 -0
- package/dist/plugin.permissions.d.ts +186 -0
- package/dist/plugin.permissions.d.ts.map +1 -0
- package/dist/testing/fake.plugin.host.d.ts +85 -0
- package/dist/testing/fake.plugin.host.d.ts.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +141 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `music-provider` kind. A provider declares any subset of the four
|
|
3
|
+
* sub-capabilities below: `catalog` (it can be searched and browsed), `stream`
|
|
4
|
+
* (it can get the station audio to play), `steer` (it owns its own audio output
|
|
5
|
+
* and takes instructions), and `oauth` (it needs a user to authorise it first).
|
|
6
|
+
* Spotify declares all four; a plain SMB library declares `catalog` and
|
|
7
|
+
* `stream`.
|
|
8
|
+
*
|
|
9
|
+
* Every shape here is JSON-safe. Durations are integers in milliseconds,
|
|
10
|
+
* never `Date` or `Duration` objects.
|
|
11
|
+
*/
|
|
12
|
+
/** A track as a provider describes it. Deliberately minimal: this is v1 of a public API. */
|
|
13
|
+
export interface ProviderTrack {
|
|
14
|
+
/** Provider-scoped identifier. Opaque to the host. */
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Ordered, primary artist first. Empty array if the provider genuinely has none. */
|
|
18
|
+
artists: string[];
|
|
19
|
+
album?: string;
|
|
20
|
+
durationMs?: number;
|
|
21
|
+
/** Recording ISRC, when the provider exposes one. The best cross-provider join key. */
|
|
22
|
+
isrc?: string;
|
|
23
|
+
artworkUrl?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The year this record was first released, as the provider reports it.
|
|
26
|
+
*
|
|
27
|
+
* A four-digit year and never a date: what a station asks of a period is which decade a record
|
|
28
|
+
* belongs to, and a provider that knows the month knows it about THIS release rather than about
|
|
29
|
+
* the recording. Precision nobody can trust is worse than none, so the field is as coarse as the
|
|
30
|
+
* question.
|
|
31
|
+
*
|
|
32
|
+
* **A reissue usually still carries the ORIGINAL year, and that was worth measuring rather than
|
|
33
|
+
* assuming.** The pessimistic reading is that a 2011 remaster is dated 2011 by whoever sells it,
|
|
34
|
+
* which would make a period filter quietly useless for exactly the old records it is most wanted
|
|
35
|
+
* for. Measured on this station's own library after one sync: of 63 tracks whose title names a
|
|
36
|
+
* remaster year, 61 came through dated to the original release and 2 to the reissue — `Dazed and
|
|
37
|
+
* Confused - 1990 Remaster` arrived as 1969 and `Paranoid - 2012 - Remaster` as 1970. So the skew
|
|
38
|
+
* is real, rare, and not a reason to distrust the field.
|
|
39
|
+
*
|
|
40
|
+
* Optional, and absent must read as "the provider did not say" rather than as old or new. A host
|
|
41
|
+
* filtering by period has to treat an unknown year as ELIGIBLE: a record whose year nobody
|
|
42
|
+
* recorded is not evidence of the wrong decade, and dropping it would silently shrink a library
|
|
43
|
+
* to whatever happened to be tagged.
|
|
44
|
+
*
|
|
45
|
+
* It exists because the host was throwing this away. Spotify sends `album.release_date` on every
|
|
46
|
+
* search and playlist row and Subsonic sends `year`, while `deadair.tracks.year` was written only
|
|
47
|
+
* by a much later enrichment pass — so a station could not be asked for a period until something
|
|
48
|
+
* else had gone and looked the same records up.
|
|
49
|
+
*/
|
|
50
|
+
year?: number;
|
|
51
|
+
/**
|
|
52
|
+
* How well known the record is, 0 to 100, when the provider has an opinion.
|
|
53
|
+
*
|
|
54
|
+
* A RANKING and not a fact: providers compute it differently and none of them says how, so the
|
|
55
|
+
* only thing it may be used for is ordering rows from the same search against each other. Never
|
|
56
|
+
* compare it across providers, never show it to a listener, and never gate on a threshold.
|
|
57
|
+
*
|
|
58
|
+
* Optional because most sources have nothing like it — a personal library knows what you own,
|
|
59
|
+
* not what the world plays — and absent must read as "no opinion" rather than as unpopular, or
|
|
60
|
+
* a station with one ranked provider and one unranked would bury the unranked one's whole
|
|
61
|
+
* catalogue.
|
|
62
|
+
*
|
|
63
|
+
* It exists because of a specific failure: asked for "popular rap songs from the USA", the
|
|
64
|
+
* model browsed a genre, got two dozen obscure records back in the provider's own order, and
|
|
65
|
+
* named them. Nothing in the chain could tell a hit from an unknown, so the brief was
|
|
66
|
+
* unservable however well the model behaved. See `CatalogSearchTool`.
|
|
67
|
+
*/
|
|
68
|
+
popularity?: number;
|
|
69
|
+
/**
|
|
70
|
+
* The parental advisory this copy carries, when the provider reports one.
|
|
71
|
+
*
|
|
72
|
+
* A LABEL and not a reading of the words. A provider is passing on the marking a release
|
|
73
|
+
* carries; nothing here has looked at a lyric. That is why the field is not called `lyrics`,
|
|
74
|
+
* which stays free for the text itself — a thing a future enrichment source may genuinely fetch,
|
|
75
|
+
* the way `plugins/wikipedia` fetches article prose.
|
|
76
|
+
*
|
|
77
|
+
* It is about LYRICS rather than about LENGTH, which is the other half of the same confusion. A
|
|
78
|
+
* radio edit is a length cut and may still be explicit, so an edit-length preference is a
|
|
79
|
+
* separate axis and belongs in a separate field on the day something needs one. Do not widen
|
|
80
|
+
* this one to carry it.
|
|
81
|
+
*
|
|
82
|
+
* Optional because most sources have nothing like it — a file on a disk carries no marking, and
|
|
83
|
+
* Subsonic has no such field — and absent must read as "the provider did not say" rather than as
|
|
84
|
+
* clean. A host enforcing a clean-only policy has to demand a positive `'clean'`: treating
|
|
85
|
+
* silence as consent is how a station promises something it cannot deliver.
|
|
86
|
+
*/
|
|
87
|
+
advisory?: 'explicit' | 'clean';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* What a source will let the connected account do with one playlist's items.
|
|
91
|
+
*
|
|
92
|
+
* Both values are item-scoped, mirroring the endpoint pair they describe
|
|
93
|
+
* (Spotify's `GET` and `PUT /playlists/{id}/items`). Neither says anything
|
|
94
|
+
* about the playlist's own name or description: on Spotify a collaborator may
|
|
95
|
+
* change the items and not the details.
|
|
96
|
+
*/
|
|
97
|
+
export type ProviderPlaylistPermission =
|
|
98
|
+
/** The source will return this playlist's items. */
|
|
99
|
+
'read'
|
|
100
|
+
/** The source will permit modifying this playlist's items. */
|
|
101
|
+
| 'edit';
|
|
102
|
+
export interface ProviderPlaylist {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
/** Number of tracks, when the provider reports it cheaply. */
|
|
107
|
+
trackCount?: number;
|
|
108
|
+
artworkUrl?: string;
|
|
109
|
+
/**
|
|
110
|
+
* What the SOURCE permits for the connected account on this playlist, so
|
|
111
|
+
* the host can avoid offering a call that cannot succeed. Not deadair's own
|
|
112
|
+
* authorization for the requesting actor, which is a separate question
|
|
113
|
+
* asked of the permission model, and not a claim that the plugin implements
|
|
114
|
+
* the action either, which is what the manifest's capabilities declare. All
|
|
115
|
+
* three have to hold before an action is worth offering.
|
|
116
|
+
*
|
|
117
|
+
* Absent and empty mean different things, and the difference matters:
|
|
118
|
+
*
|
|
119
|
+
* - `undefined` — the source did not say. Providers with no such split (most
|
|
120
|
+
* of them) leave it alone, and a host must read this as "no reason to
|
|
121
|
+
* think otherwise" rather than hiding the playlist.
|
|
122
|
+
* - `[]` — the source was asked and permits nothing.
|
|
123
|
+
*
|
|
124
|
+
* Spotify populates it because a listing there mixes playlists the account
|
|
125
|
+
* owns with playlists it merely follows, and since the February 2026 Web
|
|
126
|
+
* API changes only the owned half can be read at all.
|
|
127
|
+
*/
|
|
128
|
+
permissions?: ProviderPlaylistPermission[];
|
|
129
|
+
}
|
|
130
|
+
/** A playable stream, plus how long the URL stays good for. */
|
|
131
|
+
export interface ProviderStream {
|
|
132
|
+
url: string;
|
|
133
|
+
/** Unix epoch millis after which `url` must be re-resolved. */
|
|
134
|
+
expiresAt?: number;
|
|
135
|
+
/** e.g. `audio/mpeg`. */
|
|
136
|
+
mimeType?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface SearchTracksOptions {
|
|
139
|
+
limit?: number;
|
|
140
|
+
offset?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Narrow to records released in a period, inclusive, as four-digit years.
|
|
143
|
+
*
|
|
144
|
+
* Either end may stand alone: `yearFrom` with no `yearTo` is "this year onwards".
|
|
145
|
+
*/
|
|
146
|
+
yearFrom?: number;
|
|
147
|
+
yearTo?: number;
|
|
148
|
+
}
|
|
149
|
+
export interface ListPlaylistsOptions {
|
|
150
|
+
limit?: number;
|
|
151
|
+
offset?: number;
|
|
152
|
+
}
|
|
153
|
+
export interface GetPlaylistTracksOptions {
|
|
154
|
+
limit?: number;
|
|
155
|
+
offset?: number;
|
|
156
|
+
}
|
|
157
|
+
/** Search and browse. */
|
|
158
|
+
export interface MusicProviderCatalog {
|
|
159
|
+
/**
|
|
160
|
+
* Records matching `query`, narrowed by whatever {@link SearchTracksOptions} carries.
|
|
161
|
+
*
|
|
162
|
+
* **A filter you cannot apply means you have nothing to offer for that search — answer `[]`.**
|
|
163
|
+
* Never ignore one and answer as though it had not been asked for. The caller merges several
|
|
164
|
+
* providers into one list and cannot tell which rows honoured a filter, so a provider that
|
|
165
|
+
* quietly drops a period does not degrade the answer, it poisons it: the station asked for
|
|
166
|
+
* records from 1955 and is handed something else with nothing marking it. Declining costs the caller one
|
|
167
|
+
* provider's share of a result set it already treats as partial, which is the cheaper mistake by
|
|
168
|
+
* a wide margin. This is the same reasoning `ProviderTrackLookup` is strict for — a near miss
|
|
169
|
+
* here does not raise an error, it airs the wrong record.
|
|
170
|
+
*
|
|
171
|
+
* `limit` is a TOTAL, not a page size. Page internally if the upstream's own ceiling is lower.
|
|
172
|
+
*/
|
|
173
|
+
searchTracks(query: string, options?: SearchTracksOptions): Promise<ProviderTrack[]>;
|
|
174
|
+
/** Resolves to `undefined` when the id is unknown to the provider. */
|
|
175
|
+
getTrack(trackId: string): Promise<ProviderTrack | undefined>;
|
|
176
|
+
listPlaylists(options?: ListPlaylistsOptions): Promise<ProviderPlaylist[]>;
|
|
177
|
+
getPlaylistTracks(playlistId: string, options?: GetPlaylistTracksOptions): Promise<ProviderTrack[]>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Getting the station actual audio: one method, because it is one job.
|
|
181
|
+
*
|
|
182
|
+
* A provider that cannot answer it plays its own audio and never hands anything
|
|
183
|
+
* over, which is what "steer only" means (a remote Spotify Connect device, say).
|
|
184
|
+
*
|
|
185
|
+
* How the audio reaches the player is the provider's business, not the caller's.
|
|
186
|
+
* Most mint a URL out of their own head. A provider whose audio is reachable
|
|
187
|
+
* only to a process speaking a protocol it does not — Spotify's, whose tracks
|
|
188
|
+
* come off the CDN encrypted — lends the station's fetcher a login through
|
|
189
|
+
* `host.trackFetcher` and returns the URL that comes back. Both answer here.
|
|
190
|
+
*/
|
|
191
|
+
export interface MusicProviderStream {
|
|
192
|
+
/**
|
|
193
|
+
* Turn a track id into something the host can actually play: a complete URL
|
|
194
|
+
* that carries its own authentication, because the player fetches it with no
|
|
195
|
+
* headers from us.
|
|
196
|
+
*
|
|
197
|
+
* Optional only so a "steer only" provider can leave it out. Resolves to
|
|
198
|
+
* `undefined` when the provider is not connected yet, or when this station
|
|
199
|
+
* has nothing that can serve the track: the host reads that as "not
|
|
200
|
+
* available", skips the item, and holds nothing against the plugin.
|
|
201
|
+
*/
|
|
202
|
+
resolveStreamUrl?(trackId: string): Promise<ProviderStream | undefined>;
|
|
203
|
+
}
|
|
204
|
+
export type PlaybackStatus = 'playing' | 'paused' | 'stopped';
|
|
205
|
+
export interface PlaybackState {
|
|
206
|
+
status: PlaybackStatus;
|
|
207
|
+
/** The track currently loaded, if any. */
|
|
208
|
+
trackId?: string;
|
|
209
|
+
/** Playhead position in ms. */
|
|
210
|
+
positionMs?: number;
|
|
211
|
+
durationMs?: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Transport control, for providers that own their own audio output: deadair
|
|
215
|
+
* tells them what to do rather than playing anything itself.
|
|
216
|
+
*
|
|
217
|
+
* Named for what the plugin is asked to do, not for what the station calls the
|
|
218
|
+
* job. deadair's own `playout` module is the other end of this — it owns the
|
|
219
|
+
* running order and drives the player — so a capability by that name would have
|
|
220
|
+
* meant the opposite thing to anyone reading both.
|
|
221
|
+
*/
|
|
222
|
+
export interface MusicProviderSteer {
|
|
223
|
+
/** Append track ids to the provider's own queue. */
|
|
224
|
+
enqueue(trackIds: string[]): Promise<void>;
|
|
225
|
+
/** Start (or resume). With `trackId`, start that track immediately. */
|
|
226
|
+
play(trackId?: string): Promise<void>;
|
|
227
|
+
pause(): Promise<void>;
|
|
228
|
+
skip(): Promise<void>;
|
|
229
|
+
getPlaybackState(): Promise<PlaybackState>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Authorisation-code flow. The host owns the redirect endpoint and calls
|
|
233
|
+
* `handleCallback` with the query parameters it received.
|
|
234
|
+
*/
|
|
235
|
+
export interface MusicProviderOAuth {
|
|
236
|
+
getAuthorizeUrl(state: string): Promise<string>;
|
|
237
|
+
handleCallback(params: Record<string, string>): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Convenience alias for a provider implementing every sub-capability.
|
|
241
|
+
* Implement the individual interfaces instead when you only support some.
|
|
242
|
+
*/
|
|
243
|
+
export interface MusicProvider extends MusicProviderCatalog, MusicProviderStream, MusicProviderSteer, MusicProviderOAuth {
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=music.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"music.provider.d.ts","sourceRoot":"","sources":["../../src/capabilities/music.provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `news` kind. A news plugin answers "what happened", as entries somebody
|
|
3
|
+
* else published.
|
|
4
|
+
*
|
|
5
|
+
* ## It reports, and it does not schedule
|
|
6
|
+
*
|
|
7
|
+
* Nothing here says anything about a break, a bulletin or a running order. A
|
|
8
|
+
* plugin hands over what its sources published and the station decides whether
|
|
9
|
+
* that is worth saying, when, and in whose voice — the same boundary
|
|
10
|
+
* `capabilities/charts.ts` keeps for the same reason. A source able to put
|
|
11
|
+
* itself on air is a source that can talk over an operator's decisions.
|
|
12
|
+
*
|
|
13
|
+
* ## Several plugins, several newsrooms
|
|
14
|
+
*
|
|
15
|
+
* Unlike `enrichment`, this is not a fan-out that merges: two news services do
|
|
16
|
+
* not produce one better story, they produce two stories. So there is no
|
|
17
|
+
* `priority` here, and the host qualifies every {@link NewsFeedDescriptor.id}
|
|
18
|
+
* with the plugin that offered it — two services will both call something
|
|
19
|
+
* `world`.
|
|
20
|
+
*
|
|
21
|
+
* ## `id` + `publishedAt` + `since` are a de-duplication contract
|
|
22
|
+
*
|
|
23
|
+
* The one rule in this file that is not obvious from the shapes, and the one
|
|
24
|
+
* that anything polling depends on: **a {@link NewsItem.id} is stable for the
|
|
25
|
+
* same entry across calls**, so a caller can tell an arrival from something it
|
|
26
|
+
* has already seen without holding state on the plugin's behalf. A plugin must
|
|
27
|
+
* not renumber, re-order or synthesise ids per call, and must not let a cache
|
|
28
|
+
* of its own hide an entry that {@link NewsQuery.since} asked for. Both are
|
|
29
|
+
* free if the plugin builds its items with `feed.parse.ts`, which is where that
|
|
30
|
+
* ladder lives.
|
|
31
|
+
*
|
|
32
|
+
* Every shape here is JSON-safe.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* One feed this plugin can serve.
|
|
36
|
+
*
|
|
37
|
+
* `id` is scoped to this plugin and needs to be unique only within it. The host
|
|
38
|
+
* qualifies it before anything outside sees it, so a short flat id is right.
|
|
39
|
+
*/
|
|
40
|
+
export interface NewsFeedDescriptor {
|
|
41
|
+
id: string;
|
|
42
|
+
/** What to call it in a list an operator reads, e.g. `World news`. */
|
|
43
|
+
name: string;
|
|
44
|
+
/** Broad subject, e.g. `world`, `sport`, `technology`. The publisher's own word for it. */
|
|
45
|
+
category?: string;
|
|
46
|
+
/** ISO 639-1, when the plugin knows what language the entries are in. */
|
|
47
|
+
language?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
/**
|
|
50
|
+
* How often this source is worth asking, in milliseconds.
|
|
51
|
+
*
|
|
52
|
+
* A hint from the only party that knows: a wire service publishes minute by
|
|
53
|
+
* minute and a weekly column does not. Nothing is obliged to honour it, and
|
|
54
|
+
* a plugin that has no opinion leaves it out rather than inventing one.
|
|
55
|
+
*/
|
|
56
|
+
pollHintMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/** What a news source is asked for. */
|
|
59
|
+
export interface NewsQuery {
|
|
60
|
+
/**
|
|
61
|
+
* A {@link NewsFeedDescriptor.id} this plugin offered, or absent for all of
|
|
62
|
+
* them merged newest first.
|
|
63
|
+
*
|
|
64
|
+
* Absent is the common case and is why it is optional: "what is going on"
|
|
65
|
+
* is one question, and making a caller pick a feed first would cost a model
|
|
66
|
+
* a whole round trip to learn ids it has no basis for choosing between.
|
|
67
|
+
*/
|
|
68
|
+
feedId?: string;
|
|
69
|
+
/** How many entries to return. A plugin may return fewer; it must not return more. */
|
|
70
|
+
limit: number;
|
|
71
|
+
/**
|
|
72
|
+
* Only entries published after this instant, as an ISO-8601 string. Never a
|
|
73
|
+
* `Date`.
|
|
74
|
+
*
|
|
75
|
+
* How a caller asks what is new since it last looked. A plugin that cannot
|
|
76
|
+
* filter upstream filters what it got rather than ignoring this, because a
|
|
77
|
+
* caller cannot tell "there is nothing new" from "this was not applied".
|
|
78
|
+
*/
|
|
79
|
+
since?: string;
|
|
80
|
+
/**
|
|
81
|
+
* `true` when the caller will not read {@link NewsItem.content}, so a plugin
|
|
82
|
+
* that would go and fetch one should not bother.
|
|
83
|
+
*
|
|
84
|
+
* A hint about COST rather than about shape: a plugin that has the story
|
|
85
|
+
* already, because the entry carried it, still sends it. What this asks it
|
|
86
|
+
* to skip is work it would otherwise do on the caller's behalf — for a feed
|
|
87
|
+
* reader that means following each entry's link and reading the publisher's
|
|
88
|
+
* page, which is an order of magnitude more expensive than the feed itself
|
|
89
|
+
* and is why this exists.
|
|
90
|
+
*
|
|
91
|
+
* Absent means the ordinary thing, so a plugin that ignores this is slow
|
|
92
|
+
* rather than wrong, and a caller that forgets it gets stories it does not
|
|
93
|
+
* need. That is the right way round: the console asked for a page of
|
|
94
|
+
* headlines and waited three seconds for article bodies it never drew.
|
|
95
|
+
*/
|
|
96
|
+
headlinesOnly?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** One published entry. */
|
|
99
|
+
export interface NewsItem {
|
|
100
|
+
/** Stable across calls. See the de-duplication contract in this file's header. */
|
|
101
|
+
id: string;
|
|
102
|
+
/** Which feed it came from, as the plugin's own unqualified id. */
|
|
103
|
+
feedId: string;
|
|
104
|
+
/** That feed's name, so a caller reading one merged list can say where a story came from. */
|
|
105
|
+
feedName: string;
|
|
106
|
+
title: string;
|
|
107
|
+
/**
|
|
108
|
+
* The entry's own words, as PLAIN TEXT.
|
|
109
|
+
*
|
|
110
|
+
* Never markup. This ends up in a model's context and possibly in a
|
|
111
|
+
* speaking voice, and a `<p>` reaching either of those is a bug that is
|
|
112
|
+
* only noticed on air. `parseFeed` in `feed.parse.ts` already guarantees it.
|
|
113
|
+
*/
|
|
114
|
+
summary?: string;
|
|
115
|
+
/**
|
|
116
|
+
* The story itself, as PLAIN TEXT, when the plugin could read it.
|
|
117
|
+
*
|
|
118
|
+
* Distinct from {@link summary} rather than replacing it, because they are
|
|
119
|
+
* two different things a publisher wrote: a summary is the teaser attached
|
|
120
|
+
* to the entry and this is the article. In practice the teaser is often one
|
|
121
|
+
* sentence restating the title, which is why a caller wanting to say what
|
|
122
|
+
* HAPPENED needs somewhere else to look.
|
|
123
|
+
*
|
|
124
|
+
* Still the publisher's own words in the publisher's own order, never a
|
|
125
|
+
* plugin's paraphrase. A plugin fetches and the host thinks — the same
|
|
126
|
+
* boundary `capabilities/enrichment.ts` keeps with `SourceDocument`, for
|
|
127
|
+
* the same reason: only the host can check a claim against the text it came
|
|
128
|
+
* from, and prose that has been through a plugin's own summariser is prose
|
|
129
|
+
* nothing can check.
|
|
130
|
+
*
|
|
131
|
+
* Absent is entirely ordinary. An entry that links to an audio piece, a
|
|
132
|
+
* page a plugin was refused, and one it had no budget left to read all
|
|
133
|
+
* arrive the same way, and every caller's fallback is the entry's own
|
|
134
|
+
* words. `extractArticle` in `article.parse.ts` is what produces this for
|
|
135
|
+
* a plugin reading pages.
|
|
136
|
+
*/
|
|
137
|
+
content?: string;
|
|
138
|
+
url?: string;
|
|
139
|
+
/** ISO-8601. Never a `Date`, and absent when the source published no readable one. */
|
|
140
|
+
publishedAt?: string;
|
|
141
|
+
/** The publisher's own labels, unmapped. Useful for filtering, never authoritative. */
|
|
142
|
+
categories?: string[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Implemented by a `news` plugin.
|
|
146
|
+
*/
|
|
147
|
+
export interface NewsProvider {
|
|
148
|
+
/**
|
|
149
|
+
* What this plugin can serve, right now.
|
|
150
|
+
*
|
|
151
|
+
* Asked per call rather than cached by the host, for the reason
|
|
152
|
+
* `ChartsProvider.listCharts` is: an operator reconfiguring a plugin
|
|
153
|
+
* reinitializes it, and a feed list built from a config field would
|
|
154
|
+
* otherwise be a boot snapshot of a setting that has since changed.
|
|
155
|
+
*
|
|
156
|
+
* An empty array is an ordinary answer — a plugin nobody has pointed at a
|
|
157
|
+
* feed yet has nothing to offer — and is not a failure.
|
|
158
|
+
*/
|
|
159
|
+
listFeeds(): Promise<NewsFeedDescriptor[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Entries, newest first.
|
|
162
|
+
*
|
|
163
|
+
* Return `[]` for a `feedId` you do not recognise rather than throwing: the
|
|
164
|
+
* host asks the plugin that named the id, so an unknown one means the menu
|
|
165
|
+
* moved underneath a caller, which is a stale request and not a fault. A
|
|
166
|
+
* source that could not be read is the same answer for the same reason, and
|
|
167
|
+
* one failing feed must not cost the others.
|
|
168
|
+
*/
|
|
169
|
+
fetchItems(query: NewsQuery): Promise<NewsItem[]>;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=news.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"news.d.ts","sourceRoot":"","sources":["../../src/capabilities/news.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACtB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,2BAA2B;AAC3B,MAAM,WAAW,QAAQ;IACrB,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB;;;;;;;;;;OAUG;IACH,SAAS,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAE3C;;;;;;;;OAQG;IACH,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACrD"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `scrobble` kind. A scrobble plugin reports what the station played to
|
|
3
|
+
* somebody else's service.
|
|
4
|
+
*
|
|
5
|
+
* ## The only capability that SENDS
|
|
6
|
+
*
|
|
7
|
+
* Everything else a plugin does is a read: enrichment asks an upstream what it
|
|
8
|
+
* knows, a chart asks what is popular, a provider asks for audio. This publishes
|
|
9
|
+
* the station's own listening to an account the operator holds, which makes it
|
|
10
|
+
* the one place where getting it wrong writes to somebody else's records rather
|
|
11
|
+
* than producing a worse hour.
|
|
12
|
+
*
|
|
13
|
+
* Three things follow from that and are worth knowing before implementing one.
|
|
14
|
+
*
|
|
15
|
+
* **Late is fine and lost is not.** The host queues every play durably and
|
|
16
|
+
* retries with a backoff, so a plugin never has to hold anything: answer for the
|
|
17
|
+
* batch you were given and say what happened to it.
|
|
18
|
+
*
|
|
19
|
+
* **{@link ScrobbleRejection.retryable} is the field that matters.** "The service
|
|
20
|
+
* was down" and "that timestamp is too old to accept" are opposite facts, and a
|
|
21
|
+
* queue that cannot tell them apart either loses plays or retries a permanent
|
|
22
|
+
* rejection forever. The plugin classifies, because only the plugin knows what
|
|
23
|
+
* its upstream meant; the host obeys.
|
|
24
|
+
*
|
|
25
|
+
* **{@link ScrobbleProvider.accepting} is how an operator says no.** A service
|
|
26
|
+
* that also does something else — Last.fm is enrichment, charts, similarity and
|
|
27
|
+
* this — is one installed plugin, and wanting its tags without sending it your
|
|
28
|
+
* listening is an ordinary position. Since `plugin_configs.plugin_id` is a
|
|
29
|
+
* primary key, it cannot be expressed by installing twice, and since capabilities
|
|
30
|
+
* are fixed when the plugin is written it cannot be expressed by the manifest
|
|
31
|
+
* either. So it is a config field, read back through one optional method.
|
|
32
|
+
*
|
|
33
|
+
* Every shape here is JSON-safe.
|
|
34
|
+
*/
|
|
35
|
+
/** One record the station played. */
|
|
36
|
+
export interface ScrobblePlay {
|
|
37
|
+
title: string;
|
|
38
|
+
/**
|
|
39
|
+
* The LEAD artist, as the catalog credits it.
|
|
40
|
+
*
|
|
41
|
+
* Not a joined credit line, for the reason every other capability here gives:
|
|
42
|
+
* a scrobbling service matches on artist and title, and "A, B & C" matches
|
|
43
|
+
* nothing. {@link albumArtist} is where a compilation's own credit goes.
|
|
44
|
+
*/
|
|
45
|
+
artist: string;
|
|
46
|
+
album?: string;
|
|
47
|
+
/** The record's own credit, where it differs from the track's. */
|
|
48
|
+
albumArtist?: string;
|
|
49
|
+
/** Integer milliseconds. Some services use it to decide whether a play counts. */
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
trackNumber?: number;
|
|
52
|
+
/** MusicBrainz recording id, when the catalog has resolved one. */
|
|
53
|
+
mbid?: string;
|
|
54
|
+
/**
|
|
55
|
+
* When it started, as Unix epoch MILLISECONDS. Never a `Date`.
|
|
56
|
+
*
|
|
57
|
+
* Milliseconds because that is what the rest of this SDK uses; a service
|
|
58
|
+
* wanting seconds divides. It is the moment the record went to air rather
|
|
59
|
+
* than the moment this call was made, and the difference is the whole reason
|
|
60
|
+
* the host queues: a play sent an hour late is still a play at the time it
|
|
61
|
+
* happened.
|
|
62
|
+
*/
|
|
63
|
+
playedAt: number;
|
|
64
|
+
}
|
|
65
|
+
/** Why one play in a batch was refused. */
|
|
66
|
+
export interface ScrobbleRejection {
|
|
67
|
+
/** Which play, as its index in the batch that was submitted. */
|
|
68
|
+
index: number;
|
|
69
|
+
/** What the upstream said, summarized by the plugin. Never the raw body. */
|
|
70
|
+
reason: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether sending it again could work.
|
|
73
|
+
*
|
|
74
|
+
* `true` for a timeout, a 5xx, a rate limit. `false` for a timestamp the
|
|
75
|
+
* service will not accept, a malformed record, a revoked authorization. When
|
|
76
|
+
* in doubt, say `false`: a play retried forever is a queue that never drains,
|
|
77
|
+
* and one dropped is one listen missing from a history nobody audits.
|
|
78
|
+
*/
|
|
79
|
+
retryable: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** What became of one batch. */
|
|
82
|
+
export interface ScrobbleResult {
|
|
83
|
+
/** How many the service took. */
|
|
84
|
+
accepted: number;
|
|
85
|
+
/** The ones it did not, each said to be worth retrying or not. */
|
|
86
|
+
rejected: ScrobbleRejection[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Implemented by a `scrobble` plugin.
|
|
90
|
+
*/
|
|
91
|
+
export interface ScrobbleProvider {
|
|
92
|
+
/**
|
|
93
|
+
* How many plays this plugin will take in one {@link scrobble} call.
|
|
94
|
+
*
|
|
95
|
+
* The host chunks to it. Absent means the host picks a modest default, which
|
|
96
|
+
* is the right answer for a service with no published batch limit.
|
|
97
|
+
*/
|
|
98
|
+
maxBatchSize?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Report plays that have already happened.
|
|
101
|
+
*
|
|
102
|
+
* The batch is ordered oldest first. Answer for every entry: anything neither
|
|
103
|
+
* counted in `accepted` nor listed in `rejected` is treated as accepted, since
|
|
104
|
+
* a plugin that quietly dropped one is a worse outcome than a duplicate.
|
|
105
|
+
*
|
|
106
|
+
* THROW only when the whole batch failed for one reason — no credentials, the
|
|
107
|
+
* service unreachable — which the host treats as retryable for all of them.
|
|
108
|
+
* A per-play problem belongs in `rejected`, where it can be classified.
|
|
109
|
+
*/
|
|
110
|
+
scrobble(plays: ScrobblePlay[]): Promise<ScrobbleResult>;
|
|
111
|
+
/**
|
|
112
|
+
* Say what is playing right now.
|
|
113
|
+
*
|
|
114
|
+
* Optional, and deliberately not part of the durable path: it is worthless
|
|
115
|
+
* late, so the host sends it once, at the moment the record goes to air, and
|
|
116
|
+
* never retries or queues it. A plugin should treat a failure here as
|
|
117
|
+
* unimportant.
|
|
118
|
+
*/
|
|
119
|
+
nowPlaying?(play: ScrobblePlay): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Whether this destination currently wants the station's plays.
|
|
122
|
+
*
|
|
123
|
+
* Optional, and ABSENT MEANS YES — a plugin that exists only to scrobble
|
|
124
|
+
* never has to write it. Implement it when the same plugin does other things
|
|
125
|
+
* and an operator might want those without this.
|
|
126
|
+
*
|
|
127
|
+
* Asked before anything is queued, so answering `false` means nothing
|
|
128
|
+
* accumulates rather than a queue that drains into a discard. Keep it cheap:
|
|
129
|
+
* it is a config read, not a call to the service.
|
|
130
|
+
*/
|
|
131
|
+
accepting?(): Promise<boolean>;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=scrobble.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrobble.d.ts","sourceRoot":"","sources":["../../src/capabilities/scrobble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,qCAAqC;AACrC,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAC9B,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,EAAE,OAAO,CAAC;CACtB;AAED,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC3B,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAEzD;;;;;;;OAOG;IACH,UAAU,CAAC,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;;;;;;;;;OAUG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAClC"}
|