import { fetchApi } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { defaultCover } from '@libs/defaultCover'; import { NovelStatus } from '@libs/novelStatus'; type PortableSpan = { _type: 'span'; text: string; marks?: string[]; }; type PortableBlock = | { _type: 'block'; style?: string; children?: PortableSpan[]; } | { _type: 'horizontalLine' } | { _type: 'image'; asset?: { _ref?: string } } | { _type: string }; type SanityVolume = { title: string; volkeyword: string; mainImage?: { asset?: { url?: string } }; banner?: { asset?: { url?: string } }; progress?: number; releaseDate?: string; synopsis?: PortableBlock[]; chapters?: { chkeyword: string; title: string; }[]; }; type SanityAllVolumesEntry = { title: string; volkeyword: string; mainImage?: { asset?: { url?: string } }; }; type NextData = { props: { pageProps: T } }; type HomePageProps = { allVolumes?: SanityAllVolumesEntry[] }; type VolumePageProps = { vol?: SanityVolume; statusCode?: number }; type ChapterPageProps = { chapter?: { content?: PortableBlock[] }; statusCode?: number; }; /** One novel per volume — each is published as its own distinct book. */ type CatalogueEntry = { volkeyword: string; name: string; cover: string; releaseDate: string; }; class AnimeAnyway implements Plugin.PluginBase { id = 'animeanyway'; name = 'Anime Anyway'; icon = 'src/en/animeanyway/icon.png'; site = 'https://animeanyway.com/'; version = '1.0.0'; /** * The site's own volume titles ("Year 3 Vol. 4") don't name the franchise, * unlike "High School Syndrome" which is already self-explanatory — prefix * just the former so both are recognisable in a novel list. */ private readonly yearVolumePattern = /^Year\s+\d+\s+Vol\.?\s*\d+/i; /** * Sanity's image CDN URL for a Portable Text image block, which only carries * an asset reference ("image--x-"), not a resolved URL — * unlike mainImage/banner elsewhere on the site, which come pre-resolved. * Project id and dataset are this site's own fixed identifiers. */ private readonly sanityImageBase = 'https://cdn.sanity.io/images/m1xj6lbt/production/'; private cataloguePromise: Promise | null = null; /** Extracts the Next.js page data every server-rendered page embeds. */ private async fetchPageData(path: string): Promise { const response = await fetchApi(`${this.site}${path}`); const html = await response.text(); const match = html.match( /