@discord-player/plugin-host 0.0.0-stage → 8.0.0-dev.1

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -2
  3. package/dist/compat/base-extractor.d.ts +88 -0
  4. package/dist/compat/base-extractor.js +118 -0
  5. package/dist/compat/base-extractor.js.map +1 -0
  6. package/dist/compat/extractor-execution-context.d.ts +69 -0
  7. package/dist/compat/extractor-execution-context.js +121 -0
  8. package/dist/compat/extractor-execution-context.js.map +1 -0
  9. package/dist/compat/extractor-plugin.d.ts +1 -0
  10. package/dist/compat/extractor-plugin.js +126 -0
  11. package/dist/compat/extractor-plugin.js.map +1 -0
  12. package/dist/compat/extractor-types.d.ts +49 -0
  13. package/dist/compat/extractor-types.js +2 -0
  14. package/dist/compat/extractor-types.js.map +1 -0
  15. package/dist/compat/extractors.d.ts +4 -0
  16. package/dist/compat/extractors.js +6 -0
  17. package/dist/compat/extractors.js.map +1 -0
  18. package/dist/compat/query-types.d.ts +15 -0
  19. package/dist/compat/query-types.js +27 -0
  20. package/dist/compat/query-types.js.map +1 -0
  21. package/dist/compat/utils.d.ts +28 -0
  22. package/dist/compat/utils.js +30 -0
  23. package/dist/compat/utils.js.map +1 -0
  24. package/dist/errors.d.ts +56 -0
  25. package/dist/errors.js +63 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/host.d.ts +76 -0
  28. package/dist/host.js +317 -0
  29. package/dist/host.js.map +1 -0
  30. package/dist/index.d.ts +17 -0
  31. package/dist/index.js +17 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/legacy-player.d.ts +47 -0
  34. package/dist/legacy-player.js +2 -0
  35. package/dist/legacy-player.js.map +1 -0
  36. package/dist/media.d.ts +15 -0
  37. package/dist/media.js +193 -0
  38. package/dist/media.js.map +1 -0
  39. package/dist/plugins/execution-options.d.ts +25 -0
  40. package/dist/plugins/execution-options.js +2 -0
  41. package/dist/plugins/execution-options.js.map +1 -0
  42. package/dist/plugins/operation-dispatch.d.ts +1 -0
  43. package/dist/plugins/operation-dispatch.js +111 -0
  44. package/dist/plugins/operation-dispatch.js.map +1 -0
  45. package/dist/plugins/plugin-error.d.ts +17 -0
  46. package/dist/plugins/plugin-error.js +23 -0
  47. package/dist/plugins/plugin-error.js.map +1 -0
  48. package/dist/plugins/registry-internals.d.ts +58 -0
  49. package/dist/plugins/registry-internals.js +2 -0
  50. package/dist/plugins/registry-internals.js.map +1 -0
  51. package/dist/plugins/registry.d.ts +66 -0
  52. package/dist/plugins/registry.js +257 -0
  53. package/dist/plugins/registry.js.map +1 -0
  54. package/dist/plugins/stream-resolver.d.ts +1 -0
  55. package/dist/plugins/stream-resolver.js +98 -0
  56. package/dist/plugins/stream-resolver.js.map +1 -0
  57. package/dist/plugins/types.d.ts +170 -0
  58. package/dist/plugins/types.js +11 -0
  59. package/dist/plugins/types.js.map +1 -0
  60. package/dist/protocol.d.ts +28 -0
  61. package/dist/protocol.js +37 -0
  62. package/dist/protocol.js.map +1 -0
  63. package/dist/runtime.d.ts +58 -0
  64. package/dist/runtime.js +118 -0
  65. package/dist/runtime.js.map +1 -0
  66. package/dist/structures/playlist.d.ts +139 -0
  67. package/dist/structures/playlist.js +117 -0
  68. package/dist/structures/playlist.js.map +1 -0
  69. package/dist/structures/search-result.d.ts +30 -0
  70. package/dist/structures/search-result.js +35 -0
  71. package/dist/structures/search-result.js.map +1 -0
  72. package/dist/structures/track-types.d.ts +122 -0
  73. package/dist/structures/track-types.js +2 -0
  74. package/dist/structures/track-types.js.map +1 -0
  75. package/dist/structures/track.d.ts +138 -0
  76. package/dist/structures/track.js +270 -0
  77. package/dist/structures/track.js.map +1 -0
  78. package/dist/types.d.ts +27 -0
  79. package/dist/types.js +2 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/utils/legacy-serialization-schemas.d.ts +166 -0
  82. package/dist/utils/legacy-serialization-schemas.js +108 -0
  83. package/dist/utils/legacy-serialization-schemas.js.map +1 -0
  84. package/dist/utils/parse-serialized-entity.d.ts +1 -0
  85. package/dist/utils/parse-serialized-entity.js +43 -0
  86. package/dist/utils/parse-serialized-entity.js.map +1 -0
  87. package/dist/utils/safe-event-emitter.d.ts +10 -0
  88. package/dist/utils/safe-event-emitter.js +10 -0
  89. package/dist/utils/safe-event-emitter.js.map +1 -0
  90. package/dist/utils/serde.d.ts +54 -0
  91. package/dist/utils/serde.js +83 -0
  92. package/dist/utils/serde.js.map +1 -0
  93. package/dist/utils/serialization-schemas.d.ts +93 -0
  94. package/dist/utils/serialization-schemas.js +49 -0
  95. package/dist/utils/serialization-schemas.js.map +1 -0
  96. package/dist/utils/serialization.d.ts +36 -0
  97. package/dist/utils/serialization.js +84 -0
  98. package/dist/utils/serialization.js.map +1 -0
  99. package/docs/remedia-contract.md +54 -0
  100. package/package.json +59 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Androz2091
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,348 @@
1
- # Temporary Holding Version
1
+ # @discord-player/plugin-host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Standalone source execution for Discord Player v8, built on the published
4
+ `@remedia/plugin-host`. Requires Node.js 22.18 or newer.
5
+
6
+ ```text
7
+ discord-player → @discord-player/plugin-host → @remedia/plugin-host
8
+ ```
9
+
10
+ `discord-player` installs this package as a regular dependency and re-exports its
11
+ canonical `BaseExtractor`, `Track`, `Playlist`, `SearchResult`, plugin declarations,
12
+ registry, and serialization classes. Constructor identity and `instanceof` work
13
+ across package imports. No Player, queue manager, Discord client, native audio
14
+ binding, or Remedia audio engine is initialized by importing or creating a host.
15
+ The `@remedia/client` dependency supplies source descriptor types only.
16
+
17
+ The official `@discord-player/extractor` collection is an optional, separate package.
18
+ It is neither bundled nor automatically registered.
19
+
20
+ ## Using a host from a bot
21
+
22
+ Normal applications do not need to create an HTTP host. Register sources locally
23
+ as before, or configure standalone deployments once in Player options:
24
+
25
+ ```ts
26
+ const player = new Player({
27
+ // Existing Remedia nodes and gateway send callback:
28
+ ...audioOptions,
29
+ sourceHosts: [
30
+ {
31
+ hostId: 'sources-1',
32
+ url: 'https://plugins.example.com',
33
+ token: process.env.SOURCE_TOKEN,
34
+ timeoutMs: 15000,
35
+ },
36
+ ],
37
+ });
38
+
39
+ const result = await player.search('some song', { hostId: 'sources-1' });
40
+ await player.play(channel, result);
41
+ ```
42
+
43
+ This requires `@remedia/client@0.1.0-dev.5` and a matching remote server configured
44
+ with the same `pluginHosts` ID and the `discord-player.source` / `source.stream`
45
+ operation. The bot searches over HTTP and sends an operation descriptor for play
46
+ and automatic crossfade staging. Encoded bytes flow directly from host to audio
47
+ node. Local sources run locally. Embedded nodes cannot play hosted Tracks.
48
+
49
+ See the [complete deployment example](https://discord-player.js.org/docs/plugins/remote-hosts).
50
+ The direct HTTP examples below describe protocol consumers; a bot should use
51
+ `player.search()` and queue playback, without fetching media grants itself.
52
+
53
+ ## Registration and direct execution
54
+
55
+ ```ts
56
+ import { createSourcePluginHost, SOURCE_PLUGIN_ID } from '@discord-player/plugin-host';
57
+ import { MyExtractor } from './my-extractor.js';
58
+ import { mySourcePlugin } from './my-source-plugin.js';
59
+
60
+ // Local-only handle() usage may omit hostId.
61
+ const host = createSourcePluginHost({ name: 'My Source Host' });
62
+ await host.extractors.register(MyExtractor);
63
+ await host.plugins.register(mySourcePlugin());
64
+
65
+ const response = await host.handle({
66
+ pluginId: SOURCE_PLUGIN_ID,
67
+ operation: 'source.search',
68
+ input: { query: 'example' },
69
+ });
70
+ if (response.handled) console.log(response.result);
71
+ // Or use typed source APIs directly:
72
+ const result = await host.runtime.search('example');
73
+ const track = result.first();
74
+ if (track) {
75
+ const source = await host.plugins.resolveStream(track);
76
+ // Caller owns consumption and destruction of returned Readables.
77
+ console.log(source);
78
+ }
79
+ await host.close();
80
+ ```
81
+
82
+ Direct execution preserves model and stream references. It needs no public URL,
83
+ listener, or serialization. `host.remedia` exposes the actual generic Remedia host
84
+ for discovery and lifecycle inspection; use `host.handle` and `host.fetch` as the
85
+ application entrypoints, so the boundary conversion remains active.
86
+
87
+ Both Player and standalone execution use `SourcePluginRuntime`. There is one
88
+ registry with the existing `first`, `collect`, `pipe`, and `broadcast` policies,
89
+ provider priorities, protocol matching, bridging, registration cancellation,
90
+ post-stream transforms, and cleanup ownership. Player retains all queue, voice,
91
+ Gateway, crossfade, and playback orchestration.
92
+
93
+ ## Hono
94
+
95
+ ```ts
96
+ import { Hono } from 'hono';
97
+ import { serve } from '@hono/node-server';
98
+ import { createSourcePluginHost } from '@discord-player/plugin-host';
99
+ import { MyExtractor } from './my-extractor.js';
100
+
101
+ const host = createSourcePluginHost({ name: 'Sources', hostId: 'sources-eu-1', publicUrl: 'http://localhost:3000' });
102
+ await host.extractors.register(MyExtractor);
103
+ const app = new Hono();
104
+ app.all('*', (c) => host.fetch(c.req.raw));
105
+ const server = serve({ fetch: app.fetch, port: 3000 });
106
+ process.once('SIGTERM', () => {
107
+ server.close();
108
+ void host.close();
109
+ });
110
+ ```
111
+
112
+ ## Express
113
+
114
+ Mount Remedia's existing middleware at the origin root before body parsers.
115
+
116
+ ```ts
117
+ import express from 'express';
118
+ import { createNodeMiddleware } from '@remedia/plugin-host/node';
119
+ import { createSourcePluginHost } from '@discord-player/plugin-host';
120
+ import { MyExtractor } from './my-extractor.js';
121
+
122
+ const host = createSourcePluginHost({ name: 'Sources', hostId: 'sources-eu-1', publicUrl: 'http://localhost:3000' });
123
+ await host.extractors.register(MyExtractor);
124
+ const app = express();
125
+ app.use(createNodeMiddleware(host));
126
+ app.use(express.json());
127
+ const server = app.listen(3000);
128
+ process.once('SIGTERM', () => {
129
+ server.close();
130
+ void host.close();
131
+ });
132
+ ```
133
+
134
+ No listener starts automatically. Remedia owns HTTP routing, authorization,
135
+ request limits, media tokens, backpressure, stream cancellation, and disposal.
136
+ Configure `token`, `mediaTtlMs`, and `maxMediaTokens` through the host options.
137
+ `publicUrl` is a trusted HTTP(S) origin and is needed only for HTTP media results.
138
+
139
+ ## HTTP source contract
140
+
141
+ One aggregate registration, `discord-player.source`, advertises:
142
+
143
+ | Operation | Input | Result |
144
+ | ---------------- | ------------------------------- | ------------------------------------------ |
145
+ | `source.search` | `{ query, options? }` | Metadata through normal provider matching |
146
+ | `source.resolve` | `{ query, pluginId, options? }` | Metadata from an explicit provider |
147
+ | `source.related` | `{ track }` | Related metadata from the track's provider |
148
+ | `source.stream` | `{ track }` | Deferred Remedia media descriptor |
149
+
150
+ `source.stream` includes normal bridge fallback and post-stream transformations;
151
+ internal operation handlers are not separate Remedia registrations. HTTP options
152
+ accept portable plugin/source/searchEngine hints, limit, requester ID, and blocked
153
+ provider IDs. Local HTTP request options, functions and Discord objects do not
154
+ cross the boundary. The transport validates input before source execution.
155
+
156
+ ```ts
157
+ import { SOURCE_PLUGIN_ID, deserializeSourceResult, type PortableSourceResult } from '@discord-player/plugin-host';
158
+
159
+ const response = await fetch('http://localhost:3000/remedia/v1/execute', {
160
+ method: 'POST',
161
+ headers: { 'content-type': 'application/json' },
162
+ body: JSON.stringify({ pluginId: SOURCE_PLUGIN_ID, operation: 'source.search', input: { query: 'example' } }),
163
+ });
164
+ if (!response.ok) throw new Error(`Source search failed: ${response.status}`);
165
+ const search = (await response.json()) as { handled: boolean; result: PortableSourceResult };
166
+ if (!search.handled) throw new Error('Unhandled search');
167
+ const localModels = deserializeSourceResult(search.result);
168
+ console.log(localModels.first());
169
+
170
+ // Send the original portable representation back, including its optional state hint.
171
+ const mediaResponse = await fetch('http://localhost:3000/remedia/v1/execute', {
172
+ method: 'POST',
173
+ headers: { 'content-type': 'application/json' },
174
+ body: JSON.stringify({
175
+ pluginId: SOURCE_PLUGIN_ID,
176
+ operation: 'source.stream',
177
+ input: { track: search.result.tracks[0] },
178
+ }),
179
+ });
180
+ if (!mediaResponse.ok) throw new Error(`Source stream failed: ${mediaResponse.status}`);
181
+ const media = (await mediaResponse.json()) as { handled: boolean; result: { url: string; expiresAtMs: number } };
182
+ if (!media.handled) throw new Error('Unhandled media');
183
+ // Redeem media.result.url before expiresAtMs; this is a short-lived, single-use grant.
184
+ const bytes = await fetch(media.result.url);
185
+ console.log(bytes.body);
186
+ ```
187
+
188
+ Media resolution starts when the grant is redeemed, using that request's
189
+ cancellation signal rather than the completed JSON request's signal. Readables,
190
+ files, buffers, and upstream HTTP(S) URLs become encoded byte streams before
191
+ post-stream transforms. URLs are proxied inside the host so provider resolution
192
+ and transforms remain there. The adapter never buffers complete media files.
193
+ Direct calls retain native descriptors; HTTP never returns nested Readables.
194
+
195
+ ## Portable metadata and process-local state
196
+
197
+ HTTP results contain version 1 Track and Playlist data. `deserializeSourceResult()`
198
+ retains the opaque `sourceState` token on the normal Track and links Playlist
199
+ instances. Pass `serializeSourceTrack(track)` to source operations; no second HTTP
200
+ payload needs to be retained. `serialize()` and `toJSON()` deliberately omit the
201
+ temporary token. Persistence and clones made through persistence reconstruct from
202
+ provider metadata. Ordinary queue/history insertion retains the same Track and its
203
+ token. Host identity persists in `sourceReference.hostId`, separate from
204
+ `sourceReference.pluginId` and the provider's source name/data.
205
+
206
+ HTTP hosts must configure a stable non-secret `hostId`, including deployments
207
+ that do not configure `publicUrl`. Construction with `publicUrl` requires it, and
208
+ `fetch()` rejects HTTP use without it. Local-only `handle()` hosts may omit it and
209
+ receive a generated process identifier that intentionally does not survive restarts. Two hosts
210
+ can use the same provider IDs, but cannot redeem one another's Tracks. The embedded
211
+ registry rejects hosted Tracks when used as local sources. DiscordPlayer routes them
212
+ through configured remote hosts and compatible Remedia nodes instead.
213
+ Do not put endpoints, bearer tokens or credentials in sourceReference.
214
+
215
+ The per-host FIFO cache defaults to 1000 Track states for ten minutes. Previous
216
+ 10000/one-hour defaults could retain substantial provider payloads and playlists.
217
+ Periodic cleanup runs even while idle (at most every 30 seconds), with additional
218
+ checks on use, provider lifetime changes and shutdown. Cached snapshots omit
219
+ Playlist links and metadata callbacks so one entry cannot retain the whole result
220
+ through those links. This is a count limit, not a byte memory guarantee: trusted
221
+ providers can still attach large engines. Tune limits for the deployment.
222
+
223
+ Concurrent remote playback receives copied engine/public metadata, never the
224
+ cached Track itself. Non-cloneable engines are reconstructed instead. Copies use
225
+ snapshot metadata rather than callbacks closing over the original Track. Direct
226
+ execution retains original object identity and the caller's ownership contract.
227
+
228
+ After eviction, expiration, or restart, modern plugins receive reconstructed
229
+ Track metadata and their portable sourceReference. Legacy extractors re-run
230
+ `handle()` through their original provider using the public Track URL, select the
231
+ matching URL, and then receive the newly constructed Track in `stream()`. If the
232
+ provider is unavailable or cannot reconstruct the track, execution fails with an
233
+ explicit diagnostic. This means queued tracks can survive cache loss when their
234
+ provider supports URL reconstruction; ephemeral-only sources must be searched
235
+ again. A serialized Track is never a promise of permanent source availability.
236
+
237
+ Raw engines, credentials, functions, live streams, circular objects and legacy
238
+ metadata callbacks are never serialized. Modern metadata and sourceReference
239
+ must follow the existing JSON-safe contract; plugins must put only public source
240
+ identifiers there, never secrets. Invalid metadata fails serialization rather
241
+ than being silently removed. Credentials belong to host-local plugin options.
242
+
243
+ ## Legacy compatibility
244
+
245
+ The audited v7 extractors use `this.context.player` for Track/Playlist construction,
246
+ source lookup/bridging, diagnostics and `probeTimeout`. `LegacyPlayerContext` is a
247
+ structural source API, backed by the runtime when standalone and by the actual
248
+ Player when embedded. It supports plugins, extractors, search, debug/error events,
249
+ source options, and legacy constructors. Optional `getRemedia(false)` returns
250
+ null standalone; required audio access and queue/voice operations fail explicitly.
251
+ It never instantiates a dummy Player, queue or voice connection.
252
+
253
+ Legacy raw PCM/Opus override streams remain unsupported: return encoded media.
254
+ Queue history, Discord clients and other process-local playback objects are not
255
+ part of HTTP requests. An extractor that needs arbitrary Player/voice APIs or
256
+ cannot reconstruct private state after expiration requires migration; the source
257
+ compatibility surface does not emulate the whole Player API.
258
+
259
+ ## Remedia server integration boundary
260
+
261
+ Remedia dev.5 supports generic host discovery, authenticated operation execution,
262
+ native decoding of single-use grants, and cancellation. Configure the deployment
263
+ in the server's `pluginHosts` list and in the bot's `sourceHosts` list. Installing
264
+ `@remedia/plugin-host@0.1.0-dev.5` alone does not register a host with an audio node.
265
+
266
+ DiscordPlayer sends operation descriptors for playback and staged-next preparation;
267
+ the audio node invokes `source.stream` and consumes encoded bytes directly. The
268
+ bot uses HTTP only for metadata and never redeems a playback grant. Host affinity
269
+ is checked during session selection and before playback/staging. Embedded playback
270
+ continues to execute local registrations and rejects remote-host Tracks.
271
+
272
+ ## Validation
273
+
274
+ `pnpm build`, `pnpm typecheck`, `pnpm test`, `pnpm lint`, and `pnpm format:check`
275
+ cover the workspace. Tests include the published optional extractor collection (SDK calls stubbed),
276
+ real HTTP disconnect/shutdown, and real FFmpeg decoding of generated MP3, Ogg/Opus
277
+ and faststart M4A. FFmpeg must be installed; CI installs it explicitly. No personal
278
+ media files or third-party service access are needed. CI builds the host before discord-player,
279
+ includes its dist artifacts, and publishes it before dependent packages using the
280
+ shared release inventory.
281
+
282
+ ## Remote media policy
283
+
284
+ `mediaPolicy` denies private, loopback, link-local, reserved and transition IP
285
+ ranges by default. DNS results are checked and the selected address is pinned to
286
+ the connection, preserving TLS hostname verification. Every redirect is checked;
287
+ TLS downgrades and URL user/password credentials are rejected. Optional
288
+ `allowedHosts` restricts exact hostnames. No inbound authorization or provider
289
+ request headers are copied to upstreams. Providers needing custom authorization
290
+ must return an encoded Readable opened by their trusted SDK. Plugins execute
291
+ trusted code and can open their own network/file resources; this policy does not
292
+ sandbox extractor `handle()` or provider-created streams.
293
+
294
+ File descriptors require explicit `allowedFileRoots`. Canonical paths are checked,
295
+ then regular files are opened through owned handles. Authorized roots and parent
296
+ directories must be administrator-owned; concurrent hostile filesystem mutation is
297
+ outside this policy's trust boundary. Default limits are 32 active streams, 512 MiB
298
+ per stream, 30 minutes total, five redirects and 30 seconds upstream socket idle.
299
+ Tune `maxConcurrent`, `maxBytes`, `timeoutMs`, and `allowPrivateNetworks` for trusted
300
+ private deployments. Buffers are already allocated by providers; the host bounds
301
+ output bytes but cannot undo that allocation. Streaming uses backpressure; abort,
302
+ consumer disconnect, byte/deadline errors and host shutdown release resources.
303
+ After headers, errors terminate the body/socket, never masquerade as successful
304
+ JSON responses. Use Remedia's Node middleware to connect network disconnects to
305
+ Fetch cancellation. Configure `token` and HTTPS for remote authorization.
306
+
307
+ Default proxy delivery resolves media on grant redemption. Expired provider
308
+ sources are rejected rather than fetching expired credentials; request a fresh
309
+ `source.stream` grant. Provider expiration survives materialization and stream
310
+ transforms. Grant expiration is a separate deadline managed by Remedia.
311
+ `mediaDelivery: 'direct-url'` enables direct delivery for explicitly declared
312
+ `directUrlProviders` only. Invoking an arbitrary `track.stream` handler may already
313
+ open a Readable, so unknown or mixed-source providers must stay deferred. Declare
314
+ only trusted providers whose handlers return URL metadata without opening media:
315
+
316
+ ```ts
317
+ const host = createSourcePluginHost({
318
+ name: 'Sources',
319
+ hostId: 'sources-eu-1',
320
+ publicUrl: 'https://sources.example.com',
321
+ mediaDelivery: 'direct-url',
322
+ directUrlProviders: ['example.url-provider'],
323
+ });
324
+ ```
325
+
326
+ These providers resolve URL metadata at `source.stream` invocation, never search.
327
+ The dispatcher checks active, unblocked transformations for the actual stream
328
+ owner. Applicable byte transforms force the existing secure proxy path, and run
329
+ once on redemption. Unrelated providers' transforms do not prevent direct delivery.
330
+ The check repeats after asynchronous URL preparation so newly registered transforms
331
+ cannot be skipped. A declining URL-only handler defers bridge resolution; a handler
332
+ that returns bytes despite its declaration fails explicitly and its stream is
333
+ destroyed, without a retry. Do not list legacy or mixed-result providers.
334
+
335
+ All other providers, including Readables, files and buffers, resolve and materialize
336
+ only when their grant is redeemed. There is no eager byte-source fallback or retained
337
+ live stream in an unredeemed grant. Cancellation, expiration, provider removal and
338
+ shutdown release transport resources; an old grant cannot bind a replacement
339
+ registration. Provider URL expiration remains intact. The consuming node must still
340
+ independently enforce its DNS/redirect policy for direct URLs. Authentication or
341
+ byte transformations requiring the host must use proxy delivery.
342
+
343
+ Single-use media grants reject Range requests. Remedia dev.5 opens these operation
344
+ results as sequential, non-seekable FFmpeg inputs, avoiding default Range probing.
345
+ This does not add random access or turn grant URLs into durable source references.
346
+ See [the Remedia integration contract](docs/remedia-contract.md) for the current
347
+ protocol and validation limits. Native Remedia playback and Discord voice delivery
348
+ remain outside this repository's HTTP/client integration tests.
@@ -0,0 +1,88 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { SourceRuntimeEvents } from '../legacy-player.js';
3
+ import type { Track } from '../structures/track.js';
4
+ import type { Playlist } from '../structures/playlist.js';
5
+ import type { ExtractorExecutionContext } from './extractor-execution-context.js';
6
+ import type { SearchQueryType } from './query-types.js';
7
+ import type { ExtractorInfo, ExtractorSearchContext, ExtractorStreamable, NextFunction } from './extractor-types.js';
8
+ /** Minimal v7 lifecycle and source API, adapted to the source operation registry. */
9
+ export declare class BaseExtractor<Options extends object = object> {
10
+ /** Owning legacy registration context. */
11
+ context: ExtractorExecutionContext;
12
+ /** Original local extractor configuration. */
13
+ options: Options;
14
+ /** Default identifier; subclasses should override. */
15
+ static identifier: string;
16
+ /** Higher priority wins before match confidence. */
17
+ priority: number;
18
+ /** Supported explicit query protocols. */
19
+ protocols: string[];
20
+ /** Create a compatibility instance; use player.extractors.register to own it.
21
+ * @param context Owning compatibility context.
22
+ * @param options Original extractor options.
23
+ */
24
+ constructor(
25
+ /** Owning legacy registration context. */
26
+ context: ExtractorExecutionContext,
27
+ /** Original local extractor configuration. */
28
+ options?: Options);
29
+ /** Subclass static identifier. */
30
+ get identifier(): string;
31
+ /** Obsolete demux support is unavailable. */
32
+ get supportsDemux(): boolean;
33
+ /** Acquire extractor resources. */
34
+ activate(): Promise<void>;
35
+ /** Release extractor resources. */
36
+ deactivate(): Promise<void>;
37
+ /** Recreate the owned registration with new options.
38
+ * @param options Replacement options.
39
+ */
40
+ reconfigure(options: Options): Promise<void>;
41
+ /** Test whether this extractor accepts a query.
42
+ * @param query Search text.
43
+ * @param type Query hint.
44
+ */
45
+ validate(query: string, type?: SearchQueryType | null): Promise<boolean>;
46
+ /** Resolve metadata.
47
+ * @param query Search text.
48
+ * @param context Search hints.
49
+ */
50
+ handle(query: string, context: ExtractorSearchContext): Promise<ExtractorInfo>;
51
+ /** Resolve media, or decline to enable bridging.
52
+ * @param track Track to stream.
53
+ */
54
+ stream(track: Track): Promise<ExtractorStreamable | undefined>;
55
+ /** Resolve related metadata.
56
+ * @param track Seed track.
57
+ * @param history Optional queue history; local execution only.
58
+ */
59
+ getRelatedTracks(track: Track, history?: unknown): Promise<ExtractorInfo>;
60
+ /** Explicit fallback for another source.
61
+ * @param track Track needing a stream.
62
+ * @param sourceExtractor Original extractor, if legacy.
63
+ */
64
+ bridge(track: Track, sourceExtractor: BaseExtractor<any> | null): Promise<ExtractorStreamable | null>;
65
+ /** Transform an encoded local Readable. Raw codec overrides are unsupported.
66
+ * @param stream Encoded input.
67
+ * @param next Completion callback.
68
+ */
69
+ handlePostStream(stream: Readable, next: NextFunction): void;
70
+ /** Build an anonymous bridge query.
71
+ * @param track Track metadata.
72
+ */
73
+ createBridgeQuery(track: Track): string;
74
+ /** Construct a metadata response.
75
+ * @param playlist Optional playlist.
76
+ * @param tracks Ordered tracks, defaulting to playlist entries.
77
+ */
78
+ createResponse(playlist?: Playlist | null, tracks?: Track[]): ExtractorInfo;
79
+ /** Publish a diagnostic.
80
+ * @param message Diagnostic text.
81
+ */
82
+ debug(message: string): void;
83
+ /** Forward supported Player events.
84
+ * @param event Player event.
85
+ * @param args Event arguments.
86
+ */
87
+ emit<K extends keyof SourceRuntimeEvents>(event: K, ...args: SourceRuntimeEvents[K]): boolean;
88
+ }
@@ -0,0 +1,118 @@
1
+ /** Minimal v7 lifecycle and source API, adapted to the source operation registry. */
2
+ export class BaseExtractor {
3
+ context;
4
+ options;
5
+ /** Default identifier; subclasses should override. */
6
+ static identifier = 'org.example.extractor';
7
+ /** Higher priority wins before match confidence. */
8
+ priority = 1;
9
+ /** Supported explicit query protocols. */
10
+ protocols = [];
11
+ /** Create a compatibility instance; use player.extractors.register to own it.
12
+ * @param context Owning compatibility context.
13
+ * @param options Original extractor options.
14
+ */
15
+ constructor(
16
+ /** Owning legacy registration context. */
17
+ context,
18
+ /** Original local extractor configuration. */
19
+ options = {}) {
20
+ this.context = context;
21
+ this.options = options;
22
+ }
23
+ /** Subclass static identifier. */
24
+ get identifier() {
25
+ return this.constructor.identifier;
26
+ }
27
+ /** Obsolete demux support is unavailable. */
28
+ get supportsDemux() {
29
+ return false;
30
+ }
31
+ /** Acquire extractor resources. */
32
+ async activate() { }
33
+ /** Release extractor resources. */
34
+ async deactivate() { }
35
+ /** Recreate the owned registration with new options.
36
+ * @param options Replacement options.
37
+ */
38
+ async reconfigure(options) {
39
+ await this.context.reconfigure(this.identifier, options);
40
+ }
41
+ /** Test whether this extractor accepts a query.
42
+ * @param query Search text.
43
+ * @param type Query hint.
44
+ */
45
+ async validate(query, type) {
46
+ void query;
47
+ void type;
48
+ return false;
49
+ }
50
+ /** Resolve metadata.
51
+ * @param query Search text.
52
+ * @param context Search hints.
53
+ */
54
+ async handle(query, context) {
55
+ void query;
56
+ void context;
57
+ return this.createResponse();
58
+ }
59
+ /** Resolve media, or decline to enable bridging.
60
+ * @param track Track to stream.
61
+ */
62
+ async stream(track) {
63
+ void track;
64
+ return undefined;
65
+ }
66
+ /** Resolve related metadata.
67
+ * @param track Seed track.
68
+ * @param history Optional queue history; local execution only.
69
+ */
70
+ async getRelatedTracks(track, history) {
71
+ void track;
72
+ void history;
73
+ return this.createResponse();
74
+ }
75
+ /** Explicit fallback for another source.
76
+ * @param track Track needing a stream.
77
+ * @param sourceExtractor Original extractor, if legacy.
78
+ */
79
+ async bridge(track, sourceExtractor) {
80
+ void track;
81
+ void sourceExtractor;
82
+ return null;
83
+ }
84
+ /** Transform an encoded local Readable. Raw codec overrides are unsupported.
85
+ * @param stream Encoded input.
86
+ * @param next Completion callback.
87
+ */
88
+ handlePostStream(stream, next) {
89
+ next(null, stream);
90
+ }
91
+ /** Build an anonymous bridge query.
92
+ * @param track Track metadata.
93
+ */
94
+ createBridgeQuery(track) {
95
+ return `${track.title} by ${track.author}`;
96
+ }
97
+ /** Construct a metadata response.
98
+ * @param playlist Optional playlist.
99
+ * @param tracks Ordered tracks, defaulting to playlist entries.
100
+ */
101
+ createResponse(playlist = null, tracks = playlist?.tracks ?? []) {
102
+ return { playlist, tracks };
103
+ }
104
+ /** Publish a diagnostic.
105
+ * @param message Diagnostic text.
106
+ */
107
+ debug(message) {
108
+ this.context.player.debug(message);
109
+ }
110
+ /** Forward supported Player events.
111
+ * @param event Player event.
112
+ * @param args Event arguments.
113
+ */
114
+ emit(event, ...args) {
115
+ return this.context.player.emit(event, ...args);
116
+ }
117
+ }
118
+ //# sourceMappingURL=base-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-extractor.js","sourceRoot":"","sources":["../../src/compat/base-extractor.ts"],"names":[],"mappings":"AAQA,qFAAqF;AACrF,MAAM,OAAO,aAAa;IAgBf,OAAO;IAEP,OAAO;IAjBhB,sDAAsD;IAC/C,MAAM,CAAC,UAAU,GAAG,uBAAuB,CAAC;IAEnD,oDAAoD;IAC7C,QAAQ,GAAG,CAAC,CAAC;IAEpB,0CAA0C;IACnC,SAAS,GAAa,EAAE,CAAC;IAEhC;;;OAGG;IACH;IACE,0CAA0C;IACnC,OAAkC;IACzC,8CAA8C;IACvC,OAAO,GAAY,EAAa;uBAFhC,OAAO;uBAEP,OAAO;IACb,CAAC;IAEJ,kCAAkC;IAClC,IAAW,UAAU;QACnB,OAAQ,IAAI,CAAC,WAAoC,CAAC,UAAU,CAAC;IAC/D,CAAC;IAED,6CAA6C;IAC7C,IAAW,aAAa;QACtB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,mCAAmC;IAC5B,KAAK,CAAC,QAAQ,KAAmB,CAAC;IAEzC,mCAAmC;IAC5B,KAAK,CAAC,UAAU,KAAmB,CAAC;IAE3C;;OAEG;IACI,KAAK,CAAC,WAAW,CAAC,OAAgB;QACvC,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,KAAa,EAAE,IAA6B;QAChE,KAAK,KAAK,CAAC;QACX,KAAK,IAAI,CAAC;QAEV,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAA+B;QAChE,KAAK,KAAK,CAAC;QACX,KAAK,OAAO,CAAC;QAEb,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IAC/B,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,MAAM,CAAC,KAAY;QAC9B,KAAK,KAAK,CAAC;QAEX,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,gBAAgB,CAAC,KAAY,EAAE,OAAiB;QAC3D,KAAK,KAAK,CAAC;QACX,KAAK,OAAO,CAAC;QAEb,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,MAAM,CAAC,KAAY,EAAE,eAA0C;QAC1E,KAAK,KAAK,CAAC;QACX,KAAK,eAAe,CAAC;QAErB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACI,gBAAgB,CAAC,MAAgB,EAAE,IAAkB;QAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrB,CAAC;IAED;;OAEG;IACI,iBAAiB,CAAC,KAAY;QACnC,OAAO,GAAG,KAAK,CAAC,KAAK,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACI,cAAc,CAAC,QAAQ,GAAoB,IAAI,EAAE,MAAM,GAAY,QAAQ,EAAE,MAAM,IAAI,EAAE;QAC9F,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,OAAe;QAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED;;;OAGG;IACI,IAAI,CAAsC,KAAQ,EAAE,GAAG,IAA4B;QACxF,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAI,IAAc,CAAC,CAAC;IAC7D,CAAC;CACF"}
@@ -0,0 +1,69 @@
1
+ import type { LegacyPlayerContext } from '../legacy-player.js';
2
+ import type { Track } from '../structures/track.js';
3
+ import type { SourceStream } from '../plugins/types.js';
4
+ import { BaseExtractor } from './base-extractor.js';
5
+ import type { ExtractorConstructor } from './extractor-types.js';
6
+ /** Small legacy ownership adapter; all execution goes through player.plugins. */
7
+ export declare class ExtractorExecutionContext {
8
+ /** Player owning legacy registrations. */
9
+ readonly player: LegacyPlayerContext;
10
+ private bridgeAttempts;
11
+ private instances;
12
+ /** Read-only view of registered legacy instances. */
13
+ get store(): ReadonlyMap<string, BaseExtractor<any>>;
14
+ /** Number of legacy registrations. */
15
+ get size(): number;
16
+ /** Create an adapter owned by one Player.
17
+ * @param player Owning Player.
18
+ */
19
+ constructor(
20
+ /** Player owning legacy registrations. */
21
+ player: LegacyPlayerContext);
22
+ /** Look up a legacy registration.
23
+ * @param id Extractor identifier.
24
+ */
25
+ get(id: string): BaseExtractor<any> | undefined;
26
+ /** Test legacy registration membership.
27
+ * @param id Extractor identifier.
28
+ */
29
+ isRegistered(id: string): boolean;
30
+ /** Check the Player block list.
31
+ * @param id Extractor identifier.
32
+ */
33
+ isDisabled(id: string): boolean;
34
+ /** Check whether a source is allowed.
35
+ * @param id Extractor identifier.
36
+ */
37
+ isEnabled(id: string): boolean;
38
+ /** Register a legacy class through a source plugin.
39
+ * @param Extractor Legacy BaseExtractor subclass constructor.
40
+ * @param options Original options passed to the instance.
41
+ * @returns The owned compatibility instance.
42
+ */
43
+ register<C extends ExtractorConstructor>(Extractor: C, ...args: {} extends ConstructorParameters<C>[1] ? [options?: ConstructorParameters<C>[1]] : [options: ConstructorParameters<C>[1]]): Promise<InstanceType<C>>;
44
+ /** Remove an extractor and its source subscriptions.
45
+ * @param extractor ID or owned instance.
46
+ */
47
+ unregister(extractor: string | BaseExtractor<any>): Promise<boolean>;
48
+ /** Remove all legacy registrations. */
49
+ unregisterAll(): Promise<void>;
50
+ /** Replace an instance through the same registration path.
51
+ * @param id Legacy ID.
52
+ * @param options Replacement options.
53
+ */
54
+ reconfigure(id: string, options: object): Promise<BaseExtractor<any>>;
55
+ /** Register a bundle sequentially with deterministic precedence.
56
+ * @param bundle Legacy constructors.
57
+ * @param options Options keyed by identifier.
58
+ */
59
+ loadMulti(bundle: readonly ExtractorConstructor[], options?: Record<string, object>): Promise<void>;
60
+ /** Request explicit bridge fallback using the shared registry.
61
+ * @param track Track needing a bridge.
62
+ * @param sourceExtractor Original provider.
63
+ */
64
+ requestBridge(track: Track, sourceExtractor?: BaseExtractor<any> | null): Promise<{
65
+ result: SourceStream;
66
+ error: null;
67
+ extractor: BaseExtractor<any> | null;
68
+ }>;
69
+ }