vidup 1.0.12 → 1.0.14
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/bin/cli.js +273 -4
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.js +10 -1
- package/dist/sync-CCKOz_Uf.js +543 -0
- package/package.json +22 -21
- package/dist/sync-DAKTIkq2.js +0 -7
package/dist/bin/cli.js
CHANGED
|
@@ -1,5 +1,274 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import yargs from 'yargs';
|
|
5
|
+
import { hideBin } from 'yargs/helpers';
|
|
6
|
+
import { l as log, s as stripVideoMetadataInDirectory, a as syncVideoInDirectory } from '../sync-CCKOz_Uf.js';
|
|
7
|
+
import 'bunnycdn-stream';
|
|
8
|
+
import 'hasha';
|
|
9
|
+
import 'node:fs';
|
|
10
|
+
import 'node:fs/promises';
|
|
11
|
+
import 'node:url';
|
|
12
|
+
import 'ora';
|
|
13
|
+
import 'execa';
|
|
14
|
+
import 'ffmpeg-static';
|
|
15
|
+
|
|
16
|
+
const toZeroIfInfinity = (value) => Number.isFinite(value) ? value : 0;
|
|
17
|
+
function parseNumber(milliseconds) {
|
|
18
|
+
return {
|
|
19
|
+
days: Math.trunc(milliseconds / 864e5),
|
|
20
|
+
hours: Math.trunc(milliseconds / 36e5 % 24),
|
|
21
|
+
minutes: Math.trunc(milliseconds / 6e4 % 60),
|
|
22
|
+
seconds: Math.trunc(milliseconds / 1e3 % 60),
|
|
23
|
+
milliseconds: Math.trunc(milliseconds % 1e3),
|
|
24
|
+
microseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1e3) % 1e3),
|
|
25
|
+
nanoseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1e6) % 1e3)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function parseBigint(milliseconds) {
|
|
29
|
+
return {
|
|
30
|
+
days: milliseconds / 86400000n,
|
|
31
|
+
hours: milliseconds / 3600000n % 24n,
|
|
32
|
+
minutes: milliseconds / 60000n % 60n,
|
|
33
|
+
seconds: milliseconds / 1000n % 60n,
|
|
34
|
+
milliseconds: milliseconds % 1000n,
|
|
35
|
+
microseconds: 0n,
|
|
36
|
+
nanoseconds: 0n
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function parseMilliseconds(milliseconds) {
|
|
40
|
+
switch (typeof milliseconds) {
|
|
41
|
+
case "number": {
|
|
42
|
+
if (Number.isFinite(milliseconds)) {
|
|
43
|
+
return parseNumber(milliseconds);
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "bigint": {
|
|
48
|
+
return parseBigint(milliseconds);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new TypeError("Expected a finite number or bigint");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isZero = (value) => value === 0 || value === 0n;
|
|
55
|
+
const pluralize = (word, count) => count === 1 || count === 1n ? word : `${word}s`;
|
|
56
|
+
const SECOND_ROUNDING_EPSILON = 1e-7;
|
|
57
|
+
const ONE_DAY_IN_MILLISECONDS = 24n * 60n * 60n * 1000n;
|
|
58
|
+
function prettyMilliseconds(milliseconds, options) {
|
|
59
|
+
const isBigInt = typeof milliseconds === "bigint";
|
|
60
|
+
if (!isBigInt && !Number.isFinite(milliseconds)) {
|
|
61
|
+
throw new TypeError("Expected a finite number or bigint");
|
|
62
|
+
}
|
|
63
|
+
options = { ...options };
|
|
64
|
+
const sign = milliseconds < 0 ? "-" : "";
|
|
65
|
+
milliseconds = milliseconds < 0 ? -milliseconds : milliseconds;
|
|
66
|
+
if (options.colonNotation) {
|
|
67
|
+
options.compact = false;
|
|
68
|
+
options.formatSubMilliseconds = false;
|
|
69
|
+
options.separateMilliseconds = false;
|
|
70
|
+
options.verbose = false;
|
|
71
|
+
}
|
|
72
|
+
if (options.compact) {
|
|
73
|
+
options.unitCount = 1;
|
|
74
|
+
options.secondsDecimalDigits = 0;
|
|
75
|
+
options.millisecondsDecimalDigits = 0;
|
|
76
|
+
}
|
|
77
|
+
let result = [];
|
|
78
|
+
const floorDecimals = (value, decimalDigits) => {
|
|
79
|
+
const flooredInterimValue = Math.floor(value * 10 ** decimalDigits + SECOND_ROUNDING_EPSILON);
|
|
80
|
+
const flooredValue = Math.round(flooredInterimValue) / 10 ** decimalDigits;
|
|
81
|
+
return flooredValue.toFixed(decimalDigits);
|
|
82
|
+
};
|
|
83
|
+
const add = (value, long, short, valueString) => {
|
|
84
|
+
if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short === "m")) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
valueString ??= String(value);
|
|
88
|
+
if (options.colonNotation) {
|
|
89
|
+
const wholeDigits = valueString.includes(".") ? valueString.split(".")[0].length : valueString.length;
|
|
90
|
+
const minLength = result.length > 0 ? 2 : 1;
|
|
91
|
+
valueString = "0".repeat(Math.max(0, minLength - wholeDigits)) + valueString;
|
|
92
|
+
} else {
|
|
93
|
+
valueString += options.verbose ? " " + pluralize(long, value) : short;
|
|
94
|
+
}
|
|
95
|
+
result.push(valueString);
|
|
96
|
+
};
|
|
97
|
+
const parsed = parseMilliseconds(milliseconds);
|
|
98
|
+
const days = BigInt(parsed.days);
|
|
99
|
+
if (options.hideYearAndDays) {
|
|
100
|
+
add(BigInt(days) * 24n + BigInt(parsed.hours), "hour", "h");
|
|
101
|
+
} else {
|
|
102
|
+
if (options.hideYear) {
|
|
103
|
+
add(days, "day", "d");
|
|
104
|
+
} else {
|
|
105
|
+
add(days / 365n, "year", "y");
|
|
106
|
+
add(days % 365n, "day", "d");
|
|
107
|
+
}
|
|
108
|
+
add(Number(parsed.hours), "hour", "h");
|
|
109
|
+
}
|
|
110
|
+
add(Number(parsed.minutes), "minute", "m");
|
|
111
|
+
if (!options.hideSeconds) {
|
|
112
|
+
if (options.separateMilliseconds || options.formatSubMilliseconds || !options.colonNotation && milliseconds < 1e3 && !options.subSecondsAsDecimals) {
|
|
113
|
+
const seconds = Number(parsed.seconds);
|
|
114
|
+
const milliseconds2 = Number(parsed.milliseconds);
|
|
115
|
+
const microseconds = Number(parsed.microseconds);
|
|
116
|
+
const nanoseconds = Number(parsed.nanoseconds);
|
|
117
|
+
add(seconds, "second", "s");
|
|
118
|
+
if (options.formatSubMilliseconds) {
|
|
119
|
+
add(milliseconds2, "millisecond", "ms");
|
|
120
|
+
add(microseconds, "microsecond", "\xB5s");
|
|
121
|
+
add(nanoseconds, "nanosecond", "ns");
|
|
122
|
+
} else {
|
|
123
|
+
const millisecondsAndBelow = milliseconds2 + microseconds / 1e3 + nanoseconds / 1e6;
|
|
124
|
+
const millisecondsDecimalDigits = typeof options.millisecondsDecimalDigits === "number" ? options.millisecondsDecimalDigits : 0;
|
|
125
|
+
const roundedMilliseconds = millisecondsAndBelow >= 1 ? Math.round(millisecondsAndBelow) : Math.ceil(millisecondsAndBelow);
|
|
126
|
+
const millisecondsString = millisecondsDecimalDigits ? millisecondsAndBelow.toFixed(millisecondsDecimalDigits) : roundedMilliseconds;
|
|
127
|
+
add(
|
|
128
|
+
Number.parseFloat(millisecondsString),
|
|
129
|
+
"millisecond",
|
|
130
|
+
"ms",
|
|
131
|
+
millisecondsString
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
const seconds = (isBigInt ? Number(milliseconds % ONE_DAY_IN_MILLISECONDS) : milliseconds) / 1e3 % 60;
|
|
136
|
+
const secondsDecimalDigits = typeof options.secondsDecimalDigits === "number" ? options.secondsDecimalDigits : 1;
|
|
137
|
+
const secondsFixed = floorDecimals(seconds, secondsDecimalDigits);
|
|
138
|
+
const secondsString = options.keepDecimalsOnWholeSeconds ? secondsFixed : secondsFixed.replace(/\.0+$/, "");
|
|
139
|
+
add(Number.parseFloat(secondsString), "second", "s", secondsString);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (result.length === 0) {
|
|
143
|
+
return sign + "0" + (options.verbose ? " milliseconds" : "ms");
|
|
144
|
+
}
|
|
145
|
+
const separator = options.colonNotation ? ":" : " ";
|
|
146
|
+
if (typeof options.unitCount === "number") {
|
|
147
|
+
result = result.slice(0, Math.max(options.unitCount, 1));
|
|
148
|
+
}
|
|
149
|
+
return sign + result.join(separator);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let homeDirectory;
|
|
153
|
+
let currentUser;
|
|
154
|
+
function untildify(pathWithTilde) {
|
|
155
|
+
if (typeof pathWithTilde !== "string") {
|
|
156
|
+
throw new TypeError(`Expected a string, got ${typeof pathWithTilde}`);
|
|
157
|
+
}
|
|
158
|
+
if (homeDirectory === void 0) {
|
|
159
|
+
homeDirectory = os.homedir();
|
|
160
|
+
}
|
|
161
|
+
if (homeDirectory && /^~(?=$|\/|\\)/.test(pathWithTilde)) {
|
|
162
|
+
return pathWithTilde.replace(/^~/, homeDirectory);
|
|
163
|
+
}
|
|
164
|
+
const userMatch = pathWithTilde.match(/^~([^/\\]+)(.*)/);
|
|
165
|
+
if (userMatch) {
|
|
166
|
+
if (currentUser === void 0) {
|
|
167
|
+
currentUser = os.userInfo().username;
|
|
168
|
+
}
|
|
169
|
+
if (currentUser) {
|
|
170
|
+
const username = userMatch[1];
|
|
171
|
+
const rest = userMatch[2];
|
|
172
|
+
if (username === currentUser) {
|
|
173
|
+
return homeDirectory + rest;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return pathWithTilde;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var version = "1.0.14";
|
|
181
|
+
|
|
182
|
+
const startTime = performance.now();
|
|
183
|
+
const yargsInstance = yargs(hideBin(process.argv));
|
|
184
|
+
await yargsInstance.scriptName("vidup").usage("$0 [command]", "Run a vidup command.").command(
|
|
185
|
+
["$0 <directory> [options]", "sync <directory> [options]"],
|
|
186
|
+
"Synchronize a remote video streaming service to mirror the contents of a local directory. Warning: This command will irrevocably delete remote videos that are not present in the local directory.",
|
|
187
|
+
(yargs2) => yargs2.positional("directory", {
|
|
188
|
+
default: void 0,
|
|
189
|
+
demandOption: true,
|
|
190
|
+
describe: "The path to the local directory of video files to sync.",
|
|
191
|
+
type: "string"
|
|
192
|
+
}).option("service", {
|
|
193
|
+
alias: "s",
|
|
194
|
+
choices: ["bunny", "mux", "cloudflare"],
|
|
195
|
+
demandOption: true,
|
|
196
|
+
describe: "Streaming service / platform to sync local video files to. Only the Bunny.net streaming CDN is supported at this time.",
|
|
197
|
+
type: "string"
|
|
198
|
+
}).option("key", {
|
|
199
|
+
default: void 0,
|
|
200
|
+
demandOption: true,
|
|
201
|
+
describe: "Streaming service API access key.",
|
|
202
|
+
type: "string"
|
|
203
|
+
}).option("library", {
|
|
204
|
+
default: void 0,
|
|
205
|
+
demandOption: true,
|
|
206
|
+
describe: "Streaming service library ID.",
|
|
207
|
+
type: "string"
|
|
208
|
+
}).option("strip-metadata", {
|
|
209
|
+
default: false,
|
|
210
|
+
describe: "Remove all metadata from the video files before uploading them to the streaming service. Warning: This will modify local videos in-place.",
|
|
211
|
+
type: "boolean"
|
|
212
|
+
}).option("dry-run", {
|
|
213
|
+
alias: "d",
|
|
214
|
+
default: false,
|
|
215
|
+
describe: "Perform a dry run without making any changes. Useful for testing and debugging. Pairs well with the `--json` command.",
|
|
216
|
+
type: "boolean"
|
|
217
|
+
}).option("json", {
|
|
218
|
+
default: false,
|
|
219
|
+
describe: "Output the sync report as JSON.",
|
|
220
|
+
type: "boolean"
|
|
221
|
+
}).option("verbose", {
|
|
222
|
+
default: false,
|
|
223
|
+
describe: "Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection.",
|
|
224
|
+
type: "boolean"
|
|
225
|
+
}).check((argv) => {
|
|
226
|
+
if (argv.service === "bunny") {
|
|
227
|
+
return argv;
|
|
228
|
+
}
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Sorry, vidup doesn't yet implement synchronization support the ${argv.service} streaming service. Only Bunny.net is supported at the moment. If you'd like to see support for another service, please open an issue or send a PR on GitHub: https://github.com/kitschpatrol/vidup/issues`
|
|
231
|
+
);
|
|
232
|
+
}),
|
|
233
|
+
async ({ directory, dryRun, json, key, library, service, stripMetadata, verbose }) => {
|
|
234
|
+
log.verbose = verbose;
|
|
235
|
+
const resolvedDirectory = path.resolve(untildify(directory));
|
|
236
|
+
log.info("Starting video synchronization...");
|
|
237
|
+
if (dryRun) {
|
|
238
|
+
log.warn(`Dry run enabled, not making any changes`);
|
|
239
|
+
}
|
|
240
|
+
const stripReport = stripMetadata ? await stripVideoMetadataInDirectory(resolvedDirectory, {
|
|
241
|
+
dryRun,
|
|
242
|
+
verbose
|
|
243
|
+
}) : [];
|
|
244
|
+
const syncReport = await syncVideoInDirectory(resolvedDirectory, {
|
|
245
|
+
credentials: {
|
|
246
|
+
key,
|
|
247
|
+
library
|
|
248
|
+
},
|
|
249
|
+
dryRun,
|
|
250
|
+
service,
|
|
251
|
+
verbose
|
|
252
|
+
});
|
|
253
|
+
if (json) {
|
|
254
|
+
for (const entry of syncReport) {
|
|
255
|
+
entry.stripMetadata = entry.localFile !== void 0 && stripReport.includes(entry.localFile);
|
|
256
|
+
}
|
|
257
|
+
process.stdout.write(JSON.stringify(syncReport, void 0, 2));
|
|
258
|
+
process.stdout.write("\n");
|
|
259
|
+
} else {
|
|
260
|
+
for (const entry of stripReport) {
|
|
261
|
+
process.stdout.write(`${dryRun ? "Found" : "Stripped"} metadata: "${entry}"
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
for (const entry of syncReport) {
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
`${entry.action}: "${entry.localFile}" Remote GUID: "${entry.remoteId}"
|
|
267
|
+
`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
log.info(`Synchronized video in ${prettyMilliseconds(performance.now() - startTime)}`);
|
|
272
|
+
process.exitCode = 0;
|
|
273
|
+
}
|
|
274
|
+
).demandCommand(1).alias("h", "help").version(version).alias("v", "version").help().wrap(process.stdout.isTTY ? Math.min(120, yargsInstance.terminalWidth()) : 0).parse();
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -43,4 +43,5 @@ declare const log: {
|
|
|
43
43
|
errorPrefixed(prefix: string, ...data: unknown[]): void;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
export {
|
|
46
|
+
export { log, stripVideoMetadataInDirectory, syncVideoInDirectory };
|
|
47
|
+
export type { Service, StripOptions, SyncOptions, SyncReport };
|
package/dist/lib/index.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
export { l as log, s as stripVideoMetadataInDirectory, a as syncVideoInDirectory } from '../sync-CCKOz_Uf.js';
|
|
2
|
+
import 'bunnycdn-stream';
|
|
3
|
+
import 'hasha';
|
|
4
|
+
import 'node:fs';
|
|
5
|
+
import 'node:fs/promises';
|
|
6
|
+
import 'node:path';
|
|
7
|
+
import 'node:url';
|
|
8
|
+
import 'ora';
|
|
9
|
+
import 'execa';
|
|
10
|
+
import 'ffmpeg-static';
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { BunnyCdnStream } from 'bunnycdn-stream';
|
|
2
|
+
import { hash } from 'hasha';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import fs, { writeFile, rename, readFile } from 'node:fs/promises';
|
|
5
|
+
import path, { join, dirname, basename } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { oraPromise } from 'ora';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import ffmpegPath from 'ffmpeg-static';
|
|
10
|
+
|
|
11
|
+
function getDefaultExportFromCjs (x) {
|
|
12
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var picocolors = {exports: {}};
|
|
16
|
+
|
|
17
|
+
var hasRequiredPicocolors;
|
|
18
|
+
|
|
19
|
+
function requirePicocolors () {
|
|
20
|
+
if (hasRequiredPicocolors) return picocolors.exports;
|
|
21
|
+
hasRequiredPicocolors = 1;
|
|
22
|
+
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
23
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
24
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
25
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
26
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
27
|
+
};
|
|
28
|
+
let replaceClose = (string, close, replace, index) => {
|
|
29
|
+
let result = "", cursor = 0;
|
|
30
|
+
do {
|
|
31
|
+
result += string.substring(cursor, index) + replace;
|
|
32
|
+
cursor = index + close.length;
|
|
33
|
+
index = string.indexOf(close, cursor);
|
|
34
|
+
} while (~index);
|
|
35
|
+
return result + string.substring(cursor);
|
|
36
|
+
};
|
|
37
|
+
let createColors = (enabled = isColorSupported) => {
|
|
38
|
+
let f = enabled ? formatter : () => String;
|
|
39
|
+
return {
|
|
40
|
+
isColorSupported: enabled,
|
|
41
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
42
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
43
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
44
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
45
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
46
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
47
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
48
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
49
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
50
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
51
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
52
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
53
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
54
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
55
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
56
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
57
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
58
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
59
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
60
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
61
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
62
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
63
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
64
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
65
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
66
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
67
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
68
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
69
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
70
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
71
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
72
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
73
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
74
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
75
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
76
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
77
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
78
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
79
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
80
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
81
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
picocolors.exports = createColors();
|
|
85
|
+
picocolors.exports.createColors = createColors;
|
|
86
|
+
return picocolors.exports;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var picocolorsExports = /*@__PURE__*/ requirePicocolors();
|
|
90
|
+
var pc = /*@__PURE__*/getDefaultExportFromCjs(picocolorsExports);
|
|
91
|
+
|
|
92
|
+
const isNode = process.versions.node !== void 0;
|
|
93
|
+
const log = {
|
|
94
|
+
verbose: false,
|
|
95
|
+
// Intended for temporary logging
|
|
96
|
+
log(...data) {
|
|
97
|
+
if (!this.verbose) return;
|
|
98
|
+
const levelPrefix = pc.gray("[Log]");
|
|
99
|
+
if (isNode) {
|
|
100
|
+
console.warn(levelPrefix, ...data);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(levelPrefix, ...data);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
logPrefixed(prefix, ...data) {
|
|
106
|
+
this.info(pc.blue(`[${prefix}]`), ...data);
|
|
107
|
+
},
|
|
108
|
+
info(...data) {
|
|
109
|
+
if (!this.verbose) return;
|
|
110
|
+
const levelPrefix = pc.green("[Info]");
|
|
111
|
+
if (isNode) {
|
|
112
|
+
console.warn(levelPrefix, ...data);
|
|
113
|
+
} else {
|
|
114
|
+
console.info(levelPrefix, ...data);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
infoPrefixed(prefix, ...data) {
|
|
118
|
+
this.info(pc.blue(`[${prefix}]`), ...data);
|
|
119
|
+
},
|
|
120
|
+
async infoSpin(promise, message) {
|
|
121
|
+
return oraPromise(promise, {
|
|
122
|
+
prefixText: pc.green("[Info]"),
|
|
123
|
+
stream: process.stderr,
|
|
124
|
+
text: message
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
warn(...data) {
|
|
128
|
+
console.warn(pc.yellow("[Warning]"), ...data);
|
|
129
|
+
},
|
|
130
|
+
warnPrefixed(prefix, ...data) {
|
|
131
|
+
this.warn(pc.blue(`[${prefix}]`), ...data);
|
|
132
|
+
},
|
|
133
|
+
error(...data) {
|
|
134
|
+
console.error(pc.red("[Error]"), ...data);
|
|
135
|
+
},
|
|
136
|
+
errorPrefixed(prefix, ...data) {
|
|
137
|
+
this.error(pc.blue(`[${prefix}]`), ...data);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
async function getVideosInDirectory(directory) {
|
|
142
|
+
const files = await fs.readdir(directory);
|
|
143
|
+
const videoFiles = files.filter((file) => {
|
|
144
|
+
const fileExtension = path.extname(file);
|
|
145
|
+
const videoExtensions = [".mp4", ".mov", ".avi", ".mkv"];
|
|
146
|
+
return videoExtensions.includes(fileExtension);
|
|
147
|
+
}).map((file) => path.join(directory, file));
|
|
148
|
+
return videoFiles;
|
|
149
|
+
}
|
|
150
|
+
async function videoHasMetadata(filename) {
|
|
151
|
+
if (ffmpegPath === null) {
|
|
152
|
+
throw new Error("ffmpeg-static failed to locate ffmpeg binary");
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const { stderr, stdout } = await execa(ffmpegPath, ["-i", filename, "-f", "ffmetadata", "-"]);
|
|
156
|
+
return stdout.includes("creation_time") || stderr.includes("creation_time");
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new Error(`Failed to check metadata for: ${filename}:
|
|
159
|
+
${String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function stripVideoMetadata(filename) {
|
|
163
|
+
if (ffmpegPath === null) {
|
|
164
|
+
throw new Error("ffmpeg-static failed to locate ffmpeg binary");
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const extension = path.extname(filename).slice(1);
|
|
168
|
+
const temporaryFilename = `${filename.slice(0, Math.max(0, filename.lastIndexOf(".")))}.temp.${extension}`;
|
|
169
|
+
await execa(ffmpegPath, [
|
|
170
|
+
"-i",
|
|
171
|
+
filename,
|
|
172
|
+
"-map_metadata",
|
|
173
|
+
"-1",
|
|
174
|
+
"-c:v",
|
|
175
|
+
"copy",
|
|
176
|
+
"-c:a",
|
|
177
|
+
"copy",
|
|
178
|
+
temporaryFilename
|
|
179
|
+
]);
|
|
180
|
+
await fs.rename(temporaryFilename, filename);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Failed to strip metadata from: ${filename}:
|
|
183
|
+
${String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function stripVideoMetadataInDirectory(directory, options = {}) {
|
|
188
|
+
const { dryRun = false, verbose = false } = options;
|
|
189
|
+
const files = await getVideosInDirectory(directory);
|
|
190
|
+
return stripVideoMetadataInFiles(files, {
|
|
191
|
+
dryRun,
|
|
192
|
+
verbose
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function stripVideoMetadataInFiles(files, options = {}) {
|
|
196
|
+
const { dryRun = false, verbose = false } = options;
|
|
197
|
+
const initialVerbosity = log.verbose;
|
|
198
|
+
log.verbose = verbose;
|
|
199
|
+
const localVideosWithMetadata = [];
|
|
200
|
+
for (const videoFile of files) {
|
|
201
|
+
if (await videoHasMetadata(videoFile)) {
|
|
202
|
+
localVideosWithMetadata.push(videoFile);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!dryRun) {
|
|
206
|
+
log.info(`Found ${localVideosWithMetadata.length} videos with metadata to strip`);
|
|
207
|
+
for (const videoFile of localVideosWithMetadata) {
|
|
208
|
+
console.log(`Stripping metadata from: ${videoFile}`);
|
|
209
|
+
await stripVideoMetadata(videoFile);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
log.verbose = initialVerbosity;
|
|
213
|
+
return localVideosWithMetadata;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getTempFilename(file) {
|
|
217
|
+
const f = file instanceof URL ? fileURLToPath(file) : file.toString();
|
|
218
|
+
return join(dirname(f), `.${basename(f)}.tmp`);
|
|
219
|
+
}
|
|
220
|
+
async function retryAsyncOperation(fn, maxRetries, delayMs) {
|
|
221
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
222
|
+
try {
|
|
223
|
+
return await fn();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (i < maxRetries - 1) {
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
227
|
+
} else {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
class Writer {
|
|
234
|
+
#filename;
|
|
235
|
+
#tempFilename;
|
|
236
|
+
#locked = false;
|
|
237
|
+
#prev = null;
|
|
238
|
+
#next = null;
|
|
239
|
+
#nextPromise = null;
|
|
240
|
+
#nextData = null;
|
|
241
|
+
// File is locked, add data for later
|
|
242
|
+
#add(data) {
|
|
243
|
+
this.#nextData = data;
|
|
244
|
+
this.#nextPromise ||= new Promise((resolve, reject) => {
|
|
245
|
+
this.#next = [resolve, reject];
|
|
246
|
+
});
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
this.#nextPromise?.then(resolve).catch(reject);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// File isn't locked, write data
|
|
252
|
+
async #write(data) {
|
|
253
|
+
this.#locked = true;
|
|
254
|
+
try {
|
|
255
|
+
await writeFile(this.#tempFilename, data, "utf-8");
|
|
256
|
+
await retryAsyncOperation(async () => {
|
|
257
|
+
await rename(this.#tempFilename, this.#filename);
|
|
258
|
+
}, 10, 100);
|
|
259
|
+
this.#prev?.[0]();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof Error) {
|
|
262
|
+
this.#prev?.[1](err);
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
} finally {
|
|
266
|
+
this.#locked = false;
|
|
267
|
+
this.#prev = this.#next;
|
|
268
|
+
this.#next = this.#nextPromise = null;
|
|
269
|
+
if (this.#nextData !== null) {
|
|
270
|
+
const nextData = this.#nextData;
|
|
271
|
+
this.#nextData = null;
|
|
272
|
+
await this.write(nextData);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
constructor(filename) {
|
|
277
|
+
this.#filename = filename;
|
|
278
|
+
this.#tempFilename = getTempFilename(filename);
|
|
279
|
+
}
|
|
280
|
+
async write(data) {
|
|
281
|
+
return this.#locked ? this.#add(data) : this.#write(data);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class TextFile {
|
|
286
|
+
#filename;
|
|
287
|
+
#writer;
|
|
288
|
+
constructor(filename) {
|
|
289
|
+
this.#filename = filename;
|
|
290
|
+
this.#writer = new Writer(filename);
|
|
291
|
+
}
|
|
292
|
+
async read() {
|
|
293
|
+
let data;
|
|
294
|
+
try {
|
|
295
|
+
data = await readFile(this.#filename, "utf-8");
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (e.code === "ENOENT") {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
return data;
|
|
303
|
+
}
|
|
304
|
+
write(str) {
|
|
305
|
+
return this.#writer.write(str);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
class DataFile {
|
|
310
|
+
#adapter;
|
|
311
|
+
#parse;
|
|
312
|
+
#stringify;
|
|
313
|
+
constructor(filename, { parse, stringify }) {
|
|
314
|
+
this.#adapter = new TextFile(filename);
|
|
315
|
+
this.#parse = parse;
|
|
316
|
+
this.#stringify = stringify;
|
|
317
|
+
}
|
|
318
|
+
async read() {
|
|
319
|
+
const data = await this.#adapter.read();
|
|
320
|
+
if (data === null) {
|
|
321
|
+
return null;
|
|
322
|
+
} else {
|
|
323
|
+
return this.#parse(data);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
write(obj) {
|
|
327
|
+
return this.#adapter.write(this.#stringify(obj));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class JSONFile extends DataFile {
|
|
332
|
+
constructor(filename) {
|
|
333
|
+
super(filename, {
|
|
334
|
+
parse: JSON.parse,
|
|
335
|
+
stringify: (data) => JSON.stringify(data, null, 2)
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
class Memory {
|
|
341
|
+
#data = null;
|
|
342
|
+
read() {
|
|
343
|
+
return Promise.resolve(this.#data);
|
|
344
|
+
}
|
|
345
|
+
write(obj) {
|
|
346
|
+
this.#data = obj;
|
|
347
|
+
return Promise.resolve();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function checkArgs(adapter, defaultData) {
|
|
352
|
+
if (adapter === void 0)
|
|
353
|
+
throw new Error("lowdb: missing adapter");
|
|
354
|
+
if (defaultData === void 0)
|
|
355
|
+
throw new Error("lowdb: missing default data");
|
|
356
|
+
}
|
|
357
|
+
class Low {
|
|
358
|
+
adapter;
|
|
359
|
+
data;
|
|
360
|
+
constructor(adapter, defaultData) {
|
|
361
|
+
checkArgs(adapter, defaultData);
|
|
362
|
+
this.adapter = adapter;
|
|
363
|
+
this.data = defaultData;
|
|
364
|
+
}
|
|
365
|
+
async read() {
|
|
366
|
+
const data = await this.adapter.read();
|
|
367
|
+
if (data)
|
|
368
|
+
this.data = data;
|
|
369
|
+
}
|
|
370
|
+
async write() {
|
|
371
|
+
if (this.data)
|
|
372
|
+
await this.adapter.write(this.data);
|
|
373
|
+
}
|
|
374
|
+
async update(fn) {
|
|
375
|
+
fn(this.data);
|
|
376
|
+
await this.write();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function JSONFilePreset(filename, defaultData) {
|
|
381
|
+
const adapter = process.env.NODE_ENV === "test" ? new Memory() : new JSONFile(filename);
|
|
382
|
+
const db = new Low(adapter, defaultData);
|
|
383
|
+
await db.read();
|
|
384
|
+
return db;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function syncVideoInDirectory(directory, options) {
|
|
388
|
+
const { credentials, dryRun = false, service, verbose = false } = options;
|
|
389
|
+
const initialVerbosity = log.verbose;
|
|
390
|
+
log.verbose = verbose;
|
|
391
|
+
const syncReport = [];
|
|
392
|
+
const videoFiles = await getVideosInDirectory(directory);
|
|
393
|
+
log.info(`Found ${videoFiles.length} video files in directory "${directory}"`);
|
|
394
|
+
const state = await JSONFilePreset(path.join(directory, ".vidup-state.json"), {
|
|
395
|
+
syncState: []
|
|
396
|
+
});
|
|
397
|
+
state.data.syncState = state.data.syncState.filter(
|
|
398
|
+
(entry) => videoFiles.includes(path.join(directory, entry.filename))
|
|
399
|
+
);
|
|
400
|
+
for (const videoFile of videoFiles) {
|
|
401
|
+
const filename = path.basename(videoFile);
|
|
402
|
+
const localHash = await hash(videoFile, { algorithm: "sha1" });
|
|
403
|
+
const existingEntry = state.data.syncState.find((entry) => entry.filename === filename);
|
|
404
|
+
if (existingEntry) {
|
|
405
|
+
existingEntry.localHash = localHash;
|
|
406
|
+
} else {
|
|
407
|
+
state.data.syncState.push({ filename, localHash, remoteHash: {} });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!dryRun) {
|
|
411
|
+
await state.write();
|
|
412
|
+
}
|
|
413
|
+
if (service !== "bunny") {
|
|
414
|
+
throw new Error(`Streaming service not yet implemented: ${service}`);
|
|
415
|
+
}
|
|
416
|
+
const stream = new BunnyCdnStream({
|
|
417
|
+
apiKey: credentials.key,
|
|
418
|
+
videoLibrary: credentials.library
|
|
419
|
+
});
|
|
420
|
+
const remoteVideos = await stream.listAllVideos();
|
|
421
|
+
log.info(`Found ${remoteVideos.length} video files on ${service} remote streaming service`);
|
|
422
|
+
const remoteVideosInGoodStanding = remoteVideos.filter((video) => {
|
|
423
|
+
const entry = state.data.syncState.find((entry2) => entry2.filename === video.title);
|
|
424
|
+
return entry !== void 0 && entry.localHash === entry.remoteHash[service];
|
|
425
|
+
});
|
|
426
|
+
for (const video of remoteVideosInGoodStanding) {
|
|
427
|
+
syncReport.push({
|
|
428
|
+
action: "None",
|
|
429
|
+
localFile: video.title,
|
|
430
|
+
remoteId: video.guid
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const remoteVideosToCreate = state.data.syncState.filter(
|
|
434
|
+
(entry) => !remoteVideos.some((video) => video.title === entry.filename)
|
|
435
|
+
);
|
|
436
|
+
for (const video of remoteVideosToCreate) {
|
|
437
|
+
syncReport.push({
|
|
438
|
+
action: "Create",
|
|
439
|
+
localFile: video.filename,
|
|
440
|
+
remoteId: "Not yet uploaded (Dry run)"
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const remoteVideosToUpdate = remoteVideos.filter((video) => {
|
|
444
|
+
const entry = state.data.syncState.find((entry2) => entry2.filename === video.title);
|
|
445
|
+
return entry !== void 0 && entry.localHash !== entry.remoteHash[service];
|
|
446
|
+
});
|
|
447
|
+
for (const video of remoteVideosToUpdate) {
|
|
448
|
+
syncReport.push({
|
|
449
|
+
action: "Update",
|
|
450
|
+
localFile: video.title,
|
|
451
|
+
remoteId: "Not yet uploaded (Dry run)"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const remoteVideosToDelete = remoteVideos.filter(
|
|
455
|
+
(video) => !state.data.syncState.some((entry) => entry.filename === video.title)
|
|
456
|
+
);
|
|
457
|
+
for (const video of remoteVideosToDelete) {
|
|
458
|
+
syncReport.push({
|
|
459
|
+
action: "Delete",
|
|
460
|
+
localFile: video.title,
|
|
461
|
+
remoteId: video.guid
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (!dryRun) {
|
|
465
|
+
state.data.lastRun = /* @__PURE__ */ new Date();
|
|
466
|
+
await state.write();
|
|
467
|
+
log.info(`Synchronizing...`);
|
|
468
|
+
for (const [index, remoteVideo] of remoteVideosToDelete.entries()) {
|
|
469
|
+
if (index === 0) log.info(`Deleting ${remoteVideosToDelete.length} remote videos...`);
|
|
470
|
+
const deleteResponse = await log.infoSpin(
|
|
471
|
+
stream.deleteVideo(remoteVideo.guid),
|
|
472
|
+
`Deleting remote video ${index + 1}/${remoteVideosToDelete.length}: ${remoteVideo.title}`
|
|
473
|
+
);
|
|
474
|
+
if (!deleteResponse.success) {
|
|
475
|
+
throw new Error(`Failed to delete remote video: ${remoteVideo.title}`);
|
|
476
|
+
}
|
|
477
|
+
state.data.lastUpdate = /* @__PURE__ */ new Date();
|
|
478
|
+
await state.write();
|
|
479
|
+
}
|
|
480
|
+
for (const [index, localVideo] of remoteVideosToCreate.entries()) {
|
|
481
|
+
if (index === 0) log.info(`Uploading ${remoteVideosToCreate.length} new local videos...`);
|
|
482
|
+
const videoFile = createReadStream(path.join(directory, localVideo.filename));
|
|
483
|
+
const response = await log.infoSpin(
|
|
484
|
+
stream.createAndUploadVideo(videoFile, { title: localVideo.filename }),
|
|
485
|
+
`Uploading new video ${index + 1}/${remoteVideosToCreate.length}: ${localVideo.filename}`
|
|
486
|
+
);
|
|
487
|
+
log.info(
|
|
488
|
+
`Remote video ${index + 1}/${remoteVideosToCreate.length} created with GUID: ${response.guid}`
|
|
489
|
+
);
|
|
490
|
+
const stateEntry = state.data.syncState.find(
|
|
491
|
+
(entry) => entry.filename === localVideo.filename
|
|
492
|
+
);
|
|
493
|
+
if (stateEntry === void 0) {
|
|
494
|
+
throw new Error(`Failed to find state entry for: ${localVideo.filename}`);
|
|
495
|
+
}
|
|
496
|
+
stateEntry.remoteHash[service] = localVideo.localHash;
|
|
497
|
+
state.data.lastUpdate = /* @__PURE__ */ new Date();
|
|
498
|
+
await state.write();
|
|
499
|
+
const reportEntry = syncReport.find((entry) => entry.localFile === localVideo.filename);
|
|
500
|
+
if (reportEntry === void 0) {
|
|
501
|
+
throw new Error(`Failed to find report entry for: ${localVideo.filename}`);
|
|
502
|
+
}
|
|
503
|
+
reportEntry.remoteId = response.guid;
|
|
504
|
+
}
|
|
505
|
+
for (const [index, remoteVideo] of remoteVideosToUpdate.entries()) {
|
|
506
|
+
if (index === 0) {
|
|
507
|
+
log.info(`Updating ${remoteVideosToUpdate.length} remote videos...`);
|
|
508
|
+
log.warn(
|
|
509
|
+
"Bunny does not currently support updating videos in-place, so vidup will delete and re-uploading instead... this will change the video's GUID!"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const stateEntry = state.data.syncState.find((entry) => entry.filename === remoteVideo.title);
|
|
513
|
+
if (stateEntry === void 0) {
|
|
514
|
+
throw new Error(`Failed to find state entry for: ${remoteVideo.title}`);
|
|
515
|
+
}
|
|
516
|
+
const deleteResponse = await stream.deleteVideo(remoteVideo.guid);
|
|
517
|
+
if (!deleteResponse.success) {
|
|
518
|
+
throw new Error(`Failed to delete remote video before updating: ${remoteVideo.title}`);
|
|
519
|
+
}
|
|
520
|
+
const videoFile = createReadStream(path.join(directory, stateEntry.filename));
|
|
521
|
+
const createResponse = await log.infoSpin(
|
|
522
|
+
stream.createAndUploadVideo(videoFile, { title: stateEntry.filename }),
|
|
523
|
+
`Updating remote video ${index + 1}/${remoteVideosToUpdate.length}: ${remoteVideo.title}`
|
|
524
|
+
);
|
|
525
|
+
log.info(
|
|
526
|
+
`Updated remote video ${index + 1}/${remoteVideosToUpdate.length} created with GUID: ${createResponse.guid}`
|
|
527
|
+
);
|
|
528
|
+
stateEntry.remoteHash[service] = stateEntry.localHash;
|
|
529
|
+
state.data.lastUpdate = /* @__PURE__ */ new Date();
|
|
530
|
+
await state.write();
|
|
531
|
+
const reportEntry = syncReport.find((entry) => entry.localFile === stateEntry.filename);
|
|
532
|
+
if (reportEntry === void 0) {
|
|
533
|
+
throw new Error(`Failed to find report entry for: ${stateEntry.filename}`);
|
|
534
|
+
}
|
|
535
|
+
reportEntry.remoteId = createResponse.guid;
|
|
536
|
+
}
|
|
537
|
+
log.info(`All ${videoFiles.length} videos are now in sync!`);
|
|
538
|
+
}
|
|
539
|
+
log.verbose = initialVerbosity;
|
|
540
|
+
return syncReport;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export { syncVideoInDirectory as a, log as l, stripVideoMetadataInDirectory as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vidup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Synchronize a local directory of video files to remote streaming services.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bunnycdn-stream",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
29
|
+
"types": "./dist/lib/index.d.ts",
|
|
30
|
+
"import": "./dist/lib/index.js"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"main": "./dist/lib/index.js",
|
|
@@ -40,36 +40,37 @@
|
|
|
40
40
|
"dist/*"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@types/yargs": "^17.0.
|
|
44
|
-
"bunnycdn-stream": "^2.
|
|
45
|
-
"execa": "^9.
|
|
43
|
+
"@types/yargs": "^17.0.34",
|
|
44
|
+
"bunnycdn-stream": "^2.5.5",
|
|
45
|
+
"execa": "^9.6.0",
|
|
46
46
|
"ffmpeg-static": "^5.2.0",
|
|
47
|
-
"hasha": "^
|
|
48
|
-
"ora": "^
|
|
47
|
+
"hasha": "^7.0.0",
|
|
48
|
+
"ora": "^9.0.0",
|
|
49
49
|
"yargs": "^17.7.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@kitschpatrol/shared-config": "^5.
|
|
53
|
-
"@types/node": "^20.
|
|
54
|
-
"bumpp": "^10.
|
|
55
|
-
"chalk": "^5.4.1",
|
|
52
|
+
"@kitschpatrol/shared-config": "^5.8.1",
|
|
53
|
+
"@types/node": "^20.19.24",
|
|
54
|
+
"bumpp": "^10.3.1",
|
|
56
55
|
"lowdb": "^7.0.1",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
56
|
+
"mdat-plugin-cli-help": "^1.0.1",
|
|
57
|
+
"picocolors": "^1.1.1",
|
|
58
|
+
"pkgroll": "^2.20.1",
|
|
59
|
+
"pretty-ms": "^9.3.0",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"untildify": "^6.0.0"
|
|
61
62
|
},
|
|
62
63
|
"engines": {
|
|
63
|
-
"node": ">=20.
|
|
64
|
+
"node": ">=20.19.0"
|
|
64
65
|
},
|
|
65
66
|
"publishConfig": {
|
|
66
67
|
"access": "public"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
|
-
"build": "pkgroll --clean-dist --
|
|
70
|
+
"build": "pkgroll --clean-dist --tsconfig tsconfig.build.json",
|
|
70
71
|
"clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
|
|
71
|
-
"fix": "
|
|
72
|
-
"lint": "
|
|
73
|
-
"release": "bumpp --commit 'Release: %s' && pnpm run build &&
|
|
72
|
+
"fix": "ksc fix",
|
|
73
|
+
"lint": "ksc lint",
|
|
74
|
+
"release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish"
|
|
74
75
|
}
|
|
75
76
|
}
|
package/dist/sync-DAKTIkq2.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
var Z=Object.defineProperty;var n=(t,e)=>Z(t,"name",{value:e,configurable:!0});import{BunnyCdnStream as ee}from"bunnycdn-stream";import{hash as te}from"hasha";import{createReadStream as U}from"node:fs";import k,{writeFile as oe,rename as re,readFile as ie}from"node:fs/promises";import w,{join as ne,dirname as se,basename as ae}from"node:path";import{fileURLToPath as le}from"node:url";import{oraPromise as ce}from"ora";import{execa as H}from"execa";import j from"ffmpeg-static";const M=10,G=n((t=0)=>e=>`\x1B[${e+t}m`,"wrapAnsi16"),L=n((t=0)=>e=>`\x1B[${38+t};5;${e}m`,"wrapAnsi256"),W=n((t=0)=>(e,o,r)=>`\x1B[${38+t};2;${e};${o};${r}m`,"wrapAnsi16m"),c={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(c.modifier);const de=Object.keys(c.color),fe=Object.keys(c.bgColor);[...de,...fe];function ue(){const t=new Map;for(const[e,o]of Object.entries(c)){for(const[r,i]of Object.entries(o))c[r]={open:`\x1B[${i[0]}m`,close:`\x1B[${i[1]}m`},o[r]=c[r],t.set(i[0],i[1]);Object.defineProperty(c,e,{value:o,enumerable:!1})}return Object.defineProperty(c,"codes",{value:t,enumerable:!1}),c.color.close="\x1B[39m",c.bgColor.close="\x1B[49m",c.color.ansi=G(),c.color.ansi256=L(),c.color.ansi16m=W(),c.bgColor.ansi=G(M),c.bgColor.ansi256=L(M),c.bgColor.ansi16m=W(M),Object.defineProperties(c,{rgbToAnsi256:{value(e,o,r){return e===o&&o===r?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(o/255*5)+Math.round(r/255*5)},enumerable:!1},hexToRgb:{value(e){const o=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!o)return[0,0,0];let[r]=o;r.length===3&&(r=[...r].map(l=>l+l).join(""));const i=Number.parseInt(r,16);return[i>>16&255,i>>8&255,i&255]},enumerable:!1},hexToAnsi256:{value:n(e=>c.rgbToAnsi256(...c.hexToRgb(e)),"value"),enumerable:!1},ansi256ToAnsi:{value(e){if(e<8)return 30+e;if(e<16)return 90+(e-8);let o,r,i;if(e>=232)o=((e-232)*10+8)/255,r=o,i=o;else{e-=16;const m=e%36;o=Math.floor(e/36)/5,r=Math.floor(m/6)/5,i=m%6/5}const l=Math.max(o,r,i)*2;if(l===0)return 30;let u=30+(Math.round(i)<<2|Math.round(r)<<1|Math.round(o));return l===2&&(u+=60),u},enumerable:!1},rgbToAnsi:{value:n((e,o,r)=>c.ansi256ToAnsi(c.rgbToAnsi256(e,o,r)),"value"),enumerable:!1},hexToAnsi:{value:n(e=>c.ansi256ToAnsi(c.hexToAnsi256(e)),"value"),enumerable:!1}}),c}n(ue,"assembleStyles");const g=ue(),Y=(()=>{if(!("navigator"in globalThis))return 0;if(globalThis.navigator.userAgentData){const t=navigator.userAgentData.brands.find(({brand:e})=>e==="Chromium");if(t&&t.version>93)return 3}return/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)?1:0})(),J=Y!==0&&{level:Y},he={stdout:J,stderr:J};function me(t,e,o){let r=t.indexOf(e);if(r===-1)return t;const i=e.length;let l=0,u="";do u+=t.slice(l,r)+e+o,l=r+i,r=t.indexOf(e,l);while(r!==-1);return u+=t.slice(l),u}n(me,"stringReplaceAll");function ge(t,e,o,r){let i=0,l="";do{const u=t[r-1]==="\r";l+=t.slice(i,u?r-1:r)+e+(u?`\r
|
|
2
|
-
`:`
|
|
3
|
-
`)+o,i=r+1,r=t.indexOf(`
|
|
4
|
-
`,i)}while(r!==-1);return l+=t.slice(i),l}n(ge,"stringEncaseCRLFWithFirstIndex");const{stdout:K,stderr:_}=he,V=Symbol("GENERATOR"),$=Symbol("STYLER"),T=Symbol("IS_EMPTY"),z=["ansi","ansi","ansi256","ansi16m"],x=Object.create(null),pe=n((t,e={})=>{if(e.level&&!(Number.isInteger(e.level)&&e.level>=0&&e.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");const o=K?K.level:0;t.level=e.level===void 0?o:e.level},"applyOptions"),we=n(t=>{const e=n((...o)=>o.join(" "),"chalk2");return pe(e,t),Object.setPrototypeOf(e,B.prototype),e},"chalkFactory");function B(t){return we(t)}n(B,"createChalk"),Object.setPrototypeOf(B.prototype,Function.prototype);for(const[t,e]of Object.entries(g))x[t]={get(){const o=C(this,D(e.open,e.close,this[$]),this[T]);return Object.defineProperty(this,t,{value:o}),o}};x.visible={get(){const t=C(this,this[$],!0);return Object.defineProperty(this,"visible",{value:t}),t}};const N=n((t,e,o,...r)=>t==="rgb"?e==="ansi16m"?g[o].ansi16m(...r):e==="ansi256"?g[o].ansi256(g.rgbToAnsi256(...r)):g[o].ansi(g.rgbToAnsi(...r)):t==="hex"?N("rgb",e,o,...g.hexToRgb(...r)):g[o][t](...r),"getModelAnsi"),be=["rgb","hex","ansi256"];for(const t of be){x[t]={get(){const{level:o}=this;return function(...r){const i=D(N(t,z[o],"color",...r),g.color.close,this[$]);return C(this,i,this[T])}}};const e="bg"+t[0].toUpperCase()+t.slice(1);x[e]={get(){const{level:o}=this;return function(...r){const i=D(N(t,z[o],"bgColor",...r),g.bgColor.close,this[$]);return C(this,i,this[T])}}}}const ye=Object.defineProperties(()=>{},{...x,level:{enumerable:!0,get(){return this[V].level},set(t){this[V].level=t}}}),D=n((t,e,o)=>{let r,i;return o===void 0?(r=t,i=e):(r=o.openAll+t,i=e+o.closeAll),{open:t,close:e,openAll:r,closeAll:i,parent:o}},"createStyler"),C=n((t,e,o)=>{const r=n((...i)=>ve(r,i.length===1?""+i[0]:i.join(" ")),"builder");return Object.setPrototypeOf(r,ye),r[V]=t,r[$]=e,r[T]=o,r},"createBuilder"),ve=n((t,e)=>{if(t.level<=0||!e)return t[T]?"":e;let o=t[$];if(o===void 0)return e;const{openAll:r,closeAll:i}=o;if(e.includes("\x1B"))for(;o!==void 0;)e=me(e,o.close,o.open),o=o.parent;const l=e.indexOf(`
|
|
5
|
-
`);return l!==-1&&(e=ge(e,i,r,l)),r+e+i},"applyStyle");Object.defineProperties(B.prototype,x);const p=B();B({level:_?_.level:0});const q=process.versions.node!==void 0,d={verbose:!1,log(...t){if(!this.verbose)return;const e=p.gray("[Log]");q?console.warn(e,...t):console.log(e,...t)},logPrefixed(t,...e){this.info(p.blue(`[${t}]`),...e)},info(...t){if(!this.verbose)return;const e=p.green("[Info]");q?console.warn(e,...t):console.info(e,...t)},infoPrefixed(t,...e){this.info(p.blue(`[${t}]`),...e)},async infoSpin(t,e){return ce(t,{prefixText:p.green("[Info]"),stream:process.stderr,text:e})},warn(...t){console.warn(p.yellow("[Warning]"),...t)},warnPrefixed(t,...e){this.warn(p.blue(`[${t}]`),...e)},error(...t){console.error(p.red("[Error]"),...t)},errorPrefixed(t,...e){this.error(p.blue(`[${t}]`),...e)}};async function Q(t){return(await k.readdir(t)).filter(r=>{const i=w.extname(r);return[".mp4",".mov",".avi",".mkv"].includes(i)}).map(r=>w.join(t,r))}n(Q,"getVideosInDirectory");async function $e(t){if(j===null)throw new Error("ffmpeg-static failed to locate ffmpeg binary");try{const{stderr:e,stdout:o}=await H(j,["-i",t,"-f","ffmetadata","-"]);return o.includes("creation_time")||e.includes("creation_time")}catch(e){throw new Error(`Failed to check metadata for: ${t}:
|
|
6
|
-
${String(e)}`)}}n($e,"videoHasMetadata");async function xe(t){if(j===null)throw new Error("ffmpeg-static failed to locate ffmpeg binary");try{const e=w.extname(t).slice(1),o=`${t.slice(0,Math.max(0,t.lastIndexOf(".")))}.temp.${e}`;await H(j,["-i",t,"-map_metadata","-1","-c:v","copy","-c:a","copy",o]),await k.rename(o,t)}catch(e){throw new Error(`Failed to strip metadata from: ${t}:
|
|
7
|
-
${String(e)}`)}}n(xe,"stripVideoMetadata");async function Se(t,e={}){const{dryRun:o=!1,verbose:r=!1}=e,i=await Q(t);return Fe(i,{dryRun:o,verbose:r})}n(Se,"stripVideoMetadataInDirectory");async function Fe(t,e={}){const{dryRun:o=!1,verbose:r=!1}=e,i=d.verbose;d.verbose=r;const l=[];for(const u of t)await $e(u)&&l.push(u);if(!o){d.info(`Found ${l.length} videos with metadata to strip`);for(const u of l)console.log(`Stripping metadata from: ${u}`),await xe(u)}return d.verbose=i,l}n(Fe,"stripVideoMetadataInFiles");function Ee(t){const e=t instanceof URL?le(t):t.toString();return ne(se(e),`.${ae(e)}.tmp`)}n(Ee,"getTempFilename");async function Oe(t,e,o){for(let r=0;r<e;r++)try{return await t()}catch(i){if(r<e-1)await new Promise(l=>setTimeout(l,o));else throw i}}n(Oe,"retryAsyncOperation");class Ae{static{n(this,"Writer")}#e;#t;#o=!1;#i=null;#n=null;#s=null;#r=null;#a(e){return this.#r=e,this.#s||=new Promise((o,r)=>{this.#n=[o,r]}),new Promise((o,r)=>{this.#s?.then(o).catch(r)})}async#l(e){this.#o=!0;try{await oe(this.#t,e,"utf-8"),await Oe(async()=>{await re(this.#t,this.#e)},10,100),this.#i?.[0]()}catch(o){throw o instanceof Error&&this.#i?.[1](o),o}finally{if(this.#o=!1,this.#i=this.#n,this.#n=this.#s=null,this.#r!==null){const o=this.#r;this.#r=null,await this.write(o)}}}constructor(e){this.#e=e,this.#t=Ee(e)}async write(e){return this.#o?this.#a(e):this.#l(e)}}class Te{static{n(this,"TextFile")}#e;#t;constructor(e){this.#e=e,this.#t=new Ae(e)}async read(){let e;try{e=await ie(this.#e,"utf-8")}catch(o){if(o.code==="ENOENT")return null;throw o}return e}write(e){return this.#t.write(e)}}class Be{static{n(this,"DataFile")}#e;#t;#o;constructor(e,{parse:o,stringify:r}){this.#e=new Te(e),this.#t=o,this.#o=r}async read(){const e=await this.#e.read();return e===null?null:this.#t(e)}write(e){return this.#e.write(this.#o(e))}}class Pe extends Be{static{n(this,"JSONFile")}constructor(e){super(e,{parse:JSON.parse,stringify:n(o=>JSON.stringify(o,null,2),"stringify")})}}class Re{static{n(this,"Memory")}#e=null;read(){return Promise.resolve(this.#e)}write(e){return this.#e=e,Promise.resolve()}}function je(t,e){if(t===void 0)throw new Error("lowdb: missing adapter");if(e===void 0)throw new Error("lowdb: missing default data")}n(je,"checkArgs");class Ce{static{n(this,"Low")}adapter;data;constructor(e,o){je(e,o),this.adapter=e,this.data=o}async read(){const e=await this.adapter.read();e&&(this.data=e)}async write(){this.data&&await this.adapter.write(this.data)}async update(e){e(this.data),await this.write()}}async function Ie(t,e){const o=process.env.NODE_ENV==="test"?new Re:new Pe(t),r=new Ce(o,e);return await r.read(),r}n(Ie,"JSONFilePreset");async function Me(t,e){const{credentials:o,dryRun:r=!1,service:i,verbose:l=!1}=e,u=d.verbose;d.verbose=l;const m=[],P=await Q(t);d.info(`Found ${P.length} video files in directory "${t}"`);const f=await Ie(w.join(t,".vidup-state.json"),{syncState:[]});f.data.syncState=f.data.syncState.filter(s=>P.includes(w.join(t,s.filename)));for(const s of P){const a=w.basename(s),h=await te(s,{algorithm:"sha1"}),b=f.data.syncState.find(y=>y.filename===a);b?b.localHash=h:f.data.syncState.push({filename:a,localHash:h,remoteHash:{}})}if(r||await f.write(),i!=="bunny")throw new Error(`Streaming service not yet implemented: ${i}`);const S=new ee({apiKey:o.key,videoLibrary:o.library}),F=await S.listAllVideos();d.info(`Found ${F.length} video files on ${i} remote streaming service`);const X=F.filter(s=>{const a=f.data.syncState.find(h=>h.filename===s.title);return a!==void 0&&a.localHash===a.remoteHash[i]});for(const s of X)m.push({action:"None",localFile:s.title,remoteId:s.guid});const E=f.data.syncState.filter(s=>!F.some(a=>a.title===s.filename));for(const s of E)m.push({action:"Create",localFile:s.filename,remoteId:"Not yet uploaded (Dry run)"});const O=F.filter(s=>{const a=f.data.syncState.find(h=>h.filename===s.title);return a!==void 0&&a.localHash!==a.remoteHash[i]});for(const s of O)m.push({action:"Update",localFile:s.title,remoteId:"Not yet uploaded (Dry run)"});const R=F.filter(s=>!f.data.syncState.some(a=>a.filename===s.title));for(const s of R)m.push({action:"Delete",localFile:s.title,remoteId:s.guid});if(!r){f.data.lastRun=new Date,await f.write(),d.info("Synchronizing...");for(const[s,a]of R.entries()){if(s===0&&d.info(`Deleting ${R.length} remote videos...`),!(await d.infoSpin(S.deleteVideo(a.guid),`Deleting remote video ${s+1}/${R.length}: ${a.title}`)).success)throw new Error(`Failed to delete remote video: ${a.title}`);f.data.lastUpdate=new Date,await f.write()}for(const[s,a]of E.entries()){s===0&&d.info(`Uploading ${E.length} new local videos...`);const h=U(w.join(t,a.filename)),b=await d.infoSpin(S.createAndUploadVideo(h,{title:a.filename}),`Uploading new video ${s+1}/${E.length}: ${a.filename}`);d.info(`Remote video ${s+1}/${E.length} created with GUID: ${b.guid}`);const y=f.data.syncState.find(v=>v.filename===a.filename);if(y===void 0)throw new Error(`Failed to find state entry for: ${a.filename}`);y.remoteHash[i]=a.localHash,f.data.lastUpdate=new Date,await f.write();const A=m.find(v=>v.localFile===a.filename);if(A===void 0)throw new Error(`Failed to find report entry for: ${a.filename}`);A.remoteId=b.guid}for(const[s,a]of O.entries()){s===0&&(d.info(`Updating ${O.length} remote videos...`),d.warn("Bunny does not currently support updating videos in-place, so vidup will delete and re-uploading instead... this will change the video's GUID!"));const h=f.data.syncState.find(I=>I.filename===a.title);if(h===void 0)throw new Error(`Failed to find state entry for: ${a.title}`);if(!(await S.deleteVideo(a.guid)).success)throw new Error(`Failed to delete remote video before updating: ${a.title}`);const y=U(w.join(t,h.filename)),A=await d.infoSpin(S.createAndUploadVideo(y,{title:h.filename}),`Updating remote video ${s+1}/${O.length}: ${a.title}`);d.info(`Updated remote video ${s+1}/${O.length} created with GUID: ${A.guid}`),h.remoteHash[i]=h.localHash,f.data.lastUpdate=new Date,await f.write();const v=m.find(I=>I.localFile===h.filename);if(v===void 0)throw new Error(`Failed to find report entry for: ${h.filename}`);v.remoteId=A.guid}d.info(`All ${P.length} videos are now in sync!`)}return d.verbose=u,m}n(Me,"syncVideoInDirectory");export{Me as a,d as l,Se as s};
|