ytdlp-react-native 1.0.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/android/build.gradle +25 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/ytdlp/ExpoYtDlpModule.kt +74 -0
  7. package/android/src/main/java/expo/modules/ytdlp/YtDlpDownloadManager.kt +97 -0
  8. package/android/src/main/java/expo/modules/ytdlp/YtDlpEngine.kt +277 -0
  9. package/android/src/main/java/expo/modules/ytdlp/YtDlpException.kt +14 -0
  10. package/android/src/main/java/expo/modules/ytdlp/YtDlpFileUtil.kt +89 -0
  11. package/android/src/main/java/expo/modules/ytdlp/YtDlpTask.kt +225 -0
  12. package/build/ExpoYtDlpModule.d.ts +35 -0
  13. package/build/ExpoYtDlpModule.d.ts.map +1 -0
  14. package/build/ExpoYtDlpModule.js +10 -0
  15. package/build/ExpoYtDlpModule.js.map +1 -0
  16. package/build/ExpoYtDlpModule.web.d.ts +11 -0
  17. package/build/ExpoYtDlpModule.web.d.ts.map +1 -0
  18. package/build/ExpoYtDlpModule.web.js +13 -0
  19. package/build/ExpoYtDlpModule.web.js.map +1 -0
  20. package/build/YtDlp.d.ts +23 -0
  21. package/build/YtDlp.d.ts.map +1 -0
  22. package/build/YtDlp.js +160 -0
  23. package/build/YtDlp.js.map +1 -0
  24. package/build/constants.d.ts +9 -0
  25. package/build/constants.d.ts.map +1 -0
  26. package/build/constants.js +9 -0
  27. package/build/constants.js.map +1 -0
  28. package/build/downloadTask.d.ts +30 -0
  29. package/build/downloadTask.d.ts.map +1 -0
  30. package/build/downloadTask.js +111 -0
  31. package/build/downloadTask.js.map +1 -0
  32. package/build/errors.d.ts +27 -0
  33. package/build/errors.d.ts.map +1 -0
  34. package/build/errors.js +111 -0
  35. package/build/errors.js.map +1 -0
  36. package/build/events.d.ts +20 -0
  37. package/build/events.d.ts.map +1 -0
  38. package/build/events.js +96 -0
  39. package/build/events.js.map +1 -0
  40. package/build/index.d.ts +14 -0
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +13 -0
  43. package/build/index.js.map +1 -0
  44. package/build/mappers.d.ts +15 -0
  45. package/build/mappers.d.ts.map +1 -0
  46. package/build/mappers.js +149 -0
  47. package/build/mappers.js.map +1 -0
  48. package/build/types.d.ts +183 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +2 -0
  51. package/build/types.js.map +1 -0
  52. package/expo-module.config.json +6 -0
  53. package/package.json +68 -0
  54. package/src/ExpoYtDlpModule.ts +41 -0
  55. package/src/ExpoYtDlpModule.web.ts +15 -0
  56. package/src/YtDlp.ts +151 -0
  57. package/src/constants.ts +11 -0
  58. package/src/downloadTask.ts +159 -0
  59. package/src/errors.ts +133 -0
  60. package/src/events.ts +116 -0
  61. package/src/index.ts +17 -0
  62. package/src/mappers.ts +168 -0
  63. package/src/types.ts +219 -0
