weebcli 1.0.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/.github/workflows/releases.yml +39 -0
- package/LICENSE +400 -0
- package/README-EN.md +134 -0
- package/README.md +134 -0
- package/aur/.SRCINFO +16 -0
- package/aur/PKGBUILD +43 -0
- package/eslint.config.js +49 -0
- package/jsconfig.json +9 -0
- package/package.json +45 -0
- package/src/constants.js +13 -0
- package/src/functions/episodes.js +38 -0
- package/src/functions/time.js +27 -0
- package/src/functions/variables.js +3 -0
- package/src/i18n/en.json +351 -0
- package/src/i18n/index.js +80 -0
- package/src/i18n/tr.json +348 -0
- package/src/index.js +307 -0
- package/src/jsdoc.js +72 -0
- package/src/sources/allanime.js +195 -0
- package/src/sources/animecix.js +223 -0
- package/src/sources/animely.js +100 -0
- package/src/sources/handlers/allanime.js +318 -0
- package/src/sources/handlers/animecix.js +316 -0
- package/src/sources/handlers/animely.js +338 -0
- package/src/sources/handlers/base.js +391 -0
- package/src/sources/handlers/index.js +4 -0
- package/src/sources/index.js +80 -0
- package/src/utils/anilist.js +193 -0
- package/src/utils/data_manager.js +27 -0
- package/src/utils/discord.js +86 -0
- package/src/utils/download/concurrency.js +27 -0
- package/src/utils/download/download.js +485 -0
- package/src/utils/download/progress.js +84 -0
- package/src/utils/players/mpv.js +251 -0
- package/src/utils/players/vlc.js +120 -0
- package/src/utils/process_queue.js +121 -0
- package/src/utils/resume_watch.js +137 -0
- package/src/utils/search.js +39 -0
- package/src/utils/search_download.js +21 -0
- package/src/utils/speedtest.js +30 -0
- package/src/utils/spinner.js +7 -0
- package/src/utils/storage/cache.js +42 -0
- package/src/utils/storage/config.js +71 -0
- package/src/utils/storage/history.js +69 -0
- package/src/utils/storage/queue.js +43 -0
- package/src/utils/storage/watch_progress.js +104 -0
- package/src/utils/system.js +176 -0
- package/src/utils/ui/box.js +140 -0
- package/src/utils/ui/settings_ui.js +322 -0
- package/src/utils/ui/show_history.js +92 -0
- package/src/utils/ui/stats.js +67 -0
- package/start.js +21 -0
- package/tanitim-en.md +66 -0
- package/tanitim-tr.md +66 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { setSpeed } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
export async function runSpeedTest() {
|
|
5
|
+
try {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const response = await axios.get("https://speed.cloudflare.com/__down?bytes=1000000", {
|
|
8
|
+
responseType: "arraybuffer",
|
|
9
|
+
timeout: 5000
|
|
10
|
+
});
|
|
11
|
+
const end = Date.now();
|
|
12
|
+
|
|
13
|
+
const durationSeconds = (end - start) / 1000;
|
|
14
|
+
const sizeBits = response.data.length * 8;
|
|
15
|
+
const speedBps = sizeBits / durationSeconds;
|
|
16
|
+
const speedMbps = speedBps / (1024 * 1024);
|
|
17
|
+
|
|
18
|
+
const roundedSpeed = Math.round(speedMbps);
|
|
19
|
+
|
|
20
|
+
if (roundedSpeed > 0 && isFinite(roundedSpeed)) {
|
|
21
|
+
setSpeed(roundedSpeed);
|
|
22
|
+
return roundedSpeed;
|
|
23
|
+
} else {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
const homeDir = os.homedir();
|
|
6
|
+
const configDir = path.join(homeDir, ".animely");
|
|
7
|
+
const cachePath = path.join(configDir, "anime_cache.json");
|
|
8
|
+
const CACHE_DURATION_MS = 30 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export function getCachedAnimeList() {
|
|
11
|
+
if (!fs.existsSync(cachePath)) return null;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(cachePath, "utf-8");
|
|
15
|
+
const cache = JSON.parse(raw);
|
|
16
|
+
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (now - cache.timestamp > CACHE_DURATION_MS) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return cache.data;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveAnimeListToCache(data) {
|
|
29
|
+
if (!fs.existsSync(configDir)) {
|
|
30
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cache = {
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
data: data
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
|
|
40
|
+
} catch (e) {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const homeDir = os.homedir();
|
|
7
|
+
const configDir = path.join(homeDir, ".animely");
|
|
8
|
+
const configPath = path.join(configDir, "config.json");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(configDir)) {
|
|
11
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultConfig = {
|
|
15
|
+
maxConcurrent: 3,
|
|
16
|
+
downloadDir: path.join(process.cwd(), "animely-downloads"),
|
|
17
|
+
defaultPlayer: "", // "vlc" | "mpv"
|
|
18
|
+
defaultSource: "animely", // "animely" | "animecix" | "allanime"
|
|
19
|
+
retryCount: 3,
|
|
20
|
+
retryDelay: 3000,
|
|
21
|
+
useAria2: false,
|
|
22
|
+
aria2Connections: 16,
|
|
23
|
+
useYtDlp: false,
|
|
24
|
+
ytDlpConnections: 16,
|
|
25
|
+
retryEnabled: true,
|
|
26
|
+
showAnimeDetails: true,
|
|
27
|
+
language: "" // "tr" | "en"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} Config
|
|
32
|
+
* @property {number} maxConcurrent
|
|
33
|
+
* @property {string} downloadDir
|
|
34
|
+
* @property {string} defaultPlayer
|
|
35
|
+
* @property {string} defaultSource
|
|
36
|
+
* @property {number} retryCount
|
|
37
|
+
* @property {number} retryDelay
|
|
38
|
+
* @property {boolean} useAria2
|
|
39
|
+
* @property {number} aria2Connections
|
|
40
|
+
* @property {boolean} useYtDlp
|
|
41
|
+
* @property {number} ytDlpConnections
|
|
42
|
+
* @property {boolean} retryEnabled
|
|
43
|
+
* @property {string} [anilistToken]
|
|
44
|
+
* @property {string} [anilistUsername]
|
|
45
|
+
* @property {boolean} [showAnimeDetails]
|
|
46
|
+
* @property {string} [language]
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {Config}
|
|
51
|
+
*/
|
|
52
|
+
export function getConfig() {
|
|
53
|
+
if (!fs.existsSync(configPath)) {
|
|
54
|
+
saveConfig(defaultConfig);
|
|
55
|
+
return defaultConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const fileContent = fs.readFileSync(configPath, "utf-8");
|
|
60
|
+
return { ...defaultConfig, ...JSON.parse(fileContent) };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return defaultConfig;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Config} config
|
|
68
|
+
*/
|
|
69
|
+
export function saveConfig(config) {
|
|
70
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const homeDir = os.homedir();
|
|
7
|
+
const historyDir = path.join(homeDir, ".animely");
|
|
8
|
+
const historyPath = path.join(historyDir, "history.json");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(historyDir)) {
|
|
11
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} HistoryItem
|
|
16
|
+
* @property {string} name
|
|
17
|
+
* @property {number} lastEpisode
|
|
18
|
+
* @property {number} totalEpisodes
|
|
19
|
+
* @property {boolean} completed
|
|
20
|
+
* @property {number} [anilistId]
|
|
21
|
+
* @property {string} lastWatchedAt
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object.<string, HistoryItem>} History
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {History}
|
|
30
|
+
*/
|
|
31
|
+
export function loadHistory() {
|
|
32
|
+
if (!fs.existsSync(historyPath)) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {History} history
|
|
44
|
+
*/
|
|
45
|
+
export function saveHistory(history) {
|
|
46
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} animeName
|
|
51
|
+
* @param {number} episodeNumber
|
|
52
|
+
* @param {number} totalEpisodes
|
|
53
|
+
* @param {number} [anilistId]
|
|
54
|
+
*/
|
|
55
|
+
export function updateHistory(animeName, episodeNumber, totalEpisodes, anilistId) {
|
|
56
|
+
const history = loadHistory();
|
|
57
|
+
const isCompleted = episodeNumber >= totalEpisodes;
|
|
58
|
+
|
|
59
|
+
history[animeName] = {
|
|
60
|
+
name: animeName,
|
|
61
|
+
lastEpisode: episodeNumber,
|
|
62
|
+
totalEpisodes: totalEpisodes,
|
|
63
|
+
completed: isCompleted,
|
|
64
|
+
anilistId: anilistId || (history[animeName] ? history[animeName].anilistId : undefined),
|
|
65
|
+
lastWatchedAt: new Date().toISOString()
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
saveHistory(history);
|
|
69
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const homeDir = os.homedir();
|
|
7
|
+
const configDir = path.join(homeDir, ".animely");
|
|
8
|
+
const queuePath = path.join(configDir, "queue.json");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(configDir)) {
|
|
11
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} QueueItem
|
|
16
|
+
* @property {string} animeName
|
|
17
|
+
* @property {import("../../jsdoc.js").DownloadEpisode} episode
|
|
18
|
+
* @property {string} dirPath
|
|
19
|
+
* @property {string} safeAnimeName
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns {QueueItem[]}
|
|
24
|
+
*/
|
|
25
|
+
export function loadQueue() {
|
|
26
|
+
if (!fs.existsSync(queuePath)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const fileContent = fs.readFileSync(queuePath, "utf-8");
|
|
32
|
+
return JSON.parse(fileContent);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {QueueItem[]} queue
|
|
40
|
+
*/
|
|
41
|
+
export function saveQueue(queue) {
|
|
42
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), "utf-8");
|
|
43
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const homeDir = os.homedir();
|
|
7
|
+
const configDir = path.join(homeDir, ".animely");
|
|
8
|
+
const progressPath = path.join(configDir, "watch_progress.json");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(configDir)) {
|
|
11
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} WatchProgressItem
|
|
16
|
+
* @property {string} animeName
|
|
17
|
+
* @property {number|string} episode
|
|
18
|
+
* @property {number} position
|
|
19
|
+
* @property {number} duration
|
|
20
|
+
* @property {string} updatedAt
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object.<string, WatchProgressItem>} WatchProgress
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} animeName
|
|
29
|
+
* @param {number|string} episode
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function getKey(animeName, episode) {
|
|
33
|
+
return `${animeName}::${episode}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @returns {WatchProgress}
|
|
38
|
+
*/
|
|
39
|
+
export function loadWatchProgress() {
|
|
40
|
+
if (!fs.existsSync(progressPath)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(progressPath, "utf-8"));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {WatchProgress} progress
|
|
52
|
+
*/
|
|
53
|
+
export function saveWatchProgress(progress) {
|
|
54
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} animeName
|
|
59
|
+
* @param {number|string} episode
|
|
60
|
+
* @param {number} position
|
|
61
|
+
* @param {number} duration
|
|
62
|
+
*/
|
|
63
|
+
export function updateWatchPosition(animeName, episode, position, duration) {
|
|
64
|
+
const progress = loadWatchProgress();
|
|
65
|
+
const key = getKey(animeName, episode);
|
|
66
|
+
|
|
67
|
+
if (duration > 0 && position / duration > 0.9) {
|
|
68
|
+
delete progress[key];
|
|
69
|
+
saveWatchProgress(progress);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
progress[key] = {
|
|
74
|
+
animeName,
|
|
75
|
+
episode,
|
|
76
|
+
position,
|
|
77
|
+
duration,
|
|
78
|
+
updatedAt: new Date().toISOString()
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
saveWatchProgress(progress);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} animeName
|
|
86
|
+
* @param {number|string} episode
|
|
87
|
+
* @returns {WatchProgressItem|null}
|
|
88
|
+
*/
|
|
89
|
+
export function getWatchPosition(animeName, episode) {
|
|
90
|
+
const progress = loadWatchProgress();
|
|
91
|
+
const key = getKey(animeName, episode);
|
|
92
|
+
return progress[key] || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} animeName
|
|
97
|
+
* @param {number|string} episode
|
|
98
|
+
*/
|
|
99
|
+
export function clearWatchPosition(animeName, episode) {
|
|
100
|
+
const progress = loadWatchProgress();
|
|
101
|
+
const key = getKey(animeName, episode);
|
|
102
|
+
delete progress[key];
|
|
103
|
+
saveWatchProgress(progress);
|
|
104
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawnSync, execSync } from "child_process";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @returns {"windows" | "linux" | "macos" | "unknown"}
|
|
9
|
+
*/
|
|
10
|
+
export function getOS() {
|
|
11
|
+
const platform = os.platform();
|
|
12
|
+
if (platform === "win32") return "windows";
|
|
13
|
+
if (platform === "darwin") return "macos";
|
|
14
|
+
if (platform === "linux") return "linux";
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @type {Object.<string, {cmd: string, win: string, mac: string, linux: string}>}
|
|
20
|
+
*/
|
|
21
|
+
const PACKAGES = {
|
|
22
|
+
aria2: {
|
|
23
|
+
cmd: "aria2c",
|
|
24
|
+
win: "aria2.aria2",
|
|
25
|
+
mac: "aria2",
|
|
26
|
+
linux: "aria2"
|
|
27
|
+
},
|
|
28
|
+
vlc: {
|
|
29
|
+
cmd: "vlc",
|
|
30
|
+
win: "VideoLAN.VLC",
|
|
31
|
+
mac: "--cask vlc",
|
|
32
|
+
linux: "vlc"
|
|
33
|
+
},
|
|
34
|
+
mpv: {
|
|
35
|
+
cmd: "mpv",
|
|
36
|
+
win: "mpv.mpv",
|
|
37
|
+
mac: "mpv",
|
|
38
|
+
linux: "mpv"
|
|
39
|
+
},
|
|
40
|
+
ffmpeg: {
|
|
41
|
+
cmd: "ffmpeg",
|
|
42
|
+
win: "Gyan.FFmpeg",
|
|
43
|
+
mac: "ffmpeg",
|
|
44
|
+
linux: "ffmpeg"
|
|
45
|
+
},
|
|
46
|
+
"yt-dlp": {
|
|
47
|
+
cmd: "yt-dlp",
|
|
48
|
+
win: "yt-dlp.yt-dlp",
|
|
49
|
+
mac: "yt-dlp",
|
|
50
|
+
linux: "yt-dlp"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} program
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function commandExists(program) {
|
|
59
|
+
const osType = getOS();
|
|
60
|
+
const pkg = PACKAGES[program] || { cmd: program };
|
|
61
|
+
const command = pkg.cmd;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (osType === "windows") {
|
|
65
|
+
try {
|
|
66
|
+
execSync(`where ${command}`, { stdio: "ignore" });
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
const paths = [
|
|
70
|
+
`C:\\Program Files\\VideoLAN\\VLC\\vlc.exe`,
|
|
71
|
+
`C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe`,
|
|
72
|
+
`C:\\Program Files\\MPV\\mpv.exe`,
|
|
73
|
+
`C:\\ProgramData\\chocolatey\\bin\\mpv.exe`,
|
|
74
|
+
`${process.env.USERPROFILE}\\scoop\\apps\\mpv\\current\\mpv.exe`
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
if (program === 'vlc') {
|
|
78
|
+
if (require('fs').existsSync('C:\\Program Files\\VideoLAN\\VLC\\vlc.exe')) return true;
|
|
79
|
+
if (require('fs').existsSync('C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe')) return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (program === 'mpv') {
|
|
83
|
+
if (require('fs').existsSync('C:\\Program Files\\MPV\\mpv.exe')) return true;
|
|
84
|
+
if (require('fs').existsSync('C:\\ProgramData\\chocolatey\\bin\\mpv.exe')) return true;
|
|
85
|
+
if (require('fs').existsSync(`${process.env.USERPROFILE}\\scoop\\apps\\mpv\\current\\mpv.exe`)) return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (program === 'aria2') {
|
|
89
|
+
if (require('fs').existsSync('C:\\ProgramData\\chocolatey\\bin\\aria2c.exe')) return true;
|
|
90
|
+
if (require('fs').existsSync(`${process.env.USERPROFILE}\\scoop\\apps\\aria2\\current\\aria2c.exe`)) return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (program === 'yt-dlp') {
|
|
94
|
+
if (require('fs').existsSync('C:\\ProgramData\\chocolatey\\bin\\yt-dlp.exe')) return true;
|
|
95
|
+
if (require('fs').existsSync(`${process.env.USERPROFILE}\\scoop\\apps\\yt-dlp\\current\\yt-dlp.exe`)) return true;
|
|
96
|
+
if (require('fs').existsSync(`${process.env.LOCALAPPDATA}\\Microsoft\\WinGet\\Packages\\yt-dlp.yt-dlp_Microsoft.Winget.Source_8wekyb3d8bbwe\\yt-dlp.exe`)) return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
execSync(`which ${command}`, { stdio: "ignore" });
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} program
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
export function installPackage(program) {
|
|
115
|
+
const osType = getOS();
|
|
116
|
+
const pkg = PACKAGES[program];
|
|
117
|
+
|
|
118
|
+
if (!pkg) {
|
|
119
|
+
console.log(chalk.red(t("errors.packageNotFound", { name: program })));
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
if (osType === "windows") {
|
|
125
|
+
if (!checkCommand("winget")) {
|
|
126
|
+
console.log(chalk.red(t("errors.wingetNotFound")));
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
spawnSync("winget", ["install", "-e", "--id", pkg.win], { stdio: "inherit" });
|
|
130
|
+
return true;
|
|
131
|
+
|
|
132
|
+
} else if (osType === "macos") {
|
|
133
|
+
if (!checkCommand("brew")) {
|
|
134
|
+
console.log(chalk.red(t("errors.brewNotFound")));
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const args = ["install", ...pkg.mac.split(" ")];
|
|
138
|
+
spawnSync("brew", args, { stdio: "inherit" });
|
|
139
|
+
return true;
|
|
140
|
+
|
|
141
|
+
} else if (osType === "linux") {
|
|
142
|
+
if (checkCommand("apt")) {
|
|
143
|
+
spawnSync("sudo", ["apt", "update"], { stdio: "inherit" });
|
|
144
|
+
spawnSync("sudo", ["apt", "install", "-y", pkg.linux], { stdio: "inherit" });
|
|
145
|
+
return true;
|
|
146
|
+
} else if (checkCommand("pacman")) {
|
|
147
|
+
spawnSync("sudo", ["pacman", "-S", "--noconfirm", pkg.linux], { stdio: "inherit" });
|
|
148
|
+
return true;
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.red(t("errors.packageManagerNotFound")));
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(chalk.red(t("errors.installError", { message: error.message })));
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} cmd
|
|
163
|
+
*/
|
|
164
|
+
function checkCommand(cmd) {
|
|
165
|
+
const osType = getOS();
|
|
166
|
+
try {
|
|
167
|
+
if (osType === "windows") {
|
|
168
|
+
execSync(`where ${cmd}`, { stdio: "ignore" });
|
|
169
|
+
} else {
|
|
170
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function getTerminalWidth() {
|
|
9
|
+
return process.stdout.columns || 80;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} title
|
|
14
|
+
* @param {object} [options]
|
|
15
|
+
* @param {"cyan"|"green"|"yellow"|"red"|"magenta"|"blue"} [options.color="cyan"]
|
|
16
|
+
*/
|
|
17
|
+
export function boxTitle(title, options = {}) {
|
|
18
|
+
const { color = "cyan" } = options;
|
|
19
|
+
|
|
20
|
+
const bgColors = {
|
|
21
|
+
cyan: chalk.bgCyan.black,
|
|
22
|
+
green: chalk.bgGreen.black,
|
|
23
|
+
yellow: chalk.bgYellow.black,
|
|
24
|
+
red: chalk.bgRed.white,
|
|
25
|
+
magenta: chalk.bgMagenta.black,
|
|
26
|
+
blue: chalk.bgBlue.white
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const bgFn = bgColors[color] || bgColors.cyan;
|
|
30
|
+
console.log(bgFn(` ${title} `));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string[]} lines
|
|
35
|
+
*/
|
|
36
|
+
export function boxInfo(lines) {
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
console.log(` ${line}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} title
|
|
44
|
+
* @param {string[]} content
|
|
45
|
+
* @param {object} [options]
|
|
46
|
+
* @param {"cyan"|"green"|"yellow"|"red"|"magenta"|"blue"} [options.titleColor="cyan"]
|
|
47
|
+
*/
|
|
48
|
+
export function boxWithTitle(title, content, options = {}) {
|
|
49
|
+
const { titleColor = "cyan" } = options;
|
|
50
|
+
|
|
51
|
+
const bgColors = {
|
|
52
|
+
cyan: chalk.bgCyan.black,
|
|
53
|
+
green: chalk.bgGreen.black,
|
|
54
|
+
yellow: chalk.bgYellow.black,
|
|
55
|
+
red: chalk.bgRed.white,
|
|
56
|
+
magenta: chalk.bgMagenta.black,
|
|
57
|
+
blue: chalk.bgBlue.white,
|
|
58
|
+
gray: chalk.bgGray.white
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const bgFn = bgColors[titleColor] || bgColors.cyan;
|
|
62
|
+
console.log(bgFn(` ${title} `));
|
|
63
|
+
|
|
64
|
+
for (const line of content) {
|
|
65
|
+
console.log(` ${line}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {number} [width]
|
|
71
|
+
* @param {"gray"|"cyan"|"yellow"} [color="gray"]
|
|
72
|
+
*/
|
|
73
|
+
export function divider(width, color = "gray") {
|
|
74
|
+
const w = width || Math.min(50, getTerminalWidth() - 4);
|
|
75
|
+
const colorFn = chalk[color] || chalk.gray;
|
|
76
|
+
console.log(colorFn("─".repeat(w)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} title
|
|
81
|
+
* @param {string} [subtitle]
|
|
82
|
+
* @param {"cyan"|"green"|"yellow"|"magenta"} [color="cyan"]
|
|
83
|
+
*/
|
|
84
|
+
export function banner(title, subtitle, color = "cyan") {
|
|
85
|
+
const bgColors = {
|
|
86
|
+
cyan: chalk.bgCyan.black,
|
|
87
|
+
green: chalk.bgGreen.black,
|
|
88
|
+
yellow: chalk.bgYellow.black,
|
|
89
|
+
magenta: chalk.bgMagenta.black
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const bgFn = bgColors[color] || bgColors.cyan;
|
|
93
|
+
const colorFn = chalk[color] || chalk.cyan;
|
|
94
|
+
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log(bgFn(` ${title} `));
|
|
97
|
+
if (subtitle) {
|
|
98
|
+
console.log(colorFn(subtitle));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} animeName
|
|
104
|
+
* @param {number} currentEp
|
|
105
|
+
* @param {number} totalEp
|
|
106
|
+
* @param {string} [resolution]
|
|
107
|
+
*/
|
|
108
|
+
export function menuHeader(animeName, currentEp, totalEp, resolution) {
|
|
109
|
+
const resText = resolution ? ` (${resolution})` : "";
|
|
110
|
+
const epText = currentEp > 0 ? ` | ${currentEp}/${totalEp}` : ` | ${t("episodes.episodeCount", { count: totalEp })}`;
|
|
111
|
+
console.log(chalk.bgYellow.black(` ${t("ui.nowPlaying")} `) + ` ${chalk.bold(animeName)}${chalk.gray(resText)}${chalk.yellow(epText)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} message
|
|
116
|
+
*/
|
|
117
|
+
export function errorBox(message) {
|
|
118
|
+
console.log(chalk.red(`✗ ${message}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} message
|
|
123
|
+
*/
|
|
124
|
+
export function successBox(message) {
|
|
125
|
+
console.log(chalk.green(`✓ ${message}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} message
|
|
130
|
+
*/
|
|
131
|
+
export function infoBox(message) {
|
|
132
|
+
console.log(chalk.cyan(`● ${message}`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} message
|
|
137
|
+
*/
|
|
138
|
+
export function warnBox(message) {
|
|
139
|
+
console.log(chalk.yellow(`! ${message}`));
|
|
140
|
+
}
|