diff --git a/bun.lockb b/bun.lockb index 2604a0d..f249445 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 028fbb1..f3368f0 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "prettier-plugin-toml": "^2.0.4", "ts-morph": "^22.0.0", "typescript": "^5.8.3", - "wrangler": "^4.13.0", + "wrangler": "^4.28.1", "zx": "8.1.5" }, "lint-staged": { diff --git a/src/controllers/auth/anilist/index.ts b/src/controllers/auth/anilist/index.ts index e3bd194..a6b7e56 100644 --- a/src/controllers/auth/anilist/index.ts +++ b/src/controllers/auth/anilist/index.ts @@ -4,11 +4,10 @@ import { streamSSE } from "hono/streaming"; import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; -import { sleep } from "~/libs/sleep"; import { associateDeviceIdWithUsername } from "~/models/token"; import { setWatchStatus } from "~/models/watchStatus"; import type { Env } from "~/types/env"; -import { Episode, EpisodesResponseSchema } from "~/types/episode"; +import { EpisodesResponseSchema } from "~/types/episode"; import { ErrorResponse, ErrorResponseSchema } from "~/types/schema"; import { Title } from "~/types/title"; @@ -186,13 +185,8 @@ app.openapi(route, async (c) => { continue; } - await fetchEpisodes( - media.id, - { ...env(c, "workerd"), ENABLE_ANIFY: "false" }, - true, - ).then(({ result: episodesResult }) => { - const episodes = episodesResult?.episodes; - if (!episodes) { + await fetchEpisodes(media.id, true).then(({ episodes }) => { + if (episodes.length === 0) { return; } diff --git a/src/controllers/episodes/getByAniListId/aniwatch.ts b/src/controllers/episodes/getByAniListId/aniwatch.ts index fdfc4b6..094b93b 100644 --- a/src/controllers/episodes/getByAniListId/aniwatch.ts +++ b/src/controllers/episodes/getByAniListId/aniwatch.ts @@ -97,12 +97,12 @@ async function fetchEpisodes( .then( (res) => res.json() as Promise<{ - success: boolean; + status: number; data: AniwatchEpisodesResponse; }>, ) - .then(({ success, data }) => { - if (!success || data.totalEpisodes === 0) { + .then(({ status, data }) => { + if (status >= 300 || data.totalEpisodes === 0) { console.error( `Error trying to load episodes from aniwatch; aniListId: ${aniListId}, totalEpisodes: ${data.totalEpisodes}`, ); @@ -164,12 +164,12 @@ function getAniwatchId( } const json = (await res.value.json()) as { - success: boolean; + status: number; data: AniwatchSearchResponse; }; const currentValue = await current; return { - success: currentValue.success || json.success, + success: currentValue.success || json.status === 200, data: { ...currentValue.data, animes: [ diff --git a/src/controllers/episodes/getByAniListId/consumet.ts b/src/controllers/episodes/getByAniListId/consumet.ts deleted file mode 100644 index 4801295..0000000 --- a/src/controllers/episodes/getByAniListId/consumet.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { aniList } from "~/consumet"; - -import { Episode, EpisodesResponse } from "./episode"; - -export async function getEpisodesFromConsumet( - aniListId: number, -): Promise { - try { - const episodes: Episode[] = await aniList - .fetchEpisodesListById(aniListId.toString()) - .then((episodes) => - episodes.map( - ({ id, number, title, image: img, description }): Episode => ({ - id, - number, - title, - img, - description, - rating: undefined, - updatedAt: 0, - }), - ), - ); - if (!episodes || episodes.length === 0) { - return null; - } - - return { providerId: "consumet", episodes }; - } catch (error: any) { - if (!error.message.includes("failed with status code")) { - console.error( - `Error trying to load episodes from consumet; aniListId: ${aniListId}`, - ); - console.error(error); - } - } - - return null; -} diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts index 71f95c1..f7f790c 100644 --- a/src/controllers/episodes/getByAniListId/index.ts +++ b/src/controllers/episodes/getByAniListId/index.ts @@ -1,17 +1,13 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { env } from "hono/adapter"; -import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import type { Env } from "~/types/env"; -import { EpisodesResponse, EpisodesResponseSchema } from "~/types/episode"; +import { EpisodesResponseSchema } from "~/types/episode"; import { AniListIdQuerySchema, ErrorResponse, ErrorResponseSchema, } from "~/types/schema"; -import { getEpisodesFromAnify } from "./anify"; - const route = createRoute({ tags: ["aniplay", "episodes"], summary: "Fetch episodes for a title", @@ -43,61 +39,25 @@ const route = createRoute({ const app = new OpenAPIHono(); -export function fetchEpisodesFromAllProviders( - aniListId: number, - env: Env, -): Promise { - return Promise.allSettled([ - import("./aniwatch").then(({ getEpisodesFromAniwatch }) => - getEpisodesFromAniwatch(aniListId), - ), - getEpisodesFromAnify(env, aniListId), - ]).then((episodeResults) => - episodeResults - .filter((result) => result.status === "fulfilled") - .map((result) => result.value) - .filter((episodes) => !!episodes), - ); -} - -export function fetchEpisodes( - aniListId: number, - env: Env, - shouldRetry: boolean = false, -) { - return fetchFromMultipleSources([ - () => - import("./aniwatch").then(({ getEpisodesFromAniwatch }) => - getEpisodesFromAniwatch(aniListId, shouldRetry), - ), - () => getEpisodesFromAnify(env, aniListId), - // () => - // import("./consumet").then(({ getEpisodesFromConsumet }) => - // getEpisodesFromConsumet(aniListId), - // ), - ]); +export function fetchEpisodes(aniListId: number, shouldRetry: boolean = false) { + return import("./aniwatch") + .then(({ getEpisodesFromAniwatch }) => + getEpisodesFromAniwatch(aniListId, shouldRetry), + ) + .then((episodeResults) => episodeResults?.episodes ?? []); } app.openapi(route, async (c) => { const aniListId = Number(c.req.param("aniListId")); - const { result, errorOccurred } = await fetchEpisodes( - aniListId, - env(c, "workerd"), - ); - - if (errorOccurred || !result) { - return c.json(ErrorResponse, { status: 500 }); - } - - const { episodes, providerId } = result; - if (!episodes || episodes.length === 0) { + const episodes = await fetchEpisodes(aniListId); + if (episodes.length === 0) { return c.json(ErrorResponse, { status: 404 }); } return c.json({ success: true, - result: { providerId, episodes }, + result: { providerId: "aniwatch", episodes }, }); }); diff --git a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts index dbdb0fd..28ab614 100644 --- a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts +++ b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts @@ -36,12 +36,12 @@ async function getEpisodeUrl(watchId: string, server?: string) { .then( (res) => res.json() as Promise<{ - success: boolean; + status: number; data: AniwatchEpisodeUrlResponse; }>, ) - .then(({ success, data }) => { - if (!success || !data.sources || data.sources.length === 0) { + .then(({ status, data }) => { + if (status >= 300 || !data.sources || data.sources.length === 0) { return { source: null }; } @@ -50,9 +50,10 @@ async function getEpisodeUrl(watchId: string, server?: string) { intro: convertSkipTime(intro), outro: convertSkipTime(outro), source: sources[0].url, - subtitles: tracks - .filter(({ kind }) => kind === "captions") - .map(({ file, label }) => ({ url: file, lang: label ?? "" })), + subtitles: tracks.map(({ url, lang }) => ({ + url, + lang, + })), headers, }; }); @@ -79,8 +80,8 @@ async function getEpisodeServers(watchId: string) { ) .then((res) => res.json() as Promise) .then((res) => { - if (!res.success) { - throw new Error(res.message); + if (res.status >= 300 || !res.data) { + throw new Error("Failed to fetch episode servers"); } return res; @@ -105,21 +106,16 @@ interface Source { } interface Track { - file: string; - label?: string; + url: string; + lang?: string; kind: string; default?: boolean; } -type AniwatchEpisodeServersResponse = - | { - success: true; - data: AniwatchEpisodeServers; - } - | { - success: false; - message: string; - }; +interface AniwatchEpisodeServersResponse { + status: number; + data: AniwatchEpisodeServers; +} interface AniwatchEpisodeServers { sub: AniwatchEpisodeServer[]; diff --git a/src/controllers/episodes/getEpisodeUrl/consumet.ts b/src/controllers/episodes/getEpisodeUrl/consumet.ts deleted file mode 100644 index 45ca582..0000000 --- a/src/controllers/episodes/getEpisodeUrl/consumet.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { aniList } from "~/consumet"; -import { sortByProperty } from "~/libs/sortByProperty"; -import type { FetchUrlResponse } from "~/types/episode/fetch-url-response"; - -import { qualityPriority, subtitlesPriority } from "./priorities"; - -export async function getSourcesFromConsumet( - watchId: string, -): Promise { - try { - const { sources, subtitles, intro, outro } = - await aniList.fetchEpisodeSources(watchId); - - if (sources.length === 0) { - return null; - } - - const source = sources.sort(sortByProperty(qualityPriority, "quality"))[0] - ?.url; - subtitles?.sort(sortByProperty(subtitlesPriority, "lang")); - - return { - source, - subtitles: subtitles ?? [], - audio: [], - intro: intro ? [intro.start, intro.end] : undefined, - outro: outro ? [outro.start, outro.end] : undefined, - }; - } catch (error) { - if (error.message === "Episode not found.") { - return null; - } - - throw error; - } -} diff --git a/src/controllers/episodes/getEpisodeUrl/index.spec.ts b/src/controllers/episodes/getEpisodeUrl/index.spec.ts index 483536b..adb69d3 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.spec.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.spec.ts @@ -6,14 +6,13 @@ import { server } from "~/mocks"; server.listen(); describe('requests the "/episodes/:id/url" route', () => { - it("with sources from Anify", async () => { + it("with sources from Aniwatch", async () => { const response = await app.request( - "/episodes/1/url", + "/episodes/4/url", { method: "POST", body: JSON.stringify({ - provider: "anify", - id: "/ore-dake-level-up-na-ken-episode-2", + episodeNumber: 1, }), headers: { "Content-Type": "application/json" }, }, @@ -26,19 +25,18 @@ describe('requests the "/episodes/:id/url" route', () => { success: true, result: { source: - "https://proxy.anify.tv/video/8CLGIJg8G3k%252BH%252BYV9xyOYVGZ8al8uZqqtbXk44wKRco%252BGATkCrqlkgdRiam3owmOU4f2MAB89GOblOuZbxifwbGsjvp32uxhRC4kZVYrWnZmP%252FrLxtqwi0n6zY%252BvrffUh6dbg6DADSLCWhd2bNUUIg%253D%253D/%7B%7D/.m3u8", + "https://www032.vipanicdn.net/streamhls/aa804a2400535d84dd59454b28d329fb/ep.1.1712504065.m3u8", subtitles: [], audio: [], }, }); }); - it("with no URL from Anify source", async () => { + it("with no URL from Aniwatch source", async () => { const response = await app.request("/episodes/-1/url", { method: "POST", body: JSON.stringify({ - provider: "anify", - id: "/ore-dake-level-up-na-ken-episode-2", + episodeNumber: -1, }), headers: { "Content-Type": "application/json" }, }); @@ -46,85 +44,4 @@ describe('requests the "/episodes/:id/url" route', () => { expect(response.json()).resolves.toEqual({ success: false }); expect(response.status).toBe(404); }); - - it("with sources from Consumet", async () => { - const response = await app.request( - "/episodes/1/url", - { - method: "POST", - body: JSON.stringify({ - provider: "consumet", - id: "/ore-dake-level-up-na-ken-episode-2", - }), - headers: { "Content-Type": "application/json" }, - }, - { - ENABLE_ANIFY: "true", - }, - ); - - expect(response.json()).resolves.toEqual({ - success: true, - result: { - source: "https://consumet.com", - subtitles: [], - audio: [], - }, - }); - }); - - it("with no URL from Consumet source", async () => { - const response = await app.request("/episodes/-1/url", { - method: "POST", - body: JSON.stringify({ - provider: "consumet", - id: "unknown", - }), - headers: { "Content-Type": "application/json" }, - }); - - expect(response.json()).resolves.toEqual({ success: false }); - expect(response.status).toBe(404); - }); - - // it("with sources from Aniwatch", async () => { - // const response = await app.request( - // "/episodes/1/url", - // { - // method: "POST", - // body: JSON.stringify({ - // provider: "aniwatch", - // id: "ore-dake-level-up-na-ken-episode-2", - // }), - // headers: { "Content-Type": "application/json" }, - // }, - // { - // ENABLE_ANIFY: "true", - // }, - // ); - - // expect(response.json()).resolves.toEqual({ - // success: true, - // result: { - // source: - // "https://www032.vipanicdn.net/streamhls/aa804a2400535d84dd59454b28d329fb/ep.1.1712504065.m3u8", - // subtitles: [], - // audio: [], - // }, - // }); - // }); - - // it("with no URL from Aniwatch source", async () => { - // const response = await app.request("/episodes/-1/url", { - // method: "POST", - // body: JSON.stringify({ - // provider: "aniwatch", - // id: "unknown", - // }), - // headers: { "Content-Type": "application/json" }, - // }); - - // expect(response.json()).resolves.toEqual({ success: false }); - // expect(response.status).toBe(404); - // }); }); diff --git a/src/controllers/episodes/getEpisodeUrl/index.ts b/src/controllers/episodes/getEpisodeUrl/index.ts index 77d5bab..9b3d7c0 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.ts @@ -1,9 +1,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { env } from "hono/adapter"; -import { readEnvVariable } from "~/libs/readEnvVariable"; import type { Env } from "~/types/env"; -import type { Episode } from "~/types/episode"; import { FetchUrlResponse } from "~/types/episode/fetch-url-response"; import { AniListIdQuerySchema, @@ -12,12 +9,9 @@ import { ErrorResponseSchema, } from "~/types/schema"; -import { fetchEpisodesFromAllProviders } from "../getByAniListId"; +import { fetchEpisodes } from "../getByAniListId"; -const FetchUrlRequest = z.union([ - z.object({ id: z.string(), provider: z.string() }), - z.object({ episodeNumber: EpisodeNumberSchema }), -]); +const FetchUrlRequest = z.object({ episodeNumber: EpisodeNumberSchema }); const route = createRoute({ tags: ["aniplay", "episodes"], @@ -73,89 +67,40 @@ const route = createRoute({ const app = new OpenAPIHono(); -export async function fetchEpisodeUrlFromAllProviders( - aniListId: number, - episodeNumber: number, - env: Env, -) { - const results = await fetchEpisodesFromAllProviders(aniListId, env); - if (results.length === 0) { - return { episodes: null, fetchUrlResult: null }; - } - - let episodes: Episode[] | null = null; - let fetchUrlResult: FetchUrlResponse | null = null; - - for (const { episodes: episodesResult, providerId } of results) { - const episode = episodesResult.find( - (episode) => episode.number === episodeNumber, - ); - if (!episode) { - continue; - } - episodes = episodesResult; - - const urlResult = await fetchEpisodeUrl( - providerId, - episode.id, - aniListId, - readEnvVariable(env, "ENABLE_ANIFY"), - ); - if (!urlResult) { - episodes = null; - continue; - } - - fetchUrlResult = urlResult; - break; - } - - return { episodes, fetchUrlResult }; -} - -export async function fetchEpisodeUrl( - provider: string, - id: string, - aniListId: number, - isAnifyEnabled: boolean, -): Promise { - if (provider === "consumet" || !isAnifyEnabled) { - try { - const result = await import("./consumet").then( - ({ getSourcesFromConsumet }) => getSourcesFromConsumet(id), - ); - if (!result) { - return null; - } - - return result; - } catch (e) { - console.error("Failed to fetch download URL from Consumet", e); - - throw e; - } - } - - if (provider === "aniwatch") { - try { - const result = await import("./aniwatch").then( - ({ getSourcesFromAniwatch }) => getSourcesFromAniwatch(id), - ); - if (!result) { - return null; - } - - return result; - } catch (e) { - console.error("Failed to fetch download URL from Aniwatch", e); - - throw e; - } - } - +export async function fetchEpisodeUrl({ + id, + aniListId, + episodeNumber, +}: + | { id: string; aniListId?: number; episodeNumber?: number } + | { + id?: string; + aniListId: number; + episodeNumber: number; + }): Promise { try { - const result = await import("./anify").then(({ getSourcesFromAnify }) => - getSourcesFromAnify(provider, id, aniListId), + let episodeId = id; + if (!id) { + const episodes = await fetchEpisodes(aniListId!); + if (episodes.length === 0) { + console.error(`Failed to fetch episodes for title ${aniListId}`); + return null; + } + const episode = episodes.find( + (episode) => episode.number === episodeNumber, + ); + if (!episode) { + console.error( + `Episode ${episodeNumber} not found for title ${aniListId}`, + ); + return null; + } + + episodeId = episode.id; + } + + const result = await import("./aniwatch").then( + ({ getSourcesFromAniwatch }) => getSourcesFromAniwatch(episodeId!), ); if (!result) { return null; @@ -163,7 +108,7 @@ export async function fetchEpisodeUrl( return result; } catch (e) { - console.error("Failed to fetch download URL from Anify", e); + console.error("Failed to fetch download URL from Aniwatch", e); throw e; } @@ -171,37 +116,21 @@ export async function fetchEpisodeUrl( app.openapi(route, async (c) => { const aniListId = Number(c.req.param("aniListId")); - const { provider, id, episodeNumber } = - await c.req.json(); - if (!provider && episodeNumber == undefined) { + const { episodeNumber } = await c.req.json(); + if (episodeNumber == undefined) { return c.json(ErrorResponse, { status: 400 }); } try { - const isAnifyEnabled = readEnvVariable(c.env, "ENABLE_ANIFY"); - let result: FetchUrlResponse | null; - if (provider) { - console.log(`Fetching sources from ${provider} for ${aniListId}`); - result = await fetchEpisodeUrl(provider, id, aniListId, isAnifyEnabled); - } else { - console.log(`Fetching sources from all providers for ${aniListId}`); - const { fetchUrlResult } = await fetchEpisodeUrlFromAllProviders( - aniListId, - episodeNumber!, - env(c, "workerd"), - ); - if (!fetchUrlResult) { - return c.json(ErrorResponse, { status: 404 }); - } - - result = fetchUrlResult; - } - - if (result) { - return c.json({ success: true, result }); - } else { + console.log( + `Fetching episode URL for aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`, + ); + const fetchUrlResult = await fetchEpisodeUrl({ aniListId, episodeNumber }); + if (!fetchUrlResult) { return c.json(ErrorResponse, { status: 404 }); } + + return c.json({ success: true, result: fetchUrlResult }); } catch (error) { return c.json(ErrorResponse, { status: 500 }); } diff --git a/src/controllers/internal/new-episode/index.ts b/src/controllers/internal/new-episode/index.ts index 10c2826..25f9011 100644 --- a/src/controllers/internal/new-episode/index.ts +++ b/src/controllers/internal/new-episode/index.ts @@ -3,7 +3,7 @@ import { Hono } from "hono"; import { env } from "hono/adapter"; import { z } from "zod"; -import { fetchEpisodeUrlFromAllProviders } from "~/controllers/episodes/getEpisodeUrl"; +import { fetchEpisodeUrl } from "~/controllers/episodes/getEpisodeUrl"; import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; @@ -44,20 +44,7 @@ app.post( ); } - const { episodes, fetchUrlResult } = await fetchEpisodeUrlFromAllProviders( - aniListId, - episodeNumber, - env(c, "workerd"), - ); - - if (!episodes) { - console.error(`Failed to fetch episodes for title ${aniListId}`); - return c.json( - { success: false, message: "Failed to fetch episodes" }, - 500, - ); - } - + const fetchUrlResult = await fetchEpisodeUrl({ aniListId, episodeNumber }); if (!fetchUrlResult) { console.error(`Failed to fetch episode URL for episode`); return c.json( diff --git a/src/index.ts b/src/index.ts index 392442c..332cae7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,3 +66,10 @@ app.doc("/openapi.json", { app.get("/docs", swaggerUI({ url: "/openapi.json" })); export default app; + +export class AniwatchApiContainer /* extends Container */ { + // Port the container listens on (default: 8080) + defaultPort = 4444; + // Time before container sleeps due to inactivity (default: 30s) + sleepAfter = "2m"; +} diff --git a/wrangler.toml b/wrangler.toml index 0390810..0170230 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,7 +6,7 @@ compatibility_date = "2024-09-23" [vars] TURSO_URL = "libsql://aniplay-v2-silverandroid.turso.io" -ENABLE_ANIFY = true +ENABLE_ANIFY = false [observability] enabled = true