@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -0
  3. package/dist/article.parse.d.ts +82 -0
  4. package/dist/article.parse.d.ts.map +1 -0
  5. package/dist/boundary.json.safe.d.ts +191 -0
  6. package/dist/boundary.json.safe.d.ts.map +1 -0
  7. package/dist/capabilities/analysis.d.ts +330 -0
  8. package/dist/capabilities/analysis.d.ts.map +1 -0
  9. package/dist/capabilities/charts.d.ts +134 -0
  10. package/dist/capabilities/charts.d.ts.map +1 -0
  11. package/dist/capabilities/enrichment.d.ts +255 -0
  12. package/dist/capabilities/enrichment.d.ts.map +1 -0
  13. package/dist/capabilities/llm.d.ts +318 -0
  14. package/dist/capabilities/llm.d.ts.map +1 -0
  15. package/dist/capabilities/mixer.d.ts +183 -0
  16. package/dist/capabilities/mixer.d.ts.map +1 -0
  17. package/dist/capabilities/music.provider.d.ts +245 -0
  18. package/dist/capabilities/music.provider.d.ts.map +1 -0
  19. package/dist/capabilities/news.d.ts +171 -0
  20. package/dist/capabilities/news.d.ts.map +1 -0
  21. package/dist/capabilities/scrobble.d.ts +133 -0
  22. package/dist/capabilities/scrobble.d.ts.map +1 -0
  23. package/dist/capabilities/search.d.ts +122 -0
  24. package/dist/capabilities/search.d.ts.map +1 -0
  25. package/dist/capabilities/similarity.d.ts +101 -0
  26. package/dist/capabilities/similarity.d.ts.map +1 -0
  27. package/dist/capabilities/speech.d.ts +211 -0
  28. package/dist/capabilities/speech.d.ts.map +1 -0
  29. package/dist/capabilities/weather.d.ts +192 -0
  30. package/dist/capabilities/weather.d.ts.map +1 -0
  31. package/dist/chunk-7QVYU63E.js +7 -0
  32. package/dist/chunk-7QVYU63E.js.map +1 -0
  33. package/dist/define.plugin.d.ts +56 -0
  34. package/dist/define.plugin.d.ts.map +1 -0
  35. package/dist/feed.parse.d.ts +97 -0
  36. package/dist/feed.parse.d.ts.map +1 -0
  37. package/dist/html.text.d.ts +71 -0
  38. package/dist/html.text.d.ts.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1153 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/match.text.d.ts +28 -0
  44. package/dist/match.text.d.ts.map +1 -0
  45. package/dist/plugin.api.version.d.ts +9 -0
  46. package/dist/plugin.api.version.d.ts.map +1 -0
  47. package/dist/plugin.base.d.ts +80 -0
  48. package/dist/plugin.base.d.ts.map +1 -0
  49. package/dist/plugin.config.fields.d.ts +524 -0
  50. package/dist/plugin.config.fields.d.ts.map +1 -0
  51. package/dist/plugin.config.read.d.ts +52 -0
  52. package/dist/plugin.config.read.d.ts.map +1 -0
  53. package/dist/plugin.error.d.ts +177 -0
  54. package/dist/plugin.error.d.ts.map +1 -0
  55. package/dist/plugin.host.d.ts +220 -0
  56. package/dist/plugin.host.d.ts.map +1 -0
  57. package/dist/plugin.host.response.d.ts +42 -0
  58. package/dist/plugin.host.response.d.ts.map +1 -0
  59. package/dist/plugin.http.d.ts +80 -0
  60. package/dist/plugin.http.d.ts.map +1 -0
  61. package/dist/plugin.lifecycle.d.ts +68 -0
  62. package/dist/plugin.lifecycle.d.ts.map +1 -0
  63. package/dist/plugin.manifest.d.ts +282 -0
  64. package/dist/plugin.manifest.d.ts.map +1 -0
  65. package/dist/plugin.permissions.d.ts +186 -0
  66. package/dist/plugin.permissions.d.ts.map +1 -0
  67. package/dist/testing/fake.plugin.host.d.ts +85 -0
  68. package/dist/testing/fake.plugin.host.d.ts.map +1 -0
  69. package/dist/testing/index.d.ts +10 -0
  70. package/dist/testing/index.d.ts.map +1 -0
  71. package/dist/testing/index.js +141 -0
  72. package/dist/testing/index.js.map +1 -0
  73. 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"}