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,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
+ }