xvideo-dll 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # xvideo-download
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Librería para Node.js que obtiene la información y las resoluciones disponibles de un video y lo descarga.
4
+
5
+ - Lee los datos básicos de la página (título, miniatura, descripción).
6
+ - Detecta todas las calidades: MP4 directo (`mp4-low`, `mp4-high`) y HLS (`hls-480p`, `hls-720p`, ...).
7
+ - Descarga HLS en paralelo y lo convierte a un **MP4 real** (reproducible en navegador, VS Code, etc.).
8
+ - **No necesita ffmpeg.**
9
+ - Si la URL firmada caduca a mitad de la descarga, puede renovarla automáticamente.
10
+
11
+ ## Requisitos
12
+
13
+ - Node.js 18 o superior.
14
+
15
+ ## Instalación
16
+
17
+ ```bash
18
+ npm install xvideo-download
19
+ ```
20
+
21
+ ## Uso rápido
22
+
23
+ ```ts
24
+ import { fetchPageHtml, parseVideo, getResolutions, download } from "xvideo-download";
25
+
26
+ const pageUrl = "https://sitio.com/video/xxxx";
27
+
28
+ // 1. Descargar la página (siempre en vivo: las URLs caducan)
29
+ const html = await fetchPageHtml(pageUrl);
30
+
31
+ // 2. Información básica
32
+ console.log(parseVideo(html));
33
+ // { title, thumbnail, description, jsonLd }
34
+
35
+ // 3. Resoluciones disponibles
36
+ const resolutions = await getResolutions(html);
37
+ console.log(Object.keys(resolutions));
38
+ // [ "mp4-low", "mp4-high", "hls-250p", "hls-480p", ... ]
39
+
40
+ // 4. Descargar una
41
+ const key = "hls-480p";
42
+ const result = await download(
43
+ resolutions[key],
44
+ "video.mp4",
45
+ (p) => console.log(`${Math.round(p * 100)}%`), // progreso (0 a 1)
46
+ 8, // conexiones en paralelo (solo HLS)
47
+ async () => { // opcional: renovar la URL si caduca
48
+ const fresh = await getResolutions(await fetchPageHtml(pageUrl));
49
+ return fresh[key];
50
+ }
51
+ );
52
+
53
+ console.log(result); // { path: "video.mp4", bytes: 13967360 }
54
+ ```
55
+
56
+ También funciona con `require`:
57
+
58
+ ```js
59
+ const { fetchPageHtml, getResolutions, download } = require("xvideo-download");
60
+ ```
61
+
62
+ ## API
63
+
64
+ | Función | Qué hace |
65
+ |---|---|
66
+ | `fetchPageHtml(url)` | Descarga el HTML de la página (sigue redirecciones). |
67
+ | `getVideoInfo(url)` | Descarga la página y devuelve `parseVideo` de su HTML. |
68
+ | `parseVideo(html)` | Extrae `title`, `thumbnail`, `description` y `jsonLd` de un HTML. |
69
+ | `parsePlayerUrls(html)` | Extrae las URLs `low`, `high` y `hls` del script del reproductor. |
70
+ | `parseHlsMaster(m3u8, masterUrl)` | Convierte una lista maestra `.m3u8` en `{ "hls-720p": url, ... }`. |
71
+ | `getResolutions(html)` | Devuelve todas las calidades disponibles. |
72
+ | `download(url, outPath, onProgress?, threads?, refreshUrl?)` | Descarga una resolución. Elige solo el método según la URL. |
73
+ | `downloadHlsFast(...)` | Descarga HLS en paralelo y lo convierte a MP4. |
74
+ | `downloadFile(url, outPath, onProgress?)` | Descarga un MP4 directo. |
75
+
76
+ Todas las funciones y los tipos `VideoInfo` y `Resolutions` se exportan.
77
+
78
+ ## Cosas a tener en cuenta
79
+
80
+ - **Las URLs caducan.** Contienen un token con tiempo limitado. Pide siempre la página con `fetchPageHtml` justo antes de descargar y no guardes las URLs para usarlas después.
81
+ - **Descarga HLS:** se baja en segmentos a una carpeta temporal (`.hls_temp_*`, junto al archivo de salida), se unen y se convierten a MP4. La carpeta se borra al terminar, incluso si hay un error.
82
+ - **Memoria:** en HLS el video completo pasa por la memoria durante la conversión. Es cómodo para videos de decenas de MB; con archivos muy grandes consumirá bastante RAM.
83
+ - **MP4 directo:** se descarga con una sola conexión (el parámetro `threads` no aplica).
84
+ - **Compatibilidad:** está pensada para páginas cuyo reproductor expone `setVideoUrlLow`, `setVideoUrlHigh` y `setVideoHLS`. Si el sitio cambia ese código, hay que ajustar `parsePlayerUrls`.
85
+ - **Uso responsable:** respeta los términos de uso del sitio y los derechos de autor del contenido, y no hagas muchas peticiones seguidas.
86
+
87
+ ## Licencia
88
+
89
+ ISC
package/dist/index.cjs ADDED
@@ -0,0 +1,224 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ download: () => download,
33
+ downloadFile: () => downloadFile,
34
+ downloadHlsFast: () => downloadHlsFast,
35
+ fetchPageHtml: () => fetchPageHtml,
36
+ getResolutions: () => getResolutions,
37
+ getVideoInfo: () => getVideoInfo,
38
+ parseHlsMaster: () => parseHlsMaster,
39
+ parsePlayerUrls: () => parsePlayerUrls,
40
+ parseVideo: () => parseVideo
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+ var cheerio = __toESM(require("cheerio"), 1);
44
+ var import_promises = require("fs/promises");
45
+ var import_node_fs = require("fs");
46
+ var import_node_stream = require("stream");
47
+ var import_promises2 = require("stream/promises");
48
+ var import_node_path = __toESM(require("path"), 1);
49
+ var import_mediabunny = require("mediabunny");
50
+ function parseVideo(html) {
51
+ const $ = cheerio.load(html);
52
+ const meta = (p) => $(`meta[property="${p}"]`).attr("content") ?? null;
53
+ let jsonLd = null;
54
+ try {
55
+ const raw = $('script[type="application/ld+json"]').first().html();
56
+ if (raw) jsonLd = JSON.parse(raw);
57
+ } catch {
58
+ }
59
+ return {
60
+ title: meta("og:title") ?? ($("title").text() || null),
61
+ thumbnail: meta("og:image"),
62
+ description: meta("og:description"),
63
+ jsonLd
64
+ };
65
+ }
66
+ async function getVideoInfo(url) {
67
+ const res = await fetch(url, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
68
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${url}`);
69
+ return parseVideo(await res.text());
70
+ }
71
+ async function fetchPageHtml(url) {
72
+ const res = await fetch(url, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
73
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${url}`);
74
+ return res.text();
75
+ }
76
+ function parsePlayerUrls(html) {
77
+ const grab = (name) => html.match(new RegExp(`setVideo${name}\\(\\s*['"]([^'"]+)['"]\\s*\\)`))?.[1] ?? null;
78
+ return { low: grab("UrlLow"), high: grab("UrlHigh"), hls: grab("HLS") };
79
+ }
80
+ function parseHlsMaster(m3u8, masterUrl) {
81
+ const out = {};
82
+ const lines = m3u8.split(/\r?\n/);
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+ if (!line.startsWith("#EXT-X-STREAM-INF")) continue;
86
+ const height = line.match(/RESOLUTION=\d+x(\d+)/)?.[1];
87
+ const next = lines[i + 1]?.trim();
88
+ if (!height || !next) continue;
89
+ out[`hls-${height}p`] = new URL(next, masterUrl).href;
90
+ }
91
+ return out;
92
+ }
93
+ async function getResolutions(html) {
94
+ const { low, high, hls } = parsePlayerUrls(html);
95
+ const resolutions = {};
96
+ if (low) resolutions["mp4-low"] = low;
97
+ if (high) resolutions["mp4-high"] = high;
98
+ if (hls) {
99
+ const res = await fetch(hls, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
100
+ if (res.ok) {
101
+ Object.assign(resolutions, parseHlsMaster(await res.text(), hls));
102
+ } else {
103
+ console.warn(`\u26A0 No se pudo leer el playlist HLS: HTTP ${res.status} en ${hls}`);
104
+ }
105
+ } else {
106
+ console.warn("\u26A0 No se encontr\xF3 setVideoHLS(...) en el HTML");
107
+ }
108
+ return resolutions;
109
+ }
110
+ async function remuxTsToMp4(tsData, outPath) {
111
+ const input = new import_mediabunny.Input({
112
+ source: new import_mediabunny.BufferSource(tsData),
113
+ formats: import_mediabunny.ALL_FORMATS
114
+ });
115
+ const target = new import_mediabunny.BufferTarget();
116
+ const output = new import_mediabunny.Output({
117
+ format: new import_mediabunny.Mp4OutputFormat({ fastStart: "in-memory" }),
118
+ target
119
+ });
120
+ const conversion = await import_mediabunny.Conversion.init({ input, output });
121
+ if (!conversion.isValid) {
122
+ throw new Error(
123
+ "Conversi\xF3n inv\xE1lida. Pistas descartadas: " + JSON.stringify(conversion.discardedTracks)
124
+ );
125
+ }
126
+ await conversion.execute();
127
+ if (!target.buffer) throw new Error("mediabunny no gener\xF3 ning\xFAn buffer");
128
+ await (0, import_promises.writeFile)(outPath, new Uint8Array(target.buffer));
129
+ }
130
+ async function fetchSegmentList(m3u8Url) {
131
+ const res = await fetch(m3u8Url, { headers: { "user-agent": "Mozilla/5.0" } });
132
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${m3u8Url}`);
133
+ const segments = [];
134
+ for (const raw of (await res.text()).split(/\r?\n/)) {
135
+ const line = raw.trim();
136
+ if (line && !line.startsWith("#") && !line.endsWith(".m3u8")) {
137
+ segments.push(new URL(line, m3u8Url).href);
138
+ }
139
+ }
140
+ if (segments.length === 0) throw new Error("No se encontraron segmentos HLS v\xE1lidos");
141
+ return segments;
142
+ }
143
+ async function downloadHlsFast(m3u8Url, outPath, concurrency = 8, onProgress, refreshUrl) {
144
+ let segments = await fetchSegmentList(m3u8Url);
145
+ const total = segments.length;
146
+ const tempDir = import_node_path.default.join(import_node_path.default.dirname(outPath), `.hls_temp_${Date.now()}`);
147
+ await (0, import_promises.mkdir)(tempDir, { recursive: true });
148
+ try {
149
+ let completed = 0;
150
+ const tasks = Array.from({ length: total }, (_, index) => async () => {
151
+ let buffer = null;
152
+ let lastStatus = 0;
153
+ for (let attempt = 0; attempt < 5 && !buffer; attempt++) {
154
+ try {
155
+ const segRes = await fetch(segments[index], { headers: { "user-agent": "Mozilla/5.0" } });
156
+ if (segRes.ok) {
157
+ buffer = Buffer.from(await segRes.arrayBuffer());
158
+ break;
159
+ }
160
+ lastStatus = segRes.status;
161
+ if (segRes.status === 410 || segRes.status === 403) {
162
+ if (refreshUrl) m3u8Url = await refreshUrl();
163
+ const fresh = await fetchSegmentList(m3u8Url);
164
+ if (fresh.length === total) segments = fresh;
165
+ }
166
+ } catch {
167
+ lastStatus = -1;
168
+ }
169
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
170
+ }
171
+ if (!buffer) throw new Error(`Fallo segmento ${index}: ${lastStatus}`);
172
+ const filePath = import_node_path.default.join(tempDir, `${index.toString().padStart(5, "0")}.ts`);
173
+ await (0, import_promises.writeFile)(filePath, buffer);
174
+ completed++;
175
+ if (onProgress) onProgress(completed / total);
176
+ });
177
+ for (let i = 0; i < tasks.length; i += concurrency) {
178
+ const batch = tasks.slice(i, i + concurrency);
179
+ await Promise.all(batch.map((t) => t()));
180
+ }
181
+ const parts = [];
182
+ for (let i = 0; i < total; i++) {
183
+ const filePath = import_node_path.default.join(tempDir, `${i.toString().padStart(5, "0")}.ts`);
184
+ parts.push(await import_node_fs.promises.readFile(filePath));
185
+ }
186
+ const merged = Buffer.concat(parts);
187
+ await remuxTsToMp4(merged, outPath);
188
+ } finally {
189
+ await (0, import_promises.rm)(tempDir, { recursive: true, force: true });
190
+ }
191
+ const stats = await import_node_fs.promises.stat(outPath);
192
+ return { path: outPath, bytes: stats.size };
193
+ }
194
+ async function downloadFile(url, outPath, onProgress) {
195
+ const res = await fetch(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
196
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status} en ${url}`);
197
+ const total = Number(res.headers.get("content-length")) || 0;
198
+ let received = 0;
199
+ const body = import_node_stream.Readable.fromWeb(res.body);
200
+ body.on("data", (chunk) => {
201
+ received += chunk.length;
202
+ if (total && onProgress) onProgress(received / total);
203
+ });
204
+ await (0, import_promises2.pipeline)(body, (0, import_node_fs.createWriteStream)(outPath));
205
+ return { path: outPath, bytes: received };
206
+ }
207
+ function download(url, outPath, onProgress, threads = 8, refreshUrl) {
208
+ if (url.includes(".m3u8")) {
209
+ return downloadHlsFast(url, outPath, threads, onProgress, refreshUrl);
210
+ }
211
+ return downloadFile(url, outPath, onProgress);
212
+ }
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ download,
216
+ downloadFile,
217
+ downloadHlsFast,
218
+ fetchPageHtml,
219
+ getResolutions,
220
+ getVideoInfo,
221
+ parseHlsMaster,
222
+ parsePlayerUrls,
223
+ parseVideo
224
+ });
@@ -0,0 +1,33 @@
1
+ export interface VideoInfo {
2
+ title: string | null;
3
+ thumbnail: string | null;
4
+ description: string | null;
5
+ jsonLd: unknown | null;
6
+ }
7
+ export declare function parseVideo(html: string): VideoInfo;
8
+ export declare function getVideoInfo(url: string): Promise<VideoInfo>;
9
+ /** Descarga el HTML de la página del video EN VIVO (genera URLs firmadas nuevas). */
10
+ export declare function fetchPageHtml(url: string): Promise<string>;
11
+ export type Resolutions = Record<string, string>;
12
+ export declare function parsePlayerUrls(html: string): {
13
+ low: string | null;
14
+ high: string | null;
15
+ hls: string | null;
16
+ };
17
+ export declare function parseHlsMaster(m3u8: string, masterUrl: string): Resolutions;
18
+ export declare function getResolutions(html: string): Promise<Resolutions>;
19
+ /**
20
+ * Descarga HLS: segmentos .ts en paralelo, unión en memoria y remux a MP4 real.
21
+ */
22
+ export declare function downloadHlsFast(m3u8Url: string, outPath: string, concurrency?: number, onProgress?: (p: number) => void, refreshUrl?: () => Promise<string>): Promise<{
23
+ path: string;
24
+ bytes: number;
25
+ }>;
26
+ export declare function downloadFile(url: string, outPath: string, onProgress?: (p: number) => void): Promise<{
27
+ path: string;
28
+ bytes: number;
29
+ }>;
30
+ export declare function download(url: string, outPath: string, onProgress?: (p: number) => void, threads?: number, refreshUrl?: () => Promise<string>): Promise<{
31
+ path: string;
32
+ bytes: number;
33
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ // src/index.ts
2
+ import * as cheerio from "cheerio";
3
+ import { writeFile, mkdir, rm } from "fs/promises";
4
+ import { createWriteStream, promises as fsPromises } from "fs";
5
+ import { Readable } from "stream";
6
+ import { pipeline } from "stream/promises";
7
+ import path from "path";
8
+ import {
9
+ Input,
10
+ Output,
11
+ Conversion,
12
+ BufferSource,
13
+ BufferTarget,
14
+ ALL_FORMATS,
15
+ Mp4OutputFormat
16
+ } from "mediabunny";
17
+ function parseVideo(html) {
18
+ const $ = cheerio.load(html);
19
+ const meta = (p) => $(`meta[property="${p}"]`).attr("content") ?? null;
20
+ let jsonLd = null;
21
+ try {
22
+ const raw = $('script[type="application/ld+json"]').first().html();
23
+ if (raw) jsonLd = JSON.parse(raw);
24
+ } catch {
25
+ }
26
+ return {
27
+ title: meta("og:title") ?? ($("title").text() || null),
28
+ thumbnail: meta("og:image"),
29
+ description: meta("og:description"),
30
+ jsonLd
31
+ };
32
+ }
33
+ async function getVideoInfo(url) {
34
+ const res = await fetch(url, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
35
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${url}`);
36
+ return parseVideo(await res.text());
37
+ }
38
+ async function fetchPageHtml(url) {
39
+ const res = await fetch(url, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
40
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${url}`);
41
+ return res.text();
42
+ }
43
+ function parsePlayerUrls(html) {
44
+ const grab = (name) => html.match(new RegExp(`setVideo${name}\\(\\s*['"]([^'"]+)['"]\\s*\\)`))?.[1] ?? null;
45
+ return { low: grab("UrlLow"), high: grab("UrlHigh"), hls: grab("HLS") };
46
+ }
47
+ function parseHlsMaster(m3u8, masterUrl) {
48
+ const out = {};
49
+ const lines = m3u8.split(/\r?\n/);
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+ if (!line.startsWith("#EXT-X-STREAM-INF")) continue;
53
+ const height = line.match(/RESOLUTION=\d+x(\d+)/)?.[1];
54
+ const next = lines[i + 1]?.trim();
55
+ if (!height || !next) continue;
56
+ out[`hls-${height}p`] = new URL(next, masterUrl).href;
57
+ }
58
+ return out;
59
+ }
60
+ async function getResolutions(html) {
61
+ const { low, high, hls } = parsePlayerUrls(html);
62
+ const resolutions = {};
63
+ if (low) resolutions["mp4-low"] = low;
64
+ if (high) resolutions["mp4-high"] = high;
65
+ if (hls) {
66
+ const res = await fetch(hls, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
67
+ if (res.ok) {
68
+ Object.assign(resolutions, parseHlsMaster(await res.text(), hls));
69
+ } else {
70
+ console.warn(`\u26A0 No se pudo leer el playlist HLS: HTTP ${res.status} en ${hls}`);
71
+ }
72
+ } else {
73
+ console.warn("\u26A0 No se encontr\xF3 setVideoHLS(...) en el HTML");
74
+ }
75
+ return resolutions;
76
+ }
77
+ async function remuxTsToMp4(tsData, outPath) {
78
+ const input = new Input({
79
+ source: new BufferSource(tsData),
80
+ formats: ALL_FORMATS
81
+ });
82
+ const target = new BufferTarget();
83
+ const output = new Output({
84
+ format: new Mp4OutputFormat({ fastStart: "in-memory" }),
85
+ target
86
+ });
87
+ const conversion = await Conversion.init({ input, output });
88
+ if (!conversion.isValid) {
89
+ throw new Error(
90
+ "Conversi\xF3n inv\xE1lida. Pistas descartadas: " + JSON.stringify(conversion.discardedTracks)
91
+ );
92
+ }
93
+ await conversion.execute();
94
+ if (!target.buffer) throw new Error("mediabunny no gener\xF3 ning\xFAn buffer");
95
+ await writeFile(outPath, new Uint8Array(target.buffer));
96
+ }
97
+ async function fetchSegmentList(m3u8Url) {
98
+ const res = await fetch(m3u8Url, { headers: { "user-agent": "Mozilla/5.0" } });
99
+ if (!res.ok) throw new Error(`HTTP ${res.status} en ${m3u8Url}`);
100
+ const segments = [];
101
+ for (const raw of (await res.text()).split(/\r?\n/)) {
102
+ const line = raw.trim();
103
+ if (line && !line.startsWith("#") && !line.endsWith(".m3u8")) {
104
+ segments.push(new URL(line, m3u8Url).href);
105
+ }
106
+ }
107
+ if (segments.length === 0) throw new Error("No se encontraron segmentos HLS v\xE1lidos");
108
+ return segments;
109
+ }
110
+ async function downloadHlsFast(m3u8Url, outPath, concurrency = 8, onProgress, refreshUrl) {
111
+ let segments = await fetchSegmentList(m3u8Url);
112
+ const total = segments.length;
113
+ const tempDir = path.join(path.dirname(outPath), `.hls_temp_${Date.now()}`);
114
+ await mkdir(tempDir, { recursive: true });
115
+ try {
116
+ let completed = 0;
117
+ const tasks = Array.from({ length: total }, (_, index) => async () => {
118
+ let buffer = null;
119
+ let lastStatus = 0;
120
+ for (let attempt = 0; attempt < 5 && !buffer; attempt++) {
121
+ try {
122
+ const segRes = await fetch(segments[index], { headers: { "user-agent": "Mozilla/5.0" } });
123
+ if (segRes.ok) {
124
+ buffer = Buffer.from(await segRes.arrayBuffer());
125
+ break;
126
+ }
127
+ lastStatus = segRes.status;
128
+ if (segRes.status === 410 || segRes.status === 403) {
129
+ if (refreshUrl) m3u8Url = await refreshUrl();
130
+ const fresh = await fetchSegmentList(m3u8Url);
131
+ if (fresh.length === total) segments = fresh;
132
+ }
133
+ } catch {
134
+ lastStatus = -1;
135
+ }
136
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
137
+ }
138
+ if (!buffer) throw new Error(`Fallo segmento ${index}: ${lastStatus}`);
139
+ const filePath = path.join(tempDir, `${index.toString().padStart(5, "0")}.ts`);
140
+ await writeFile(filePath, buffer);
141
+ completed++;
142
+ if (onProgress) onProgress(completed / total);
143
+ });
144
+ for (let i = 0; i < tasks.length; i += concurrency) {
145
+ const batch = tasks.slice(i, i + concurrency);
146
+ await Promise.all(batch.map((t) => t()));
147
+ }
148
+ const parts = [];
149
+ for (let i = 0; i < total; i++) {
150
+ const filePath = path.join(tempDir, `${i.toString().padStart(5, "0")}.ts`);
151
+ parts.push(await fsPromises.readFile(filePath));
152
+ }
153
+ const merged = Buffer.concat(parts);
154
+ await remuxTsToMp4(merged, outPath);
155
+ } finally {
156
+ await rm(tempDir, { recursive: true, force: true });
157
+ }
158
+ const stats = await fsPromises.stat(outPath);
159
+ return { path: outPath, bytes: stats.size };
160
+ }
161
+ async function downloadFile(url, outPath, onProgress) {
162
+ const res = await fetch(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } });
163
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status} en ${url}`);
164
+ const total = Number(res.headers.get("content-length")) || 0;
165
+ let received = 0;
166
+ const body = Readable.fromWeb(res.body);
167
+ body.on("data", (chunk) => {
168
+ received += chunk.length;
169
+ if (total && onProgress) onProgress(received / total);
170
+ });
171
+ await pipeline(body, createWriteStream(outPath));
172
+ return { path: outPath, bytes: received };
173
+ }
174
+ function download(url, outPath, onProgress, threads = 8, refreshUrl) {
175
+ if (url.includes(".m3u8")) {
176
+ return downloadHlsFast(url, outPath, threads, onProgress, refreshUrl);
177
+ }
178
+ return downloadFile(url, outPath, onProgress);
179
+ }
180
+ export {
181
+ download,
182
+ downloadFile,
183
+ downloadHlsFast,
184
+ fetchPageHtml,
185
+ getResolutions,
186
+ getVideoInfo,
187
+ parseHlsMaster,
188
+ parsePlayerUrls,
189
+ parseVideo
190
+ };
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "xvideo-dll",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Obtiene la info y las resoluciones de un video y lo descarga (MP4 directo o HLS convertido a MP4 real) sin necesitar ffmpeg.",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm,cjs --clean --target node18 && tsc src/index.ts --declaration --emitDeclarationOnly --outDir dist --skipLibCheck --module esnext --moduleResolution bundler --target es2022 --strict",
16
+ "prepublishOnly": "pnpm build"
17
+ },
18
+ "keywords": [
19
+ "video",
20
+ "download",
21
+ "downloader",
22
+ "hls",
23
+ "m3u8",
24
+ "mp4",
25
+ "scraper"
26
+ ],
27
+ "author": "",
28
+ "license": "ISC",
29
+ "packageManager": "pnpm@12.16.1",
30
+ "type": "module",
31
+ "devDependencies": {
32
+ "@clack/prompts": "^1.8.1",
33
+ "@types/node": "^26.6.6",
34
+ "tsup": "^9.0.0",
35
+ "typescript": "^7.0.2",
36
+ "vitest": "^5.1.0"
37
+ },
38
+ "exports": {
39
+ ".": {
40
+ "import": {
41
+ "types": "./dist/index.d.ts",
42
+ "default": "./dist/index.js"
43
+ },
44
+ "require": {
45
+ "types": "./dist/index.d.ts",
46
+ "default": "./dist/index.cjs"
47
+ }
48
+ }
49
+ },
50
+ "dependencies": {
51
+ "cheerio": "^1.2.0",
52
+ "mediabunny": "1.61.4"
53
+ }
6
54
  }