jav-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +188 -0
- package/README.ko.md +188 -0
- package/README.md +173 -0
- package/README.zh-CN.md +188 -0
- package/bin/jav-manager.js +3 -0
- package/dist/cli.js +774 -0
- package/dist/config.js +324 -0
- package/dist/context.js +2 -0
- package/dist/data/cache.js +201 -0
- package/dist/data/curlImpersonateFetcher.js +499 -0
- package/dist/data/everything.js +130 -0
- package/dist/data/javdb.js +646 -0
- package/dist/data/qbittorrent.js +214 -0
- package/dist/gui.js +417 -0
- package/dist/index.js +81 -0
- package/dist/interfaces.js +2 -0
- package/dist/localization.js +114 -0
- package/dist/models.js +15 -0
- package/dist/services.js +526 -0
- package/dist/utils/appInfo.js +24 -0
- package/dist/utils/appPaths.js +31 -0
- package/dist/utils/cliDisplay.js +551 -0
- package/dist/utils/curlBinaryResolver.js +154 -0
- package/dist/utils/httpHelper.js +78 -0
- package/dist/utils/platformShell.js +18 -0
- package/dist/utils/sizeParser.js +48 -0
- package/dist/utils/telemetryEndpoints.js +44 -0
- package/dist/utils/torrentNameParser.js +44 -0
- package/dist/utils/weightCalculator.js +38 -0
- package/package.json +41 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI display utilities — rich terminal output via ANSI escape sequences.
|
|
4
|
+
*
|
|
5
|
+
* Zero external dependencies. Works on modern terminals (Windows Terminal,
|
|
6
|
+
* iTerm2, most Linux terminals). Gracefully degrades when NO_COLOR is set.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.c = c;
|
|
10
|
+
exports.printBanner = printBanner;
|
|
11
|
+
exports.printHelp = printHelp;
|
|
12
|
+
exports.printTorrentList = printTorrentList;
|
|
13
|
+
exports.printSearchResultList = printSearchResultList;
|
|
14
|
+
exports.printHealthResults = printHealthResults;
|
|
15
|
+
exports.printConfig = printConfig;
|
|
16
|
+
exports.printCacheStats = printCacheStats;
|
|
17
|
+
exports.printDownloadList = printDownloadList;
|
|
18
|
+
exports.printLocalFiles = printLocalFiles;
|
|
19
|
+
exports.printSearching = printSearching;
|
|
20
|
+
exports.printSuccess = printSuccess;
|
|
21
|
+
exports.printError = printError;
|
|
22
|
+
exports.printWarning = printWarning;
|
|
23
|
+
exports.printInfo = printInfo;
|
|
24
|
+
exports.printMagnetLink = printMagnetLink;
|
|
25
|
+
exports.getPrompt = getPrompt;
|
|
26
|
+
exports.formatSize = formatBytes;
|
|
27
|
+
// ─── ANSI primitives ────────────────────────────────────────────────
|
|
28
|
+
const isColorDisabled = () => !!process.env.NO_COLOR || process.env.TERM === "dumb";
|
|
29
|
+
function esc(code) {
|
|
30
|
+
return isColorDisabled() ? "" : `\x1b[${code}m`;
|
|
31
|
+
}
|
|
32
|
+
// Reset
|
|
33
|
+
const R = () => esc("0");
|
|
34
|
+
// Styles
|
|
35
|
+
const BOLD = () => esc("1");
|
|
36
|
+
const DIM = () => esc("2");
|
|
37
|
+
const ITALIC = () => esc("3");
|
|
38
|
+
const UNDERLINE = () => esc("4");
|
|
39
|
+
// Foreground colors
|
|
40
|
+
const FG_BLACK = () => esc("30");
|
|
41
|
+
const FG_RED = () => esc("31");
|
|
42
|
+
const FG_GREEN = () => esc("32");
|
|
43
|
+
const FG_YELLOW = () => esc("33");
|
|
44
|
+
const FG_BLUE = () => esc("34");
|
|
45
|
+
const FG_MAGENTA = () => esc("35");
|
|
46
|
+
const FG_CYAN = () => esc("36");
|
|
47
|
+
const FG_WHITE = () => esc("37");
|
|
48
|
+
const FG_GRAY = () => esc("90");
|
|
49
|
+
// Bright foreground
|
|
50
|
+
const FG_BRIGHT_RED = () => esc("91");
|
|
51
|
+
const FG_BRIGHT_GREEN = () => esc("92");
|
|
52
|
+
const FG_BRIGHT_YELLOW = () => esc("93");
|
|
53
|
+
const FG_BRIGHT_BLUE = () => esc("94");
|
|
54
|
+
const FG_BRIGHT_MAGENTA = () => esc("95");
|
|
55
|
+
const FG_BRIGHT_CYAN = () => esc("96");
|
|
56
|
+
const FG_BRIGHT_WHITE = () => esc("97");
|
|
57
|
+
// Background colors
|
|
58
|
+
const BG_BLACK = () => esc("40");
|
|
59
|
+
const BG_RED = () => esc("41");
|
|
60
|
+
const BG_GREEN = () => esc("42");
|
|
61
|
+
const BG_YELLOW = () => esc("43");
|
|
62
|
+
const BG_BLUE = () => esc("44");
|
|
63
|
+
const BG_MAGENTA = () => esc("45");
|
|
64
|
+
const BG_CYAN = () => esc("46");
|
|
65
|
+
// ─── Semantic helpers ───────────────────────────────────────────────
|
|
66
|
+
function c(text, ...codes) {
|
|
67
|
+
if (isColorDisabled())
|
|
68
|
+
return text;
|
|
69
|
+
return codes.map((fn) => fn()).join("") + text + R();
|
|
70
|
+
}
|
|
71
|
+
// Strip ANSI codes for width calculations
|
|
72
|
+
function stripAnsi(s) {
|
|
73
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
74
|
+
}
|
|
75
|
+
function visibleLength(s) {
|
|
76
|
+
const plain = stripAnsi(s);
|
|
77
|
+
let width = 0;
|
|
78
|
+
for (const ch of Array.from(plain)) {
|
|
79
|
+
width += charWidth(ch);
|
|
80
|
+
}
|
|
81
|
+
return width;
|
|
82
|
+
}
|
|
83
|
+
function padEnd(s, width) {
|
|
84
|
+
const vl = visibleLength(s);
|
|
85
|
+
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
86
|
+
}
|
|
87
|
+
function padStart(s, width) {
|
|
88
|
+
const vl = visibleLength(s);
|
|
89
|
+
return vl >= width ? s : " ".repeat(width - vl) + s;
|
|
90
|
+
}
|
|
91
|
+
function centerPad(s, width) {
|
|
92
|
+
const vl = visibleLength(s);
|
|
93
|
+
if (vl >= width)
|
|
94
|
+
return s;
|
|
95
|
+
const left = Math.floor((width - vl) / 2);
|
|
96
|
+
const right = width - vl - left;
|
|
97
|
+
return " ".repeat(left) + s + " ".repeat(right);
|
|
98
|
+
}
|
|
99
|
+
function truncateText(s, width) {
|
|
100
|
+
if (width <= 0)
|
|
101
|
+
return "";
|
|
102
|
+
if (visibleLength(s) <= width)
|
|
103
|
+
return s;
|
|
104
|
+
const chars = Array.from(s);
|
|
105
|
+
if (width <= 3) {
|
|
106
|
+
let out = "";
|
|
107
|
+
let used = 0;
|
|
108
|
+
for (const ch of chars) {
|
|
109
|
+
const w = charWidth(ch);
|
|
110
|
+
if (used + w > width)
|
|
111
|
+
break;
|
|
112
|
+
out += ch;
|
|
113
|
+
used += w;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
const target = width - 3;
|
|
118
|
+
let out = "";
|
|
119
|
+
let used = 0;
|
|
120
|
+
for (const ch of chars) {
|
|
121
|
+
const w = charWidth(ch);
|
|
122
|
+
if (used + w > target)
|
|
123
|
+
break;
|
|
124
|
+
out += ch;
|
|
125
|
+
used += w;
|
|
126
|
+
}
|
|
127
|
+
return `${out}...`;
|
|
128
|
+
}
|
|
129
|
+
function charWidth(ch) {
|
|
130
|
+
const codePoint = ch.codePointAt(0);
|
|
131
|
+
if (!codePoint)
|
|
132
|
+
return 0;
|
|
133
|
+
if (isControl(codePoint) || isCombining(codePoint))
|
|
134
|
+
return 0;
|
|
135
|
+
return isFullWidth(codePoint) ? 2 : 1;
|
|
136
|
+
}
|
|
137
|
+
function isControl(codePoint) {
|
|
138
|
+
return (codePoint >= 0 && codePoint < 32) || (codePoint >= 0x7f && codePoint < 0xa0);
|
|
139
|
+
}
|
|
140
|
+
function isCombining(codePoint) {
|
|
141
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) || // Combining Diacritical Marks
|
|
142
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) || // Combining Diacritical Marks Extended
|
|
143
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) || // Combining Diacritical Marks Supplement
|
|
144
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) || // Combining Diacritical Marks for Symbols
|
|
145
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f) // Combining Half Marks
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function isFullWidth(codePoint) {
|
|
149
|
+
return (codePoint >= 0x1100 && (codePoint <= 0x115f || // Hangul Jamo
|
|
150
|
+
codePoint === 0x2329 ||
|
|
151
|
+
codePoint === 0x232a ||
|
|
152
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) || // CJK ... Yi
|
|
153
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
|
154
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
|
155
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) || // Vertical forms
|
|
156
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) || // CJK Compatibility Forms + Small Form Variants
|
|
157
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) || // Fullwidth Forms
|
|
158
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
159
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) || // Emojis
|
|
160
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
161
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
162
|
+
}
|
|
163
|
+
// ─── Box-drawing characters (Unicode) ───────────────────────────────
|
|
164
|
+
const BOX = {
|
|
165
|
+
tl: "\u250C", // ┌
|
|
166
|
+
tr: "\u2510", // ┐
|
|
167
|
+
bl: "\u2514", // └
|
|
168
|
+
br: "\u2518", // ┘
|
|
169
|
+
h: "\u2500", // ─
|
|
170
|
+
v: "\u2502", // │
|
|
171
|
+
lT: "\u251C", // ├
|
|
172
|
+
rT: "\u2524", // ┤
|
|
173
|
+
tT: "\u252C", // ┬
|
|
174
|
+
bT: "\u2534", // ┴
|
|
175
|
+
cross: "\u253C", // ┼
|
|
176
|
+
// Double-line variants for emphasis
|
|
177
|
+
dh: "\u2550", // ═
|
|
178
|
+
dv: "\u2551", // ║
|
|
179
|
+
dtl: "\u2554", // ╔
|
|
180
|
+
dtr: "\u2557", // ╗
|
|
181
|
+
dbl: "\u255A", // ╚
|
|
182
|
+
dbr: "\u255D", // ╝
|
|
183
|
+
dlT: "\u2560", // ╠
|
|
184
|
+
drT: "\u2563", // ╣
|
|
185
|
+
};
|
|
186
|
+
// ─── Terminal width ─────────────────────────────────────────────────
|
|
187
|
+
function getTermWidth() {
|
|
188
|
+
return process.stdout.columns || 80;
|
|
189
|
+
}
|
|
190
|
+
function clampWidth(desired) {
|
|
191
|
+
return Math.min(desired, getTermWidth());
|
|
192
|
+
}
|
|
193
|
+
// ─── Public components ──────────────────────────────────────────────
|
|
194
|
+
/**
|
|
195
|
+
* Print the app banner at startup.
|
|
196
|
+
*/
|
|
197
|
+
function printBanner(appName, version) {
|
|
198
|
+
const w = clampWidth(56);
|
|
199
|
+
const inner = w - 4; // inside the double-line box
|
|
200
|
+
const titleText = `${appName} v${version}`;
|
|
201
|
+
const subtitle = "Content Management CLI";
|
|
202
|
+
const top = `${c(BOX.dtl + BOX.dh.repeat(w - 2) + BOX.dtr, FG_CYAN)}`;
|
|
203
|
+
const bot = `${c(BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr, FG_CYAN)}`;
|
|
204
|
+
const mid = `${c(BOX.dlT + BOX.dh.repeat(w - 2) + BOX.drT, FG_CYAN)}`;
|
|
205
|
+
const vb = c(BOX.dv, FG_CYAN);
|
|
206
|
+
const empty = `${vb} ${" ".repeat(inner)} ${vb}`;
|
|
207
|
+
console.log("");
|
|
208
|
+
console.log(top);
|
|
209
|
+
console.log(empty);
|
|
210
|
+
console.log(`${vb} ${centerPad(c(titleText, BOLD, FG_BRIGHT_WHITE), inner)} ${vb}`);
|
|
211
|
+
console.log(`${vb} ${centerPad(c(subtitle, DIM, FG_GRAY), inner)} ${vb}`);
|
|
212
|
+
console.log(empty);
|
|
213
|
+
console.log(mid);
|
|
214
|
+
// Minimal keyboard hints
|
|
215
|
+
const hints = [
|
|
216
|
+
`${c("JAV-ID", FG_BRIGHT_CYAN)} search & download`,
|
|
217
|
+
`${c("help", FG_BRIGHT_CYAN)} show commands`,
|
|
218
|
+
`${c("quit", FG_BRIGHT_CYAN)} exit`,
|
|
219
|
+
];
|
|
220
|
+
for (const hint of hints) {
|
|
221
|
+
console.log(`${vb} ${padEnd(" " + hint, inner)} ${vb}`);
|
|
222
|
+
}
|
|
223
|
+
console.log(bot);
|
|
224
|
+
console.log("");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Print structured help with category groupings.
|
|
228
|
+
*/
|
|
229
|
+
function printHelp(commands) {
|
|
230
|
+
const w = clampWidth(62);
|
|
231
|
+
const inner = w - 4;
|
|
232
|
+
const headerLine = c(BOX.h.repeat(w), FG_CYAN);
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(headerLine);
|
|
235
|
+
console.log(c(" COMMANDS", BOLD, FG_BRIGHT_WHITE));
|
|
236
|
+
console.log(headerLine);
|
|
237
|
+
// Group by category
|
|
238
|
+
const categories = new Map();
|
|
239
|
+
for (const item of commands) {
|
|
240
|
+
if (!categories.has(item.category)) {
|
|
241
|
+
categories.set(item.category, []);
|
|
242
|
+
}
|
|
243
|
+
categories.get(item.category).push({ cmd: item.cmd, desc: item.desc });
|
|
244
|
+
}
|
|
245
|
+
for (const [cat, items] of categories) {
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(` ${c(cat, BOLD, FG_YELLOW)}`);
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
const cmdPart = c(item.cmd, FG_BRIGHT_CYAN);
|
|
250
|
+
console.log(` ${padEnd(cmdPart, 22)} ${c(item.desc, FG_WHITE)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
console.log("");
|
|
254
|
+
console.log(headerLine);
|
|
255
|
+
console.log("");
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Display search results as a formatted, numbered table.
|
|
259
|
+
*/
|
|
260
|
+
function printTorrentList(javId, torrents, legend) {
|
|
261
|
+
if (torrents.length === 0)
|
|
262
|
+
return;
|
|
263
|
+
const totalWidth = getTermWidth();
|
|
264
|
+
let idxW = 3;
|
|
265
|
+
let sizeW = 10;
|
|
266
|
+
let tagsW = 14;
|
|
267
|
+
const colCount = 4;
|
|
268
|
+
const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
|
|
269
|
+
let titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
|
|
270
|
+
if (titleW < 16) {
|
|
271
|
+
tagsW = 10;
|
|
272
|
+
titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
|
|
273
|
+
}
|
|
274
|
+
if (titleW < 16) {
|
|
275
|
+
sizeW = 8;
|
|
276
|
+
titleW = totalWidth - baseOverhead - idxW - sizeW - tagsW;
|
|
277
|
+
}
|
|
278
|
+
titleW = Math.max(16, titleW);
|
|
279
|
+
const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(titleW + 2) + BOX.tT + BOX.h.repeat(sizeW + 2) + BOX.tT + BOX.h.repeat(tagsW + 2) + BOX.tr, FG_GRAY);
|
|
280
|
+
const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(titleW + 2) + BOX.cross + BOX.h.repeat(sizeW + 2) + BOX.cross + BOX.h.repeat(tagsW + 2) + BOX.rT, FG_GRAY);
|
|
281
|
+
const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(titleW + 2) + BOX.bT + BOX.h.repeat(sizeW + 2) + BOX.bT + BOX.h.repeat(tagsW + 2) + BOX.br, FG_GRAY);
|
|
282
|
+
console.log("");
|
|
283
|
+
console.log(` ${c("Results for", DIM, FG_GRAY)} ${c(javId, BOLD, FG_BRIGHT_WHITE)}`);
|
|
284
|
+
console.log(` ${top}`);
|
|
285
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Title", BOLD, FG_BRIGHT_WHITE), titleW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Size", BOLD, FG_BRIGHT_WHITE), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Tags", BOLD, FG_BRIGHT_WHITE), tagsW)} ${c(BOX.v, FG_GRAY)}`);
|
|
286
|
+
console.log(` ${mid}`);
|
|
287
|
+
for (let i = 0; i < torrents.length; i++) {
|
|
288
|
+
const t = torrents[i];
|
|
289
|
+
const idx = String(i + 1);
|
|
290
|
+
const title = truncateText(t.title, titleW);
|
|
291
|
+
const size = t.size !== "-" ? t.size : "-";
|
|
292
|
+
const tags = truncateText(t.tags.join(" "), tagsW);
|
|
293
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(title, FG_WHITE), titleW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(size, FG_BRIGHT_YELLOW), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(tags, FG_WHITE), tagsW)} ${c(BOX.v, FG_GRAY)}`);
|
|
294
|
+
}
|
|
295
|
+
console.log(` ${bot}`);
|
|
296
|
+
if (legend) {
|
|
297
|
+
console.log(` ${c(legend, DIM, FG_GRAY)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Display searchable candidate items before showing torrents.
|
|
302
|
+
*/
|
|
303
|
+
function printSearchResultList(query, items) {
|
|
304
|
+
if (items.length === 0)
|
|
305
|
+
return;
|
|
306
|
+
const totalWidth = getTermWidth();
|
|
307
|
+
let idxW = 3;
|
|
308
|
+
let javIdW = 12;
|
|
309
|
+
let sourceW = 8;
|
|
310
|
+
const colCount = 4;
|
|
311
|
+
const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
|
|
312
|
+
let titleW = totalWidth - baseOverhead - idxW - javIdW - sourceW;
|
|
313
|
+
if (titleW < 18) {
|
|
314
|
+
javIdW = 10;
|
|
315
|
+
sourceW = 6;
|
|
316
|
+
titleW = totalWidth - baseOverhead - idxW - javIdW - sourceW;
|
|
317
|
+
}
|
|
318
|
+
titleW = Math.max(18, titleW);
|
|
319
|
+
const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(javIdW + 2) + BOX.tT + BOX.h.repeat(sourceW + 2) + BOX.tT + BOX.h.repeat(titleW + 2) + BOX.tr, FG_GRAY);
|
|
320
|
+
const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(javIdW + 2) + BOX.cross + BOX.h.repeat(sourceW + 2) + BOX.cross + BOX.h.repeat(titleW + 2) + BOX.rT, FG_GRAY);
|
|
321
|
+
const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(javIdW + 2) + BOX.bT + BOX.h.repeat(sourceW + 2) + BOX.bT + BOX.h.repeat(titleW + 2) + BOX.br, FG_GRAY);
|
|
322
|
+
console.log("");
|
|
323
|
+
console.log(` ${c("SEARCH RESULTS", BOLD, FG_BRIGHT_WHITE)} ${c(`(${items.length})`, DIM, FG_GRAY)} ${c(`for ${query}`, DIM, FG_GRAY)}`);
|
|
324
|
+
console.log(` ${top}`);
|
|
325
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("JAV-ID", BOLD, FG_BRIGHT_WHITE), javIdW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Source", BOLD, FG_BRIGHT_WHITE), sourceW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Title", BOLD, FG_BRIGHT_WHITE), titleW)} ${c(BOX.v, FG_GRAY)}`);
|
|
326
|
+
console.log(` ${mid}`);
|
|
327
|
+
for (let i = 0; i < items.length; i++) {
|
|
328
|
+
const item = items[i];
|
|
329
|
+
const idx = String(i + 1);
|
|
330
|
+
const id = truncateText(item.javId || "-", javIdW);
|
|
331
|
+
const source = truncateText(item.source || "-", sourceW);
|
|
332
|
+
const title = truncateText(item.title || "", titleW);
|
|
333
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(id, FG_BRIGHT_CYAN, BOLD), javIdW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(source, FG_GRAY), sourceW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(title, FG_WHITE), titleW)} ${c(BOX.v, FG_GRAY)}`);
|
|
334
|
+
}
|
|
335
|
+
console.log(` ${bot}`);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Format a torrent tag badge.
|
|
339
|
+
*/
|
|
340
|
+
function formatTag(tag) {
|
|
341
|
+
const upper = tag.toUpperCase();
|
|
342
|
+
if (upper.includes("UC") || upper.includes("UNCENSOR")) {
|
|
343
|
+
return c(` ${tag} `, BG_MAGENTA, FG_BRIGHT_WHITE, BOLD);
|
|
344
|
+
}
|
|
345
|
+
if (upper.includes("SUB") || upper.includes("字幕")) {
|
|
346
|
+
return c(` ${tag} `, BG_CYAN, FG_BLACK, BOLD);
|
|
347
|
+
}
|
|
348
|
+
if (upper.includes("HD") || upper.includes("4K") || upper.includes("1080")) {
|
|
349
|
+
return c(` ${tag} `, BG_BLUE, FG_BRIGHT_WHITE, BOLD);
|
|
350
|
+
}
|
|
351
|
+
return c(` ${tag} `, BG_BLACK, FG_GRAY);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Print health check results with status indicators.
|
|
355
|
+
*/
|
|
356
|
+
function printHealthResults(results) {
|
|
357
|
+
const w = clampWidth(60);
|
|
358
|
+
const line = c(BOX.h.repeat(w), FG_GRAY);
|
|
359
|
+
console.log("");
|
|
360
|
+
console.log(` ${c("SERVICE HEALTH", BOLD, FG_BRIGHT_WHITE)}`);
|
|
361
|
+
console.log(line);
|
|
362
|
+
for (const r of results) {
|
|
363
|
+
const icon = r.healthy
|
|
364
|
+
? c("\u25CF", FG_BRIGHT_GREEN) // ●
|
|
365
|
+
: c("\u25CF", FG_BRIGHT_RED); // ●
|
|
366
|
+
const status = r.healthy
|
|
367
|
+
? c("OK", FG_BRIGHT_GREEN)
|
|
368
|
+
: c("FAIL", FG_BRIGHT_RED, BOLD);
|
|
369
|
+
const name = padEnd(c(r.name, FG_WHITE), 20);
|
|
370
|
+
const msg = r.message ? c(r.message, DIM, FG_GRAY) : "";
|
|
371
|
+
console.log(` ${icon} ${name} ${status} ${msg}`);
|
|
372
|
+
}
|
|
373
|
+
console.log(line);
|
|
374
|
+
console.log("");
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Print config as a structured key-value display.
|
|
378
|
+
*/
|
|
379
|
+
function printConfig(entries) {
|
|
380
|
+
const w = clampWidth(60);
|
|
381
|
+
const line = c(BOX.h.repeat(w), FG_GRAY);
|
|
382
|
+
console.log("");
|
|
383
|
+
console.log(` ${c("CONFIGURATION", BOLD, FG_BRIGHT_WHITE)}`);
|
|
384
|
+
console.log(line);
|
|
385
|
+
let lastSection = "";
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (entry.section !== lastSection) {
|
|
388
|
+
if (lastSection)
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(` ${c(entry.section, BOLD, FG_YELLOW)}`);
|
|
391
|
+
lastSection = entry.section;
|
|
392
|
+
}
|
|
393
|
+
const key = padEnd(c(entry.key, FG_CYAN), 18);
|
|
394
|
+
const val = entry.masked
|
|
395
|
+
? c(entry.value, DIM, FG_GRAY)
|
|
396
|
+
: (entry.value === "-" ? c("-", DIM, FG_GRAY) : c(entry.value, FG_WHITE));
|
|
397
|
+
console.log(` ${key} ${val}`);
|
|
398
|
+
}
|
|
399
|
+
console.log(line);
|
|
400
|
+
console.log("");
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Print cache statistics.
|
|
404
|
+
*/
|
|
405
|
+
function printCacheStats(totalJav, totalTorrents, sizeBytes) {
|
|
406
|
+
const w = clampWidth(50);
|
|
407
|
+
const line = c(BOX.h.repeat(w), FG_GRAY);
|
|
408
|
+
console.log("");
|
|
409
|
+
console.log(` ${c("CACHE STATISTICS", BOLD, FG_BRIGHT_WHITE)}`);
|
|
410
|
+
console.log(line);
|
|
411
|
+
console.log(` ${c("Items", FG_CYAN)} ${c(String(totalJav), FG_BRIGHT_WHITE, BOLD)}`);
|
|
412
|
+
console.log(` ${c("Torrents", FG_CYAN)} ${c(String(totalTorrents), FG_BRIGHT_WHITE, BOLD)}`);
|
|
413
|
+
console.log(` ${c("Size", FG_CYAN)} ${c(formatBytes(sizeBytes), FG_BRIGHT_YELLOW)}`);
|
|
414
|
+
console.log(line);
|
|
415
|
+
console.log("");
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Print a download list.
|
|
419
|
+
*/
|
|
420
|
+
function printDownloadList(downloads) {
|
|
421
|
+
if (downloads.length === 0) {
|
|
422
|
+
printInfo("No active downloads");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const w = clampWidth(80);
|
|
426
|
+
const line = c(BOX.h.repeat(w), FG_GRAY);
|
|
427
|
+
console.log("");
|
|
428
|
+
console.log(` ${c("DOWNLOADS", BOLD, FG_BRIGHT_WHITE)} ${c(`(${downloads.length})`, DIM, FG_GRAY)}`);
|
|
429
|
+
console.log(line);
|
|
430
|
+
for (const d of downloads) {
|
|
431
|
+
const stateColor = getStateColor(d.state);
|
|
432
|
+
const icon = getStateIcon(d.state);
|
|
433
|
+
const name = c(d.name, FG_WHITE);
|
|
434
|
+
const size = c(d.size, FG_BRIGHT_YELLOW);
|
|
435
|
+
const state = c(d.state, ...stateColor);
|
|
436
|
+
console.log(` ${icon} ${name}`);
|
|
437
|
+
console.log(` ${size} ${state}`);
|
|
438
|
+
}
|
|
439
|
+
console.log(line);
|
|
440
|
+
console.log("");
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Print local file results.
|
|
444
|
+
*/
|
|
445
|
+
function printLocalFiles(files, options) {
|
|
446
|
+
if (files.length === 0)
|
|
447
|
+
return;
|
|
448
|
+
const startIndex = options?.startIndex ?? 0;
|
|
449
|
+
const totalCount = options?.totalCount ?? files.length;
|
|
450
|
+
const page = options?.page;
|
|
451
|
+
const totalPages = options?.totalPages;
|
|
452
|
+
const totalWidth = getTermWidth();
|
|
453
|
+
let idxW = 3;
|
|
454
|
+
let nameW = 24;
|
|
455
|
+
let sizeW = 10;
|
|
456
|
+
const colCount = 4;
|
|
457
|
+
const baseOverhead = 2 + 1 + colCount * 2 + (colCount - 1) + 2; // indent + borders/padding
|
|
458
|
+
let pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
|
|
459
|
+
if (pathW < 20) {
|
|
460
|
+
nameW = 18;
|
|
461
|
+
pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
|
|
462
|
+
}
|
|
463
|
+
if (pathW < 16) {
|
|
464
|
+
sizeW = 8;
|
|
465
|
+
pathW = totalWidth - baseOverhead - idxW - nameW - sizeW;
|
|
466
|
+
}
|
|
467
|
+
pathW = Math.max(16, pathW);
|
|
468
|
+
const top = c(BOX.tl + BOX.h.repeat(idxW + 2) + BOX.tT + BOX.h.repeat(nameW + 2) + BOX.tT + BOX.h.repeat(sizeW + 2) + BOX.tT + BOX.h.repeat(pathW + 2) + BOX.tr, FG_GRAY);
|
|
469
|
+
const mid = c(BOX.lT + BOX.h.repeat(idxW + 2) + BOX.cross + BOX.h.repeat(nameW + 2) + BOX.cross + BOX.h.repeat(sizeW + 2) + BOX.cross + BOX.h.repeat(pathW + 2) + BOX.rT, FG_GRAY);
|
|
470
|
+
const bot = c(BOX.bl + BOX.h.repeat(idxW + 2) + BOX.bT + BOX.h.repeat(nameW + 2) + BOX.bT + BOX.h.repeat(sizeW + 2) + BOX.bT + BOX.h.repeat(pathW + 2) + BOX.br, FG_GRAY);
|
|
471
|
+
console.log("");
|
|
472
|
+
const pageText = page && totalPages ? ` ${c(`Page ${page}/${totalPages}`, DIM, FG_GRAY)}` : "";
|
|
473
|
+
console.log(` ${c("LOCAL FILES", BOLD, FG_BRIGHT_WHITE)} ${c(`(${totalCount})`, DIM, FG_GRAY)}${pageText}`);
|
|
474
|
+
console.log(` ${top}`);
|
|
475
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c("#", BOLD, FG_BRIGHT_WHITE), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Name", BOLD, FG_BRIGHT_WHITE), nameW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Size", BOLD, FG_BRIGHT_WHITE), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c("Path", BOLD, FG_BRIGHT_WHITE), pathW)} ${c(BOX.v, FG_GRAY)}`);
|
|
476
|
+
console.log(` ${mid}`);
|
|
477
|
+
for (let i = 0; i < files.length; i++) {
|
|
478
|
+
const f = files[i];
|
|
479
|
+
const idx = String(startIndex + i + 1);
|
|
480
|
+
const name = truncateText(f.name || "-", nameW);
|
|
481
|
+
const size = f.size || "-";
|
|
482
|
+
const path = truncateText(f.path || "-", pathW);
|
|
483
|
+
console.log(` ${c(BOX.v, FG_GRAY)} ${padStart(c(idx, FG_GRAY), idxW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(name, FG_WHITE), nameW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(size, FG_BRIGHT_YELLOW), sizeW)} ${c(BOX.v, FG_GRAY)} ${padEnd(c(path, DIM, FG_GRAY), pathW)} ${c(BOX.v, FG_GRAY)}`);
|
|
484
|
+
}
|
|
485
|
+
console.log(` ${bot}`);
|
|
486
|
+
console.log("");
|
|
487
|
+
}
|
|
488
|
+
// ─── Status messages ────────────────────────────────────────────────
|
|
489
|
+
function printSearching(javId) {
|
|
490
|
+
console.log(` ${c("\u25B6", FG_BRIGHT_CYAN)} ${c("Searching", FG_WHITE)} ${c(javId, BOLD, FG_BRIGHT_WHITE)}${c("...", DIM, FG_GRAY)}`);
|
|
491
|
+
}
|
|
492
|
+
function printSuccess(msg) {
|
|
493
|
+
console.log(` ${c("\u2714", FG_BRIGHT_GREEN)} ${c(msg, FG_GREEN)}`);
|
|
494
|
+
}
|
|
495
|
+
function printError(msg) {
|
|
496
|
+
console.log(` ${c("\u2718", FG_BRIGHT_RED)} ${c(msg, FG_RED)}`);
|
|
497
|
+
}
|
|
498
|
+
function printWarning(msg) {
|
|
499
|
+
console.log(` ${c("\u26A0", FG_BRIGHT_YELLOW)} ${c(msg, FG_YELLOW)}`);
|
|
500
|
+
}
|
|
501
|
+
function printInfo(msg) {
|
|
502
|
+
console.log(` ${c("\u2022", FG_CYAN)} ${c(msg, FG_WHITE)}`);
|
|
503
|
+
}
|
|
504
|
+
function printMagnetLink(msg, link) {
|
|
505
|
+
console.log(` ${c("\u26A0", FG_BRIGHT_YELLOW)} ${c(msg, FG_YELLOW)}`);
|
|
506
|
+
console.log(` ${c(link, FG_BRIGHT_CYAN, BOLD, UNDERLINE)}`);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Styled prompt string for readline.
|
|
510
|
+
*/
|
|
511
|
+
function getPrompt() {
|
|
512
|
+
return `${c("\u276F", FG_BRIGHT_CYAN)} `;
|
|
513
|
+
}
|
|
514
|
+
// ─── Internal helpers ───────────────────────────────────────────────
|
|
515
|
+
function formatBytes(bytes) {
|
|
516
|
+
if (bytes <= 0)
|
|
517
|
+
return "-";
|
|
518
|
+
if (bytes >= 1024 * 1024 * 1024)
|
|
519
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
520
|
+
if (bytes >= 1024 * 1024)
|
|
521
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
522
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
523
|
+
}
|
|
524
|
+
function getStateColor(state) {
|
|
525
|
+
const lower = state.toLowerCase();
|
|
526
|
+
if (/download|metadl/i.test(lower))
|
|
527
|
+
return [FG_BRIGHT_CYAN, BOLD];
|
|
528
|
+
if (/stalled/i.test(lower))
|
|
529
|
+
return [FG_BRIGHT_YELLOW];
|
|
530
|
+
if (/upload|seed/i.test(lower))
|
|
531
|
+
return [FG_BRIGHT_GREEN];
|
|
532
|
+
if (/pause/i.test(lower))
|
|
533
|
+
return [FG_GRAY];
|
|
534
|
+
if (/error|missing/i.test(lower))
|
|
535
|
+
return [FG_BRIGHT_RED];
|
|
536
|
+
return [FG_WHITE];
|
|
537
|
+
}
|
|
538
|
+
function getStateIcon(state) {
|
|
539
|
+
const lower = state.toLowerCase();
|
|
540
|
+
if (/download|metadl/i.test(lower))
|
|
541
|
+
return c("\u25BC", FG_BRIGHT_CYAN); // ▼
|
|
542
|
+
if (/stalled/i.test(lower))
|
|
543
|
+
return c("\u25AC", FG_BRIGHT_YELLOW); // ▬
|
|
544
|
+
if (/upload|seed/i.test(lower))
|
|
545
|
+
return c("\u25B2", FG_BRIGHT_GREEN); // ▲
|
|
546
|
+
if (/pause/i.test(lower))
|
|
547
|
+
return c("\u25A0", FG_GRAY); // ■
|
|
548
|
+
if (/error|missing/i.test(lower))
|
|
549
|
+
return c("\u25CF", FG_BRIGHT_RED); // ●
|
|
550
|
+
return c("\u25CB", FG_WHITE); // ○
|
|
551
|
+
}
|