reelsort 0.1.0 → 0.1.2
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/cli.js +234 -321
- package/dist/index.d.mts +78 -81
- package/dist/index.d.ts +78 -81
- package/dist/index.js +1013 -1063
- package/dist/index.mjs +1003 -1053
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -72,52 +72,136 @@ __export(index_exports, {
|
|
|
72
72
|
});
|
|
73
73
|
module.exports = __toCommonJS(index_exports);
|
|
74
74
|
|
|
75
|
-
// src/actions/
|
|
75
|
+
// src/actions/clean.ts
|
|
76
76
|
var import_cosmetic = __toESM(require("cosmetic"));
|
|
77
|
-
var
|
|
77
|
+
var import_fs2 = require("fs");
|
|
78
78
|
|
|
79
|
-
// src/
|
|
79
|
+
// src/db.ts
|
|
80
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
80
81
|
var import_fs = require("fs");
|
|
81
82
|
var import_os = require("os");
|
|
82
83
|
var import_path = require("path");
|
|
83
|
-
var
|
|
84
|
-
var
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
var DB_DIR = (0, import_path.join)((0, import_os.homedir)(), ".config", "reelsort");
|
|
85
|
+
var DB_PATH = (0, import_path.join)(DB_DIR, "reelsort.db");
|
|
86
|
+
var _db = null;
|
|
87
|
+
var db = () => {
|
|
88
|
+
if (_db) return _db;
|
|
89
|
+
if (!(0, import_fs.existsSync)(DB_DIR)) (0, import_fs.mkdirSync)(DB_DIR, { recursive: true });
|
|
90
|
+
_db = new import_better_sqlite3.default(DB_PATH);
|
|
91
|
+
_db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS shows (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
path TEXT NOT NULL UNIQUE,
|
|
95
|
+
tmdbId INTEGER,
|
|
96
|
+
title TEXT,
|
|
97
|
+
ended INTEGER NOT NULL DEFAULT 0,
|
|
98
|
+
addedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
99
|
+
);
|
|
100
|
+
CREATE TABLE IF NOT EXISTS renameHistory (
|
|
101
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
102
|
+
sessionId TEXT NOT NULL,
|
|
103
|
+
oldPath TEXT NOT NULL,
|
|
104
|
+
newPath TEXT NOT NULL,
|
|
105
|
+
renamedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
106
|
+
);
|
|
107
|
+
CREATE TABLE IF NOT EXISTS mediaInfo (
|
|
108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
+
filePath TEXT NOT NULL UNIQUE,
|
|
110
|
+
codec TEXT,
|
|
111
|
+
resolution TEXT,
|
|
112
|
+
width INTEGER,
|
|
113
|
+
height INTEGER,
|
|
114
|
+
duration REAL,
|
|
115
|
+
probedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
116
|
+
);
|
|
117
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
sessionId TEXT NOT NULL,
|
|
120
|
+
sourcePath TEXT NOT NULL,
|
|
121
|
+
destinationPath TEXT NOT NULL,
|
|
122
|
+
mode TEXT NOT NULL,
|
|
123
|
+
tmdbId INTEGER,
|
|
124
|
+
importedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
125
|
+
)
|
|
126
|
+
`);
|
|
127
|
+
try {
|
|
128
|
+
_db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
return _db;
|
|
88
132
|
};
|
|
89
|
-
var
|
|
90
|
-
|
|
91
|
-
(0, import_fs.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
133
|
+
var recordRename = (sessionId, oldPath, newPath) => {
|
|
134
|
+
db().prepare("INSERT INTO renameHistory (sessionId, oldPath, newPath) VALUES (?, ?, ?)").run(sessionId, oldPath, newPath);
|
|
92
135
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
var renderEpisode = (format, season, episode, title, name) => {
|
|
98
|
-
return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
|
|
136
|
+
var getLastSession = () => {
|
|
137
|
+
const last = db().prepare("SELECT sessionId FROM renameHistory ORDER BY id DESC LIMIT 1").get();
|
|
138
|
+
if (!last) return [];
|
|
139
|
+
return db().prepare("SELECT * FROM renameHistory WHERE sessionId = ? ORDER BY id DESC").all(last.sessionId);
|
|
99
140
|
};
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
if (double) {
|
|
103
|
-
const a = renderEpisode(format, season, episode, title, name);
|
|
104
|
-
const b = renderEpisode(format, season, episode + 1, title, name);
|
|
105
|
-
return `${a}-${b}`;
|
|
106
|
-
}
|
|
107
|
-
return renderEpisode(format, season, episode, title, name);
|
|
141
|
+
var deleteSession = (sessionId) => {
|
|
142
|
+
db().prepare("DELETE FROM renameHistory WHERE sessionId = ?").run(sessionId);
|
|
108
143
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
var
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId) => {
|
|
145
|
+
db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId) VALUES (?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null);
|
|
146
|
+
};
|
|
147
|
+
var getMediaInfo = (filePath) => {
|
|
148
|
+
return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
|
|
149
|
+
};
|
|
150
|
+
var upsertMediaInfo = (filePath, codec, resolution, width, height, duration) => {
|
|
151
|
+
db().prepare(
|
|
152
|
+
`INSERT INTO mediaInfo (filePath, codec, resolution, width, height, duration, probedAt)
|
|
153
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
154
|
+
ON CONFLICT(filePath) DO UPDATE SET
|
|
155
|
+
codec = excluded.codec, resolution = excluded.resolution,
|
|
156
|
+
width = excluded.width, height = excluded.height,
|
|
157
|
+
duration = excluded.duration, probedAt = excluded.probedAt`
|
|
158
|
+
).run(filePath, codec, resolution, width, height, duration);
|
|
159
|
+
};
|
|
160
|
+
var getImportByDest = (destPath) => {
|
|
161
|
+
return db().prepare("SELECT * FROM imports WHERE destinationPath = ? LIMIT 1").get(destPath);
|
|
162
|
+
};
|
|
163
|
+
var rowToShow = (row) => ({
|
|
164
|
+
...row,
|
|
165
|
+
ended: row.ended === 1
|
|
166
|
+
});
|
|
167
|
+
var getShows = () => db().prepare("SELECT * FROM shows ORDER BY path ASC").all().map(rowToShow);
|
|
168
|
+
var upsertShow = (path, tmdbId, title) => {
|
|
169
|
+
db().prepare(
|
|
170
|
+
`
|
|
171
|
+
INSERT INTO shows (path, tmdbId, title) VALUES (?, ?, ?)
|
|
172
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
173
|
+
tmdbId = COALESCE(excluded.tmdbId, tmdbId),
|
|
174
|
+
title = COALESCE(excluded.title, title)
|
|
175
|
+
`
|
|
176
|
+
).run(path, tmdbId, title ?? null);
|
|
177
|
+
};
|
|
178
|
+
var getShowByTitle = (title) => {
|
|
179
|
+
const t = title.toLowerCase();
|
|
180
|
+
return getShows().find((s) => {
|
|
181
|
+
if (s.title?.toLowerCase() === t) return true;
|
|
182
|
+
const folderTitle = (s.path.split("/").pop() ?? "").replace(/\s*\(\d{4}\).*$/, "").trim().toLowerCase();
|
|
183
|
+
return folderTitle === t;
|
|
184
|
+
}) ?? null;
|
|
185
|
+
};
|
|
186
|
+
var getCleanableImports = () => {
|
|
187
|
+
return db().prepare(`SELECT * FROM imports WHERE mode IN ('hardlink', 'copy') ORDER BY importedAt ASC`).all();
|
|
188
|
+
};
|
|
189
|
+
var deleteImport = (id) => {
|
|
190
|
+
db().prepare("DELETE FROM imports WHERE id = ?").run(id);
|
|
191
|
+
};
|
|
192
|
+
var getImportHistory = (limit = 10) => {
|
|
193
|
+
const sessionIds = db().prepare("SELECT sessionId FROM imports GROUP BY sessionId ORDER BY MAX(id) DESC LIMIT ?").all(limit);
|
|
194
|
+
return sessionIds.map(({ sessionId }) => ({
|
|
195
|
+
sessionId,
|
|
196
|
+
records: db().prepare("SELECT * FROM imports WHERE sessionId = ? ORDER BY id ASC").all(sessionId)
|
|
197
|
+
}));
|
|
198
|
+
};
|
|
199
|
+
var getHistory = (limit = 10) => {
|
|
200
|
+
const sessionIds = db().prepare("SELECT sessionId FROM renameHistory GROUP BY sessionId ORDER BY MAX(id) DESC LIMIT ?").all(limit);
|
|
201
|
+
return sessionIds.map(({ sessionId }) => ({
|
|
202
|
+
sessionId,
|
|
203
|
+
records: db().prepare("SELECT * FROM renameHistory WHERE sessionId = ? ORDER BY id DESC").all(sessionId)
|
|
204
|
+
}));
|
|
121
205
|
};
|
|
122
206
|
|
|
123
207
|
// src/refs/spinner.ts
|
|
@@ -170,39 +254,140 @@ var Spinner = class {
|
|
|
170
254
|
};
|
|
171
255
|
var spinner_default = new Spinner();
|
|
172
256
|
|
|
257
|
+
// src/actions/clean.ts
|
|
258
|
+
var parseOlderThan = (s) => {
|
|
259
|
+
const match = s.match(/^(\d+)([dhm])$/);
|
|
260
|
+
if (!match) return null;
|
|
261
|
+
const n = parseInt(match[1]);
|
|
262
|
+
const unit = match[2];
|
|
263
|
+
const ms = unit === "d" ? n * 864e5 : unit === "h" ? n * 36e5 : n * 6e4;
|
|
264
|
+
return ms;
|
|
265
|
+
};
|
|
266
|
+
var clean = async ({ dryRun, olderThan }) => {
|
|
267
|
+
spinner_default.start();
|
|
268
|
+
const imports = getCleanableImports();
|
|
269
|
+
if (imports.length === 0) {
|
|
270
|
+
spinner_default.info("nothing to clean");
|
|
271
|
+
spinner_default.stop();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const cutoffMs = olderThan ? parseOlderThan(olderThan) : null;
|
|
275
|
+
if (olderThan && cutoffMs === null) throw new Error(`invalid --older-than format, expected e.g. 14d, 6h, 30m`);
|
|
276
|
+
let cleaned = 0, skipped = 0;
|
|
277
|
+
for (const imp of imports) {
|
|
278
|
+
if (cutoffMs !== null) {
|
|
279
|
+
const age = Date.now() - new Date(imp.importedAt).getTime();
|
|
280
|
+
if (age < cutoffMs) {
|
|
281
|
+
skipped++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!(0, import_fs2.existsSync)(imp.sourcePath)) {
|
|
286
|
+
if (!dryRun) deleteImport(imp.id);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (dryRun) {
|
|
290
|
+
spinner_default.succeed(`[dry] would remove ${import_cosmetic.default.blue.encoder(imp.sourcePath)}`);
|
|
291
|
+
cleaned++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
(0, import_fs2.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
296
|
+
deleteImport(imp.id);
|
|
297
|
+
spinner_default.succeed(`removed ${import_cosmetic.default.blue.encoder(imp.sourcePath)}`);
|
|
298
|
+
cleaned++;
|
|
299
|
+
} catch {
|
|
300
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${import_cosmetic.default.blue.encoder(imp.sourcePath)}`);
|
|
301
|
+
skipped++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
spinner_default.succeed(`cleaned ${cleaned} items`);
|
|
305
|
+
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
306
|
+
spinner_default.stop();
|
|
307
|
+
};
|
|
308
|
+
var clean_default = clean;
|
|
309
|
+
|
|
310
|
+
// src/actions/config.ts
|
|
311
|
+
var import_cosmetic2 = __toESM(require("cosmetic"));
|
|
312
|
+
var import_path3 = require("path");
|
|
313
|
+
|
|
314
|
+
// src/config.ts
|
|
315
|
+
var import_fs3 = require("fs");
|
|
316
|
+
var import_os2 = require("os");
|
|
317
|
+
var import_path2 = require("path");
|
|
318
|
+
var CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".config", "reelsort");
|
|
319
|
+
var CONFIG_PATH = (0, import_path2.join)(CONFIG_DIR, "config.json");
|
|
320
|
+
var getConfig = () => {
|
|
321
|
+
if (!(0, import_fs3.existsSync)(CONFIG_PATH)) return { sources: [], dest: {} };
|
|
322
|
+
return JSON.parse((0, import_fs3.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
323
|
+
};
|
|
324
|
+
var saveConfig = (config) => {
|
|
325
|
+
if (!(0, import_fs3.existsSync)(CONFIG_DIR)) (0, import_fs3.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
326
|
+
(0, import_fs3.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/helpers/formatEpisode.ts
|
|
330
|
+
var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
|
|
331
|
+
var DEFAULT_SEASON_FORMAT = "Season {s}";
|
|
332
|
+
var renderEpisode = (format, season, episode, title, name) => {
|
|
333
|
+
return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
|
|
334
|
+
};
|
|
335
|
+
var formatSeasonFolder = (format, season) => format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).trim();
|
|
336
|
+
var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
|
|
337
|
+
if (double) {
|
|
338
|
+
const a = renderEpisode(format, season, episode, title, name);
|
|
339
|
+
const b = renderEpisode(format, season, episode + 1, title, name);
|
|
340
|
+
return `${a}-${b}`;
|
|
341
|
+
}
|
|
342
|
+
return renderEpisode(format, season, episode, title, name);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/helpers/formatName.ts
|
|
346
|
+
var DEFAULT_MOVIE_FORMAT = "{title} ({year})";
|
|
347
|
+
var formatMovieName = (template, title, year, edition) => {
|
|
348
|
+
const editionTag = edition ? ` {edition-${edition}}` : "";
|
|
349
|
+
let result = template.replace("{title}", title).replace("{edition}", editionTag);
|
|
350
|
+
if (year) {
|
|
351
|
+
result = result.replace("{year}", year.toString());
|
|
352
|
+
} else {
|
|
353
|
+
result = result.replace(/\s*[([{]\{year\}[)\]}]/g, "").replace("{year}", "");
|
|
354
|
+
}
|
|
355
|
+
return result.replace(/\s+/g, " ").trim();
|
|
356
|
+
};
|
|
357
|
+
|
|
173
358
|
// src/actions/config.ts
|
|
174
359
|
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
175
360
|
var configAdd = async ({ key, value }) => {
|
|
176
361
|
if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
|
|
177
|
-
const dir = (0,
|
|
362
|
+
const dir = (0, import_path3.resolve)(value);
|
|
178
363
|
const config = getConfig();
|
|
179
364
|
if (config.sources.includes(dir)) {
|
|
180
365
|
spinner_default.start();
|
|
181
|
-
spinner_default.info(`source already configured: ${
|
|
366
|
+
spinner_default.info(`source already configured: ${import_cosmetic2.default.blue.encoder(dir)}`);
|
|
182
367
|
spinner_default.stop();
|
|
183
368
|
return;
|
|
184
369
|
}
|
|
185
370
|
config.sources.push(dir);
|
|
186
371
|
saveConfig(config);
|
|
187
372
|
spinner_default.start();
|
|
188
|
-
spinner_default.succeed(`added source: ${
|
|
373
|
+
spinner_default.succeed(`added source: ${import_cosmetic2.default.blue.encoder(dir)}`);
|
|
189
374
|
spinner_default.stop();
|
|
190
375
|
};
|
|
191
376
|
var configRemove = async ({ key, value }) => {
|
|
192
377
|
if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
|
|
193
|
-
const dir = (0,
|
|
378
|
+
const dir = (0, import_path3.resolve)(value);
|
|
194
379
|
const config = getConfig();
|
|
195
380
|
const index = config.sources.indexOf(dir);
|
|
196
381
|
if (index === -1) {
|
|
197
382
|
spinner_default.start();
|
|
198
|
-
spinner_default.warn(`source not found: ${
|
|
383
|
+
spinner_default.warn(`source not found: ${import_cosmetic2.default.blue.encoder(dir)}`);
|
|
199
384
|
spinner_default.stop();
|
|
200
385
|
return;
|
|
201
386
|
}
|
|
202
387
|
config.sources.splice(index, 1);
|
|
203
388
|
saveConfig(config);
|
|
204
389
|
spinner_default.start();
|
|
205
|
-
spinner_default.succeed(`removed source: ${
|
|
390
|
+
spinner_default.succeed(`removed source: ${import_cosmetic2.default.blue.encoder(dir)}`);
|
|
206
391
|
spinner_default.stop();
|
|
207
392
|
};
|
|
208
393
|
var configSet = async ({ key, subkey, value }) => {
|
|
@@ -211,7 +396,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
211
396
|
config.language = subkey;
|
|
212
397
|
saveConfig(config);
|
|
213
398
|
spinner_default.start();
|
|
214
|
-
spinner_default.succeed(`set subtitle language: ${
|
|
399
|
+
spinner_default.succeed(`set subtitle language: ${import_cosmetic2.default.cyan.encoder(subkey)}`);
|
|
215
400
|
spinner_default.stop();
|
|
216
401
|
return;
|
|
217
402
|
}
|
|
@@ -241,7 +426,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
241
426
|
}
|
|
242
427
|
saveConfig(config);
|
|
243
428
|
spinner_default.start();
|
|
244
|
-
spinner_default.succeed(`set ${subkey} format: ${
|
|
429
|
+
spinner_default.succeed(`set ${subkey} format: ${import_cosmetic2.default.cyan.encoder(value ?? subkey)}`);
|
|
245
430
|
spinner_default.stop();
|
|
246
431
|
return;
|
|
247
432
|
}
|
|
@@ -250,20 +435,20 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
250
435
|
throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
|
|
251
436
|
}
|
|
252
437
|
if (!value) throw new Error(`missing path for dest ${subkey}`);
|
|
253
|
-
const dir = (0,
|
|
438
|
+
const dir = (0, import_path3.resolve)(value);
|
|
254
439
|
config.dest[subkey] = dir;
|
|
255
440
|
saveConfig(config);
|
|
256
441
|
spinner_default.start();
|
|
257
|
-
spinner_default.succeed(`set ${subkey} destination: ${
|
|
442
|
+
spinner_default.succeed(`set ${subkey} destination: ${import_cosmetic2.default.cyan.encoder(dir)}`);
|
|
258
443
|
spinner_default.stop();
|
|
259
444
|
};
|
|
260
|
-
var configShow = async (
|
|
445
|
+
var configShow = async () => {
|
|
261
446
|
const config = getConfig();
|
|
262
447
|
console.log("\nSources:");
|
|
263
448
|
if (config.sources.length === 0) {
|
|
264
449
|
console.log(" (none)");
|
|
265
450
|
} else {
|
|
266
|
-
for (const s of config.sources) console.log(` ${
|
|
451
|
+
for (const s of config.sources) console.log(` ${import_cosmetic2.default.blue.encoder(s)}`);
|
|
267
452
|
}
|
|
268
453
|
console.log("\nDestinations:");
|
|
269
454
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -271,147 +456,185 @@ var configShow = async (_) => {
|
|
|
271
456
|
console.log(" (none)");
|
|
272
457
|
} else {
|
|
273
458
|
for (const { type, path } of entries) {
|
|
274
|
-
console.log(` ${type.padEnd(6)} ${
|
|
459
|
+
console.log(` ${type.padEnd(6)} ${import_cosmetic2.default.cyan.encoder(path)}`);
|
|
275
460
|
}
|
|
276
461
|
}
|
|
277
462
|
console.log(`
|
|
278
|
-
Subtitle language: ${
|
|
279
|
-
console.log(`TMDb API key: ${config.tmdbApiKey ?
|
|
280
|
-
console.log(`Movie format: ${
|
|
281
|
-
console.log(`Episode format: ${
|
|
282
|
-
console.log(`Season folder: ${
|
|
463
|
+
Subtitle language: ${import_cosmetic2.default.cyan.encoder(config.language ?? "eng (default)")}`);
|
|
464
|
+
console.log(`TMDb API key: ${config.tmdbApiKey ? import_cosmetic2.default.green.encoder("configured") : import_cosmetic2.default.red.encoder("not set")}`);
|
|
465
|
+
console.log(`Movie format: ${import_cosmetic2.default.cyan.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
466
|
+
console.log(`Episode format: ${import_cosmetic2.default.cyan.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
467
|
+
console.log(`Season folder: ${import_cosmetic2.default.cyan.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
283
468
|
console.log();
|
|
284
469
|
};
|
|
285
470
|
|
|
286
|
-
// src/actions/
|
|
287
|
-
var
|
|
288
|
-
var
|
|
289
|
-
var import_fs3 = require("fs");
|
|
471
|
+
// src/actions/differences.ts
|
|
472
|
+
var import_cosmetic3 = __toESM(require("cosmetic"));
|
|
473
|
+
var import_fs4 = require("fs");
|
|
290
474
|
var import_path4 = require("path");
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
addedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
312
|
-
);
|
|
313
|
-
CREATE TABLE IF NOT EXISTS renameHistory (
|
|
314
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
315
|
-
sessionId TEXT NOT NULL,
|
|
316
|
-
oldPath TEXT NOT NULL,
|
|
317
|
-
newPath TEXT NOT NULL,
|
|
318
|
-
renamedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
319
|
-
);
|
|
320
|
-
CREATE TABLE IF NOT EXISTS mediaInfo (
|
|
321
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
322
|
-
filePath TEXT NOT NULL UNIQUE,
|
|
323
|
-
codec TEXT,
|
|
324
|
-
resolution TEXT,
|
|
325
|
-
width INTEGER,
|
|
326
|
-
height INTEGER,
|
|
327
|
-
duration REAL,
|
|
328
|
-
probedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
329
|
-
);
|
|
330
|
-
CREATE TABLE IF NOT EXISTS imports (
|
|
331
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
332
|
-
sessionId TEXT NOT NULL,
|
|
333
|
-
sourcePath TEXT NOT NULL,
|
|
334
|
-
destinationPath TEXT NOT NULL,
|
|
335
|
-
mode TEXT NOT NULL,
|
|
336
|
-
tmdbId INTEGER,
|
|
337
|
-
importedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
338
|
-
)
|
|
339
|
-
`);
|
|
340
|
-
try {
|
|
341
|
-
_db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
|
|
342
|
-
} catch {
|
|
475
|
+
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
476
|
+
let dir1 = rawDir1;
|
|
477
|
+
let dir2 = rawDir2;
|
|
478
|
+
spinner_default.text = `checking differences between ${import_cosmetic3.default.blue.encoder(dir1)} and ${import_cosmetic3.default.blue.encoder(dir2)}`;
|
|
479
|
+
spinner_default.start();
|
|
480
|
+
dir1 = (0, import_path4.resolve)(dir1);
|
|
481
|
+
dir2 = (0, import_path4.resolve)(dir2);
|
|
482
|
+
if (!(0, import_fs4.existsSync)(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
|
|
483
|
+
if (!(0, import_fs4.existsSync)(dir2)) throw new Error(`dir2 ${dir2} does not exist`);
|
|
484
|
+
let list1 = (0, import_fs4.readdirSync)(dir1);
|
|
485
|
+
let list2 = (0, import_fs4.readdirSync)(dir2);
|
|
486
|
+
if (only && only.length) {
|
|
487
|
+
list1 = list1.filter((i) => {
|
|
488
|
+
for (const o of only) if (i.endsWith(o)) return true;
|
|
489
|
+
return false;
|
|
490
|
+
});
|
|
491
|
+
list2 = list2.filter((i) => {
|
|
492
|
+
for (const o of only) if (i.endsWith(o)) return true;
|
|
493
|
+
return false;
|
|
494
|
+
});
|
|
343
495
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
width = excluded.width, height = excluded.height,
|
|
369
|
-
duration = excluded.duration, probedAt = excluded.probedAt`).run(filePath, codec, resolution, width, height, duration);
|
|
370
|
-
};
|
|
371
|
-
var getImportByDest = (destPath) => {
|
|
372
|
-
return db().prepare("SELECT * FROM imports WHERE destinationPath = ? LIMIT 1").get(destPath);
|
|
496
|
+
if (ignore && ignore.length) {
|
|
497
|
+
list1 = list1.filter((i) => {
|
|
498
|
+
for (const o of ignore) if (i.endsWith(o)) return false;
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
list2 = list2.filter((i) => {
|
|
502
|
+
for (const o of ignore) if (i.endsWith(o)) return false;
|
|
503
|
+
return true;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const added = [], removed = [];
|
|
507
|
+
for (const l of list1) {
|
|
508
|
+
if (list2.includes(l)) {
|
|
509
|
+
added.push(l);
|
|
510
|
+
} else {
|
|
511
|
+
removed.push(l);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
spinner_default.succeed(`checked differences between ${import_cosmetic3.default.blue.encoder(dir1)} and ${import_cosmetic3.default.blue.encoder(dir2)}`);
|
|
515
|
+
spinner_default.succeed(`found ${added.length} added files`);
|
|
516
|
+
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
517
|
+
spinner_default.stop();
|
|
518
|
+
for (const i of added) console.log(`${import_cosmetic3.default.green.encoder("added")} ${i}`);
|
|
519
|
+
for (const i of removed) console.log(`${import_cosmetic3.default.red.encoder("removed")} ${i}`);
|
|
373
520
|
};
|
|
374
|
-
var
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
var
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
521
|
+
var differences_default = differences;
|
|
522
|
+
|
|
523
|
+
// src/actions/history.ts
|
|
524
|
+
var import_cosmetic4 = __toESM(require("cosmetic"));
|
|
525
|
+
var import_path5 = require("path");
|
|
526
|
+
var history = async ({ limit, imports }) => {
|
|
527
|
+
if (imports) {
|
|
528
|
+
const sessions = getImportHistory(limit ?? 10);
|
|
529
|
+
if (sessions.length === 0) {
|
|
530
|
+
console.log("no import history found");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const session of sessions) {
|
|
534
|
+
const date = new Date(session.sessionId);
|
|
535
|
+
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
536
|
+
console.log(`
|
|
537
|
+
${import_cosmetic4.default.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
538
|
+
for (const r of session.records) {
|
|
539
|
+
const src = (0, import_path5.basename)(r.sourcePath);
|
|
540
|
+
const dest = import_cosmetic4.default.cyan.encoder(r.destinationPath);
|
|
541
|
+
const mode = r.mode !== "move" ? ` ${import_cosmetic4.default.blue.encoder(`[${r.mode}]`)}` : "";
|
|
542
|
+
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
const sessions = getHistory(limit ?? 10);
|
|
547
|
+
if (sessions.length === 0) {
|
|
548
|
+
console.log("no history found");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
for (const session of sessions) {
|
|
552
|
+
const date = new Date(session.sessionId);
|
|
553
|
+
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
554
|
+
const folders = session.records.filter((r) => (0, import_path5.extname)(r.newPath) === "");
|
|
555
|
+
console.log(`
|
|
556
|
+
${import_cosmetic4.default.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
|
|
557
|
+
for (const r of folders) {
|
|
558
|
+
const oldName = (0, import_path5.basename)(r.oldPath);
|
|
559
|
+
const newName = (0, import_path5.basename)(r.newPath);
|
|
560
|
+
console.log(` ${import_cosmetic4.default.blue.encoder(oldName)} \u2192 ${import_cosmetic4.default.cyan.encoder(newName)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
console.log();
|
|
386
565
|
};
|
|
387
|
-
var
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
566
|
+
var history_default = history;
|
|
567
|
+
|
|
568
|
+
// src/actions/list.ts
|
|
569
|
+
var import_cosmetic5 = __toESM(require("cosmetic"));
|
|
570
|
+
var import_fs6 = require("fs");
|
|
571
|
+
var import_path7 = require("path");
|
|
572
|
+
|
|
573
|
+
// src/helpers/dirSize.ts
|
|
574
|
+
var import_fs5 = require("fs");
|
|
575
|
+
var import_path6 = require("path");
|
|
576
|
+
var dirSize = (dir) => {
|
|
577
|
+
let total = 0;
|
|
578
|
+
try {
|
|
579
|
+
for (const entry of (0, import_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
580
|
+
const full = (0, import_path6.resolve)(dir, entry.name);
|
|
581
|
+
if (entry.isDirectory()) {
|
|
582
|
+
total += dirSize(full);
|
|
583
|
+
} else if (entry.isFile()) {
|
|
584
|
+
try {
|
|
585
|
+
total += (0, import_fs5.statSync)(full).size;
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
return total;
|
|
394
593
|
};
|
|
395
|
-
var
|
|
396
|
-
|
|
594
|
+
var formatSize = (bytes) => {
|
|
595
|
+
if (bytes >= 1099511627776) return `${(bytes / 1099511627776).toFixed(1)} TB`;
|
|
596
|
+
if (bytes >= 1073741824) return `${(bytes / 1073741824).toFixed(1)} GB`;
|
|
597
|
+
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
598
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
397
599
|
};
|
|
398
|
-
|
|
399
|
-
|
|
600
|
+
|
|
601
|
+
// src/helpers/parseQuality.ts
|
|
602
|
+
var RESOLUTION_MAP = {
|
|
603
|
+
"480p": "480p",
|
|
604
|
+
"576p": "576p",
|
|
605
|
+
"720p": "720p",
|
|
606
|
+
"1080p": "1080p",
|
|
607
|
+
"2160p": "2160p",
|
|
608
|
+
"4k": "2160p",
|
|
609
|
+
uhd: "2160p",
|
|
610
|
+
"8k": "8K"
|
|
400
611
|
};
|
|
401
|
-
var
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
612
|
+
var CODEC_MAP = {
|
|
613
|
+
x264: "x264",
|
|
614
|
+
h264: "x264",
|
|
615
|
+
avc: "x264",
|
|
616
|
+
x265: "x265",
|
|
617
|
+
h265: "x265",
|
|
618
|
+
hevc: "x265",
|
|
619
|
+
xvid: "XviD",
|
|
620
|
+
divx: "DivX"
|
|
407
621
|
};
|
|
408
|
-
var
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
622
|
+
var parseQuality = (filename) => {
|
|
623
|
+
const tokens = filename.toLowerCase().split(/[.\s_\-/\\]+/);
|
|
624
|
+
let resolution;
|
|
625
|
+
let codec;
|
|
626
|
+
for (const token of tokens) {
|
|
627
|
+
if (!resolution && RESOLUTION_MAP[token]) resolution = RESOLUTION_MAP[token];
|
|
628
|
+
if (!codec && CODEC_MAP[token]) codec = CODEC_MAP[token];
|
|
629
|
+
if (resolution && codec) break;
|
|
630
|
+
}
|
|
631
|
+
return { resolution, codec };
|
|
414
632
|
};
|
|
633
|
+
var normalizeResolution = (s) => RESOLUTION_MAP[s.toLowerCase()] ?? s;
|
|
634
|
+
var normalizeCodec = (s) => CODEC_MAP[s.toLowerCase()] ?? s;
|
|
635
|
+
|
|
636
|
+
// src/refs/subtitleExtensions.json
|
|
637
|
+
var subtitleExtensions_default = ["srt", "sub", "idx", "ass", "ssa", "vtt", "sup"];
|
|
415
638
|
|
|
416
639
|
// src/refs/videoExtensions.json
|
|
417
640
|
var videoExtensions_default = [
|
|
@@ -450,26 +673,114 @@ var videoExtensions_default = [
|
|
|
450
673
|
"yuv"
|
|
451
674
|
];
|
|
452
675
|
|
|
453
|
-
// src/actions/
|
|
676
|
+
// src/actions/list.ts
|
|
454
677
|
var DEST_TYPES2 = ["movie", "tv", "ps3"];
|
|
455
|
-
var
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
mpeg4: "XviD",
|
|
465
|
-
mpeg2video: "MPEG-2"
|
|
678
|
+
var findVideoFile = (dir) => {
|
|
679
|
+
try {
|
|
680
|
+
return (0, import_fs6.readdirSync)(dir).find((f) => {
|
|
681
|
+
const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
|
|
682
|
+
return ext && videoExtensions_default.includes(ext);
|
|
683
|
+
}) ?? null;
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
466
687
|
};
|
|
467
|
-
var
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
688
|
+
var hasSubtitle = (dir) => {
|
|
689
|
+
try {
|
|
690
|
+
return (0, import_fs6.readdirSync)(dir).some((f) => {
|
|
691
|
+
const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
|
|
692
|
+
return ext && subtitleExtensions_default.includes(ext);
|
|
693
|
+
});
|
|
694
|
+
} catch {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var parseLibraryFolder = (name) => {
|
|
699
|
+
const match = name.match(/^(.+?)\s*\((\d{4})\)/);
|
|
700
|
+
if (match) return { title: match[1].trim(), year: parseInt(match[2]) };
|
|
701
|
+
return { title: name };
|
|
702
|
+
};
|
|
703
|
+
var col = (s, width) => s.padEnd(width).substring(0, width);
|
|
704
|
+
var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter, sort }) => {
|
|
705
|
+
const config = getConfig();
|
|
706
|
+
const types = (type ? [type] : DEST_TYPES2).filter((t) => config.dest[t]);
|
|
707
|
+
if (types.length === 0) throw new Error("no destinations configured \u2014 run: reelsort config set dest movie <dir>");
|
|
708
|
+
for (const t of types) {
|
|
709
|
+
const destRoot = config.dest[t];
|
|
710
|
+
if (!(0, import_fs6.existsSync)(destRoot)) {
|
|
711
|
+
console.log(`
|
|
712
|
+
${t.toUpperCase()} ${import_cosmetic5.default.blue.encoder(destRoot)} (not found)`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const folders = (0, import_fs6.readdirSync)(destRoot).filter((f) => {
|
|
716
|
+
try {
|
|
717
|
+
return (0, import_fs6.lstatSync)((0, import_path7.resolve)(destRoot, f)).isDirectory();
|
|
718
|
+
} catch {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const entries = folders.map((folder) => {
|
|
723
|
+
const folderPath = (0, import_path7.resolve)(destRoot, folder);
|
|
724
|
+
const parsed = parseLibraryFolder(folder);
|
|
725
|
+
const videoFile = findVideoFile(folderPath);
|
|
726
|
+
const mediaInfo = videoFile ? getMediaInfo((0, import_path7.resolve)(folderPath, videoFile)) : null;
|
|
727
|
+
const quality = mediaInfo ? { resolution: mediaInfo.resolution ?? void 0, codec: mediaInfo.codec ?? void 0 } : (() => {
|
|
728
|
+
const imp = getImportByDest(folderPath);
|
|
729
|
+
return imp ? parseQuality(imp.sourcePath) : {};
|
|
730
|
+
})();
|
|
731
|
+
return { ...parsed, ...quality, hasSub: hasSubtitle(folderPath), size: formatSize(dirSize(folderPath)) };
|
|
732
|
+
});
|
|
733
|
+
let filtered = entries;
|
|
734
|
+
if (missingSubs) filtered = filtered.filter((e) => !e.hasSub);
|
|
735
|
+
if (codecFilter) filtered = filtered.filter((e) => e.codec?.toLowerCase() === normalizeCodec(codecFilter).toLowerCase());
|
|
736
|
+
if (resFilter) filtered = filtered.filter((e) => e.resolution?.toLowerCase() === normalizeResolution(resFilter).toLowerCase());
|
|
737
|
+
filtered.sort((a, b) => {
|
|
738
|
+
if (sort === "title") return a.title.localeCompare(b.title);
|
|
739
|
+
const yearDiff = (b.year ?? 0) - (a.year ?? 0);
|
|
740
|
+
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
741
|
+
});
|
|
742
|
+
const titleW = Math.min(50, Math.max(10, ...filtered.map((e) => e.title.length)) + 2);
|
|
743
|
+
const divider = "\u2500".repeat(titleW + 44);
|
|
744
|
+
console.log(`
|
|
745
|
+
${import_cosmetic5.default.yellow.encoder(t.toUpperCase())} ${import_cosmetic5.default.blue.encoder(destRoot)}`);
|
|
746
|
+
console.log(divider);
|
|
747
|
+
console.log(`${"Title".padEnd(titleW)} ${"Year".padEnd(6)} ${"Res".padEnd(6)} ${"Codec".padEnd(6)} ${"Size".padEnd(10)} Sub`);
|
|
748
|
+
console.log(divider);
|
|
749
|
+
for (const e of filtered) {
|
|
750
|
+
const sub = e.hasSub ? import_cosmetic5.default.green.encoder("\u2713") : import_cosmetic5.default.red.encoder("\u2717");
|
|
751
|
+
console.log(`${col(e.title, titleW)} ${col(e.year?.toString() ?? "\u2014", 6)} ${col(e.resolution ?? "\u2014", 6)} ${col(e.codec ?? "\u2014", 6)} ${col(e.size, 10)} ${sub}`);
|
|
752
|
+
}
|
|
753
|
+
console.log(divider);
|
|
754
|
+
console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
|
|
755
|
+
}
|
|
756
|
+
console.log();
|
|
757
|
+
};
|
|
758
|
+
var list_default = list;
|
|
759
|
+
|
|
760
|
+
// src/actions/probe.ts
|
|
761
|
+
var import_child_process = require("child_process");
|
|
762
|
+
var import_cosmetic6 = __toESM(require("cosmetic"));
|
|
763
|
+
var import_fs7 = require("fs");
|
|
764
|
+
var import_path8 = require("path");
|
|
765
|
+
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
766
|
+
var CODEC_MAP2 = {
|
|
767
|
+
hevc: "x265",
|
|
768
|
+
h265: "x265",
|
|
769
|
+
h264: "x264",
|
|
770
|
+
avc: "x264",
|
|
771
|
+
av1: "AV1",
|
|
772
|
+
vp9: "VP9",
|
|
773
|
+
vp8: "VP8",
|
|
774
|
+
xvid: "XviD",
|
|
775
|
+
mpeg4: "XviD",
|
|
776
|
+
mpeg2video: "MPEG-2"
|
|
777
|
+
};
|
|
778
|
+
var deriveResolution = (width, height) => {
|
|
779
|
+
if (height >= 2160 || width >= 3840) return "2160p";
|
|
780
|
+
if (height >= 1080 || width >= 1920) return "1080p";
|
|
781
|
+
if (height >= 720 || width >= 1280) return "720p";
|
|
782
|
+
if (height >= 576 || width >= 768) return "576p";
|
|
783
|
+
return "480p";
|
|
473
784
|
};
|
|
474
785
|
var isFfprobeAvailable = () => {
|
|
475
786
|
const result = (0, import_child_process.spawnSync)("ffprobe", ["-version"], { encoding: "utf-8" });
|
|
@@ -485,7 +796,7 @@ var runFfprobe = (filePath) => {
|
|
|
485
796
|
const width = video.width ?? null;
|
|
486
797
|
const height = video.height ?? null;
|
|
487
798
|
return {
|
|
488
|
-
codec:
|
|
799
|
+
codec: CODEC_MAP2[video.codec_name?.toLowerCase()] ?? video.codec_name ?? null,
|
|
489
800
|
resolution: width && height ? deriveResolution(width, height) : null,
|
|
490
801
|
width: width ?? null,
|
|
491
802
|
height: height ?? null,
|
|
@@ -496,12 +807,12 @@ var runFfprobe = (filePath) => {
|
|
|
496
807
|
}
|
|
497
808
|
};
|
|
498
809
|
var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
|
|
499
|
-
if (!(0,
|
|
810
|
+
if (!(0, import_fs7.existsSync)(dir) || depth > maxDepth) return [];
|
|
500
811
|
const results = [];
|
|
501
|
-
for (const entry of (0,
|
|
502
|
-
const entryPath = (0,
|
|
812
|
+
for (const entry of (0, import_fs7.readdirSync)(dir)) {
|
|
813
|
+
const entryPath = (0, import_path8.resolve)(dir, entry);
|
|
503
814
|
try {
|
|
504
|
-
if ((0,
|
|
815
|
+
if ((0, import_fs7.lstatSync)(entryPath).isDirectory()) {
|
|
505
816
|
results.push(...walkVideoFiles(entryPath, depth + 1, maxDepth));
|
|
506
817
|
} else {
|
|
507
818
|
const ext = entry.match(/([^.]+$)/)?.[0]?.toLowerCase();
|
|
@@ -520,13 +831,13 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
520
831
|
return;
|
|
521
832
|
}
|
|
522
833
|
const config = getConfig();
|
|
523
|
-
const types = (type ? [type] :
|
|
834
|
+
const types = (type ? [type] : DEST_TYPES3).filter((t) => config.dest[t]);
|
|
524
835
|
if (types.length === 0) throw new Error("no destinations configured \u2014 run: reelsort config set dest movie <dir>");
|
|
525
836
|
let probed = 0, skipped = 0, failed = 0;
|
|
526
837
|
for (const t of types) {
|
|
527
838
|
const destRoot = config.dest[t];
|
|
528
|
-
if (!(0,
|
|
529
|
-
spinner_default.text = `scanning ${
|
|
839
|
+
if (!(0, import_fs7.existsSync)(destRoot)) continue;
|
|
840
|
+
spinner_default.text = `scanning ${import_cosmetic6.default.blue.encoder(destRoot)}`;
|
|
530
841
|
const files = walkVideoFiles(destRoot);
|
|
531
842
|
for (const filePath of files) {
|
|
532
843
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -534,7 +845,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
534
845
|
skipped++;
|
|
535
846
|
continue;
|
|
536
847
|
}
|
|
537
|
-
spinner_default.text = `probing ${
|
|
848
|
+
spinner_default.text = `probing ${import_cosmetic6.default.blue.encoder(filePath)}`;
|
|
538
849
|
const result = runFfprobe(filePath);
|
|
539
850
|
if (!result) {
|
|
540
851
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -553,186 +864,11 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
553
864
|
};
|
|
554
865
|
var probe_default = probe;
|
|
555
866
|
|
|
556
|
-
// src/actions/
|
|
557
|
-
var
|
|
558
|
-
var
|
|
559
|
-
var
|
|
560
|
-
|
|
561
|
-
// src/helpers/dirSize.ts
|
|
562
|
-
var import_fs4 = require("fs");
|
|
563
|
-
var import_path5 = require("path");
|
|
564
|
-
var dirSize = (dir) => {
|
|
565
|
-
let total = 0;
|
|
566
|
-
try {
|
|
567
|
-
for (const entry of (0, import_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
568
|
-
const full = (0, import_path5.resolve)(dir, entry.name);
|
|
569
|
-
if (entry.isDirectory()) {
|
|
570
|
-
total += dirSize(full);
|
|
571
|
-
} else if (entry.isFile()) {
|
|
572
|
-
try {
|
|
573
|
-
total += (0, import_fs4.statSync)(full).size;
|
|
574
|
-
} catch {
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
} catch {
|
|
579
|
-
}
|
|
580
|
-
return total;
|
|
581
|
-
};
|
|
582
|
-
var formatSize = (bytes) => {
|
|
583
|
-
if (bytes >= 1099511627776) return `${(bytes / 1099511627776).toFixed(1)} TB`;
|
|
584
|
-
if (bytes >= 1073741824) return `${(bytes / 1073741824).toFixed(1)} GB`;
|
|
585
|
-
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
586
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
// src/helpers/parseQuality.ts
|
|
590
|
-
var RESOLUTION_MAP = {
|
|
591
|
-
"480p": "480p",
|
|
592
|
-
"576p": "576p",
|
|
593
|
-
"720p": "720p",
|
|
594
|
-
"1080p": "1080p",
|
|
595
|
-
"2160p": "2160p",
|
|
596
|
-
"4k": "2160p",
|
|
597
|
-
"uhd": "2160p",
|
|
598
|
-
"8k": "8K"
|
|
599
|
-
};
|
|
600
|
-
var CODEC_MAP2 = {
|
|
601
|
-
x264: "x264",
|
|
602
|
-
h264: "x264",
|
|
603
|
-
avc: "x264",
|
|
604
|
-
x265: "x265",
|
|
605
|
-
h265: "x265",
|
|
606
|
-
hevc: "x265",
|
|
607
|
-
xvid: "XviD",
|
|
608
|
-
divx: "DivX"
|
|
609
|
-
};
|
|
610
|
-
var parseQuality = (filename) => {
|
|
611
|
-
const tokens = filename.toLowerCase().split(/[.\s_\-/\\]+/);
|
|
612
|
-
let resolution;
|
|
613
|
-
let codec;
|
|
614
|
-
for (const token of tokens) {
|
|
615
|
-
if (!resolution && RESOLUTION_MAP[token]) resolution = RESOLUTION_MAP[token];
|
|
616
|
-
if (!codec && CODEC_MAP2[token]) codec = CODEC_MAP2[token];
|
|
617
|
-
if (resolution && codec) break;
|
|
618
|
-
}
|
|
619
|
-
return { resolution, codec };
|
|
620
|
-
};
|
|
621
|
-
var normalizeResolution = (s) => RESOLUTION_MAP[s.toLowerCase()] ?? s;
|
|
622
|
-
var normalizeCodec = (s) => CODEC_MAP2[s.toLowerCase()] ?? s;
|
|
623
|
-
|
|
624
|
-
// src/refs/subtitleExtensions.json
|
|
625
|
-
var subtitleExtensions_default = ["srt", "sub", "idx", "ass", "ssa", "vtt", "sup"];
|
|
626
|
-
|
|
627
|
-
// src/actions/list.ts
|
|
628
|
-
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
629
|
-
var findVideoFile = (dir) => {
|
|
630
|
-
try {
|
|
631
|
-
return (0, import_fs5.readdirSync)(dir).find((f) => {
|
|
632
|
-
const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
|
|
633
|
-
return ext && videoExtensions_default.includes(ext);
|
|
634
|
-
}) ?? null;
|
|
635
|
-
} catch {
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
var hasSubtitle = (dir) => {
|
|
640
|
-
try {
|
|
641
|
-
return (0, import_fs5.readdirSync)(dir).some((f) => {
|
|
642
|
-
const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
|
|
643
|
-
return ext && subtitleExtensions_default.includes(ext);
|
|
644
|
-
});
|
|
645
|
-
} catch {
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
var parseLibraryFolder = (name) => {
|
|
650
|
-
const match = name.match(/^(.+?)\s*\((\d{4})\)/);
|
|
651
|
-
if (match) return { title: match[1].trim(), year: parseInt(match[2]) };
|
|
652
|
-
return { title: name };
|
|
653
|
-
};
|
|
654
|
-
var col = (s, width) => s.padEnd(width).substring(0, width);
|
|
655
|
-
var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter, sort }) => {
|
|
656
|
-
const config = getConfig();
|
|
657
|
-
const types = (type ? [type] : DEST_TYPES3).filter((t) => config.dest[t]);
|
|
658
|
-
if (types.length === 0) throw new Error("no destinations configured \u2014 run: reelsort config set dest movie <dir>");
|
|
659
|
-
for (const t of types) {
|
|
660
|
-
const destRoot = config.dest[t];
|
|
661
|
-
if (!(0, import_fs5.existsSync)(destRoot)) {
|
|
662
|
-
console.log(`
|
|
663
|
-
${t.toUpperCase()} ${import_cosmetic3.default.blue.encoder(destRoot)} (not found)`);
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
const folders = (0, import_fs5.readdirSync)(destRoot).filter((f) => {
|
|
667
|
-
try {
|
|
668
|
-
return (0, import_fs5.lstatSync)((0, import_path6.resolve)(destRoot, f)).isDirectory();
|
|
669
|
-
} catch {
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
const entries = folders.map((folder) => {
|
|
674
|
-
const folderPath = (0, import_path6.resolve)(destRoot, folder);
|
|
675
|
-
const parsed = parseLibraryFolder(folder);
|
|
676
|
-
const videoFile = findVideoFile(folderPath);
|
|
677
|
-
const mediaInfo = videoFile ? getMediaInfo((0, import_path6.resolve)(folderPath, videoFile)) : null;
|
|
678
|
-
const quality = mediaInfo ? { resolution: mediaInfo.resolution ?? void 0, codec: mediaInfo.codec ?? void 0 } : (() => {
|
|
679
|
-
const imp = getImportByDest(folderPath);
|
|
680
|
-
return imp ? parseQuality(imp.sourcePath) : {};
|
|
681
|
-
})();
|
|
682
|
-
return { ...parsed, ...quality, hasSub: hasSubtitle(folderPath), size: formatSize(dirSize(folderPath)) };
|
|
683
|
-
});
|
|
684
|
-
let filtered = entries;
|
|
685
|
-
if (missingSubs) filtered = filtered.filter((e) => !e.hasSub);
|
|
686
|
-
if (codecFilter) filtered = filtered.filter((e) => e.codec?.toLowerCase() === normalizeCodec(codecFilter).toLowerCase());
|
|
687
|
-
if (resFilter) filtered = filtered.filter((e) => e.resolution?.toLowerCase() === normalizeResolution(resFilter).toLowerCase());
|
|
688
|
-
filtered.sort((a, b) => {
|
|
689
|
-
if (sort === "title") return a.title.localeCompare(b.title);
|
|
690
|
-
const yearDiff = (b.year ?? 0) - (a.year ?? 0);
|
|
691
|
-
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
692
|
-
});
|
|
693
|
-
const titleW = Math.min(50, Math.max(10, ...filtered.map((e) => e.title.length)) + 2);
|
|
694
|
-
const divider = "\u2500".repeat(titleW + 44);
|
|
695
|
-
console.log(`
|
|
696
|
-
${import_cosmetic3.default.yellow.encoder(t.toUpperCase())} ${import_cosmetic3.default.blue.encoder(destRoot)}`);
|
|
697
|
-
console.log(divider);
|
|
698
|
-
console.log(`${"Title".padEnd(titleW)} ${"Year".padEnd(6)} ${"Res".padEnd(6)} ${"Codec".padEnd(6)} ${"Size".padEnd(10)} Sub`);
|
|
699
|
-
console.log(divider);
|
|
700
|
-
for (const e of filtered) {
|
|
701
|
-
const sub = e.hasSub ? import_cosmetic3.default.green.encoder("\u2713") : import_cosmetic3.default.red.encoder("\u2717");
|
|
702
|
-
console.log(
|
|
703
|
-
`${col(e.title, titleW)} ${col(e.year?.toString() ?? "\u2014", 6)} ${col(e.resolution ?? "\u2014", 6)} ${col(e.codec ?? "\u2014", 6)} ${col(e.size, 10)} ${sub}`
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
console.log(divider);
|
|
707
|
-
console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
|
|
708
|
-
}
|
|
709
|
-
console.log();
|
|
710
|
-
};
|
|
711
|
-
var list_default = list;
|
|
712
|
-
|
|
713
|
-
// src/helpers/detectEdition.ts
|
|
714
|
-
var EDITIONS = [
|
|
715
|
-
{ pattern: /director.?s?.?cut/i, name: "Director's Cut" },
|
|
716
|
-
{ pattern: /final\.?cut/i, name: "Final Cut" },
|
|
717
|
-
{ pattern: /extended\.?(cut|edition|version)?/i, name: "Extended" },
|
|
718
|
-
{ pattern: /theatrical\.?(cut|version)?/i, name: "Theatrical" },
|
|
719
|
-
{ pattern: /unrated\.?(cut|version)?/i, name: "Unrated" },
|
|
720
|
-
{ pattern: /anniversary\.?edition/i, name: "Anniversary Edition" },
|
|
721
|
-
{ pattern: /collector.?s?.?(edition|cut)/i, name: "Collector's Edition" },
|
|
722
|
-
{ pattern: /special\.?edition/i, name: "Special Edition" }
|
|
723
|
-
];
|
|
724
|
-
var detectEdition = (filename) => {
|
|
725
|
-
for (const { pattern, name } of EDITIONS) {
|
|
726
|
-
if (pattern.test(filename)) return name;
|
|
727
|
-
}
|
|
728
|
-
return null;
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
// src/actions/watch.ts
|
|
732
|
-
var import_chokidar = __toESM(require("chokidar"));
|
|
733
|
-
var import_cosmetic4 = __toESM(require("cosmetic"));
|
|
734
|
-
var import_fs6 = require("fs");
|
|
735
|
-
var import_path7 = require("path");
|
|
867
|
+
// src/actions/rename.ts
|
|
868
|
+
var import_cosmetic7 = __toESM(require("cosmetic"));
|
|
869
|
+
var import_fs8 = require("fs");
|
|
870
|
+
var import_path9 = require("path");
|
|
871
|
+
var import_rimraf = require("rimraf");
|
|
736
872
|
|
|
737
873
|
// src/helpers/findSubtitle.ts
|
|
738
874
|
var LANGUAGE_ALIASES = {
|
|
@@ -786,69 +922,200 @@ var titleCase_default = (s) => {
|
|
|
786
922
|
return s;
|
|
787
923
|
};
|
|
788
924
|
|
|
789
|
-
// src/
|
|
790
|
-
var
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
"
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
if (!
|
|
851
|
-
|
|
925
|
+
// src/actions/rename.ts
|
|
926
|
+
var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
927
|
+
const dir = (0, import_path9.resolve)(inputDir);
|
|
928
|
+
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
929
|
+
const config = getConfig();
|
|
930
|
+
const language = config.language ?? "eng";
|
|
931
|
+
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
932
|
+
spinner_default.text = `renaming in ${import_cosmetic7.default.blue.encoder(dir)}`;
|
|
933
|
+
spinner_default.start();
|
|
934
|
+
if (!(0, import_fs8.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
935
|
+
const list2 = (0, import_fs8.readdirSync)(dir);
|
|
936
|
+
let renamed = 0, removed = 0, skipped = 0;
|
|
937
|
+
for (const [index, entry] of list2.entries()) {
|
|
938
|
+
spinner_default.text = `renaming in ${import_cosmetic7.default.blue.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
939
|
+
if (!(0, import_fs8.lstatSync)((0, import_path9.resolve)(dir, entry)).isDirectory()) {
|
|
940
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
941
|
+
skipped++;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const isPs3Candidate = /(?<=\[).+?(?=\])/.test(entry);
|
|
945
|
+
const usePs3 = type === "ps3" || !type && isPs3Candidate;
|
|
946
|
+
if (usePs3) {
|
|
947
|
+
const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
|
|
948
|
+
const id = entry.split("-")[0];
|
|
949
|
+
if (!nameMatch || !id) {
|
|
950
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
951
|
+
skipped++;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const ps3Old = (0, import_path9.resolve)(dir, entry);
|
|
955
|
+
const ps3New = (0, import_path9.resolve)(dir, `${nameMatch[0]} [${id}]`);
|
|
956
|
+
(0, import_fs8.renameSync)(ps3Old, ps3New);
|
|
957
|
+
recordRename(sessionId, ps3Old, ps3New);
|
|
958
|
+
spinner_default.succeed(`${nameMatch[0]} [${id}]`);
|
|
959
|
+
renamed++;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
|
|
963
|
+
if (!yearMatch) {
|
|
964
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
965
|
+
skipped++;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const year = yearMatch[0];
|
|
969
|
+
if (year.length !== 6) {
|
|
970
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
971
|
+
skipped++;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const title = titleCase_default(entry.substring(0, entry.indexOf(year)).trim());
|
|
975
|
+
const sublist = (0, import_fs8.readdirSync)((0, import_path9.resolve)(dir, entry));
|
|
976
|
+
const video = sublist.find((f) => {
|
|
977
|
+
const ext2 = f.match(/([^.]+$)/)?.[0];
|
|
978
|
+
return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
979
|
+
});
|
|
980
|
+
if (!video) {
|
|
981
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
982
|
+
skipped++;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const ext = video.match(/([^.]+$)/)?.[0];
|
|
986
|
+
if (!ext) {
|
|
987
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
988
|
+
skipped++;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const yearNum = parseInt(year.replace(/\D/g, ""));
|
|
992
|
+
const formatted = formatMovieName(movieFormat, title, yearNum);
|
|
993
|
+
if (entry === formatted && video === `${formatted}.${ext}`) {
|
|
994
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
995
|
+
skipped++;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const subtitle = findSubtitle(sublist, language);
|
|
999
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1000
|
+
const keep = new Set([video, subtitle].filter(Boolean));
|
|
1001
|
+
const others = sublist.filter((f) => !keep.has(f));
|
|
1002
|
+
for (const f of others) {
|
|
1003
|
+
await (0, import_rimraf.rimraf)((0, import_path9.resolve)(dir, entry, f));
|
|
1004
|
+
removed++;
|
|
1005
|
+
}
|
|
1006
|
+
const fileOld = (0, import_path9.resolve)(dir, entry, video);
|
|
1007
|
+
const fileNew = (0, import_path9.resolve)(dir, entry, `${formatted}.${ext}`);
|
|
1008
|
+
const folderOld = (0, import_path9.resolve)(dir, entry);
|
|
1009
|
+
const folderNew = (0, import_path9.resolve)(dir, formatted);
|
|
1010
|
+
(0, import_fs8.renameSync)(fileOld, fileNew);
|
|
1011
|
+
if (subtitle && subtitleExt) {
|
|
1012
|
+
(0, import_fs8.renameSync)((0, import_path9.resolve)(dir, entry, subtitle), (0, import_path9.resolve)(dir, entry, `${formatted}.${subtitleExt}`));
|
|
1013
|
+
}
|
|
1014
|
+
(0, import_fs8.renameSync)(folderOld, folderNew);
|
|
1015
|
+
recordRename(sessionId, fileOld, fileNew);
|
|
1016
|
+
recordRename(sessionId, folderOld, folderNew);
|
|
1017
|
+
spinner_default.succeed(formatted);
|
|
1018
|
+
renamed++;
|
|
1019
|
+
}
|
|
1020
|
+
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1021
|
+
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1022
|
+
spinner_default.info(`skipped ${skipped} files`);
|
|
1023
|
+
spinner_default.succeed(`done in ${import_cosmetic7.default.cyan.encoder(dir)}`);
|
|
1024
|
+
spinner_default.stop();
|
|
1025
|
+
};
|
|
1026
|
+
var rename_default = rename;
|
|
1027
|
+
|
|
1028
|
+
// src/actions/reset.ts
|
|
1029
|
+
var import_cosmetic8 = __toESM(require("cosmetic"));
|
|
1030
|
+
var import_fs9 = require("fs");
|
|
1031
|
+
var import_path10 = require("path");
|
|
1032
|
+
var reset = async ({ dir: inputDir, double }) => {
|
|
1033
|
+
let dir = inputDir;
|
|
1034
|
+
spinner_default.text = `resetting episodes in ${import_cosmetic8.default.blue.encoder(dir)}`;
|
|
1035
|
+
spinner_default.start();
|
|
1036
|
+
dir = (0, import_path10.resolve)(dir);
|
|
1037
|
+
if (!(0, import_fs9.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
1038
|
+
const list2 = (0, import_fs9.readdirSync)(dir).sort();
|
|
1039
|
+
const folder = dir.replace(/\./g, " ").split(import_path10.sep).pop();
|
|
1040
|
+
let season;
|
|
1041
|
+
let sub = folder.includes("season") ? folder.substring(folder.indexOf("season") + "season".length, folder.length).trim() : /s\d/i.test(folder) ? folder.substring(folder.search(/s\d/i) + 1, folder.length).trim() : null;
|
|
1042
|
+
if (sub)
|
|
1043
|
+
while (sub.search(/[0-9]/) === 0) {
|
|
1044
|
+
season = `${season || ""}${sub.substring(0, 1)}`;
|
|
1045
|
+
sub = sub.substring(1, sub.length);
|
|
1046
|
+
}
|
|
1047
|
+
const seasonNum = parseInt(season);
|
|
1048
|
+
if (!seasonNum) throw new Error(`unable to identify season number`);
|
|
1049
|
+
const parentFolder = (0, import_path10.basename)((0, import_path10.dirname)(dir));
|
|
1050
|
+
const showTitle = parentFolder.match(/^(.+?)\s*(?:\(\d{4}\))?$/)?.[1]?.trim() || void 0;
|
|
1051
|
+
spinner_default.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
|
|
1052
|
+
const sublist = list2.filter((f) => {
|
|
1053
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1054
|
+
return videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
1055
|
+
});
|
|
1056
|
+
const other = list2.filter((f) => {
|
|
1057
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1058
|
+
return !videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
1059
|
+
});
|
|
1060
|
+
const episodeFormat = getConfig().format?.episode;
|
|
1061
|
+
let renamed = 0, skipped = other.length;
|
|
1062
|
+
for (const [index, i] of sublist.entries()) {
|
|
1063
|
+
spinner_default.text = `resetting episodes in ${import_cosmetic8.default.blue.encoder(dir)} ${index}/${list2.length}`;
|
|
1064
|
+
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1065
|
+
const episode = double ? index * 2 + 1 : index + 1;
|
|
1066
|
+
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
1067
|
+
if (i === name) {
|
|
1068
|
+
skipped++;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
(0, import_fs9.renameSync)((0, import_path10.resolve)(dir, i), (0, import_path10.resolve)(dir, name));
|
|
1072
|
+
renamed++;
|
|
1073
|
+
}
|
|
1074
|
+
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1075
|
+
spinner_default.info(`skipped ${skipped} files`);
|
|
1076
|
+
spinner_default.succeed(`done in ${import_cosmetic8.default.cyan.encoder(dir)}`);
|
|
1077
|
+
spinner_default.stop();
|
|
1078
|
+
};
|
|
1079
|
+
var reset_default = reset;
|
|
1080
|
+
|
|
1081
|
+
// src/actions/scan.ts
|
|
1082
|
+
var import_cosmetic9 = __toESM(require("cosmetic"));
|
|
1083
|
+
var import_fs10 = require("fs");
|
|
1084
|
+
var import_path11 = require("path");
|
|
1085
|
+
var import_termpulse2 = require("termpulse");
|
|
1086
|
+
|
|
1087
|
+
// src/helpers/detectEdition.ts
|
|
1088
|
+
var EDITIONS = [
|
|
1089
|
+
{ pattern: /director.?s?.?cut/i, name: "Director's Cut" },
|
|
1090
|
+
{ pattern: /final\.?cut/i, name: "Final Cut" },
|
|
1091
|
+
{ pattern: /extended\.?(cut|edition|version)?/i, name: "Extended" },
|
|
1092
|
+
{ pattern: /theatrical\.?(cut|version)?/i, name: "Theatrical" },
|
|
1093
|
+
{ pattern: /unrated\.?(cut|version)?/i, name: "Unrated" },
|
|
1094
|
+
{ pattern: /anniversary\.?edition/i, name: "Anniversary Edition" },
|
|
1095
|
+
{ pattern: /collector.?s?.?(edition|cut)/i, name: "Collector's Edition" },
|
|
1096
|
+
{ pattern: /special\.?edition/i, name: "Special Edition" }
|
|
1097
|
+
];
|
|
1098
|
+
var detectEdition = (filename) => {
|
|
1099
|
+
for (const { pattern, name } of EDITIONS) {
|
|
1100
|
+
if (pattern.test(filename)) return name;
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// src/helpers/hyperlink.ts
|
|
1106
|
+
var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
1107
|
+
|
|
1108
|
+
// src/helpers/parseDownloadName.ts
|
|
1109
|
+
var QUALITY_TOKENS = /* @__PURE__ */ new Set(["480p", "576p", "720p", "1080p", "2160p", "4k", "8k", "bluray", "bdrip", "bdremux", "brrip", "webrip", "web-dl", "webdl", "web", "hdtv", "dvdrip", "dvdscr", "cam", "ts", "scr", "x264", "x265", "hevc", "avc", "h264", "h265", "xvid", "divx", "dts", "ac3", "aac", "mp3", "truehd", "atmos", "dd5", "hdr", "hdr10", "hlg", "dv", "dolby", "remux", "proper", "repack", "extended", "theatrical", "unrated", "multi", "dubbed", "subbed", "internal"]);
|
|
1110
|
+
var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
|
|
1111
|
+
var parseDownloadName = (name) => {
|
|
1112
|
+
const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
|
|
1113
|
+
const tvMatch = TV_PATTERN.exec(base);
|
|
1114
|
+
if (tvMatch) {
|
|
1115
|
+
const raw = tvMatch[1].replace(/[._]/g, " ").replace(/\[.*?\]/g, "").trim();
|
|
1116
|
+
const title = titleCase_default(raw);
|
|
1117
|
+
if (!title) return null;
|
|
1118
|
+
const season = parseInt(tvMatch[2] ?? tvMatch[4] ?? tvMatch[6]);
|
|
852
1119
|
const episode = parseInt(tvMatch[3] ?? tvMatch[5]);
|
|
853
1120
|
return {
|
|
854
1121
|
title,
|
|
@@ -934,33 +1201,33 @@ var searchTv = async (title, apiKey) => {
|
|
|
934
1201
|
}
|
|
935
1202
|
};
|
|
936
1203
|
|
|
937
|
-
// src/actions/
|
|
1204
|
+
// src/actions/scan.ts
|
|
938
1205
|
var sameDev = (a, b) => {
|
|
939
1206
|
try {
|
|
940
1207
|
let bExisting = b;
|
|
941
|
-
while (!(0,
|
|
942
|
-
return (0,
|
|
1208
|
+
while (!(0, import_fs10.existsSync)(bExisting)) bExisting = (0, import_path11.dirname)(bExisting);
|
|
1209
|
+
return (0, import_fs10.statSync)(a).dev === (0, import_fs10.statSync)(bExisting).dev;
|
|
943
1210
|
} catch {
|
|
944
1211
|
return false;
|
|
945
1212
|
}
|
|
946
1213
|
};
|
|
947
|
-
var
|
|
1214
|
+
var moveFolder = (src, dest) => {
|
|
948
1215
|
if (sameDev(src, dest)) {
|
|
949
|
-
(0,
|
|
1216
|
+
(0, import_fs10.renameSync)(src, dest);
|
|
950
1217
|
} else {
|
|
951
|
-
(0,
|
|
952
|
-
(0,
|
|
1218
|
+
(0, import_fs10.cpSync)(src, dest, { recursive: true });
|
|
1219
|
+
(0, import_fs10.rmSync)(src, { recursive: true, force: true });
|
|
953
1220
|
}
|
|
954
1221
|
};
|
|
955
|
-
var findVideo = (dir) => (0,
|
|
1222
|
+
var findVideo = (dir) => (0, import_fs10.readdirSync)(dir).find((f) => {
|
|
956
1223
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
957
1224
|
return ext && videoExtensions_default.includes(ext);
|
|
958
1225
|
}) ?? null;
|
|
959
1226
|
var findSeasonFolder = (showPath, season) => {
|
|
960
|
-
if (!(0,
|
|
961
|
-
const folders = (0,
|
|
1227
|
+
if (!(0, import_fs10.existsSync)(showPath)) return null;
|
|
1228
|
+
const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
|
|
962
1229
|
try {
|
|
963
|
-
return (0,
|
|
1230
|
+
return (0, import_fs10.lstatSync)((0, import_path11.resolve)(showPath, f)).isDirectory();
|
|
964
1231
|
} catch {
|
|
965
1232
|
return false;
|
|
966
1233
|
}
|
|
@@ -970,332 +1237,30 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
970
1237
|
return match && parseInt(match[1]) === season;
|
|
971
1238
|
}) ?? null;
|
|
972
1239
|
};
|
|
973
|
-
var
|
|
1240
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
974
1241
|
const config = getConfig();
|
|
975
1242
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
976
|
-
const
|
|
1243
|
+
const language = config.language ?? "eng";
|
|
977
1244
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
978
1245
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
987
|
-
detectedType = "tv";
|
|
988
|
-
} else {
|
|
989
|
-
detectedType = "movie";
|
|
990
|
-
}
|
|
991
|
-
const destRoot = config.dest[detectedType];
|
|
992
|
-
if (!destRoot) {
|
|
993
|
-
if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (detectedType === "ps3") {
|
|
997
|
-
const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
|
|
998
|
-
const id = entry.split("-")[0];
|
|
999
|
-
if (!nameMatch || !id) return;
|
|
1000
|
-
const destName = `${nameMatch[0]} [${id}]`;
|
|
1001
|
-
const destPath = (0, import_path7.resolve)(destRoot, destName);
|
|
1002
|
-
if ((0, import_fs6.existsSync)(destPath)) {
|
|
1003
|
-
spinner_default.warn(`already exists: ${destName}`);
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
moveItem(entryPath, destPath);
|
|
1007
|
-
recordImport(sessionId, entryPath, destPath, "move");
|
|
1008
|
-
spinner_default.succeed(`imported ${import_cosmetic4.default.cyan.encoder(destName)}`);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
const parsed = parseDownloadName(entry);
|
|
1012
|
-
if (!parsed) {
|
|
1013
|
-
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
if (detectedType === "tv") {
|
|
1017
|
-
if (parsed.season === void 0) {
|
|
1018
|
-
if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
const registeredShow = getShowByTitle(parsed.title);
|
|
1022
|
-
let showPath;
|
|
1023
|
-
let showFolderName;
|
|
1024
|
-
if (registeredShow) {
|
|
1025
|
-
showPath = registeredShow.path;
|
|
1026
|
-
showFolderName = showPath.split("/").pop() ?? registeredShow.path;
|
|
1027
|
-
} else if (auto) {
|
|
1028
|
-
showFolderName = formatMovieName(movieFormat, parsed.title, parsed.year);
|
|
1029
|
-
showPath = (0, import_path7.resolve)(destRoot, showFolderName);
|
|
1030
|
-
upsertShow(showPath, null, parsed.title);
|
|
1031
|
-
} else {
|
|
1032
|
-
if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1036
|
-
const seasonPath = (0, import_path7.resolve)(showPath, seasonFolderName);
|
|
1037
|
-
const videoFile2 = isDir ? findVideo(entryPath) : entry;
|
|
1038
|
-
if (!videoFile2) {
|
|
1039
|
-
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
|
|
1043
|
-
const tmdbEpisodeName = registeredShow?.tmdbId && config.tmdbApiKey ? await getEpisodeName(registeredShow.tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1044
|
-
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, parsed.title, tmdbEpisodeName ?? void 0);
|
|
1045
|
-
const destVideoName2 = `${episodeName}.${videoExt2}`;
|
|
1046
|
-
const destVideoPath = (0, import_path7.resolve)(seasonPath, destVideoName2);
|
|
1047
|
-
const videoSourcePath2 = isDir ? (0, import_path7.resolve)(entryPath, videoFile2) : entryPath;
|
|
1048
|
-
if ((0, import_fs6.existsSync)(destVideoPath)) {
|
|
1049
|
-
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1050
|
-
return;
|
|
1246
|
+
spinner_default.start();
|
|
1247
|
+
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1248
|
+
let imported = 0, skipped = 0;
|
|
1249
|
+
for (const source of config.sources) {
|
|
1250
|
+
if (!(0, import_fs10.existsSync)(source)) {
|
|
1251
|
+
spinner_default.warn(`source not found: ${import_cosmetic9.default.blue.encoder(source)}`);
|
|
1252
|
+
continue;
|
|
1051
1253
|
}
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
(0, import_fs6.linkSync)(videoSourcePath2, destVideoPath);
|
|
1063
|
-
mode = "hardlink";
|
|
1064
|
-
} catch {
|
|
1065
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1066
|
-
(0, import_fs6.cpSync)(videoSourcePath2, destVideoPath);
|
|
1067
|
-
mode = "copy";
|
|
1068
|
-
}
|
|
1069
|
-
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs6.cpSync)(subtitleSourcePath2, (0, import_path7.resolve)(seasonPath, destSubtitleName2));
|
|
1070
|
-
} else {
|
|
1071
|
-
if (sameDev(videoSourcePath2, seasonPath)) {
|
|
1072
|
-
(0, import_fs6.renameSync)(videoSourcePath2, destVideoPath);
|
|
1073
|
-
} else {
|
|
1074
|
-
(0, import_fs6.cpSync)(videoSourcePath2, destVideoPath);
|
|
1075
|
-
(0, import_fs6.rmSync)(videoSourcePath2);
|
|
1076
|
-
}
|
|
1077
|
-
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs6.renameSync)(subtitleSourcePath2, (0, import_path7.resolve)(seasonPath, destSubtitleName2));
|
|
1078
|
-
if (isDir) (0, import_fs6.rmSync)(entryPath, { recursive: true, force: true });
|
|
1079
|
-
}
|
|
1080
|
-
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1081
|
-
spinner_default.succeed(`imported ${import_cosmetic4.default.cyan.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const edition = detectEdition(entry);
|
|
1085
|
-
const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
|
|
1086
|
-
const destFolder = (0, import_path7.resolve)(destRoot, folderName);
|
|
1087
|
-
if ((0, import_fs6.existsSync)(destFolder)) {
|
|
1088
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1092
|
-
if (!videoFile) {
|
|
1093
|
-
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1097
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1098
|
-
const videoSourcePath = isDir ? (0, import_path7.resolve)(entryPath, videoFile) : entryPath;
|
|
1099
|
-
const dirFiles = isDir ? (0, import_fs6.readdirSync)(entryPath) : [];
|
|
1100
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1101
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1102
|
-
const subtitleSourcePath = subtitle ? (0, import_path7.resolve)(entryPath, subtitle) : null;
|
|
1103
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1104
|
-
if (useHardlink) {
|
|
1105
|
-
(0, import_fs6.mkdirSync)(destFolder, { recursive: true });
|
|
1106
|
-
const destVideoPath = (0, import_path7.resolve)(destFolder, destVideoName);
|
|
1107
|
-
let mode;
|
|
1108
|
-
try {
|
|
1109
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1110
|
-
(0, import_fs6.linkSync)(videoSourcePath, destVideoPath);
|
|
1111
|
-
mode = "hardlink";
|
|
1112
|
-
} catch {
|
|
1113
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1114
|
-
(0, import_fs6.cpSync)(videoSourcePath, destVideoPath);
|
|
1115
|
-
mode = "copy";
|
|
1116
|
-
}
|
|
1117
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs6.cpSync)(subtitleSourcePath, (0, import_path7.resolve)(destFolder, destSubtitleName));
|
|
1118
|
-
recordImport(sessionId, entryPath, destFolder, mode);
|
|
1119
|
-
} else {
|
|
1120
|
-
if (isDir) {
|
|
1121
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1122
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs6.rmSync)((0, import_path7.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1123
|
-
(0, import_fs6.renameSync)(videoSourcePath, (0, import_path7.resolve)(entryPath, destVideoName));
|
|
1124
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs6.renameSync)(subtitleSourcePath, (0, import_path7.resolve)(entryPath, destSubtitleName));
|
|
1125
|
-
moveItem(entryPath, destFolder);
|
|
1126
|
-
} else {
|
|
1127
|
-
(0, import_fs6.mkdirSync)(destFolder, { recursive: true });
|
|
1128
|
-
const destVideoPath = (0, import_path7.resolve)(destFolder, destVideoName);
|
|
1129
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1130
|
-
(0, import_fs6.renameSync)(videoSourcePath, destVideoPath);
|
|
1131
|
-
} else {
|
|
1132
|
-
(0, import_fs6.cpSync)(videoSourcePath, destVideoPath);
|
|
1133
|
-
(0, import_fs6.rmSync)(videoSourcePath);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1137
|
-
}
|
|
1138
|
-
spinner_default.succeed(`imported ${import_cosmetic4.default.cyan.encoder(folderName)}`);
|
|
1139
|
-
};
|
|
1140
|
-
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1141
|
-
const config = getConfig();
|
|
1142
|
-
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1143
|
-
const language = config.language ?? "eng";
|
|
1144
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1145
|
-
const handle = (path) => {
|
|
1146
|
-
const existing = pending.get(path);
|
|
1147
|
-
if (existing) clearTimeout(existing);
|
|
1148
|
-
pending.set(
|
|
1149
|
-
path,
|
|
1150
|
-
setTimeout(async () => {
|
|
1151
|
-
pending.delete(path);
|
|
1152
|
-
try {
|
|
1153
|
-
await processItem(path, hardlink, verbose, language, auto);
|
|
1154
|
-
} catch (err) {
|
|
1155
|
-
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
|
1156
|
-
}
|
|
1157
|
-
}, 5e3)
|
|
1158
|
-
);
|
|
1159
|
-
};
|
|
1160
|
-
const watcher = import_chokidar.default.watch(config.sources, {
|
|
1161
|
-
depth: 0,
|
|
1162
|
-
ignoreInitial: true,
|
|
1163
|
-
awaitWriteFinish: { stabilityThreshold: 5e3, pollInterval: 500 }
|
|
1164
|
-
});
|
|
1165
|
-
watcher.on("addDir", handle);
|
|
1166
|
-
watcher.on("add", handle);
|
|
1167
|
-
spinner_default.start();
|
|
1168
|
-
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1169
|
-
for (const s of config.sources) spinner_default.info(` ${import_cosmetic4.default.blue.encoder(s)}`);
|
|
1170
|
-
spinner_default.stop();
|
|
1171
|
-
process.stdin.resume();
|
|
1172
|
-
};
|
|
1173
|
-
var watch_default = watch;
|
|
1174
|
-
|
|
1175
|
-
// src/actions/clean.ts
|
|
1176
|
-
var import_cosmetic5 = __toESM(require("cosmetic"));
|
|
1177
|
-
var import_fs7 = require("fs");
|
|
1178
|
-
var parseOlderThan = (s) => {
|
|
1179
|
-
const match = s.match(/^(\d+)([dhm])$/);
|
|
1180
|
-
if (!match) return null;
|
|
1181
|
-
const n = parseInt(match[1]);
|
|
1182
|
-
const unit = match[2];
|
|
1183
|
-
const ms = unit === "d" ? n * 864e5 : unit === "h" ? n * 36e5 : n * 6e4;
|
|
1184
|
-
return ms;
|
|
1185
|
-
};
|
|
1186
|
-
var clean = async ({ dryRun, olderThan }) => {
|
|
1187
|
-
spinner_default.start();
|
|
1188
|
-
const imports = getCleanableImports();
|
|
1189
|
-
if (imports.length === 0) {
|
|
1190
|
-
spinner_default.info("nothing to clean");
|
|
1191
|
-
spinner_default.stop();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const cutoffMs = olderThan ? parseOlderThan(olderThan) : null;
|
|
1195
|
-
if (olderThan && cutoffMs === null) throw new Error(`invalid --older-than format, expected e.g. 14d, 6h, 30m`);
|
|
1196
|
-
let cleaned = 0, skipped = 0;
|
|
1197
|
-
for (const imp of imports) {
|
|
1198
|
-
if (cutoffMs !== null) {
|
|
1199
|
-
const age = Date.now() - new Date(imp.importedAt).getTime();
|
|
1200
|
-
if (age < cutoffMs) {
|
|
1201
|
-
skipped++;
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
if (!(0, import_fs7.existsSync)(imp.sourcePath)) {
|
|
1206
|
-
if (!dryRun) deleteImport(imp.id);
|
|
1207
|
-
continue;
|
|
1208
|
-
}
|
|
1209
|
-
if (dryRun) {
|
|
1210
|
-
spinner_default.succeed(`[dry] would remove ${import_cosmetic5.default.blue.encoder(imp.sourcePath)}`);
|
|
1211
|
-
cleaned++;
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
try {
|
|
1215
|
-
(0, import_fs7.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
1216
|
-
deleteImport(imp.id);
|
|
1217
|
-
spinner_default.succeed(`removed ${import_cosmetic5.default.blue.encoder(imp.sourcePath)}`);
|
|
1218
|
-
cleaned++;
|
|
1219
|
-
} catch {
|
|
1220
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${import_cosmetic5.default.blue.encoder(imp.sourcePath)}`);
|
|
1221
|
-
skipped++;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
spinner_default.succeed(`cleaned ${cleaned} items`);
|
|
1225
|
-
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
1226
|
-
spinner_default.stop();
|
|
1227
|
-
};
|
|
1228
|
-
var clean_default = clean;
|
|
1229
|
-
|
|
1230
|
-
// src/actions/scan.ts
|
|
1231
|
-
var import_cosmetic6 = __toESM(require("cosmetic"));
|
|
1232
|
-
var import_fs8 = require("fs");
|
|
1233
|
-
var import_path8 = require("path");
|
|
1234
|
-
var import_termpulse2 = require("termpulse");
|
|
1235
|
-
|
|
1236
|
-
// src/helpers/hyperlink.ts
|
|
1237
|
-
var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
1238
|
-
|
|
1239
|
-
// src/actions/scan.ts
|
|
1240
|
-
var sameDev2 = (a, b) => {
|
|
1241
|
-
try {
|
|
1242
|
-
let bExisting = b;
|
|
1243
|
-
while (!(0, import_fs8.existsSync)(bExisting)) bExisting = (0, import_path8.dirname)(bExisting);
|
|
1244
|
-
return (0, import_fs8.statSync)(a).dev === (0, import_fs8.statSync)(bExisting).dev;
|
|
1245
|
-
} catch {
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
var moveFolder = (src, dest) => {
|
|
1250
|
-
if (sameDev2(src, dest)) {
|
|
1251
|
-
(0, import_fs8.renameSync)(src, dest);
|
|
1252
|
-
} else {
|
|
1253
|
-
(0, import_fs8.cpSync)(src, dest, { recursive: true });
|
|
1254
|
-
(0, import_fs8.rmSync)(src, { recursive: true, force: true });
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
var findVideo2 = (dir) => (0, import_fs8.readdirSync)(dir).find((f) => {
|
|
1258
|
-
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1259
|
-
return ext && videoExtensions_default.includes(ext);
|
|
1260
|
-
}) ?? null;
|
|
1261
|
-
var findSeasonFolder2 = (showPath, season) => {
|
|
1262
|
-
if (!(0, import_fs8.existsSync)(showPath)) return null;
|
|
1263
|
-
const folders = (0, import_fs8.readdirSync)(showPath).filter((f) => {
|
|
1264
|
-
try {
|
|
1265
|
-
return (0, import_fs8.lstatSync)((0, import_path8.resolve)(showPath, f)).isDirectory();
|
|
1266
|
-
} catch {
|
|
1267
|
-
return false;
|
|
1268
|
-
}
|
|
1269
|
-
});
|
|
1270
|
-
return folders.find((f) => {
|
|
1271
|
-
const match = f.match(/(?:season|s)\s*0*(\d+)/i);
|
|
1272
|
-
return match && parseInt(match[1]) === season;
|
|
1273
|
-
}) ?? null;
|
|
1274
|
-
};
|
|
1275
|
-
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
1276
|
-
const config = getConfig();
|
|
1277
|
-
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1278
|
-
const language = config.language ?? "eng";
|
|
1279
|
-
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1280
|
-
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1281
|
-
spinner_default.start();
|
|
1282
|
-
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1283
|
-
let imported = 0, skipped = 0;
|
|
1284
|
-
for (const source of config.sources) {
|
|
1285
|
-
if (!(0, import_fs8.existsSync)(source)) {
|
|
1286
|
-
spinner_default.warn(`source not found: ${import_cosmetic6.default.blue.encoder(source)}`);
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
spinner_default.text = `scanning ${import_cosmetic6.default.blue.encoder(source)}`;
|
|
1290
|
-
for (const entry of (0, import_fs8.readdirSync)(source)) {
|
|
1291
|
-
const entryPath = (0, import_path8.resolve)(source, entry);
|
|
1292
|
-
const isDir = (0, import_fs8.lstatSync)(entryPath).isDirectory();
|
|
1293
|
-
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1294
|
-
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1295
|
-
if (!isDir && !isVideo) {
|
|
1296
|
-
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1297
|
-
skipped++;
|
|
1298
|
-
continue;
|
|
1254
|
+
spinner_default.text = `scanning ${import_cosmetic9.default.blue.encoder(source)}`;
|
|
1255
|
+
for (const entry of (0, import_fs10.readdirSync)(source)) {
|
|
1256
|
+
const entryPath = (0, import_path11.resolve)(source, entry);
|
|
1257
|
+
const isDir = (0, import_fs10.lstatSync)(entryPath).isDirectory();
|
|
1258
|
+
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1259
|
+
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1260
|
+
if (!isDir && !isVideo) {
|
|
1261
|
+
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1262
|
+
skipped++;
|
|
1263
|
+
continue;
|
|
1299
1264
|
}
|
|
1300
1265
|
let detectedType;
|
|
1301
1266
|
if (type) {
|
|
@@ -1321,8 +1286,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1321
1286
|
continue;
|
|
1322
1287
|
}
|
|
1323
1288
|
const destName = `${nameMatch[0]} [${id}]`;
|
|
1324
|
-
const destPath = (0,
|
|
1325
|
-
if ((0,
|
|
1289
|
+
const destPath = (0, import_path11.resolve)(destRoot, destName);
|
|
1290
|
+
if ((0, import_fs10.existsSync)(destPath)) {
|
|
1326
1291
|
spinner_default.warn(`already exists: ${destName}`);
|
|
1327
1292
|
skipped++;
|
|
1328
1293
|
continue;
|
|
@@ -1390,16 +1355,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1390
1355
|
showFolderName = showPath.split("/").pop() ?? registeredShow.path;
|
|
1391
1356
|
} else if (auto) {
|
|
1392
1357
|
showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
|
|
1393
|
-
showPath = (0,
|
|
1358
|
+
showPath = (0, import_path11.resolve)(destRoot, showFolderName);
|
|
1394
1359
|
if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
|
|
1395
1360
|
} else {
|
|
1396
1361
|
if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
|
|
1397
1362
|
skipped++;
|
|
1398
1363
|
continue;
|
|
1399
1364
|
}
|
|
1400
|
-
const seasonFolderName =
|
|
1401
|
-
const seasonPath = (0,
|
|
1402
|
-
const videoFile2 = isDir ?
|
|
1365
|
+
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1366
|
+
const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
|
|
1367
|
+
const videoFile2 = isDir ? findVideo(entryPath) : entry;
|
|
1403
1368
|
if (!videoFile2) {
|
|
1404
1369
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1405
1370
|
skipped++;
|
|
@@ -1409,41 +1374,41 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1409
1374
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1410
1375
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1411
1376
|
const destVideoName2 = `${episodeName}.${videoExt2}`;
|
|
1412
|
-
const destVideoPath = (0,
|
|
1413
|
-
const videoSourcePath2 = isDir ? (0,
|
|
1414
|
-
if ((0,
|
|
1377
|
+
const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName2);
|
|
1378
|
+
const videoSourcePath2 = isDir ? (0, import_path11.resolve)(entryPath, videoFile2) : entryPath;
|
|
1379
|
+
if ((0, import_fs10.existsSync)(destVideoPath)) {
|
|
1415
1380
|
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1416
1381
|
skipped++;
|
|
1417
1382
|
continue;
|
|
1418
1383
|
}
|
|
1419
|
-
const dirFiles2 = isDir ? (0,
|
|
1384
|
+
const dirFiles2 = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1420
1385
|
const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
|
|
1421
1386
|
const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
|
|
1422
|
-
const subtitleSourcePath2 = subtitle2 ? (0,
|
|
1387
|
+
const subtitleSourcePath2 = subtitle2 ? (0, import_path11.resolve)(entryPath, subtitle2) : null;
|
|
1423
1388
|
const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
|
|
1424
1389
|
if (!dryRun) {
|
|
1425
|
-
(0,
|
|
1390
|
+
(0, import_fs10.mkdirSync)(seasonPath, { recursive: true });
|
|
1426
1391
|
let mode = "move";
|
|
1427
1392
|
if (useHardlink) {
|
|
1428
1393
|
try {
|
|
1429
|
-
if (!
|
|
1430
|
-
(0,
|
|
1394
|
+
if (!sameDev(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
|
|
1395
|
+
(0, import_fs10.linkSync)(videoSourcePath2, destVideoPath);
|
|
1431
1396
|
mode = "hardlink";
|
|
1432
1397
|
} catch {
|
|
1433
1398
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1434
|
-
(0,
|
|
1399
|
+
(0, import_fs10.cpSync)(videoSourcePath2, destVideoPath);
|
|
1435
1400
|
mode = "copy";
|
|
1436
1401
|
}
|
|
1437
|
-
if (subtitleSourcePath2 && destSubtitleName2) (0,
|
|
1402
|
+
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs10.cpSync)(subtitleSourcePath2, (0, import_path11.resolve)(seasonPath, destSubtitleName2));
|
|
1438
1403
|
} else {
|
|
1439
|
-
if (
|
|
1440
|
-
(0,
|
|
1404
|
+
if (sameDev(videoSourcePath2, seasonPath)) {
|
|
1405
|
+
(0, import_fs10.renameSync)(videoSourcePath2, destVideoPath);
|
|
1441
1406
|
} else {
|
|
1442
|
-
(0,
|
|
1443
|
-
(0,
|
|
1407
|
+
(0, import_fs10.cpSync)(videoSourcePath2, destVideoPath);
|
|
1408
|
+
(0, import_fs10.rmSync)(videoSourcePath2);
|
|
1444
1409
|
}
|
|
1445
|
-
if (subtitleSourcePath2 && destSubtitleName2) (0,
|
|
1446
|
-
if (isDir) (0,
|
|
1410
|
+
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs10.renameSync)(subtitleSourcePath2, (0, import_path11.resolve)(seasonPath, destSubtitleName2));
|
|
1411
|
+
if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
|
|
1447
1412
|
}
|
|
1448
1413
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
1449
1414
|
}
|
|
@@ -1453,13 +1418,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1453
1418
|
}
|
|
1454
1419
|
const edition = detectEdition(entry);
|
|
1455
1420
|
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1456
|
-
const destFolder = (0,
|
|
1457
|
-
if ((0,
|
|
1421
|
+
const destFolder = (0, import_path11.resolve)(destRoot, folderName);
|
|
1422
|
+
if ((0, import_fs10.existsSync)(destFolder)) {
|
|
1458
1423
|
spinner_default.warn(`already exists: ${folderName}`);
|
|
1459
1424
|
skipped++;
|
|
1460
1425
|
continue;
|
|
1461
1426
|
}
|
|
1462
|
-
const videoFile = isDir ?
|
|
1427
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1463
1428
|
if (!videoFile) {
|
|
1464
1429
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1465
1430
|
skipped++;
|
|
@@ -1467,43 +1432,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1467
1432
|
}
|
|
1468
1433
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1469
1434
|
const destVideoName = `${folderName}.${videoExt}`;
|
|
1470
|
-
const videoSourcePath = isDir ? (0,
|
|
1471
|
-
const dirFiles = isDir ? (0,
|
|
1435
|
+
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1436
|
+
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1472
1437
|
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1473
1438
|
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1474
|
-
const subtitleSourcePath = subtitle ? (0,
|
|
1439
|
+
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1475
1440
|
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1476
1441
|
if (!dryRun) {
|
|
1477
1442
|
if (useHardlink) {
|
|
1478
|
-
(0,
|
|
1479
|
-
const destVideoPath = (0,
|
|
1443
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1444
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1480
1445
|
let mode;
|
|
1481
1446
|
try {
|
|
1482
|
-
if (!
|
|
1483
|
-
(0,
|
|
1447
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1448
|
+
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1484
1449
|
mode = "hardlink";
|
|
1485
1450
|
} catch {
|
|
1486
1451
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1487
|
-
(0,
|
|
1452
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1488
1453
|
mode = "copy";
|
|
1489
1454
|
}
|
|
1490
|
-
if (subtitleSourcePath && destSubtitleName) (0,
|
|
1455
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
|
|
1491
1456
|
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1492
1457
|
} else {
|
|
1493
1458
|
if (isDir) {
|
|
1494
1459
|
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1495
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0,
|
|
1496
|
-
(0,
|
|
1497
|
-
if (subtitleSourcePath && destSubtitleName) (0,
|
|
1460
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1461
|
+
(0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
|
|
1462
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
|
|
1498
1463
|
moveFolder(entryPath, destFolder);
|
|
1499
1464
|
} else {
|
|
1500
|
-
(0,
|
|
1501
|
-
const destVideoPath = (0,
|
|
1502
|
-
if (
|
|
1503
|
-
(0,
|
|
1465
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1466
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1467
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1468
|
+
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1504
1469
|
} else {
|
|
1505
|
-
(0,
|
|
1506
|
-
(0,
|
|
1470
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1471
|
+
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1507
1472
|
}
|
|
1508
1473
|
}
|
|
1509
1474
|
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
@@ -1519,55 +1484,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1519
1484
|
};
|
|
1520
1485
|
var scan_default = scan;
|
|
1521
1486
|
|
|
1522
|
-
// src/actions/history.ts
|
|
1523
|
-
var import_cosmetic7 = __toESM(require("cosmetic"));
|
|
1524
|
-
var import_path9 = require("path");
|
|
1525
|
-
var history = async ({ limit, imports }) => {
|
|
1526
|
-
if (imports) {
|
|
1527
|
-
const sessions = getImportHistory(limit ?? 10);
|
|
1528
|
-
if (sessions.length === 0) {
|
|
1529
|
-
console.log("no import history found");
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
for (const session of sessions) {
|
|
1533
|
-
const date = new Date(session.sessionId);
|
|
1534
|
-
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
1535
|
-
console.log(`
|
|
1536
|
-
${import_cosmetic7.default.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
1537
|
-
for (const r of session.records) {
|
|
1538
|
-
const src = (0, import_path9.basename)(r.sourcePath);
|
|
1539
|
-
const dest = import_cosmetic7.default.cyan.encoder(r.destinationPath);
|
|
1540
|
-
const mode = r.mode !== "move" ? ` ${import_cosmetic7.default.blue.encoder(`[${r.mode}]`)}` : "";
|
|
1541
|
-
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
} else {
|
|
1545
|
-
const sessions = getHistory(limit ?? 10);
|
|
1546
|
-
if (sessions.length === 0) {
|
|
1547
|
-
console.log("no history found");
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
for (const session of sessions) {
|
|
1551
|
-
const date = new Date(session.sessionId);
|
|
1552
|
-
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
1553
|
-
const folders = session.records.filter((r) => (0, import_path9.extname)(r.newPath) === "");
|
|
1554
|
-
console.log(`
|
|
1555
|
-
${import_cosmetic7.default.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
|
|
1556
|
-
for (const r of folders) {
|
|
1557
|
-
const oldName = (0, import_path9.basename)(r.oldPath);
|
|
1558
|
-
const newName = (0, import_path9.basename)(r.newPath);
|
|
1559
|
-
console.log(` ${import_cosmetic7.default.blue.encoder(oldName)} \u2192 ${import_cosmetic7.default.cyan.encoder(newName)}`);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
console.log();
|
|
1564
|
-
};
|
|
1565
|
-
var history_default = history;
|
|
1566
|
-
|
|
1567
1487
|
// src/actions/undo.ts
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
1570
|
-
var undo = async (
|
|
1488
|
+
var import_cosmetic10 = __toESM(require("cosmetic"));
|
|
1489
|
+
var import_fs11 = require("fs");
|
|
1490
|
+
var undo = async () => {
|
|
1571
1491
|
spinner_default.start();
|
|
1572
1492
|
const records = getLastSession();
|
|
1573
1493
|
if (records.length === 0) {
|
|
@@ -1577,8 +1497,8 @@ var undo = async (_) => {
|
|
|
1577
1497
|
}
|
|
1578
1498
|
let undone = 0;
|
|
1579
1499
|
for (const record of records) {
|
|
1580
|
-
(0,
|
|
1581
|
-
spinner_default.succeed(`${
|
|
1500
|
+
(0, import_fs11.renameSync)(record.newPath, record.oldPath);
|
|
1501
|
+
spinner_default.succeed(`${import_cosmetic10.default.cyan.encoder(record.newPath)} \u2192 ${import_cosmetic10.default.blue.encoder(record.oldPath)}`);
|
|
1582
1502
|
undone++;
|
|
1583
1503
|
}
|
|
1584
1504
|
deleteSession(records[0].sessionId);
|
|
@@ -1587,217 +1507,247 @@ var undo = async (_) => {
|
|
|
1587
1507
|
};
|
|
1588
1508
|
var undo_default = undo;
|
|
1589
1509
|
|
|
1590
|
-
// src/actions/
|
|
1591
|
-
var
|
|
1592
|
-
var
|
|
1593
|
-
var
|
|
1594
|
-
var
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (!(0, import_fs10.existsSync)(dir2)) throw new Error(`dir2 ${dir2} does not exist`);
|
|
1603
|
-
let list1 = (0, import_fs10.readdirSync)(dir1);
|
|
1604
|
-
let list2 = (0, import_fs10.readdirSync)(dir2);
|
|
1605
|
-
if (only && only.length) {
|
|
1606
|
-
list1 = list1.filter((i) => {
|
|
1607
|
-
for (const o of only) if (i.endsWith(o)) return true;
|
|
1608
|
-
return false;
|
|
1609
|
-
});
|
|
1610
|
-
list2 = list2.filter((i) => {
|
|
1611
|
-
for (const o of only) if (i.endsWith(o)) return true;
|
|
1612
|
-
return false;
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
if (ignore && ignore.length) {
|
|
1616
|
-
list1 = list1.filter((i) => {
|
|
1617
|
-
for (const o of ignore) if (i.endsWith(o)) return false;
|
|
1618
|
-
return true;
|
|
1619
|
-
});
|
|
1620
|
-
list2 = list2.filter((i) => {
|
|
1621
|
-
for (const o of ignore) if (i.endsWith(o)) return false;
|
|
1622
|
-
return true;
|
|
1623
|
-
});
|
|
1510
|
+
// src/actions/watch.ts
|
|
1511
|
+
var import_chokidar = __toESM(require("chokidar"));
|
|
1512
|
+
var import_cosmetic11 = __toESM(require("cosmetic"));
|
|
1513
|
+
var import_fs12 = require("fs");
|
|
1514
|
+
var import_path12 = require("path");
|
|
1515
|
+
var sameDev2 = (a, b) => {
|
|
1516
|
+
try {
|
|
1517
|
+
let bExisting = b;
|
|
1518
|
+
while (!(0, import_fs12.existsSync)(bExisting)) bExisting = (0, import_path12.dirname)(bExisting);
|
|
1519
|
+
return (0, import_fs12.statSync)(a).dev === (0, import_fs12.statSync)(bExisting).dev;
|
|
1520
|
+
} catch {
|
|
1521
|
+
return false;
|
|
1624
1522
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
}
|
|
1523
|
+
};
|
|
1524
|
+
var moveItem = (src, dest) => {
|
|
1525
|
+
if (sameDev2(src, dest)) {
|
|
1526
|
+
(0, import_fs12.renameSync)(src, dest);
|
|
1527
|
+
} else {
|
|
1528
|
+
(0, import_fs12.cpSync)(src, dest, { recursive: true });
|
|
1529
|
+
(0, import_fs12.rmSync)(src, { recursive: true, force: true });
|
|
1632
1530
|
}
|
|
1633
|
-
spinner_default.succeed(`checked differences between ${import_cosmetic9.default.blue.encoder(dir1)} and ${import_cosmetic9.default.blue.encoder(dir2)}`);
|
|
1634
|
-
spinner_default.succeed(`found ${added.length} added files`);
|
|
1635
|
-
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
1636
|
-
spinner_default.stop();
|
|
1637
|
-
for (const i of added) console.log(`${import_cosmetic9.default.green.encoder("added")} ${i}`);
|
|
1638
|
-
for (const i of removed) console.log(`${import_cosmetic9.default.red.encoder("removed")} ${i}`);
|
|
1639
1531
|
};
|
|
1640
|
-
var
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
var
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1532
|
+
var findVideo2 = (dir) => (0, import_fs12.readdirSync)(dir).find((f) => {
|
|
1533
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1534
|
+
return ext && videoExtensions_default.includes(ext);
|
|
1535
|
+
}) ?? null;
|
|
1536
|
+
var findSeasonFolder2 = (showPath, season) => {
|
|
1537
|
+
if (!(0, import_fs12.existsSync)(showPath)) return null;
|
|
1538
|
+
const folders = (0, import_fs12.readdirSync)(showPath).filter((f) => {
|
|
1539
|
+
try {
|
|
1540
|
+
return (0, import_fs12.lstatSync)((0, import_path12.resolve)(showPath, f)).isDirectory();
|
|
1541
|
+
} catch {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
return folders.find((f) => {
|
|
1546
|
+
const match = f.match(/(?:season|s)\s*0*(\d+)/i);
|
|
1547
|
+
return match && parseInt(match[1]) === season;
|
|
1548
|
+
}) ?? null;
|
|
1549
|
+
};
|
|
1550
|
+
var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
1650
1551
|
const config = getConfig();
|
|
1651
|
-
const
|
|
1552
|
+
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1553
|
+
const entry = (0, import_path12.basename)(entryPath);
|
|
1652
1554
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
renamed++;
|
|
1681
|
-
continue;
|
|
1555
|
+
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1556
|
+
const isDir = (0, import_fs12.lstatSync)(entryPath).isDirectory();
|
|
1557
|
+
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1558
|
+
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1559
|
+
if (!isDir && !isVideo) return;
|
|
1560
|
+
let detectedType;
|
|
1561
|
+
if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1562
|
+
detectedType = "ps3";
|
|
1563
|
+
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1564
|
+
detectedType = "tv";
|
|
1565
|
+
} else {
|
|
1566
|
+
detectedType = "movie";
|
|
1567
|
+
}
|
|
1568
|
+
const destRoot = config.dest[detectedType];
|
|
1569
|
+
if (!destRoot) {
|
|
1570
|
+
if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (detectedType === "ps3") {
|
|
1574
|
+
const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
|
|
1575
|
+
const id = entry.split("-")[0];
|
|
1576
|
+
if (!nameMatch || !id) return;
|
|
1577
|
+
const destName = `${nameMatch[0]} [${id}]`;
|
|
1578
|
+
const destPath = (0, import_path12.resolve)(destRoot, destName);
|
|
1579
|
+
if ((0, import_fs12.existsSync)(destPath)) {
|
|
1580
|
+
spinner_default.warn(`already exists: ${destName}`);
|
|
1581
|
+
return;
|
|
1682
1582
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1583
|
+
moveItem(entryPath, destPath);
|
|
1584
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1585
|
+
spinner_default.succeed(`imported ${import_cosmetic11.default.cyan.encoder(destName)}`);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const parsed = parseDownloadName(entry);
|
|
1589
|
+
if (!parsed) {
|
|
1590
|
+
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (detectedType === "tv") {
|
|
1594
|
+
if (parsed.season === void 0) {
|
|
1595
|
+
if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1596
|
+
return;
|
|
1688
1597
|
}
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1598
|
+
const registeredShow = getShowByTitle(parsed.title);
|
|
1599
|
+
let showPath;
|
|
1600
|
+
let showFolderName;
|
|
1601
|
+
if (registeredShow) {
|
|
1602
|
+
showPath = registeredShow.path;
|
|
1603
|
+
showFolderName = showPath.split("/").pop() ?? registeredShow.path;
|
|
1604
|
+
} else if (auto) {
|
|
1605
|
+
showFolderName = formatMovieName(movieFormat, parsed.title, parsed.year);
|
|
1606
|
+
showPath = (0, import_path12.resolve)(destRoot, showFolderName);
|
|
1607
|
+
upsertShow(showPath, null, parsed.title);
|
|
1608
|
+
} else {
|
|
1609
|
+
if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
|
|
1610
|
+
return;
|
|
1694
1611
|
}
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
if (!video) {
|
|
1702
|
-
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1703
|
-
skipped++;
|
|
1704
|
-
continue;
|
|
1612
|
+
const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1613
|
+
const seasonPath = (0, import_path12.resolve)(showPath, seasonFolderName);
|
|
1614
|
+
const videoFile2 = isDir ? findVideo2(entryPath) : entry;
|
|
1615
|
+
if (!videoFile2) {
|
|
1616
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1617
|
+
return;
|
|
1705
1618
|
}
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1619
|
+
const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
|
|
1620
|
+
const tmdbEpisodeName = registeredShow?.tmdbId && config.tmdbApiKey ? await getEpisodeName(registeredShow.tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1621
|
+
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, parsed.title, tmdbEpisodeName ?? void 0);
|
|
1622
|
+
const destVideoName2 = `${episodeName}.${videoExt2}`;
|
|
1623
|
+
const destVideoPath = (0, import_path12.resolve)(seasonPath, destVideoName2);
|
|
1624
|
+
const videoSourcePath2 = isDir ? (0, import_path12.resolve)(entryPath, videoFile2) : entryPath;
|
|
1625
|
+
if ((0, import_fs12.existsSync)(destVideoPath)) {
|
|
1626
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1627
|
+
return;
|
|
1711
1628
|
}
|
|
1712
|
-
const
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1629
|
+
const dirFiles2 = isDir ? (0, import_fs12.readdirSync)(entryPath) : [];
|
|
1630
|
+
const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
|
|
1631
|
+
const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
|
|
1632
|
+
const subtitleSourcePath2 = subtitle2 ? (0, import_path12.resolve)(entryPath, subtitle2) : null;
|
|
1633
|
+
const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
|
|
1634
|
+
(0, import_fs12.mkdirSync)(seasonPath, { recursive: true });
|
|
1635
|
+
let mode = "move";
|
|
1636
|
+
if (useHardlink) {
|
|
1637
|
+
try {
|
|
1638
|
+
if (!sameDev2(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
|
|
1639
|
+
(0, import_fs12.linkSync)(videoSourcePath2, destVideoPath);
|
|
1640
|
+
mode = "hardlink";
|
|
1641
|
+
} catch {
|
|
1642
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1643
|
+
(0, import_fs12.cpSync)(videoSourcePath2, destVideoPath);
|
|
1644
|
+
mode = "copy";
|
|
1645
|
+
}
|
|
1646
|
+
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.cpSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
|
|
1647
|
+
} else {
|
|
1648
|
+
if (sameDev2(videoSourcePath2, seasonPath)) {
|
|
1649
|
+
(0, import_fs12.renameSync)(videoSourcePath2, destVideoPath);
|
|
1650
|
+
} else {
|
|
1651
|
+
(0, import_fs12.cpSync)(videoSourcePath2, destVideoPath);
|
|
1652
|
+
(0, import_fs12.rmSync)(videoSourcePath2);
|
|
1653
|
+
}
|
|
1654
|
+
if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.renameSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
|
|
1655
|
+
if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
|
|
1718
1656
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1657
|
+
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1658
|
+
spinner_default.succeed(`imported ${import_cosmetic11.default.cyan.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const edition = detectEdition(entry);
|
|
1662
|
+
const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
|
|
1663
|
+
const destFolder = (0, import_path12.resolve)(destRoot, folderName);
|
|
1664
|
+
if ((0, import_fs12.existsSync)(destFolder)) {
|
|
1665
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const videoFile = isDir ? findVideo2(entryPath) : entry;
|
|
1669
|
+
if (!videoFile) {
|
|
1670
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1674
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1675
|
+
const videoSourcePath = isDir ? (0, import_path12.resolve)(entryPath, videoFile) : entryPath;
|
|
1676
|
+
const dirFiles = isDir ? (0, import_fs12.readdirSync)(entryPath) : [];
|
|
1677
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1678
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1679
|
+
const subtitleSourcePath = subtitle ? (0, import_path12.resolve)(entryPath, subtitle) : null;
|
|
1680
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1681
|
+
if (useHardlink) {
|
|
1682
|
+
(0, import_fs12.mkdirSync)(destFolder, { recursive: true });
|
|
1683
|
+
const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
|
|
1684
|
+
let mode;
|
|
1685
|
+
try {
|
|
1686
|
+
if (!sameDev2(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1687
|
+
(0, import_fs12.linkSync)(videoSourcePath, destVideoPath);
|
|
1688
|
+
mode = "hardlink";
|
|
1689
|
+
} catch {
|
|
1690
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1691
|
+
(0, import_fs12.cpSync)(videoSourcePath, destVideoPath);
|
|
1692
|
+
mode = "copy";
|
|
1726
1693
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
(0,
|
|
1694
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs12.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(destFolder, destSubtitleName));
|
|
1695
|
+
recordImport(sessionId, entryPath, destFolder, mode);
|
|
1696
|
+
} else {
|
|
1697
|
+
if (isDir) {
|
|
1698
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1699
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs12.rmSync)((0, import_path12.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1700
|
+
(0, import_fs12.renameSync)(videoSourcePath, (0, import_path12.resolve)(entryPath, destVideoName));
|
|
1701
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs12.renameSync)(subtitleSourcePath, (0, import_path12.resolve)(entryPath, destSubtitleName));
|
|
1702
|
+
moveItem(entryPath, destFolder);
|
|
1703
|
+
} else {
|
|
1704
|
+
(0, import_fs12.mkdirSync)(destFolder, { recursive: true });
|
|
1705
|
+
const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
|
|
1706
|
+
if (sameDev2(videoSourcePath, destRoot)) {
|
|
1707
|
+
(0, import_fs12.renameSync)(videoSourcePath, destVideoPath);
|
|
1708
|
+
} else {
|
|
1709
|
+
(0, import_fs12.cpSync)(videoSourcePath, destVideoPath);
|
|
1710
|
+
(0, import_fs12.rmSync)(videoSourcePath);
|
|
1711
|
+
}
|
|
1734
1712
|
}
|
|
1735
|
-
(
|
|
1736
|
-
recordRename(sessionId, fileOld, fileNew);
|
|
1737
|
-
recordRename(sessionId, folderOld, folderNew);
|
|
1738
|
-
spinner_default.succeed(formatted);
|
|
1739
|
-
renamed++;
|
|
1713
|
+
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1740
1714
|
}
|
|
1741
|
-
spinner_default.succeed(`
|
|
1742
|
-
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1743
|
-
spinner_default.info(`skipped ${skipped} files`);
|
|
1744
|
-
spinner_default.succeed(`done in ${import_cosmetic10.default.cyan.encoder(dir)}`);
|
|
1745
|
-
spinner_default.stop();
|
|
1715
|
+
spinner_default.succeed(`imported ${import_cosmetic11.default.cyan.encoder(folderName)}`);
|
|
1746
1716
|
};
|
|
1747
|
-
var
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
const showTitle = parentFolder.match(/^(.+?)\s*(?:\(\d{4}\))?$/)?.[1]?.trim() || void 0;
|
|
1772
|
-
spinner_default.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
|
|
1773
|
-
const sublist = list2.filter((f) => {
|
|
1774
|
-
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1775
|
-
return videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
1776
|
-
});
|
|
1777
|
-
const other = list2.filter((f) => {
|
|
1778
|
-
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1779
|
-
return !videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
1717
|
+
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1718
|
+
const config = getConfig();
|
|
1719
|
+
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1720
|
+
const language = config.language ?? "eng";
|
|
1721
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1722
|
+
const handle = (path) => {
|
|
1723
|
+
const existing = pending.get(path);
|
|
1724
|
+
if (existing) clearTimeout(existing);
|
|
1725
|
+
pending.set(
|
|
1726
|
+
path,
|
|
1727
|
+
setTimeout(async () => {
|
|
1728
|
+
pending.delete(path);
|
|
1729
|
+
try {
|
|
1730
|
+
await processItem(path, hardlink, verbose, language, auto);
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
|
1733
|
+
}
|
|
1734
|
+
}, 5e3)
|
|
1735
|
+
);
|
|
1736
|
+
};
|
|
1737
|
+
const watcher = import_chokidar.default.watch(config.sources, {
|
|
1738
|
+
depth: 0,
|
|
1739
|
+
ignoreInitial: true,
|
|
1740
|
+
awaitWriteFinish: { stabilityThreshold: 5e3, pollInterval: 500 }
|
|
1780
1741
|
});
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const episode = double ? index * 2 + 1 : index + 1;
|
|
1787
|
-
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
1788
|
-
if (i === name) {
|
|
1789
|
-
skipped++;
|
|
1790
|
-
continue;
|
|
1791
|
-
}
|
|
1792
|
-
(0, import_fs12.renameSync)((0, import_path12.resolve)(dir, i), (0, import_path12.resolve)(dir, name));
|
|
1793
|
-
renamed++;
|
|
1794
|
-
}
|
|
1795
|
-
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1796
|
-
spinner_default.info(`skipped ${skipped} files`);
|
|
1797
|
-
spinner_default.succeed(`done in ${import_cosmetic11.default.cyan.encoder(dir)}`);
|
|
1742
|
+
watcher.on("addDir", handle);
|
|
1743
|
+
watcher.on("add", handle);
|
|
1744
|
+
spinner_default.start();
|
|
1745
|
+
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1746
|
+
for (const s of config.sources) spinner_default.info(` ${import_cosmetic11.default.blue.encoder(s)}`);
|
|
1798
1747
|
spinner_default.stop();
|
|
1748
|
+
process.stdin.resume();
|
|
1799
1749
|
};
|
|
1800
|
-
var
|
|
1750
|
+
var watch_default = watch;
|
|
1801
1751
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1802
1752
|
0 && (module.exports = {
|
|
1803
1753
|
DEFAULT_EPISODE_FORMAT,
|