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,251 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawn, execSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import net from "net";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { t } from "../../i18n/index.js";
|
|
9
|
+
|
|
10
|
+
const isWindows = os.platform() === "win32";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @returns {Promise<string|null>}
|
|
14
|
+
*/
|
|
15
|
+
export async function getMpvPath() {
|
|
16
|
+
if (isWindows) {
|
|
17
|
+
const commonPaths = [
|
|
18
|
+
"C:\\Program Files\\MPV\\mpv.exe",
|
|
19
|
+
"C:\\Program Files (x86)\\MPV\\mpv.exe",
|
|
20
|
+
"C:\\ProgramData\\chocolatey\\bin\\mpv.exe",
|
|
21
|
+
`${process.env.USERPROFILE}\\scoop\\apps\\mpv\\current\\mpv.exe`
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const p of commonPaths) {
|
|
25
|
+
if (fs.existsSync(p)) return p;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = execSync("where mpv").toString().trim().split("\n")[0];
|
|
30
|
+
if (result && fs.existsSync(result)) return result;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
try {
|
|
35
|
+
const result = execSync("which mpv").toString().trim();
|
|
36
|
+
if (result) return result;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync("/usr/bin/mpv")) return "/usr/bin/mpv";
|
|
41
|
+
if (fs.existsSync("/usr/local/bin/mpv")) return "/usr/local/bin/mpv";
|
|
42
|
+
if (fs.existsSync("/opt/homebrew/bin/mpv")) return "/opt/homebrew/bin/mpv";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @returns {Promise<boolean>}
|
|
50
|
+
*/
|
|
51
|
+
export async function installMpv() {
|
|
52
|
+
const platform = os.platform();
|
|
53
|
+
|
|
54
|
+
if (platform === "win32") {
|
|
55
|
+
console.log(chalk.cyan(t("errors.mpvNotFound", { manager: "Winget" })));
|
|
56
|
+
try {
|
|
57
|
+
execSync("winget install io.mpv.mpv -e --source winget", { stdio: "inherit" });
|
|
58
|
+
console.log(chalk.green(t("errors.mpvInstalled")));
|
|
59
|
+
return true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(chalk.red(t("errors.mpvInstallFailed")));
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
} else if (platform === "darwin") {
|
|
65
|
+
console.log(chalk.cyan(t("errors.mpvNotFound", { manager: "Homebrew" })));
|
|
66
|
+
try {
|
|
67
|
+
execSync("brew install mpv", { stdio: "inherit" });
|
|
68
|
+
console.log(chalk.green(t("errors.mpvInstalled")));
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(chalk.red(t("errors.mpvInstallFailedMac")));
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
} else if (platform === "linux") {
|
|
75
|
+
console.log(chalk.cyan(t("errors.mpvNotFound", { manager: "package manager" })));
|
|
76
|
+
|
|
77
|
+
const managers = [
|
|
78
|
+
{ cmd: "apt-get", install: "sudo apt-get update && sudo apt-get install mpv -y" },
|
|
79
|
+
{ cmd: "dnf", install: "sudo dnf install mpv -y" },
|
|
80
|
+
{ cmd: "pacman", install: "sudo pacman -S mpv --noconfirm" },
|
|
81
|
+
{ cmd: "zypper", install: "sudo zypper install mpv -y" },
|
|
82
|
+
{ cmd: "snap", install: "sudo snap install mpv" }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const mgr of managers) {
|
|
86
|
+
try {
|
|
87
|
+
execSync(`which ${mgr.cmd}`, { stdio: "ignore" });
|
|
88
|
+
console.log(chalk.yellow(t("errors.managerDetected", { manager: mgr.cmd })));
|
|
89
|
+
execSync(mgr.install, { stdio: "inherit" });
|
|
90
|
+
console.log(chalk.green(t("errors.mpvInstalled")));
|
|
91
|
+
return true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function getSocketPath() {
|
|
105
|
+
if (isWindows) {
|
|
106
|
+
return "\\\\.\\pipe\\animely-mpv-socket";
|
|
107
|
+
}
|
|
108
|
+
return path.join(os.tmpdir(), `animely-mpv-${process.pid}.sock`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} socketPath
|
|
113
|
+
* @param {string} command
|
|
114
|
+
* @returns {Promise<any>}
|
|
115
|
+
*/
|
|
116
|
+
function sendMpvCommand(socketPath, command) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const client = net.createConnection(socketPath, () => {
|
|
119
|
+
client.write(command + "\n");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
let data = "";
|
|
123
|
+
client.on("data", (chunk) => {
|
|
124
|
+
data += chunk.toString();
|
|
125
|
+
try {
|
|
126
|
+
const lines = data.split("\n").filter(l => l.trim());
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const parsed = JSON.parse(line);
|
|
129
|
+
if (parsed.data !== undefined || parsed.error) {
|
|
130
|
+
client.end();
|
|
131
|
+
resolve(parsed);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
client.on("error", reject);
|
|
141
|
+
client.setTimeout(2000, () => {
|
|
142
|
+
client.end();
|
|
143
|
+
reject(new Error("Timeout"));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} socketPath
|
|
150
|
+
* @returns {Promise<{position: number, duration: number}|null>}
|
|
151
|
+
*/
|
|
152
|
+
async function getMpvPosition(socketPath) {
|
|
153
|
+
try {
|
|
154
|
+
const posResult = await sendMpvCommand(socketPath, '{"command": ["get_property", "time-pos"]}');
|
|
155
|
+
const durResult = await sendMpvCommand(socketPath, '{"command": ["get_property", "duration"]}');
|
|
156
|
+
|
|
157
|
+
if (posResult?.data !== undefined && durResult?.data !== undefined) {
|
|
158
|
+
return {
|
|
159
|
+
position: Math.floor(posResult.data),
|
|
160
|
+
duration: Math.floor(durResult.data)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string} url
|
|
171
|
+
* @param {object} [options]
|
|
172
|
+
* @param {number} [options.startPosition]
|
|
173
|
+
* @param {(position: number, duration: number) => void} [options.onClose]
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
export async function openInMpv(url, options = {}) {
|
|
177
|
+
let mpvPath = await getMpvPath();
|
|
178
|
+
|
|
179
|
+
if (!mpvPath) {
|
|
180
|
+
const installed = await installMpv();
|
|
181
|
+
if (installed) {
|
|
182
|
+
mpvPath = await getMpvPath();
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error(t("errors.mpvNotFoundAndFailed"));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!mpvPath) throw new Error(t("errors.mpvPathNotFound"));
|
|
189
|
+
|
|
190
|
+
const socketPath = getSocketPath();
|
|
191
|
+
|
|
192
|
+
if (!isWindows && fs.existsSync(socketPath)) {
|
|
193
|
+
try {
|
|
194
|
+
fs.unlinkSync(socketPath);
|
|
195
|
+
} catch (e) {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const args = [
|
|
200
|
+
url,
|
|
201
|
+
"--force-window",
|
|
202
|
+
"--fs",
|
|
203
|
+
`--input-ipc-server=${socketPath}`
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// Başlangıç pozisyonu varsa ekle
|
|
207
|
+
if (options.startPosition && options.startPosition > 0) {
|
|
208
|
+
args.push(`--start=${options.startPosition}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const player = spawn(mpvPath, args, {
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
detached: false
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
let lastPosition = null;
|
|
217
|
+
let positionInterval = null;
|
|
218
|
+
|
|
219
|
+
const startTracking = () => {
|
|
220
|
+
positionInterval = setInterval(async () => {
|
|
221
|
+
const pos = await getMpvPosition(socketPath);
|
|
222
|
+
if (pos) {
|
|
223
|
+
lastPosition = pos;
|
|
224
|
+
}
|
|
225
|
+
}, 3000);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
setTimeout(startTracking, 2000);
|
|
229
|
+
|
|
230
|
+
player.on("error", (err) => {
|
|
231
|
+
if (positionInterval) clearInterval(positionInterval);
|
|
232
|
+
reject(err);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
player.on("close", () => {
|
|
236
|
+
if (positionInterval) clearInterval(positionInterval);
|
|
237
|
+
|
|
238
|
+
if (!isWindows && fs.existsSync(socketPath)) {
|
|
239
|
+
try {
|
|
240
|
+
fs.unlinkSync(socketPath);
|
|
241
|
+
} catch (e) {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (lastPosition && options.onClose) {
|
|
245
|
+
options.onClose(lastPosition.position, lastPosition.duration);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
resolve();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawn, execSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
|
|
8
|
+
const isWindows = os.platform() === "win32";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @returns {Promise<string|null>}
|
|
12
|
+
*/
|
|
13
|
+
export async function getVlcPath() {
|
|
14
|
+
if (isWindows) {
|
|
15
|
+
const commonPaths = [
|
|
16
|
+
"C:\\Program Files\\VideoLAN\\VLC\\vlc.exe",
|
|
17
|
+
"C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const p of commonPaths) {
|
|
21
|
+
if (fs.existsSync(p)) return p;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = execSync("where vlc").toString().trim().split("\n")[0];
|
|
26
|
+
if (result && fs.existsSync(result)) return result;
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
const result = execSync("which vlc").toString().trim();
|
|
31
|
+
if (result) return result;
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync("/usr/bin/vlc")) return "/usr/bin/vlc";
|
|
35
|
+
if (fs.existsSync("/Applications/VLC.app/Contents/MacOS/VLC")) return "/Applications/VLC.app/Contents/MacOS/VLC";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns {Promise<boolean>}
|
|
43
|
+
*/
|
|
44
|
+
export async function installVlc() {
|
|
45
|
+
const platform = os.platform();
|
|
46
|
+
|
|
47
|
+
if (platform === "win32") {
|
|
48
|
+
console.log(chalk.cyan(t("errors.vlcNotFound", { manager: "Winget" })));
|
|
49
|
+
try {
|
|
50
|
+
execSync("winget install VideoLAN.VLC -e --source winget", { stdio: "inherit" });
|
|
51
|
+
console.log(chalk.green(t("errors.vlcInstalled")));
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(chalk.red(t("errors.vlcInstallFailed")));
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
} else if (platform === "darwin") {
|
|
58
|
+
console.log(chalk.cyan(t("errors.vlcNotFound", { manager: "Homebrew" })));
|
|
59
|
+
try {
|
|
60
|
+
execSync("brew install --cask vlc", { stdio: "inherit" });
|
|
61
|
+
console.log(chalk.green(t("errors.vlcInstalled")));
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red(t("errors.vlcInstallFailed")));
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
} else if (platform === "linux") {
|
|
68
|
+
const managers = [
|
|
69
|
+
{ cmd: "apt-get", install: "sudo apt-get update && sudo apt-get install vlc -y" },
|
|
70
|
+
{ cmd: "dnf", install: "sudo dnf install vlc -y" },
|
|
71
|
+
{ cmd: "pacman", install: "sudo pacman -S vlc --noconfirm" }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const mgr of managers) {
|
|
75
|
+
try {
|
|
76
|
+
execSync(`which ${mgr.cmd}`, { stdio: "ignore" });
|
|
77
|
+
execSync(mgr.install, { stdio: "inherit" });
|
|
78
|
+
console.log(chalk.green(t("errors.vlcInstalled")));
|
|
79
|
+
return true;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} url
|
|
91
|
+
* @param {object} [options]
|
|
92
|
+
* @param {number} [options.startPosition] - VLC'de desteklenmiyor
|
|
93
|
+
* @param {(position: number, duration: number) => void} [options.onClose] - VLC'de desteklenmiyor
|
|
94
|
+
* @returns {Promise<number|void>}
|
|
95
|
+
*/
|
|
96
|
+
export async function openInVlc(url, options = {}) {
|
|
97
|
+
let vlcPath = await getVlcPath();
|
|
98
|
+
|
|
99
|
+
if (!vlcPath) {
|
|
100
|
+
const installed = await installVlc();
|
|
101
|
+
if (installed) {
|
|
102
|
+
vlcPath = await getVlcPath();
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error(t("errors.vlcNotFoundAndFailed"));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!vlcPath) throw new Error(t("errors.vlcPathNotFound"));
|
|
109
|
+
|
|
110
|
+
console.log(chalk.cyan(t("errors.vlcStarting")));
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const child = spawn(vlcPath, ["--fullscreen", url], {
|
|
114
|
+
stdio: "ignore"
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
child.on("error", (err) => reject(err));
|
|
118
|
+
child.on("close", (code) => resolve(code));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getConfig } from "./storage/config.js";
|
|
6
|
+
import { saveQueue } from "./storage/queue.js";
|
|
7
|
+
import { batch } from "./download/concurrency.js";
|
|
8
|
+
import { dl } from "./download/download.js";
|
|
9
|
+
import { spinner } from "./spinner.js";
|
|
10
|
+
import { ProgressBar } from "./download/progress.js";
|
|
11
|
+
import { EstSpeed, EP_SIZE } from "../constants.js";
|
|
12
|
+
import notifier from "node-notifier";
|
|
13
|
+
import { t } from "../i18n/index.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import("./storage/queue.js").QueueItem[]} queue
|
|
17
|
+
*/
|
|
18
|
+
export async function processQueue(queue) {
|
|
19
|
+
console.clear();
|
|
20
|
+
const totalEpisodes = queue.length;
|
|
21
|
+
const estimatedSizeMB = totalEpisodes * EP_SIZE;
|
|
22
|
+
const estimatedSizeGB = (estimatedSizeMB / 1024).toFixed(2);
|
|
23
|
+
|
|
24
|
+
console.log(chalk.green(`\n${t("queue.title")}`));
|
|
25
|
+
console.log(chalk.gray(t("queue.totalEpisodes", { count: totalEpisodes })));
|
|
26
|
+
console.log(chalk.gray(t("queue.estimatedSize", { size: estimatedSizeGB })));
|
|
27
|
+
|
|
28
|
+
if (EstSpeed) {
|
|
29
|
+
const estimatedSeconds = estimatedSizeMB / (EstSpeed / 8);
|
|
30
|
+
const estimatedMinutes = Math.ceil(estimatedSeconds / 60);
|
|
31
|
+
console.log(chalk.gray(t("queue.estimatedTime", { minutes: estimatedMinutes, speed: EstSpeed })));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { confirm } = await inquirer.prompt([{
|
|
35
|
+
type: "confirm",
|
|
36
|
+
name: "confirm",
|
|
37
|
+
message: t("queue.confirmStart"),
|
|
38
|
+
default: true
|
|
39
|
+
}]);
|
|
40
|
+
|
|
41
|
+
if (!confirm) return;
|
|
42
|
+
|
|
43
|
+
const config = getConfig();
|
|
44
|
+
|
|
45
|
+
const dirs = new Set(queue.map(item => item.dirPath));
|
|
46
|
+
dirs.forEach(dir => {
|
|
47
|
+
if (!fs.existsSync(dir)) {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(chalk.cyan(`\n${t("queue.downloading", { count: totalEpisodes, limit: config.maxConcurrent })}`));
|
|
53
|
+
|
|
54
|
+
const progressUI = new ProgressBar();
|
|
55
|
+
|
|
56
|
+
queue.forEach((item) => {
|
|
57
|
+
const key = `${item.safeAnimeName}-${item.episode.episode_number}`;
|
|
58
|
+
progressUI.update(key, {
|
|
59
|
+
name: `${item.animeName} - ${item.episode.episode_number}`,
|
|
60
|
+
status: t("progress.waiting"),
|
|
61
|
+
percent: 0
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (queue.length > 0) {
|
|
66
|
+
console.log("\n".repeat(Math.min(queue.length, config.maxConcurrent)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tasks = queue.map((item) => async () => {
|
|
70
|
+
const key = `${item.safeAnimeName}-${item.episode.episode_number}`;
|
|
71
|
+
const downloadPath = path.join(item.dirPath, `${item.safeAnimeName} - ${item.episode.episode_number}`);
|
|
72
|
+
|
|
73
|
+
if (!item.episode.link) {
|
|
74
|
+
progressUI.update(key, { status: t("progress.error"), percent: 0 });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
progressUI.update(key, { status: t("progress.downloading"), percent: 0 });
|
|
80
|
+
|
|
81
|
+
await dl(item.episode.link, downloadPath, {
|
|
82
|
+
silent: true,
|
|
83
|
+
onProgress: (data) => {
|
|
84
|
+
progressUI.update(key, {
|
|
85
|
+
status: t("progress.downloading"),
|
|
86
|
+
percent: data.percent,
|
|
87
|
+
speed: data.speed,
|
|
88
|
+
eta: data.eta,
|
|
89
|
+
downloaded: data.downloaded,
|
|
90
|
+
total: data.total
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, {
|
|
94
|
+
count: config.retryEnabled ? (config.retryCount || 3) : 0,
|
|
95
|
+
delay: config.retryDelay || 3000
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
progressUI.update(key, { status: t("progress.completed"), percent: 100 });
|
|
99
|
+
|
|
100
|
+
const index = queue.indexOf(item);
|
|
101
|
+
if (index > -1) {
|
|
102
|
+
queue.splice(index, 1);
|
|
103
|
+
saveQueue(queue);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
progressUI.update(key, { status: t("progress.error"), percent: 0 });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await batch(tasks, config.maxConcurrent);
|
|
111
|
+
progressUI.clear();
|
|
112
|
+
spinner.succeed(chalk.bold(t("queue.allComplete")));
|
|
113
|
+
|
|
114
|
+
notifier.notify({
|
|
115
|
+
title: 'Animely',
|
|
116
|
+
message: t("queue.notificationComplete"),
|
|
117
|
+
sound: true
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
121
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { getConfig } from "./storage/config.js";
|
|
5
|
+
import { formatName, getLink } from "../functions/episodes.js";
|
|
6
|
+
import { openInVlc } from "./players/vlc.js";
|
|
7
|
+
import { openInMpv } from "./players/mpv.js";
|
|
8
|
+
import { setActivity, setWatchingActivity } from "./discord.js";
|
|
9
|
+
import { updateHistory, loadHistory } from "./storage/history.js";
|
|
10
|
+
import { getWatchPosition, updateWatchPosition, clearWatchPosition } from "./storage/watch_progress.js";
|
|
11
|
+
import { searchAnime, updateAnilistProgress } from "./anilist.js";
|
|
12
|
+
import { spinner } from "./spinner.js";
|
|
13
|
+
import { API_URL } from "../constants.js";
|
|
14
|
+
import { t } from "../i18n/index.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import("../jsdoc.js").Anime} anime
|
|
18
|
+
* @param {number} nextEpisodeNumber
|
|
19
|
+
*/
|
|
20
|
+
export async function resumeWatch(anime, nextEpisodeNumber) {
|
|
21
|
+
console.clear();
|
|
22
|
+
spinner.start(t("spinner.fetchingEpisodes", { name: anime.NAME }));
|
|
23
|
+
|
|
24
|
+
let episodes;
|
|
25
|
+
try {
|
|
26
|
+
const response = await axios.post(`${API_URL}/searchAnime`, { payload: anime.SLUG });
|
|
27
|
+
episodes = response.data.episodes;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
spinner.fail(chalk.red(t("episodes.loadFailed")));
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
spinner.stop();
|
|
34
|
+
|
|
35
|
+
const episode = episodes.find(e => e.episode_number == nextEpisodeNumber);
|
|
36
|
+
|
|
37
|
+
if (!episode) {
|
|
38
|
+
console.log(chalk.yellow(`\n${t("player.episodeNotFound", { episode: nextEpisodeNumber })}`));
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const links = [episode.backblaze_link, episode.watch_link_1, episode.watch_link_2, episode.watch_link_3];
|
|
44
|
+
const link = getLink(links);
|
|
45
|
+
|
|
46
|
+
if (!link) {
|
|
47
|
+
console.log(chalk.red("\n" + t("player.linkNotFound")));
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = getConfig();
|
|
53
|
+
const player = config.defaultPlayer || "vlc";
|
|
54
|
+
|
|
55
|
+
let startPosition = 0;
|
|
56
|
+
if (player === "mpv") {
|
|
57
|
+
const savedProgress = getWatchPosition(anime.NAME, nextEpisodeNumber);
|
|
58
|
+
|
|
59
|
+
if (savedProgress && savedProgress.position > 10) {
|
|
60
|
+
const minutes = Math.floor(savedProgress.position / 60);
|
|
61
|
+
const seconds = savedProgress.position % 60;
|
|
62
|
+
const { resumeFromSaved } = await inquirer.prompt([{
|
|
63
|
+
type: "confirm",
|
|
64
|
+
name: "resumeFromSaved",
|
|
65
|
+
message: t("player.resumePrompt", { time: `${minutes}:${seconds.toString().padStart(2, '0')}` }),
|
|
66
|
+
default: true
|
|
67
|
+
}]);
|
|
68
|
+
|
|
69
|
+
if (resumeFromSaved) {
|
|
70
|
+
startPosition = savedProgress.position;
|
|
71
|
+
} else {
|
|
72
|
+
clearWatchPosition(anime.NAME, nextEpisodeNumber);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.green(`\n${t("player.episodeOpening", { name: anime.NAME, episode: nextEpisodeNumber, player })}`));
|
|
78
|
+
setWatchingActivity({
|
|
79
|
+
animeName: anime.NAME,
|
|
80
|
+
animeImage: anime.FIRST_IMAGE,
|
|
81
|
+
episode: nextEpisodeNumber,
|
|
82
|
+
totalEpisodes: episodes.length
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (player === "mpv") {
|
|
87
|
+
const onPlayerClose = (position, duration) => {
|
|
88
|
+
if (position > 10 && duration > 0) {
|
|
89
|
+
updateWatchPosition(anime.NAME, nextEpisodeNumber, position, duration);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
await openInMpv(link, { startPosition, onClose: onPlayerClose });
|
|
93
|
+
} else {
|
|
94
|
+
await openInVlc(link);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setActivity(t("menu.browsingMenu"));
|
|
98
|
+
|
|
99
|
+
const { watched } = await inquirer.prompt([{
|
|
100
|
+
type: "confirm",
|
|
101
|
+
name: "watched",
|
|
102
|
+
message: t("player.markWatched"),
|
|
103
|
+
default: true
|
|
104
|
+
}]);
|
|
105
|
+
|
|
106
|
+
if (watched) {
|
|
107
|
+
const totalEpisodes = episodes.length;
|
|
108
|
+
|
|
109
|
+
let anilistId;
|
|
110
|
+
const history = loadHistory();
|
|
111
|
+
if (history[anime.NAME]) {
|
|
112
|
+
anilistId = history[anime.NAME].anilistId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (config.anilistToken && !anilistId) {
|
|
116
|
+
spinner.start(t("player.anilistSearching"));
|
|
117
|
+
anilistId = await searchAnime(anime.NAME);
|
|
118
|
+
spinner.stop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
updateHistory(anime.NAME, nextEpisodeNumber, totalEpisodes, anilistId);
|
|
122
|
+
|
|
123
|
+
if (config.anilistToken && anilistId) {
|
|
124
|
+
spinner.start(t("anilist.historyUpdating"));
|
|
125
|
+
const success = await updateAnilistProgress(anilistId, nextEpisodeNumber, nextEpisodeNumber >= totalEpisodes);
|
|
126
|
+
spinner.stop();
|
|
127
|
+
if (success) {
|
|
128
|
+
console.log(chalk.green(""));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(chalk.red(t("player.playerError", { message: error.message })));
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} searchTerm
|
|
3
|
+
* @param {import("../jsdoc.js").Anime[]} animes
|
|
4
|
+
* @returns {import("../jsdoc.js").Anime[]}
|
|
5
|
+
*/
|
|
6
|
+
export function searchAnimes(searchTerm, animes) {
|
|
7
|
+
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
|
8
|
+
|
|
9
|
+
const exactMatches = animes.filter(({ NAME, OTHER_NAMES }) => {
|
|
10
|
+
const lowerName = NAME.toLowerCase();
|
|
11
|
+
const lowerOthers = OTHER_NAMES.map(n => n.toLowerCase());
|
|
12
|
+
return lowerName === lowerSearchTerm || lowerOthers.includes(lowerSearchTerm);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (exactMatches.length > 0) {
|
|
16
|
+
return exactMatches;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const partialMatches = animes.filter(({ NAME, OTHER_NAMES }) => {
|
|
20
|
+
const lowerName = NAME.toLowerCase();
|
|
21
|
+
const lowerOthers = OTHER_NAMES.map(n => n.toLowerCase());
|
|
22
|
+
return lowerName.includes(lowerSearchTerm) ||
|
|
23
|
+
lowerOthers.some(name => name.includes(lowerSearchTerm));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (partialMatches.length > 0) {
|
|
27
|
+
return partialMatches;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fuzzyMatches = animes.filter(({ NAME, OTHER_NAMES }) => {
|
|
31
|
+
const allNames = [NAME, ...OTHER_NAMES].map(n => n.toLowerCase());
|
|
32
|
+
return allNames.some(name => {
|
|
33
|
+
const words = lowerSearchTerm.split(" ");
|
|
34
|
+
return words.every(word => name.includes(word));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return fuzzyMatches;
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { handleAnimecix } from "../sources/handlers/animecix.js";
|
|
3
|
+
import { handleAnimely } from "../sources/handlers/animely.js";
|
|
4
|
+
import { handleAllAnime } from "../sources/handlers/allanime.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("../jsdoc.js").Anime[]|null} animes
|
|
8
|
+
* @param {import("./storage/queue.js").QueueItem[]} downloadQueue
|
|
9
|
+
* @param {import("../sources/index.js").Source} source
|
|
10
|
+
*/
|
|
11
|
+
export async function searchAndDownload(animes, downloadQueue, source) {
|
|
12
|
+
if (source.id === "animecix") {
|
|
13
|
+
return handleAnimecix(downloadQueue, source);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (source.id === "allanime") {
|
|
17
|
+
return handleAllAnime(downloadQueue, source);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return handleAnimely(animes, downloadQueue);
|
|
21
|
+
}
|