@iobroker/youtube-lounge 0.0.0-stage → 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.
@@ -0,0 +1,111 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type YouTubeTarget } from './target.js';
3
+ /** The YouTube app of a TV as the lounge knows it; store it to reconnect without pairing again */
4
+ export interface LoungeScreen {
5
+ screenId: string;
6
+ loungeToken: string;
7
+ /** Name of the screen, only known after pairing with a TV code */
8
+ name?: string;
9
+ }
10
+ /** Player states as the lounge reports them */
11
+ export declare enum PlayerState {
12
+ Unstarted = -1,
13
+ Ended = 0,
14
+ Playing = 1,
15
+ Paused = 2,
16
+ Buffering = 3,
17
+ Cued = 5,
18
+ Advertisement = 1081
19
+ }
20
+ /** What the TV plays */
21
+ export interface NowPlaying {
22
+ videoId: string;
23
+ listId: string;
24
+ /** Position in seconds */
25
+ currentTime: number;
26
+ /** Length in seconds, 0 if not known yet */
27
+ duration: number;
28
+ state: PlayerState;
29
+ }
30
+ export interface LoungeOptions {
31
+ /** Name of this remote control as the TV shows it */
32
+ name?: string;
33
+ /** Stable ID of this remote control */
34
+ deviceId?: string;
35
+ /** For tests */
36
+ fetch?: typeof fetch;
37
+ /** For tests */
38
+ api?: string;
39
+ }
40
+ type Params = Record<string, string | number | undefined>;
41
+ /**
42
+ * A remote control session with the YouTube app of a TV over the YouTube Lounge API ("Link with TV"). The
43
+ * requests go to Google's servers; the protocol follows pyytlounge (FabioGNR/pyytlounge). Google does not document
44
+ * it, so it may change.
45
+ *
46
+ * Events: `nowPlaying` (NowPlaying | null), `volume` ({ volume, muted }), `screen` (boolean: the YouTube app of
47
+ * the TV joined or left the lounge), `disconnected` (the TV left the lounge), `error` (Error of the long poll).
48
+ */
49
+ export declare class YouTubeLounge extends EventEmitter {
50
+ private screenInfo;
51
+ private sid;
52
+ private gsessionid;
53
+ private aid;
54
+ private rid;
55
+ private ofs;
56
+ private polling;
57
+ private readonly name;
58
+ private readonly deviceId;
59
+ private readonly fetchFn;
60
+ private readonly api;
61
+ private current;
62
+ private screenUp;
63
+ constructor(options?: LoungeOptions);
64
+ /** The paired screen; `null` before pairing */
65
+ get screen(): LoungeScreen | null;
66
+ get connected(): boolean;
67
+ /** Whether the YouTube app of the TV is in the lounge; Google accepts commands without it, but they get lost */
68
+ get screenOnline(): boolean;
69
+ /** What the TV plays, as last reported */
70
+ get nowPlaying(): NowPlaying | null;
71
+ /** Use a screen stored from an earlier pairing */
72
+ useScreen(screen: LoungeScreen): void;
73
+ /** Pair with the screen ID the YouTube app reports over DIAL, see `getYouTubeScreenId` */
74
+ pairWithScreenId(screenId: string): Promise<LoungeScreen>;
75
+ /** Pair with the code the YouTube app of the TV shows under Settings > Link with TV code */
76
+ pairWithCode(code: string): Promise<LoungeScreen>;
77
+ /** Open a session; the answer brings what the TV plays */
78
+ connect(): Promise<void>;
79
+ /** Send a lounge command, e.g. setPlaylist, play, pause, next, previous */
80
+ command(name: string, params?: Params): Promise<void>;
81
+ /**
82
+ * Wait until the YouTube app of the TV is in the lounge, e.g. after the TV or the app was started. Starts the
83
+ * long poll if it does not run (`close()` ends it).
84
+ *
85
+ * @param timeout in ms
86
+ * @returns whether the app is there
87
+ */
88
+ waitForScreen(timeout?: number): Promise<boolean>;
89
+ /** Play a target of `parseYouTubeTarget`; the TV starts the YouTube app if it can */
90
+ play(target: YouTubeTarget): Promise<void>;
91
+ /** Jump to a position in seconds */
92
+ seekTo(seconds: number): Promise<void>;
93
+ /** Mute or unmute the YouTube player */
94
+ setMuted(muted: boolean): Promise<void>;
95
+ /** Set the volume 0-100 of the YouTube player */
96
+ setVolume(volume: number): Promise<void>;
97
+ /** Follow the TV: emits `nowPlaying` and `volume` until `close()` */
98
+ subscribe(): void;
99
+ /** Stop following the TV */
100
+ close(): void;
101
+ private poll;
102
+ private bind;
103
+ private sendCommand;
104
+ private sessionParams;
105
+ private consume;
106
+ private setScreen;
107
+ /** `nowPlaying` names the video, `onStateChange` only updates state and position */
108
+ private update;
109
+ private post;
110
+ }
111
+ export {};
@@ -0,0 +1,390 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { ChunkParser, parseBindChunks } from './chunks.js';
4
+ import { uploadsPlaylistId } from './target.js';
5
+ const API = 'https://www.youtube.com/api/lounge';
6
+ const TIMEOUT = 10_000;
7
+ /** Pause before the long poll starts again after an error: 5 s, doubled per failure, at most 5 min */
8
+ const RETRY_DELAY = 5_000;
9
+ const RETRY_DELAY_MAX = 300_000;
10
+ /** Player states as the lounge reports them */
11
+ export var PlayerState;
12
+ (function (PlayerState) {
13
+ PlayerState[PlayerState["Unstarted"] = -1] = "Unstarted";
14
+ PlayerState[PlayerState["Ended"] = 0] = "Ended";
15
+ PlayerState[PlayerState["Playing"] = 1] = "Playing";
16
+ PlayerState[PlayerState["Paused"] = 2] = "Paused";
17
+ PlayerState[PlayerState["Buffering"] = 3] = "Buffering";
18
+ PlayerState[PlayerState["Cued"] = 5] = "Cued";
19
+ PlayerState[PlayerState["Advertisement"] = 1081] = "Advertisement";
20
+ })(PlayerState || (PlayerState = {}));
21
+ /**
22
+ * A remote control session with the YouTube app of a TV over the YouTube Lounge API ("Link with TV"). The
23
+ * requests go to Google's servers; the protocol follows pyytlounge (FabioGNR/pyytlounge). Google does not document
24
+ * it, so it may change.
25
+ *
26
+ * Events: `nowPlaying` (NowPlaying | null), `volume` ({ volume, muted }), `screen` (boolean: the YouTube app of
27
+ * the TV joined or left the lounge), `disconnected` (the TV left the lounge), `error` (Error of the long poll).
28
+ */
29
+ export class YouTubeLounge extends EventEmitter {
30
+ screenInfo = null;
31
+ sid = '';
32
+ gsessionid = '';
33
+ aid = 0;
34
+ rid = 1;
35
+ ofs = 0;
36
+ polling = null;
37
+ name;
38
+ deviceId;
39
+ fetchFn;
40
+ api;
41
+ current = null;
42
+ screenUp = false;
43
+ constructor(options = {}) {
44
+ super();
45
+ this.name = options.name ?? 'ioBroker';
46
+ this.deviceId = options.deviceId ?? randomUUID();
47
+ this.fetchFn = options.fetch ?? fetch;
48
+ this.api = options.api ?? API;
49
+ }
50
+ /** The paired screen; `null` before pairing */
51
+ get screen() {
52
+ return this.screenInfo;
53
+ }
54
+ get connected() {
55
+ return !!(this.sid && this.gsessionid);
56
+ }
57
+ /** Whether the YouTube app of the TV is in the lounge; Google accepts commands without it, but they get lost */
58
+ get screenOnline() {
59
+ return this.screenUp;
60
+ }
61
+ /** What the TV plays, as last reported */
62
+ get nowPlaying() {
63
+ return this.current;
64
+ }
65
+ /** Use a screen stored from an earlier pairing */
66
+ useScreen(screen) {
67
+ this.screenInfo = { ...screen };
68
+ this.sid = this.gsessionid = '';
69
+ }
70
+ /** Pair with the screen ID the YouTube app reports over DIAL, see `getYouTubeScreenId` */
71
+ async pairWithScreenId(screenId) {
72
+ const { status, text } = await this.post(`${this.api}/pairing/get_lounge_token_batch`, {
73
+ screen_ids: screenId,
74
+ });
75
+ const screen = status === 200
76
+ ? JSON.parse(text).screens?.[0]
77
+ : undefined;
78
+ if (!screen?.loungeToken) {
79
+ throw new Error(`YouTube: no lounge token for this screen (HTTP ${status})`);
80
+ }
81
+ this.useScreen({ screenId: screen.screenId ?? screenId, loungeToken: screen.loungeToken });
82
+ return this.screenInfo;
83
+ }
84
+ /** Pair with the code the YouTube app of the TV shows under Settings > Link with TV code */
85
+ async pairWithCode(code) {
86
+ const { status, text } = await this.post(`${this.api}/pairing/get_screen`, {
87
+ pairing_code: String(code).replace(/\D/g, ''),
88
+ });
89
+ const screen = status === 200
90
+ ? JSON.parse(text).screen
91
+ : undefined;
92
+ if (!screen?.loungeToken || !screen.screenId) {
93
+ throw new Error(`YouTube: the TV code was not accepted (HTTP ${status})`);
94
+ }
95
+ this.useScreen({ screenId: screen.screenId, loungeToken: screen.loungeToken, name: screen.name });
96
+ return this.screenInfo;
97
+ }
98
+ /** Open a session; the answer brings what the TV plays */
99
+ async connect() {
100
+ if (!this.screenInfo) {
101
+ throw new Error('YouTube: not paired');
102
+ }
103
+ this.rid = 1;
104
+ this.ofs = 0;
105
+ let { status, text } = await this.bind();
106
+ if (status === 401 || status === 410) {
107
+ // the lounge token expired: a new one for the same screen
108
+ await this.pairWithScreenId(this.screenInfo.screenId);
109
+ ({ status, text } = await this.bind());
110
+ }
111
+ if (status !== 200) {
112
+ throw new Error(`YouTube: connection refused (HTTP ${status})`);
113
+ }
114
+ this.consume(parseBindChunks(text));
115
+ if (!this.connected) {
116
+ throw new Error('YouTube: no session');
117
+ }
118
+ }
119
+ /** Send a lounge command, e.g. setPlaylist, play, pause, next, previous */
120
+ async command(name, params = {}) {
121
+ if (!this.connected) {
122
+ await this.connect();
123
+ }
124
+ let result = await this.sendCommand(name, params);
125
+ if (result.status === 400 && /Unknown SID/i.test(result.text)) {
126
+ // the session ended: once more with a new one
127
+ this.sid = this.gsessionid = '';
128
+ await this.connect();
129
+ result = await this.sendCommand(name, params);
130
+ }
131
+ if (result.status !== 200) {
132
+ throw new Error(`YouTube: command ${name} failed (HTTP ${result.status})`);
133
+ }
134
+ }
135
+ /**
136
+ * Wait until the YouTube app of the TV is in the lounge, e.g. after the TV or the app was started. Starts the
137
+ * long poll if it does not run (`close()` ends it).
138
+ *
139
+ * @param timeout in ms
140
+ * @returns whether the app is there
141
+ */
142
+ async waitForScreen(timeout = 30_000) {
143
+ if (!this.connected) {
144
+ // the answer names the devices in the lounge
145
+ await this.connect();
146
+ }
147
+ if (this.screenUp) {
148
+ return true;
149
+ }
150
+ this.subscribe();
151
+ return new Promise(resolve => {
152
+ const wait = {};
153
+ const onScreen = (online) => {
154
+ if (online) {
155
+ clearTimeout(wait.timer);
156
+ this.off('screen', onScreen);
157
+ resolve(true);
158
+ }
159
+ };
160
+ wait.timer = setTimeout(() => {
161
+ this.off('screen', onScreen);
162
+ resolve(false);
163
+ }, timeout);
164
+ this.on('screen', onScreen);
165
+ });
166
+ }
167
+ /** Play a target of `parseYouTubeTarget`; the TV starts the YouTube app if it can */
168
+ play(target) {
169
+ switch (target.kind) {
170
+ case 'video':
171
+ return this.command('setPlaylist', { videoId: target.videoId, currentTime: 0, currentIndex: -1 });
172
+ case 'playlist':
173
+ return this.command('setPlaylist', {
174
+ listId: target.listId,
175
+ videoId: target.videoId,
176
+ currentIndex: 0,
177
+ currentTime: 0,
178
+ });
179
+ case 'channel':
180
+ // the channel page cannot be opened this way, its uploads can
181
+ return this.command('setPlaylist', {
182
+ listId: uploadsPlaylistId(target.channelId),
183
+ currentIndex: 0,
184
+ currentTime: 0,
185
+ });
186
+ default:
187
+ return this.command(target.command);
188
+ }
189
+ }
190
+ /** Jump to a position in seconds */
191
+ seekTo(seconds) {
192
+ return this.command('seekTo', { newTime: Math.max(0, Math.round(seconds)) });
193
+ }
194
+ /** Mute or unmute the YouTube player */
195
+ setMuted(muted) {
196
+ return this.command(muted ? 'mute' : 'unMute');
197
+ }
198
+ /** Set the volume 0-100 of the YouTube player */
199
+ setVolume(volume) {
200
+ return this.command('setVolume', { volume: Math.min(100, Math.max(0, Math.round(volume))) });
201
+ }
202
+ /** Follow the TV: emits `nowPlaying` and `volume` until `close()` */
203
+ subscribe() {
204
+ if (this.polling) {
205
+ return;
206
+ }
207
+ this.polling = new AbortController();
208
+ void this.poll(this.polling.signal);
209
+ }
210
+ /** Stop following the TV */
211
+ close() {
212
+ this.polling?.abort();
213
+ this.polling = null;
214
+ }
215
+ async poll(signal) {
216
+ let failures = 0;
217
+ while (!signal.aborted) {
218
+ try {
219
+ if (!this.connected) {
220
+ await this.connect();
221
+ }
222
+ const url = new URL(`${this.api}/bc/bind`);
223
+ for (const [key, value] of Object.entries(this.sessionParams({ RID: 'rpc', CI: 0, TYPE: 'xmlhttp' }))) {
224
+ if (value !== undefined) {
225
+ url.searchParams.set(key, String(value));
226
+ }
227
+ }
228
+ const res = await this.fetchFn(url, { signal });
229
+ if (res.status === 400 || res.status === 404 || res.status === 410) {
230
+ // session or token ended
231
+ this.sid = this.gsessionid = '';
232
+ await res.body?.cancel();
233
+ continue;
234
+ }
235
+ if (res.status !== 200 || !res.body) {
236
+ throw new Error(`YouTube: long poll failed (HTTP ${res.status})`);
237
+ }
238
+ failures = 0;
239
+ const parser = new ChunkParser();
240
+ const reader = res.body.getReader();
241
+ for (;;) {
242
+ const { done, value } = await reader.read();
243
+ if (done) {
244
+ break;
245
+ }
246
+ this.consume(parser.push(value));
247
+ }
248
+ }
249
+ catch (e) {
250
+ if (signal.aborted) {
251
+ return;
252
+ }
253
+ // without a listener an `error` event would end the process
254
+ if (this.listenerCount('error')) {
255
+ this.emit('error', e instanceof Error ? e : new Error(String(e)));
256
+ }
257
+ failures++;
258
+ await new Promise(resolve => setTimeout(resolve, Math.min(RETRY_DELAY_MAX, RETRY_DELAY * 2 ** (failures - 1))));
259
+ }
260
+ }
261
+ }
262
+ bind() {
263
+ return this.post(`${this.api}/bc/bind`, {
264
+ app: 'web',
265
+ 'mdx-version': 3,
266
+ name: this.name,
267
+ id: this.deviceId,
268
+ device: 'REMOTE_CONTROL',
269
+ capabilities: 'que,dsdtr,atp',
270
+ magnaKey: 'cloudPairedDevice',
271
+ ui: 'false',
272
+ theme: 'cl',
273
+ loungeIdToken: this.screenInfo?.loungeToken,
274
+ }, { RID: this.rid, VER: 8, CVER: 1, auth_failure_option: 'send_error' });
275
+ }
276
+ sendCommand(name, params) {
277
+ const form = { count: 1, ofs: this.ofs++, req0__sc: name };
278
+ for (const [key, value] of Object.entries(params)) {
279
+ form[`req0_${key}`] = value;
280
+ }
281
+ this.rid++;
282
+ return this.post(`${this.api}/bc/bind`, form, this.sessionParams({ RID: this.rid, CVER: 1 }));
283
+ }
284
+ sessionParams(extra) {
285
+ return {
286
+ name: this.name,
287
+ loungeIdToken: this.screenInfo?.loungeToken,
288
+ SID: this.sid,
289
+ AID: this.aid,
290
+ gsessionid: this.gsessionid,
291
+ device: 'REMOTE_CONTROL',
292
+ app: 'youtube-desktop',
293
+ VER: 8,
294
+ v: 2,
295
+ ...extra,
296
+ };
297
+ }
298
+ consume(events) {
299
+ for (const [id, [type, ...args]] of events) {
300
+ this.aid = id;
301
+ const data = (args[0] ?? {});
302
+ switch (type) {
303
+ case 'c':
304
+ this.sid = String(args[0]);
305
+ break;
306
+ case 'S':
307
+ this.gsessionid = String(args[0]);
308
+ break;
309
+ case 'nowPlaying':
310
+ // only the screen reports what plays
311
+ this.setScreen(true);
312
+ this.update(data, true);
313
+ break;
314
+ case 'onStateChange':
315
+ this.setScreen(true);
316
+ this.update(data, false);
317
+ break;
318
+ case 'loungeStatus':
319
+ this.setScreen(hasScreen(data.devices));
320
+ break;
321
+ case 'onVolumeChanged':
322
+ this.emit('volume', { volume: Number(data.volume), muted: data.muted === 'true' });
323
+ break;
324
+ case 'loungeScreenDisconnected':
325
+ this.sid = this.gsessionid = '';
326
+ this.setScreen(false);
327
+ this.emit('disconnected');
328
+ break;
329
+ default:
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ setScreen(online) {
335
+ if (online !== this.screenUp) {
336
+ this.screenUp = online;
337
+ this.emit('screen', online);
338
+ }
339
+ }
340
+ /** `nowPlaying` names the video, `onStateChange` only updates state and position */
341
+ update(data, newVideo) {
342
+ if (newVideo && !data.videoId) {
343
+ this.current = null;
344
+ this.emit('nowPlaying', null);
345
+ return;
346
+ }
347
+ const number = (value, fallback) => value === undefined || value === '' || Number.isNaN(Number(value)) ? fallback : Number(value);
348
+ const base = newVideo || !this.current ? null : this.current;
349
+ this.current = {
350
+ videoId: typeof data.videoId === 'string' ? data.videoId : (base?.videoId ?? ''),
351
+ listId: typeof data.listId === 'string' ? data.listId : (base?.listId ?? ''),
352
+ currentTime: number(data.currentTime, base?.currentTime ?? 0),
353
+ duration: number(data.duration, base?.duration ?? 0),
354
+ state: number(data.state, base?.state ?? PlayerState.Unstarted),
355
+ };
356
+ this.emit('nowPlaying', this.current);
357
+ }
358
+ async post(url, form, params = {}) {
359
+ const target = new URL(url);
360
+ for (const [key, value] of Object.entries(params)) {
361
+ if (value !== undefined) {
362
+ target.searchParams.set(key, String(value));
363
+ }
364
+ }
365
+ const body = new URLSearchParams();
366
+ for (const [key, value] of Object.entries(form)) {
367
+ if (value !== undefined) {
368
+ body.set(key, String(value));
369
+ }
370
+ }
371
+ const res = await this.fetchFn(target, {
372
+ method: 'POST',
373
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
374
+ body,
375
+ signal: AbortSignal.timeout(TIMEOUT),
376
+ });
377
+ return { status: res.status, text: await res.text() };
378
+ }
379
+ }
380
+ /** `devices` of `loungeStatus` is a JSON string; the YouTube app of the TV is the `LOUNGE_SCREEN` */
381
+ function hasScreen(devices) {
382
+ try {
383
+ const list = JSON.parse(String(devices));
384
+ return Array.isArray(list) && list.some(device => device.type === 'LOUNGE_SCREEN');
385
+ }
386
+ catch {
387
+ return false;
388
+ }
389
+ }
390
+ //# sourceMappingURL=lounge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lounge.js","sourceRoot":"","sources":["../../src/lounge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": false
4
+ }
@@ -0,0 +1,25 @@
1
+ /** What to play: a video, a playlist, the uploads of a channel, or a player command */
2
+ export type YouTubeTarget = {
3
+ kind: 'video';
4
+ videoId: string;
5
+ } | {
6
+ kind: 'playlist';
7
+ listId: string;
8
+ videoId?: string;
9
+ } | {
10
+ kind: 'channel';
11
+ channelId: string;
12
+ } | {
13
+ kind: 'command';
14
+ command: PlayerCommand;
15
+ };
16
+ export type PlayerCommand = 'play' | 'pause' | 'next' | 'previous';
17
+ /**
18
+ * Understand what to play.
19
+ *
20
+ * @param input a video ID, `PL…`, `UC…`, a youtube.com / youtu.be URL, or `play`, `pause`, `next`, `previous`
21
+ * @throws {Error} for anything else; handles (`@name`) need the YouTube Data API to be resolved
22
+ */
23
+ export declare function parseYouTubeTarget(input: string): YouTubeTarget;
24
+ /** The uploads playlist of a channel: `UC…` -> `UU…` */
25
+ export declare function uploadsPlaylistId(channelId: string): string;
@@ -0,0 +1,59 @@
1
+ const COMMANDS = ['play', 'pause', 'next', 'previous'];
2
+ const VIDEO_ID = /^[\w-]{11}$/;
3
+ const CHANNEL_ID = /^UC[\w-]{22}$/;
4
+ const LIST_ID = /^(?:PL|UU|LL|FL|OL|RD)[\w-]{10,}$/;
5
+ /**
6
+ * Understand what to play.
7
+ *
8
+ * @param input a video ID, `PL…`, `UC…`, a youtube.com / youtu.be URL, or `play`, `pause`, `next`, `previous`
9
+ * @throws {Error} for anything else; handles (`@name`) need the YouTube Data API to be resolved
10
+ */
11
+ export function parseYouTubeTarget(input) {
12
+ const text = String(input ?? '').trim();
13
+ const command = text.toLowerCase();
14
+ if (COMMANDS.includes(command)) {
15
+ return { kind: 'command', command: command };
16
+ }
17
+ let url = null;
18
+ try {
19
+ url = new URL(/^https?:\/\//i.test(text) ? text : `https://${text}`);
20
+ }
21
+ catch {
22
+ url = null;
23
+ }
24
+ if (url && /(^|\.)(youtube\.com|youtu\.be)$/i.test(url.hostname)) {
25
+ const list = url.searchParams.get('list');
26
+ const video = url.hostname.endsWith('youtu.be') ? url.pathname.slice(1) : url.searchParams.get('v');
27
+ if (list && LIST_ID.test(list)) {
28
+ return video && VIDEO_ID.test(video)
29
+ ? { kind: 'playlist', listId: list, videoId: video }
30
+ : { kind: 'playlist', listId: list };
31
+ }
32
+ if (video && VIDEO_ID.test(video)) {
33
+ return { kind: 'video', videoId: video };
34
+ }
35
+ const channel = /^\/channel\/(UC[\w-]{22})/.exec(url.pathname)?.[1];
36
+ if (channel) {
37
+ return { kind: 'channel', channelId: channel };
38
+ }
39
+ const short = /^\/(?:shorts|live)\/([\w-]{11})/.exec(url.pathname)?.[1];
40
+ if (short) {
41
+ return { kind: 'video', videoId: short };
42
+ }
43
+ }
44
+ if (CHANNEL_ID.test(text)) {
45
+ return { kind: 'channel', channelId: text };
46
+ }
47
+ if (LIST_ID.test(text)) {
48
+ return { kind: 'playlist', listId: text };
49
+ }
50
+ if (VIDEO_ID.test(text)) {
51
+ return { kind: 'video', videoId: text };
52
+ }
53
+ throw new Error(`Not a YouTube video, playlist or channel: "${text}"`);
54
+ }
55
+ /** The uploads playlist of a channel: `UC…` -> `UU…` */
56
+ export function uploadsPlaylistId(channelId) {
57
+ return `UU${channelId.slice(2)}`;
58
+ }
59
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../src/target.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { ChunkParser, parseBindChunks, type LoungeEvent } from './chunks.js';
2
+ export { discoverDialDevices, getYouTubeScreenId, launchYouTubeOverDial, readDialDevice, type DialDevice, } from './dial.js';
3
+ export { PlayerState, YouTubeLounge, type LoungeOptions, type LoungeScreen, type NowPlaying } from './lounge.js';
4
+ export { parseYouTubeTarget, uploadsPlaylistId, type PlayerCommand, type YouTubeTarget } from './target.js';
package/build/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadsPlaylistId = exports.parseYouTubeTarget = exports.YouTubeLounge = exports.PlayerState = exports.readDialDevice = exports.launchYouTubeOverDial = exports.getYouTubeScreenId = exports.discoverDialDevices = exports.parseBindChunks = exports.ChunkParser = void 0;
4
+ var chunks_js_1 = require("./chunks.js");
5
+ Object.defineProperty(exports, "ChunkParser", { enumerable: true, get: function () { return chunks_js_1.ChunkParser; } });
6
+ Object.defineProperty(exports, "parseBindChunks", { enumerable: true, get: function () { return chunks_js_1.parseBindChunks; } });
7
+ var dial_js_1 = require("./dial.js");
8
+ Object.defineProperty(exports, "discoverDialDevices", { enumerable: true, get: function () { return dial_js_1.discoverDialDevices; } });
9
+ Object.defineProperty(exports, "getYouTubeScreenId", { enumerable: true, get: function () { return dial_js_1.getYouTubeScreenId; } });
10
+ Object.defineProperty(exports, "launchYouTubeOverDial", { enumerable: true, get: function () { return dial_js_1.launchYouTubeOverDial; } });
11
+ Object.defineProperty(exports, "readDialDevice", { enumerable: true, get: function () { return dial_js_1.readDialDevice; } });
12
+ var lounge_js_1 = require("./lounge.js");
13
+ Object.defineProperty(exports, "PlayerState", { enumerable: true, get: function () { return lounge_js_1.PlayerState; } });
14
+ Object.defineProperty(exports, "YouTubeLounge", { enumerable: true, get: function () { return lounge_js_1.YouTubeLounge; } });
15
+ var target_js_1 = require("./target.js");
16
+ Object.defineProperty(exports, "parseYouTubeTarget", { enumerable: true, get: function () { return target_js_1.parseYouTubeTarget; } });
17
+ Object.defineProperty(exports, "uploadsPlaylistId", { enumerable: true, get: function () { return target_js_1.uploadsPlaylistId; } });
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,yCAA6E;AAApE,wGAAA,WAAW,OAAA;AAAE,4GAAA,eAAe,OAAA;AACrC,qCAMmB;AALf,8GAAA,mBAAmB,OAAA;AACnB,6GAAA,kBAAkB,OAAA;AAClB,gHAAA,qBAAqB,OAAA;AACrB,yGAAA,cAAc,OAAA;AAGlB,yCAAiH;AAAxG,wGAAA,WAAW,OAAA;AAAE,0GAAA,aAAa,OAAA;AACnC,yCAA4G;AAAnG,+GAAA,kBAAkB,OAAA;AAAE,8GAAA,iBAAiB,OAAA"}