umedia 0.4.1 → 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 +12 -98
- package/index.d.ts +2 -180
- package/index.js +6 -74
- package/package.json +10 -53
- package/LICENSE +0 -21
- package/README.md +0 -156
- package/lib/download.js +0 -248
- package/lib/errors.js +0 -30
- package/lib/providers/bluesky.js +0 -118
- package/lib/providers/dailymotion.js +0 -100
- package/lib/providers/facebook.js +0 -126
- package/lib/providers/instagram.js +0 -191
- package/lib/providers/itunes.js +0 -47
- package/lib/providers/pinterest.js +0 -181
- package/lib/providers/reddit.js +0 -123
- package/lib/providers/rumble.js +0 -110
- package/lib/providers/snapchat.js +0 -177
- package/lib/providers/soundcloud.js +0 -138
- package/lib/providers/streamable.js +0 -93
- package/lib/providers/threads.js +0 -103
- package/lib/providers/tiktok.js +0 -165
- package/lib/providers/tumblr.js +0 -153
- package/lib/providers/twitch.js +0 -108
- package/lib/providers/vimeo.js +0 -108
- package/lib/providers/x.js +0 -215
- package/lib/providers/youtube.js +0 -146
- package/lib/providers/ytdlp.js +0 -97
- package/lib/quality.js +0 -49
- package/lib/registry.js +0 -171
- package/lib/util.js +0 -93
package/bin/umedia.js
CHANGED
|
@@ -1,99 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
"description": "
|
|
5
|
-
"
|
|
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
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
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
|
-
[](https://www.npmjs.com/package/umedia)
|
|
14
|
-
[](https://nodejs.org)
|
|
15
|
-
[](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>
|