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,318 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getConfig } from "../../utils/storage/config.js";
|
|
5
|
+
import { loadHistory } from "../../utils/storage/history.js";
|
|
6
|
+
import { setActivity } from "../../utils/discord.js";
|
|
7
|
+
import { spinner } from "../../utils/spinner.js";
|
|
8
|
+
import { menuHeader } from "../../utils/ui/box.js";
|
|
9
|
+
import { t } from "../../i18n/index.js";
|
|
10
|
+
import {
|
|
11
|
+
askSelectionMethod,
|
|
12
|
+
parseRange,
|
|
13
|
+
selectQuality,
|
|
14
|
+
askDownloadAction,
|
|
15
|
+
downloadEpisodes,
|
|
16
|
+
playEpisode,
|
|
17
|
+
postWatchActions,
|
|
18
|
+
addToQueue,
|
|
19
|
+
buildEpisodeChoices
|
|
20
|
+
} from "./base.js";
|
|
21
|
+
|
|
22
|
+
export async function handleAllAnime(downloadQueue, source) {
|
|
23
|
+
let selectedAnime;
|
|
24
|
+
let episodes;
|
|
25
|
+
|
|
26
|
+
const { audioType } = await inquirer.prompt([{
|
|
27
|
+
type: "list",
|
|
28
|
+
name: "audioType",
|
|
29
|
+
message: t("allanime.selectAudioType"),
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: t("allanime.subbed"), value: "sub" },
|
|
32
|
+
{ name: t("allanime.dubbed"), value: "dub" }
|
|
33
|
+
],
|
|
34
|
+
default: "sub"
|
|
35
|
+
}]);
|
|
36
|
+
|
|
37
|
+
source.setMode(audioType);
|
|
38
|
+
|
|
39
|
+
while (true) {
|
|
40
|
+
console.clear();
|
|
41
|
+
const { name } = await inquirer.prompt([{
|
|
42
|
+
type: "input",
|
|
43
|
+
name: "name",
|
|
44
|
+
message: t("search.enterName"),
|
|
45
|
+
validate: (input) => input?.trim() ? true : t("search.invalidName")
|
|
46
|
+
}]);
|
|
47
|
+
|
|
48
|
+
if (name.toLowerCase() === t("search.cancel") || name.toLowerCase() === "cancel") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
spinner.start(t("search.searching"));
|
|
53
|
+
|
|
54
|
+
let foundAnimes;
|
|
55
|
+
try {
|
|
56
|
+
foundAnimes = await source.search(name);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
spinner.fail(chalk.red(t("search.searchFailed")));
|
|
59
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (foundAnimes.length === 0) {
|
|
64
|
+
spinner.fail(chalk.gray(t("search.noResults")));
|
|
65
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
spinner.stop();
|
|
70
|
+
|
|
71
|
+
const { anime } = await inquirer.prompt([{
|
|
72
|
+
type: "list",
|
|
73
|
+
name: "anime",
|
|
74
|
+
message: t("search.selectAnime"),
|
|
75
|
+
pageSize: 15,
|
|
76
|
+
loop: false,
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: t("search.goBack"), value: "back" },
|
|
79
|
+
new inquirer.Separator(),
|
|
80
|
+
...foundAnimes.map(a => ({
|
|
81
|
+
name: `${a.name} ${chalk.gray(`(${a.totalEpisodes} ${t("search.episodes")})`)}`,
|
|
82
|
+
value: a
|
|
83
|
+
}))
|
|
84
|
+
]
|
|
85
|
+
}]);
|
|
86
|
+
|
|
87
|
+
if (anime === "back") continue;
|
|
88
|
+
selectedAnime = anime;
|
|
89
|
+
|
|
90
|
+
spinner.start(t("episodes.loading"));
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
episodes = await source.getEpisodes(selectedAnime.id);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
spinner.fail(chalk.red(t("episodes.loadFailed")));
|
|
96
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!episodes || episodes.length === 0) {
|
|
101
|
+
spinner.fail(chalk.gray(t("episodes.notFound")));
|
|
102
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
spinner.stop();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.clear();
|
|
111
|
+
const audioLabel = audioType === "sub" ? t("allanime.subbed") : t("allanime.dubbed");
|
|
112
|
+
console.log(chalk.bgCyan.black(` ${selectedAnime.name} `) + chalk.gray(` ${t("episodes.episodeCount", { count: episodes.length })} | ${audioLabel}`));
|
|
113
|
+
console.log("");
|
|
114
|
+
|
|
115
|
+
while (true) {
|
|
116
|
+
const { action } = await inquirer.prompt([{
|
|
117
|
+
type: "list",
|
|
118
|
+
name: "action",
|
|
119
|
+
message: t("episodes.selectAction"),
|
|
120
|
+
choices: [
|
|
121
|
+
{ name: t("episodes.watch"), value: "watch" },
|
|
122
|
+
{ name: t("episodes.download"), value: "download" },
|
|
123
|
+
{ name: t("episodes.goBack"), value: "back" }
|
|
124
|
+
]
|
|
125
|
+
}]);
|
|
126
|
+
|
|
127
|
+
if (action === "back") return;
|
|
128
|
+
|
|
129
|
+
if (action === "watch") {
|
|
130
|
+
await watchEpisode(selectedAnime, episodes, source);
|
|
131
|
+
} else if (action === "download") {
|
|
132
|
+
await downloadEpisodesFlow(selectedAnime, episodes, source, downloadQueue);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function watchEpisode(selectedAnime, episodes, source) {
|
|
138
|
+
const history = loadHistory();
|
|
139
|
+
const animeHistory = history[selectedAnime.name];
|
|
140
|
+
const lastWatchedEp = animeHistory?.lastEpisode || 0;
|
|
141
|
+
|
|
142
|
+
while (true) {
|
|
143
|
+
console.clear();
|
|
144
|
+
menuHeader(selectedAnime.name, lastWatchedEp, episodes.length);
|
|
145
|
+
|
|
146
|
+
const choices = buildEpisodeChoices(episodes, lastWatchedEp, (ep) => ({
|
|
147
|
+
name: ep.name || t("episodes.episodeNumber", { number: ep.episode_number }),
|
|
148
|
+
value: ep
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const { episode } = await inquirer.prompt([{
|
|
152
|
+
type: "list",
|
|
153
|
+
name: "episode",
|
|
154
|
+
message: t("episodes.selectEpisode"),
|
|
155
|
+
pageSize: 15,
|
|
156
|
+
loop: false,
|
|
157
|
+
choices
|
|
158
|
+
}]);
|
|
159
|
+
|
|
160
|
+
if (episode === "back") break;
|
|
161
|
+
|
|
162
|
+
spinner.start(t("player.gettingLink"));
|
|
163
|
+
|
|
164
|
+
let streamLinks;
|
|
165
|
+
try {
|
|
166
|
+
streamLinks = await source.getStreamLinks(episode);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
spinner.fail(chalk.red(t("player.linkFailed")));
|
|
169
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!streamLinks || streamLinks.length === 0) {
|
|
174
|
+
spinner.fail(chalk.red(t("player.sourceNotFound")));
|
|
175
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
spinner.stop();
|
|
180
|
+
|
|
181
|
+
const selectedLink = await selectQuality(streamLinks);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await playEpisode({
|
|
185
|
+
animeName: selectedAnime.name,
|
|
186
|
+
animeImage: selectedAnime.poster || "",
|
|
187
|
+
episodeNumber: episode.episode_number,
|
|
188
|
+
totalEpisodes: episodes.length,
|
|
189
|
+
streamUrl: selectedLink,
|
|
190
|
+
supportResume: true
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await postWatchActions({
|
|
194
|
+
animeName: selectedAnime.name,
|
|
195
|
+
episodeNumber: episode.episode_number,
|
|
196
|
+
totalEpisodes: episodes.length,
|
|
197
|
+
existingAnilistId: animeHistory?.anilistId
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(chalk.red(t("player.playerError", { message: error.message })));
|
|
202
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function downloadEpisodesFlow(selectedAnime, episodes, source, downloadQueue) {
|
|
208
|
+
setActivity(`${selectedAnime.name}`, t("progress.downloading"));
|
|
209
|
+
|
|
210
|
+
const selectionMethod = await askSelectionMethod(selectedAnime.name);
|
|
211
|
+
if (selectionMethod === "back") return;
|
|
212
|
+
|
|
213
|
+
let selectedEpisodes = [];
|
|
214
|
+
|
|
215
|
+
if (selectionMethod === "list") {
|
|
216
|
+
console.clear();
|
|
217
|
+
console.log(chalk.bgCyan.black(` ${t("download.episodeSelection", { name: selectedAnime.name })} `));
|
|
218
|
+
console.log("");
|
|
219
|
+
|
|
220
|
+
const { selected } = await inquirer.prompt([{
|
|
221
|
+
type: "checkbox",
|
|
222
|
+
name: "selected",
|
|
223
|
+
message: t("download.selectEpisodes"),
|
|
224
|
+
choices: episodes.map(ep => ({
|
|
225
|
+
name: ep.name || t("episodes.episodeNumber", { number: ep.episode_number }),
|
|
226
|
+
value: ep
|
|
227
|
+
}))
|
|
228
|
+
}]);
|
|
229
|
+
selectedEpisodes = selected;
|
|
230
|
+
|
|
231
|
+
} else if (selectionMethod === "range") {
|
|
232
|
+
console.clear();
|
|
233
|
+
console.log(chalk.bgCyan.black(` ${t("download.rangeSelection", { name: selectedAnime.name })} `));
|
|
234
|
+
console.log("");
|
|
235
|
+
|
|
236
|
+
const { range } = await inquirer.prompt([{
|
|
237
|
+
type: "input",
|
|
238
|
+
name: "range",
|
|
239
|
+
message: t("download.enterRangePrompt"),
|
|
240
|
+
validate: (input) => input?.trim() ? true : t("download.invalidRange")
|
|
241
|
+
}]);
|
|
242
|
+
|
|
243
|
+
const numbers = parseRange(range);
|
|
244
|
+
selectedEpisodes = episodes.filter(ep => numbers.has(ep.episode_number));
|
|
245
|
+
|
|
246
|
+
} else if (selectionMethod === "all") {
|
|
247
|
+
selectedEpisodes = [...episodes];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!selectedEpisodes || selectedEpisodes.length === 0) {
|
|
251
|
+
console.log(chalk.yellow(t("download.noEpisodeSelected")));
|
|
252
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
spinner.start(t("download.gettingQualities"));
|
|
257
|
+
let availableQualities = [];
|
|
258
|
+
try {
|
|
259
|
+
const sampleLinks = await source.getStreamLinks(selectedEpisodes[0]);
|
|
260
|
+
availableQualities = sampleLinks || [];
|
|
261
|
+
} catch (e) {}
|
|
262
|
+
spinner.stop();
|
|
263
|
+
|
|
264
|
+
let selectedQuality = null;
|
|
265
|
+
if (availableQualities.length > 1) {
|
|
266
|
+
const { quality } = await inquirer.prompt([{
|
|
267
|
+
type: "list",
|
|
268
|
+
name: "quality",
|
|
269
|
+
message: t("download.selectQuality"),
|
|
270
|
+
choices: availableQualities.map(q => ({
|
|
271
|
+
name: q.label || q.quality || t("player.default"),
|
|
272
|
+
value: q.quality || q.label
|
|
273
|
+
}))
|
|
274
|
+
}]);
|
|
275
|
+
selectedQuality = quality;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.clear();
|
|
279
|
+
|
|
280
|
+
const downloadAction = await askDownloadAction();
|
|
281
|
+
const config = getConfig();
|
|
282
|
+
const safeAnimeName = selectedAnime.name.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
283
|
+
const dirPath = path.join(config.downloadDir, safeAnimeName);
|
|
284
|
+
|
|
285
|
+
const getDownloadUrl = async (episode) => {
|
|
286
|
+
const streamLinks = await source.getStreamLinks(episode);
|
|
287
|
+
if (!streamLinks || streamLinks.length === 0) return null;
|
|
288
|
+
|
|
289
|
+
if (selectedQuality) {
|
|
290
|
+
const matched = streamLinks.find(s => (s.quality || s.label) === selectedQuality);
|
|
291
|
+
if (matched) return matched.url;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const sorted = [...streamLinks].sort((a, b) => {
|
|
295
|
+
const getRes = (s) => parseInt((s.quality || s.label || "0").replace(/\D/g, "")) || 0;
|
|
296
|
+
return getRes(b) - getRes(a);
|
|
297
|
+
});
|
|
298
|
+
return sorted[0].url;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (downloadAction === "queue") {
|
|
302
|
+
addToQueue({
|
|
303
|
+
animeName: selectedAnime.name,
|
|
304
|
+
episodes: selectedEpisodes,
|
|
305
|
+
dirPath,
|
|
306
|
+
downloadQueue
|
|
307
|
+
});
|
|
308
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await downloadEpisodes({
|
|
313
|
+
animeName: selectedAnime.name,
|
|
314
|
+
episodes: selectedEpisodes,
|
|
315
|
+
dirPath,
|
|
316
|
+
getDownloadUrl
|
|
317
|
+
});
|
|
318
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getConfig } from "../../utils/storage/config.js";
|
|
6
|
+
import { loadHistory } from "../../utils/storage/history.js";
|
|
7
|
+
import { setActivity } from "../../utils/discord.js";
|
|
8
|
+
import { spinner } from "../../utils/spinner.js";
|
|
9
|
+
import { menuHeader } from "../../utils/ui/box.js";
|
|
10
|
+
import { t } from "../../i18n/index.js";
|
|
11
|
+
import {
|
|
12
|
+
askSelectionMethod,
|
|
13
|
+
parseRange,
|
|
14
|
+
selectQuality,
|
|
15
|
+
askDownloadAction,
|
|
16
|
+
downloadEpisodes,
|
|
17
|
+
playEpisode,
|
|
18
|
+
postWatchActions,
|
|
19
|
+
addToQueue,
|
|
20
|
+
buildEpisodeChoices
|
|
21
|
+
} from "./base.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Animecix kaynağı için arama ve indirme
|
|
25
|
+
* @param {Array<any>} downloadQueue
|
|
26
|
+
* @param {import("../index.js").Source} source
|
|
27
|
+
*/
|
|
28
|
+
export async function handleAnimecix(downloadQueue, source) {
|
|
29
|
+
let selectedAnime;
|
|
30
|
+
let episodes;
|
|
31
|
+
|
|
32
|
+
// Anime arama döngüsü
|
|
33
|
+
while (true) {
|
|
34
|
+
console.clear();
|
|
35
|
+
const { name } = await inquirer.prompt([{
|
|
36
|
+
type: "input",
|
|
37
|
+
name: "name",
|
|
38
|
+
message: t("search.enterName"),
|
|
39
|
+
validate: (input) => input?.trim() ? true : t("search.invalidName")
|
|
40
|
+
}]);
|
|
41
|
+
|
|
42
|
+
if (name.toLowerCase() === t("search.cancel") || name.toLowerCase() === "iptal" || name.toLowerCase() === "cancel") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
spinner.start(t("search.searching"));
|
|
47
|
+
|
|
48
|
+
let foundAnimes;
|
|
49
|
+
try {
|
|
50
|
+
foundAnimes = await source.search(name);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
spinner.fail(chalk.red(t("search.searchFailed")));
|
|
53
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (foundAnimes.length === 0) {
|
|
58
|
+
spinner.fail(chalk.gray(t("search.noResults")));
|
|
59
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
spinner.stop();
|
|
64
|
+
|
|
65
|
+
const { anime } = await inquirer.prompt([{
|
|
66
|
+
type: "list",
|
|
67
|
+
name: "anime",
|
|
68
|
+
message: t("search.selectAnime"),
|
|
69
|
+
pageSize: 15,
|
|
70
|
+
loop: false,
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: t("search.goBack"), value: "back" },
|
|
73
|
+
new inquirer.Separator(),
|
|
74
|
+
...foundAnimes.map(a => ({
|
|
75
|
+
name: `${a.name} ${a.type ? chalk.gray(`(${a.type})`) : ""}`,
|
|
76
|
+
value: a
|
|
77
|
+
}))
|
|
78
|
+
]
|
|
79
|
+
}]);
|
|
80
|
+
|
|
81
|
+
if (anime === "back") continue;
|
|
82
|
+
selectedAnime = anime;
|
|
83
|
+
|
|
84
|
+
spinner.start(t("episodes.loading"));
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
episodes = await source.getEpisodes(selectedAnime.id);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
spinner.fail(chalk.red(t("episodes.loadFailed")));
|
|
90
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!episodes || episodes.length === 0) {
|
|
95
|
+
spinner.fail(chalk.gray(t("episodes.notFound")));
|
|
96
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
spinner.stop();
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ana menü
|
|
105
|
+
console.clear();
|
|
106
|
+
console.log(chalk.bgCyan.black(` ${selectedAnime.name} `) + chalk.gray(` ${t("episodes.episodeCount", { count: episodes.length })}`));
|
|
107
|
+
console.log("");
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
const { action } = await inquirer.prompt([{
|
|
111
|
+
type: "list",
|
|
112
|
+
name: "action",
|
|
113
|
+
message: t("episodes.selectAction"),
|
|
114
|
+
choices: [
|
|
115
|
+
{ name: t("episodes.watch"), value: "watch" },
|
|
116
|
+
{ name: t("episodes.download"), value: "download" },
|
|
117
|
+
{ name: t("episodes.goBack"), value: "back" }
|
|
118
|
+
]
|
|
119
|
+
}]);
|
|
120
|
+
|
|
121
|
+
if (action === "back") return;
|
|
122
|
+
|
|
123
|
+
if (action === "watch") {
|
|
124
|
+
await watchEpisode(selectedAnime, episodes, source);
|
|
125
|
+
} else if (action === "download") {
|
|
126
|
+
await downloadEpisodesFlow(selectedAnime, episodes, source, downloadQueue);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bölüm izleme
|
|
133
|
+
*/
|
|
134
|
+
async function watchEpisode(selectedAnime, episodes, source) {
|
|
135
|
+
const history = loadHistory();
|
|
136
|
+
const animeHistory = history[selectedAnime.name];
|
|
137
|
+
const lastWatchedEp = animeHistory?.lastEpisode || 0;
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
console.clear();
|
|
141
|
+
menuHeader(selectedAnime.name, lastWatchedEp, episodes.length);
|
|
142
|
+
|
|
143
|
+
const choices = buildEpisodeChoices(episodes, lastWatchedEp, (ep) => ({
|
|
144
|
+
name: ep.name || t("episodes.episodeNumber", { number: ep.episode_number }),
|
|
145
|
+
value: ep
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const { episode } = await inquirer.prompt([{
|
|
149
|
+
type: "list",
|
|
150
|
+
name: "episode",
|
|
151
|
+
message: t("episodes.selectEpisode"),
|
|
152
|
+
pageSize: 15,
|
|
153
|
+
loop: false,
|
|
154
|
+
choices
|
|
155
|
+
}]);
|
|
156
|
+
|
|
157
|
+
if (episode === "back") break;
|
|
158
|
+
|
|
159
|
+
spinner.start(t("player.gettingLink"));
|
|
160
|
+
|
|
161
|
+
let streamLinks;
|
|
162
|
+
try {
|
|
163
|
+
streamLinks = await source.getStreamLinks({
|
|
164
|
+
...episode,
|
|
165
|
+
_animeId: selectedAnime.id,
|
|
166
|
+
_isMovie: selectedAnime._isMovie
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
spinner.fail(chalk.red(t("player.linkFailed")));
|
|
170
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!streamLinks || streamLinks.length === 0) {
|
|
175
|
+
spinner.fail(chalk.red(t("player.sourceNotFound")));
|
|
176
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
spinner.stop();
|
|
181
|
+
|
|
182
|
+
const selectedLink = await selectQuality(streamLinks);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await playEpisode({
|
|
186
|
+
animeName: selectedAnime.name,
|
|
187
|
+
animeImage: selectedAnime.poster || "",
|
|
188
|
+
episodeNumber: episode.episode_number,
|
|
189
|
+
totalEpisodes: episodes.length,
|
|
190
|
+
streamUrl: selectedLink,
|
|
191
|
+
supportResume: false // Animecix için şimdilik kapalı
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(chalk.red(t("player.playerError", { message: error.message })));
|
|
197
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Bölüm indirme akışı
|
|
204
|
+
*/
|
|
205
|
+
async function downloadEpisodesFlow(selectedAnime, episodes, source, downloadQueue) {
|
|
206
|
+
setActivity(`${selectedAnime.name}`, t("progress.downloading"));
|
|
207
|
+
|
|
208
|
+
const selectionMethod = await askSelectionMethod(selectedAnime.name);
|
|
209
|
+
if (selectionMethod === "back") return;
|
|
210
|
+
|
|
211
|
+
let selectedEpisodes = [];
|
|
212
|
+
|
|
213
|
+
if (selectionMethod === "list") {
|
|
214
|
+
console.clear();
|
|
215
|
+
console.log(chalk.bgCyan.black(` ${t("download.episodeSelection", { name: selectedAnime.name })} `));
|
|
216
|
+
console.log("");
|
|
217
|
+
|
|
218
|
+
const { selected } = await inquirer.prompt([{
|
|
219
|
+
type: "checkbox",
|
|
220
|
+
name: "selected",
|
|
221
|
+
message: t("download.selectEpisodes"),
|
|
222
|
+
choices: episodes.map(ep => ({
|
|
223
|
+
name: ep.name || t("episodes.episodeNumber", { number: ep.episode_number }),
|
|
224
|
+
value: ep
|
|
225
|
+
}))
|
|
226
|
+
}]);
|
|
227
|
+
selectedEpisodes = selected;
|
|
228
|
+
|
|
229
|
+
} else if (selectionMethod === "range") {
|
|
230
|
+
console.clear();
|
|
231
|
+
console.log(chalk.bgCyan.black(` ${t("download.rangeSelection", { name: selectedAnime.name })} `));
|
|
232
|
+
console.log("");
|
|
233
|
+
|
|
234
|
+
const { range } = await inquirer.prompt([{
|
|
235
|
+
type: "input",
|
|
236
|
+
name: "range",
|
|
237
|
+
message: t("download.enterRangePrompt"),
|
|
238
|
+
validate: (input) => input?.trim() ? true : t("download.invalidRange")
|
|
239
|
+
}]);
|
|
240
|
+
|
|
241
|
+
const numbers = parseRange(range);
|
|
242
|
+
selectedEpisodes = episodes.filter(ep => numbers.has(ep.episode_number));
|
|
243
|
+
|
|
244
|
+
} else if (selectionMethod === "all") {
|
|
245
|
+
selectedEpisodes = [...episodes];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!selectedEpisodes || selectedEpisodes.length === 0) {
|
|
249
|
+
console.log(chalk.yellow(t("download.noEpisodeSelected")));
|
|
250
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Kalite seçimi
|
|
255
|
+
spinner.start(t("download.gettingQualities"));
|
|
256
|
+
let availableQualities = [];
|
|
257
|
+
try {
|
|
258
|
+
const sampleLinks = await source.getStreamLinks({
|
|
259
|
+
...selectedEpisodes[0],
|
|
260
|
+
_animeId: selectedAnime.id,
|
|
261
|
+
_isMovie: selectedAnime._isMovie
|
|
262
|
+
});
|
|
263
|
+
availableQualities = sampleLinks || [];
|
|
264
|
+
} catch (e) {}
|
|
265
|
+
spinner.stop();
|
|
266
|
+
|
|
267
|
+
let selectedQuality = null;
|
|
268
|
+
if (availableQualities.length > 1) {
|
|
269
|
+
const { quality } = await inquirer.prompt([{
|
|
270
|
+
type: "list",
|
|
271
|
+
name: "quality",
|
|
272
|
+
message: t("download.selectQuality"),
|
|
273
|
+
choices: availableQualities.map(q => ({
|
|
274
|
+
name: q.quality || q.label || t("player.default"),
|
|
275
|
+
value: q.quality || q.label
|
|
276
|
+
}))
|
|
277
|
+
}]);
|
|
278
|
+
selectedQuality = quality;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.clear();
|
|
282
|
+
|
|
283
|
+
const config = getConfig();
|
|
284
|
+
const safeAnimeName = selectedAnime.name.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
285
|
+
const dirPath = path.join(config.downloadDir, safeAnimeName);
|
|
286
|
+
|
|
287
|
+
// URL alma fonksiyonu
|
|
288
|
+
const getDownloadUrl = async (episode) => {
|
|
289
|
+
const streamLinks = await source.getStreamLinks({
|
|
290
|
+
...episode,
|
|
291
|
+
_animeId: selectedAnime.id,
|
|
292
|
+
_isMovie: selectedAnime._isMovie
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (!streamLinks || streamLinks.length === 0) return null;
|
|
296
|
+
|
|
297
|
+
if (selectedQuality) {
|
|
298
|
+
const matched = streamLinks.find(s => (s.quality || s.label) === selectedQuality);
|
|
299
|
+
if (matched) return matched.url;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// En yüksek kaliteyi seç
|
|
303
|
+
const sorted = [...streamLinks].sort((a, b) => {
|
|
304
|
+
const getRes = (s) => parseInt((s.quality || s.label || "0").replace(/\D/g, "")) || 0;
|
|
305
|
+
return getRes(b) - getRes(a);
|
|
306
|
+
});
|
|
307
|
+
return sorted[0].url;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
await downloadEpisodes({
|
|
311
|
+
animeName: selectedAnime.name,
|
|
312
|
+
episodes: selectedEpisodes,
|
|
313
|
+
dirPath,
|
|
314
|
+
getDownloadUrl
|
|
315
|
+
});
|
|
316
|
+
}
|