@eluvio/elv-client-js 4.2.13 → 4.2.15
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/dist/ElvClient-min.js +1 -1
- package/dist/ElvClient-node-min.js +1 -1
- package/dist/ElvFrameClient-min.js +1 -1
- package/dist/ElvPermissionsClient-min.js +1 -1
- package/dist/ElvWalletClient-min.js +1 -1
- package/dist/ElvWalletClient-node-min.js +1 -1
- package/dist/src/AuthorizationClient.js +723 -710
- package/dist/src/ContentObjectAudit.js +56 -56
- package/dist/src/Crypto.js +85 -85
- package/dist/src/ElvClient.js +502 -530
- package/dist/src/ElvWallet.js +28 -30
- package/dist/src/EthClient.js +311 -311
- package/dist/src/FrameClient.js +65 -64
- package/dist/src/HttpClient.js +60 -60
- package/dist/src/Id.js +2 -1
- package/dist/src/PermissionsClient.js +487 -499
- package/dist/src/RemoteSigner.js +178 -123
- package/dist/src/UserProfileClient.js +374 -392
- package/dist/src/Utils.js +66 -69
- package/dist/src/Validation.js +10 -10
- package/dist/src/client/ABRPublishing.js +239 -239
- package/dist/src/client/AccessGroups.js +474 -477
- package/dist/src/client/ContentAccess.js +1713 -1708
- package/dist/src/client/ContentManagement.js +871 -871
- package/dist/src/client/Contracts.js +736 -582
- package/dist/src/client/Files.js +684 -700
- package/dist/src/client/LiveConf.js +6 -1
- package/dist/src/client/LiveStream.js +686 -722
- package/dist/src/client/NFT.js +14 -14
- package/dist/src/client/NTP.js +84 -84
- package/dist/src/client/Shares.js +60 -53
- package/dist/src/walletClient/ClientMethods.js +951 -977
- package/dist/src/walletClient/Notifications.js +14 -14
- package/dist/src/walletClient/Profile.js +66 -66
- package/dist/src/walletClient/Utils.js +15 -15
- package/dist/src/walletClient/index.js +584 -581
- package/package.json +3 -2
- package/src/ElvClient.js +2 -1
- package/src/FrameClient.js +3 -0
- package/src/LogMessage.js +1 -1
- package/src/RemoteSigner.js +17 -3
- package/src/client/ABRPublishing.js +1 -1
- package/src/client/ContentAccess.js +17 -13
- package/src/client/Contracts.js +88 -7
- package/src/walletClient/index.js +15 -4
- package/utilities/ChannelCreate.js +1 -1
- package/utilities/CompositionCreate.js +517 -0
- package/utilities/LibraryDownloadMp4.js +371 -0
- package/utilities/MezDownloadMp4.js +177 -0
- package/utilities/lib/DownloadFile.js +88 -0
- package/utilities/lib/FrameAccurateVideo.js +431 -0
- package/utilities/lib/concerns/Metadata.js +2 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Utility that accepts a library ID and generates a download URL for each object inside of it
|
|
2
|
+
const R = require("ramda");
|
|
3
|
+
const { ModOpt, NewOpt } = require("./lib/options");
|
|
4
|
+
const Utility = require("./lib/Utility");
|
|
5
|
+
const { PublicMetadataPathArrayModel } = require("./lib/models/PublicMetadataPath");
|
|
6
|
+
|
|
7
|
+
const JSONConcern = require("./lib/concerns/JSON");
|
|
8
|
+
const ArgLibraryId = require("./lib/concerns/ArgLibraryId");
|
|
9
|
+
const Metadata = require("./lib/concerns/Metadata");
|
|
10
|
+
const FabricObject = require("./lib/concerns/FabricObject");
|
|
11
|
+
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const https = require("https");
|
|
15
|
+
|
|
16
|
+
class LibraryDownloadMp4 extends Utility {
|
|
17
|
+
blueprint() {
|
|
18
|
+
return {
|
|
19
|
+
concerns: [JSONConcern, ArgLibraryId, Metadata, FabricObject],
|
|
20
|
+
options: [
|
|
21
|
+
ModOpt("libraryId", { demand: true }),
|
|
22
|
+
NewOpt("filter", {
|
|
23
|
+
descTemplate:
|
|
24
|
+
"JSON expression (or path to JSON file if starting with '@') to filter objects by (public) metadata",
|
|
25
|
+
type: "string",
|
|
26
|
+
}),
|
|
27
|
+
NewOpt("date", {
|
|
28
|
+
descTemplate: "include latest commit date/time if available",
|
|
29
|
+
type: "boolean",
|
|
30
|
+
}),
|
|
31
|
+
NewOpt("fields", {
|
|
32
|
+
coerce: PublicMetadataPathArrayModel,
|
|
33
|
+
descTemplate:
|
|
34
|
+
"Path(s) for additional metadata values to include (each must start with /public/)",
|
|
35
|
+
string: true,
|
|
36
|
+
type: "array",
|
|
37
|
+
}),
|
|
38
|
+
NewOpt("hash", {
|
|
39
|
+
descTemplate: "include latest version hash",
|
|
40
|
+
type: "boolean",
|
|
41
|
+
}),
|
|
42
|
+
NewOpt("name", {
|
|
43
|
+
descTemplate: "include object name if available",
|
|
44
|
+
type: "boolean",
|
|
45
|
+
}),
|
|
46
|
+
NewOpt("size", {
|
|
47
|
+
descTemplate: "include object total size",
|
|
48
|
+
type: "boolean",
|
|
49
|
+
}),
|
|
50
|
+
NewOpt("offering", {
|
|
51
|
+
descTemplate: "Offering name for download URL",
|
|
52
|
+
type: "string",
|
|
53
|
+
}),
|
|
54
|
+
NewOpt("format", {
|
|
55
|
+
descTemplate: "Format for download URL (default mp4)",
|
|
56
|
+
type: "string",
|
|
57
|
+
}),
|
|
58
|
+
NewOpt("downloadDir", {
|
|
59
|
+
descTemplate: "Directory to save files",
|
|
60
|
+
type: "string",
|
|
61
|
+
}),
|
|
62
|
+
NewOpt("failLog", {
|
|
63
|
+
descTemplate: "Write failures to a JSON file",
|
|
64
|
+
type: "string",
|
|
65
|
+
})
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
header() {
|
|
71
|
+
return `List and download objects for library ${this.args.libraryId}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sleep(ms) {
|
|
75
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async retry(fn, opts = {}) {
|
|
79
|
+
const {
|
|
80
|
+
retries = 3,
|
|
81
|
+
delay = 1000,
|
|
82
|
+
onRetry = null
|
|
83
|
+
} = opts;
|
|
84
|
+
|
|
85
|
+
let attempt = 0;
|
|
86
|
+
while (attempt < retries) {
|
|
87
|
+
try {
|
|
88
|
+
return await fn();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
attempt++;
|
|
91
|
+
if (attempt >= retries) throw err;
|
|
92
|
+
|
|
93
|
+
if (onRetry) onRetry(err, attempt);
|
|
94
|
+
else this.logger.warn(`Retry ${attempt}/${retries} after error: ${err.message}`);
|
|
95
|
+
|
|
96
|
+
await this.sleep(delay * Math.pow(2, attempt));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async downloadFile(url, filepath) {
|
|
103
|
+
return this.retry(() => {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const startDownload = (currentUrl, redirCount = 0) => {
|
|
106
|
+
if (redirCount > 5) {
|
|
107
|
+
return reject(new Error("Too many redirects"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const req = https.get(currentUrl, (res) => {
|
|
111
|
+
// Handle redirects
|
|
112
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
113
|
+
const nextUrl = res.headers.location.startsWith("http")
|
|
114
|
+
? res.headers.location
|
|
115
|
+
: new URL(res.headers.location, currentUrl).href;
|
|
116
|
+
|
|
117
|
+
this.logger.log(`Redirected → ${nextUrl}`);
|
|
118
|
+
return startDownload(nextUrl, redirCount + 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (res.statusCode !== 200) {
|
|
122
|
+
return reject(new Error(`Download failed (HTTP ${res.statusCode})`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const totalSize = parseInt(res.headers["content-length"] || "0", 10);
|
|
126
|
+
let downloaded = 0;
|
|
127
|
+
|
|
128
|
+
const writeStream = fs.createWriteStream(filepath);
|
|
129
|
+
|
|
130
|
+
// Progress bar
|
|
131
|
+
res.on("data", (chunk) => {
|
|
132
|
+
downloaded += chunk.length;
|
|
133
|
+
|
|
134
|
+
if (totalSize > 0) {
|
|
135
|
+
const pct = (downloaded / totalSize) * 100;
|
|
136
|
+
const barLength = 30;
|
|
137
|
+
const filled = Math.round((pct / 100) * barLength);
|
|
138
|
+
const bar = "█".repeat(filled) + "░".repeat(barLength - filled);
|
|
139
|
+
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
`\r${bar} ${pct.toFixed(1)}% (${(downloaded / 1e6).toFixed(2)}MB/${(totalSize / 1e6).toFixed(2)}MB)`
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
process.stdout.write(`\rDownloaded ${(downloaded / 1e6).toFixed(2)}MB`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
res.on("end", () => process.stdout.write("\n"));
|
|
149
|
+
|
|
150
|
+
res.pipe(writeStream);
|
|
151
|
+
|
|
152
|
+
writeStream.on("finish", () => writeStream.close(resolve));
|
|
153
|
+
|
|
154
|
+
writeStream.on("error", (err) => {
|
|
155
|
+
fs.unlink(filepath, () => reject(err));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// -----------------------------
|
|
160
|
+
// Add timeout here
|
|
161
|
+
// -----------------------------
|
|
162
|
+
const timeoutMs = 5000; // 5 seconds
|
|
163
|
+
req.setTimeout(timeoutMs, () => {
|
|
164
|
+
req.abort();
|
|
165
|
+
// Throw a special error for retry logic
|
|
166
|
+
reject(new Error("DOWNLOAD_TIMEOUT"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Handle request errors
|
|
170
|
+
req.on("error", (err) => {
|
|
171
|
+
if (err.code === "ECONNRESET") {
|
|
172
|
+
reject(new Error("DOWNLOAD_TIMEOUT"));
|
|
173
|
+
} else {
|
|
174
|
+
reject(err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// begin download
|
|
180
|
+
startDownload(url);
|
|
181
|
+
});
|
|
182
|
+
}, {
|
|
183
|
+
retries: 3, // retry up to 3 times
|
|
184
|
+
onRetry: (err, attempt) => {
|
|
185
|
+
if (err.message === "DOWNLOAD_TIMEOUT") {
|
|
186
|
+
this.logger.warn(`Download timed out. Retrying attempt ${attempt}...`);
|
|
187
|
+
} else {
|
|
188
|
+
this.logger.warn(`Download failed: ${err.message}. Retrying attempt ${attempt}...`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
sanitizeFilename(name, fallback) {
|
|
196
|
+
if (!name) return fallback;
|
|
197
|
+
return name
|
|
198
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
199
|
+
.replace(/_+/g, "_")
|
|
200
|
+
.substring(0, 180);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async processObject(e, client, libraryId, format, offering, targetDir, failedDownloads) {
|
|
204
|
+
const objectId = e.objectId;
|
|
205
|
+
const objectName = R.path(["metadata", "public", "name"], e) || objectId;
|
|
206
|
+
|
|
207
|
+
this.logger.log(`\n--- Processing ${objectName} (${objectId}) ---`);
|
|
208
|
+
|
|
209
|
+
const formattedObj = { object_id: objectId, name: objectName };
|
|
210
|
+
|
|
211
|
+
// Skip existing
|
|
212
|
+
const existing = fs.readdirSync(targetDir).find((f) => f.includes(objectId));
|
|
213
|
+
if (existing) {
|
|
214
|
+
this.logger.log(`Skipping ${objectName}: already exists (${existing})`);
|
|
215
|
+
formattedObj.download_url = "SKIPPED_ALREADY_EXISTS";
|
|
216
|
+
return formattedObj;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Version hash
|
|
221
|
+
const versionHash = await this.retry(() =>
|
|
222
|
+
this.concerns.FabricObject.latestVersionHash({ libraryId, objectId })
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Start job
|
|
226
|
+
const response = await this.retry(() =>
|
|
227
|
+
client.MakeFileServiceRequest({
|
|
228
|
+
versionHash,
|
|
229
|
+
path: "/call/media/files",
|
|
230
|
+
method: "POST",
|
|
231
|
+
body: { format, offering },
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const jobId = response.job_id;
|
|
236
|
+
this.logger.log(`Started job ${jobId}`);
|
|
237
|
+
|
|
238
|
+
// Poll job
|
|
239
|
+
let status;
|
|
240
|
+
let lastProgress = -1;
|
|
241
|
+
|
|
242
|
+
do {
|
|
243
|
+
await this.sleep(2000);
|
|
244
|
+
|
|
245
|
+
status = await this.retry(() =>
|
|
246
|
+
client.MakeFileServiceRequest({
|
|
247
|
+
versionHash,
|
|
248
|
+
path: `/call/media/files/${jobId}`,
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const progress = status?.progress || 0;
|
|
253
|
+
if (progress !== lastProgress) {
|
|
254
|
+
process.stdout.write(`Progress: ${progress.toFixed(1)}%\r`);
|
|
255
|
+
lastProgress = progress;
|
|
256
|
+
}
|
|
257
|
+
} while (status?.status !== "completed");
|
|
258
|
+
|
|
259
|
+
process.stdout.write("\n");
|
|
260
|
+
|
|
261
|
+
const filename = this.sanitizeFilename(
|
|
262
|
+
status.filename,
|
|
263
|
+
`${objectId}.mp4`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const outputFile = path.join(targetDir, filename);
|
|
267
|
+
|
|
268
|
+
const downloadUrl = await this.retry(() =>
|
|
269
|
+
client.FabricUrl({
|
|
270
|
+
versionHash,
|
|
271
|
+
call: `/media/files/${jobId}/download`,
|
|
272
|
+
service: "files",
|
|
273
|
+
queryParams: {
|
|
274
|
+
"header-x_set_content_disposition": `attachment; filename=${filename}`,
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
formattedObj.download_url = downloadUrl;
|
|
280
|
+
|
|
281
|
+
// NOW using HTTPS downloader
|
|
282
|
+
this.logger.log(`Downloading → ${outputFile}`);
|
|
283
|
+
await this.downloadFile(downloadUrl, outputFile);
|
|
284
|
+
|
|
285
|
+
this.logger.log(`✔ Completed: ${outputFile}`);
|
|
286
|
+
return formattedObj;
|
|
287
|
+
|
|
288
|
+
} catch (err) {
|
|
289
|
+
this.logger.error(`FAILED: ${objectId} - ${err.message}`);
|
|
290
|
+
|
|
291
|
+
failedDownloads.push({
|
|
292
|
+
object_id: objectId,
|
|
293
|
+
name: objectName,
|
|
294
|
+
error: err.message,
|
|
295
|
+
timestamp: new Date().toISOString(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return formattedObj;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async body() {
|
|
303
|
+
const libraryId = this.args.libraryId;
|
|
304
|
+
const format = this.args.format || "mp4";
|
|
305
|
+
const offering = this.args.offering || "default";
|
|
306
|
+
|
|
307
|
+
const filter =
|
|
308
|
+
this.args.filter &&
|
|
309
|
+
this.concerns.JSON.parseStringOrFile({ strOrPath: this.args.filter });
|
|
310
|
+
|
|
311
|
+
if (!this.args.fields) this.args.fields = [];
|
|
312
|
+
|
|
313
|
+
const select = ["/public/name", ...this.args.fields];
|
|
314
|
+
|
|
315
|
+
let objectList = await this.concerns.ArgLibraryId.libObjectList({
|
|
316
|
+
filterOptions: { select, filter },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
this.logger.log(`Found ${objectList.length} object(s)\n`);
|
|
320
|
+
|
|
321
|
+
const client = await this.concerns.Client.get();
|
|
322
|
+
const failedDownloads = [];
|
|
323
|
+
|
|
324
|
+
const targetDir = this.args.downloadDir
|
|
325
|
+
? path.resolve(this.args.downloadDir)
|
|
326
|
+
: process.cwd();
|
|
327
|
+
|
|
328
|
+
if (!fs.existsSync(targetDir))
|
|
329
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
330
|
+
|
|
331
|
+
// Sequential downloads
|
|
332
|
+
const results = [];
|
|
333
|
+
for (const obj of objectList) {
|
|
334
|
+
const r = await this.processObject(
|
|
335
|
+
obj,
|
|
336
|
+
client,
|
|
337
|
+
libraryId,
|
|
338
|
+
format,
|
|
339
|
+
offering,
|
|
340
|
+
targetDir,
|
|
341
|
+
failedDownloads
|
|
342
|
+
);
|
|
343
|
+
results.push(r);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Summary
|
|
347
|
+
this.logger.log("\n=== SUMMARY ===");
|
|
348
|
+
this.logger.log(`Processed: ${objectList.length}`);
|
|
349
|
+
this.logger.log(`Successful: ${results.length - failedDownloads.length}`);
|
|
350
|
+
this.logger.log(`Failed: ${failedDownloads.length}`);
|
|
351
|
+
|
|
352
|
+
if (failedDownloads.length > 0) {
|
|
353
|
+
this.logger.warn("\n=== FAILED DOWNLOADS ===");
|
|
354
|
+
this.logger.logTable({ list: failedDownloads });
|
|
355
|
+
|
|
356
|
+
if (this.args.failLog) {
|
|
357
|
+
const failPath = path.resolve(this.args.failLog);
|
|
358
|
+
fs.writeFileSync(failPath, JSON.stringify(failedDownloads, null, 2));
|
|
359
|
+
this.logger.warn(`Failures written to: ${failPath}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { results, failedDownloads };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (require.main === module) {
|
|
368
|
+
Utility.cmdLineInvoke(LibraryDownloadMp4);
|
|
369
|
+
} else {
|
|
370
|
+
module.exports = LibraryDownloadMp4;
|
|
371
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Utility that accepts an object ID and generates a download URL
|
|
2
|
+
|
|
3
|
+
const { NewOpt } = require("./lib/options");
|
|
4
|
+
const Utility = require("./lib/Utility");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const https = require("https");
|
|
8
|
+
|
|
9
|
+
const ArgOutfile = require("./lib/concerns/ArgOutfile");
|
|
10
|
+
const ExistObj = require("./lib/concerns/ExistObj");
|
|
11
|
+
const FabricObject = require("./lib/concerns/FabricObject");
|
|
12
|
+
const DownloadFile = require("./lib/downloadFile");
|
|
13
|
+
|
|
14
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
const sanitizeFilename = (name, fallback) => {
|
|
16
|
+
if (!name) return fallback;
|
|
17
|
+
return name
|
|
18
|
+
.replace(/\s+/g, "_")
|
|
19
|
+
.replace(/\//g, "_")
|
|
20
|
+
.replace(/ - /g, "-")
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class MezDownloadMp4 extends Utility {
|
|
24
|
+
blueprint() {
|
|
25
|
+
return {
|
|
26
|
+
concerns: [ArgOutfile, ExistObj, FabricObject],
|
|
27
|
+
options: [
|
|
28
|
+
NewOpt("versionHash", {
|
|
29
|
+
descTemplate: "specific versionHash to download (optional)",
|
|
30
|
+
type: "string"
|
|
31
|
+
}),
|
|
32
|
+
NewOpt("offering", {
|
|
33
|
+
descTemplate: "Offering name to use, defaults to 'default'",
|
|
34
|
+
type: "string"
|
|
35
|
+
}),
|
|
36
|
+
NewOpt("format", {
|
|
37
|
+
descTemplate: "Format to request (default mp4)",
|
|
38
|
+
type: "string"
|
|
39
|
+
}),
|
|
40
|
+
NewOpt("downloadDir", {
|
|
41
|
+
descTemplate: "Directory to save downloaded file",
|
|
42
|
+
type: "string"
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
header() {
|
|
49
|
+
return `Download file for object ${this.args.objectId}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async body() {
|
|
53
|
+
const { libraryId, objectId } = await this.concerns.ExistObj.argsProc();
|
|
54
|
+
const client = await this.concerns.Client.get();
|
|
55
|
+
|
|
56
|
+
// -----------------------
|
|
57
|
+
// Determine versionHash
|
|
58
|
+
// -----------------------
|
|
59
|
+
const versionHash = await this.concerns.FabricObject.latestVersionHash({
|
|
60
|
+
libraryId,
|
|
61
|
+
objectId
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const offeringName = this.args.offering || "default";
|
|
65
|
+
const format = this.args.format || "mp4";
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Start media download job
|
|
69
|
+
const response = await client.MakeFileServiceRequest({
|
|
70
|
+
versionHash,
|
|
71
|
+
path: "/call/media/files",
|
|
72
|
+
method: "POST",
|
|
73
|
+
body: {
|
|
74
|
+
format,
|
|
75
|
+
offering: offeringName
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const jobId = response.job_id;
|
|
80
|
+
|
|
81
|
+
// Poll job progress
|
|
82
|
+
let status;
|
|
83
|
+
do {
|
|
84
|
+
await sleep(2000);
|
|
85
|
+
status = await client.MakeFileServiceRequest({
|
|
86
|
+
versionHash,
|
|
87
|
+
path: `/call/media/files/${jobId}`
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.logger.log(`${(status?.progress || 0).toFixed(1)} / 100`);
|
|
91
|
+
} while (status?.status !== "completed");
|
|
92
|
+
|
|
93
|
+
// Clean filename
|
|
94
|
+
const filename = sanitizeFilename(status.filename, "file_download");
|
|
95
|
+
|
|
96
|
+
// Build download URL
|
|
97
|
+
const downloadUrl = await client.FabricUrl({
|
|
98
|
+
versionHash,
|
|
99
|
+
call: `/media/files/${jobId}/download`,
|
|
100
|
+
service: "files",
|
|
101
|
+
queryParams: {
|
|
102
|
+
"header-x_set_content_disposition": `attachment; filename=${filename}`
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const output = {
|
|
107
|
+
libraryId,
|
|
108
|
+
objectId,
|
|
109
|
+
versionHash,
|
|
110
|
+
jobId,
|
|
111
|
+
filename,
|
|
112
|
+
downloadUrl
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Display
|
|
116
|
+
this.logger.log("\n=== Download Info ===\n");
|
|
117
|
+
this.logger.logTable([{
|
|
118
|
+
"Version Hash": output.versionHash,
|
|
119
|
+
"Filename": output.filename
|
|
120
|
+
}]);
|
|
121
|
+
this.logger.log("Download URL:\n", output.downloadUrl, "\n");
|
|
122
|
+
|
|
123
|
+
// -----------------------
|
|
124
|
+
// HTTPS download with progress bar
|
|
125
|
+
// -----------------------
|
|
126
|
+
if (downloadUrl) {
|
|
127
|
+
|
|
128
|
+
const targetDir = this.args.downloadDir
|
|
129
|
+
? path.resolve(this.args.downloadDir)
|
|
130
|
+
: process.cwd();
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(targetDir)) {
|
|
133
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const outputFile = path.join(targetDir, output.filename || "download.mp4");
|
|
137
|
+
|
|
138
|
+
this.logger.log(`Downloading via https → ${outputFile}\n`);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await DownloadFile({
|
|
142
|
+
url: downloadUrl,
|
|
143
|
+
dest: outputFile,
|
|
144
|
+
logger: this.logger,
|
|
145
|
+
maxRedirects: 5,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.logger.log(`\nDownload complete: ${outputFile}`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
this.logger.error("\nHTTPS download failed:", err.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -----------------------
|
|
155
|
+
// Outfile support
|
|
156
|
+
// -----------------------
|
|
157
|
+
if (this.args.outfile) {
|
|
158
|
+
if (this.args.json) {
|
|
159
|
+
this.concerns.ArgOutfile.writeJson({ obj: output });
|
|
160
|
+
} else {
|
|
161
|
+
this.concerns.ArgOutfile.writeTable({ list: [output] });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.logger.error(error);
|
|
167
|
+
this.logger.error(JSON.stringify(error, null, 2));
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (require.main === module) {
|
|
174
|
+
Utility.cmdLineInvoke(MezDownloadMp4);
|
|
175
|
+
} else {
|
|
176
|
+
module.exports = MezDownloadMp4;
|
|
177
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Download a file over HTTPS with redirect handling and progress reporting.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} params
|
|
8
|
+
* @param {string} params.url - Initial download URL
|
|
9
|
+
* @param {string} params.dest - Destination file path
|
|
10
|
+
* @param {Object} [params.logger=console] - Logger with log/error methods
|
|
11
|
+
* @param {number} [params.maxRedirects=5] - Maximum redirect depth
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
const DownloadFile = ({
|
|
15
|
+
url,
|
|
16
|
+
dest,
|
|
17
|
+
logger = console,
|
|
18
|
+
maxRedirects = 5
|
|
19
|
+
}) => {
|
|
20
|
+
const download = (currentUrl, redirects = 0) =>
|
|
21
|
+
new Promise((resolve, reject) => {
|
|
22
|
+
if (redirects > maxRedirects) {
|
|
23
|
+
return reject(new Error("Too many redirects"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
https.get(currentUrl, res => {
|
|
27
|
+
|
|
28
|
+
// Handle redirects
|
|
29
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
30
|
+
const nextUrl = res.headers.location.startsWith("http")
|
|
31
|
+
? res.headers.location
|
|
32
|
+
: new URL(res.headers.location, currentUrl).href;
|
|
33
|
+
|
|
34
|
+
logger.log(`Redirected → ${nextUrl}`);
|
|
35
|
+
return resolve(download(nextUrl, redirects + 1));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (res.statusCode !== 200) {
|
|
39
|
+
return reject(new Error(`Download failed (HTTP ${res.statusCode})`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const totalSize = parseInt(res.headers["content-length"] || "0", 10);
|
|
43
|
+
let downloaded = 0;
|
|
44
|
+
|
|
45
|
+
const writeStream = fs.createWriteStream(dest);
|
|
46
|
+
|
|
47
|
+
// Progress reporting
|
|
48
|
+
res.on("data", chunk => {
|
|
49
|
+
downloaded += chunk.length;
|
|
50
|
+
|
|
51
|
+
if (totalSize > 0) {
|
|
52
|
+
const percent = (downloaded / totalSize) * 100;
|
|
53
|
+
const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(2);
|
|
54
|
+
const mbTotal = (totalSize / (1024 * 1024)).toFixed(2);
|
|
55
|
+
|
|
56
|
+
const barLength = 30;
|
|
57
|
+
const filled = Math.round((percent / 100) * barLength);
|
|
58
|
+
const bar = "█".repeat(filled) + "░".repeat(barLength - filled);
|
|
59
|
+
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
`\r${bar} ${percent.toFixed(1)}% (${mbDownloaded} MB / ${mbTotal} MB)`
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
`\rDownloaded ${(downloaded / (1024 * 1024)).toFixed(2)} MB`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
res.on("end", () => process.stdout.write("\n"));
|
|
71
|
+
|
|
72
|
+
res.pipe(writeStream);
|
|
73
|
+
|
|
74
|
+
writeStream.on("finish", () => {
|
|
75
|
+
writeStream.close(resolve);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
writeStream.on("error", err => {
|
|
79
|
+
fs.unlink(dest, () => reject(err));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
}).on("error", reject);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return download(url);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = DownloadFile;
|