package/src/mappers.ts ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Mapping from the raw JSON produced by yt-dlp into the stable public types.
3
+ *
4
+ * Everything is defensive: unknown fields become `undefined`, never fake
5
+ * values (see AGENTS.md §11, §69, §70).
6
+ */
7
+ import type { DownloadProgress, Format, Thumbnail, VideoInfo } from './types';
8
+
9
+ type RawRecord = Record<string, unknown>;
10
+
11
+ const NONE_CODEC = 'none';
12
+
13
+ export function mapVideoInfo(raw: unknown): VideoInfo {
14
+ const info = asRecord(raw);
15
+
16
+ const formats = Array.isArray(info.formats) ? info.formats.map(mapFormat) : [];
17
+
18
+ const liveStatus = stringOf(info.live_status);
19
+ const thumbnails = normalizeThumbnails(info.thumbnails, stringOf(info.thumbnail));
20
+
21
+ return {
22
+ id: stringOf(info.id),
23
+ title: stringOf(info.title),
24
+ description: stringOf(info.description),
25
+ uploader: stringOf(info.uploader),
26
+ uploaderId: stringOf(info.uploader_id),
27
+ uploaderUrl: stringOf(info.uploader_url),
28
+ channel: stringOf(info.channel),
29
+ channelId: stringOf(info.channel_id),
30
+ channelUrl: stringOf(info.channel_url),
31
+ webpageUrl: stringOf(info.webpage_url),
32
+ originalUrl: stringOf(info.original_url),
33
+ thumbnail: firstThumbnailUrl(thumbnails) ?? stringOf(info.thumbnail),
34
+ thumbnails,
35
+ duration: numberOf(info.duration),
36
+ durationString: stringOf(info.duration_string),
37
+ uploadDate: stringOf(info.upload_date),
38
+ timestamp: numberOf(info.timestamp),
39
+ viewCount: numberOf(info.view_count),
40
+ likeCount: numberOf(info.like_count),
41
+ commentCount: numberOf(info.comment_count),
42
+ ageLimit: numberOf(info.age_limit),
43
+ isLive: liveStatus === 'is_live',
44
+ wasLive: liveStatus === 'was_live',
45
+ extractor: stringOf(info.extractor),
46
+ extractorKey: stringOf(info.extractor_key),
47
+ webpageUrlDomain: stringOf(info.webpage_url_domain),
48
+ formats,
49
+ };
50
+ }
51
+
52
+ export function mapFormat(raw: unknown): Format {
53
+ const format = asRecord(raw);
54
+ const vcodec = stringOf(format.vcodec);
55
+ const acodec = stringOf(format.acodec);
56
+ const hasVideo = vcodec !== undefined && vcodec.toLowerCase() !== NONE_CODEC;
57
+ const hasAudio = acodec !== undefined && acodec.toLowerCase() !== NONE_CODEC;
58
+
59
+ return {
60
+ id: stringOf(format.format_id) ?? stringOf(format.id) ?? '',
61
+ url: stringOf(format.url),
62
+ ext: stringOf(format.ext),
63
+ protocol: stringOf(format.protocol),
64
+ format: stringOf(format.format),
65
+ formatNote: stringOf(format.format_note),
66
+ width: numberOf(format.width),
67
+ height: numberOf(format.height),
68
+ fps: numberOf(format.fps),
69
+ vcodec,
70
+ acodec,
71
+ abr: numberOf(format.abr),
72
+ vbr: numberOf(format.vbr),
73
+ tbr: numberOf(format.tbr),
74
+ filesize: numberOf(format.filesize),
75
+ filesizeApprox: numberOf(format.filesize_approx),
76
+ quality: numberOf(format.quality),
77
+ audioOnly: hasAudio && !hasVideo,
78
+ videoOnly: hasVideo && !hasAudio,
79
+ hasVideo,
80
+ hasAudio,
81
+ language: stringOf(format.language),
82
+ container: stringOf(format.container),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Maps the raw progress payload from the native `downloadEvent` into a
88
+ * `DownloadProgress`, replacing any non-finite numbers with `undefined`.
89
+ */
90
+ export function mapDownloadProgress(raw: unknown, taskId: string): DownloadProgress | null {
91
+ const progress = asRecord(raw);
92
+ if (!progress) return null;
93
+
94
+ const percent = numberOf(progress.percent);
95
+ const phaseRaw = stringOf(progress.phase);
96
+
97
+ return {
98
+ taskId,
99
+ status: statusOf(progress.status),
100
+ phase: phaseRaw === 'extracting' || phaseRaw === 'processing' ? phaseRaw : 'downloading',
101
+ percent,
102
+ downloadedBytes: numberOf(progress.downloadedBytes),
103
+ totalBytes: numberOf(progress.totalBytes),
104
+ speedBytesPerSecond: numberOf(progress.speedBytesPerSecond),
105
+ etaSeconds: numberOf(progress.etaSeconds),
106
+ filename: stringOf(progress.filename),
107
+ };
108
+ }
109
+
110
+ function statusOf(value: unknown): DownloadProgress['status'] {
111
+ const s = stringOf(value);
112
+ switch (s) {
113
+ case 'queued':
114
+ case 'extracting':
115
+ case 'downloading':
116
+ case 'processing':
117
+ case 'completed':
118
+ case 'cancelled':
119
+ case 'failed':
120
+ return s;
121
+ default:
122
+ return 'downloading';
123
+ }
124
+ }
125
+
126
+ function normalizeThumbnails(value: unknown, fallbackUrl?: string): Thumbnail[] {
127
+ const result: Thumbnail[] = [];
128
+ if (Array.isArray(value)) {
129
+ for (const item of value) {
130
+ const record = asRecord(item);
131
+ const url = stringOf(record.url);
132
+ if (!url) continue;
133
+ result.push({
134
+ url,
135
+ width: numberOf(record.width),
136
+ height: numberOf(record.height),
137
+ resolution: stringOf(record.resolution),
138
+ id: stringOf(record.id),
139
+ });
140
+ }
141
+ }
142
+ if (result.length === 0 && fallbackUrl) {
143
+ result.push({ url: fallbackUrl });
144
+ }
145
+ return result;
146
+ }
147
+
148
+ function firstThumbnailUrl(thumbnails: Thumbnail[]): string | undefined {
149
+ return thumbnails[0]?.url;
150
+ }
151
+
152
+ function stringOf(value: unknown): string | undefined {
153
+ if (typeof value === 'string' && value.length > 0) return value;
154
+ return undefined;
155
+ }
156
+
157
+ function numberOf(value: unknown): number | undefined {
158
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
159
+ if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
160
+ return Number(value);
161
+ }
162
+ return undefined;
163
+ }
164
+
165
+ function asRecord(value: unknown): RawRecord {
166
+ if (typeof value === 'object' && value !== null) return value as RawRecord;
167
+ return {};
168
+ }
package/src/types.ts ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Public types for expo-ytdlp-native.
3
+ *
4
+ * These types are the stable contract of the package. Native details never
5
+ * leak into this surface (see AGENTS.md §68).
6
+ */
7
+ import type { YtDlpError } from './errors';
8
+
9
+ export type DownloadStatus =
10
+ 'queued' | 'extracting' | 'downloading' | 'processing' | 'completed' | 'cancelled' | 'failed';
11
+
12
+ export type DownloadPhase = 'extracting' | 'downloading' | 'processing';
13
+
14
+ export interface Thumbnail {
15
+ url: string;
16
+ width?: number;
17
+ height?: number;
18
+ resolution?: string;
19
+ id?: string;
20
+ }
21
+
22
+ /** Normalized media information. Every field is optional except `formats`. */
23
+ export interface VideoInfo {
24
+ id?: string;
25
+ title?: string;
26
+ description?: string;
27
+ uploader?: string;
28
+ uploaderId?: string;
29
+ uploaderUrl?: string;
30
+ channel?: string;
31
+ channelId?: string;
32
+ channelUrl?: string;
33
+ webpageUrl?: string;
34
+ originalUrl?: string;
35
+ thumbnail?: string;
36
+ thumbnails?: Thumbnail[];
37
+ duration?: number;
38
+ durationString?: string;
39
+ uploadDate?: string;
40
+ timestamp?: number;
41
+ viewCount?: number;
42
+ likeCount?: number;
43
+ commentCount?: number;
44
+ ageLimit?: number;
45
+ isLive?: boolean;
46
+ wasLive?: boolean;
47
+ extractor?: string;
48
+ extractorKey?: string;
49
+ webpageUrlDomain?: string;
50
+ formats: Format[];
51
+ }
52
+
53
+ /** Normalized format model. Unknown native values become `undefined`. */
54
+ export interface Format {
55
+ id: string;
56
+ url?: string;
57
+ ext?: string;
58
+ protocol?: string;
59
+ format?: string;
60
+ formatNote?: string;
61
+ width?: number;
62
+ height?: number;
63
+ fps?: number;
64
+ vcodec?: string;
65
+ acodec?: string;
66
+ abr?: number;
67
+ vbr?: number;
68
+ tbr?: number;
69
+ filesize?: number;
70
+ filesizeApprox?: number;
71
+ quality?: number;
72
+ audioOnly: boolean;
73
+ videoOnly: boolean;
74
+ hasVideo: boolean;
75
+ hasAudio: boolean;
76
+ language?: string;
77
+ container?: string;
78
+ }
79
+
80
+ export interface OutputOptions {
81
+ /**
82
+ * Directory name, relative to the app-specific external storage directory.
83
+ * Single path segments only; `..` is rejected.
84
+ */
85
+ directory?: string;
86
+ /**
87
+ * yt-dlp output template, e.g. `%(title)s.%(ext)s`.
88
+ * Static parts are sanitized; template directives are preserved.
89
+ */
90
+ filename?: string;
91
+ }
92
+
93
+ export interface SubtitleOptions {
94
+ enabled?: boolean;
95
+ languages?: string[];
96
+ autoGenerated?: boolean;
97
+ }
98
+
99
+ export interface CookieOptions {
100
+ source: 'file';
101
+ path: string;
102
+ }
103
+
104
+ export interface NetworkOptions {
105
+ timeout?: number;
106
+ retries?: number;
107
+ }
108
+
109
+ export interface PlaylistOptions {
110
+ enabled?: boolean;
111
+ start?: number;
112
+ end?: number;
113
+ }
114
+
115
+ export interface ExtractOptions {
116
+ /** Cookies used during extraction (e.g. for login-gated sources). */
117
+ cookies?: CookieOptions;
118
+ /** Additional HTTP headers sent during extraction. Never logged. */
119
+ headers?: Record<string, string>;
120
+ /** Custom `User-Agent`. */
121
+ userAgent?: string;
122
+ proxy?: string;
123
+ }
124
+
125
+ export interface DownloadOptions {
126
+ url: string;
127
+ /** Raw yt-dlp format expression, e.g. `best`, `bestvideo+bestaudio`. */
128
+ format?: string;
129
+ output?: OutputOptions;
130
+ merge?: boolean;
131
+ subtitles?: SubtitleOptions;
132
+ cookies?: CookieOptions;
133
+ /** Additional HTTP headers. Validated; secrets are never logged. */
134
+ headers?: Record<string, string>;
135
+ userAgent?: string;
136
+ referer?: string;
137
+ proxy?: string;
138
+ playlist?: PlaylistOptions;
139
+ network?: NetworkOptions;
140
+ }
141
+
142
+ export interface DownloadProgress {
143
+ taskId: string;
144
+ status: DownloadStatus;
145
+ phase: DownloadPhase;
146
+ percent?: number;
147
+ downloadedBytes?: number;
148
+ totalBytes?: number;
149
+ speedBytesPerSecond?: number;
150
+ etaSeconds?: number;
151
+ filename?: string;
152
+ }
153
+
154
+ export interface DownloadResult {
155
+ taskId: string;
156
+ path?: string;
157
+ uri?: string;
158
+ filename?: string;
159
+ mimeType?: string;
160
+ size?: number;
161
+ duration?: number;
162
+ }
163
+
164
+ export type DownloadEventType = 'progress' | 'state' | 'completed' | 'error' | 'cancelled';
165
+
166
+ export interface DownloadEvent {
167
+ taskId: string;
168
+ type: DownloadEventType;
169
+ status: DownloadStatus;
170
+ phase: DownloadPhase;
171
+ progress?: DownloadProgress;
172
+ result?: DownloadResult;
173
+ error?: {
174
+ code: string;
175
+ message: string;
176
+ };
177
+ }
178
+
179
+ export type DownloadStateEvent = {
180
+ taskId: string;
181
+ status: DownloadStatus;
182
+ };
183
+
184
+ export interface Subscription {
185
+ remove(): void;
186
+ }
187
+
188
+ export interface DownloadTask {
189
+ id: string;
190
+ cancel(): Promise<void>;
191
+ getStatus(): Promise<DownloadStatus>;
192
+ getProgress(): Promise<DownloadProgress | null>;
193
+ addListener(event: 'progress', listener: (progress: DownloadProgress) => void): Subscription;
194
+ addListener(event: 'state', listener: (state: DownloadStateEvent) => void): Subscription;
195
+ addListener(event: 'completed', listener: (result: DownloadResult) => void): Subscription;
196
+ addListener(event: 'error', listener: (error: YtDlpError) => void): Subscription;
197
+ }
198
+
199
+ export interface YtDlpVersion {
200
+ ytDlp: string;
201
+ library: string;
202
+ }
203
+
204
+ export type YtDlpErrorCode =
205
+ | 'INVALID_URL'
206
+ | 'EXTRACTION_FAILED'
207
+ | 'DOWNLOAD_FAILED'
208
+ | 'CANCELLED'
209
+ | 'FORMAT_UNAVAILABLE'
210
+ | 'NETWORK_ERROR'
211
+ | 'AUTHENTICATION_REQUIRED'
212
+ | 'GEO_RESTRICTED'
213
+ | 'PRIVATE_CONTENT'
214
+ | 'AGE_RESTRICTED'
215
+ | 'PROCESSING_FAILED'
216
+ | 'STORAGE_ERROR'
217
+ | 'INIT_FAILED'
218
+ | 'UNSUPPORTED_PLATFORM'
219
+ | 'UNKNOWN';