vidup 1.0.18 → 1.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidup",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Synchronize a local directory of video files to remote streaming services.",
5
5
  "keywords": [
6
6
  "bunnycdn-stream",
@@ -43,43 +43,38 @@
43
43
  "dependencies": {
44
44
  "@types/yargs": "^17.0.35",
45
45
  "bunnycdn-stream": "^2.5.5",
46
- "execa": "^9.6.1",
46
+ "execa": "^10.0.1",
47
47
  "ffmpeg-static": "^5.3.0",
48
48
  "hasha": "^7.0.0",
49
+ "lognow": "^0.8.4",
49
50
  "lowdb": "^7.0.1",
50
- "ora": "^9.4.0",
51
- "picocolors": "^1.1.1",
52
- "pretty-ms": "^9.3.0",
51
+ "ora": "^9.4.1",
52
+ "pretty-ms": "^9.3.1",
53
53
  "untildify": "^6.0.0",
54
- "yargs": "^18.0.0"
54
+ "yargs": "^18.2.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@arethetypeswrong/core": "^0.18.2",
58
- "@kitschpatrol/shared-config": "^7.6.0",
59
- "@types/node": "^20.19.41",
60
- "bumpp": "^11.1.0",
61
- "mdat-plugin-cli-help": "^3.0.0",
62
- "publint": "^0.3.21",
57
+ "@arethetypeswrong/core": "^0.18.5",
58
+ "@kitschpatrol/shared-config": "^8.9.0",
59
+ "@types/node": "~24.13.4",
60
+ "brewpub": "^0.3.0",
61
+ "bumpp": "^12.3.0",
62
+ "mdat-plugin-cli-help": "^3.2.0",
63
+ "publint": "^0.3.24",
63
64
  "shx": "^0.4.0",
64
- "tsdown": "^0.22.0",
65
- "typescript": "^5.9.3",
66
- "vitest": "^4.1.6"
65
+ "tsdown": "^0.23.0",
66
+ "typescript": "^6.0.3",
67
+ "vitest": "^5.0.1"
67
68
  },
68
69
  "engines": {
69
- "node": ">=20.19.0"
70
- },
71
- "devEngines": {
72
- "runtime": {
73
- "name": "node",
74
- "version": ">=22.18.0"
75
- }
70
+ "node": ">=24.16.0"
76
71
  },
77
72
  "scripts": {
78
73
  "build": "tsdown",
79
- "clean": "shx rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
74
+ "clean-deep": "shx rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
80
75
  "fix": "ksc fix",
81
76
  "lint": "ksc lint",
82
- "release": "bumpp --commit 'Release: %s' && pnpm build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') pnpm publish",
77
+ "release": "bumpp --commit 'Release: %s' && pnpm build && pnpm publish --otp $(op item get npm --otp) && brewpub --tap kitschpatrol/tap",
83
78
  "test": "vitest run"
84
79
  }
85
80
  }
package/readme.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <!-- badges -->
8
8
 
9
- [![NPM Package vidup](https://img.shields.io/npm/v/vidup.svg)](https://npmjs.com/package/vidup)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit/)
9
+ [![NPM Package vidup](https://img.shields.io/npm/v/vidup.svg)](https://www.npmjs.com/package/vidup)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit)
11
11
  [![CI](https://github.com/kitschpatrol/vidup/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/vidup/actions/workflows/ci.yml)
12
12
 
13
13
  <!-- /badges -->
@@ -51,7 +51,13 @@ Or, install locally to access the CLI commands in a single project or to import
51
51
  npm install vidup
52
52
  ```
53
53
 
54
- Or, install globally for access across your system:
54
+ Or, install globally for access across your system with either Homebrew or npm:
55
+
56
+ ```sh
57
+ brew install kitschpatrol/tap/vidup
58
+ ```
59
+
60
+ _or_
55
61
 
56
62
  ```sh
57
63
  npm install --global vidup
@@ -116,7 +122,7 @@ vidup sync <directory> [options]
116
122
  | `--strip-metadata` | Remove all metadata from the video files before uploading them to the streaming service. Warning: This will modify local videos in-place. | `boolean` | `false` |
117
123
  | `--dry-run`<br>`-d` | Perform a dry run without making any changes. Useful for testing and debugging. Pairs well with the `--json` command. | `boolean` | `false` |
118
124
  | `--json` | Output the sync report as JSON. | `boolean` | `false` |
119
- | `--verbose` | Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection. | `boolean` | `false` |
125
+ | `--verbose` | Enable verbose logging. All verbose logs are prefixed with their log level and are printed to `stderr` for ease of redirection. | `boolean` | `false` |
120
126
  | `--help`<br>`-h` | Show help | `boolean` | |
121
127
  | `--version`<br>`-v` | Show version number | `boolean` | |
122
128
 
@@ -165,7 +171,6 @@ async function syncVideoInDirectory(
165
171
  }
166
172
  dryRun?: boolean // Defaults to false
167
173
  service: Service
168
- verbose?: boolean // Defaults to false
169
174
  },
170
175
  ): Promise<SyncReport>
171
176
  ```
@@ -173,11 +178,10 @@ async function syncVideoInDirectory(
173
178
  A `stripVideoMetadataInDirectory` function is also exported, which will remove metadata from all videos in the target directory. It returns a list of video filepaths with metadata:
174
179
 
175
180
  ```ts
176
- export async function stripVideoMetadataInFiles(
181
+ async function stripVideoMetadataInDirectory(
177
182
  directory: string,
178
183
  options: {
179
184
  dryRun?: boolean // Defaults to false
180
- verbose?: boolean // Defaults to false
181
185
  },
182
186
  ): Promise<string[]>
183
187
  ```
@@ -198,6 +202,21 @@ const syncReport = await syncVideoInDirectory(process.cwd(), {
198
202
  console.log(JSON.stringify(syncReport, undefined, 2))
199
203
  ```
200
204
 
205
+ #### Logging
206
+
207
+ Vidup logs progress at the `debug` level and problems at the `warn` and `error` levels via [lognow](https://github.com/kitschpatrol/lognow). By default, only warnings and errors are shown. To see everything, or to route the library's logs into your own logging system, inject a logger:
208
+
209
+ ```ts
210
+ import { setLogger } from 'vidup'
211
+
212
+ // Send all library logs to the console
213
+ setLogger(console)
214
+
215
+ // Or pass any LogLayer instance
216
+ ```
217
+
218
+ Call `setLogger()` with no argument to silence the library entirely.
219
+
201
220
  ## Background
202
221
 
203
222
  ### Motivation
@@ -206,7 +225,6 @@ Video streaming providers have slow and clunky web UIs. I'd rather not maintain
206
225
 
207
226
  ## The future
208
227
 
209
- - [ ] Tests
210
228
  - [ ] Add [Cosmiconfig](https://github.com/cosmiconfig/cosmiconfig)
211
229
  - [ ] Synchronize thumbnails and other metadata
212
230
  - [ ] Remote collection / folder support
@@ -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 };