Back to Home
Gistify Snippet
kyiov
3d ago·2 file(s)
Public

Plugin Stream anime

#javascript#scraper#anime
13
Raw
anime.jsjavascript
257 lines
1
2import { SamehadakuScraper } from '#scraper/samehadaku.js'
3import * as AnimeIndo from '#scraper/animeindo.js'
4import { searchAnimeLovers, getAnimeLoversDetail, getAnimeLoversStream } from '#system/lib/scraper/animelovers.js'
5
6const ANIME_ALIASES = {
7 'tenkosaki': 'tenkou',
8 'tenkousaki': 'tenkou',
9 'roshidere': 'alya',
10 'alya-san': 'alya',
11 'alya san': 'alya',
12 'bokuyaba': 'boku no kokoro',
13 'kimetsu': 'demon slayer',
14 'kny': 'demon slayer',
15 'snk': 'shingeki no kyojin',
16 'aot': 'shingeki no kyojin',
17 'jjk': 'jujutsu kaisen',
18 'mha': 'my hero academia',
19 'op': 'one piece',
20 'jojos': 'jojo',
21 'frieren': 'sousou no frieren',
22 'sololev': 'solo leveling',
23 'chainsaw': 'chainsaw man',
24 'csm': 'chainsaw man',
25 'slime': 'tensei shitara slime'
26}
27
28async function samehadakuSearch(query) {
29 try {
30 const s = new SamehadakuScraper()
31 const res = await s.search(query)
32 return (res.data || []).map(r => ({
33 source: 'samehadaku',
34 title: r.title,
35 url: r.url,
36 image: r.thumbnail || '',
37 rating: r.score || '',
38 type: r.type || 'TV',
39 }))
40 } catch {
41 return []
42 }
43}
44
45async function animeIndoSearch(query) {
46 try {
47 const res = await AnimeIndo.searchAnime(query)
48 return (res || []).map(r => ({
49 source: 'animeindo',
50 title: r.title,
51 url: r.url,
52 image: r.image || '',
53 type: r.type || '',
54 }))
55 } catch {
56 return []
57 }
58}
59
60async function animeLoversSearchEngine(query) {
61 try {
62 const qClean = query.toLowerCase().trim()
63 const alias = ANIME_ALIASES[qClean] || (Object.keys(ANIME_ALIASES).find(k => qClean.includes(k)) ? ANIME_ALIASES[Object.keys(ANIME_ALIASES).find(k => qClean.includes(k))] : null)
64
65 let list = await searchAnimeLovers(query)
66 if (!list.length && alias) {
67 list = await searchAnimeLovers(alias)
68 }
69
70 if (!list.length && qClean.length >= 6) {
71 const rootWord = qClean.slice(0, 6).replace(/saki$/, '').replace(/ko$/, 'kou')
72 if (rootWord !== qClean) {
73 list = await searchAnimeLovers(rootWord)
74 }
75 }
76
77 return (list || []).map(r => ({
78 source: 'animelovers',
79 title: r.title,
80 url: r.slug,
81 image: r.cover || '',
82 rating: r.score || '',
83 status: r.status || '',
84 type: 'TV'
85 }))
86 } catch {
87 return []
88 }
89}
90
91export async function searchAnime(query) {
92 const [animelovers, samehadaku, animeindo] = await Promise.allSettled([
93 animeLoversSearchEngine(query),
94 samehadakuSearch(query),
95 animeIndoSearch(query),
96 ])
97
98 const results = [
99 ...(animelovers.status === 'fulfilled' ? animelovers.value : []),
100 ...(samehadaku.status === 'fulfilled' ? samehadaku.value : []),
101 ...(animeindo.status === 'fulfilled' ? animeindo.value : []),
102 ]
103
104 const seen = new Set()
105 return results.filter(r => {
106 if (!r.url || seen.has(r.url)) return false
107 seen.add(r.url)
108 return true
109 })
110}
111
112export async function getAnimeDetail(source, url) {
113 if (source === 'animelovers') {
114 const d = await getAnimeLoversDetail(url)
115 return {
116 source,
117 title: d.title,
118 image: d.cover || '',
119 rating: d.rating || '',
120 status: d.status || '',
121 studio: d.author || '',
122 genres: (d.genres || []).join(', '),
123 synopsis: d.synopsis || '',
124 episodes: (d.episodes || []).map(e => ({
125 number: parseInt(String(e.episode).replace(/\D/g, '')) || null,
126 title: `Episode ${e.episode}`,
127 url: `${url}|${e.episode}`,
128 date: e.date || ''
129 }))
130 }
131 }
132
133 if (source === 'samehadaku') {
134 const s = new SamehadakuScraper()
135 const d = await s.anime(url)
136 return {
137 source,
138 title: d.title,
139 image: d.poster || '',
140 rating: d.score || '',
141 status: d.status || '',
142 studio: d.metadata?.studio || '',
143 genres: d.metadata?.genre || '',
144 synopsis: d.synopsis || '',
145 episodes: (d.episodes || []).map(e => ({
146 number: parseInt(String(e.title).replace(/\D/g, '')) || null,
147 title: e.title,
148 url: e.url,
149 date: e.date || '',
150 })),
151 }
152 }
153
154 if (source === 'animeindo') {
155 const d = await AnimeIndo.getAnimeDetail(url)
156 return {
157 source,
158 title: d.title,
159 image: d.image || '',
160 rating: '',
161 status: '',
162 studio: '',
163 genres: d.genres || [],
164 synopsis: d.synopsis || '',
165 episodes: (d.episodes || []).map(e => ({
166 number: e.number,
167 title: e.title,
168 url: e.url,
169 date: e.date || '',
170 })),
171 }
172 }
173
174 throw new Error(`Detail tidak didukung untuk sumber ${source}.`)
175}
176
177export async function getEpisode(source, url, chosenResolution = null) {
178 if (source === 'animelovers') {
179 const [slug, epNum] = url.split('|')
180 const stream = await getAnimeLoversStream(slug, epNum || '1')
181
182 const availableResolutions = Object.keys(stream.streams || {}).filter(
183 r => Array.isArray(stream.streams[r]) && stream.streams[r].length > 0
184 )
185
186 let targetReso = chosenResolution
187 if (!targetReso || !stream.streams[targetReso]?.length) {
188 targetReso = ['720p', '480p', '360p', '1080p'].find(r => stream.streams[r]?.length) || availableResolutions[0]
189 }
190
191 const servers = stream.streams[targetReso] || []
192 const directVideos = servers.map(s => s.url).filter(Boolean)
193
194 const allLinks = []
195 for (const resKey of availableResolutions) {
196 for (const srv of stream.streams[resKey] || []) {
197 if (srv.url) allLinks.push(srv.url)
198 }
199 }
200
201 return {
202 source,
203 title: `${stream.title} Episode ${stream.episode}`,
204 image: stream.cover,
205 episodeNumber: String(stream.episode).match(/\d+/)?.[0] || '1',
206 selectedResolution: targetReso,
207 availableResolutions,
208 streamsByResolution: stream.streams,
209 directVideo: directVideos[0] || allLinks[0] || null,
210 downloadLinks: directVideos.length ? directVideos : allLinks
211 }
212 }
213
214 if (source === 'samehadaku') {
215 const s = new SamehadakuScraper()
216 const ep = await s.episode(url)
217
218 const qualityLinks = []
219 for (const [format, sections] of Object.entries(ep.downloads || {})) {
220 for (const sec of sections) {
221 for (const mirror of (sec.mirrors || [])) {
222 if (mirror.url) qualityLinks.push(mirror.url)
223 }
224 }
225 }
226
227 return {
228 source,
229 title: ep.title,
230 selectedResolution: '720p',
231 availableResolutions: ['360p', '480p', '720p'],
232 directVideo: ep.iframe || qualityLinks[0] || null,
233 downloadLinks: qualityLinks.length ? qualityLinks : (ep.downloadLinks || [])
234 }
235 }
236
237 if (source === 'animeindo') {
238 const ep = await AnimeIndo.getEpisode(url)
239 return {
240 source,
241 title: ep.title,
242 selectedResolution: '720p',
243 availableResolutions: ['360p', '480p', '720p'],
244 directVideo: ep.iframe || null,
245 downloadLinks: (ep.downloadLinks || []).map(l => l.url).filter(Boolean)
246 }
247 }
248
249 throw new Error(`Episode tidak didukung untuk sumber ${source}.`)
250}
251
252export default {
253 searchAnime,
254 getAnimeDetail,
255 getEpisode
256}
257
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.