vidup 1.0.18 → 1.0.20
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.d.ts +1 -0
- package/dist/bin/cli.js +85 -2
- package/dist/lib/index.d.ts +14 -19
- package/dist/lib/index.js +3 -261
- package/dist/lib-Ddx3eQbF.js +251 -0
- package/package.json +20 -24
- package/readme.md +53 -16
- package/dist/sync-ChGtXZ_b.js +0 -543
package/dist/sync-ChGtXZ_b.js
DELETED
|
@@ -1,543 +0,0 @@
|
|
|
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 };
|