@echovisionlab/youtube-audio 0.1.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.
- package/DISCLOSURE.md +12 -0
- package/LICENSE.md +6 -0
- package/README.md +58 -0
- package/dist/contracts.d.ts +74 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-store.d.ts +3 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +15 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/service.d.ts +3 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +248 -0
- package/dist/service.js.map +1 -0
- package/dist/youtube-js.d.ts +14 -0
- package/dist/youtube-js.d.ts.map +1 -0
- package/dist/youtube-js.js +107 -0
- package/dist/youtube-js.js.map +1 -0
- package/dist/youtube-url.d.ts +3 -0
- package/dist/youtube-url.d.ts.map +1 -0
- package/dist/youtube-url.js +59 -0
- package/dist/youtube-url.js.map +1 -0
- package/package.json +61 -0
package/DISCLOSURE.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Dual-use software disclosure
|
|
2
|
+
|
|
3
|
+
`@echovisionlab/youtube-audio` resolves and proxies authenticated access to
|
|
4
|
+
YouTube audio sources. This capability can be used for legitimate workflows,
|
|
5
|
+
including processing media owned by the user or covered by permission, license,
|
|
6
|
+
or another lawful basis. It can also be misused to copy protected media without
|
|
7
|
+
authorization.
|
|
8
|
+
|
|
9
|
+
Integrators must restrict access to authenticated users, obtain all necessary
|
|
10
|
+
copyright and rights clearances, and comply with YouTube's terms and applicable
|
|
11
|
+
law. Echo Vision Lab does not encourage infringement, piracy, or unauthorized
|
|
12
|
+
copying.
|
package/LICENSE.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# YouTube audio
|
|
2
|
+
|
|
3
|
+
Server-side YouTube audio source resolution and authenticated byte-range
|
|
4
|
+
proxying for applications that perform browser-local audio processing.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @echovisionlab/youtube-audio
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The browser receives a same-origin opaque source URL, not the upstream media
|
|
11
|
+
URL. Integrators must authenticate every resolve and range request, derive the
|
|
12
|
+
subject from the server-side session, and store short-lived source records in a
|
|
13
|
+
shared subject-bound store.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createYoutubeAudioService } from "@echovisionlab/youtube-audio";
|
|
17
|
+
import { createYoutubeJsAudioProvider } from "@echovisionlab/youtube-audio/youtube-js";
|
|
18
|
+
|
|
19
|
+
const service = createYoutubeAudioService({
|
|
20
|
+
provider: createYoutubeJsAudioProvider(),
|
|
21
|
+
sourceStore,
|
|
22
|
+
makeSourceUrl: (id) => `/api/youtube-audio/${id}`,
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Responsible use
|
|
27
|
+
|
|
28
|
+
This package is dual-use software, not a public download service. Access must
|
|
29
|
+
be restricted to signed-in users. Use it only for media you own or are
|
|
30
|
+
authorized to acquire and process, and obtain all required copyright and rights
|
|
31
|
+
clearances. Echo Vision Lab does not encourage infringement or unauthorized
|
|
32
|
+
copying. See [DISCLOSURE.md](DISCLOSURE.md).
|
|
33
|
+
|
|
34
|
+
YouTube and its private APIs can change independently. Keep failures observable
|
|
35
|
+
and review YouTube's terms and applicable law for your integration.
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
corepack enable
|
|
41
|
+
pnpm install --frozen-lockfile
|
|
42
|
+
pnpm typecheck
|
|
43
|
+
pnpm test:coverage
|
|
44
|
+
pnpm build
|
|
45
|
+
pnpm package:verify
|
|
46
|
+
pnpm package:pack
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Release
|
|
50
|
+
|
|
51
|
+
The initial package version requires an interactive npm publish with 2FA. Later
|
|
52
|
+
versions use npm staged publishing from a GitHub Actions trusted publisher and
|
|
53
|
+
still require maintainer approval with 2FA.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
PolyForm Noncommercial 1.0.0. Commercial use requires a separate license from
|
|
58
|
+
Echo Vision Lab. See [LICENSE.md](LICENSE.md).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface YoutubeVideoReference {
|
|
2
|
+
readonly canonicalUrl: string;
|
|
3
|
+
readonly videoId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface YoutubeAudioUpstreamSource {
|
|
6
|
+
readonly contentType: string;
|
|
7
|
+
readonly expiresAt: number | null;
|
|
8
|
+
readonly fileName: string;
|
|
9
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly size: number;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly videoId: string;
|
|
14
|
+
}
|
|
15
|
+
export interface YoutubeAudioProvider {
|
|
16
|
+
resolve(video: YoutubeVideoReference, signal?: AbortSignal): Promise<YoutubeAudioUpstreamSource>;
|
|
17
|
+
}
|
|
18
|
+
export interface YoutubeAudioSourceRecord {
|
|
19
|
+
readonly expiresAt: number;
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly subject: string;
|
|
22
|
+
readonly upstream: YoutubeAudioUpstreamSource;
|
|
23
|
+
}
|
|
24
|
+
export interface YoutubeAudioSourceStore {
|
|
25
|
+
delete(id: string): Promise<void>;
|
|
26
|
+
get(id: string): Promise<YoutubeAudioSourceRecord | null>;
|
|
27
|
+
put(source: YoutubeAudioSourceRecord): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface YoutubeAudioTranscoderInput {
|
|
30
|
+
readonly http: {
|
|
31
|
+
readonly credentials: 'include';
|
|
32
|
+
readonly size: number;
|
|
33
|
+
readonly url: string;
|
|
34
|
+
};
|
|
35
|
+
readonly name: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedYoutubeAudio {
|
|
38
|
+
readonly contentType: string;
|
|
39
|
+
readonly expiresAt: number;
|
|
40
|
+
readonly input: YoutubeAudioTranscoderInput;
|
|
41
|
+
readonly sourceId: string;
|
|
42
|
+
readonly title: string;
|
|
43
|
+
readonly videoId: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ResolveYoutubeAudioRequest {
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
readonly subject: string;
|
|
48
|
+
readonly url: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ReadYoutubeAudioRequest {
|
|
51
|
+
readonly range: string;
|
|
52
|
+
readonly signal?: AbortSignal;
|
|
53
|
+
readonly sourceId: string;
|
|
54
|
+
readonly subject: string;
|
|
55
|
+
}
|
|
56
|
+
export interface RevokeYoutubeAudioRequest {
|
|
57
|
+
readonly sourceId: string;
|
|
58
|
+
readonly subject: string;
|
|
59
|
+
}
|
|
60
|
+
export interface CreateYoutubeAudioServiceOptions {
|
|
61
|
+
readonly createSourceId?: () => string;
|
|
62
|
+
readonly fetch?: typeof fetch;
|
|
63
|
+
readonly makeSourceUrl: (sourceId: string) => string;
|
|
64
|
+
readonly now?: () => number;
|
|
65
|
+
readonly provider: YoutubeAudioProvider;
|
|
66
|
+
readonly sourceStore: YoutubeAudioSourceStore;
|
|
67
|
+
readonly sourceTtlMs?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface YoutubeAudioService {
|
|
70
|
+
read(request: ReadYoutubeAudioRequest): Promise<Response>;
|
|
71
|
+
resolve(request: ResolveYoutubeAudioRequest): Promise<ResolvedYoutubeAudio>;
|
|
72
|
+
revoke(request: RevokeYoutubeAudioRequest): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":""}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type YoutubeAudioErrorCode = 'INVALID_REQUEST' | 'INVALID_UPSTREAM_RESPONSE' | 'REQUEST_ABORTED' | 'SOURCE_EXPIRED' | 'SOURCE_NOT_FOUND' | 'UNAUTHORIZED' | 'UNSUPPORTED_VIDEO' | 'UPSTREAM_FAILURE';
|
|
2
|
+
export declare class YoutubeAudioError extends Error {
|
|
3
|
+
readonly code: YoutubeAudioErrorCode;
|
|
4
|
+
constructor(code: YoutubeAudioErrorCode, message: string);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAC7B,iBAAiB,GACjB,2BAA2B,GAC3B,iBAAiB,GACjB,gBAAgB,GAChB,kBAAkB,GAClB,cAAc,GACd,mBAAmB,GACnB,kBAAkB,CAAC;AAEvB,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAErC,YAAY,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,EAIvD;CACF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAUA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,IAAI,CAAwB;IAErC,YAAY,IAA2B,EAAE,OAAe;QACtD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { YoutubeAudioError } from './errors.js';
|
|
2
|
+
export { createMemoryYoutubeAudioSourceStore } from './memory-store.js';
|
|
3
|
+
export { createYoutubeAudioService } from './service.js';
|
|
4
|
+
export { parseYoutubeVideoUrl } from './youtube-url.js';
|
|
5
|
+
export type { CreateYoutubeAudioServiceOptions, ReadYoutubeAudioRequest, ResolvedYoutubeAudio, ResolveYoutubeAudioRequest, RevokeYoutubeAudioRequest, YoutubeAudioProvider, YoutubeAudioService, YoutubeAudioSourceRecord, YoutubeAudioSourceStore, YoutubeAudioTranscoderInput, YoutubeAudioUpstreamSource, YoutubeVideoReference, } from './contracts.js';
|
|
6
|
+
export type { YoutubeAudioErrorCode } from './errors.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,mCAAmC,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EACV,gCAAgC,EAChC,uBAAuB,EACvB,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { YoutubeAudioError } from './errors.js';
|
|
2
|
+
export { createMemoryYoutubeAudioSourceStore } from './memory-store.js';
|
|
3
|
+
export { createYoutubeAudioService } from './service.js';
|
|
4
|
+
export { parseYoutubeVideoUrl } from './youtube-url.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,mCAAmC,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,uBAAuB,EACxB,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,mCAAmC,IAAI,uBAAuB,CAa7E"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function createMemoryYoutubeAudioSourceStore() {
|
|
2
|
+
const sources = new Map();
|
|
3
|
+
return {
|
|
4
|
+
async delete(id) {
|
|
5
|
+
sources.delete(id);
|
|
6
|
+
},
|
|
7
|
+
async get(id) {
|
|
8
|
+
return sources.get(id) ?? null;
|
|
9
|
+
},
|
|
10
|
+
async put(source) {
|
|
11
|
+
sources.set(source.id, source);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=memory-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,mCAAmC;IACjD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoC,CAAC;IAC5D,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,EAAE;YACb,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,EAAE;YACV,OAAO,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;QACjC,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,MAAM;YACd,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gCAAgC,EAKhC,mBAAmB,EAGpB,MAAM,gBAAgB,CAAC;AAQxB,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,mBAAmB,CAqIrB"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { YoutubeAudioError } from './errors.js';
|
|
3
|
+
import { parseYoutubeVideoUrl } from './youtube-url.js';
|
|
4
|
+
const DEFAULT_SOURCE_TTL_MS = 10 * 60 * 1_000;
|
|
5
|
+
const MAX_SOURCE_TTL_MS = 60 * 60 * 1_000;
|
|
6
|
+
const SUBJECT_MAX_LENGTH = 256;
|
|
7
|
+
export function createYoutubeAudioService(options) {
|
|
8
|
+
const sourceTtlMs = validateTtl(options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS);
|
|
9
|
+
const now = options.now ?? Date.now;
|
|
10
|
+
const createSourceId = options.createSourceId ?? randomUUID;
|
|
11
|
+
const fetchSource = options.fetch ?? fetch;
|
|
12
|
+
return {
|
|
13
|
+
async resolve(request) {
|
|
14
|
+
return resolveSource(request);
|
|
15
|
+
},
|
|
16
|
+
async read(request) {
|
|
17
|
+
return readSource(request);
|
|
18
|
+
},
|
|
19
|
+
async revoke(request) {
|
|
20
|
+
await revokeSource(request);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
async function resolveSource(request) {
|
|
24
|
+
const subject = validateSubject(request.subject);
|
|
25
|
+
throwIfAborted(request.signal);
|
|
26
|
+
const video = parseYoutubeVideoUrl(request.url);
|
|
27
|
+
let upstream;
|
|
28
|
+
try {
|
|
29
|
+
upstream = await options.provider.resolve(video, request.signal);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
rethrowProviderError(error, request.signal);
|
|
33
|
+
}
|
|
34
|
+
throwIfAborted(request.signal);
|
|
35
|
+
validateUpstreamSource(upstream, video.videoId, now());
|
|
36
|
+
const issuedAt = now();
|
|
37
|
+
const ttlExpiry = issuedAt + sourceTtlMs;
|
|
38
|
+
const expiresAt = upstream.expiresAt === null
|
|
39
|
+
? ttlExpiry
|
|
40
|
+
: Math.min(ttlExpiry, upstream.expiresAt);
|
|
41
|
+
const sourceId = validateSourceId(createSourceId());
|
|
42
|
+
const sourceUrl = validateSourceUrl(options.makeSourceUrl(sourceId));
|
|
43
|
+
const record = Object.freeze({
|
|
44
|
+
expiresAt,
|
|
45
|
+
id: sourceId,
|
|
46
|
+
subject,
|
|
47
|
+
upstream: freezeUpstream(upstream),
|
|
48
|
+
});
|
|
49
|
+
await options.sourceStore.put(record);
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
contentType: upstream.contentType,
|
|
52
|
+
expiresAt,
|
|
53
|
+
input: Object.freeze({
|
|
54
|
+
http: Object.freeze({
|
|
55
|
+
credentials: 'include',
|
|
56
|
+
size: upstream.size,
|
|
57
|
+
url: sourceUrl,
|
|
58
|
+
}),
|
|
59
|
+
name: upstream.fileName,
|
|
60
|
+
}),
|
|
61
|
+
sourceId,
|
|
62
|
+
title: upstream.title,
|
|
63
|
+
videoId: upstream.videoId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function readSource(request) {
|
|
67
|
+
const subject = validateSubject(request.subject);
|
|
68
|
+
const sourceId = validateSourceId(request.sourceId);
|
|
69
|
+
throwIfAborted(request.signal);
|
|
70
|
+
const record = await options.sourceStore.get(sourceId);
|
|
71
|
+
if (record === null) {
|
|
72
|
+
throw new YoutubeAudioError('SOURCE_NOT_FOUND', 'Audio source was not found.');
|
|
73
|
+
}
|
|
74
|
+
if (record.subject !== subject) {
|
|
75
|
+
throw new YoutubeAudioError('UNAUTHORIZED', 'Audio source access is denied.');
|
|
76
|
+
}
|
|
77
|
+
if (record.expiresAt <= now()) {
|
|
78
|
+
await options.sourceStore.delete(sourceId);
|
|
79
|
+
throw new YoutubeAudioError('SOURCE_EXPIRED', 'Audio source has expired.');
|
|
80
|
+
}
|
|
81
|
+
const range = parseClosedRange(request.range, record.upstream.size);
|
|
82
|
+
const headers = createUpstreamHeaders(record.upstream, range.header);
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await fetchSource(record.upstream.url, {
|
|
86
|
+
cache: 'no-store',
|
|
87
|
+
headers,
|
|
88
|
+
method: 'GET',
|
|
89
|
+
redirect: 'follow',
|
|
90
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throwIfAborted(request.signal);
|
|
95
|
+
throw new YoutubeAudioError('UPSTREAM_FAILURE', 'The upstream audio range request failed.');
|
|
96
|
+
}
|
|
97
|
+
throwIfAborted(request.signal);
|
|
98
|
+
validateUpstreamRangeResponse(response, range, record.upstream.size);
|
|
99
|
+
if (response.body === null) {
|
|
100
|
+
throw new YoutubeAudioError('INVALID_UPSTREAM_RESPONSE', 'The upstream audio response has no body.');
|
|
101
|
+
}
|
|
102
|
+
return new Response(response.body, {
|
|
103
|
+
headers: {
|
|
104
|
+
'Accept-Ranges': 'bytes',
|
|
105
|
+
'Cache-Control': 'private, no-store',
|
|
106
|
+
'Content-Length': String(range.end - range.start + 1),
|
|
107
|
+
'Content-Range': `bytes ${range.start}-${range.end}/${record.upstream.size}`,
|
|
108
|
+
'Content-Type': record.upstream.contentType,
|
|
109
|
+
},
|
|
110
|
+
status: 206,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function revokeSource(request) {
|
|
114
|
+
const subject = validateSubject(request.subject);
|
|
115
|
+
const sourceId = validateSourceId(request.sourceId);
|
|
116
|
+
const record = await options.sourceStore.get(sourceId);
|
|
117
|
+
if (record === null) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (record.subject !== subject) {
|
|
121
|
+
throw new YoutubeAudioError('UNAUTHORIZED', 'Audio source access is denied.');
|
|
122
|
+
}
|
|
123
|
+
await options.sourceStore.delete(sourceId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function parseClosedRange(value, size) {
|
|
127
|
+
const match = /^bytes=(\d+)-(\d+)$/.exec(value);
|
|
128
|
+
if (match === null) {
|
|
129
|
+
throw new YoutubeAudioError('INVALID_REQUEST', 'A single closed byte range is required.');
|
|
130
|
+
}
|
|
131
|
+
const start = Number(match[1]);
|
|
132
|
+
const end = Number(match[2]);
|
|
133
|
+
if (!Number.isSafeInteger(start) ||
|
|
134
|
+
!Number.isSafeInteger(end) ||
|
|
135
|
+
start < 0 ||
|
|
136
|
+
end < start ||
|
|
137
|
+
end >= size) {
|
|
138
|
+
throw new YoutubeAudioError('INVALID_REQUEST', 'The requested byte range is outside the audio source.');
|
|
139
|
+
}
|
|
140
|
+
return { end, header: `bytes=${start}-${end}`, start };
|
|
141
|
+
}
|
|
142
|
+
function validateUpstreamRangeResponse(response, range, size) {
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new YoutubeAudioError('UPSTREAM_FAILURE', `The upstream audio range request returned status ${response.status}.`);
|
|
145
|
+
}
|
|
146
|
+
const expected = `bytes ${range.start}-${range.end}/${size}`;
|
|
147
|
+
if (response.status !== 206 || response.headers.get('content-range') !== expected) {
|
|
148
|
+
throw new YoutubeAudioError('INVALID_UPSTREAM_RESPONSE', 'The upstream server did not honor the exact audio byte range.');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function createUpstreamHeaders(source, range) {
|
|
152
|
+
let headers;
|
|
153
|
+
try {
|
|
154
|
+
headers = new Headers(source.headers);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new YoutubeAudioError('INVALID_UPSTREAM_RESPONSE', 'The upstream audio request headers are invalid.');
|
|
158
|
+
}
|
|
159
|
+
if (headers.has('range')) {
|
|
160
|
+
throw new YoutubeAudioError('INVALID_UPSTREAM_RESPONSE', 'The upstream audio source must not override Range.');
|
|
161
|
+
}
|
|
162
|
+
headers.set('Range', range);
|
|
163
|
+
return headers;
|
|
164
|
+
}
|
|
165
|
+
function validateUpstreamSource(source, expectedVideoId, now) {
|
|
166
|
+
let url;
|
|
167
|
+
try {
|
|
168
|
+
url = new URL(source.url);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw invalidUpstream();
|
|
172
|
+
}
|
|
173
|
+
if (url.protocol !== 'https:' ||
|
|
174
|
+
url.username !== '' ||
|
|
175
|
+
url.password !== '' ||
|
|
176
|
+
source.videoId !== expectedVideoId ||
|
|
177
|
+
!Number.isSafeInteger(source.size) ||
|
|
178
|
+
source.size < 1 ||
|
|
179
|
+
!isNonEmpty(source.contentType) ||
|
|
180
|
+
!isNonEmpty(source.fileName) ||
|
|
181
|
+
!isNonEmpty(source.title) ||
|
|
182
|
+
(source.expiresAt !== null &&
|
|
183
|
+
(!Number.isSafeInteger(source.expiresAt) || source.expiresAt <= now))) {
|
|
184
|
+
throw invalidUpstream();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function freezeUpstream(source) {
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
...source,
|
|
190
|
+
...(source.headers === undefined
|
|
191
|
+
? {}
|
|
192
|
+
: { headers: Object.freeze({ ...source.headers }) }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function validateSubject(subject) {
|
|
196
|
+
const value = subject.trim();
|
|
197
|
+
if (value.length === 0 || value.length > SUBJECT_MAX_LENGTH) {
|
|
198
|
+
throw new YoutubeAudioError('UNAUTHORIZED', 'An authenticated subject is required.');
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function validateSourceId(sourceId) {
|
|
203
|
+
const value = sourceId.trim();
|
|
204
|
+
if (!/^[A-Za-z0-9_-]{8,128}$/.test(value)) {
|
|
205
|
+
throw new YoutubeAudioError('INVALID_REQUEST', 'Audio source ID is invalid.');
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function validateSourceUrl(value) {
|
|
210
|
+
let url;
|
|
211
|
+
try {
|
|
212
|
+
url = new URL(value);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new YoutubeAudioError('INVALID_REQUEST', 'makeSourceUrl must return an absolute HTTP(S) URL.');
|
|
216
|
+
}
|
|
217
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
218
|
+
url.username !== '' ||
|
|
219
|
+
url.password !== '') {
|
|
220
|
+
throw new YoutubeAudioError('INVALID_REQUEST', 'makeSourceUrl must return an absolute HTTP(S) URL.');
|
|
221
|
+
}
|
|
222
|
+
return url.href;
|
|
223
|
+
}
|
|
224
|
+
function validateTtl(value) {
|
|
225
|
+
if (!Number.isSafeInteger(value) || value < 1_000 || value > MAX_SOURCE_TTL_MS) {
|
|
226
|
+
throw new YoutubeAudioError('INVALID_REQUEST', `sourceTtlMs must be an integer from 1000 to ${MAX_SOURCE_TTL_MS}.`);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function throwIfAborted(signal) {
|
|
231
|
+
if (signal?.aborted) {
|
|
232
|
+
throw new YoutubeAudioError('REQUEST_ABORTED', 'The audio request was aborted.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function rethrowProviderError(error, signal) {
|
|
236
|
+
throwIfAborted(signal);
|
|
237
|
+
if (error instanceof YoutubeAudioError) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
throw new YoutubeAudioError('UPSTREAM_FAILURE', 'YouTube audio resolution failed.');
|
|
241
|
+
}
|
|
242
|
+
function invalidUpstream() {
|
|
243
|
+
return new YoutubeAudioError('INVALID_UPSTREAM_RESPONSE', 'The resolved YouTube audio source is invalid.');
|
|
244
|
+
}
|
|
245
|
+
function isNonEmpty(value) {
|
|
246
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Innertube, type Types } from 'youtubei.js';
|
|
2
|
+
import type { YoutubeAudioProvider } from './contracts.js';
|
|
3
|
+
export interface CreateYoutubeJsAudioProviderOptions {
|
|
4
|
+
/** Optional YouTube.js session, useful when the application owns its cache. */
|
|
5
|
+
readonly innertube?: Innertube;
|
|
6
|
+
/** Used only when `innertube` is omitted. */
|
|
7
|
+
readonly innertubeConfig?: Types.InnerTubeConfig;
|
|
8
|
+
/** Optional YouTube client override. */
|
|
9
|
+
readonly client?: Types.InnerTubeClient;
|
|
10
|
+
/** Optional container preference such as `m4a` or `webm`. */
|
|
11
|
+
readonly format?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createYoutubeJsAudioProvider(options?: CreateYoutubeJsAudioProviderOptions): YoutubeAudioProvider;
|
|
14
|
+
//# sourceMappingURL=youtube-js.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube-js.d.ts","sourceRoot":"","sources":["../src/youtube-js.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EACV,oBAAoB,EAGrB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,mCAAmC;IAClD,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,eAAe,CAAC;IACjD,wCAAwC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,eAAe,CAAC;IACxC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,mCAAwC,GAChD,oBAAoB,CA0CtB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Innertube } from 'youtubei.js';
|
|
2
|
+
import { YoutubeAudioError } from './errors.js';
|
|
3
|
+
export function createYoutubeJsAudioProvider(options = {}) {
|
|
4
|
+
let clientPromise;
|
|
5
|
+
const clientType = options.client ?? 'IOS';
|
|
6
|
+
return {
|
|
7
|
+
async resolve(video, signal) {
|
|
8
|
+
throwIfAborted(signal);
|
|
9
|
+
const innertube = await getInnertube();
|
|
10
|
+
throwIfAborted(signal);
|
|
11
|
+
try {
|
|
12
|
+
const info = await innertube.getBasicInfo(video.videoId, videoInfoOptions(clientType));
|
|
13
|
+
throwIfAborted(signal);
|
|
14
|
+
if (info.basic_info.is_live || info.basic_info.is_upcoming) {
|
|
15
|
+
throw new YoutubeAudioError('UNSUPPORTED_VIDEO', 'Live and upcoming YouTube videos are not supported.');
|
|
16
|
+
}
|
|
17
|
+
const format = info.chooseFormat({
|
|
18
|
+
...videoInfoOptions(clientType),
|
|
19
|
+
format: options.format ?? 'any',
|
|
20
|
+
quality: 'best',
|
|
21
|
+
type: 'audio',
|
|
22
|
+
});
|
|
23
|
+
format.url = await format.decipher(innertube.session.player);
|
|
24
|
+
throwIfAborted(signal);
|
|
25
|
+
return toUpstreamSource(video, info.basic_info.title, format);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
rethrowYoutubeJsError(error, signal);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
function getInnertube() {
|
|
33
|
+
clientPromise ??= options.innertube === undefined
|
|
34
|
+
? Innertube.create(options.innertubeConfig)
|
|
35
|
+
: Promise.resolve(options.innertube);
|
|
36
|
+
return clientPromise;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function toUpstreamSource(video, title, format) {
|
|
40
|
+
if (typeof title !== 'string' ||
|
|
41
|
+
title.trim().length === 0 ||
|
|
42
|
+
typeof format.url !== 'string' ||
|
|
43
|
+
format.url.length === 0 ||
|
|
44
|
+
!Number.isSafeInteger(format.content_length) ||
|
|
45
|
+
(format.content_length ?? 0) < 1 ||
|
|
46
|
+
!format.has_audio ||
|
|
47
|
+
format.has_video) {
|
|
48
|
+
throw new YoutubeAudioError('UNSUPPORTED_VIDEO', 'YouTube did not provide a finite audio-only source.');
|
|
49
|
+
}
|
|
50
|
+
const contentType = format.mime_type.split(';', 1)[0]?.trim();
|
|
51
|
+
if (contentType === undefined || !contentType.startsWith('audio/')) {
|
|
52
|
+
throw new YoutubeAudioError('UNSUPPORTED_VIDEO', 'YouTube did not provide a supported audio content type.');
|
|
53
|
+
}
|
|
54
|
+
const upstreamUrl = new URL(format.url);
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
contentType,
|
|
57
|
+
expiresAt: readExpiry(upstreamUrl),
|
|
58
|
+
fileName: `${sanitizeFileName(title)}.${extensionFor(contentType)}`,
|
|
59
|
+
size: format.content_length,
|
|
60
|
+
title: title.trim(),
|
|
61
|
+
url: upstreamUrl.href,
|
|
62
|
+
videoId: video.videoId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function videoInfoOptions(client) {
|
|
66
|
+
return { client };
|
|
67
|
+
}
|
|
68
|
+
function readExpiry(url) {
|
|
69
|
+
const seconds = Number(url.searchParams.get('expire'));
|
|
70
|
+
if (!Number.isSafeInteger(seconds) || seconds < 1) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return seconds * 1_000;
|
|
74
|
+
}
|
|
75
|
+
function extensionFor(contentType) {
|
|
76
|
+
switch (contentType) {
|
|
77
|
+
case 'audio/mp4':
|
|
78
|
+
return 'm4a';
|
|
79
|
+
case 'audio/ogg':
|
|
80
|
+
return 'ogg';
|
|
81
|
+
case 'audio/webm':
|
|
82
|
+
return 'webm';
|
|
83
|
+
default:
|
|
84
|
+
return 'audio';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function sanitizeFileName(title) {
|
|
88
|
+
const sanitized = title
|
|
89
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
|
|
90
|
+
.replace(/[. ]+$/g, '')
|
|
91
|
+
.trim()
|
|
92
|
+
.slice(0, 120);
|
|
93
|
+
return sanitized.length === 0 ? 'youtube-audio' : sanitized;
|
|
94
|
+
}
|
|
95
|
+
function throwIfAborted(signal) {
|
|
96
|
+
if (signal?.aborted) {
|
|
97
|
+
throw new YoutubeAudioError('REQUEST_ABORTED', 'The audio request was aborted.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function rethrowYoutubeJsError(error, signal) {
|
|
101
|
+
throwIfAborted(signal);
|
|
102
|
+
if (error instanceof YoutubeAudioError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
throw new YoutubeAudioError('UPSTREAM_FAILURE', 'YouTube.js could not resolve the audio source.');
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=youtube-js.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube-js.js","sourceRoot":"","sources":["../src/youtube-js.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube-url.d.ts","sourceRoot":"","sources":["../src/youtube-url.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAY5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAwCzE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { YoutubeAudioError } from './errors.js';
|
|
2
|
+
const VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
|
3
|
+
const YOUTUBE_HOSTS = new Set([
|
|
4
|
+
'm.youtube.com',
|
|
5
|
+
'music.youtube.com',
|
|
6
|
+
'www.youtube-nocookie.com',
|
|
7
|
+
'www.youtube.com',
|
|
8
|
+
'youtube.com',
|
|
9
|
+
]);
|
|
10
|
+
const PATH_VIDEO_PREFIXES = new Set(['embed', 'live', 'shorts']);
|
|
11
|
+
export function parseYoutubeVideoUrl(input) {
|
|
12
|
+
const value = input.trim();
|
|
13
|
+
if (VIDEO_ID_PATTERN.test(value)) {
|
|
14
|
+
return reference(value);
|
|
15
|
+
}
|
|
16
|
+
if (value.length === 0 || value.length > 2_048) {
|
|
17
|
+
throw invalidUrl();
|
|
18
|
+
}
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw invalidUrl();
|
|
25
|
+
}
|
|
26
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
27
|
+
url.username !== '' ||
|
|
28
|
+
url.password !== '') {
|
|
29
|
+
throw invalidUrl();
|
|
30
|
+
}
|
|
31
|
+
const host = url.hostname.toLowerCase();
|
|
32
|
+
let videoId = null;
|
|
33
|
+
if (host === 'youtu.be') {
|
|
34
|
+
videoId = url.pathname.split('/').filter(Boolean)[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
else if (YOUTUBE_HOSTS.has(host)) {
|
|
37
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
38
|
+
if (url.pathname === '/watch') {
|
|
39
|
+
videoId = url.searchParams.get('v');
|
|
40
|
+
}
|
|
41
|
+
else if (segments[0] !== undefined && PATH_VIDEO_PREFIXES.has(segments[0])) {
|
|
42
|
+
videoId = segments[1] ?? null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (videoId === null || !VIDEO_ID_PATTERN.test(videoId)) {
|
|
46
|
+
throw invalidUrl();
|
|
47
|
+
}
|
|
48
|
+
return reference(videoId);
|
|
49
|
+
}
|
|
50
|
+
function reference(videoId) {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
canonicalUrl: `https://www.youtube.com/watch?v=${videoId}`,
|
|
53
|
+
videoId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function invalidUrl() {
|
|
57
|
+
return new YoutubeAudioError('INVALID_REQUEST', 'Provide a valid YouTube video URL or video ID.');
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=youtube-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube-url.js","sourceRoot":"","sources":["../src/youtube-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AAC/C,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,eAAe;IACf,mBAAmB;IACnB,0BAA0B;IAC1B,iBAAiB;IACjB,aAAa;CACd,CAAC,CAAC;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjE,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;QAC/C,MAAM,UAAU,EAAE,CAAC;IACrB,CAAC;IAED,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,UAAU,EAAE,CAAC;IACrB,CAAC;IACD,IACE,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC;QACvD,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,EAAE,EACnB,CAAC;QACD,MAAM,UAAU,EAAE,CAAC;IACrB,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC/D,CAAC;SAAM,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7E,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QAChC,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACxD,MAAM,UAAU,EAAE,CAAC;IACrB,CAAC;IACD,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,YAAY,EAAE,mCAAmC,OAAO,EAAE;QAC1D,OAAO;KACR,CAAC,CAAC;AACL,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAI,iBAAiB,CAC1B,iBAAiB,EACjB,gDAAgD,CACjD,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@echovisionlab/youtube-audio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Authenticated server-side YouTube audio resolution and byte-range proxying.",
|
|
5
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./youtube-js": {
|
|
13
|
+
"types": "./dist/youtube-js.d.ts",
|
|
14
|
+
"import": "./dist/youtube-js.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"DISCLOSURE.md",
|
|
20
|
+
"LICENSE.md",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/echovisionlab/youtube-audio.git"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "24.19.0"
|
|
29
|
+
},
|
|
30
|
+
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc --project tsconfig.build.json",
|
|
33
|
+
"build:clean": "node ./scripts/clean-dist.mjs",
|
|
34
|
+
"package:verify": "node ./scripts/verify-package.mjs",
|
|
35
|
+
"package:pack": "npm pack --dry-run",
|
|
36
|
+
"dist:verify-clean": "node ./scripts/verify-dist-clean.mjs",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test:workflow": "node ./scripts/verify-workflow-contract.mjs"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"youtubei.js": "18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "24.13.3",
|
|
47
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
48
|
+
"typescript": "7.0.2",
|
|
49
|
+
"vitest": "4.1.10"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/echovisionlab/youtube-audio#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/echovisionlab/youtube-audio/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"contentPolicy": {
|
|
59
|
+
"class": "dual-use"
|
|
60
|
+
}
|
|
61
|
+
}
|