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,391 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { getConfig } from "../../utils/storage/config.js";
|
|
7
|
+
import { saveQueue } from "../../utils/storage/queue.js";
|
|
8
|
+
import { openInVlc } from "../../utils/players/vlc.js";
|
|
9
|
+
import { openInMpv } from "../../utils/players/mpv.js";
|
|
10
|
+
import { setActivity, setWatchingActivity } from "../../utils/discord.js";
|
|
11
|
+
import { updateHistory, loadHistory } from "../../utils/storage/history.js";
|
|
12
|
+
import { getWatchPosition, updateWatchPosition, clearWatchPosition } from "../../utils/storage/watch_progress.js";
|
|
13
|
+
import { searchAnime, updateAnilistProgress } from "../../utils/anilist.js";
|
|
14
|
+
import { spinner } from "../../utils/spinner.js";
|
|
15
|
+
import { dl } from "../../utils/download/download.js";
|
|
16
|
+
import { batch } from "../../utils/download/concurrency.js";
|
|
17
|
+
import { ProgressBar } from "../../utils/download/progress.js";
|
|
18
|
+
import { menuHeader } from "../../utils/ui/box.js";
|
|
19
|
+
import { t } from "../../i18n/index.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bölüm seçim metodunu sor
|
|
23
|
+
* @param {string} animeName
|
|
24
|
+
* @returns {Promise<"list"|"range"|"all"|"back">}
|
|
25
|
+
*/
|
|
26
|
+
export async function askSelectionMethod(animeName) {
|
|
27
|
+
console.clear();
|
|
28
|
+
console.log(chalk.bgGreen.black(` ${t("download.downloadTitle", { name: animeName })} `));
|
|
29
|
+
console.log("");
|
|
30
|
+
|
|
31
|
+
const { selectionMethod } = await inquirer.prompt([{
|
|
32
|
+
type: "list",
|
|
33
|
+
name: "selectionMethod",
|
|
34
|
+
message: t("download.selectionMethod"),
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: t("download.selectFromList"), value: "list" },
|
|
37
|
+
{ name: t("download.enterRange"), value: "range" },
|
|
38
|
+
{ name: t("download.downloadAll"), value: "all" },
|
|
39
|
+
{ name: t("download.goBack"), value: "back" }
|
|
40
|
+
]
|
|
41
|
+
}]);
|
|
42
|
+
|
|
43
|
+
return selectionMethod;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Aralık girişinden bölüm numaralarını parse et
|
|
48
|
+
* @param {string} rangeInput
|
|
49
|
+
* @returns {Set<number>}
|
|
50
|
+
*/
|
|
51
|
+
export function parseRange(rangeInput) {
|
|
52
|
+
const parts = rangeInput.split(",").map(p => p.trim());
|
|
53
|
+
const numbers = new Set();
|
|
54
|
+
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (part.includes("-")) {
|
|
57
|
+
const [start, end] = part.split("-").map(n => parseInt(n.trim(), 10));
|
|
58
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
59
|
+
for (let i = start; i <= end; i++) numbers.add(i);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const num = parseInt(part, 10);
|
|
63
|
+
if (!isNaN(num)) numbers.add(num);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return numbers;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Kalite seçimi yap
|
|
72
|
+
* @param {import("../index.js").StreamLink[]} streamLinks
|
|
73
|
+
* @returns {Promise<string>}
|
|
74
|
+
*/
|
|
75
|
+
export async function selectQuality(streamLinks) {
|
|
76
|
+
if (streamLinks.length <= 1) {
|
|
77
|
+
return streamLinks[0]?.url || "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { quality } = await inquirer.prompt([{
|
|
81
|
+
type: "list",
|
|
82
|
+
name: "quality",
|
|
83
|
+
message: t("player.selectQuality"),
|
|
84
|
+
choices: streamLinks.map(s => ({
|
|
85
|
+
name: s.quality || s.label || t("player.default"),
|
|
86
|
+
value: s.url
|
|
87
|
+
}))
|
|
88
|
+
}]);
|
|
89
|
+
|
|
90
|
+
return quality;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* İndirme veya kuyruğa ekleme seçimi
|
|
95
|
+
* @returns {Promise<"queue"|"now">}
|
|
96
|
+
*/
|
|
97
|
+
export async function askDownloadAction() {
|
|
98
|
+
const { downloadAction } = await inquirer.prompt([{
|
|
99
|
+
type: "list",
|
|
100
|
+
name: "downloadAction",
|
|
101
|
+
message: t("download.whatToDo"),
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: t("download.addToQueue"), value: "queue" },
|
|
104
|
+
{ name: t("download.downloadNow"), value: "now" }
|
|
105
|
+
]
|
|
106
|
+
}]);
|
|
107
|
+
|
|
108
|
+
return downloadAction;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Bölümleri indir
|
|
113
|
+
* @param {object} params
|
|
114
|
+
* @param {string} params.animeName
|
|
115
|
+
* @param {Array<{episode_number: number, link?: string, url?: string}>} params.episodes
|
|
116
|
+
* @param {string} params.dirPath
|
|
117
|
+
* @param {(episode: any) => Promise<string|null>} [params.getDownloadUrl] - Dinamik URL alma fonksiyonu
|
|
118
|
+
*/
|
|
119
|
+
export async function downloadEpisodes({ animeName, episodes, dirPath, getDownloadUrl }) {
|
|
120
|
+
const config = getConfig();
|
|
121
|
+
const safeAnimeName = animeName.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(dirPath)) {
|
|
124
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (episodes.length > 1) {
|
|
128
|
+
console.log(chalk.cyan(`\n${t("download.episodesSelected", { count: episodes.length, limit: config.maxConcurrent })}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const progressUI = new ProgressBar();
|
|
132
|
+
episodes.forEach(ep => {
|
|
133
|
+
progressUI.update(ep.episode_number, {
|
|
134
|
+
percent: 0,
|
|
135
|
+
status: t("progress.waiting"),
|
|
136
|
+
name: `${t("episodes.episode")} ${ep.episode_number}`
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (episodes.length > 1) {
|
|
141
|
+
console.log("\n".repeat(Math.min(episodes.length, config.maxConcurrent)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const tasks = episodes.map((episode) => async () => {
|
|
145
|
+
const downloadPath = path.join(dirPath, `${safeAnimeName} - ${episode.episode_number}`);
|
|
146
|
+
const isSingle = episodes.length === 1;
|
|
147
|
+
|
|
148
|
+
async function downloadEpisode(attempt = 1) {
|
|
149
|
+
try {
|
|
150
|
+
let downloadUrl = episode.link || episode.url;
|
|
151
|
+
|
|
152
|
+
// Dinamik URL alma (Animecix için)
|
|
153
|
+
if (!downloadUrl && getDownloadUrl) {
|
|
154
|
+
if (!isSingle) {
|
|
155
|
+
progressUI.update(episode.episode_number, {
|
|
156
|
+
percent: 0,
|
|
157
|
+
status: attempt === 1 ? t("progress.gettingLink") : t("progress.retrying")
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
spinner.start(t("download.gettingLink", { episode: episode.episode_number }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
downloadUrl = await getDownloadUrl(episode);
|
|
164
|
+
if (isSingle) spinner.stop();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!downloadUrl) {
|
|
168
|
+
throw new Error(t("download.linkNotFound"));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!isSingle) {
|
|
172
|
+
progressUI.update(episode.episode_number, {
|
|
173
|
+
percent: 0,
|
|
174
|
+
status: attempt === 1 ? t("progress.downloading") : t("progress.retrying")
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await dl(downloadUrl, downloadPath, {
|
|
179
|
+
silent: !isSingle,
|
|
180
|
+
onProgress: (data) => {
|
|
181
|
+
if (!isSingle) {
|
|
182
|
+
progressUI.update(episode.episode_number, {
|
|
183
|
+
percent: data.percent,
|
|
184
|
+
status: attempt === 1 ? t("progress.downloading") : t("progress.retrying"),
|
|
185
|
+
speed: data.speed,
|
|
186
|
+
eta: data.eta,
|
|
187
|
+
downloaded: data.downloaded,
|
|
188
|
+
total: data.total
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}, {
|
|
193
|
+
count: config.retryEnabled ? (config.retryCount || 3) : 0,
|
|
194
|
+
delay: config.retryDelay || 3000
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!isSingle) {
|
|
198
|
+
progressUI.update(episode.episode_number, { percent: 100, status: t("progress.completed") });
|
|
199
|
+
} else {
|
|
200
|
+
spinner.succeed(chalk.bold(t("download.episodeDownloaded", { name: animeName, episode: episode.episode_number })));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (attempt === 1) {
|
|
205
|
+
if (!isSingle) {
|
|
206
|
+
progressUI.update(episode.episode_number, { percent: 0, status: t("progress.errorRetrying") });
|
|
207
|
+
}
|
|
208
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
209
|
+
await downloadEpisode(2);
|
|
210
|
+
} else {
|
|
211
|
+
if (!isSingle) {
|
|
212
|
+
progressUI.update(episode.episode_number, { percent: 0, status: t("progress.error") });
|
|
213
|
+
} else {
|
|
214
|
+
spinner.fail(chalk.red(t("progress.error")));
|
|
215
|
+
console.error(chalk.gray(t("app.errorDetail", { message: error.message })));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await downloadEpisode(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await batch(tasks, config.maxConcurrent);
|
|
225
|
+
progressUI.clear();
|
|
226
|
+
|
|
227
|
+
if (episodes.length > 1) {
|
|
228
|
+
spinner.succeed(chalk.bold(t("download.downloadComplete")));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Bölümü oynat
|
|
236
|
+
* @param {object} params
|
|
237
|
+
* @param {string} params.animeName
|
|
238
|
+
* @param {string} params.animeImage
|
|
239
|
+
* @param {number} params.episodeNumber
|
|
240
|
+
* @param {number} params.totalEpisodes
|
|
241
|
+
* @param {string} params.streamUrl
|
|
242
|
+
* @param {boolean} [params.supportResume=true]
|
|
243
|
+
*/
|
|
244
|
+
export async function playEpisode({ animeName, animeImage, episodeNumber, totalEpisodes, streamUrl, supportResume = true }) {
|
|
245
|
+
const config = getConfig();
|
|
246
|
+
const player = config.defaultPlayer || "vlc";
|
|
247
|
+
|
|
248
|
+
console.clear();
|
|
249
|
+
|
|
250
|
+
let startPosition = 0;
|
|
251
|
+
if (player === "mpv" && supportResume) {
|
|
252
|
+
const savedProgress = getWatchPosition(animeName, episodeNumber);
|
|
253
|
+
|
|
254
|
+
if (savedProgress && savedProgress.position > 10) {
|
|
255
|
+
const minutes = Math.floor(savedProgress.position / 60);
|
|
256
|
+
const seconds = savedProgress.position % 60;
|
|
257
|
+
const { resumeFromSaved } = await inquirer.prompt([{
|
|
258
|
+
type: "confirm",
|
|
259
|
+
name: "resumeFromSaved",
|
|
260
|
+
message: t("player.resumePrompt", { time: `${minutes}:${seconds.toString().padStart(2, '0')}` }),
|
|
261
|
+
default: true
|
|
262
|
+
}]);
|
|
263
|
+
|
|
264
|
+
if (resumeFromSaved) {
|
|
265
|
+
startPosition = savedProgress.position;
|
|
266
|
+
} else {
|
|
267
|
+
clearWatchPosition(animeName, episodeNumber);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(chalk.green(t("player.opening", { name: animeName, episode: episodeNumber, player })));
|
|
273
|
+
|
|
274
|
+
setWatchingActivity({
|
|
275
|
+
animeName,
|
|
276
|
+
animeImage: animeImage || "",
|
|
277
|
+
episode: episodeNumber,
|
|
278
|
+
totalEpisodes
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (player === "mpv") {
|
|
282
|
+
const onPlayerClose = (position, duration) => {
|
|
283
|
+
if (position > 10 && duration > 0) {
|
|
284
|
+
updateWatchPosition(animeName, episodeNumber, position, duration);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
await openInMpv(streamUrl, { startPosition, onClose: onPlayerClose });
|
|
288
|
+
} else {
|
|
289
|
+
await openInVlc(streamUrl);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setActivity(t("menu.browsingMenu"));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* İzleme sonrası işlemler (geçmiş güncelleme, anilist)
|
|
297
|
+
* @param {object} params
|
|
298
|
+
* @param {string} params.animeName
|
|
299
|
+
* @param {number} params.episodeNumber
|
|
300
|
+
* @param {number} params.totalEpisodes
|
|
301
|
+
* @param {number} [params.existingAnilistId]
|
|
302
|
+
*/
|
|
303
|
+
export async function postWatchActions({ animeName, episodeNumber, totalEpisodes, existingAnilistId }) {
|
|
304
|
+
const { watched } = await inquirer.prompt([{
|
|
305
|
+
type: "confirm",
|
|
306
|
+
name: "watched",
|
|
307
|
+
message: t("player.markWatched"),
|
|
308
|
+
default: true
|
|
309
|
+
}]);
|
|
310
|
+
|
|
311
|
+
if (!watched) return;
|
|
312
|
+
|
|
313
|
+
const config = getConfig();
|
|
314
|
+
let anilistId = existingAnilistId;
|
|
315
|
+
|
|
316
|
+
if (config.anilistToken && !anilistId) {
|
|
317
|
+
spinner.start(t("player.anilistSearching"));
|
|
318
|
+
anilistId = await searchAnime(animeName);
|
|
319
|
+
spinner.stop();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
updateHistory(animeName, episodeNumber, totalEpisodes, anilistId);
|
|
323
|
+
console.log(chalk.green(t("player.historyUpdated")));
|
|
324
|
+
|
|
325
|
+
if (config.anilistToken && anilistId) {
|
|
326
|
+
spinner.start(t("player.anilistUpdating"));
|
|
327
|
+
const success = await updateAnilistProgress(anilistId, episodeNumber, episodeNumber >= totalEpisodes);
|
|
328
|
+
spinner.stop();
|
|
329
|
+
if (success) {
|
|
330
|
+
console.log(chalk.green(t("player.anilistUpdated")));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Kuyruğa ekle
|
|
339
|
+
* @param {object} params
|
|
340
|
+
* @param {string} params.animeName
|
|
341
|
+
* @param {Array<any>} params.episodes
|
|
342
|
+
* @param {string} params.dirPath
|
|
343
|
+
* @param {Array<any>} params.downloadQueue
|
|
344
|
+
*/
|
|
345
|
+
export function addToQueue({ animeName, episodes, dirPath, downloadQueue }) {
|
|
346
|
+
const safeAnimeName = animeName.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
347
|
+
|
|
348
|
+
episodes.forEach(ep => {
|
|
349
|
+
downloadQueue.push({
|
|
350
|
+
animeName,
|
|
351
|
+
episode: ep,
|
|
352
|
+
dirPath,
|
|
353
|
+
safeAnimeName
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
saveQueue(downloadQueue);
|
|
358
|
+
console.log(chalk.green(t("download.addedToQueue", { count: episodes.length })));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Bölüm listesi seçim UI'ı oluştur
|
|
363
|
+
* @param {Array<any>} episodes
|
|
364
|
+
* @param {number} lastWatchedEp
|
|
365
|
+
* @param {(ep: any) => {name: string, value: any, disabled?: boolean}} mapFn
|
|
366
|
+
*/
|
|
367
|
+
export function buildEpisodeChoices(episodes, lastWatchedEp, mapFn) {
|
|
368
|
+
return [
|
|
369
|
+
{ name: t("episodes.goBack"), value: "back" },
|
|
370
|
+
new inquirer.Separator(),
|
|
371
|
+
...episodes.map((ep, idx) => {
|
|
372
|
+
const epNum = typeof ep.episode_number === "number" ? ep.episode_number : parseInt(ep.episode_number, 10);
|
|
373
|
+
const isLastWatched = epNum === lastWatchedEp;
|
|
374
|
+
const isNext = epNum === lastWatchedEp + 1;
|
|
375
|
+
|
|
376
|
+
let prefix = " ";
|
|
377
|
+
if (isLastWatched) prefix = chalk.yellow("▶ ");
|
|
378
|
+
else if (isNext) prefix = chalk.green("● ");
|
|
379
|
+
|
|
380
|
+
const mapped = mapFn(ep);
|
|
381
|
+
const suffix = isLastWatched
|
|
382
|
+
? chalk.gray(` (${t("episodes.lastWatched")})`)
|
|
383
|
+
: (isNext ? chalk.gray(` (${t("episodes.nextUp")})`) : "");
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...mapped,
|
|
387
|
+
name: `${prefix}${mapped.name}${suffix}`
|
|
388
|
+
};
|
|
389
|
+
})
|
|
390
|
+
];
|
|
391
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} Source
|
|
4
|
+
* @property {string} name
|
|
5
|
+
* @property {string} id
|
|
6
|
+
* @property {string} [language] - Kaynak dili: "tr" | "en"
|
|
7
|
+
* @property {boolean} supportsLocalSearch - Lokal fuzzy search destekliyor mu
|
|
8
|
+
* @property {() => Promise<import("../jsdoc.js").Anime[]>} [getAnimeList] - Tüm anime listesi (lokal search için)
|
|
9
|
+
* @property {(query: string) => Promise<SearchResult[]>} search - Anime arama
|
|
10
|
+
* @property {(animeId: string) => Promise<Episode[]>} getEpisodes - Bölüm listesi
|
|
11
|
+
* @property {(episodeData: any) => Promise<StreamLink[]>} getStreamLinks - İzleme linkleri
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} SearchResult
|
|
16
|
+
* @property {string} id
|
|
17
|
+
* @property {string} name
|
|
18
|
+
* @property {string} [poster]
|
|
19
|
+
* @property {string} [type]
|
|
20
|
+
* @property {number} [totalEpisodes]
|
|
21
|
+
* @property {string[]} [otherNames]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Episode
|
|
26
|
+
* @property {string} id
|
|
27
|
+
* @property {number|string} episode_number
|
|
28
|
+
* @property {string} [name]
|
|
29
|
+
* @property {number} [season]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} StreamLink
|
|
34
|
+
* @property {string} url
|
|
35
|
+
* @property {string} [quality]
|
|
36
|
+
* @property {string} [label]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { AnimecixSource } from "./animecix.js";
|
|
40
|
+
import { AllAnimeSource } from "./allanime.js";
|
|
41
|
+
|
|
42
|
+
/** @type {Source[]} */
|
|
43
|
+
export const sources = [
|
|
44
|
+
new AnimecixSource(),
|
|
45
|
+
new AllAnimeSource()
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} id
|
|
50
|
+
* @returns {Source|undefined}
|
|
51
|
+
*/
|
|
52
|
+
export function getSourceById(id) {
|
|
53
|
+
return sources.find(s => s.id === id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {Source}
|
|
58
|
+
*/
|
|
59
|
+
export function getDefaultSource() {
|
|
60
|
+
return sources[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Dile göre kaynakları filtrele
|
|
65
|
+
* @param {string} language - "tr" | "en"
|
|
66
|
+
* @returns {Source[]}
|
|
67
|
+
*/
|
|
68
|
+
export function getSourcesByLanguage(language) {
|
|
69
|
+
return sources.filter(s => s.language === language);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Belirli bir dil için varsayılan kaynağı getir
|
|
74
|
+
* @param {string} language - "tr" | "en"
|
|
75
|
+
* @returns {Source}
|
|
76
|
+
*/
|
|
77
|
+
export function getDefaultSourceForLanguage(language) {
|
|
78
|
+
const filtered = getSourcesByLanguage(language);
|
|
79
|
+
return filtered[0] || sources[0];
|
|
80
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { getConfig } from "./storage/config.js";
|
|
7
|
+
import { ANILIST_ID, AUTH_URL } from "../constants.js";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
9
|
+
|
|
10
|
+
const ANILIST_API = "https://graphql.anilist.co";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} token
|
|
14
|
+
* @returns {Promise<string|null>} Username if valid, null otherwise
|
|
15
|
+
*/
|
|
16
|
+
export async function verifyToken(token) {
|
|
17
|
+
const query = `
|
|
18
|
+
query {
|
|
19
|
+
Viewer {
|
|
20
|
+
name
|
|
21
|
+
id
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await axios.post(ANILIST_API, { query }, {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'Accept': 'application/json',
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return response.data.data.Viewer.name;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} search
|
|
42
|
+
* @returns {Promise<number|null>} Media ID
|
|
43
|
+
*/
|
|
44
|
+
export async function searchAnime(search) {
|
|
45
|
+
const query = `
|
|
46
|
+
query ($search: String) {
|
|
47
|
+
Media (search: $search, type: ANIME) {
|
|
48
|
+
id
|
|
49
|
+
title {
|
|
50
|
+
romaji
|
|
51
|
+
english
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await axios.post(ANILIST_API, {
|
|
59
|
+
query,
|
|
60
|
+
variables: { search }
|
|
61
|
+
});
|
|
62
|
+
return response.data.data.Media.id;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {number} mediaId
|
|
70
|
+
* @param {number} progress
|
|
71
|
+
* @param {boolean} completed
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
export async function updateAnilistProgress(mediaId, progress, completed) {
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
if (!config.anilistToken) return false;
|
|
78
|
+
|
|
79
|
+
const query = `
|
|
80
|
+
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
|
81
|
+
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
|
82
|
+
id
|
|
83
|
+
status
|
|
84
|
+
progress
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const variables = {
|
|
90
|
+
mediaId,
|
|
91
|
+
progress,
|
|
92
|
+
status: completed ? "COMPLETED" : "CURRENT"
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await axios.post(ANILIST_API, {
|
|
97
|
+
query,
|
|
98
|
+
variables
|
|
99
|
+
}, {
|
|
100
|
+
headers: {
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
Authorization: `Bearer ${config.anilistToken}`,
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
'Accept': 'application/json',
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red(t("anilist.updateError")), error.response?.data || error.message);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Opens a local server to capture the AniList token
|
|
116
|
+
* @returns {Promise<string>} The access token
|
|
117
|
+
*/
|
|
118
|
+
export function authenticate() {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const server = http.createServer((req, res) => {
|
|
121
|
+
const url = new URL(req.url || "/", "http://localhost:6677");
|
|
122
|
+
|
|
123
|
+
if (url.pathname === "/callback") {
|
|
124
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
125
|
+
res.end(`
|
|
126
|
+
<html>
|
|
127
|
+
<body style="background: #1a1a1a; color: #fff; font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh;">
|
|
128
|
+
<div id="msg">${t("anilist.loggingIn")}</div>
|
|
129
|
+
<script>
|
|
130
|
+
const hash = window.location.hash.substring(1);
|
|
131
|
+
const params = new URLSearchParams(hash);
|
|
132
|
+
const token = params.get('access_token');
|
|
133
|
+
if (token) {
|
|
134
|
+
fetch('/token', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: JSON.stringify({ token }),
|
|
137
|
+
headers: { 'Content-Type': 'application/json' }
|
|
138
|
+
}).then(() => {
|
|
139
|
+
document.getElementById('msg').innerText = '${t("anilist.loginSuccessPage")}';
|
|
140
|
+
window.close();
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
document.getElementById('msg').innerText = '${t("anilist.tokenNotFound")}';
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
</body>
|
|
147
|
+
</html>
|
|
148
|
+
`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (url.pathname === "/token" && req.method === "POST") {
|
|
153
|
+
let body = "";
|
|
154
|
+
req.on("data", chunk => body += chunk);
|
|
155
|
+
req.on("end", () => {
|
|
156
|
+
try {
|
|
157
|
+
const { token } = JSON.parse(body);
|
|
158
|
+
res.writeHead(200);
|
|
159
|
+
res.end();
|
|
160
|
+
server.close();
|
|
161
|
+
resolve(token);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
res.writeHead(400);
|
|
164
|
+
res.end();
|
|
165
|
+
reject(e);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.writeHead(404);
|
|
172
|
+
res.end();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
server.listen(6677, () => {
|
|
176
|
+
const authUrl = AUTH_URL;
|
|
177
|
+
console.log(chalk.cyan("\n" + t("anilist.browserOpening")));
|
|
178
|
+
console.log(chalk.gray(t("anilist.manualLink", { url: authUrl })));
|
|
179
|
+
|
|
180
|
+
const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open');
|
|
181
|
+
if (process.platform === 'win32') {
|
|
182
|
+
exec(`start "" "${authUrl}"`);
|
|
183
|
+
} else {
|
|
184
|
+
exec(`${start} "${authUrl}"`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
server.on("error", (err) => {
|
|
189
|
+
reject(err);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_URL } from "../constants.js";
|
|
3
|
+
import { getCachedAnimeList, saveAnimeListToCache } from "./storage/cache.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {boolean} forceUpdate
|
|
7
|
+
* @returns {Promise<import("../jsdoc.js").Anime[]>}
|
|
8
|
+
*/
|
|
9
|
+
export async function getAnimeList(forceUpdate = false) {
|
|
10
|
+
if (!forceUpdate) {
|
|
11
|
+
const cached = getCachedAnimeList();
|
|
12
|
+
if (cached) {
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await axios.get(`${API_URL}/animes`);
|
|
19
|
+
const animes = response.data;
|
|
20
|
+
|
|
21
|
+
saveAnimeListToCache(animes);
|
|
22
|
+
|
|
23
|
+
return animes;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|