umedia 0.4.2 → 0.5.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/bin/umedia.js CHANGED
@@ -1,99 +1,13 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * umedia CLI — the standalone media engine, in your terminal.
4
- *
5
- * umedia search "afrobeats" --type music --limit 5
6
- * umedia resolve "https://www.tiktok.com/@user/photo/123"
7
- * umedia download "https://youtu.be/jNQXAC9IVRw" --quality best -o ./out
8
- * umedia status
9
- * umedia capabilities
10
- */
11
- import { UMedia, UMediaError } from "../index.js";
12
-
13
- function parseArgs(argv) {
14
- const out = { _: [] };
15
- for (let i = 0; i < argv.length; i++) {
16
- const a = argv[i];
17
- if (a.startsWith("--") || a.startsWith("-")) {
18
- const key = a.replace(/^-+/, "");
19
- const next = argv[i + 1];
20
- if (next !== undefined && !next.startsWith("-")) { out[key] = next; i++; }
21
- else out[key] = true;
22
- } else out._.push(a);
23
- }
24
- return out;
25
- }
26
-
27
- const HELP = `umedia — standalone media engine (runs on YOUR machine, no API needed)
28
-
29
- umedia search "<query>" [--type video|music|movie|short_video] [--limit N]
30
- umedia resolve <url>
31
- umedia download <url> [--quality best|720p|audio] [-o <dir>] [--zip] [--ytdlp]
32
- umedia status
33
- umedia capabilities`;
34
-
35
- const args = parseArgs(process.argv.slice(2));
36
- const cmd = args._[0];
37
- if (!cmd || cmd === "help" || args.help) {
38
- console.log(HELP);
39
- process.exit(0);
40
- }
41
-
42
- const client = new UMedia({ downloadDir: args.o || args.out || "./umedia-downloads" });
43
- const print = (x) => console.log(JSON.stringify(x, null, 2));
44
-
45
- async function main() {
46
- switch (cmd) {
47
- case "search": {
48
- const q = args._.slice(1).join(" ");
49
- if (!q) throw new UMediaError("INVALID_REQUEST", 'usage: umedia search "<query>"');
50
- const { data, meta, engine } = await client.search({
51
- q, type: args.type || "video", limit: args.limit ? Number(args.limit) : 12, adult: Boolean(args.adult),
52
- });
53
- print({ items: data.items, meta, engine });
54
- break;
55
- }
56
- case "resolve": {
57
- const url = args._[1];
58
- if (!url) throw new UMediaError("INVALID_REQUEST", "usage: umedia resolve <url>");
59
- const r = await client.resolve(url);
60
- print(r);
61
- break;
62
- }
63
- case "download": {
64
- const url = args._[1];
65
- if (!url) throw new UMediaError("INVALID_REQUEST", "usage: umedia download <url>");
66
- const onProgress = args.json ? undefined : (done, total) => {
67
- if (total) process.stderr.write(`\r ${((done / total) * 100).toFixed(1)}%`);
68
- };
69
- const fn = args.ytdlp ? client.downloadWithYtdlp.bind(client) : client.download.bind(client);
70
- const r = await fn({ url, quality: args.quality || "best", type: args.type || "video", zip: Boolean(args.zip), onProgress });
71
- if (!args.json) process.stderr.write("\n");
72
- print(r.data);
73
- break;
74
- }
75
- case "status": {
76
- print((await client.status()).data);
77
- break;
78
- }
79
- case "capabilities": {
80
- print((await client.capabilities()).data);
81
- break;
82
- }
83
- default:
84
- console.log(HELP);
85
- process.exit(1);
86
- }
87
- }
88
-
89
- main().catch((e) => {
90
- if (e instanceof UMediaError) {
91
- console.error(JSON.stringify({
92
- success: false,
93
- error: { code: e.code, message: e.message, requestId: e.requestId, attempts: e.attempts },
94
- }, null, 2));
95
- } else {
96
- console.error(String(e && e.message ? e.message : e));
97
- }
98
- process.exit(1);
99
- });
2
+ import { realpathSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ console.error("[umedia] this package was renamed to pappy-media-api — re-exporting it. Please run: npm i pappy-media-api");
9
+
10
+ // run the real CLI from the pappy-media-api package
11
+ const bin = require.resolve("pappy-media-api/bin/umedia.js");
12
+ const r = spawnSync(process.execPath, [bin, ...process.argv.slice(2)], { stdio: "inherit" });
13
+ process.exit(r.status ?? 0);
package/index.d.ts CHANGED
@@ -1,180 +1,2 @@
1
- /** umedia — standalone media engine. Runs entirely on your machine: no hosted API, no keys, no bills. */
2
-
3
- export type ErrorCode =
4
- | "INVALID_REQUEST" | "INVALID_URL" | "SSRF_BLOCKED" | "AUTH_REQUIRED" | "AUTH_INVALID"
5
- | "RATE_LIMITED" | "MEDIA_NOT_FOUND" | "CONTENT_PRIVATE" | "ACCESS_RESTRICTED"
6
- | "QUALITY_UNAVAILABLE" | "PROVIDER_ERROR" | "PROVIDER_UNAVAILABLE" | "TIMEOUT"
7
- | "NOT_FOUND" | "INTERNAL";
8
-
9
- export const Codes: Readonly<Record<ErrorCode, ErrorCode>>;
10
- export const qualityLadder: readonly string[];
11
-
12
- export class UMediaError extends Error {
13
- name: "UMediaError";
14
- code: ErrorCode;
15
- status: number;
16
- requestId: string | null;
17
- attempts: ProviderAttempt[];
18
- details: unknown;
19
- constructor(code: ErrorCode, message: string, opts?: {
20
- status?: number; requestId?: string | null; attempts?: ProviderAttempt[]; details?: unknown;
21
- });
22
- }
23
-
24
- /** Honest provider record — which adapter actually did the work (or why it couldn't). */
25
- export interface ProviderAttempt {
26
- provider: string;
27
- status: "success" | "failed" | "skipped";
28
- latencyMs?: number | null;
29
- error?: string | null;
30
- }
31
-
32
- export interface MediaVariant {
33
- quality: string; // derived from ACTUAL source height — never upscaled
34
- width?: number | null;
35
- height?: number | null;
36
- url?: string | null; // null when the network is bot-gated — never faked
37
- mimeType?: string | null;
38
- bitrateKbps?: number | null;
39
- hasAudio?: boolean | null;
40
- hasVideo?: boolean | null;
41
- }
42
-
43
- export interface MediaObject {
44
- type: "image" | "video" | "audio" | "short_video" | "gif";
45
- index: number;
46
- url: string | null;
47
- thumbnail?: string | null;
48
- mimeType?: string | null;
49
- width?: number | null;
50
- height?: number | null;
51
- duration?: number | null;
52
- size?: number | null;
53
- quality?: string | null;
54
- hasAudio?: boolean | null;
55
- hasVideo?: boolean | null;
56
- source?: string;
57
- variants: MediaVariant[];
58
- }
59
-
60
- export interface MediaCounts { images: number; videos: number; audios: number; other: number; }
61
-
62
- export interface ResolveResult {
63
- sourceUrl: string;
64
- platform?: string | null;
65
- title?: string | null;
66
- author?: string | null;
67
- thumbnail?: string | null;
68
- itemCount: number;
69
- counts: MediaCounts;
70
- /** true only when an item cap was hit — never silent truncation */
71
- truncated: boolean;
72
- media: MediaObject[];
73
- /** YouTube-specific honesty about stream URL availability on this network */
74
- streamUrlsAvailable?: boolean;
75
- streamAccess?: string;
76
- [k: string]: unknown;
77
- }
78
-
79
- export interface ResolveMeta {
80
- attempts: ProviderAttempt[];
81
- finalProvider?: string | null;
82
- totalLatencyMs?: number | null;
83
- }
84
-
85
- export interface SearchItem {
86
- id?: string;
87
- title?: string;
88
- author?: string;
89
- pageUrl?: string;
90
- thumbnail?: string | null;
91
- duration?: number | null;
92
- mediaType?: string;
93
- source?: string;
94
- explicit?: boolean;
95
- /** real 30s clip / trailer / official embed — `none` when nothing honest exists */
96
- previewUrl?: string | null;
97
- previewKind?: "clip30s" | "trailer" | "embed" | "none";
98
- [k: string]: unknown;
99
- }
100
-
101
- export interface DownloadedFile {
102
- name: string;
103
- path: string;
104
- size: number;
105
- ext: string;
106
- mimeType?: string | null;
107
- index: number;
108
- quality?: string | null;
109
- }
110
-
111
- export interface DownloadResult {
112
- url: string;
113
- title?: string | null;
114
- source?: string | null;
115
- requestedQuality: string;
116
- selectedQuality: string | null;
117
- fallback: boolean;
118
- files: DownloadedFile[];
119
- itemCount: number;
120
- requestedItems: number;
121
- truncated: boolean;
122
- failedItems: Array<{ index?: number; type?: string; url?: string; error?: string }>;
123
- note?: string;
124
- }
125
-
126
- export interface Meta {
127
- requestId: string;
128
- source?: string | null;
129
- timestamp: string;
130
- elapsedMs?: number;
131
- }
132
-
133
- export interface Envelope<T> { data: T; meta: Meta; }
134
-
135
- export interface UMediaOptions {
136
- /** default directory for downloads (default "./umedia-downloads") */
137
- downloadDir?: string;
138
- timeoutMs?: number;
139
- }
140
-
141
- export class UMedia {
142
- constructor(opts?: UMediaOptions);
143
- downloadDir: string;
144
-
145
- search(p?: {
146
- q?: string; query?: string;
147
- type?: "video" | "music" | "movie" | "short_video";
148
- limit?: number; adult?: boolean;
149
- }): Promise<Envelope<{ items: SearchItem[]; adultFilter?: unknown }> & { engine: { attempts: ProviderAttempt[] } }>;
150
-
151
- resolve(url: string): Promise<{ data: ResolveResult; engine: ResolveMeta }>;
152
-
153
- download(p: {
154
- url: string;
155
- /** original | best | 2160p … 144p | audio */
156
- quality?: string;
157
- type?: "video" | "music" | "movie" | "short_video";
158
- dir?: string;
159
- zip?: boolean;
160
- onProgress?: (done: number, total: number) => void;
161
- }): Promise<Envelope<DownloadResult> & { engine?: ResolveMeta }>;
162
-
163
- downloadWithYtdlp(p: {
164
- url: string; quality?: string; dir?: string;
165
- onProgress?: (done: number, total: number) => void;
166
- }): Promise<Envelope<DownloadResult>>;
167
-
168
- capabilities(): Promise<Envelope<{
169
- platforms: Array<{ platform: string; search: boolean; resolve: boolean; download: boolean | string; notes?: string }>;
170
- qualityLadder: string[];
171
- }>>;
172
-
173
- status(): Promise<Envelope<{
174
- overall: string;
175
- runtime: { node: string; platform: string };
176
- providers: Array<{ name: string; state: string; via: string }>;
177
- }>>;
178
- }
179
-
180
- export default UMedia;
1
+ export * from "pappy-media-api";
2
+ export { default } from "pappy-media-api";
package/index.js CHANGED
@@ -1,76 +1,8 @@
1
1
  /**
2
- * umedia — standalone media engine. Runs entirely on YOUR machine:
3
- * no hosted API, no keys, no rate limits, no server bills.
4
- *
5
- * Search · Resolve · Download — honest quality labels, real previews, complete galleries.
2
+ * umedia -> pappy-media-api compatibility shim.
3
+ * The engine now lives in pappy-media-api. Install that instead:
4
+ * npm uninstall umedia && npm install pappy-media-api
5
+ * The API is identical; only the import path changes.
6
6
  */
7
- import { rid } from "./lib/util.js";
8
- import * as registry from "./lib/registry.js";
9
- import { download, downloadWithYtdlp } from "./lib/download.js";
10
- import { UMediaError, Codes } from "./lib/errors.js";
11
-
12
- export { UMediaError, Codes };
13
- export { LADDER as qualityLadder } from "./lib/quality.js";
14
-
15
- export class UMedia {
16
- /** @param {{downloadDir?: string, timeoutMs?: number}} [opts] */
17
- constructor(opts = {}) {
18
- this.downloadDir = opts.downloadDir || "./umedia-downloads";
19
- this.timeoutMs = opts.timeoutMs ?? 120_000;
20
- }
21
-
22
- /**
23
- * Multi-source search. Music leads with REAL 30s previews (iTunes), then YouTube.
24
- * @param {{q?: string, query?: string, type?: "video"|"music"|"movie"|"short_video", limit?: number, adult?: boolean}} [p]
25
- */
26
- async search(p = {}) {
27
- const t0 = Date.now();
28
- const { items, attempts } = await registry.search({
29
- q: p.q ?? p.query ?? "",
30
- type: p.type ?? "video",
31
- limit: Math.max(1, Math.min(25, p.limit ?? 12)),
32
- adult: Boolean(p.adult),
33
- });
34
- return {
35
- data: { items, adultFilter: { enabled: !p.adult } },
36
- meta: { requestId: rid(), source: attempts.find((a) => a.status === "success")?.provider ?? null, timestamp: new Date().toISOString(), elapsedMs: Date.now() - t0 },
37
- engine: { attempts },
38
- };
39
- }
40
-
41
- /**
42
- * Resolve a post URL into ALL of its media — every photo, every video, in order.
43
- * `engine.attempts` is the honest provider trail.
44
- */
45
- async resolve(url) {
46
- return registry.resolve(url);
47
- }
48
-
49
- /**
50
- * Download every media item to disk (order preserved, failures named).
51
- * Quality is honest: `requestedQuality` vs `selectedQuality` + `fallback`.
52
- * @param {{url: string, quality?: string, type?: string, dir?: string, zip?: boolean, onProgress?: (done:number,total:number)=>void}} p
53
- */
54
- async download(p = {}) {
55
- if (!p.url) throw new UMediaError(Codes.INVALID_REQUEST, "download({ url }) — url is required");
56
- return download({ ...p, dir: p.dir || this.downloadDir });
57
- }
58
-
59
- /** Power tier: route a download through yt-dlp (1800+ sites, hard networks, muxing). */
60
- async downloadWithYtdlp(p = {}) {
61
- if (!p.url) throw new UMediaError(Codes.INVALID_REQUEST, "downloadWithYtdlp({ url }) — url is required");
62
- return downloadWithYtdlp({ ...p, dir: p.dir || this.downloadDir });
63
- }
64
-
65
- /** What this engine can actually do on THIS machine — never overstated. */
66
- async capabilities() {
67
- return { data: registry.capabilities(), meta: { requestId: rid(), source: "local", timestamp: new Date().toISOString() } };
68
- }
69
-
70
- /** Real engine status — which adapters are operational here. */
71
- async status() {
72
- return { data: registry.status(), meta: { requestId: rid(), source: "local", timestamp: new Date().toISOString() } };
73
- }
74
- }
75
-
76
- export default UMedia;
7
+ export * from "pappy-media-api";
8
+ export { default } from "pappy-media-api";
package/package.json CHANGED
@@ -1,57 +1,14 @@
1
1
  {
2
2
  "name": "umedia",
3
- "version": "0.4.2",
4
- "description": "Standalone media engine — search, resolve and download media from YouTube, TikTok, Instagram, Reddit, X, iTunes (and 1800+ sites via the yt-dlp tier) entirely on your machine. No hosted API, no keys, no server bills. Honest quality labels, complete galleries, typed errors.",
5
- "type": "module",
6
- "main": "./index.js",
7
- "types": "./index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./index.d.ts",
11
- "import": "./index.js"
12
- }
13
- },
14
- "bin": {
15
- "umedia": "bin/umedia.js"
16
- },
17
- "files": [
18
- "index.js",
19
- "index.d.ts",
20
- "lib/",
21
- "bin/",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "engines": {
26
- "node": ">=18"
27
- },
28
- "scripts": {
29
- "test": "node --test test/"
30
- },
31
- "dependencies": {
32
- "fflate": "^0.8.2",
33
- "youtubei.js": "^18.1.0"
34
- },
35
- "keywords": [
36
- "media",
37
- "downloader",
38
- "youtube",
39
- "tiktok",
40
- "instagram",
41
- "reddit",
42
- "search",
43
- "engine",
44
- "ytdlp",
45
- "cli"
46
- ],
3
+ "version": "0.5.0",
4
+ "description": "Renamed to pappy-media-api. This package is now a thin compatibility shim that re-exports pappy-media-api.",
5
+ "keywords": ["media", "downloader", "youtube", "tiktok", "instagram", "pinterest", "snapchat", "pappy"],
47
6
  "license": "MIT",
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://github.com/Anonymous20666/unified-media-api.git",
51
- "directory": "sdk"
52
- },
53
- "bugs": {
54
- "url": "https://github.com/Anonymous20666/unified-media-api/issues"
55
- },
56
- "homepage": "https://github.com/Anonymous20666/unified-media-api#readme"
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "types": "index.d.ts",
10
+ "bin": { "umedia": "bin/umedia.js", "pappy-media-api": "bin/umedia.js" },
11
+ "files": ["index.js", "index.d.ts", "bin"],
12
+ "dependencies": { "pappy-media-api": "^0.5.0" },
13
+ "engines": { "node": ">=18" }
57
14
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Anonymous20666
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 DELETED
@@ -1,156 +0,0 @@
1
- <div align="center">
2
-
3
- <img src="https://dev-pappy-api-downloader.duckdns.org/og-image.jpg" alt="Unified Media API — one API for every media platform" width="760" />
4
-
5
- # umedia
6
-
7
- **The standalone media engine — search, resolve and download, entirely on YOUR machine.**
8
-
9
- No hosted API. No API keys. No rate limits. **No server bill.**
10
-
11
- YouTube · TikTok · Pinterest · SoundCloud · Dailymotion · Vimeo · Streamable · Twitch · Bluesky · Instagram · Reddit · X · Facebook · Threads · Snapchat · Rumble · Tumblr · iTunes · 1800+ sites (via the optional yt-dlp tier)
12
-
13
- [![npm version](https://img.shields.io/npm/v/umedia?color=67e8f9&label=npm)](https://www.npmjs.com/package/umedia)
14
- [![node](https://img.shields.io/badge/node-%E2%89%A5%2018-brightgreen)](https://nodejs.org)
15
- [![license](https://img.shields.io/npm/l/umedia?color=22d3ee)](LICENSE)
16
-
17
- </div>
18
-
19
- ---
20
-
21
- ## 💸 Why this exists
22
-
23
- Hosted media APIs cost money **every month** — per request, per user, per server.
24
- `umedia` flips the model: the engine runs **inside your app**. Your users' machines do the work,
25
- so your costs are… zero. Install it, call it, ship.
26
-
27
- ```bash
28
- npm install umedia
29
- ```
30
-
31
- ```js
32
- import { UMedia } from "umedia";
33
-
34
- const media = new UMedia();
35
-
36
- // 1) Search — music leads with REAL 30-second previews
37
- const { data } = await media.search({ q: "afrobeats", type: "music", limit: 3 });
38
- for (const item of data.items) console.log(item.title, "—", item.previewKind, "→", item.previewUrl);
39
-
40
- // 2) Resolve a post → ALL media, in order
41
- const { data: post } = await media.resolve("https://www.tiktok.com/@user/photo/7690630628697459990");
42
- console.log(post.itemCount, post.counts); // 30 { images: 30, … } — never truncated silently
43
-
44
- // 3) Download everything to disk
45
- const { data: job } = await media.download({ url: post.sourceUrl, quality: "best", dir: "./out" });
46
- console.log(job.requestedQuality, "→", job.selectedQuality); // "best" → "720p" — real, never invented
47
- ```
48
-
49
- That's the whole model: **your code, your machine, your files.**
50
-
51
- ---
52
-
53
- ## 🎯 The contract that keeps your app honest
54
-
55
- ### 🏷️ Quality is never manufactured
56
- `requestedQuality` vs `selectedQuality` on every download. A 240p source never arrives wearing a
57
- 1080p sticker. `fallback: true` tells you exactly when reality differed from the request.
58
-
59
- ### 🖼️ Galleries arrive complete
60
- 100 photos → **100 files, in order** (`001 - …`, `002 - …`). Anything that failed is named in
61
- `failedItems[]` with the reason — never silent, never partial-by-accident.
62
-
63
- ### 🎧 Previews drop first
64
- Music search leads with real **30s iTunes clips** (`previewKind: "clip30s"`), movies with YouTube
65
- trailers, social posts with official embeds. `previewKind: "none"` beats a fake preview.
66
-
67
- ### 🧯 Typed failures, always
68
- `UMediaError` with a stable `code` (`RATE_LIMITED`, `MEDIA_NOT_FOUND`, `PROVIDER_UNAVAILABLE`,
69
- `SSRF_BLOCKED`, …), plus `engine.attempts[]` — the real trail of which providers were tried and why.
70
-
71
- ---
72
-
73
- ## 🗺️ What runs where (honest edition)
74
-
75
- | Platform | Search | Resolve | Download | How |
76
- |---|---|---|---|---|
77
- | **YouTube** | ✅ | ✅ | ✅ * | InnerTube — search & metadata always; stream URLs depend on your network (see below) |
78
- | **TikTok** | — | ✅ | ✅ | video posts = direct CDN mp4; photo galleries = every slide the platform publicly exposes, `truncated: true` when it degrades galleries |
79
- | **Pinterest** | — | ✅ | ✅ | image pins, video pins (direct mp4 + HLS variants), idea/story pins, `pin.it` short links |
80
- | **SoundCloud** | — | ✅ | ✅ | full tracks as direct progressive MP3 (+ HLS) with real titles/artists |
81
- | **Dailymotion** | — | ✅ | ✅ | public player metadata — HLS/progressive renditions (HLS saved as `.ts`) |
82
- | **Vimeo** | — | ✅ | ✅ | player config — progressive mp4 when served, HLS otherwise |
83
- | **Streamable** | — | ✅ | ✅ | direct mp4 renditions |
84
- | **Twitch** | — | ✅ | ✅ | clips = direct CloudFront mp4s; VODs/livestreams via the yt-dlp tier |
85
- | **Bluesky** | — | ✅ | ✅ | public AppView API — full-size images + video playlists |
86
- | **Instagram** | — | ✅ | ✅ | oEmbed→mobile info API: photos, videos, full carousels |
87
- | **Reddit** | — | ✅ | ✅ | posts + galleries + video with its audio sidecar (DC IPs may 403 — typed honest error) |
88
- | **X** | — | ✅ | ✅ | syndication token + guest GraphQL: photos/videos/gifs at real bitrates |
89
- | **Facebook** | — | ✅ | ✅ | reels/watch/videos/shares/fb.watch via the web player JSON |
90
- | **Threads** | — | ✅ | ✅ | post SSR state + og media (Meta gates some networks — typed honest error) |
91
- | **Snapchat** | — | ✅ | ✅ | spotlight videos (direct sc-cdn mp4) + public stories/highlights — ALL snaps in order |
92
- | **Rumble** | — | ✅ best-effort | ✅ best-effort | embed API direct mp4s (site blocks datacenter IPs) |
93
- | **Tumblr** | — | ✅ | ✅ | mobile API: video/audio/photo posts + reblog trails, all in order |
94
- | **iTunes** | ✅ | — | ✅ previews | real 30s clips, rock solid |
95
- | **1800+ sites** | — | ✅ | ✅ | optional tier: `pip install yt-dlp` — labeled `source: "ytdlp"` |
96
-
97
- **\* About YouTube stream URLs:** YouTube bot-gates datacenter IPs and serves formats without URLs.
98
- On typical home connections `download()` just works; on hard networks you'll get a typed
99
- `PROVIDER_UNAVAILABLE` with the real reason — or run `download({ … })` via
100
- `downloadWithYtdlp()` / `umedia download <url> --ytdlp` which handles PO tokens, cookies and muxing.
101
- We'd rather tell you the truth than fake a download.
102
-
103
- **Direct media URLs always work:** give `resolve()`/`download()` any direct `.mp4/.m4a/.jpg/…` link and it
104
- goes straight to disk — no platform needed.
105
-
106
- ```js
107
- // the power tier — one function, 1800+ sites, hard networks
108
- await media.downloadWithYtdlp({ url: "https://example.com/anything", quality: "720p", dir: "./out" });
109
- ```
110
-
111
- ---
112
-
113
- ## 📚 API
114
-
115
- | Call | What it does |
116
- |---|---|
117
- | `media.search({q, type, limit, adult})` | Multi-source search — `type`: `video` \| `music` \| `movie` \| `short_video` |
118
- | `media.resolve(url)` | Every media item of a post, in order + honest `engine.attempts` |
119
- | `media.download({url, quality, dir, zip, onProgress})` | Saves files to disk — `quality`: `original` \| `best` \| `2160p` … `144p` \| `audio` |
120
- | `media.downloadWithYtdlp({url, quality, dir})` | yt-dlp tier (requires `yt-dlp` on PATH) |
121
- | `media.capabilities()` | What THIS machine can actually do — never overstated |
122
- | `media.status()` | Real adapter status |
123
-
124
- **Error codes** — `INVALID_REQUEST` · `INVALID_URL` · `SSRF_BLOCKED` · `AUTH_REQUIRED` ·
125
- `AUTH_INVALID` · `RATE_LIMITED` · `MEDIA_NOT_FOUND` · `CONTENT_PRIVATE` · `ACCESS_RESTRICTED` ·
126
- `QUALITY_UNAVAILABLE` · `PROVIDER_ERROR` · `PROVIDER_UNAVAILABLE` · `TIMEOUT` · `NOT_FOUND` · `INTERNAL`
127
-
128
- ---
129
-
130
- ## 🖥️ CLI
131
-
132
- ```bash
133
- npx umedia search "lofi beats" --type music --limit 5
134
- npx umedia resolve "https://www.tiktok.com/@user/photo/7690630628697459990"
135
- npx umedia download "https://youtu.be/jNQXAC9IVRw" --quality best -o ./out
136
- npx umedia download "https://example.com/video" --ytdlp # power tier
137
- npx umedia status
138
- ```
139
-
140
- ---
141
-
142
- ## 🧯 Safety, built in
143
-
144
- - **SSRF hygiene** — private, loopback and internal hosts are refused (`SSRF_BLOCKED`).
145
- - **No secrets to manage** — there are none. No keys, no tokens, no dashboards.
146
- - **Zero telemetry.** Your users' requests never touch anyone's server — because there isn't one.
147
-
148
- ---
149
-
150
- <div align="center">
151
-
152
- **Built to save you the hosting bill.** Ship the engine, not the infrastructure.
153
-
154
- MIT © 2026 · part of the [Unified Media API](https://dev-pappy-api-downloader.duckdns.org) project
155
-
156
- </div>