expo-yt-dlp 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/.gitattributes +7 -0
- package/.prettierrc +8 -0
- package/LICENSE +21 -0
- package/README.md +521 -0
- package/android/build.gradle +18 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/bzip2.txt +42 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/cpython.txt +277 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/libffi.txt +21 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/openssl.txt +177 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/quickjs-ng.txt +24 -0
- package/android/src/main/assets/expo-yt-dlp/licenses/xz.txt +77 -0
- package/android/src/main/assets/expo-yt-dlp/manifest.json +57 -0
- package/android/src/main/assets/expo-yt-dlp/python-stdlib.zip +0 -0
- package/android/src/main/assets/expo-yt-dlp/site-packages.zip +0 -0
- package/android/src/main/java/expo/modules/ytdlp/ExpoYtDlpModule.kt +270 -0
- package/android/src/main/java/expo/modules/ytdlp/NativeMuxer.kt +86 -0
- package/android/src/main/java/expo/modules/ytdlp/Records.kt +36 -0
- package/android/src/main/java/expo/modules/ytdlp/YtDlpProcess.kt +90 -0
- package/android/src/main/java/expo/modules/ytdlp/YtDlpRuntime.kt +193 -0
- package/android/src/main/jniLibs/arm64-v8a/libcrypto_python.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__asyncio.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__bisect.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__blake2.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__bz2.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__csv.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__ctypes.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__decimal.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__elementtree.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__hashlib.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__heapq.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__hmac.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__json.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__lzma.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__md5.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__pickle.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__posixsubprocess.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__queue.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__random.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__sha1.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__sha2.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__sha3.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__socket.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__ssl.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__statistics.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__struct.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod__zoneinfo.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_array.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_binascii.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_cmath.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_fcntl.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_math.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_mmap.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_pyexpat.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_resource.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_select.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_syslog.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_termios.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_unicodedata.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpymod_zlib.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libpython3.14.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libqjs.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libssl_python.so +0 -0
- package/android/src/main/jniLibs/arm64-v8a/libytdlp_python.so +0 -0
- package/android/src/main/jniLibs/x86_64/libcrypto_python.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__asyncio.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__bisect.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__blake2.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__bz2.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__csv.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__ctypes.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__decimal.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__elementtree.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__hashlib.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__heapq.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__hmac.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__json.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__lzma.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__md5.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__pickle.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__posixsubprocess.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__queue.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__random.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__sha1.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__sha2.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__sha3.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__socket.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__ssl.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__statistics.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__struct.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod__zoneinfo.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_array.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_binascii.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_cmath.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_fcntl.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_math.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_mmap.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_pyexpat.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_resource.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_select.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_syslog.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_termios.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_unicodedata.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpymod_zlib.so +0 -0
- package/android/src/main/jniLibs/x86_64/libpython3.14.so +0 -0
- package/android/src/main/jniLibs/x86_64/libqjs.so +0 -0
- package/android/src/main/jniLibs/x86_64/libssl_python.so +0 -0
- package/android/src/main/jniLibs/x86_64/libytdlp_python.so +0 -0
- package/app.plugin.js +32 -0
- package/build/ExpoYtDlp.types.d.ts +273 -0
- package/build/ExpoYtDlp.types.d.ts.map +1 -0
- package/build/ExpoYtDlp.types.js +5 -0
- package/build/ExpoYtDlp.types.js.map +1 -0
- package/build/ExpoYtDlpModule.d.ts +38 -0
- package/build/ExpoYtDlpModule.d.ts.map +1 -0
- package/build/ExpoYtDlpModule.js +4 -0
- package/build/ExpoYtDlpModule.js.map +1 -0
- package/build/helpers.d.ts +10 -0
- package/build/helpers.d.ts.map +1 -0
- package/build/helpers.js +73 -0
- package/build/helpers.js.map +1 -0
- package/build/index.d.ts +52 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +184 -0
- package/build/index.js.map +1 -0
- package/build/options.d.ts +9 -0
- package/build/options.d.ts.map +1 -0
- package/build/options.js +46 -0
- package/build/options.js.map +1 -0
- package/eslint.config.cjs +5 -0
- package/expo-module.config.json +6 -0
- package/package.json +67 -0
- package/runtime-versions.json +37 -0
- package/src/ExpoYtDlp.types.ts +288 -0
- package/src/ExpoYtDlpModule.ts +39 -0
- package/src/helpers.ts +88 -0
- package/src/index.ts +252 -0
- package/src/options.ts +48 -0
- package/tsconfig.json +28 -0
package/.gitattributes
ADDED
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# expo-yt-dlp
|
|
2
|
+
|
|
3
|
+
[yt-dlp](https://github.com/yt-dlp/yt-dlp) inside your Expo / React Native app. Get video
|
|
4
|
+
info, check sizes, download video, audio, subtitles and thumbnails, whole playlists or batches
|
|
5
|
+
of links, all on the device, with no server.
|
|
6
|
+
|
|
7
|
+
The package bundles its own stripped-down Python 3.14 runtime and a copy of yt-dlp cut down to
|
|
8
|
+
the sites you need, and wraps them in a typed JS API. Anything the wrapper doesn't cover is one
|
|
9
|
+
`exec([...])` away, because it's the real yt-dlp underneath.
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| **Platforms** | Android (arm64-v8a, x86_64). iOS is planned. |
|
|
14
|
+
| **Sites in this build** | YouTube, Facebook, Instagram, plus yt-dlp's generic extractor (pages with plain `<video>` tags) |
|
|
15
|
+
| **Size** | About 11 MB added to a single-ABI APK; about 28 MB on the device once extracted |
|
|
16
|
+
| **Requires** | A development build (`npx expo run:android` or EAS Build). It does not work in Expo Go. |
|
|
17
|
+
|
|
18
|
+
- [Install](#install)
|
|
19
|
+
- [Quick start](#quick-start)
|
|
20
|
+
- [API](#api)
|
|
21
|
+
- [Recipes](#recipes)
|
|
22
|
+
- [How it works](#how-it-works)
|
|
23
|
+
- [Maintaining the package](#maintaining-the-package)
|
|
24
|
+
- [Limitations](#limitations)
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx expo install expo-yt-dlp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add the config plugin to `app.json`. It is **required**: the bundled binaries have to be
|
|
33
|
+
extracted to disk to run, and the plugin turns that on.
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"expo": {
|
|
38
|
+
"plugins": [["expo-yt-dlp", { "abiFilters": ["arm64-v8a", "x86_64"] }]]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`abiFilters` is optional. The runtime only exists for 64-bit ABIs, so limiting the build to
|
|
44
|
+
them also drops 32-bit copies of your other native libraries. Then rebuild the native app with
|
|
45
|
+
`npx expo prebuild` and `npx expo run:android`, or with EAS Build.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
51
|
+
|
|
52
|
+
async function quickStart() {
|
|
53
|
+
if (!YtDlp.isSupported()) return; // iOS/web, 32-bit phones, or the plugin is missing
|
|
54
|
+
|
|
55
|
+
const url = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
|
|
56
|
+
|
|
57
|
+
const info = await YtDlp.getInfo(url);
|
|
58
|
+
console.log(info.title, info.duration);
|
|
59
|
+
|
|
60
|
+
const task = YtDlp.download(url, {
|
|
61
|
+
maxHeight: 720,
|
|
62
|
+
onProgress: (p) => console.log(p.status, p.percent?.toFixed(0), '%'),
|
|
63
|
+
});
|
|
64
|
+
const { files } = await task.promise; // absolute paths, e.g. [".../files/downloads/Me at the zoo [jNQXAC9IVRw].mp4"]
|
|
65
|
+
console.log(files);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The first call takes a second or two longer while the runtime is unpacked, which happens once
|
|
70
|
+
per app version. To do that at startup instead, call `YtDlp.prepare()`.
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
Every function takes a URL from any supported site. All options are optional.
|
|
75
|
+
|
|
76
|
+
### Setup
|
|
77
|
+
|
|
78
|
+
| Function | Returns | |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `isSupported()` | `boolean` | Whether the runtime can run on this device and build. Synchronous. |
|
|
81
|
+
| `prepare()` | `Promise<RuntimeInfo>` | Unpacks the runtime ahead of time. Returns the bundled versions of Python, yt-dlp and QuickJS, whether ffmpeg is bundled, the ABI, and the extractor list. |
|
|
82
|
+
|
|
83
|
+
### Looking things up (nothing is downloaded)
|
|
84
|
+
|
|
85
|
+
| Function | Returns | |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `getInfo(url, options?)` | `Promise<VideoInfo>` | yt-dlp's full info dict: title, duration, uploader, formats, subtitles, thumbnails… |
|
|
88
|
+
| `getFormats(urlOrInfo, options?)` | `Promise<FormatSummary[]>` | Every format, best first: resolution, codecs, bitrate, size. |
|
|
89
|
+
| `estimateDownload(url, options?)` | `Promise<DownloadEstimate>` | Exactly what `download` would fetch with the same options, including files and sizes, without downloading. |
|
|
90
|
+
| `getSubtitles(urlOrInfo, options?)` | `Promise<SubtitleTrack[]>` | Uploaded subtitles, then auto-generated captions. |
|
|
91
|
+
| `getPlaylist(url, { limit? })` | `Promise<Playlist>` | Entries of a playlist or channel, without resolving each video. |
|
|
92
|
+
| `search(query, { limit? })` | `Promise<PlaylistEntry[]>` | YouTube search results (default 10). |
|
|
93
|
+
|
|
94
|
+
`getFormats` and `getSubtitles` also accept an info dict you already fetched, which saves a
|
|
95
|
+
network round trip:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
99
|
+
|
|
100
|
+
async function inspect(url: string) {
|
|
101
|
+
const info = await YtDlp.getInfo(url);
|
|
102
|
+
const formats = await YtDlp.getFormats(info);
|
|
103
|
+
const subs = await YtDlp.getSubtitles(info);
|
|
104
|
+
console.log(formats[0].resolution, formats[0].size, subs.map((s) => s.language));
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Options for lookups (`InfoOptions`):
|
|
109
|
+
|
|
110
|
+
| Option | Default | |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `playlist` | `false` | For `watch?v=…&list=…` URLs, resolve the playlist instead of just the video. |
|
|
113
|
+
| `flatPlaylist` | `false` | List playlist entries without resolving each one (fast). |
|
|
114
|
+
| `cookiesFile` | | Path to a Netscape-format cookies file, for content that needs a login. |
|
|
115
|
+
| `networkRetries` | `3` | Retries after dropped connections, timeouts and 5xx responses. |
|
|
116
|
+
| `extraArgs` | | Any extra yt-dlp CLI arguments. |
|
|
117
|
+
| `signal` | | An `AbortSignal` that cancels the lookup. |
|
|
118
|
+
|
|
119
|
+
### Downloading
|
|
120
|
+
|
|
121
|
+
| Function | Returns | |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `download(url, options?)` | `DownloadTask` | Downloads media (plus subtitles/thumbnails if asked). |
|
|
124
|
+
| `downloadSubtitles(url, options?)` | `DownloadTask` | Only subtitle files. |
|
|
125
|
+
| `downloadThumbnail(url, options?)` | `DownloadTask` | Only the thumbnail image. |
|
|
126
|
+
| `downloadBatch(urls, options?)` | `BatchTask` | Several URLs, `concurrency` at a time; per-item results. |
|
|
127
|
+
|
|
128
|
+
A `DownloadTask` has an `id`, a `promise` that resolves to a `DownloadResult`, and `cancel()`.
|
|
129
|
+
A `DownloadResult` has `files` (all media paths) and `items` (one per video: id, title, files
|
|
130
|
+
with sizes and codecs, subtitle files, thumbnail files).
|
|
131
|
+
|
|
132
|
+
By default `download` gets the best H.264 video and AAC audio and merges them into one MP4.
|
|
133
|
+
These are the codecs every phone plays. Options (`DownloadOptions`):
|
|
134
|
+
|
|
135
|
+
| Option | Default | |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `outputDir` | `<filesDir>/downloads` | Where files go (`file://` URIs are accepted). |
|
|
138
|
+
| `maxHeight` | | Caps the resolution (short side, so `720` also works for portrait videos). |
|
|
139
|
+
| `audioOnly` | `false` | Best audio only, AAC `.m4a` where available. |
|
|
140
|
+
| `format` | | A yt-dlp format selector, e.g. `'18'` or `'bv*[height<=480]'`. Overrides the defaults above. |
|
|
141
|
+
| `playlist` | `false` | Download the whole playlist when the URL is (or includes) one. |
|
|
142
|
+
| `playlistItems` | | Which items: `'1:5'`, `'1,3,7'`, `'-3:'` (last three). Implies `playlist`. |
|
|
143
|
+
| `subtitles` | `false` | `true` for English (falling back to auto-captions), or `{ languages, auto, format }`. |
|
|
144
|
+
| `thumbnail` | `false` | Also save the thumbnail image. |
|
|
145
|
+
| `media` | `true` | `false` writes only subtitles/thumbnails. |
|
|
146
|
+
| `rateLimit` | | Max speed, e.g. `'500K'`, `'2M'`. |
|
|
147
|
+
| `cookiesFile`, `networkRetries`, `extraArgs` | | As for lookups. |
|
|
148
|
+
| `onProgress` | | Called with `DownloadProgress` events. |
|
|
149
|
+
|
|
150
|
+
**Progress events** (`DownloadProgress`) have a `status`:
|
|
151
|
+
- `downloading`: includes `percent`, `downloadedBytes`, `totalBytes`, `speed` (bytes/s) and `eta` (s). Video and audio are separate streams, so each reports its own progress, identified by `formatId`.
|
|
152
|
+
- `finished`: one stream is done.
|
|
153
|
+
- `merging`: video and audio are being combined.
|
|
154
|
+
- `retrying`: includes `attempt`, `retries` and `retryDelay`.
|
|
155
|
+
|
|
156
|
+
**Errors.** Rejections carry a `code`:
|
|
157
|
+
|
|
158
|
+
| Code | Meaning |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `ERR_YTDLP` | yt-dlp failed. The message is yt-dlp's `ERROR:` line (unavailable video, login required, bot check…). |
|
|
161
|
+
| `ERR_YTDLP_CANCELLED` | You called `cancel()`, or an `AbortSignal` fired. Partial files are deleted. |
|
|
162
|
+
| `ERR_YTDLP_UNSUPPORTED` | This device or build can't run the runtime. The message says why (for example, a missing config plugin). |
|
|
163
|
+
|
|
164
|
+
### Running any yt-dlp command
|
|
165
|
+
|
|
166
|
+
`exec(args)` runs yt-dlp with raw CLI arguments and returns `{ exitCode, stdout, stderr }`.
|
|
167
|
+
The runtime adds its own flags first (JS runtime, cache dir, `--ignore-config`), so write
|
|
168
|
+
commands exactly as you would in a terminal.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
172
|
+
|
|
173
|
+
async function rawCommands(url: string) {
|
|
174
|
+
// yt-dlp's own format table
|
|
175
|
+
console.log((await YtDlp.exec(['-F', url])).stdout);
|
|
176
|
+
|
|
177
|
+
// Custom fields, one line per video
|
|
178
|
+
const r = await YtDlp.exec(['--print', '%(title)s | %(duration_string)s | %(view_count)s', url]);
|
|
179
|
+
console.log(r.stdout);
|
|
180
|
+
|
|
181
|
+
// Channel's latest uploads as JSON lines
|
|
182
|
+
await YtDlp.exec(['--flat-playlist', '-I', '1:20', '-j', 'https://www.youtube.com/@blender/videos']);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
See yt-dlp's [options](https://github.com/yt-dlp/yt-dlp#usage-and-options) and
|
|
187
|
+
[output template](https://github.com/yt-dlp/yt-dlp#output-template) docs. `extraArgs` on the
|
|
188
|
+
other functions accepts the same flags.
|
|
189
|
+
|
|
190
|
+
## Recipes
|
|
191
|
+
|
|
192
|
+
These examples use `import * as YtDlp from 'expo-yt-dlp'`.
|
|
193
|
+
|
|
194
|
+
### Check the size before downloading
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
198
|
+
|
|
199
|
+
async function downloadIfSmall(url: string, limitBytes = 50_000_000) {
|
|
200
|
+
const plan = await YtDlp.estimateDownload(url, { maxHeight: 720 });
|
|
201
|
+
// totalSize is null when the site doesn't report sizes (e.g. some Facebook videos)
|
|
202
|
+
if (plan.totalSize != null && plan.totalSize > limitBytes) {
|
|
203
|
+
throw new Error(`Too big: ${(plan.totalSize / 1e6).toFixed(0)} MB`);
|
|
204
|
+
}
|
|
205
|
+
return YtDlp.download(url, { maxHeight: 720 }).promise;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
To let users pick a quality, list the formats and pass the chosen id as `format`:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
213
|
+
|
|
214
|
+
async function pickQuality(url: string) {
|
|
215
|
+
const formats = await YtDlp.getFormats(url);
|
|
216
|
+
const choices = formats.filter((f) => f.hasVideo && f.vcodec?.startsWith('avc1'));
|
|
217
|
+
// e.g. [{ formatId: '137', resolution: '1920x1080', size: 48_000_000, ... }, ...]
|
|
218
|
+
const chosen = choices[0];
|
|
219
|
+
// Video-only formats have no sound: combine with the best audio using yt-dlp syntax.
|
|
220
|
+
return YtDlp.download(url, { format: `${chosen.formatId}+bestaudio[ext=m4a]` }).promise;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Without a bundled ffmpeg, a `+` combination is merged on the device when Android's
|
|
225
|
+
`MediaMuxer` supports the pair: H.264 + AAC into `.mp4`, or VP8/VP9 + Opus/Vorbis into
|
|
226
|
+
`.webm`. Other pairs, such as VP9 + AAC, come back as two files: the video and the audio.
|
|
227
|
+
|
|
228
|
+
### Audio only
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
232
|
+
|
|
233
|
+
async function audio(url: string) {
|
|
234
|
+
const { files } = await YtDlp.download(url, { audioOnly: true }).promise; // .m4a (AAC)
|
|
235
|
+
return files[0];
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Subtitles
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
243
|
+
|
|
244
|
+
async function subtitles(url: string) {
|
|
245
|
+
// What's available? Uploaded tracks first, then auto-generated ones.
|
|
246
|
+
const tracks = await YtDlp.getSubtitles(url);
|
|
247
|
+
console.log(tracks.filter((t) => !t.automatic).map((t) => t.language));
|
|
248
|
+
|
|
249
|
+
// Only the subtitle files:
|
|
250
|
+
const subs = await YtDlp.downloadSubtitles(url, { languages: ['en', 'es'], format: 'vtt' }).promise;
|
|
251
|
+
console.log(subs.items[0].subtitles); // [{ language: 'en', ext: 'vtt', path: '...' }]
|
|
252
|
+
|
|
253
|
+
// Or together with the video:
|
|
254
|
+
await YtDlp.download(url, { subtitles: { languages: ['en'] }, thumbnail: true }).promise;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Playlists, channels and search
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
262
|
+
|
|
263
|
+
async function playlists() {
|
|
264
|
+
const list = await YtDlp.getPlaylist('https://www.youtube.com/playlist?list=PL…', { limit: 50 });
|
|
265
|
+
console.log(list.title, list.entries.length);
|
|
266
|
+
|
|
267
|
+
// Download the first five items as one job…
|
|
268
|
+
await YtDlp.download('https://www.youtube.com/playlist?list=PL…', { playlistItems: '1:5' }).promise;
|
|
269
|
+
|
|
270
|
+
// …or search and pick
|
|
271
|
+
const results = await YtDlp.search('blender open movie', { limit: 5 });
|
|
272
|
+
await YtDlp.download(results[0].url).promise;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
YouTube **Mixes** (the auto-generated "radio" playlists) work through the watch URL that
|
|
277
|
+
YouTube gives you, `watch?v=ID&list=RD…`. They can run to dozens of songs, so always pass a
|
|
278
|
+
`limit` or `playlistItems`. Not every video has a Mix, and in that case you get the single
|
|
279
|
+
video back. A bare `playlist?list=RD…` URL doesn't work, because YouTube refuses to show
|
|
280
|
+
Mixes that way.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
284
|
+
|
|
285
|
+
async function mix(videoId: string) {
|
|
286
|
+
const url = `https://www.youtube.com/watch?v=${videoId}&list=RD${videoId}`;
|
|
287
|
+
const radio = await YtDlp.getPlaylist(url, { limit: 25 });
|
|
288
|
+
console.log(radio.title, radio.entries.map((e) => e.title));
|
|
289
|
+
|
|
290
|
+
// Plain download(url) fetches only the video; opt into the playlist to take songs from it
|
|
291
|
+
await YtDlp.download(url, { audioOnly: true, playlistItems: '1:10' }).promise;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Batches
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
299
|
+
|
|
300
|
+
async function batch(urls: string[]) {
|
|
301
|
+
const job = YtDlp.downloadBatch(urls, {
|
|
302
|
+
concurrency: 2, // each download is its own process; 2-3 is plenty on a phone
|
|
303
|
+
maxHeight: 720,
|
|
304
|
+
onProgress: (p, index) => console.log(`#${index}`, p.status, p.percent),
|
|
305
|
+
onItemDone: (r, index) => console.log(`#${index}`, r.ok ? 'done' : r.error.message),
|
|
306
|
+
});
|
|
307
|
+
// job.cancel() stops everything
|
|
308
|
+
const results = await job.promise; // never rejects; one entry per URL, in order
|
|
309
|
+
return results.filter((r) => r.ok);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Items can override the shared options:
|
|
314
|
+
`downloadBatch([url1, { url: url2, options: { audioOnly: true } }])`.
|
|
315
|
+
|
|
316
|
+
### Cancelling
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
320
|
+
|
|
321
|
+
async function cancelling(url: string) {
|
|
322
|
+
const task = YtDlp.download(url);
|
|
323
|
+
setTimeout(() => task.cancel(), 5000);
|
|
324
|
+
try {
|
|
325
|
+
await task.promise;
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
if (e.code === 'ERR_YTDLP_CANCELLED') console.log('cancelled, partial files removed');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const controller = new AbortController();
|
|
331
|
+
const info = YtDlp.getInfo(url, { signal: controller.signal });
|
|
332
|
+
controller.abort();
|
|
333
|
+
await info.catch(() => {});
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Save to the gallery
|
|
338
|
+
|
|
339
|
+
Files land in app-private storage. To publish them, move them with
|
|
340
|
+
[`expo-media-library`](https://docs.expo.dev/versions/latest/sdk/media-library/) or
|
|
341
|
+
`expo-file-system`:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import * as MediaLibrary from 'expo-media-library';
|
|
345
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
346
|
+
|
|
347
|
+
async function saveToGallery(url: string) {
|
|
348
|
+
const { files } = await YtDlp.download(url, { maxHeight: 1080 }).promise;
|
|
349
|
+
await MediaLibrary.requestPermissionsAsync();
|
|
350
|
+
await MediaLibrary.saveToLibraryAsync(`file://${files[0]}`);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Logged-in content (Instagram, Facebook)
|
|
355
|
+
|
|
356
|
+
Many Instagram and some Facebook posts require a session. Export cookies in Netscape format
|
|
357
|
+
(for example with a "cookies.txt" browser extension), ship or download the file to the device,
|
|
358
|
+
and pass `cookiesFile`:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import * as YtDlp from 'expo-yt-dlp';
|
|
362
|
+
|
|
363
|
+
async function withCookies(url: string, cookiesPath: string) {
|
|
364
|
+
return YtDlp.download(url, { cookiesFile: cookiesPath }).promise;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## How it works
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
JS (src/index.ts) ─ options → yt-dlp arguments
|
|
372
|
+
└─ Expo module (Kotlin) ── spawns ──> libytdlp_python.so -m expo_yt_dlp_runner … (child process)
|
|
373
|
+
├─ libpython3.14.so + stripped stdlib (python.org Android build)
|
|
374
|
+
├─ yt-dlp via its Python API (python/expo_yt_dlp_runner.py)
|
|
375
|
+
├─ spawns libqjs.so (QuickJS-ng: YouTube JS challenges)
|
|
376
|
+
└─ spawns libffmpeg.so (optional)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- **Python.** python.org's official Android build of CPython ships only as an embeddable
|
|
380
|
+
`libpython`. [`native/python-launcher`](native/python-launcher/main.c) is a 10-line `main()`
|
|
381
|
+
linked against it, which gives yt-dlp a normal `python` to run under.
|
|
382
|
+
- **Processes.** Every call is a separate child process, so cancelling means killing that
|
|
383
|
+
process.
|
|
384
|
+
- **Why the binaries are named `lib*.so`.** Android only allows executing files from
|
|
385
|
+
`nativeLibraryDir`, and only extracts `lib*.so` files there. The launcher, QuickJS, ffmpeg
|
|
386
|
+
and the stdlib C extensions (`libpymod_*.so`, symlinked into `lib-dynload`) are all
|
|
387
|
+
packaged that way.
|
|
388
|
+
- **Pure-Python code.** The stdlib and yt-dlp are precompiled to `.pyc`, zipped into the APK's
|
|
389
|
+
assets, and unpacked into app storage on first use.
|
|
390
|
+
- **The driver.** [`expo_yt_dlp_runner.py`](python/expo_yt_dlp_runner.py) wraps yt-dlp's API.
|
|
391
|
+
It handles format selection for merging, JSON progress events, per-video results with sizes
|
|
392
|
+
and side files, and retries for transient network errors.
|
|
393
|
+
- **Merging.** The best qualities are separate video and audio streams. Without ffmpeg, the
|
|
394
|
+
driver picks an H.264 + AAC pair and Kotlin merges it with Android's `MediaMuxer` (a stream
|
|
395
|
+
copy, not a re-encode). If a video has no such pair, which is common on Facebook, the best
|
|
396
|
+
single file is used instead.
|
|
397
|
+
|
|
398
|
+
## Maintaining the package
|
|
399
|
+
|
|
400
|
+
Everything the runtime is built from, including which sites are kept, is pinned in
|
|
401
|
+
[`runtime-versions.json`](runtime-versions.json). Built binaries are not committed; they're
|
|
402
|
+
produced by `npm run build:runtime` and published inside the npm tarball.
|
|
403
|
+
|
|
404
|
+
**Requirements:** Python 3.14 (the bundle is precompiled for it), Node 20+, and the Android SDK
|
|
405
|
+
with NDK r27+ and CMake. Set `ANDROID_HOME` / `ANDROID_NDK_HOME` if they aren't in the default
|
|
406
|
+
locations.
|
|
407
|
+
|
|
408
|
+
### Updating yt-dlp (and the rest)
|
|
409
|
+
|
|
410
|
+
```sh
|
|
411
|
+
npm run deps:check # pinned vs. latest upstream
|
|
412
|
+
npm run deps:update # newest stable yt-dlp + the yt-dlp-ejs it pins + certifi
|
|
413
|
+
npm run deps:update -- --channel nightly # yt-dlp nightly build
|
|
414
|
+
npm run deps:update -- --ytdlp 2026.8.19 # a specific version (also how to roll back)
|
|
415
|
+
npm run deps:update -- --all # also Python 3.14.x, QuickJS-ng, ffmpeg
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`deps:update` rewrites the pins with verified SHA-256 hashes and rebuilds the runtime,
|
|
419
|
+
re-trimming yt-dlp to the configured extractors. It then runs the offline tests. After it
|
|
420
|
+
succeeds:
|
|
421
|
+
1. Run `npm run test:online` to try the three sites for real.
|
|
422
|
+
2. Run `npm run build:runtime` for a full native build.
|
|
423
|
+
3. Try the example app on a device.
|
|
424
|
+
4. Bump the version and tag it (see Releasing).
|
|
425
|
+
|
|
426
|
+
The **Update bundled yt-dlp** workflow does the first part weekly and opens a PR when there's
|
|
427
|
+
a new version.
|
|
428
|
+
|
|
429
|
+
### Changing which sites are included
|
|
430
|
+
|
|
431
|
+
Edit `"extractors"` in `runtime-versions.json`. The names are yt-dlp's extractor module names
|
|
432
|
+
(`yt_dlp/extractor/<name>.py`), e.g. `"tiktok"`, `"vimeo"`, `"twitter"`, `"reddit"`. The generic
|
|
433
|
+
extractor is always kept. Then run `npm run build:runtime && npm run test:runtime`; the build
|
|
434
|
+
fails if a name doesn't exist. Each site adds a little to the bundle, and removing the list
|
|
435
|
+
entirely isn't supported (all ~1,800 extractors would add several MB).
|
|
436
|
+
|
|
437
|
+
### Building and testing
|
|
438
|
+
|
|
439
|
+
| Command | What it does |
|
|
440
|
+
| --- | --- |
|
|
441
|
+
| `npm run build:runtime` | Builds `android/src/main/jniLibs` and `assets/expo-yt-dlp`. `--python-only` skips the NDK builds. |
|
|
442
|
+
| `npm test` | Unit tests for the TypeScript layer (jest). |
|
|
443
|
+
| `npm run test:runtime` | Offline checks of the built bundle: the trimmed yt-dlp imports and runs on the stripped stdlib, and network retries behave (local test server). |
|
|
444
|
+
| `npm run test:online` | Resolves, plans and downloads from the real sites using the bundled runtime. Can fail for reasons outside this repo (bot checks, rate limits). |
|
|
445
|
+
| `python scripts/device_smoke_test.py -- -J <url>` | Pushes the runtime to a connected device or emulator over `adb` and runs yt-dlp there, without building an app. |
|
|
446
|
+
| `cd example && npx expo run:android` | The example app, which exercises every function. |
|
|
447
|
+
|
|
448
|
+
The optional ffmpeg (`scripts/build_ffmpeg_android.sh`, Linux/macOS) builds a mux-only
|
|
449
|
+
ffmpeg that the next `build:runtime` bundles. Without it, merging uses `MediaMuxer`.
|
|
450
|
+
|
|
451
|
+
### CI
|
|
452
|
+
|
|
453
|
+
| Workflow | When | What |
|
|
454
|
+
| --- | --- | --- |
|
|
455
|
+
| **CI** (`ci.yml`) | Every push to `main`, every PR | Lint, types, unit tests; full runtime build on Linux with offline tests; checks the npm tarball's contents; on PRs, also a Gradle build of the example app. |
|
|
456
|
+
| **Update bundled yt-dlp** (`update-deps.yml`) | Mondays, or manually (stable/nightly) | `deps:update` plus tests, then a PR with the new pins. Needs *Settings → Actions → General → Allow GitHub Actions to create and approve pull requests*. |
|
|
457
|
+
| **Release** (`release.yml`) | Pushing a `v*` tag | Builds the runtime, runs the tests, publishes to npm with trusted publishing (no token stored), creates a GitHub release listing the bundled versions. |
|
|
458
|
+
|
|
459
|
+
### Releasing
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
npm version patch # or minor/major
|
|
463
|
+
git push --follow-tags # the Release workflow builds and publishes
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers): npm
|
|
467
|
+
trusts `release.yml` in this repository, so no npm token is stored in GitHub. npm only lets
|
|
468
|
+
you set that up for a package that already exists, so the very first version is published
|
|
469
|
+
by hand, once:
|
|
470
|
+
|
|
471
|
+
1. Turn on two-factor authentication for your npm account.
|
|
472
|
+
2. Build and publish from a clean checkout:
|
|
473
|
+
```sh
|
|
474
|
+
npm login
|
|
475
|
+
npm ci
|
|
476
|
+
npm run build:runtime # both ABIs
|
|
477
|
+
npm run build
|
|
478
|
+
npm pack --dry-run # check the file list and size
|
|
479
|
+
npm publish --access public
|
|
480
|
+
```
|
|
481
|
+
3. On npmjs.com, open the package's **Settings → Trusted publishing**, choose GitHub Actions
|
|
482
|
+
and enter owner `nigelbasa`, repository `expo-yt-dlp`, workflow `release.yml`, and leave
|
|
483
|
+
the environment empty.
|
|
484
|
+
4. Optionally, under **Publishing access**, require 2FA and disallow tokens, so only the
|
|
485
|
+
workflow can publish.
|
|
486
|
+
|
|
487
|
+
After that, every tag is published by the workflow. Provenance statements need a public
|
|
488
|
+
source repository; add `--provenance` to the publish step in `release.yml` once the repo is
|
|
489
|
+
public.
|
|
490
|
+
|
|
491
|
+
## Limitations
|
|
492
|
+
|
|
493
|
+
- **Android only for now.** iOS needs an in-process interpreter, because iOS apps can't
|
|
494
|
+
spawn processes.
|
|
495
|
+
- **64-bit devices only.** python.org doesn't build 32-bit Android.
|
|
496
|
+
- **Downloads run while your app's process is alive.** Use a foreground service in your app
|
|
497
|
+
for long background downloads.
|
|
498
|
+
- **Without a bundled ffmpeg there's no transcoding.** That means no MP3 conversion and no SRT
|
|
499
|
+
from VTT, and merging needs an H.264 + AAC pair (the best-quality VP9/AV1 streams are
|
|
500
|
+
skipped).
|
|
501
|
+
- **Bot checks and rate limits.** YouTube may ask to "confirm you're not a bot", or rate-limit
|
|
502
|
+
subtitle downloads. Those errors aren't retried; cookies from a logged-in session help.
|
|
503
|
+
- **Sites change and yt-dlp follows quickly.** Keep the bundled version current with
|
|
504
|
+
`deps:update`.
|
|
505
|
+
- **App stores.** Google Play and the App Store reject apps that download from YouTube and
|
|
506
|
+
similar sites without the rights holder's authorization.
|
|
507
|
+
|
|
508
|
+
## Licenses
|
|
509
|
+
|
|
510
|
+
MIT for this package. Bundled components:
|
|
511
|
+
- yt-dlp: Unlicense
|
|
512
|
+
- CPython: PSF-2.0
|
|
513
|
+
- OpenSSL: Apache-2.0
|
|
514
|
+
- libffi: MIT; xz (liblzma): public domain; bzip2: BSD-style (all linked into CPython)
|
|
515
|
+
- QuickJS-ng: MIT
|
|
516
|
+
- certifi: MPL-2.0
|
|
517
|
+
- yt-dlp-ejs: Unlicense, MIT and ISC
|
|
518
|
+
- ffmpeg, if bundled: LGPL-2.1+
|
|
519
|
+
|
|
520
|
+
The license texts ship in the APK (`assets/expo-yt-dlp/licenses`, and inside
|
|
521
|
+
`site-packages.zip`). List them in your app's third-party notices.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'expo.modules.ytdlp'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "expo.modules.ytdlp"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName "0.1.0"
|
|
14
|
+
}
|
|
15
|
+
lintOptions {
|
|
16
|
+
abortOnError false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
--------------------------------------------------------------------------
|
|
3
|
+
|
|
4
|
+
This program, "bzip2", the associated library "libbzip2", and all
|
|
5
|
+
documentation, are copyright (C) 1996-2019 Julian R Seward. All
|
|
6
|
+
rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source and binary forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions
|
|
10
|
+
are met:
|
|
11
|
+
|
|
12
|
+
1. Redistributions of source code must retain the above copyright
|
|
13
|
+
notice, this list of conditions and the following disclaimer.
|
|
14
|
+
|
|
15
|
+
2. The origin of this software must not be misrepresented; you must
|
|
16
|
+
not claim that you wrote the original software. If you use this
|
|
17
|
+
software in a product, an acknowledgment in the product
|
|
18
|
+
documentation would be appreciated but is not required.
|
|
19
|
+
|
|
20
|
+
3. Altered source versions must be plainly marked as such, and must
|
|
21
|
+
not be misrepresented as being the original software.
|
|
22
|
+
|
|
23
|
+
4. The name of the author may not be used to endorse or promote
|
|
24
|
+
products derived from this software without specific prior written
|
|
25
|
+
permission.
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
|
28
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
29
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
30
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
31
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
32
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
33
|
+
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
34
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
35
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
36
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
37
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
38
|
+
|
|
39
|
+
Julian Seward, jseward@acm.org
|
|
40
|
+
bzip2/libbzip2 version 1.0.8 of 13 July 2019
|
|
41
|
+
|
|
42
|
+
--------------------------------------------------------------------------
|