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
package/src/jsdoc.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @typedef {{
3
+ * _id: string;
4
+ * NAME: string;
5
+ * TOTAL_EPISODES: number;
6
+ * FIRST_IMAGE: string;
7
+ * CATEGORIES: string[];
8
+ * DESCRIPTION: string;
9
+ * SLUG: string;
10
+ * SEASON_NUMBER: number;
11
+ * OTHER_NAMES: string[];
12
+ * }} Anime
13
+ */
14
+
15
+ /**
16
+ * @typedef {{
17
+ * id: string;
18
+ * episode_number: number;
19
+ * backblaze_link: string;
20
+ * watch_link_1: string;
21
+ * watch_link_2: string;
22
+ * watch_link_3: string;
23
+ * type: string;
24
+ * fansub: string;
25
+ * }} Episode
26
+ */
27
+
28
+ /**
29
+ * @typedef {{
30
+ * id: string;
31
+ * episode_number: number|string;
32
+ * link: string;
33
+ * }} DownloadEpisode
34
+ */
35
+
36
+ /**
37
+ * @typedef {{
38
+ * id: string;
39
+ * name: string;
40
+ * poster?: string;
41
+ * type?: string;
42
+ * totalEpisodes?: number;
43
+ * otherNames?: string[];
44
+ * _raw?: any;
45
+ * _isMovie?: boolean;
46
+ * }} SearchResult
47
+ */
48
+
49
+ /**
50
+ * @typedef {{
51
+ * id: string;
52
+ * episode_number: number|string;
53
+ * name?: string;
54
+ * season?: number;
55
+ * type?: string;
56
+ * fansub?: string;
57
+ * _links?: string[];
58
+ * _url?: string;
59
+ * _animeId?: string;
60
+ * }} SourceEpisode
61
+ */
62
+
63
+ /**
64
+ * @typedef {{
65
+ * url: string;
66
+ * quality?: string;
67
+ * label?: string;
68
+ * }} StreamLink
69
+ */
70
+
71
+
72
+ export { };
@@ -0,0 +1,195 @@
1
+ // @ts-check
2
+ import axios from "axios";
3
+
4
+ const ALLANIME_API = "https://api.allanime.day";
5
+ const ALLANIME_REFR = "https://allmanga.to";
6
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0";
7
+
8
+ function decodeProviderUrl(encoded) {
9
+ const hexMap = {
10
+ "79": "A", "7a": "B", "7b": "C", "7c": "D", "7d": "E", "7e": "F", "7f": "G",
11
+ "70": "H", "71": "I", "72": "J", "73": "K", "74": "L", "75": "M", "76": "N", "77": "O",
12
+ "68": "P", "69": "Q", "6a": "R", "6b": "S", "6c": "T", "6d": "U", "6e": "V", "6f": "W",
13
+ "60": "X", "61": "Y", "62": "Z",
14
+ "59": "a", "5a": "b", "5b": "c", "5c": "d", "5d": "e", "5e": "f", "5f": "g",
15
+ "50": "h", "51": "i", "52": "j", "53": "k", "54": "l", "55": "m", "56": "n", "57": "o",
16
+ "48": "p", "49": "q", "4a": "r", "4b": "s", "4c": "t", "4d": "u", "4e": "v", "4f": "w",
17
+ "40": "x", "41": "y", "42": "z",
18
+ "08": "0", "09": "1", "0a": "2", "0b": "3", "0c": "4", "0d": "5", "0e": "6", "0f": "7",
19
+ "00": "8", "01": "9",
20
+ "15": "-", "16": ".", "67": "_", "46": "~", "02": ":", "17": "/", "07": "?",
21
+ "1b": "#", "63": "[", "65": "]", "78": "@", "19": "!", "1c": "$", "1e": "&",
22
+ "10": "(", "11": ")", "12": "*", "13": "+", "14": ",", "03": ";", "05": "=", "1d": "%"
23
+ };
24
+
25
+ let result = "";
26
+ for (let i = 0; i < encoded.length; i += 2) {
27
+ const hex = encoded.substring(i, i + 2).toLowerCase();
28
+ result += hexMap[hex] || "";
29
+ }
30
+ return result.replace("/clock", "/clock.json");
31
+ }
32
+
33
+ export class AllAnimeSource {
34
+ constructor() {
35
+ this.name = "AllAnime";
36
+ this.id = "allanime";
37
+ this.language = "en";
38
+ this.supportsLocalSearch = false;
39
+ this.mode = "sub";
40
+ }
41
+
42
+ async _gqlRequest(params) {
43
+ try {
44
+ const response = await axios.get(`${ALLANIME_API}/api`, {
45
+ params,
46
+ headers: {
47
+ "User-Agent": USER_AGENT,
48
+ "Referer": ALLANIME_REFR
49
+ },
50
+ timeout: 15000
51
+ });
52
+ return response.data;
53
+ } catch (e) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async search(query) {
59
+ const searchGql = `query($search: SearchInput $limit: Int $page: Int $translationType: VaildTranslationTypeEnumType $countryOrigin: VaildCountryOriginEnumType) {
60
+ shows(search: $search limit: $limit page: $page translationType: $translationType countryOrigin: $countryOrigin) {
61
+ edges { _id name availableEpisodes __typename }
62
+ }
63
+ }`;
64
+
65
+ const variables = JSON.stringify({
66
+ search: { allowAdult: false, allowUnknown: false, query },
67
+ limit: 40,
68
+ page: 1,
69
+ translationType: this.mode,
70
+ countryOrigin: "ALL"
71
+ });
72
+
73
+ const data = await this._gqlRequest({ variables, query: searchGql });
74
+ if (!data?.data?.shows?.edges) return [];
75
+
76
+ return data.data.shows.edges
77
+ .filter(show => show.availableEpisodes?.[this.mode] > 0)
78
+ .map(show => ({
79
+ id: show._id,
80
+ name: show.name,
81
+ totalEpisodes: show.availableEpisodes?.[this.mode] || 0,
82
+ _mode: this.mode
83
+ }));
84
+ }
85
+
86
+ async getEpisodes(showId) {
87
+ const episodesGql = `query($showId: String!) {
88
+ show(_id: $showId) { _id availableEpisodesDetail }
89
+ }`;
90
+
91
+ const variables = JSON.stringify({ showId });
92
+ const data = await this._gqlRequest({ variables, query: episodesGql });
93
+
94
+ const episodeList = data?.data?.show?.availableEpisodesDetail?.[this.mode];
95
+ if (!episodeList || !Array.isArray(episodeList)) return [];
96
+
97
+ const sorted = [...episodeList].sort((a, b) => parseFloat(a) - parseFloat(b));
98
+
99
+ return sorted.map((epNum, idx) => ({
100
+ id: `${showId}_${epNum}`,
101
+ episode_number: parseFloat(epNum) || idx + 1,
102
+ name: `Episode ${epNum}`,
103
+ _showId: showId,
104
+ _epString: epNum
105
+ }));
106
+ }
107
+
108
+ async _getLinksFromProvider(providerId) {
109
+ try {
110
+ const response = await axios.get(`https://allanime.day${providerId}`, {
111
+ headers: {
112
+ "User-Agent": USER_AGENT,
113
+ "Referer": ALLANIME_REFR
114
+ },
115
+ timeout: 10000
116
+ });
117
+
118
+ const data = response.data;
119
+ const links = [];
120
+
121
+ if (data.links) {
122
+ for (const link of data.links) {
123
+ if (link.hls && link.link) {
124
+ links.push({ quality: link.resolutionStr || "auto", url: link.link, _type: "hls" });
125
+ } else if (link.mp4 && link.link) {
126
+ links.push({ quality: link.resolutionStr || "auto", url: link.link, _type: "mp4" });
127
+ } else if (link.link) {
128
+ links.push({ quality: link.resolutionStr || "auto", url: link.link });
129
+ }
130
+ }
131
+ }
132
+
133
+ return links;
134
+ } catch (e) {
135
+ return [];
136
+ }
137
+ }
138
+
139
+ async getStreamLinks(episodeData) {
140
+ const showId = episodeData._showId;
141
+ const epString = episodeData._epString;
142
+
143
+ if (!showId || !epString) return [];
144
+
145
+ const episodeGql = `query($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) {
146
+ episode(showId: $showId translationType: $translationType episodeString: $episodeString) {
147
+ episodeString sourceUrls
148
+ }
149
+ }`;
150
+
151
+ const variables = JSON.stringify({
152
+ showId,
153
+ translationType: this.mode,
154
+ episodeString: epString
155
+ });
156
+
157
+ const data = await this._gqlRequest({ variables, query: episodeGql });
158
+
159
+ const sourceUrls = data?.data?.episode?.sourceUrls;
160
+ if (!sourceUrls || !Array.isArray(sourceUrls)) return [];
161
+
162
+ const providers = [];
163
+ for (const source of sourceUrls) {
164
+ if (source.sourceUrl && source.sourceName) {
165
+ const encodedUrl = source.sourceUrl.replace("--", "");
166
+ const decodedUrl = decodeProviderUrl(encodedUrl);
167
+ providers.push({ name: source.sourceName, url: decodedUrl });
168
+ }
169
+ }
170
+
171
+ const preferredOrder = ["Luf-Mp4", "Default", "S-mp4", "Yt-mp4"];
172
+ const sortedProviders = providers.sort((a, b) => {
173
+ const aIdx = preferredOrder.indexOf(a.name);
174
+ const bIdx = preferredOrder.indexOf(b.name);
175
+ return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
176
+ });
177
+
178
+ for (const provider of sortedProviders) {
179
+ const links = await this._getLinksFromProvider(provider.url);
180
+ if (links.length > 0) {
181
+ return links.map(l => ({
182
+ url: l.url,
183
+ quality: l.quality,
184
+ label: `${l.quality} (${provider.name})`
185
+ }));
186
+ }
187
+ }
188
+
189
+ return [];
190
+ }
191
+
192
+ setMode(mode) {
193
+ this.mode = mode;
194
+ }
195
+ }
@@ -0,0 +1,223 @@
1
+ // @ts-check
2
+ import axios from "axios";
3
+
4
+ const BASE_URL = "https://animecix.tv/";
5
+ const MANGACIX_URL = "https://mangacix.net";
6
+ const VIDEO_PLAYER = "tau-video.xyz";
7
+
8
+ const HEADERS = {
9
+ "Accept": "application/json",
10
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
11
+ };
12
+
13
+ /**
14
+ * Animecix.tv kaynağı - API üzerinden arama yapar
15
+ */
16
+ export class AnimecixSource {
17
+ constructor() {
18
+ this.name = "Animecix";
19
+ this.id = "animecix";
20
+ this.language = "tr";
21
+ this.supportsLocalSearch = false;
22
+ }
23
+
24
+ /**
25
+ * @param {string} url
26
+ * @returns {Promise<any>}
27
+ */
28
+ async _getJson(url) {
29
+ try {
30
+ const response = await axios.get(url, { headers: HEADERS, timeout: 10000 });
31
+ return response.data;
32
+ } catch (e) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Anime arama (API üzerinden)
39
+ * @param {string} query
40
+ * @returns {Promise<import("./index.js").SearchResult[]>}
41
+ */
42
+ async search(query) {
43
+ const url = `${BASE_URL}secure/search/${encodeURIComponent(query)}?type=&limit=20`;
44
+ const data = await this._getJson(url);
45
+
46
+ if (!data || !data.results) return [];
47
+
48
+ return data.results.map(item => ({
49
+ id: String(item.id),
50
+ name: item.name,
51
+ poster: item.poster,
52
+ type: item.title_type || item.type,
53
+ _isMovie: item.title_type === "movie" || item.type === "movie",
54
+ _originalTitle: item.original_title
55
+ }));
56
+ }
57
+
58
+ /**
59
+ * Sezon listesi
60
+ * @param {string} animeId
61
+ * @returns {Promise<number[]>}
62
+ */
63
+ async _getSeasons(animeId) {
64
+ const url = `${MANGACIX_URL}/secure/related-videos?episode=1&season=1&titleId=${animeId}&videoId=637113`;
65
+ const data = await this._getJson(url);
66
+
67
+ if (!data || !data.videos || !data.videos[0]) return [];
68
+
69
+ const seasons = data.videos[0]?.title?.seasons;
70
+ if (Array.isArray(seasons)) {
71
+ return seasons.map((_, i) => i);
72
+ }
73
+ return [];
74
+ }
75
+
76
+ /**
77
+ * Bölüm listesi
78
+ * @param {string} animeId
79
+ * @returns {Promise<import("./index.js").Episode[]>}
80
+ */
81
+ async getEpisodes(animeId) {
82
+ const seasons = await this._getSeasons(animeId);
83
+
84
+ // Eğer sezon bulunamazsa, tek sezon varsay
85
+ const seasonList = seasons.length > 0 ? seasons : [0];
86
+
87
+ const episodes = [];
88
+ const seenNames = new Set();
89
+
90
+ for (const seasonIndex of seasonList) {
91
+ const url = `${MANGACIX_URL}/secure/related-videos?episode=1&season=${seasonIndex + 1}&titleId=${animeId}&videoId=637113`;
92
+ const data = await this._getJson(url);
93
+
94
+ if (!data || !data.videos) continue;
95
+
96
+ for (const item of data.videos) {
97
+ const name = item.name || "Bilinmeyen";
98
+ if (seenNames.has(name)) continue;
99
+
100
+ seenNames.add(name);
101
+ episodes.push({
102
+ id: `${animeId}_${seasonIndex + 1}_${episodes.length + 1}`,
103
+ episode_number: episodes.length + 1,
104
+ name: name,
105
+ season: item.season_num || seasonIndex + 1,
106
+ _url: item.url // Bu URL'i izleme için kullanacağız
107
+ });
108
+ }
109
+ }
110
+
111
+ return episodes;
112
+ }
113
+
114
+ /**
115
+ * Episode URL'den stream linklerini çıkar (Python'daki fetch_anime_watch_api_url)
116
+ * @param {string} episodeUrl - Episode URL (örn: "izle/anime-adi/bolum-1")
117
+ * @returns {Promise<import("./index.js").StreamLink[]>}
118
+ */
119
+ async _getStreamLinksFromUrl(episodeUrl) {
120
+ try {
121
+ // URL'i tam hale getir
122
+ const fullUrl = episodeUrl.startsWith("http") ? episodeUrl : `${BASE_URL}${episodeUrl}`;
123
+
124
+ // Redirect'i takip et
125
+ const response = await axios.get(fullUrl, {
126
+ headers: HEADERS,
127
+ maxRedirects: 10,
128
+ timeout: 15000,
129
+ validateStatus: () => true
130
+ });
131
+
132
+ const finalUrl = response.request?.res?.responseUrl || response.request?.responseURL || fullUrl;
133
+
134
+ const urlObj = new URL(finalUrl);
135
+ const pathParts = urlObj.pathname.split("/").filter(p => p);
136
+
137
+ let embedId = null;
138
+ for (let i = 0; i < pathParts.length; i++) {
139
+ if (pathParts[i] === "e" || pathParts[i] === "embed") {
140
+ embedId = pathParts[i + 1];
141
+ break;
142
+ }
143
+ }
144
+
145
+ if (!embedId && pathParts.length >= 2) {
146
+ embedId = pathParts[pathParts.length - 1];
147
+ }
148
+
149
+ if (!embedId) {
150
+ console.log("Embed ID not found:", finalUrl);
151
+ return [];
152
+ }
153
+
154
+ const vid = urlObj.searchParams.get("vid");
155
+
156
+ const apiUrl = `https://${VIDEO_PLAYER}/api/video/${embedId}${vid ? `?vid=${vid}` : ""}`;
157
+
158
+ const apiResponse = await axios.get(apiUrl, {
159
+ timeout: 10000,
160
+ headers: {
161
+ "User-Agent": HEADERS["User-Agent"],
162
+ "Referer": finalUrl
163
+ }
164
+ });
165
+
166
+ const urls = apiResponse.data?.urls || [];
167
+
168
+ return urls.map(item => ({
169
+ url: item.url,
170
+ quality: item.label || "default",
171
+ label: item.label
172
+ }));
173
+ } catch (e) {
174
+ console.error("Stream link error:", e.message);
175
+ return [];
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {string} titleId
181
+ * @returns {Promise<import("./index.js").StreamLink[]>}
182
+ */
183
+ async _getMovieStreamLinks(titleId) {
184
+ const url = `${BASE_URL}secure/titles/${titleId}?titleId=${titleId}`;
185
+ const headers = { ...HEADERS, "x-e-h": "=.a" };
186
+
187
+ try {
188
+ const response = await axios.get(url, { headers, timeout: 10000 });
189
+ const data = response.data;
190
+ const videos = data?.title?.videos || [];
191
+
192
+ for (const video of videos) {
193
+ const videoUrl = video.url;
194
+ if (!videoUrl) continue;
195
+
196
+ const streamLinks = await this._getStreamLinksFromUrl(videoUrl);
197
+ if (streamLinks.length > 0) return streamLinks;
198
+ }
199
+ } catch (e) {
200
+ console.error("Movie stream error:", e.message);
201
+ }
202
+
203
+ return [];
204
+ }
205
+
206
+ /**
207
+ * @param {any} episodeData
208
+ * @returns {Promise<import("./index.js").StreamLink[]>}
209
+ */
210
+ async getStreamLinks(episodeData) {
211
+ if (episodeData._isMovie) {
212
+ return this._getMovieStreamLinks(episodeData._animeId);
213
+ }
214
+
215
+ const videoUrl = episodeData._url;
216
+ if (!videoUrl) {
217
+ console.log("Episode URL not found:", episodeData);
218
+ return [];
219
+ }
220
+
221
+ return this._getStreamLinksFromUrl(videoUrl);
222
+ }
223
+ }
@@ -0,0 +1,100 @@
1
+ // @ts-check
2
+ import axios from "axios";
3
+ import { API_URL } from "../constants.js";
4
+ import { getCachedAnimeList, saveAnimeListToCache } from "../utils/storage/cache.js";
5
+
6
+ /**
7
+ * Animely.net
8
+ */
9
+ export class AnimelySource {
10
+ constructor() {
11
+ this.name = "Animely";
12
+ this.id = "animely";
13
+ this.language = "tr";
14
+ this.supportsLocalSearch = true;
15
+ }
16
+
17
+ /**
18
+ * @returns {Promise<import("../jsdoc.js").Anime[]>}
19
+ */
20
+ async getAnimeList() {
21
+ const cached = getCachedAnimeList();
22
+ if (cached) return cached;
23
+
24
+ const response = await axios.get(`${API_URL}/animes`);
25
+ const animes = response.data;
26
+ saveAnimeListToCache(animes);
27
+ return animes;
28
+ }
29
+
30
+ /**
31
+ * @param {string} query
32
+ * @returns {Promise<import("./index.js").SearchResult[]>}
33
+ */
34
+ async search(query) {
35
+ const animes = await this.getAnimeList();
36
+ const lowerQuery = query.toLowerCase().trim();
37
+
38
+ let results = animes.filter(({ NAME, OTHER_NAMES }) => {
39
+ const lowerName = NAME.toLowerCase();
40
+ const lowerOthers = OTHER_NAMES.map(n => n.toLowerCase());
41
+ return lowerName === lowerQuery || lowerOthers.includes(lowerQuery);
42
+ });
43
+
44
+ if (results.length === 0) {
45
+ results = animes.filter(({ NAME, OTHER_NAMES }) => {
46
+ const lowerName = NAME.toLowerCase();
47
+ const lowerOthers = OTHER_NAMES.map(n => n.toLowerCase());
48
+ return lowerName.includes(lowerQuery) || lowerOthers.some(n => n.includes(lowerQuery));
49
+ });
50
+ }
51
+
52
+ if (results.length === 0) {
53
+ results = animes.filter(({ NAME, OTHER_NAMES }) => {
54
+ const allNames = [NAME, ...OTHER_NAMES].map(n => n.toLowerCase());
55
+ const words = lowerQuery.split(" ");
56
+ return allNames.some(name => words.every(word => name.includes(word)));
57
+ });
58
+ }
59
+
60
+ return results.map(anime => ({
61
+ id: anime.SLUG,
62
+ name: anime.NAME,
63
+ poster: anime.FIRST_IMAGE,
64
+ totalEpisodes: anime.TOTAL_EPISODES,
65
+ otherNames: anime.OTHER_NAMES,
66
+ _raw: anime
67
+ }));
68
+ }
69
+
70
+ /**
71
+ * @param {string} animeSlug
72
+ * @returns {Promise<import("./index.js").Episode[]>}
73
+ */
74
+ async getEpisodes(animeSlug) {
75
+ const response = await axios.post(`${API_URL}/searchAnime`, { payload: animeSlug });
76
+ const episodes = response.data.episodes || [];
77
+
78
+ return episodes.map(ep => ({
79
+ id: ep.id,
80
+ episode_number: ep.episode_number,
81
+ name: `${ep.episode_number}. Bölüm`,
82
+ type: ep.type,
83
+ fansub: ep.fansub,
84
+ _links: [ep.backblaze_link, ep.watch_link_1, ep.watch_link_2, ep.watch_link_3]
85
+ }));
86
+ }
87
+
88
+ /**
89
+ * @param {any} episodeData
90
+ * @returns {Promise<import("./index.js").StreamLink[]>}
91
+ */
92
+ async getStreamLinks(episodeData) {
93
+ const links = episodeData._links || [];
94
+ const validLink = links.find(l => l && typeof l === "string" && l.trim() !== "");
95
+
96
+ if (!validLink) return [];
97
+
98
+ return [{ url: validLink, quality: "default" }];
99
+ }
100
+ }