movizone 1.0.4 → 1.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/index.ts +301 -99
- package/package.json +6 -5
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import boxen from "boxen";
|
|
|
6
6
|
import Table from "cli-table3";
|
|
7
7
|
import figlet from "figlet";
|
|
8
8
|
import gradient from "gradient-string";
|
|
9
|
+
import terminalImage from "terminal-image";
|
|
9
10
|
import { homedir } from "os";
|
|
10
11
|
import { join, dirname } from "path";
|
|
11
12
|
import { createInterface } from "readline";
|
|
@@ -143,15 +144,28 @@ async function getMovieSuggestions(movieId: number): Promise<ListResponse> {
|
|
|
143
144
|
return apiGet("movie_suggestions.json", { movie_id: movieId });
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function buildMagnet(hash: string, title: string): string {
|
|
147
|
+
export function buildMagnet(hash: string, title: string): string {
|
|
147
148
|
const dn = encodeURIComponent(title);
|
|
148
149
|
const trackers = TRACKERS.map((t) => `&tr=${encodeURIComponent(t)}`).join("");
|
|
149
150
|
return `magnet:?xt=urn:btih:${hash}&dn=${dn}${trackers}`;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// --- Poster Image ---
|
|
154
|
+
|
|
155
|
+
async function fetchPosterImage(url: string): Promise<string | null> {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(url);
|
|
158
|
+
if (!res.ok) return null;
|
|
159
|
+
const buffer = new Uint8Array(await res.arrayBuffer());
|
|
160
|
+
return await terminalImage.buffer(buffer, { width: 30 });
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
152
166
|
// --- Fuzzy Search ---
|
|
153
167
|
|
|
154
|
-
function levenshtein(a: string, b: string): number {
|
|
168
|
+
export function levenshtein(a: string, b: string): number {
|
|
155
169
|
const m = a.length, n = b.length;
|
|
156
170
|
const dp: number[][] = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0));
|
|
157
171
|
for (let i = 0; i <= m; i++) dp[i]![0] = i;
|
|
@@ -166,7 +180,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
166
180
|
return dp[m]![n]!;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
function fuzzyScore(query: string, title: string): number {
|
|
183
|
+
export function fuzzyScore(query: string, title: string): number {
|
|
170
184
|
const q = query.toLowerCase().trim();
|
|
171
185
|
const t = title.toLowerCase().trim();
|
|
172
186
|
|
|
@@ -197,12 +211,12 @@ function fuzzyScore(query: string, title: string): number {
|
|
|
197
211
|
return (wordMatches / nonYearWords.length) * 80;
|
|
198
212
|
}
|
|
199
213
|
|
|
200
|
-
function extractYear(query: string): number | null {
|
|
214
|
+
export function extractYear(query: string): number | null {
|
|
201
215
|
const match = query.match(/\b(19|20)\d{2}\b/);
|
|
202
216
|
return match ? parseInt(match[0]) : null;
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
function generateTypoCorrections(word: string): string[] {
|
|
219
|
+
export function generateTypoCorrections(word: string): string[] {
|
|
206
220
|
const corrections: string[] = [];
|
|
207
221
|
const w = word.toLowerCase();
|
|
208
222
|
|
|
@@ -330,7 +344,7 @@ async function smartSearch(query: string): Promise<Movie[]> {
|
|
|
330
344
|
|
|
331
345
|
// --- Download with WebTorrent (via Node.js subprocess) ---
|
|
332
346
|
|
|
333
|
-
function formatBytes(bytes: number): string {
|
|
347
|
+
export function formatBytes(bytes: number): string {
|
|
334
348
|
if (bytes === 0) return "0 B";
|
|
335
349
|
const k = 1024;
|
|
336
350
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
@@ -338,11 +352,11 @@ function formatBytes(bytes: number): string {
|
|
|
338
352
|
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
|
|
339
353
|
}
|
|
340
354
|
|
|
341
|
-
function formatSpeed(bytesPerSec: number): string {
|
|
355
|
+
export function formatSpeed(bytesPerSec: number): string {
|
|
342
356
|
return `${formatBytes(bytesPerSec)}/s`;
|
|
343
357
|
}
|
|
344
358
|
|
|
345
|
-
function formatEta(ms: number): string {
|
|
359
|
+
export function formatEta(ms: number): string {
|
|
346
360
|
const seconds = ms / 1000;
|
|
347
361
|
if (!seconds || !isFinite(seconds)) return "--:--";
|
|
348
362
|
const h = Math.floor(seconds / 3600);
|
|
@@ -352,75 +366,67 @@ function formatEta(ms: number): string {
|
|
|
352
366
|
return `${m}m ${s}s`;
|
|
353
367
|
}
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
357
|
-
const helperPath = join(scriptDir, "download.mjs");
|
|
369
|
+
// --- Download Manager ---
|
|
358
370
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}));
|
|
371
|
+
interface DownloadState {
|
|
372
|
+
id: string;
|
|
373
|
+
movieTitle: string;
|
|
374
|
+
quality: string;
|
|
375
|
+
status: "connecting" | "downloading" | "done" | "error" | "timeout";
|
|
376
|
+
progress: number;
|
|
377
|
+
downloaded: number;
|
|
378
|
+
total: number;
|
|
379
|
+
speed: number;
|
|
380
|
+
eta: number;
|
|
381
|
+
peers: number;
|
|
382
|
+
filePath?: string;
|
|
383
|
+
error?: string;
|
|
384
|
+
}
|
|
374
385
|
|
|
375
|
-
|
|
386
|
+
export class DownloadManager {
|
|
387
|
+
private downloads = new Map<string, DownloadState>();
|
|
388
|
+
private processes = new Map<string, ReturnType<typeof Bun.spawn>>();
|
|
389
|
+
private idCounter = 0;
|
|
390
|
+
|
|
391
|
+
startDownload(magnet: string, movieTitle: string, torrentInfo?: Torrent): string {
|
|
392
|
+
const id = String(++this.idCounter);
|
|
393
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
394
|
+
const helperPath = join(scriptDir, "download.mjs");
|
|
395
|
+
|
|
396
|
+
const state: DownloadState = {
|
|
397
|
+
id,
|
|
398
|
+
movieTitle,
|
|
399
|
+
quality: torrentInfo?.quality || "unknown",
|
|
400
|
+
status: "connecting",
|
|
401
|
+
progress: 0,
|
|
402
|
+
downloaded: 0,
|
|
403
|
+
total: torrentInfo?.size_bytes || 0,
|
|
404
|
+
speed: 0,
|
|
405
|
+
eta: 0,
|
|
406
|
+
peers: 0,
|
|
407
|
+
};
|
|
408
|
+
this.downloads.set(id, state);
|
|
376
409
|
|
|
377
|
-
return new Promise<void>((resolve) => {
|
|
378
410
|
const child = Bun.spawn(["node", helperPath, magnet, DOWNLOAD_DIR], {
|
|
379
411
|
stdout: "pipe",
|
|
380
412
|
stderr: "inherit",
|
|
381
413
|
});
|
|
414
|
+
this.processes.set(id, child);
|
|
415
|
+
|
|
416
|
+
// Start reading output in background (no await)
|
|
417
|
+
this.readOutput(id, child);
|
|
382
418
|
|
|
383
|
-
|
|
419
|
+
return id;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async readOutput(id: string, child: ReturnType<typeof Bun.spawn>): Promise<void> {
|
|
423
|
+
const stdout = child.stdout as ReadableStream<Uint8Array>;
|
|
424
|
+
const reader = stdout.getReader();
|
|
384
425
|
const decoder = new TextDecoder();
|
|
385
426
|
let buffer = "";
|
|
386
|
-
|
|
387
|
-
let progressLines = 0;
|
|
388
|
-
|
|
389
|
-
function renderProgress(percent: string, downloaded: string, speed: string, eta: string, peers: number, progress: number) {
|
|
390
|
-
const barWidth = 50;
|
|
391
|
-
const filled = Math.round(progress * barWidth);
|
|
392
|
-
const bar = chalk.green("█".repeat(filled)) + chalk.dim("░".repeat(barWidth - filled));
|
|
393
|
-
|
|
394
|
-
const statsTable = new Table({
|
|
395
|
-
style: { head: [], border: ["gray"], compact: true },
|
|
396
|
-
colWidths: [16, 16, 14, 12],
|
|
397
|
-
});
|
|
398
|
-
statsTable.push([
|
|
399
|
-
`${chalk.dim("Downloaded")} ${chalk.white(downloaded + "/" + totalSize)}`,
|
|
400
|
-
`${chalk.dim("Speed")} ${chalk.cyan(speed)}`,
|
|
401
|
-
`${chalk.dim("ETA")} ${chalk.white(eta)}`,
|
|
402
|
-
`${chalk.dim("Peers")} ${chalk.white(String(peers))}`,
|
|
403
|
-
]);
|
|
404
|
-
|
|
405
|
-
const content = ` ${bar} ${chalk.bold(percent + "%")}\n${statsTable.toString()}`;
|
|
406
|
-
const frame = boxen(content, {
|
|
407
|
-
title: chalk.bold(" Progress "),
|
|
408
|
-
titleAlignment: "left",
|
|
409
|
-
borderStyle: "round",
|
|
410
|
-
borderColor: "cyan",
|
|
411
|
-
dimBorder: true,
|
|
412
|
-
padding: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Clear previous progress frame
|
|
416
|
-
if (progressLines > 0) {
|
|
417
|
-
process.stdout.write(`\x1b[${progressLines}A\x1b[0J`);
|
|
418
|
-
}
|
|
419
|
-
process.stdout.write(frame + "\n");
|
|
420
|
-
progressLines = frame.split("\n").length;
|
|
421
|
-
}
|
|
427
|
+
const state = this.downloads.get(id)!;
|
|
422
428
|
|
|
423
|
-
|
|
429
|
+
try {
|
|
424
430
|
while (true) {
|
|
425
431
|
const { done, value } = await reader.read();
|
|
426
432
|
if (done) break;
|
|
@@ -435,46 +441,98 @@ async function downloadTorrent(magnet: string, movieTitle: string, torrentInfo?:
|
|
|
435
441
|
const msg = JSON.parse(line);
|
|
436
442
|
|
|
437
443
|
if (msg.type === "meta") {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(chalk.dim(` Size: ${totalSize}\n`));
|
|
444
|
+
state.total = msg.size;
|
|
445
|
+
state.status = "downloading";
|
|
441
446
|
} else if (msg.type === "progress") {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
+
state.status = "downloading";
|
|
448
|
+
state.progress = msg.progress;
|
|
449
|
+
state.downloaded = msg.downloaded;
|
|
450
|
+
state.total = msg.total;
|
|
451
|
+
state.speed = msg.speed;
|
|
452
|
+
state.eta = msg.eta;
|
|
453
|
+
state.peers = msg.peers;
|
|
447
454
|
} else if (msg.type === "done") {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log(boxen(
|
|
452
|
-
chalk.green.bold(" Download complete!") + "\n" + chalk.dim(` Saved to: ${msg.path}`),
|
|
453
|
-
{
|
|
454
|
-
borderStyle: "round",
|
|
455
|
-
borderColor: "green",
|
|
456
|
-
padding: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
457
|
-
}
|
|
458
|
-
));
|
|
455
|
+
state.status = "done";
|
|
456
|
+
state.progress = 1;
|
|
457
|
+
state.filePath = msg.path;
|
|
459
458
|
} else if (msg.type === "error") {
|
|
460
|
-
|
|
459
|
+
state.status = "error";
|
|
460
|
+
state.error = msg.message;
|
|
461
461
|
} else if (msg.type === "timeout") {
|
|
462
|
-
|
|
462
|
+
state.status = "timeout";
|
|
463
|
+
state.error = "Could not connect to peers";
|
|
463
464
|
}
|
|
464
465
|
} catch {}
|
|
465
466
|
}
|
|
466
467
|
}
|
|
468
|
+
} catch {}
|
|
469
|
+
|
|
470
|
+
// Ensure terminal state if process ended without explicit done/error
|
|
471
|
+
await child.exited;
|
|
472
|
+
if (state.status === "connecting" || state.status === "downloading") {
|
|
473
|
+
state.status = "error";
|
|
474
|
+
state.error = "Process ended unexpectedly";
|
|
467
475
|
}
|
|
476
|
+
this.processes.delete(id);
|
|
477
|
+
}
|
|
468
478
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
479
|
+
getDownloads(): DownloadState[] {
|
|
480
|
+
return [...this.downloads.values()];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
getActive(): DownloadState[] {
|
|
484
|
+
return this.getDownloads().filter((d) => d.status === "connecting" || d.status === "downloading");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
cancelDownload(id: string): void {
|
|
488
|
+
const child = this.processes.get(id);
|
|
489
|
+
if (child) {
|
|
490
|
+
child.kill();
|
|
491
|
+
this.processes.delete(id);
|
|
492
|
+
}
|
|
493
|
+
const state = this.downloads.get(id);
|
|
494
|
+
if (state && (state.status === "connecting" || state.status === "downloading")) {
|
|
495
|
+
state.status = "error";
|
|
496
|
+
state.error = "Cancelled";
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
clearCompleted(): void {
|
|
501
|
+
for (const [id, state] of this.downloads) {
|
|
502
|
+
if (state.status === "done" || state.status === "error" || state.status === "timeout") {
|
|
503
|
+
this.downloads.delete(id);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const downloadManager = new DownloadManager();
|
|
510
|
+
|
|
511
|
+
async function downloadTorrent(magnet: string, movieTitle: string, torrentInfo?: Torrent): Promise<void> {
|
|
512
|
+
// Movie info box
|
|
513
|
+
const infoLines = [
|
|
514
|
+
`${chalk.bold("Title:")} ${chalk.white(movieTitle)}`,
|
|
515
|
+
torrentInfo ? `${chalk.bold("Quality:")} ${chalk.cyan(torrentInfo.quality)} ${chalk.dim(torrentInfo.type)}` : "",
|
|
516
|
+
torrentInfo ? `${chalk.bold("Size:")} ${torrentInfo.size}` : "",
|
|
517
|
+
torrentInfo ? `${chalk.bold("Codec:")} ${chalk.dim(`${torrentInfo.video_codec} ${torrentInfo.audio_channels}ch`)}` : "",
|
|
518
|
+
`${chalk.bold("Save to:")} ${chalk.dim(DOWNLOAD_DIR)}`,
|
|
519
|
+
].filter(Boolean).join("\n");
|
|
520
|
+
console.log(boxen(infoLines, {
|
|
521
|
+
title: chalk.bold(" Download "),
|
|
522
|
+
titleAlignment: "left",
|
|
523
|
+
borderStyle: "round",
|
|
524
|
+
borderColor: "green",
|
|
525
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
downloadManager.startDownload(magnet, movieTitle, torrentInfo);
|
|
529
|
+
console.log(chalk.green("\n Download started in background!"));
|
|
530
|
+
console.log(chalk.dim(" Check progress from the Downloads menu.\n"));
|
|
473
531
|
}
|
|
474
532
|
|
|
475
533
|
// --- Display Helpers ---
|
|
476
534
|
|
|
477
|
-
function formatRuntime(minutes: number): string {
|
|
535
|
+
export function formatRuntime(minutes: number): string {
|
|
478
536
|
if (!minutes) return "N/A";
|
|
479
537
|
const h = Math.floor(minutes / 60);
|
|
480
538
|
const m = minutes % 60;
|
|
@@ -525,9 +583,17 @@ function displayMovieTable(movies: Movie[]): void {
|
|
|
525
583
|
console.log(table.toString());
|
|
526
584
|
}
|
|
527
585
|
|
|
528
|
-
function displayMovieDetail(movie: Movie): void {
|
|
586
|
+
async function displayMovieDetail(movie: Movie): Promise<void> {
|
|
529
587
|
console.log();
|
|
530
588
|
|
|
589
|
+
// Poster image
|
|
590
|
+
if (movie.medium_cover_image) {
|
|
591
|
+
const poster = await fetchPosterImage(movie.medium_cover_image);
|
|
592
|
+
if (poster) {
|
|
593
|
+
console.log(poster);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
531
597
|
// Title bar
|
|
532
598
|
const titleLine = chalk.bold.white(movie.title) + chalk.dim(` (${movie.year})`) + " " + chalk.dim(movie.imdb_code);
|
|
533
599
|
console.log(boxen(titleLine, {
|
|
@@ -711,7 +777,7 @@ async function searchAction(): Promise<void> {
|
|
|
711
777
|
}
|
|
712
778
|
|
|
713
779
|
async function viewMovie(movie: Movie): Promise<void> {
|
|
714
|
-
displayMovieDetail(movie);
|
|
780
|
+
await displayMovieDetail(movie);
|
|
715
781
|
|
|
716
782
|
let viewing = true;
|
|
717
783
|
while (viewing) {
|
|
@@ -867,6 +933,131 @@ async function paginatedList(
|
|
|
867
933
|
}
|
|
868
934
|
}
|
|
869
935
|
|
|
936
|
+
// --- Downloads View ---
|
|
937
|
+
|
|
938
|
+
function downloadStatusIcon(status: DownloadState["status"]): string {
|
|
939
|
+
switch (status) {
|
|
940
|
+
case "connecting": return chalk.yellow("◌");
|
|
941
|
+
case "downloading": return chalk.cyan("▼");
|
|
942
|
+
case "done": return chalk.green("✓");
|
|
943
|
+
case "error": return chalk.red("✗");
|
|
944
|
+
case "timeout": return chalk.yellow("⏱");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function downloadProgressBar(progress: number, width = 20): string {
|
|
949
|
+
const filled = Math.round(progress * width);
|
|
950
|
+
return chalk.green("█".repeat(filled)) + chalk.dim("░".repeat(width - filled));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function viewDownloads(): Promise<void> {
|
|
954
|
+
let viewing = true;
|
|
955
|
+
|
|
956
|
+
while (viewing) {
|
|
957
|
+
const downloads = downloadManager.getDownloads();
|
|
958
|
+
|
|
959
|
+
if (!downloads.length) {
|
|
960
|
+
console.log(chalk.dim("\n No downloads yet.\n"));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
console.log();
|
|
965
|
+
|
|
966
|
+
const table = new Table({
|
|
967
|
+
head: [
|
|
968
|
+
chalk.dim("#"),
|
|
969
|
+
chalk.bold("Title"),
|
|
970
|
+
chalk.cyan("Quality"),
|
|
971
|
+
chalk.white("Progress"),
|
|
972
|
+
chalk.cyan("Speed"),
|
|
973
|
+
chalk.white("ETA"),
|
|
974
|
+
chalk.dim("Status"),
|
|
975
|
+
],
|
|
976
|
+
colWidths: [5, 28, 10, 28, 12, 10, 12],
|
|
977
|
+
style: { head: [], border: ["gray"], compact: false },
|
|
978
|
+
wordWrap: true,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
for (let i = 0; i < downloads.length; i++) {
|
|
982
|
+
const d = downloads[i]!;
|
|
983
|
+
const pct = (d.progress * 100).toFixed(1) + "%";
|
|
984
|
+
const progressCell = d.status === "downloading"
|
|
985
|
+
? `${downloadProgressBar(d.progress)} ${chalk.bold(pct)}`
|
|
986
|
+
: d.status === "done"
|
|
987
|
+
? `${downloadProgressBar(1)} ${chalk.bold("100%")}`
|
|
988
|
+
: chalk.dim("--");
|
|
989
|
+
const speedCell = d.status === "downloading" ? chalk.cyan(formatSpeed(d.speed)) : chalk.dim("--");
|
|
990
|
+
const etaCell = d.status === "downloading" ? formatEta(d.eta) : chalk.dim("--");
|
|
991
|
+
const statusCell = `${downloadStatusIcon(d.status)} ${d.status === "error" || d.status === "timeout" ? chalk.red(d.error || d.status) : d.status}`;
|
|
992
|
+
|
|
993
|
+
table.push([
|
|
994
|
+
chalk.dim(`${i + 1}`),
|
|
995
|
+
chalk.white(d.movieTitle),
|
|
996
|
+
chalk.cyan(d.quality),
|
|
997
|
+
progressCell,
|
|
998
|
+
speedCell,
|
|
999
|
+
etaCell,
|
|
1000
|
+
statusCell,
|
|
1001
|
+
]);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
console.log(boxen(table.toString(), {
|
|
1005
|
+
title: chalk.bold(" Downloads "),
|
|
1006
|
+
titleAlignment: "left",
|
|
1007
|
+
borderStyle: "round",
|
|
1008
|
+
borderColor: "cyan",
|
|
1009
|
+
dimBorder: true,
|
|
1010
|
+
padding: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
1011
|
+
}));
|
|
1012
|
+
|
|
1013
|
+
// Show file paths for completed downloads
|
|
1014
|
+
const completed = downloads.filter((d) => d.status === "done" && d.filePath);
|
|
1015
|
+
if (completed.length) {
|
|
1016
|
+
for (const d of completed) {
|
|
1017
|
+
console.log(chalk.dim(` ✓ ${d.movieTitle}: ${d.filePath}`));
|
|
1018
|
+
}
|
|
1019
|
+
console.log();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const choices: any[] = [];
|
|
1023
|
+
|
|
1024
|
+
const active = downloadManager.getActive();
|
|
1025
|
+
if (active.length) {
|
|
1026
|
+
for (const d of active) {
|
|
1027
|
+
choices.push({
|
|
1028
|
+
name: `Cancel: ${d.movieTitle} (${d.quality})`,
|
|
1029
|
+
value: `cancel_${d.id}`,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const inactive = downloads.filter((d) => d.status === "done" || d.status === "error" || d.status === "timeout");
|
|
1035
|
+
if (inactive.length) {
|
|
1036
|
+
choices.push({ name: "Clear completed/failed", value: "clear" });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
choices.push({ name: "Refresh", value: "refresh" });
|
|
1040
|
+
choices.push({ name: "Back to menu", value: "back" });
|
|
1041
|
+
|
|
1042
|
+
const { action } = await inquirer.prompt([
|
|
1043
|
+
{ type: "list", name: "action", message: "Action:", choices },
|
|
1044
|
+
]);
|
|
1045
|
+
|
|
1046
|
+
if (action === "back") {
|
|
1047
|
+
viewing = false;
|
|
1048
|
+
} else if (action === "clear") {
|
|
1049
|
+
downloadManager.clearCompleted();
|
|
1050
|
+
console.log(chalk.dim(" Cleared.\n"));
|
|
1051
|
+
} else if (action === "refresh") {
|
|
1052
|
+
// loop continues, will re-render
|
|
1053
|
+
} else if (action.startsWith("cancel_")) {
|
|
1054
|
+
const id = action.slice(7);
|
|
1055
|
+
downloadManager.cancelDownload(id);
|
|
1056
|
+
console.log(chalk.yellow(" Download cancelled.\n"));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
870
1061
|
// --- SIGINT / Exit Handling ---
|
|
871
1062
|
|
|
872
1063
|
function isExitPromptError(err: unknown): boolean {
|
|
@@ -890,6 +1081,11 @@ async function main(): Promise<void> {
|
|
|
890
1081
|
|
|
891
1082
|
while (running) {
|
|
892
1083
|
try {
|
|
1084
|
+
const activeCount = downloadManager.getActive().length;
|
|
1085
|
+
const downloadsLabel = activeCount > 0
|
|
1086
|
+
? `Downloads (${activeCount} active)`
|
|
1087
|
+
: "Downloads";
|
|
1088
|
+
|
|
893
1089
|
const { action } = await inquirer.prompt([
|
|
894
1090
|
{
|
|
895
1091
|
type: "list",
|
|
@@ -900,6 +1096,7 @@ async function main(): Promise<void> {
|
|
|
900
1096
|
{ name: "Browse movies", value: "browse" },
|
|
901
1097
|
{ name: "Trending now", value: "trending" },
|
|
902
1098
|
{ name: "Top rated", value: "top" },
|
|
1099
|
+
{ name: downloadsLabel, value: "downloads" },
|
|
903
1100
|
{ name: "Exit", value: "exit" },
|
|
904
1101
|
],
|
|
905
1102
|
},
|
|
@@ -918,6 +1115,9 @@ async function main(): Promise<void> {
|
|
|
918
1115
|
case "top":
|
|
919
1116
|
await paginatedList((p) => listMovies(p, { sort_by: "rating", order_by: "desc", minimum_rating: 7 }), "Top Rated");
|
|
920
1117
|
break;
|
|
1118
|
+
case "downloads":
|
|
1119
|
+
await viewDownloads();
|
|
1120
|
+
break;
|
|
921
1121
|
case "exit":
|
|
922
1122
|
exitGracefully();
|
|
923
1123
|
}
|
|
@@ -928,8 +1128,10 @@ async function main(): Promise<void> {
|
|
|
928
1128
|
}
|
|
929
1129
|
}
|
|
930
1130
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1131
|
+
if (import.meta.main) {
|
|
1132
|
+
main().catch((err) => {
|
|
1133
|
+
if (isExitPromptError(err)) exitGracefully();
|
|
1134
|
+
console.error(err);
|
|
1135
|
+
process.exit(1);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "movizone",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Movie torrent explorer CLI with fuzzy search and in-terminal downloads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "bun run index.ts",
|
|
16
|
+
"test": "bun test",
|
|
16
17
|
"typecheck": "bun x tsc --noEmit"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
@@ -36,15 +37,15 @@
|
|
|
36
37
|
"cli-table3": "^0.6.5",
|
|
37
38
|
"figlet": "^1.10.0",
|
|
38
39
|
"gradient-string": "^3.0.0",
|
|
39
|
-
"inquirer": "
|
|
40
|
+
"inquirer": "9",
|
|
40
41
|
"ora": "8",
|
|
41
|
-
"
|
|
42
|
-
"webtorrent": "^2.8.5"
|
|
43
|
-
"wrap-ansi": "^10.0.0"
|
|
42
|
+
"terminal-image": "^4.2.0",
|
|
43
|
+
"webtorrent": "^2.8.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/bun": "latest",
|
|
47
47
|
"@types/figlet": "^1.7.0",
|
|
48
|
+
"@types/inquirer": "^9.0.9",
|
|
48
49
|
"typescript": "^5"
|
|
49
50
|
},
|
|
50
51
|
"trustedDependencies": [
|