@nekosuneprojects/nekosunevrtools 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js ADDED
@@ -0,0 +1,550 @@
1
+ const path = require("path");
2
+ const axios = require("axios");
3
+ const { UploadClient, EarningsClient, shortenerPresets } = require("./index");
4
+
5
+ async function runCli(argv = process.argv.slice(2)) {
6
+ const parsed = parseArgs(argv);
7
+
8
+ if (parsed.help || !parsed.command) {
9
+ printHelp();
10
+ return 0;
11
+ }
12
+
13
+ if (parsed.command === "upload") {
14
+ return runUploadCommand(parsed);
15
+ }
16
+ if (parsed.command === "shorten") {
17
+ return runShortenCommand(parsed);
18
+ }
19
+ if (parsed.command === "upload-shorten") {
20
+ return runUploadShortenCommand(parsed);
21
+ }
22
+ if (parsed.command === "video-upload") {
23
+ return runVideoUploadCommand(parsed);
24
+ }
25
+ if (parsed.command === "list-shorteners") {
26
+ process.stdout.write(`${Object.keys(shortenerPresets).join("\n")}\n`);
27
+ return 0;
28
+ }
29
+
30
+ printHelp();
31
+ return 1;
32
+ }
33
+
34
+ async function runUploadCommand(parsed) {
35
+ const files = parsed.files;
36
+ if (files.length === 0) {
37
+ throw new Error("No file path provided. Use --file <path>.");
38
+ }
39
+
40
+ const platform = parsed.platform || "upfiles";
41
+ const concurrency = Math.max(1, parsed.parallel || 1);
42
+ const retries = Math.max(0, parsed.retries || 0);
43
+ const timeoutMs = parsed.timeoutMs || 60000;
44
+ const metadata = parsed.metadata;
45
+ const apiKey = parsed.apiKey || null;
46
+
47
+ const client = new UploadClient({
48
+ platform,
49
+ apiKey,
50
+ timeoutMs
51
+ });
52
+
53
+ const discord = parsed.discordWebhook ? new DiscordProgressReporter(parsed.discordWebhook, parsed.discordTitle) : null;
54
+ const tasks = files.map((filePath) => () => uploadWithRetries({
55
+ client,
56
+ filePath,
57
+ platform,
58
+ retries,
59
+ apiKey,
60
+ metadata,
61
+ discord
62
+ }));
63
+
64
+ const results = await runConcurrently(tasks, concurrency);
65
+ return printResults(results, parsed.json);
66
+ }
67
+
68
+ async function runShortenCommand(parsed) {
69
+ if (!parsed.url) {
70
+ throw new Error("Missing --url for shorten command.");
71
+ }
72
+ const client = new EarningsClient({
73
+ timeoutMs: parsed.timeoutMs || 60000
74
+ });
75
+ const result = await client.shortenUrl(parsed.url, {
76
+ provider: "adlinkfly-compatible",
77
+ apiKey: parsed.apiKey || null,
78
+ preset: parsed.shortenerPreset || null,
79
+ baseUrl: parsed.baseUrl || null,
80
+ alias: parsed.alias || null,
81
+ adsType: Number.isFinite(parsed.adsType) ? parsed.adsType : null
82
+ });
83
+
84
+ if (parsed.json) {
85
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
86
+ } else {
87
+ process.stdout.write(`${result.shortUrl}\n`);
88
+ }
89
+ return 0;
90
+ }
91
+
92
+ async function runUploadShortenCommand(parsed) {
93
+ const files = parsed.files;
94
+ if (files.length === 0) {
95
+ throw new Error("No file path provided. Use --file <path>.");
96
+ }
97
+ if (!parsed.apiKey && (parsed.uploadPlatform || parsed.platform || "upfiles") === "upfiles") {
98
+ throw new Error("Missing --apikey for upfiles upload.");
99
+ }
100
+ if (!parsed.shortenerApiKey && !parsed.apiKey) {
101
+ throw new Error("Missing shortener API key. Use --shortener-apikey or --apikey.");
102
+ }
103
+
104
+ const earnings = new EarningsClient({
105
+ timeoutMs: parsed.timeoutMs || 60000,
106
+ upload: {
107
+ platform: parsed.uploadPlatform || parsed.platform || "upfiles",
108
+ apiKey: parsed.apiKey || null
109
+ }
110
+ });
111
+ const discord = parsed.discordWebhook ? new DiscordProgressReporter(parsed.discordWebhook, parsed.discordTitle) : null;
112
+
113
+ const tasks = files.map((filePath) => async () => {
114
+ try {
115
+ const fileName = path.basename(filePath);
116
+ const result = await earnings.uploadAndShorten(filePath, {
117
+ uploadPlatform: parsed.uploadPlatform || parsed.platform || "upfiles",
118
+ uploadApiKey: parsed.apiKey || null,
119
+ uploadMetadata: parsed.metadata,
120
+ shortenerProvider: "adlinkfly-compatible",
121
+ shortenerApiKey: parsed.shortenerApiKey || parsed.apiKey || null,
122
+ shortenerPreset: parsed.shortenerPreset || null,
123
+ shortenerBaseUrl: parsed.baseUrl || null,
124
+ alias: parsed.alias || null,
125
+ adsType: Number.isFinite(parsed.adsType) ? parsed.adsType : null,
126
+ onProgress: createProgressHandler({
127
+ fileName,
128
+ attempt: 1,
129
+ retries: 0,
130
+ discord
131
+ })
132
+ });
133
+ if (discord) {
134
+ await discord.complete(fileName, result.shortlink.shortUrl);
135
+ }
136
+ return {
137
+ ok: true,
138
+ file: filePath,
139
+ result
140
+ };
141
+ } catch (error) {
142
+ const message = error && error.message ? error.message : String(error);
143
+ if (discord) {
144
+ await discord.fail(path.basename(filePath), message);
145
+ }
146
+ return {
147
+ ok: false,
148
+ file: filePath,
149
+ error: message
150
+ };
151
+ }
152
+ });
153
+
154
+ const results = await runConcurrently(tasks, Math.max(1, parsed.parallel || 1));
155
+ return printResults(results, parsed.json);
156
+ }
157
+
158
+ async function runVideoUploadCommand(parsed) {
159
+ const files = parsed.files;
160
+ if (files.length === 0) {
161
+ throw new Error("No file path provided. Use --file <path>.");
162
+ }
163
+ if (!parsed.apiKey) {
164
+ throw new Error("Missing --apikey for video upload provider.");
165
+ }
166
+
167
+ const provider = parsed.videoProvider || "doodstream";
168
+ const earnings = new EarningsClient({
169
+ timeoutMs: parsed.timeoutMs || 60000
170
+ });
171
+ const discord = parsed.discordWebhook ? new DiscordProgressReporter(parsed.discordWebhook, parsed.discordTitle) : null;
172
+
173
+ const tasks = files.map((filePath) => async () => {
174
+ try {
175
+ const fileName = path.basename(filePath);
176
+ const result = await earnings.uploadVideo(filePath, {
177
+ provider,
178
+ apiKey: parsed.apiKey,
179
+ onProgress: createProgressHandler({
180
+ fileName,
181
+ attempt: 1,
182
+ retries: 0,
183
+ discord
184
+ })
185
+ });
186
+ if (discord) {
187
+ await discord.complete(fileName, result.watchUrl);
188
+ }
189
+ return {
190
+ ok: true,
191
+ file: filePath,
192
+ result
193
+ };
194
+ } catch (error) {
195
+ const message = error && error.message ? error.message : String(error);
196
+ if (discord) {
197
+ await discord.fail(path.basename(filePath), message);
198
+ }
199
+ return {
200
+ ok: false,
201
+ file: filePath,
202
+ error: message
203
+ };
204
+ }
205
+ });
206
+
207
+ const results = await runConcurrently(tasks, Math.max(1, parsed.parallel || 1));
208
+ return printResults(results, parsed.json);
209
+ }
210
+
211
+ function printResults(results, asJson) {
212
+ const success = results.filter((item) => item.ok).length;
213
+ const failed = results.length - success;
214
+ if (asJson) {
215
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
216
+ } else {
217
+ for (const result of results) {
218
+ if (result.ok) {
219
+ const finalUrl = getResultUrl(result.result);
220
+ process.stdout.write(`[ok] ${result.file} -> ${finalUrl}\n`);
221
+ } else {
222
+ process.stdout.write(`[fail] ${result.file} -> ${result.error}\n`);
223
+ }
224
+ }
225
+ process.stdout.write(`Done: ${success} success, ${failed} failed.\n`);
226
+ }
227
+ return failed > 0 ? 1 : 0;
228
+ }
229
+
230
+ function getResultUrl(result) {
231
+ if (!result) {
232
+ return "";
233
+ }
234
+ if (result.url) {
235
+ return result.url;
236
+ }
237
+ if (result.watchUrl) {
238
+ return result.watchUrl;
239
+ }
240
+ if (result.shortlink && result.shortlink.shortUrl) {
241
+ return result.shortlink.shortUrl;
242
+ }
243
+ return "";
244
+ }
245
+
246
+ async function uploadWithRetries(params) {
247
+ const { client, filePath, platform, retries, apiKey, metadata, discord } = params;
248
+ let attempt = 0;
249
+ while (attempt <= retries) {
250
+ attempt += 1;
251
+ try {
252
+ const fileName = path.basename(filePath);
253
+ const progressHandler = createProgressHandler({ fileName, attempt, retries, discord });
254
+
255
+ const result = await client.upload(filePath, {
256
+ platform,
257
+ apiKey,
258
+ metadata,
259
+ onProgress: progressHandler
260
+ });
261
+ await maybeReportComplete(discord, fileName, result.url);
262
+ return {
263
+ ok: true,
264
+ file: filePath,
265
+ attempts: attempt,
266
+ result
267
+ };
268
+ } catch (error) {
269
+ const message = error && error.message ? error.message : String(error);
270
+ if (attempt > retries) {
271
+ await maybeReportFail(discord, path.basename(filePath), message);
272
+ return {
273
+ ok: false,
274
+ file: filePath,
275
+ attempts: attempt,
276
+ error: message
277
+ };
278
+ }
279
+ process.stdout.write(`[retry] ${filePath} attempt ${attempt}/${retries + 1}: ${message}\n`);
280
+ }
281
+ }
282
+ }
283
+
284
+ function createProgressHandler({ fileName, attempt, retries, discord }) {
285
+ let lastPercent = -1;
286
+ return (progress) => {
287
+ const percent = typeof progress.percent === "number" ? progress.percent : 0;
288
+ if (percent !== lastPercent && (percent % 5 === 0 || percent === 100)) {
289
+ process.stdout.write(`[${fileName}] ${percent}% (attempt ${attempt}/${retries + 1})\n`);
290
+ lastPercent = percent;
291
+ }
292
+ if (discord) {
293
+ discord.update(fileName, percent, progress.phase || "uploading").catch(() => {});
294
+ }
295
+ };
296
+ }
297
+
298
+ async function maybeReportComplete(discord, fileName, url) {
299
+ if (!discord) {
300
+ return;
301
+ }
302
+ await discord.complete(fileName, url);
303
+ }
304
+
305
+ async function maybeReportFail(discord, fileName, message) {
306
+ if (!discord) {
307
+ return;
308
+ }
309
+ await discord.fail(fileName, message);
310
+ }
311
+
312
+ class DiscordProgressReporter {
313
+ constructor(webhookUrl, title) {
314
+ this.webhookUrl = webhookUrl;
315
+ this.title = title || "Upload Progress";
316
+ this.messages = new Map();
317
+ this.lastUpdate = new Map();
318
+ }
319
+
320
+ async update(fileName, percent, phase) {
321
+ const now = Date.now();
322
+ const key = fileName;
323
+ const last = this.lastUpdate.get(key) || 0;
324
+ if (percent !== 100 && now - last < 3000 && percent % 10 !== 0) {
325
+ return;
326
+ }
327
+ this.lastUpdate.set(key, now);
328
+
329
+ const embed = {
330
+ title: this.title,
331
+ description: `${fileName}\n${buildProgressBar(percent)} ${percent}%`,
332
+ color: 0x3498db,
333
+ fields: [
334
+ { name: "Status", value: phase || "uploading", inline: true }
335
+ ],
336
+ timestamp: new Date().toISOString()
337
+ };
338
+
339
+ await this.upsertMessage(fileName, embed);
340
+ }
341
+
342
+ async complete(fileName, url) {
343
+ const embed = {
344
+ title: this.title,
345
+ description: `${fileName}\n${buildProgressBar(100)} 100%`,
346
+ color: 0x2ecc71,
347
+ fields: [
348
+ { name: "Status", value: "completed", inline: true },
349
+ { name: "URL", value: truncate(url, 1024), inline: false }
350
+ ],
351
+ timestamp: new Date().toISOString()
352
+ };
353
+ await this.upsertMessage(fileName, embed);
354
+ }
355
+
356
+ async fail(fileName, message) {
357
+ const embed = {
358
+ title: this.title,
359
+ description: `${fileName}\n${buildProgressBar(0)} 0%`,
360
+ color: 0xe74c3c,
361
+ fields: [
362
+ { name: "Status", value: "failed", inline: true },
363
+ { name: "Error", value: truncate(message, 1024), inline: false }
364
+ ],
365
+ timestamp: new Date().toISOString()
366
+ };
367
+ await this.upsertMessage(fileName, embed);
368
+ }
369
+
370
+ async upsertMessage(fileName, embed) {
371
+ const messageId = this.messages.get(fileName);
372
+ if (!messageId) {
373
+ const response = await axios.post(`${this.webhookUrl}?wait=true`, { embeds: [embed] });
374
+ const createdId = response && response.data && response.data.id;
375
+ if (createdId) {
376
+ this.messages.set(fileName, createdId);
377
+ }
378
+ return;
379
+ }
380
+ await axios.patch(`${this.webhookUrl}/messages/${messageId}`, { embeds: [embed] });
381
+ }
382
+ }
383
+
384
+ function buildProgressBar(percent) {
385
+ const clamped = Math.max(0, Math.min(100, Math.floor(percent)));
386
+ const size = 20;
387
+ const filled = Math.round((clamped / 100) * size);
388
+ return `[${"#".repeat(filled)}${"-".repeat(size - filled)}]`;
389
+ }
390
+
391
+ function truncate(text, max) {
392
+ const value = String(text || "");
393
+ if (value.length <= max) {
394
+ return value;
395
+ }
396
+ return `${value.slice(0, max - 3)}...`;
397
+ }
398
+
399
+ async function runConcurrently(tasks, concurrency) {
400
+ const results = [];
401
+ let index = 0;
402
+ const workers = Array.from({ length: concurrency }, async () => {
403
+ while (index < tasks.length) {
404
+ const taskIndex = index;
405
+ index += 1;
406
+ results[taskIndex] = await tasks[taskIndex]();
407
+ }
408
+ });
409
+ await Promise.all(workers);
410
+ return results;
411
+ }
412
+
413
+ function parseArgs(argv) {
414
+ const firstToken = argv[0];
415
+ const hasCommand = firstToken && !String(firstToken).startsWith("-");
416
+ const result = {
417
+ command: hasCommand ? firstToken : null,
418
+ files: [],
419
+ metadata: {},
420
+ help: false,
421
+ json: false
422
+ };
423
+
424
+ for (let i = hasCommand ? 1 : 0; i < argv.length; i += 1) {
425
+ const token = argv[i];
426
+ if (token === "--help" || token === "-h") {
427
+ result.help = true;
428
+ } else if (token === "--json") {
429
+ result.json = true;
430
+ } else if (token === "--file" || token === "-f") {
431
+ const value = argv[i + 1];
432
+ i += 1;
433
+ if (value) {
434
+ result.files.push(value);
435
+ }
436
+ } else if (token === "--url") {
437
+ result.url = argv[i + 1];
438
+ i += 1;
439
+ } else if (token === "--platform" || token === "-p") {
440
+ result.platform = argv[i + 1];
441
+ i += 1;
442
+ } else if (token === "--upload-platform") {
443
+ result.uploadPlatform = argv[i + 1];
444
+ i += 1;
445
+ } else if (token === "--video-provider") {
446
+ result.videoProvider = argv[i + 1];
447
+ i += 1;
448
+ } else if (token === "--apikey" || token === "--api-key" || token === "-k") {
449
+ result.apiKey = argv[i + 1];
450
+ i += 1;
451
+ } else if (token === "--shortener-apikey") {
452
+ result.shortenerApiKey = argv[i + 1];
453
+ i += 1;
454
+ } else if (token === "--shortener-preset") {
455
+ result.shortenerPreset = argv[i + 1];
456
+ i += 1;
457
+ } else if (token === "--base-url") {
458
+ result.baseUrl = argv[i + 1];
459
+ i += 1;
460
+ } else if (token === "--alias") {
461
+ result.alias = argv[i + 1];
462
+ i += 1;
463
+ } else if (token === "--ads-type") {
464
+ result.adsType = Number(argv[i + 1]);
465
+ i += 1;
466
+ } else if (token === "--timeout") {
467
+ result.timeoutMs = Number(argv[i + 1]);
468
+ i += 1;
469
+ } else if (token === "--retry" || token === "--retries" || token === "-r") {
470
+ result.retries = Number(argv[i + 1]);
471
+ i += 1;
472
+ } else if (token === "--parallel") {
473
+ result.parallel = Number(argv[i + 1]);
474
+ i += 1;
475
+ } else if (token === "--discord-webhook") {
476
+ result.discordWebhook = argv[i + 1];
477
+ i += 1;
478
+ } else if (token === "--discord-title") {
479
+ result.discordTitle = argv[i + 1];
480
+ i += 1;
481
+ } else if (token === "--expires") {
482
+ result.metadata.expires = argv[i + 1];
483
+ i += 1;
484
+ } else if (token === "--meta") {
485
+ const pair = argv[i + 1];
486
+ i += 1;
487
+ if (pair && pair.includes("=")) {
488
+ const eqIndex = pair.indexOf("=");
489
+ const key = pair.slice(0, eqIndex);
490
+ const value = pair.slice(eqIndex + 1);
491
+ result.metadata[key] = value;
492
+ }
493
+ }
494
+ }
495
+
496
+ return result;
497
+ }
498
+
499
+ function printHelp() {
500
+ const helpText = `
501
+ upload2earn CLI
502
+
503
+ Commands:
504
+ upload Upload files
505
+ shorten Convert a URL into monetized short link
506
+ upload-shorten Upload file then monetize resulting URL
507
+ video-upload Upload video to video-earn provider
508
+ list-shorteners Show built-in shortlink preset names
509
+
510
+ Usage:
511
+ upload2earn upload --file <path> [options]
512
+ upload2earn shorten --url <url> --shortener-preset shrinkme --apikey <key>
513
+ upload2earn upload-shorten --file <path> --upload-platform upfiles --apikey <upload-key> --shortener-preset shrinkme --shortener-apikey <short-key>
514
+ upload2earn video-upload --file <video.mp4> --video-provider doodstream --apikey <key>
515
+
516
+ Shared options:
517
+ --json JSON output
518
+ --help, -h Show help
519
+
520
+ Upload options:
521
+ --platform, -p <name> Platform: upfiles|fileio|catbox|transfersh (default: upfiles)
522
+ --file, -f <path> File path (repeatable)
523
+ --apikey, --api-key, -k API key for provider
524
+ --expires <value> file.io expiration metadata (example: 1w)
525
+ --meta key=value Additional metadata field (repeatable)
526
+ --retry, --retries, -r <n> Retry count per file (default: 0)
527
+ --parallel <n> Parallel uploads (default: 1)
528
+ --timeout <ms> Request timeout in milliseconds (default: 60000)
529
+
530
+ Short-link options:
531
+ --url <url> URL to shorten
532
+ --shortener-preset <name> Preset from list-shorteners
533
+ --base-url <url> Custom AdLinkFly-compatible API base URL
534
+ --shortener-apikey <key> API key for shortener (or reuse --apikey)
535
+ --alias <value> Custom short alias
536
+ --ads-type <1|2> 1=interstitial, 2=banner for many AdLinkFly APIs
537
+
538
+ Video-earn options:
539
+ --video-provider <name> Currently: doodstream
540
+
541
+ Discord progress options:
542
+ --discord-webhook <url> Discord webhook URL for embed progress updates
543
+ --discord-title <text> Discord embed title (default: Upload Progress)
544
+ `;
545
+ process.stdout.write(helpText);
546
+ }
547
+
548
+ module.exports = {
549
+ runCli
550
+ };
@@ -0,0 +1,128 @@
1
+ const axios = require("axios");
2
+ const { UploadClient } = require("./upload/client");
3
+ const { providers: shorturlProviders, presets: shortenerPresets } = require("./shorturl/providers");
4
+ const videoProviders = require("./video/providers");
5
+
6
+ class EarningsClient {
7
+ constructor(options = {}) {
8
+ this.timeoutMs = options.timeoutMs || 60000;
9
+ this.uploadClient = options.uploadClient || new UploadClient(options.upload || {});
10
+ this.shorteners = { ...shorturlProviders };
11
+ this.videos = { ...videoProviders };
12
+ }
13
+
14
+ registerShortener(name, adapter) {
15
+ if (!name || typeof name !== "string") {
16
+ throw new Error("Shortener name must be a non-empty string.");
17
+ }
18
+ if (!adapter || typeof adapter.shorten !== "function") {
19
+ throw new Error("Shortener adapter must provide shorten(context).");
20
+ }
21
+ this.shorteners[name] = adapter;
22
+ return this;
23
+ }
24
+
25
+ registerVideoProvider(name, adapter) {
26
+ if (!name || typeof name !== "string") {
27
+ throw new Error("Video provider name must be a non-empty string.");
28
+ }
29
+ if (!adapter || typeof adapter.upload !== "function") {
30
+ throw new Error("Video adapter must provide upload(context).");
31
+ }
32
+ this.videos[name] = adapter;
33
+ return this;
34
+ }
35
+
36
+ async shortenUrl(longUrl, options = {}) {
37
+ if (!longUrl || typeof longUrl !== "string") {
38
+ throw new Error("longUrl is required and must be a string.");
39
+ }
40
+
41
+ const providerName = options.provider || "adlinkfly-compatible";
42
+ const adapter = this.shorteners[providerName];
43
+ if (!adapter) {
44
+ throw new Error(`Unsupported shortener provider "${providerName}".`);
45
+ }
46
+
47
+ const resolved = resolveShortenerOptions(providerName, options);
48
+ return adapter.shorten({
49
+ longUrl,
50
+ apiKey: options.apiKey || null,
51
+ options: resolved,
52
+ http: axios.create({ timeout: options.timeoutMs || this.timeoutMs })
53
+ });
54
+ }
55
+
56
+ async uploadAndShorten(filePath, options = {}) {
57
+ const uploadResult = await this.uploadClient.upload(filePath, {
58
+ platform: options.uploadPlatform,
59
+ apiKey: options.uploadApiKey,
60
+ metadata: options.uploadMetadata,
61
+ headers: options.uploadHeaders,
62
+ timeoutMs: options.timeoutMs || this.timeoutMs,
63
+ onProgress: options.onProgress
64
+ });
65
+
66
+ const shortResult = await this.shortenUrl(uploadResult.url, {
67
+ provider: options.shortenerProvider || "adlinkfly-compatible",
68
+ apiKey: options.shortenerApiKey,
69
+ preset: options.shortenerPreset,
70
+ baseUrl: options.shortenerBaseUrl,
71
+ alias: options.alias,
72
+ adsType: options.adsType,
73
+ responseFormat: options.responseFormat,
74
+ timeoutMs: options.timeoutMs || this.timeoutMs
75
+ });
76
+
77
+ return {
78
+ success: true,
79
+ upload: uploadResult,
80
+ shortlink: shortResult
81
+ };
82
+ }
83
+
84
+ async uploadVideo(filePath, options = {}) {
85
+ const provider = options.provider || "doodstream";
86
+ const adapter = this.videos[provider];
87
+ if (!adapter) {
88
+ throw new Error(`Unsupported video provider "${provider}".`);
89
+ }
90
+
91
+ return adapter.upload({
92
+ filePath,
93
+ apiKey: options.apiKey || null,
94
+ metadata: options.metadata || {},
95
+ onProgress: options.onProgress,
96
+ http: axios.create({ timeout: options.timeoutMs || this.timeoutMs })
97
+ });
98
+ }
99
+ }
100
+
101
+ function resolveShortenerOptions(provider, options) {
102
+ if (provider !== "adlinkfly-compatible") {
103
+ return options;
104
+ }
105
+
106
+ let baseUrl = options.baseUrl || null;
107
+ if (!baseUrl && options.preset) {
108
+ baseUrl = shortenerPresets[options.preset] || null;
109
+ }
110
+ if (!baseUrl) {
111
+ throw new Error(
112
+ `Missing shortener base URL. Use "baseUrl" or "preset". Presets: ${Object.keys(shortenerPresets).join(", ")}`
113
+ );
114
+ }
115
+
116
+ return {
117
+ provider,
118
+ baseUrl,
119
+ alias: options.alias || null,
120
+ adsType: typeof options.adsType === "number" ? options.adsType : null,
121
+ responseFormat: options.responseFormat || "json"
122
+ };
123
+ }
124
+
125
+ module.exports = {
126
+ EarningsClient,
127
+ shortenerPresets
128
+ };