recker 1.0.26 → 1.0.27
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.
- package/dist/browser/browser/cache.d.ts +40 -0
- package/dist/browser/browser/cache.js +199 -0
- package/dist/browser/browser/crypto.d.ts +24 -0
- package/dist/browser/browser/crypto.js +80 -0
- package/dist/browser/browser/index.d.ts +31 -0
- package/dist/browser/browser/index.js +31 -0
- package/dist/browser/browser/recker.d.ts +26 -0
- package/dist/browser/browser/recker.js +61 -0
- package/dist/browser/cache/basic-file-storage.d.ts +12 -0
- package/dist/browser/cache/basic-file-storage.js +50 -0
- package/dist/browser/cache/memory-limits.d.ts +20 -0
- package/dist/browser/cache/memory-limits.js +96 -0
- package/dist/browser/cache/memory-storage.d.ts +132 -0
- package/dist/browser/cache/memory-storage.js +454 -0
- package/dist/browser/cache.d.ts +40 -0
- package/dist/browser/cache.js +199 -0
- package/dist/browser/constants/http-status.d.ts +73 -0
- package/dist/browser/constants/http-status.js +156 -0
- package/dist/browser/cookies/memory-cookie-jar.d.ts +30 -0
- package/dist/browser/cookies/memory-cookie-jar.js +210 -0
- package/dist/browser/core/client.d.ts +118 -0
- package/dist/browser/core/client.js +667 -0
- package/dist/browser/core/errors.d.ts +142 -0
- package/dist/browser/core/errors.js +308 -0
- package/dist/browser/core/index.d.ts +5 -0
- package/dist/browser/core/index.js +5 -0
- package/dist/browser/core/request-promise.d.ts +23 -0
- package/dist/browser/core/request-promise.js +82 -0
- package/dist/browser/core/request.d.ts +20 -0
- package/dist/browser/core/request.js +76 -0
- package/dist/browser/core/response.d.ts +34 -0
- package/dist/browser/core/response.js +178 -0
- package/dist/browser/crypto.d.ts +24 -0
- package/dist/browser/crypto.js +80 -0
- package/dist/browser/index.d.ts +31 -0
- package/dist/browser/index.js +31 -0
- package/dist/browser/plugins/auth/api-key.d.ts +8 -0
- package/dist/browser/plugins/auth/api-key.js +27 -0
- package/dist/browser/plugins/auth/auth0.d.ts +33 -0
- package/dist/browser/plugins/auth/auth0.js +94 -0
- package/dist/browser/plugins/auth/aws-sigv4.d.ts +10 -0
- package/dist/browser/plugins/auth/aws-sigv4.js +88 -0
- package/dist/browser/plugins/auth/azure-ad.d.ts +48 -0
- package/dist/browser/plugins/auth/azure-ad.js +152 -0
- package/dist/browser/plugins/auth/basic.d.ts +7 -0
- package/dist/browser/plugins/auth/basic.js +13 -0
- package/dist/browser/plugins/auth/bearer.d.ts +8 -0
- package/dist/browser/plugins/auth/bearer.js +17 -0
- package/dist/browser/plugins/auth/cognito.d.ts +45 -0
- package/dist/browser/plugins/auth/cognito.js +208 -0
- package/dist/browser/plugins/auth/digest.d.ts +8 -0
- package/dist/browser/plugins/auth/digest.js +100 -0
- package/dist/browser/plugins/auth/firebase.d.ts +32 -0
- package/dist/browser/plugins/auth/firebase.js +195 -0
- package/dist/browser/plugins/auth/github-app.d.ts +36 -0
- package/dist/browser/plugins/auth/github-app.js +170 -0
- package/dist/browser/plugins/auth/google-service-account.d.ts +49 -0
- package/dist/browser/plugins/auth/google-service-account.js +172 -0
- package/dist/browser/plugins/auth/index.d.ts +15 -0
- package/dist/browser/plugins/auth/index.js +15 -0
- package/dist/browser/plugins/auth/mtls.d.ts +37 -0
- package/dist/browser/plugins/auth/mtls.js +140 -0
- package/dist/browser/plugins/auth/oauth2.d.ts +8 -0
- package/dist/browser/plugins/auth/oauth2.js +26 -0
- package/dist/browser/plugins/auth/oidc.d.ts +55 -0
- package/dist/browser/plugins/auth/oidc.js +222 -0
- package/dist/browser/plugins/auth/okta.d.ts +47 -0
- package/dist/browser/plugins/auth/okta.js +157 -0
- package/dist/browser/plugins/auth.d.ts +1 -0
- package/dist/browser/plugins/auth.js +1 -0
- package/dist/browser/plugins/cache.d.ts +15 -0
- package/dist/browser/plugins/cache.js +486 -0
- package/dist/browser/plugins/circuit-breaker.d.ts +13 -0
- package/dist/browser/plugins/circuit-breaker.js +100 -0
- package/dist/browser/plugins/compression.d.ts +4 -0
- package/dist/browser/plugins/compression.js +130 -0
- package/dist/browser/plugins/cookie-jar.d.ts +5 -0
- package/dist/browser/plugins/cookie-jar.js +72 -0
- package/dist/browser/plugins/dedup.d.ts +5 -0
- package/dist/browser/plugins/dedup.js +35 -0
- package/dist/browser/plugins/graphql.d.ts +13 -0
- package/dist/browser/plugins/graphql.js +58 -0
- package/dist/browser/plugins/grpc-web.d.ts +79 -0
- package/dist/browser/plugins/grpc-web.js +261 -0
- package/dist/browser/plugins/hls.d.ts +105 -0
- package/dist/browser/plugins/hls.js +395 -0
- package/dist/browser/plugins/jsonrpc.d.ts +75 -0
- package/dist/browser/plugins/jsonrpc.js +143 -0
- package/dist/browser/plugins/logger.d.ts +13 -0
- package/dist/browser/plugins/logger.js +108 -0
- package/dist/browser/plugins/odata.d.ts +181 -0
- package/dist/browser/plugins/odata.js +564 -0
- package/dist/browser/plugins/pagination.d.ts +16 -0
- package/dist/browser/plugins/pagination.js +105 -0
- package/dist/browser/plugins/rate-limit.d.ts +15 -0
- package/dist/browser/plugins/rate-limit.js +162 -0
- package/dist/browser/plugins/retry.d.ts +14 -0
- package/dist/browser/plugins/retry.js +116 -0
- package/dist/browser/plugins/scrape.d.ts +21 -0
- package/dist/browser/plugins/scrape.js +82 -0
- package/dist/browser/plugins/server-timing.d.ts +7 -0
- package/dist/browser/plugins/server-timing.js +24 -0
- package/dist/browser/plugins/soap.d.ts +72 -0
- package/dist/browser/plugins/soap.js +347 -0
- package/dist/browser/plugins/xml.d.ts +9 -0
- package/dist/browser/plugins/xml.js +194 -0
- package/dist/browser/plugins/xsrf.d.ts +9 -0
- package/dist/browser/plugins/xsrf.js +48 -0
- package/dist/browser/recker.d.ts +26 -0
- package/dist/browser/recker.js +61 -0
- package/dist/browser/runner/request-runner.d.ts +46 -0
- package/dist/browser/runner/request-runner.js +89 -0
- package/dist/browser/scrape/document.d.ts +44 -0
- package/dist/browser/scrape/document.js +210 -0
- package/dist/browser/scrape/element.d.ts +49 -0
- package/dist/browser/scrape/element.js +176 -0
- package/dist/browser/scrape/extractors.d.ts +16 -0
- package/dist/browser/scrape/extractors.js +356 -0
- package/dist/browser/scrape/types.d.ts +107 -0
- package/dist/browser/scrape/types.js +1 -0
- package/dist/browser/transport/fetch.d.ts +11 -0
- package/dist/browser/transport/fetch.js +143 -0
- package/dist/browser/transport/undici.d.ts +38 -0
- package/dist/browser/transport/undici.js +897 -0
- package/dist/browser/types/ai.d.ts +267 -0
- package/dist/browser/types/ai.js +1 -0
- package/dist/browser/types/index.d.ts +351 -0
- package/dist/browser/types/index.js +1 -0
- package/dist/browser/types/logger.d.ts +16 -0
- package/dist/browser/types/logger.js +66 -0
- package/dist/browser/types/udp.d.ts +138 -0
- package/dist/browser/types/udp.js +1 -0
- package/dist/browser/utils/agent-manager.d.ts +29 -0
- package/dist/browser/utils/agent-manager.js +160 -0
- package/dist/browser/utils/body.d.ts +10 -0
- package/dist/browser/utils/body.js +148 -0
- package/dist/browser/utils/charset.d.ts +15 -0
- package/dist/browser/utils/charset.js +169 -0
- package/dist/browser/utils/concurrency.d.ts +20 -0
- package/dist/browser/utils/concurrency.js +120 -0
- package/dist/browser/utils/dns.d.ts +6 -0
- package/dist/browser/utils/dns.js +26 -0
- package/dist/browser/utils/header-parser.d.ts +94 -0
- package/dist/browser/utils/header-parser.js +617 -0
- package/dist/browser/utils/html-cleaner.d.ts +1 -0
- package/dist/browser/utils/html-cleaner.js +21 -0
- package/dist/browser/utils/link-header.d.ts +69 -0
- package/dist/browser/utils/link-header.js +190 -0
- package/dist/browser/utils/optional-require.d.ts +19 -0
- package/dist/browser/utils/optional-require.js +105 -0
- package/dist/browser/utils/progress.d.ts +8 -0
- package/dist/browser/utils/progress.js +82 -0
- package/dist/browser/utils/request-pool.d.ts +22 -0
- package/dist/browser/utils/request-pool.js +101 -0
- package/dist/browser/utils/sse.d.ts +7 -0
- package/dist/browser/utils/sse.js +67 -0
- package/dist/browser/utils/streaming.d.ts +17 -0
- package/dist/browser/utils/streaming.js +84 -0
- package/dist/browser/utils/try-fn.d.ts +3 -0
- package/dist/browser/utils/try-fn.js +59 -0
- package/dist/browser/utils/user-agent.d.ts +44 -0
- package/dist/browser/utils/user-agent.js +100 -0
- package/dist/browser/utils/whois.d.ts +32 -0
- package/dist/browser/utils/whois.js +246 -0
- package/dist/browser/websocket/client.d.ts +65 -0
- package/dist/browser/websocket/client.js +313 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/transport/fetch.d.ts +7 -1
- package/dist/transport/fetch.js +58 -76
- package/package.json +34 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Client } from '../core/client.js';
|
|
2
|
+
import { type WriteStream } from 'node:fs';
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
4
|
+
export interface HlsVariant {
|
|
5
|
+
url: string;
|
|
6
|
+
bandwidth?: number;
|
|
7
|
+
resolution?: string;
|
|
8
|
+
codecs?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HlsSegment {
|
|
12
|
+
url: string;
|
|
13
|
+
duration: number;
|
|
14
|
+
sequence: number;
|
|
15
|
+
key?: HlsKeyInfo;
|
|
16
|
+
discontinuity?: boolean;
|
|
17
|
+
programDateTime?: Date;
|
|
18
|
+
}
|
|
19
|
+
export interface HlsKeyInfo {
|
|
20
|
+
method: 'NONE' | 'AES-128' | 'SAMPLE-AES';
|
|
21
|
+
uri?: string;
|
|
22
|
+
iv?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface HlsPlaylist {
|
|
25
|
+
segments: HlsSegment[];
|
|
26
|
+
targetDuration: number;
|
|
27
|
+
mediaSequence: number;
|
|
28
|
+
endList: boolean;
|
|
29
|
+
playlistType?: 'VOD' | 'EVENT';
|
|
30
|
+
discontinuitySequence: number;
|
|
31
|
+
}
|
|
32
|
+
export interface HlsMasterPlaylist {
|
|
33
|
+
variants: HlsVariant[];
|
|
34
|
+
isMaster: true;
|
|
35
|
+
}
|
|
36
|
+
export interface SegmentData {
|
|
37
|
+
sequence: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
data: Uint8Array;
|
|
40
|
+
url: string;
|
|
41
|
+
downloadedAt: Date;
|
|
42
|
+
}
|
|
43
|
+
export interface HlsProgress {
|
|
44
|
+
downloadedSegments: number;
|
|
45
|
+
totalSegments?: number;
|
|
46
|
+
downloadedBytes: number;
|
|
47
|
+
currentSegment: number;
|
|
48
|
+
isLive: boolean;
|
|
49
|
+
elapsed: number;
|
|
50
|
+
}
|
|
51
|
+
export interface HlsOptions {
|
|
52
|
+
mode?: 'merge' | 'chunks';
|
|
53
|
+
format?: 'ts' | 'mp4' | 'mkv';
|
|
54
|
+
live?: boolean | {
|
|
55
|
+
duration: number;
|
|
56
|
+
};
|
|
57
|
+
quality?: 'highest' | 'lowest' | {
|
|
58
|
+
bandwidth?: number;
|
|
59
|
+
resolution?: string;
|
|
60
|
+
};
|
|
61
|
+
concurrency?: number;
|
|
62
|
+
onSegment?: (segment: SegmentData) => void | Promise<void>;
|
|
63
|
+
onProgress?: (progress: HlsProgress) => void;
|
|
64
|
+
onError?: (error: Error) => void;
|
|
65
|
+
headers?: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
type DownloadDest = string | ((segment: HlsSegment) => string);
|
|
68
|
+
export declare class HlsPromise implements Promise<void> {
|
|
69
|
+
private client;
|
|
70
|
+
private manifestUrl;
|
|
71
|
+
private options;
|
|
72
|
+
private seenSequences;
|
|
73
|
+
private downloadedBytes;
|
|
74
|
+
private downloadedSegments;
|
|
75
|
+
private startTime;
|
|
76
|
+
private aborted;
|
|
77
|
+
private abortController;
|
|
78
|
+
constructor(client: Client, manifestUrl: string, options?: HlsOptions);
|
|
79
|
+
get [Symbol.toStringTag](): string;
|
|
80
|
+
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
81
|
+
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
|
|
82
|
+
finally(onfinally?: (() => void) | null): Promise<void>;
|
|
83
|
+
cancel(): void;
|
|
84
|
+
download(dest: DownloadDest): Promise<void>;
|
|
85
|
+
stream(): AsyncGenerator<SegmentData>;
|
|
86
|
+
pipe(writable: Writable | WriteStream): Promise<void>;
|
|
87
|
+
info(): Promise<{
|
|
88
|
+
master?: HlsMasterPlaylist;
|
|
89
|
+
playlist?: HlsPlaylist;
|
|
90
|
+
selectedVariant?: HlsVariant;
|
|
91
|
+
isLive: boolean;
|
|
92
|
+
totalDuration?: number;
|
|
93
|
+
}>;
|
|
94
|
+
private resolveMediaPlaylist;
|
|
95
|
+
private fetchMediaPlaylist;
|
|
96
|
+
private downloadSegment;
|
|
97
|
+
private downloadMerged;
|
|
98
|
+
private downloadChunks;
|
|
99
|
+
private isLiveMode;
|
|
100
|
+
private getMaxDuration;
|
|
101
|
+
private emitProgress;
|
|
102
|
+
private sleep;
|
|
103
|
+
}
|
|
104
|
+
export declare function hls(client: Client, manifestUrl: string, options?: HlsOptions): HlsPromise;
|
|
105
|
+
export type { HlsVariant as Variant, HlsSegment as Segment, HlsKeyInfo as KeyInfo, };
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
function parseAttributes(line, prefix) {
|
|
5
|
+
const attrs = {};
|
|
6
|
+
const content = line.substring(prefix.length);
|
|
7
|
+
const regex = /([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g;
|
|
8
|
+
let match;
|
|
9
|
+
while ((match = regex.exec(content)) !== null) {
|
|
10
|
+
attrs[match[1]] = match[2] ?? match[3];
|
|
11
|
+
}
|
|
12
|
+
return attrs;
|
|
13
|
+
}
|
|
14
|
+
function resolveUrl(url, baseUrl) {
|
|
15
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const base = new URL(baseUrl);
|
|
20
|
+
const basePath = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1);
|
|
21
|
+
return new URL(url, base.origin + basePath).toString();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseMasterPlaylist(content, baseUrl) {
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
const variants = [];
|
|
30
|
+
let pendingVariant = {};
|
|
31
|
+
for (const rawLine of lines) {
|
|
32
|
+
const line = rawLine.trim();
|
|
33
|
+
if (!line)
|
|
34
|
+
continue;
|
|
35
|
+
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
|
36
|
+
const attrs = parseAttributes(line, '#EXT-X-STREAM-INF:');
|
|
37
|
+
pendingVariant = {
|
|
38
|
+
bandwidth: attrs.BANDWIDTH ? parseInt(attrs.BANDWIDTH, 10) : undefined,
|
|
39
|
+
resolution: attrs.RESOLUTION,
|
|
40
|
+
codecs: attrs.CODECS,
|
|
41
|
+
name: attrs.NAME,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else if (!line.startsWith('#') && pendingVariant.bandwidth !== undefined) {
|
|
45
|
+
variants.push({
|
|
46
|
+
...pendingVariant,
|
|
47
|
+
url: resolveUrl(line, baseUrl),
|
|
48
|
+
});
|
|
49
|
+
pendingVariant = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { variants, isMaster: true };
|
|
53
|
+
}
|
|
54
|
+
function parseMediaPlaylist(content, baseUrl) {
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
const segments = [];
|
|
57
|
+
let targetDuration = 5;
|
|
58
|
+
let mediaSequence = 0;
|
|
59
|
+
let discontinuitySequence = 0;
|
|
60
|
+
let endList = false;
|
|
61
|
+
let playlistType;
|
|
62
|
+
let currentDuration = 0;
|
|
63
|
+
let currentKey;
|
|
64
|
+
let currentDiscontinuity = false;
|
|
65
|
+
let currentProgramDateTime;
|
|
66
|
+
let segmentIndex = 0;
|
|
67
|
+
for (const rawLine of lines) {
|
|
68
|
+
const line = rawLine.trim();
|
|
69
|
+
if (!line)
|
|
70
|
+
continue;
|
|
71
|
+
if (line.startsWith('#EXT-X-TARGETDURATION:')) {
|
|
72
|
+
targetDuration = parseInt(line.split(':')[1], 10);
|
|
73
|
+
}
|
|
74
|
+
else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
|
|
75
|
+
mediaSequence = parseInt(line.split(':')[1], 10);
|
|
76
|
+
}
|
|
77
|
+
else if (line.startsWith('#EXT-X-DISCONTINUITY-SEQUENCE:')) {
|
|
78
|
+
discontinuitySequence = parseInt(line.split(':')[1], 10);
|
|
79
|
+
}
|
|
80
|
+
else if (line.startsWith('#EXT-X-PLAYLIST-TYPE:')) {
|
|
81
|
+
playlistType = line.split(':')[1];
|
|
82
|
+
}
|
|
83
|
+
else if (line === '#EXT-X-ENDLIST') {
|
|
84
|
+
endList = true;
|
|
85
|
+
}
|
|
86
|
+
else if (line === '#EXT-X-DISCONTINUITY') {
|
|
87
|
+
currentDiscontinuity = true;
|
|
88
|
+
}
|
|
89
|
+
else if (line.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) {
|
|
90
|
+
currentProgramDateTime = new Date(line.split(':').slice(1).join(':'));
|
|
91
|
+
}
|
|
92
|
+
else if (line.startsWith('#EXT-X-KEY:')) {
|
|
93
|
+
const attrs = parseAttributes(line, '#EXT-X-KEY:');
|
|
94
|
+
if (attrs.METHOD === 'NONE') {
|
|
95
|
+
currentKey = undefined;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
currentKey = {
|
|
99
|
+
method: attrs.METHOD,
|
|
100
|
+
uri: attrs.URI ? resolveUrl(attrs.URI, baseUrl) : undefined,
|
|
101
|
+
iv: attrs.IV,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (line.startsWith('#EXTINF:')) {
|
|
106
|
+
const durationStr = line.substring(8).split(',')[0];
|
|
107
|
+
currentDuration = parseFloat(durationStr);
|
|
108
|
+
}
|
|
109
|
+
else if (!line.startsWith('#')) {
|
|
110
|
+
segments.push({
|
|
111
|
+
url: resolveUrl(line, baseUrl),
|
|
112
|
+
duration: currentDuration,
|
|
113
|
+
sequence: mediaSequence + segmentIndex,
|
|
114
|
+
key: currentKey,
|
|
115
|
+
discontinuity: currentDiscontinuity,
|
|
116
|
+
programDateTime: currentProgramDateTime,
|
|
117
|
+
});
|
|
118
|
+
segmentIndex++;
|
|
119
|
+
currentDiscontinuity = false;
|
|
120
|
+
currentProgramDateTime = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
segments,
|
|
125
|
+
targetDuration,
|
|
126
|
+
mediaSequence,
|
|
127
|
+
endList,
|
|
128
|
+
playlistType,
|
|
129
|
+
discontinuitySequence,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function isMasterPlaylist(content) {
|
|
133
|
+
return content.includes('#EXT-X-STREAM-INF');
|
|
134
|
+
}
|
|
135
|
+
function selectVariant(variants, quality) {
|
|
136
|
+
if (!variants.length) {
|
|
137
|
+
throw new Error('No variants found in master playlist');
|
|
138
|
+
}
|
|
139
|
+
const sorted = [...variants].sort((a, b) => (a.bandwidth ?? 0) - (b.bandwidth ?? 0));
|
|
140
|
+
if (quality === 'lowest') {
|
|
141
|
+
return sorted[0];
|
|
142
|
+
}
|
|
143
|
+
if (quality === 'highest' || quality === undefined) {
|
|
144
|
+
return sorted[sorted.length - 1];
|
|
145
|
+
}
|
|
146
|
+
if (quality.resolution) {
|
|
147
|
+
const match = variants.find((v) => v.resolution === quality.resolution);
|
|
148
|
+
if (match)
|
|
149
|
+
return match;
|
|
150
|
+
}
|
|
151
|
+
if (quality.bandwidth) {
|
|
152
|
+
const target = quality.bandwidth;
|
|
153
|
+
return sorted.reduce((prev, curr) => {
|
|
154
|
+
const prevDiff = Math.abs((prev.bandwidth ?? 0) - target);
|
|
155
|
+
const currDiff = Math.abs((curr.bandwidth ?? 0) - target);
|
|
156
|
+
return currDiff < prevDiff ? curr : prev;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return sorted[sorted.length - 1];
|
|
160
|
+
}
|
|
161
|
+
export class HlsPromise {
|
|
162
|
+
client;
|
|
163
|
+
manifestUrl;
|
|
164
|
+
options;
|
|
165
|
+
seenSequences = new Set();
|
|
166
|
+
downloadedBytes = 0;
|
|
167
|
+
downloadedSegments = 0;
|
|
168
|
+
startTime = 0;
|
|
169
|
+
aborted = false;
|
|
170
|
+
abortController = new AbortController();
|
|
171
|
+
constructor(client, manifestUrl, options = {}) {
|
|
172
|
+
this.client = client;
|
|
173
|
+
this.manifestUrl = manifestUrl;
|
|
174
|
+
this.options = {
|
|
175
|
+
mode: 'merge',
|
|
176
|
+
format: 'ts',
|
|
177
|
+
concurrency: 5,
|
|
178
|
+
...options,
|
|
179
|
+
};
|
|
180
|
+
if (this.options.format !== 'ts') {
|
|
181
|
+
throw new Error(`Format '${this.options.format}' requires ffmpeg. Use format: 'ts' or install ffmpeg.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
get [Symbol.toStringTag]() {
|
|
185
|
+
return 'HlsPromise';
|
|
186
|
+
}
|
|
187
|
+
then(onfulfilled, onrejected) {
|
|
188
|
+
return Promise.reject(new Error('HlsPromise requires .download(), .stream(), or .pipe() to execute')).then(onfulfilled, onrejected);
|
|
189
|
+
}
|
|
190
|
+
catch(onrejected) {
|
|
191
|
+
return this.then(null, onrejected);
|
|
192
|
+
}
|
|
193
|
+
finally(onfinally) {
|
|
194
|
+
return this.then(() => {
|
|
195
|
+
onfinally?.();
|
|
196
|
+
}, () => {
|
|
197
|
+
onfinally?.();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
cancel() {
|
|
201
|
+
this.aborted = true;
|
|
202
|
+
this.abortController.abort();
|
|
203
|
+
}
|
|
204
|
+
async download(dest) {
|
|
205
|
+
this.startTime = Date.now();
|
|
206
|
+
const mediaPlaylistUrl = await this.resolveMediaPlaylist();
|
|
207
|
+
if (this.options.mode === 'chunks') {
|
|
208
|
+
await this.downloadChunks(mediaPlaylistUrl, dest);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (typeof dest !== 'string') {
|
|
212
|
+
throw new Error('Merge mode requires a string path, not a function');
|
|
213
|
+
}
|
|
214
|
+
await this.downloadMerged(mediaPlaylistUrl, dest);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async *stream() {
|
|
218
|
+
this.startTime = Date.now();
|
|
219
|
+
const mediaPlaylistUrl = await this.resolveMediaPlaylist();
|
|
220
|
+
const isLive = this.isLiveMode();
|
|
221
|
+
const maxDuration = this.getMaxDuration();
|
|
222
|
+
while (!this.aborted) {
|
|
223
|
+
const playlist = await this.fetchMediaPlaylist(mediaPlaylistUrl);
|
|
224
|
+
const newSegments = playlist.segments.filter((s) => !this.seenSequences.has(s.sequence));
|
|
225
|
+
for (const segment of newSegments) {
|
|
226
|
+
if (this.aborted)
|
|
227
|
+
break;
|
|
228
|
+
if (maxDuration && Date.now() - this.startTime > maxDuration) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.seenSequences.add(segment.sequence);
|
|
232
|
+
const data = await this.downloadSegment(segment);
|
|
233
|
+
const segmentData = {
|
|
234
|
+
sequence: segment.sequence,
|
|
235
|
+
duration: segment.duration,
|
|
236
|
+
data,
|
|
237
|
+
url: segment.url,
|
|
238
|
+
downloadedAt: new Date(),
|
|
239
|
+
};
|
|
240
|
+
this.downloadedSegments++;
|
|
241
|
+
this.downloadedBytes += data.byteLength;
|
|
242
|
+
this.emitProgress(playlist, isLive);
|
|
243
|
+
if (this.options.onSegment) {
|
|
244
|
+
await this.options.onSegment(segmentData);
|
|
245
|
+
}
|
|
246
|
+
yield segmentData;
|
|
247
|
+
}
|
|
248
|
+
if (!isLive || playlist.endList) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (maxDuration && Date.now() - this.startTime > maxDuration) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
const pollInterval = Math.max(1000, (playlist.targetDuration * 1000) / 2);
|
|
255
|
+
await this.sleep(pollInterval);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async pipe(writable) {
|
|
259
|
+
try {
|
|
260
|
+
for await (const segment of this.stream()) {
|
|
261
|
+
const canContinue = writable.write(segment.data);
|
|
262
|
+
if (!canContinue) {
|
|
263
|
+
await new Promise((resolve) => writable.once('drain', resolve));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if ('end' in writable && typeof writable.end === 'function') {
|
|
269
|
+
writable.end();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async info() {
|
|
274
|
+
const content = await this.client.get(this.manifestUrl).text();
|
|
275
|
+
if (isMasterPlaylist(content)) {
|
|
276
|
+
const master = parseMasterPlaylist(content, this.manifestUrl);
|
|
277
|
+
const selectedVariant = selectVariant(master.variants, this.options.quality);
|
|
278
|
+
const playlistContent = await this.client.get(selectedVariant.url).text();
|
|
279
|
+
const playlist = parseMediaPlaylist(playlistContent, selectedVariant.url);
|
|
280
|
+
const totalDuration = playlist.endList
|
|
281
|
+
? playlist.segments.reduce((sum, s) => sum + s.duration, 0)
|
|
282
|
+
: undefined;
|
|
283
|
+
return {
|
|
284
|
+
master,
|
|
285
|
+
playlist,
|
|
286
|
+
selectedVariant,
|
|
287
|
+
isLive: !playlist.endList,
|
|
288
|
+
totalDuration,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const playlist = parseMediaPlaylist(content, this.manifestUrl);
|
|
292
|
+
const totalDuration = playlist.endList
|
|
293
|
+
? playlist.segments.reduce((sum, s) => sum + s.duration, 0)
|
|
294
|
+
: undefined;
|
|
295
|
+
return {
|
|
296
|
+
playlist,
|
|
297
|
+
isLive: !playlist.endList,
|
|
298
|
+
totalDuration,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async resolveMediaPlaylist() {
|
|
302
|
+
const content = await this.client.get(this.manifestUrl).text();
|
|
303
|
+
if (!isMasterPlaylist(content)) {
|
|
304
|
+
return this.manifestUrl;
|
|
305
|
+
}
|
|
306
|
+
const master = parseMasterPlaylist(content, this.manifestUrl);
|
|
307
|
+
const variant = selectVariant(master.variants, this.options.quality);
|
|
308
|
+
return variant.url;
|
|
309
|
+
}
|
|
310
|
+
async fetchMediaPlaylist(url) {
|
|
311
|
+
const content = await this.client.get(url).text();
|
|
312
|
+
return parseMediaPlaylist(content, url);
|
|
313
|
+
}
|
|
314
|
+
async downloadSegment(segment) {
|
|
315
|
+
if (segment.key && segment.key.method !== 'NONE') {
|
|
316
|
+
throw new Error(`Encrypted HLS (${segment.key.method}) requires ffmpeg. Use unencrypted streams or install ffmpeg.`);
|
|
317
|
+
}
|
|
318
|
+
const response = await this.client.get(segment.url, {
|
|
319
|
+
headers: this.options.headers,
|
|
320
|
+
signal: this.abortController.signal,
|
|
321
|
+
});
|
|
322
|
+
const blob = await response.blob();
|
|
323
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
324
|
+
}
|
|
325
|
+
async downloadMerged(playlistUrl, outputPath) {
|
|
326
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
327
|
+
const output = createWriteStream(outputPath);
|
|
328
|
+
try {
|
|
329
|
+
await this.pipe(output);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
output.destroy();
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async downloadChunks(playlistUrl, dest) {
|
|
337
|
+
const getPath = typeof dest === 'string'
|
|
338
|
+
? (seg) => join(dest, `segment-${seg.sequence}.ts`)
|
|
339
|
+
: dest;
|
|
340
|
+
const baseDir = typeof dest === 'string' ? dest : dirname(getPath({ sequence: 0, duration: 0, url: '' }));
|
|
341
|
+
await mkdir(baseDir, { recursive: true });
|
|
342
|
+
for await (const segment of this.stream()) {
|
|
343
|
+
const filePath = getPath({
|
|
344
|
+
sequence: segment.sequence,
|
|
345
|
+
duration: segment.duration,
|
|
346
|
+
url: segment.url,
|
|
347
|
+
});
|
|
348
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
349
|
+
const output = createWriteStream(filePath);
|
|
350
|
+
await new Promise((resolve, reject) => {
|
|
351
|
+
output.write(segment.data, (err) => {
|
|
352
|
+
if (err)
|
|
353
|
+
reject(err);
|
|
354
|
+
else {
|
|
355
|
+
output.end();
|
|
356
|
+
resolve();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
isLiveMode() {
|
|
363
|
+
return this.options.live === true || typeof this.options.live === 'object';
|
|
364
|
+
}
|
|
365
|
+
getMaxDuration() {
|
|
366
|
+
if (typeof this.options.live === 'object' && this.options.live.duration) {
|
|
367
|
+
return this.options.live.duration;
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
emitProgress(playlist, isLive) {
|
|
372
|
+
if (!this.options.onProgress)
|
|
373
|
+
return;
|
|
374
|
+
this.options.onProgress({
|
|
375
|
+
downloadedSegments: this.downloadedSegments,
|
|
376
|
+
totalSegments: isLive ? undefined : playlist.segments.length,
|
|
377
|
+
downloadedBytes: this.downloadedBytes,
|
|
378
|
+
currentSegment: Math.max(...this.seenSequences),
|
|
379
|
+
isLive,
|
|
380
|
+
elapsed: Date.now() - this.startTime,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
sleep(ms) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
const timeout = setTimeout(resolve, ms);
|
|
386
|
+
this.abortController.signal.addEventListener('abort', () => {
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
resolve();
|
|
389
|
+
}, { once: true });
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export function hls(client, manifestUrl, options = {}) {
|
|
394
|
+
return new HlsPromise(client, manifestUrl, options);
|
|
395
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Client } from '../core/client.js';
|
|
2
|
+
import type { RequestOptions } from '../types/index.js';
|
|
3
|
+
export interface JsonRpcRequest {
|
|
4
|
+
jsonrpc: '2.0';
|
|
5
|
+
method: string;
|
|
6
|
+
params?: unknown[] | Record<string, unknown>;
|
|
7
|
+
id?: string | number | null;
|
|
8
|
+
}
|
|
9
|
+
export interface JsonRpcResponse<T = unknown> {
|
|
10
|
+
jsonrpc: '2.0';
|
|
11
|
+
result?: T;
|
|
12
|
+
error?: JsonRpcError;
|
|
13
|
+
id: string | number | null;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonRpcError {
|
|
16
|
+
code: number;
|
|
17
|
+
message: string;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface JsonRpcBatchResponse<T = unknown> {
|
|
21
|
+
responses: JsonRpcResponse<T>[];
|
|
22
|
+
errors: JsonRpcError[];
|
|
23
|
+
hasErrors: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare const JsonRpcErrorCodes: {
|
|
26
|
+
readonly PARSE_ERROR: -32700;
|
|
27
|
+
readonly INVALID_REQUEST: -32600;
|
|
28
|
+
readonly METHOD_NOT_FOUND: -32601;
|
|
29
|
+
readonly INVALID_PARAMS: -32602;
|
|
30
|
+
readonly INTERNAL_ERROR: -32603;
|
|
31
|
+
readonly SERVER_ERROR: -32000;
|
|
32
|
+
};
|
|
33
|
+
export declare class JsonRpcException extends Error {
|
|
34
|
+
readonly code: number;
|
|
35
|
+
readonly data?: unknown;
|
|
36
|
+
constructor(error: JsonRpcError);
|
|
37
|
+
static isParseError(error: JsonRpcException): boolean;
|
|
38
|
+
static isInvalidRequest(error: JsonRpcException): boolean;
|
|
39
|
+
static isMethodNotFound(error: JsonRpcException): boolean;
|
|
40
|
+
static isInvalidParams(error: JsonRpcException): boolean;
|
|
41
|
+
static isInternalError(error: JsonRpcException): boolean;
|
|
42
|
+
static isServerError(error: JsonRpcException): boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface JsonRpcClientOptions {
|
|
45
|
+
endpoint: string;
|
|
46
|
+
requestOptions?: RequestOptions;
|
|
47
|
+
autoId?: boolean;
|
|
48
|
+
idGenerator?: () => string | number;
|
|
49
|
+
throwOnError?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare class JsonRpcClient {
|
|
52
|
+
private client;
|
|
53
|
+
private options;
|
|
54
|
+
private idCounter;
|
|
55
|
+
constructor(client: Client, options: JsonRpcClientOptions);
|
|
56
|
+
call<T = unknown>(method: string, params?: unknown[] | Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
|
57
|
+
notify(method: string, params?: unknown[] | Record<string, unknown>, options?: RequestOptions): Promise<void>;
|
|
58
|
+
batch<T = unknown>(requests: Array<{
|
|
59
|
+
method: string;
|
|
60
|
+
params?: unknown[] | Record<string, unknown>;
|
|
61
|
+
id?: string | number;
|
|
62
|
+
}>, options?: RequestOptions): Promise<JsonRpcBatchResponse<T>>;
|
|
63
|
+
getFromBatch<T>(batch: JsonRpcBatchResponse<T>, id: string | number): T | undefined;
|
|
64
|
+
proxy<T extends Record<string, (...args: unknown[]) => unknown>>(): {
|
|
65
|
+
[K in keyof T]: (...args: Parameters<T[K]>) => Promise<ReturnType<T[K]>>;
|
|
66
|
+
};
|
|
67
|
+
private sendRequest;
|
|
68
|
+
}
|
|
69
|
+
export declare function createJsonRpcClient(client: Client, options: JsonRpcClientOptions): JsonRpcClient;
|
|
70
|
+
export declare function jsonrpc(): (client: Client) => void;
|
|
71
|
+
declare module '../core/client.js' {
|
|
72
|
+
interface Client {
|
|
73
|
+
jsonrpc(endpoint: string, options?: Omit<JsonRpcClientOptions, 'endpoint'>): JsonRpcClient;
|
|
74
|
+
}
|
|
75
|
+
}
|