@chatpanel/events 0.30.0 → 0.31.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.
@@ -170,6 +170,54 @@ export function pickCaptionTrack(tracks, { language = '', languages = ['en'] } =
170
170
  .sort((a, b) => b.s - a.s)[0].t;
171
171
  }
172
172
 
173
+ // --------------------------------------------------------------------------
174
+ // The InnerTube player request — how a caption URL that WORKS is obtained
175
+ // --------------------------------------------------------------------------
176
+ //
177
+ // The caption URLs printed into the watch page's HTML answer HTTP 200 with an EMPTY BODY —
178
+ // measured on every video tried, with and without session cookies, Referer and Origin. The
179
+ // ones returned by the InnerTube player endpoint for the ANDROID client do not.
180
+ //
181
+ // AND THE CLIENT VERSION IS THE WHOLE DIFFERENCE, which is worth stating because it is
182
+ // invisible and it will go stale:
183
+ //
184
+ // clientVersion 20.10.38 -> 1 track, 60,441 bytes of captions
185
+ // clientVersion 19.09.37 -> no captionTracks at all
186
+ // clientVersion 17.31.35 -> no captionTracks at all
187
+ //
188
+ // A stale version does not error. It returns a well-formed player response with the
189
+ // `captions` block missing, which reads exactly like "this video has no subtitles" — so the
190
+ // failure mode of letting this rot is a feature that quietly claims videos have no captions.
191
+ // tests/media-transcript.test.js pins the shape; a live check is the client's job.
192
+
193
+ /** The InnerTube client whose player response carries usable caption URLs. */
194
+ export const INNERTUBE_ANDROID = Object.freeze({ clientName: 'ANDROID', clientVersion: '20.10.38' });
195
+
196
+ /** The public InnerTube key is printed into every watch page; it is not a secret. */
197
+ export function innertubeApiKeyFromHtml(html) {
198
+ const m = /"INNERTUBE_API_KEY":\s*"([^"]+)"/.exec(String(html || ''))
199
+ || /INNERTUBE_API_KEY\\":\\"([^\\"]+)/.exec(String(html || ''));
200
+ return m ? m[1] : '';
201
+ }
202
+
203
+ /**
204
+ * The request to make, as data — so the caller performs it wherever its network is.
205
+ *
206
+ * Returned rather than sent for the same reason nothing else here fetches: the extension, the
207
+ * bridge and a mobile client each have their own idea of what "fetch" means, and this file
208
+ * has to run in all three.
209
+ */
210
+ export function innertubePlayerRequest(videoId, { apiKey = '', client = INNERTUBE_ANDROID } = {}) {
211
+ if (!videoId) return null;
212
+ const query = apiKey ? `?key=${encodeURIComponent(apiKey)}` : '';
213
+ return {
214
+ url: `https://www.youtube.com/youtubei/v1/player${query}`,
215
+ method: 'POST',
216
+ headers: { 'content-type': 'application/json' },
217
+ body: JSON.stringify({ context: { client: { ...client } }, videoId }),
218
+ };
219
+ }
220
+
173
221
  /**
174
222
  * Ask a caption URL for a specific serialisation.
175
223
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.30.0",
4
- "description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
3
+ "version": "0.31.0",
4
+ "description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {