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,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,4 @@
1
+ // @ts-check
2
+ export { handleAnimecix } from "./animecix.js";
3
+ export { handleAllAnime } from "./allanime.js";
4
+ export * from "./base.js";
@@ -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
+ }