@khorsheed/dsh-reader 0.3.0 → 0.3.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 (87) hide show
  1. package/CHANGELOG.md +22 -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 +29054 -6698
  7. package/lib/index.js +3416 -334
  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 +501 -28
  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/index.d.ts +6 -0
  54. package/lib/types/index.js +3 -0
  55. package/lib/types/local-transcribe.d.ts +171 -0
  56. package/lib/types/local-transcribe.js +473 -0
  57. package/lib/types/media-host.d.ts +91 -0
  58. package/lib/types/media-host.js +230 -0
  59. package/lib/types/media.d.ts +100 -0
  60. package/lib/types/media.js +207 -0
  61. package/lib/types/notes-types.d.ts +158 -0
  62. package/lib/types/notes-types.js +8 -0
  63. package/lib/types/notes.d.ts +213 -0
  64. package/lib/types/notes.js +718 -0
  65. package/lib/types/polish.d.ts +41 -0
  66. package/lib/types/polish.js +136 -0
  67. package/lib/types/remote.d.ts +118 -0
  68. package/lib/types/remote.js +151 -1
  69. package/lib/types/service.d.ts +71 -1
  70. package/lib/types/service.js +280 -59
  71. package/lib/types/skill.d.ts +20 -0
  72. package/lib/types/skill.js +78 -0
  73. package/lib/types/store.js +30 -8
  74. package/lib/types/tool.d.ts +23 -0
  75. package/lib/types/tool.js +40 -0
  76. package/lib/types/tools.d.ts +69 -0
  77. package/lib/types/tools.js +400 -0
  78. package/lib/types/types.d.ts +87 -0
  79. package/lib/types/types.js +66 -0
  80. package/lib/types/xiaoyuzhou.d.ts +43 -0
  81. package/lib/types/xiaoyuzhou.js +134 -0
  82. package/lib/types/ytdlp.d.ts +111 -0
  83. package/lib/types/ytdlp.js +269 -0
  84. package/package.json +29 -20
  85. package/lib/invariant.js +0 -38
  86. package/lib/types/invariant.d.ts +0 -29
  87. 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. */
@@ -415,6 +417,11 @@ export interface ReaderEntry {
415
417
  * text is real but stops where the cap landed. The detail view says so.
416
418
  */
417
419
  readonly partial?: boolean;
420
+ /**
421
+ * The video or audio this entry plays, decided per ITEM (media.ts): a feed
422
+ * enclosure, a recognized video link, or a saved link's synthetic page.
423
+ */
424
+ readonly media?: ReaderMedia;
418
425
  }
419
426
  /** One source as the browser sees it (no payload: bodies are fetched on demand). */
420
427
  export interface ReaderSourceSummary {
@@ -567,6 +574,38 @@ export declare const MAX_TOTAL_BODY_CHARS: number;
567
574
  * governs it exactly like an inline body, and eviction deletes the file.
568
575
  */
569
576
  export declare const INLINE_BODY_MAX_CHARS: number;
577
+ /**
578
+ * Hosts that bounce every first visit through a cookie handshake, not a login.
579
+ *
580
+ * Springer Nature answers a cookie-less request with a 303 to
581
+ * `idp.<brand>/authorize?response_type=cookie`, which mints a one-time code and
582
+ * sends the browser back to the article (measured 2026-10-04 on
583
+ * link.springer.com and www.nature.com). The host fetch does not follow the
584
+ * cross-origin hop and keeps no cookies, so the plain fetch can never finish
585
+ * it; a headless browser does (the rendered fetch read the full EPJ Data
586
+ * Science article). The `idp` name alone would read as a login wall.
587
+ */
588
+ export declare const COOKIE_HANDSHAKE_HOSTS: ReadonlySet<string>;
589
+ /**
590
+ * Whether a fetch failure is a cross-origin hop into a known cookie handshake.
591
+ *
592
+ * @param message - the seam's recorded failure message.
593
+ * @returns true when the failure is the handshake, which a browser completes.
594
+ */
595
+ export declare function isCookieHandshake(message: string): boolean;
596
+ /**
597
+ * The article a challenge page stands in front of, when its URL names it.
598
+ *
599
+ * A bot wall usually redirects to its own page and carries the original in a
600
+ * query parameter (mp.weixin.qq.com → `/mp/wappoc_appmsgcaptcha?…&target_url=`).
601
+ * Builds before 2026-10-04 saved that landing URL as the link itself, which
602
+ * then titled the card and was what every retry — the rendered fetch included
603
+ * — asked for; the state normalizer walks such a link back with this.
604
+ *
605
+ * @param url - a stored link.
606
+ * @returns the http(s) target, or `undefined` when the URL is not a challenge landing.
607
+ */
608
+ export declare function challengeTarget(url: string): string | undefined;
570
609
  /**
571
610
  * Build a display label for a source from its URL: the registrable-ish host
572
611
  * plus the last meaningful path segment, which is what distinguishes several
@@ -726,4 +765,52 @@ export interface ReaderImageResult {
726
765
  /** Why the rescue failed, when it did. */
727
766
  readonly error?: string;
728
767
  }
768
+ export type * from './notes-types.ts';
769
+ /** An article's 润色 layer as the browser shows it. */
770
+ export interface ReaderPolishView {
771
+ /** Paragraphs in the article (the ¶ numbering the model read). */
772
+ readonly total: number;
773
+ readonly done: number;
774
+ /** ¶ number (1-based, as a string key) → the agent's translation. */
775
+ readonly paragraphs: Readonly<Record<string, string>>;
776
+ /** Paragraphs the reader turned back to the machine translation. */
777
+ readonly off: readonly number[];
778
+ readonly updatedAt: string;
779
+ readonly complete: boolean;
780
+ }
781
+ /** What a video / audio card says about its transcript. */
782
+ export type MediaCardState = 'platform' | 'local' | 'none' | 'fetching' | 'queued' | 'transcribing' | 'failed';
783
+ /** One media card's transcript state (absent: never opened, nothing known). */
784
+ export interface MediaCardStatus {
785
+ readonly state: MediaCardState;
786
+ /** 0–1, while transcribing. */
787
+ readonly progress?: number;
788
+ /** Jobs ahead in the engine's queue, while queued. */
789
+ readonly ahead?: number;
790
+ }
791
+ /** The wall's media cards, plus whether local transcription is installed. */
792
+ export interface MediaCardStates {
793
+ readonly engine: boolean;
794
+ readonly states: Readonly<Record<string, MediaCardStatus>>;
795
+ }
796
+ /** Where an entry's local transcription stands. */
797
+ export type LocalTranscriptState = 'unavailable' | 'needs-models' | 'fetching' | 'queued' | 'transcribing' | 'speakers' | 'done' | 'failed' | 'idle';
798
+ /** What the browser polls. */
799
+ export interface LocalTranscriptStatus {
800
+ readonly entryId: string;
801
+ readonly state: LocalTranscriptState;
802
+ /** 0–1 across fetching the audio and transcribing it. */
803
+ readonly progress?: number;
804
+ /** Jobs ahead in the engine's queue. */
805
+ readonly ahead?: number;
806
+ /** Bytes of speech models still to download (`needs-models`). */
807
+ readonly bytes?: number;
808
+ /** The transcript so far (or in full, once `done`). */
809
+ readonly html?: string;
810
+ /** The language the transcript is in, once known. */
811
+ readonly language?: string;
812
+ /** How many voices the transcript has, once the speakers pass settled it. */
813
+ readonly speakers?: number;
814
+ readonly error?: string;
815
+ }
729
816
  //# 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