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.
Files changed (54) hide show
  1. package/.github/workflows/releases.yml +39 -0
  2. package/LICENSE +400 -0
  3. package/README-EN.md +134 -0
  4. package/README.md +134 -0
  5. package/aur/.SRCINFO +16 -0
  6. package/aur/PKGBUILD +43 -0
  7. package/eslint.config.js +49 -0
  8. package/jsconfig.json +9 -0
  9. package/package.json +45 -0
  10. package/src/constants.js +13 -0
  11. package/src/functions/episodes.js +38 -0
  12. package/src/functions/time.js +27 -0
  13. package/src/functions/variables.js +3 -0
  14. package/src/i18n/en.json +351 -0
  15. package/src/i18n/index.js +80 -0
  16. package/src/i18n/tr.json +348 -0
  17. package/src/index.js +307 -0
  18. package/src/jsdoc.js +72 -0
  19. package/src/sources/allanime.js +195 -0
  20. package/src/sources/animecix.js +223 -0
  21. package/src/sources/animely.js +100 -0
  22. package/src/sources/handlers/allanime.js +318 -0
  23. package/src/sources/handlers/animecix.js +316 -0
  24. package/src/sources/handlers/animely.js +338 -0
  25. package/src/sources/handlers/base.js +391 -0
  26. package/src/sources/handlers/index.js +4 -0
  27. package/src/sources/index.js +80 -0
  28. package/src/utils/anilist.js +193 -0
  29. package/src/utils/data_manager.js +27 -0
  30. package/src/utils/discord.js +86 -0
  31. package/src/utils/download/concurrency.js +27 -0
  32. package/src/utils/download/download.js +485 -0
  33. package/src/utils/download/progress.js +84 -0
  34. package/src/utils/players/mpv.js +251 -0
  35. package/src/utils/players/vlc.js +120 -0
  36. package/src/utils/process_queue.js +121 -0
  37. package/src/utils/resume_watch.js +137 -0
  38. package/src/utils/search.js +39 -0
  39. package/src/utils/search_download.js +21 -0
  40. package/src/utils/speedtest.js +30 -0
  41. package/src/utils/spinner.js +7 -0
  42. package/src/utils/storage/cache.js +42 -0
  43. package/src/utils/storage/config.js +71 -0
  44. package/src/utils/storage/history.js +69 -0
  45. package/src/utils/storage/queue.js +43 -0
  46. package/src/utils/storage/watch_progress.js +104 -0
  47. package/src/utils/system.js +176 -0
  48. package/src/utils/ui/box.js +140 -0
  49. package/src/utils/ui/settings_ui.js +322 -0
  50. package/src/utils/ui/show_history.js +92 -0
  51. package/src/utils/ui/stats.js +67 -0
  52. package/start.js +21 -0
  53. package/tanitim-en.md +66 -0
  54. 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,7 @@
1
+ // @ts-check
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { t } from "../i18n/index.js";
5
+
6
+ /** @type {import("ora").Ora} */
7
+ export const spinner = ora(chalk.gray(t("spinner.loading")));
@@ -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
+ }