From 9f22349271d00d1217e520c6b5a9ce888bd96d1f Mon Sep 17 00:00:00 2001 From: Aykhan Date: Wed, 3 Sep 2025 18:07:22 +0400 Subject: [PATCH] add myanime --- src/providers/all.ts | 6 +++ src/providers/embeds/myanimedub.ts | 61 +++++++++++++++++++++++ src/providers/embeds/myanimesub.ts | 61 +++++++++++++++++++++++ src/providers/sources/myanime.ts | 77 ++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 src/providers/embeds/myanimedub.ts create mode 100644 src/providers/embeds/myanimesub.ts create mode 100644 src/providers/sources/myanime.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index 5231ab1..b08424a 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -27,6 +27,8 @@ import { cinemaosEmbeds } from './embeds/cinemaos'; import { closeLoadScraper } from './embeds/closeload'; import { madplayBaseEmbed, madplayNsapiEmbed, madplayNsapiVidFastEmbed, madplayRoperEmbed } from './embeds/madplay'; import { mp4hydraServer1Scraper, mp4hydraServer2Scraper } from './embeds/mp4hydra'; +import { myanimedubScraper } from './embeds/myanimedub'; +import { myanimesubScraper } from './embeds/myanimesub'; import { ridooScraper } from './embeds/ridoo'; import { streamtapeLatinoScraper, streamtapeScraper } from './embeds/streamtape'; import { streamvidScraper } from './embeds/streamvid'; @@ -76,6 +78,7 @@ import { iosmirrorScraper } from './sources/iosmirror'; import { iosmirrorPVScraper } from './sources/iosmirrorpv'; import { lookmovieScraper } from './sources/lookmovie'; import { madplayScraper } from './sources/madplay'; +import { myanimeScraper } from './sources/myanime'; import { nunflixScraper } from './sources/nunflix'; import { rgshowsScraper } from './sources/rgshows'; import { ridooMoviesScraper } from './sources/ridomovies'; @@ -99,6 +102,7 @@ export function gatherAllSources(): Array { insertunitScraper, soaperTvScraper, autoembedScraper, + myanimeScraper, tugaflixScraper, ee3Scraper, fsharetvScraper, @@ -187,5 +191,7 @@ export function gatherAllEmbeds(): Array { vidnestAllmoviesEmbed, vidnestFlixhqEmbed, vidnestOfficialEmbed, + myanimesubScraper, + myanimedubScraper, ]; } diff --git a/src/providers/embeds/myanimedub.ts b/src/providers/embeds/myanimedub.ts new file mode 100644 index 0000000..5784293 --- /dev/null +++ b/src/providers/embeds/myanimedub.ts @@ -0,0 +1,61 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +export const myanimedubScraper = makeEmbed({ + id: 'myanimedub', + name: 'MyAnime (Dub)', + rank: 205, + async scrape(ctx) { + const streamData = await ctx.proxiedFetcher( + `https://anime.aether.mom/api/stream?id=${ctx.url}&server=HD-2&type=dub`, + ); + + if (!streamData.results.streamingLink?.link?.file) { + throw new NotFoundError('No watchable sources found'); + } + + const getValidTimestamp = (timestamp: any) => { + if (!timestamp || typeof timestamp !== 'object') return null; + const start = parseInt(timestamp.start, 10); + const end = parseInt(timestamp.end, 10); + if (Number.isNaN(start) || Number.isNaN(end) || start <= 0 || end <= 0 || start >= end) return null; + return { start, end }; + }; + + const intro = getValidTimestamp(streamData.results.streamingLink.intro); + const outro = getValidTimestamp(streamData.results.streamingLink.outro); + + return { + stream: [ + { + id: 'dub', + type: 'hls', + playlist: createM3U8ProxyUrl(streamData.results.streamingLink.link.file, { + Referer: 'https://rapid-cloud.co/', + }), + flags: [flags.CORS_ALLOWED], + captions: + (streamData.results.streamingLink.tracks + ?.map((track: any) => { + const lang = labelToLanguageCode(track.label); + const type = getCaptionTypeFromUrl(track.file); + if (!lang || !type) return null; + return { + id: track.file, + url: track.file, + language: lang, + type, + hasCorsRestrictions: true, + }; + }) + .filter((x: any) => x) as Caption[]) ?? [], + intro, + outro, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/myanimesub.ts b/src/providers/embeds/myanimesub.ts new file mode 100644 index 0000000..b392762 --- /dev/null +++ b/src/providers/embeds/myanimesub.ts @@ -0,0 +1,61 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions'; +import { NotFoundError } from '@/utils/errors'; +import { createM3U8ProxyUrl } from '@/utils/proxy'; + +export const myanimesubScraper = makeEmbed({ + id: 'myanimesub', + name: 'MyAnime (Sub)', + rank: 204, + async scrape(ctx) { + const streamData = await ctx.proxiedFetcher( + `https://anime.aether.mom/api/stream?id=${ctx.url}&server=HD-2&type=sub`, + ); + + if (!streamData.results.streamingLink?.link?.file) { + throw new NotFoundError('No watchable sources found'); + } + + const getValidTimestamp = (timestamp: any) => { + if (!timestamp || typeof timestamp !== 'object') return null; + const start = parseInt(timestamp.start, 10); + const end = parseInt(timestamp.end, 10); + if (Number.isNaN(start) || Number.isNaN(end) || start <= 0 || end <= 0 || start >= end) return null; + return { start, end }; + }; + + const intro = getValidTimestamp(streamData.results.streamingLink.intro); + const outro = getValidTimestamp(streamData.results.streamingLink.outro); + + return { + stream: [ + { + id: 'sub', + type: 'hls', + playlist: createM3U8ProxyUrl(streamData.results.streamingLink.link.file, { + Referer: 'https://rapid-cloud.co/', + }), + flags: [flags.CORS_ALLOWED], + captions: + (streamData.results.streamingLink.tracks + ?.map((track: any) => { + const lang = labelToLanguageCode(track.label); + const type = getCaptionTypeFromUrl(track.file); + if (!lang || !type) return null; + return { + id: track.file, + url: track.file, + language: lang, + type, + hasCorsRestrictions: true, + }; + }) + .filter((x: any) => x) as Caption[]) ?? [], + intro, + outro, + }, + ], + }; + }, +}); diff --git a/src/providers/sources/myanime.ts b/src/providers/sources/myanime.ts new file mode 100644 index 0000000..0aced0e --- /dev/null +++ b/src/providers/sources/myanime.ts @@ -0,0 +1,77 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { compareTitle } from '@/utils/compare'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +// Levenshtein distance function for string similarity +const levenshtein = (s: string, t: string): number => { + if (!s.length) return t.length; + if (!t.length) return s.length; + const arr: number[][] = []; + for (let i = 0; i <= t.length; i++) { + arr[i] = [i]; + for (let j = 1; j <= s.length; j++) { + arr[i][j] = + i === 0 + ? j + : Math.min(arr[i - 1][j] + 1, arr[i][j - 1] + 1, arr[i - 1][j - 1] + (s[j - 1] === t[i - 1] ? 0 : 1)); + } + } + return arr[t.length][s.length]; +}; + +const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext): Promise => { + const searchResults = await ctx.proxiedFetcher( + `https://anime-api-cyan-zeta.vercel.app/api/search?keyword=${encodeURIComponent(ctx.media.title)}`, + ); + + const bestMatch = searchResults.results.data + .map((item: any) => { + const similarity = + 1 - levenshtein(item.title, ctx.media.title) / Math.max(item.title.length, ctx.media.title.length); + const isExactMatch = compareTitle(item.title, ctx.media.title); + return { ...item, similarity, isExactMatch }; + }) + .sort((a: any, b: any) => { + if (a.isExactMatch && !b.isExactMatch) return -1; + if (!a.isExactMatch && b.isExactMatch) return 1; + return b.similarity - a.similarity; + })[0]; + + if (!bestMatch) { + throw new NotFoundError('No watchable sources found'); + } + + const episodeData = await ctx.proxiedFetcher(`https://anime.aether.mom/api/episodes/${bestMatch.id}`); + + const episode = episodeData.results.episodes.find( + (e: any) => e.episode_no === (ctx.media.type === 'show' ? ctx.media.episode.number : 1), + ); + + if (!episode) { + throw new NotFoundError('No watchable sources found'); + } + + return { + embeds: [ + { + embedId: 'myanimesub', + url: episode.id, + }, + { + embedId: 'myanimedub', + url: episode.id, + }, + ], + }; +}; + +export const myanimeScraper = makeSourcerer({ + id: 'myanime', + name: 'MyAnime', + rank: 101, + flags: [flags.CORS_ALLOWED], + scrapeMovie: universalScraper, + scrapeShow: universalScraper, +});