@khorsheed/dsh-reader 0.3.0 → 0.3.2

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 (89) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.en.md +26 -121
  3. package/README.i18n.yaml +3 -3
  4. package/README.md +26 -121
  5. package/cordis.patch.yml +7 -0
  6. package/lib/client.js +29105 -6692
  7. package/lib/index.js +3500 -321
  8. package/lib/typert.host.js +2145 -259
  9. package/lib/typert.remote-client.d.ts +97 -3
  10. package/lib/typert.remote-client.js +1590 -250
  11. package/lib/types/captions.d.ts +99 -0
  12. package/lib/types/captions.js +236 -0
  13. package/lib/types/client/MediaPlayer.d.ts +55 -0
  14. package/lib/types/client/MediaPlayer.js +106 -0
  15. package/lib/types/client/NotesView.d.ts +57 -0
  16. package/lib/types/client/NotesView.js +124 -0
  17. package/lib/types/client/ReaderPane.d.ts +20 -3
  18. package/lib/types/client/ReaderPane.js +860 -164
  19. package/lib/types/client/SpeakerLegend.d.ts +18 -0
  20. package/lib/types/client/SpeakerLegend.js +26 -0
  21. package/lib/types/client/canvas-sync.d.ts +84 -0
  22. package/lib/types/client/canvas-sync.js +59 -0
  23. package/lib/types/client/contract.d.ts +130 -3
  24. package/lib/types/client/extract-article.d.ts +38 -1
  25. package/lib/types/client/extract-article.js +561 -33
  26. package/lib/types/client/icons.d.ts +1 -1
  27. package/lib/types/client/icons.js +1 -1
  28. package/lib/types/client/index.js +81 -2
  29. package/lib/types/client/locales.d.ts +279 -11
  30. package/lib/types/client/locales.js +278 -10
  31. package/lib/types/client/notes.d.ts +142 -0
  32. package/lib/types/client/notes.js +334 -0
  33. package/lib/types/client/parse-rss.js +40 -0
  34. package/lib/types/client/preset-visibility.d.ts +93 -0
  35. package/lib/types/client/preset-visibility.js +115 -0
  36. package/lib/types/client/selectors.d.ts +5 -0
  37. package/lib/types/client/selectors.js +5 -0
  38. package/lib/types/client/session.d.ts +21 -1
  39. package/lib/types/client/session.js +12 -0
  40. package/lib/types/client/store.js +4 -1
  41. package/lib/types/client/translate.d.ts +26 -1
  42. package/lib/types/client/translate.js +29 -12
  43. package/lib/types/client/useLocalTranscript.d.ts +43 -0
  44. package/lib/types/client/useLocalTranscript.js +129 -0
  45. package/lib/types/client/useMedia.d.ts +68 -0
  46. package/lib/types/client/useMedia.js +299 -0
  47. package/lib/types/client/useMediaStates.d.ts +9 -0
  48. package/lib/types/client/useMediaStates.js +43 -0
  49. package/lib/types/client/useNotes.d.ts +110 -0
  50. package/lib/types/client/useNotes.js +423 -0
  51. package/lib/types/client/usePolish.d.ts +47 -0
  52. package/lib/types/client/usePolish.js +162 -0
  53. package/lib/types/document-fetch.d.ts +106 -0
  54. package/lib/types/document-fetch.js +100 -0
  55. package/lib/types/index.d.ts +15 -0
  56. package/lib/types/index.js +3 -0
  57. package/lib/types/local-transcribe.d.ts +171 -0
  58. package/lib/types/local-transcribe.js +473 -0
  59. package/lib/types/media-host.d.ts +91 -0
  60. package/lib/types/media-host.js +230 -0
  61. package/lib/types/media.d.ts +100 -0
  62. package/lib/types/media.js +207 -0
  63. package/lib/types/notes-types.d.ts +158 -0
  64. package/lib/types/notes-types.js +8 -0
  65. package/lib/types/notes.d.ts +213 -0
  66. package/lib/types/notes.js +718 -0
  67. package/lib/types/polish.d.ts +41 -0
  68. package/lib/types/polish.js +136 -0
  69. package/lib/types/remote.d.ts +118 -0
  70. package/lib/types/remote.js +151 -1
  71. package/lib/types/service.d.ts +81 -6
  72. package/lib/types/service.js +286 -65
  73. package/lib/types/skill.d.ts +20 -0
  74. package/lib/types/skill.js +78 -0
  75. package/lib/types/store.js +30 -8
  76. package/lib/types/tool.d.ts +23 -0
  77. package/lib/types/tool.js +40 -0
  78. package/lib/types/tools.d.ts +69 -0
  79. package/lib/types/tools.js +400 -0
  80. package/lib/types/types.d.ts +90 -2
  81. package/lib/types/types.js +66 -0
  82. package/lib/types/xiaoyuzhou.d.ts +43 -0
  83. package/lib/types/xiaoyuzhou.js +134 -0
  84. package/lib/types/ytdlp.d.ts +111 -0
  85. package/lib/types/ytdlp.js +269 -0
  86. package/package.json +33 -20
  87. package/lib/invariant.js +0 -38
  88. package/lib/types/invariant.d.ts +0 -29
  89. package/lib/types/invariant.js +0 -38
