diff --git a/anify-backend/src/mapping/impl/anime/animeflix.ts b/anify-backend/src/mapping/impl/anime/animeflix.ts index e625810..e55af0a 100644 --- a/anify-backend/src/mapping/impl/anime/animeflix.ts +++ b/anify-backend/src/mapping/impl/anime/animeflix.ts @@ -51,13 +51,7 @@ export default class AnimeFlix extends AnimeProvider { // /idtoinfo?ids=[] // /episodes?id=fuufu-ijou-koibito-miman&dub=false&a=j43o4d4d3o4d1j4142474d1j4347413k414c471j4541453j46 - const hash = this.generateRandomStringWithSameLength("j43o4d4d3o4d1j4142474d1j4347413k414c471j4541453j46"); - - const test = await this.request(`${this.api}/episodes?id=${id}&dub=false&a=${hash}`, { - headers: { - "User-Agent": this.userAgent - }, - }); + const hash = this.generateHash(id); const [dataResponse, dubResponse] = await Promise.all([ this.request(`${this.api}/episodes?id=${id}&dub=false&a=${hash}`, { @@ -78,8 +72,6 @@ export default class AnimeFlix extends AnimeProvider { const [data, dubData] = await Promise.all([dataResponse.json(), dubResponse.json()]); - console.log("yeah baby"); - const dubNumbers = new Set((dubData?.episodes ?? []).map((dub) => dub.number)); const results: Episode[] = data.episodes.map((res) => ({ @@ -129,16 +121,18 @@ export default class AnimeFlix extends AnimeProvider { return await new Extractor(data.source, result).extract(server); } - private generateRandomStringWithSameLength(source: string): string { - const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - - for (let i = 0; i < source.length; i++) { - const randomIndex = Math.floor(Math.random() * characters.length); - result += characters[randomIndex]; + private generateHash(source: string): string { + const currentDate = new Date(); + const averageMonthAndDate = 9 + (currentDate.getUTCDate() + currentDate.getUTCMonth()) / 2; + let result = "j4"; + + const inputLength = source.length; + for (let i = 0; i < inputLength; i++) { + const charCode = source.charCodeAt(i); + const encodedValue = charCode.toString(Math.floor(averageMonthAndDate)); + result += encodedValue; } - + return result; - } - + } } diff --git a/anify-backend/src/mapping/impl/anime/bilibili.ts b/anify-backend/src/mapping/impl/anime/bilibili.ts new file mode 100644 index 0000000..830543e --- /dev/null +++ b/anify-backend/src/mapping/impl/anime/bilibili.ts @@ -0,0 +1,174 @@ +import AnimeProvider, { Episode, Source, StreamingServers, SubType } from "."; +import { Format, Formats, Result } from "../.."; +import { load } from "cheerio"; +import Extractor from "@/src/helper/extractor"; + +export default class Bilibili extends AnimeProvider { + override rateLimit = 250; + override id = "bilibili"; + override url = "https://bilibili.com"; + + private api = "https://api.bilibili.tv/intl/gateway/web"; + + override formats: Format[] = [Format.MOVIE, Format.ONA, Format.OVA, Format.SPECIAL, Format.TV, Format.TV_SHORT]; + + override get subTypes(): SubType[] { + return [SubType.SUB]; + } + + override get headers(): Record | undefined { + return { Referer: "https://kwik.cx" }; + } + + override async search(query: string, format?: Format, year?: number): Promise { + const request = await this.request(`${this.api}/v2/search?keyword=${encodeURIComponent(query)}&platform=web&pn=1&ps=20&qid=&s_locale=en_US`); + if (!request.ok) { + console.log(await request.text()) + return []; + } + const data = await request.json(); + const results: Result[] = []; + + console.log(data) + + if (!data?.data) { + return []; + } + + data.data.map((item) => { + const formatString: string = item.type.toUpperCase(); + const f: Format = Formats.includes(formatString as Format) ? (formatString as Format) : Format.UNKNOWN; + + results.push({ + id: String(item.id) ?? item.session, + title: item.title, + year: item.year, + img: item.poster, + format: f, + altTitles: [], + providerId: this.id, + }); + }); + + return results; + } + + override async fetchEpisodes(id: string): Promise { + const episodes: Episode[] = []; + + const req = await (await this.request(`${this.url}${id.includes("-") ? `/anime/${id}` : `/a/${id}`}`)).text(); + + const $ = load(req); + + const tempId = $("head > meta[property='og:url']").attr("content")!.split("/").pop()!; + const { last_page, data } = await (await this.request(`${this.url}/api?m=release&id=${tempId}&sort=episode_asc&page=1`)).json(); + + data.map((item) => { + const updatedAt = new Date(item.created_at ?? Date.now()).getTime(); + + episodes.push({ + id: item.id + "-" + id, + number: item.episode, + title: item.title && item.title.length > 0 ? item.title : "Episode " + item.episode, + img: item.snapshot, + isFiller: item.filler === 1, + hasDub: false, + updatedAt, + }); + }); + + const pageNumbers = Array.from({ length: last_page - 1 }, (_, i) => i + 2); + + const promises = pageNumbers.map((pageNumber) => this.request(`${this.url}/api?m=release&id=${tempId}&sort=episode_asc&page=${pageNumber}`).then((res) => res.json())); + const results = await Promise.all(promises); + + results.forEach((showData) => { + for (const data of showData.data) { + if (data) { + const updatedAt = new Date(data.created_at ?? Date.now()).getTime(); + + episodes.push({ + id: data.id + "-" + id, + number: data.episode, + title: data.title && data.title.length > 0 ? data.title : "Episode " + data.episode, + img: data.snapshot, + isFiller: data.filler === 1, + hasDub: false, + updatedAt, + }); + } + } + }); + (data as any[]).sort((a, b) => a.number - b.number); + return episodes; + } + + override async fetchSources(id: string, subType: SubType = SubType.SUB, server: StreamingServers = StreamingServers.Kwik): Promise { + const animeId = id.split("-").pop()!; + const episodeId = id.split("-")[0]; + + const req = await this.request(`${this.url}${animeId.includes("-") ? `/anime/${animeId}` : `/a/${animeId}`}`); + + try { + const url = req.url; + // Need session id to fetch the watch page + const sessionId = url.split("/anime/").pop()!; + + const $ = load(await req.text()); + const tempId = $("head > meta[property='og:url']").attr("content")!.split("/").pop()!; + const { last_page, data } = await (await this.request(`${this.url}/api?m=release&id=${tempId}&sort=episode_asc&page=1`)).json(); + + let episodeSession = ""; + + for (let i = 0; i < data.length; i++) { + if (String(data[i].id) === episodeId) { + episodeSession = data[i].session; + break; + } + } + + if (episodeSession === "") { + for (let i = 1; i < last_page; i++) { + const data = await (await this.request(`${this.url}/api?m=release&id=${tempId}&sort=episode_asc&page=${i + 1}`)).json(); + + for (let j = 0; j < data.length; j++) { + if (String(data[j].id) === episodeId) { + episodeSession = data[j].session; + break; + } + } + + if (episodeSession !== "") break; + } + } + + if (episodeSession === "") return undefined; + + const watchReq = await (await this.request(`${this.url}/play/${sessionId}/${episodeSession}`)).text(); + + const regex = /https:\/\/kwik\.cx\/e\/\w+/g; + const matches = watchReq.match(regex); + + if (matches === null) return undefined; + + const result: Source = { + sources: [], + subtitles: [], + intro: { + start: 0, + end: 0, + }, + outro: { + start: 0, + end: 0, + }, + headers: this.headers ?? {}, + }; + + return await new Extractor(matches[0], result).extract(server); + } catch (e) { + console.error(e); + return undefined; + } + } +} diff --git a/anify-backend/src/mapping/index.ts b/anify-backend/src/mapping/index.ts index 8e17f28..35a9942 100644 --- a/anify-backend/src/mapping/index.ts +++ b/anify-backend/src/mapping/index.ts @@ -28,8 +28,9 @@ import SimklMeta from "./impl/meta/simkl"; import Simkl from "./impl/information/simkl"; import ColoredManga from "./impl/manga/coloredmanga"; import AnimeFlix from "./impl/anime/animeflix"; +import Bilibili from "./impl/anime/bilibili"; -const ANIME_PROVIDERS: AnimeProvider[] = [new NineAnime(), new GogoAnime(), new Zoro(), new AnimePahe()]; +const ANIME_PROVIDERS: AnimeProvider[] = [new NineAnime(), new GogoAnime(), new Zoro(), new AnimePahe(), new Bilibili()]; const animeProviders: Record = ANIME_PROVIDERS.reduce((acc, provider) => { acc[provider.id] = provider; return acc; diff --git a/anify-backend/src/test.ts b/anify-backend/src/test.ts new file mode 100644 index 0000000..6cdbbe8 --- /dev/null +++ b/anify-backend/src/test.ts @@ -0,0 +1,86 @@ +import dotenv from "dotenv"; +dotenv.config(); + +import queues from "./worker"; +import emitter, { Events } from "@/src/helper/event"; +import { start } from "./server/server"; +import Database from "./database"; +import { MediaStatus, animeProviders } from "./mapping"; +import { fetchCorsProxies } from "./helper/proxies"; + +emitter.on(Events.COMPLETED_SEASONAL_LOAD, async (data) => { + for (let i = 0; i < data.trending.length; i++) { + if (data.trending[i].status === MediaStatus.NOT_YET_RELEASED) { + continue; + } + const existing = await Database.info(String(data.trending[i].aniListId)); + if (!existing) { + queues.mappingQueue.add({ id: data.trending[i].aniListId, type: data.trending[i].type }); + } + } + + for (let i = 0; i < data.popular.length; i++) { + if (data.popular[i].status === MediaStatus.NOT_YET_RELEASED) { + continue; + } + const existing = await Database.info(String(data.popular[i].aniListId)); + if (!existing) queues.mappingQueue.add({ id: data.popular[i].aniListId, type: data.popular[i].type }); + } + + for (let i = 0; i < data.top.length; i++) { + if (data.top[i].status === MediaStatus.NOT_YET_RELEASED) { + continue; + } + const existing = await Database.info(String(data.top[i].aniListId)); + if (!existing) queues.mappingQueue.add({ id: data.top[i].aniListId, type: data.top[i].type }); + } + + for (let i = 0; i < data.seasonal.length; i++) { + if (data.seasonal[i].status === MediaStatus.NOT_YET_RELEASED) { + continue; + } + const existing = await Database.info(String(data.seasonal[i].aniListId)); + if (!existing) queues.mappingQueue.add({ id: data.seasonal[i].aniListId, type: data.seasonal[i].type }); + } +}); + +emitter.on(Events.COMPLETED_MAPPING_LOAD, async (data) => { + for (let i = 0; i < data.length; i++) { + if (await Database.info(String(data[i].aniListId))) { + continue; + } + queues.createEntry.add({ toInsert: data[i], type: data[i].type }); + } +}); + +emitter.on(Events.COMPLETED_SEARCH_LOAD, (data) => { + if (data[0]?.aniListId) { + for (let i = 0; i < data.length; i++) { + if (data[i].status === MediaStatus.NOT_YET_RELEASED) { + continue; + } + queues.mappingQueue.add({ id: data[i].aniListId, type: data[i].type }); + } + } +}); + +// Todo: For inserting all skip times, merge the episodescrape repo so that it adds a bunch of events lol +emitter.on(Events.COMPLETED_SKIPTIMES_LOAD, (data) => { + // Do something +}); + +emitter.on(Events.COMPLETED_PAGE_UPLOAD, (data) => { + // Do something +}); + +queues.seasonQueue.start(); +queues.mappingQueue.start(); +queues.createEntry.start(); +queues.searchQueue.start(); +queues.skipTimes.start(); +queues.uploadPages.start(); + +fetchCorsProxies().then(async () => { + await Database.initializeDatabase(); + await animeProviders.bilibili.search("Mushoku Tensei").then(console.log) +}); diff --git a/package.json b/package.json index 33e22c9..f39e066 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,11 @@ "description": "The best Japanese media web application.", "private": true, "scripts": { - "test": "cd anify-backend && npm run test", "start": "cd anify-manager && npm run dev", - "install": "cd anify-manager && npm i && cd ../anify-frontend && npm i && cd ../anify-backend && npm i", - "lint": "cd anify-frontend && npx next lint && cd ../anify-backend && npm run lint", + "install": "cd anify-manager && npm i && cd ../anify-frontend && npm i && cd ../anify-backend && npm i && cd ../anify-auth && npm i", + "lint": "cd anify-frontend && npx next lint && cd ../anify-backend && npm run lint && cd ../anify-auth && npm run lint", "build": "npm run install && npm run build:ts && npm run build:db", - "build:ts": "cd anify-manager && npm run build:ts && cd ../anify-backend && npm run build:ts && cd ../anify-frontend && npm run build", - "build:db": "cd anify-frontend && npm run db:generate && npm run db:push && npm run db:validate && npm run build && cd ../anify-backend && npm run db:generate && npm run db:push && npm run db:validate", - "redis:flush": "redis-cli flushall", - "db:generate": "npx prisma generate", - "db:push": "npx prisma db push", - "db:validate": "npx prisma validate" + "build:ts": "cd anify-manager && npm run build:ts && cd ../anify-backend && npm run build:ts && cd ../anify-frontend && npm run build && cd ../anify-auth && npm run build", + "build:db": "cd anify-frontend && npm run build:db && cd ../anify-backend && npm run build:db && cd ../anify-auth && npm run build:db" } }