@@ -15,6 +15,8 @@
15
15
  *
16
16
  * @module @khorsheed/dsh-reader/types
17
17
  */
18
+ import type { ReaderMedia } from './media.ts';
19
+ export type { ReaderMedia } from './media.ts';
18
20
  /** What a source is: a syndication feed, or one manually saved article link. */
19
21
  export type ReaderSourceKind = 'rss' | 'link';
20
22
  /** Fetch state of one source's most recent attempt. */
@@ -95,8 +97,9 @@ export interface ReaderSource {
95
97
  /** Human-readable failure of the last attempt, when `status` is `error`. */
96
98
  readonly error?: string;
97
99
  /**
98
- * True when the host's fetch seam cut the payload at its size limit
99
- * (`maxBodyChars`, 100 000 by default). A truncated XML document cannot be
100
+ * True when the fetch cut the payload at its size limit (`maxBodyChars`:
101
+ * the reader's own 2 000 000, or the seam's limit where the reader fetches
102
+ * through it). A truncated XML document cannot be
100
103
  * parsed and a truncated HTML page yields a partial article, so every
101
104
  * consumer treats this as "incomplete" and says so rather than guessing.
102
105
  */
@@ -415,6 +418,11 @@ export interface ReaderEntry {
415
418
  * text is real but stops where the cap landed. The detail view says so.
416
419
  */
417
420
  readonly partial?: boolean;
421
+ /**
422
+ * The video or audio this entry plays, decided per ITEM (media.ts): a feed
423
+ * enclosure, a recognized video link, or a saved link's synthetic page.
424
+ */
425
+ readonly media?: ReaderMedia;
418
426
  }
419
427
  /** One source as the browser sees it (no payload: bodies are fetched on demand). */
420
428
  export interface ReaderSourceSummary {
@@ -567,6 +575,38 @@ export declare const MAX_TOTAL_BODY_CHARS: number;
567
575
  * governs it exactly like an inline body, and eviction deletes the file.
568
576
  */
569
577
  export declare const INLINE_BODY_MAX_CHARS: number;
578
+ /**
579
+ * Hosts that bounce every first visit through a cookie handshake, not a login.
580
+ *
581
+ * Springer Nature answers a cookie-less request with a 303 to
582
+ * `idp.<brand>/authorize?response_type=cookie`, which mints a one-time code and
583
+ * sends the browser back to the article (measured 2026-10-04 on
584
+ * link.springer.com and www.nature.com). The host fetch does not follow the
585
+ * cross-origin hop and keeps no cookies, so the plain fetch can never finish
586
+ * it; a headless browser does (the rendered fetch read the full EPJ Data
587
+ * Science article). The `idp` name alone would read as a login wall.
588
+ */
589
+ export declare const COOKIE_HANDSHAKE_HOSTS: ReadonlySet<string>;
590
+ /**
591
+ * Whether a fetch failure is a cross-origin hop into a known cookie handshake.
592
+ *
593
+ * @param message - the seam's recorded failure message.
594
+ * @returns true when the failure is the handshake, which a browser completes.
595
+ */
596
+ export declare function isCookieHandshake(message: string): boolean;
597
+ /**
598
+ * The article a challenge page stands in front of, when its URL names it.
599
+ *
600
+ * A bot wall usually redirects to its own page and carries the original in a
601
+ * query parameter (mp.weixin.qq.com → `/mp/wappoc_appmsgcaptcha?…&target_url=`).
602
+ * Builds before 2026-10-04 saved that landing URL as the link itself, which
603
+ * then titled the card and was what every retry — the rendered fetch included
604
+ * — asked for; the state normalizer walks such a link back with this.
605
+ *
606
+ * @param url - a stored link.
607
+ * @returns the http(s) target, or `undefined` when the URL is not a challenge landing.
608
+ */
609
+ export declare function challengeTarget(url: string): string | undefined;
570
610
  /**
571
611
  * Build a display label for a source from its URL: the registrable-ish host
572
612
  * plus the last meaningful path segment, which is what distinguishes several
@@ -726,4 +766,52 @@ export interface ReaderImageResult {
726
766
  /** Why the rescue failed, when it did. */
727
767
  readonly error?: string;
728
768
  }
769
+ export type * from './notes-types.ts';
770
+ /** An article's 润色 layer as the browser shows it. */
771
+ export interface ReaderPolishView {
772
+ /** Paragraphs in the article (the ¶ numbering the model read). */
773
+ readonly total: number;
774
+ readonly done: number;
775
+ /** ¶ number (1-based, as a string key) → the agent's translation. */
776
+ readonly paragraphs: Readonly<Record<string, string>>;
777
+ /** Paragraphs the reader turned back to the machine translation. */
778
+ readonly off: readonly number[];
779
+ readonly updatedAt: string;
780
+ readonly complete: boolean;
781
+ }
782
+ /** What a video / audio card says about its transcript. */
783
+ export type MediaCardState = 'platform' | 'local' | 'none' | 'fetching' | 'queued' | 'transcribing' | 'failed';
784
+ /** One media card's transcript state (absent: never opened, nothing known). */
785
+ export interface MediaCardStatus {
786
+ readonly state: MediaCardState;
787
+ /** 0–1, while transcribing. */
788
+ readonly progress?: number;
789
+ /** Jobs ahead in the engine's queue, while queued. */
790
+ readonly ahead?: number;
791
+ }
792
+ /** The wall's media cards, plus whether local transcription is installed. */
793
+ export interface MediaCardStates {
794
+ readonly engine: boolean;
795
+ readonly states: Readonly<Record<string, MediaCardStatus>>;
796
+ }
797
+ /** Where an entry's local transcription stands. */
798
+ export type LocalTranscriptState = 'unavailable' | 'needs-models' | 'fetching' | 'queued' | 'transcribing' | 'speakers' | 'done' | 'failed' | 'idle';
799
+ /** What the browser polls. */
800
+ export interface LocalTranscriptStatus {
801
+ readonly entryId: string;
802
+ readonly state: LocalTranscriptState;
803
+ /** 0–1 across fetching the audio and transcribing it. */
804
+ readonly progress?: number;
805
+ /** Jobs ahead in the engine's queue. */
806
+ readonly ahead?: number;
807
+ /** Bytes of speech models still to download (`needs-models`). */
808
+ readonly bytes?: number;
809
+ /** The transcript so far (or in full, once `done`). */
810
+ readonly html?: string;
811
+ /** The language the transcript is in, once known. */
812
+ readonly language?: string;
813
+ /** How many voices the transcript has, once the speakers pass settled it. */
814
+ readonly speakers?: number;
815
+ readonly error?: string;
816
+ }
729
817
  //# sourceMappingURL=types.d.ts.map
@@ -79,6 +79,72 @@ export const MAX_TOTAL_BODY_CHARS = 12 * 1024 * 1024;
79
79
  * governs it exactly like an inline body, and eviction deletes the file.
80
80
  */
81
81
  export const INLINE_BODY_MAX_CHARS = 256 * 1024;
82
+ /**
83
+ * Hosts that bounce every first visit through a cookie handshake, not a login.
84
+ *
85
+ * Springer Nature answers a cookie-less request with a 303 to
86
+ * `idp.<brand>/authorize?response_type=cookie`, which mints a one-time code and
87
+ * sends the browser back to the article (measured 2026-10-04 on
88
+ * link.springer.com and www.nature.com). The host fetch does not follow the
89
+ * cross-origin hop and keeps no cookies, so the plain fetch can never finish
90
+ * it; a headless browser does (the rendered fetch read the full EPJ Data
91
+ * Science article). The `idp` name alone would read as a login wall.
92
+ */
93
+ export const COOKIE_HANDSHAKE_HOSTS = new Set(['idp.springer.com', 'idp.nature.com']);
94
+ /**
95
+ * Whether a fetch failure is a cross-origin hop into a known cookie handshake.
96
+ *
97
+ * @param message - the seam's recorded failure message.
98
+ * @returns true when the failure is the handshake, which a browser completes.
99
+ */
100
+ export function isCookieHandshake(message) {
101
+ const origin = /cross-origin redirect to (\S+)/i.exec(message)?.[1];
102
+ if (origin === undefined)
103
+ return false;
104
+ try {
105
+ return COOKIE_HANDSHAKE_HOSTS.has(new URL(origin).hostname.toLowerCase());
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * The article a challenge page stands in front of, when its URL names it.
113
+ *
114
+ * A bot wall usually redirects to its own page and carries the original in a
115
+ * query parameter (mp.weixin.qq.com → `/mp/wappoc_appmsgcaptcha?…&target_url=`).
116
+ * Builds before 2026-10-04 saved that landing URL as the link itself, which
117
+ * then titled the card and was what every retry — the rendered fetch included
118
+ * — asked for; the state normalizer walks such a link back with this.
119
+ *
120
+ * @param url - a stored link.
121
+ * @returns the http(s) target, or `undefined` when the URL is not a challenge landing.
122
+ */
123
+ export function challengeTarget(url) {
124
+ let parsed;
125
+ try {
126
+ parsed = new URL(url);
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ if (!/captcha|challenge/i.test(parsed.pathname))
132
+ return undefined;
133
+ for (const key of ['target_url', 'targetUrl', 'return_url', 'redirect_url']) {
134
+ const value = parsed.searchParams.get(key);
135
+ if (value === null)
136
+ continue;
137
+ try {
138
+ const target = new URL(value);
139
+ if (target.protocol === 'https:' || target.protocol === 'http:')
140
+ return target.toString();
141
+ }
142
+ catch {
143
+ // Not a URL: try the next name.
144
+ }
145
+ }
146
+ return undefined;
147
+ }
82
148
  /**
83
149
  * Build a display label for a source from its URL: the registrable-ish host
84
150
  * plus the last meaningful path segment, which is what distinguishes several
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Xiaoyuzhou (小宇宙): episode and podcast pages → what the reader stores.
3
+ *
4
+ * Xiaoyuzhou publishes no RSS. Its pages do carry their data as Next.js
5
+ * `__NEXT_DATA__` JSON (measured 2026-10-20): an episode page has
6
+ * `props.pageProps.episode` with the audio enclosure, duration, shownotes and
7
+ * podcast; a podcast page has `props.pageProps.podcast.episodes[]`. A podcast
8
+ * page is turned into an RSS document on the host, so subscribing to it is an
9
+ * ordinary feed subscription for every other part of the reader.
10
+ *
11
+ * Pure: the host fetches the page and hands over its text.
12
+ *
13
+ * @module @khorsheed/dsh-reader/xiaoyuzhou
14
+ */
15
+ /** One episode, as the reader needs it. */
16
+ export interface XiaoyuzhouEpisode {
17
+ readonly id: string;
18
+ readonly title: string;
19
+ readonly audio: string;
20
+ readonly durationSec?: number;
21
+ readonly publishedAt?: string;
22
+ /** The shownotes (Xiaoyuzhou's own HTML), else the plain description. */
23
+ readonly notesHtml?: string;
24
+ readonly image?: string;
25
+ readonly podcastTitle?: string;
26
+ }
27
+ /**
28
+ * Read an episode page.
29
+ *
30
+ * @param html - the page's HTML.
31
+ * @returns the episode, or `undefined` when the page carries none.
32
+ */
33
+ export declare function readEpisodePage(html: string): XiaoyuzhouEpisode | undefined;
34
+ /**
35
+ * Turn a podcast page into an RSS document the reader subscribes to like any
36
+ * other feed.
37
+ *
38
+ * @param html - the podcast page's HTML.
39
+ * @param pageUrl - the page's URL (the channel link).
40
+ * @returns the RSS text, or `undefined` when the page carries no podcast.
41
+ */
42
+ export declare function podcastPageToRss(html: string, pageUrl: string): string | undefined;
43
+ //# sourceMappingURL=xiaoyuzhou.d.ts.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Xiaoyuzhou (小宇宙): episode and podcast pages → what the reader stores.
3
+ *
4
+ * Xiaoyuzhou publishes no RSS. Its pages do carry their data as Next.js
5
+ * `__NEXT_DATA__` JSON (measured 2026-10-20): an episode page has
6
+ * `props.pageProps.episode` with the audio enclosure, duration, shownotes and
7
+ * podcast; a podcast page has `props.pageProps.podcast.episodes[]`. A podcast
8
+ * page is turned into an RSS document on the host, so subscribing to it is an
9
+ * ordinary feed subscription for every other part of the reader.
10
+ *
11
+ * Pure: the host fetches the page and hands over its text.
12
+ *
13
+ * @module @khorsheed/dsh-reader/xiaoyuzhou
14
+ */
15
+ /** The page's Next.js data, or `undefined` when the page has none. */
16
+ function nextData(html) {
17
+ const match = /<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
18
+ if (match === null)
19
+ return undefined;
20
+ try {
21
+ return JSON.parse(match[1]);
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ /** A nested record field, tolerantly. */
28
+ function field(value, ...path) {
29
+ let current = value;
30
+ for (const key of path) {
31
+ if (typeof current !== 'object' || current === null)
32
+ return undefined;
33
+ current = current[key];
34
+ }
35
+ return current;
36
+ }
37
+ /** A string field, or `undefined`. */
38
+ function str(value) {
39
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
40
+ }
41
+ /** One episode record → the reader's shape. */
42
+ function readEpisode(record, podcastTitle) {
43
+ const id = str(field(record, 'eid'));
44
+ const title = str(field(record, 'title'));
45
+ const audio = str(field(record, 'enclosure', 'url')) ?? str(field(record, 'media', 'source', 'url'));
46
+ if (id === undefined || title === undefined || audio === undefined)
47
+ return undefined;
48
+ const duration = Number(field(record, 'duration'));
49
+ const notes = str(field(record, 'shownotes')) ?? str(field(record, 'description'));
50
+ const image = str(field(record, 'image', 'picUrl')) ?? str(field(record, 'podcast', 'image', 'picUrl'));
51
+ const podcast = podcastTitle ?? str(field(record, 'podcast', 'title'));
52
+ const publishedAt = str(field(record, 'pubDate'));
53
+ return {
54
+ id,
55
+ title,
56
+ audio,
57
+ ...(Number.isFinite(duration) && duration > 0 ? { durationSec: duration } : {}),
58
+ ...(publishedAt === undefined ? {} : { publishedAt }),
59
+ ...(notes === undefined ? {} : { notesHtml: notes }),
60
+ ...(image === undefined ? {} : { image }),
61
+ ...(podcast === undefined ? {} : { podcastTitle: podcast }),
62
+ };
63
+ }
64
+ /**
65
+ * Read an episode page.
66
+ *
67
+ * @param html - the page's HTML.
68
+ * @returns the episode, or `undefined` when the page carries none.
69
+ */
70
+ export function readEpisodePage(html) {
71
+ return readEpisode(field(nextData(html), 'props', 'pageProps', 'episode'));
72
+ }
73
+ /** Escape text for XML content and attributes. */
74
+ function xml(text) {
75
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
76
+ }
77
+ /** CDATA, safe against a literal `]]>` in the content. */
78
+ function cdata(text) {
79
+ return `<![CDATA[${text.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`;
80
+ }
81
+ /** The MIME type an audio URL most likely has. */
82
+ function audioType(url) {
83
+ const path = url.toLowerCase().split('?')[0] ?? '';
84
+ if (path.endsWith('.m4a') || path.endsWith('.mp4'))
85
+ return 'audio/mp4';
86
+ return 'audio/mpeg';
87
+ }
88
+ /**
89
+ * Turn a podcast page into an RSS document the reader subscribes to like any
90
+ * other feed.
91
+ *
92
+ * @param html - the podcast page's HTML.
93
+ * @param pageUrl - the page's URL (the channel link).
94
+ * @returns the RSS text, or `undefined` when the page carries no podcast.
95
+ */
96
+ export function podcastPageToRss(html, pageUrl) {
97
+ const podcast = field(nextData(html), 'props', 'pageProps', 'podcast');
98
+ const title = str(field(podcast, 'title'));
99
+ if (title === undefined)
100
+ return undefined;
101
+ const episodes = Array.isArray(field(podcast, 'episodes')) ? field(podcast, 'episodes') : [];
102
+ const items = episodes.flatMap(record => {
103
+ const episode = readEpisode(record, title);
104
+ if (episode === undefined)
105
+ return [];
106
+ const date = episode.publishedAt === undefined ? undefined : new Date(episode.publishedAt);
107
+ return [[
108
+ '<item>',
109
+ `<title>${xml(episode.title)}</title>`,
110
+ `<link>https://www.xiaoyuzhoufm.com/episode/${episode.id}</link>`,
111
+ `<guid isPermaLink="false">xiaoyuzhou:${episode.id}</guid>`,
112
+ date === undefined || Number.isNaN(date.getTime()) ? '' : `<pubDate>${date.toUTCString()}</pubDate>`,
113
+ episode.notesHtml === undefined ? '' : `<description>${cdata(episode.notesHtml)}</description>`,
114
+ `<enclosure url="${xml(episode.audio)}" type="${audioType(episode.audio)}" length="0"/>`,
115
+ episode.durationSec === undefined ? '' : `<itunes:duration>${String(episode.durationSec)}</itunes:duration>`,
116
+ episode.image === undefined ? '' : `<itunes:image href="${xml(episode.image)}"/>`,
117
+ '</item>',
118
+ ].filter(line => line !== '').join('')];
119
+ });
120
+ const description = str(field(podcast, 'description'));
121
+ const image = str(field(podcast, 'image', 'picUrl'));
122
+ return [
123
+ '<?xml version="1.0" encoding="UTF-8"?>',
124
+ '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">',
125
+ '<channel>',
126
+ `<title>${xml(title)}</title>`,
127
+ `<link>${xml(pageUrl)}</link>`,
128
+ description === undefined ? '' : `<description>${xml(description)}</description>`,
129
+ image === undefined ? '' : `<image><url>${xml(image)}</url><title>${xml(title)}</title><link>${xml(pageUrl)}</link></image>`,
130
+ ...items,
131
+ '</channel>',
132
+ '</rss>',
133
+ ].filter(line => line !== '').join('\n');
134
+ }
@@ -0,0 +1,111 @@
1
+ import { type Cue } from './captions.ts';
2
+ /** The release asset for this platform, or `undefined` where none is published. */
3
+ export declare function releaseAsset(platform?: NodeJS.Platform, arch?: string): string | undefined;
4
+ /** The subset of yt-dlp's info JSON the reader reads. */
5
+ export interface YtDlpInfo {
6
+ readonly id?: string;
7
+ readonly title?: string;
8
+ readonly description?: string;
9
+ readonly duration?: number;
10
+ readonly thumbnail?: string;
11
+ readonly uploader?: string;
12
+ readonly channel?: string;
13
+ readonly channel_id?: string;
14
+ readonly upload_date?: string;
15
+ readonly timestamp?: number;
16
+ readonly language?: string;
17
+ readonly webpage_url?: string;
18
+ readonly subtitles?: Record<string, readonly {
19
+ ext: string;
20
+ }[]>;
21
+ readonly automatic_captions?: Record<string, readonly {
22
+ ext: string;
23
+ }[]>;
24
+ }
25
+ /** The caption track chosen for a video. */
26
+ export interface CaptionTrack {
27
+ readonly lang: string;
28
+ readonly auto: boolean;
29
+ readonly ext: string;
30
+ }
31
+ /**
32
+ * Choose the caption track: the uploader's own captions in the video's
33
+ * language first, then any uploaded track, then the platform's automatic
34
+ * captions in the ORIGINAL language (YouTube marks it `<lang>-orig`; the other
35
+ * auto tracks are machine translations, which are also what trips its rate
36
+ * limit). Manual and automatic tracks are equally "captions" to the reader
37
+ * (2026-10-20); the choice only prefers the better one.
38
+ *
39
+ * @param info - the video's info JSON.
40
+ */
41
+ export declare function chooseCaptionTrack(info: YtDlpInfo): CaptionTrack | undefined;
42
+ /**
43
+ * Run a process to completion, honoring an abort signal and reporting stdout
44
+ * lines as they come.
45
+ */
46
+ export declare function runStreaming(file: string, args: readonly string[], options: {
47
+ signal?: AbortSignal;
48
+ onLine?: (line: string) => void;
49
+ timeoutMs: number;
50
+ }): Promise<void>;
51
+ /** The yt-dlp manager. */
52
+ export declare class YtDlp {
53
+ private readonly configured?;
54
+ private readonly binDir;
55
+ private readonly tmpRoot;
56
+ private ensuring;
57
+ /**
58
+ * @param stateRoot - the reader's state root (the managed binary lives under `bin/`).
59
+ * @param configured - a deployment-configured binary path, which wins.
60
+ */
61
+ constructor(stateRoot: string, configured?: string | undefined);
62
+ /** The binary to run: the configured one, else the managed one (fetched / updated as needed). */
63
+ binary(): Promise<string>;
64
+ /** Fetch the standalone release on first use; self-update it when stale. */
65
+ private ensureManaged;
66
+ /** The version of the binary in use. */
67
+ version(): Promise<string>;
68
+ /** The common flags: no download, no formats needed, Node as the JS runtime. */
69
+ private base;
70
+ /**
71
+ * A video's metadata.
72
+ *
73
+ * @param url - the video page URL.
74
+ */
75
+ info(url: string): Promise<YtDlpInfo>;
76
+ /**
77
+ * A channel's newest videos (a flat listing: no video is opened).
78
+ *
79
+ * YouTube's own channel feed (`feeds/videos.xml`) answers 404 (measured
80
+ * 2026-10-20), so the listing is yt-dlp's; `approximate_date` gives each
81
+ * entry a day-precision date.
82
+ *
83
+ * @param url - the channel URL.
84
+ * @param limit - how many of the newest videos.
85
+ */
86
+ channel(url: string, limit?: number): Promise<{
87
+ title?: string;
88
+ entries: YtDlpInfo[];
89
+ }>;
90
+ /**
91
+ * A video's audio track, downloaded for local transcription (the smallest
92
+ * audio-only format; the platform's combined stream when there is none).
93
+ *
94
+ * @param url - the video page URL.
95
+ * @param dir - an empty directory the file lands in.
96
+ * @param options - abort, and download progress 0–1.
97
+ * @returns the downloaded file.
98
+ */
99
+ audio(url: string, dir: string, options?: {
100
+ signal?: AbortSignal;
101
+ onProgress?: (fraction: number) => void;
102
+ }): Promise<string>;
103
+ /**
104
+ * One caption track's cues.
105
+ *
106
+ * @param url - the video page URL.
107
+ * @param track - the chosen track.
108
+ */
109
+ captions(url: string, track: CaptionTrack): Promise<Cue[]>;
110
+ }
111
+ //# sourceMappingURL=ytdlp.d.ts.map