From 16337a78a0040e4631cf80b0b01a0652acae7c5e Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 12 Aug 2026 16:54:18 -0600 Subject: [PATCH 1/2] feat: add Anime Anyway plugin (english) Reads the site's embedded Next.js page data (__NEXT_DATA__) instead of scraping rendered HTML: volume/chapter lists and chapter bodies (Sanity Portable Text) come straight from the JSON the site itself was built from. Volumes titled "Year N Vol. M" are grouped into one novel per year since no single page represents an entire year; everything else is its own standalone novel. --- plugins/english/animeAnyway.ts | 379 ++++++++++++++++++++++ public/static/src/en/animeanyway/icon.png | Bin 0 -> 3449 bytes 2 files changed, 379 insertions(+) create mode 100644 plugins/english/animeAnyway.ts create mode 100644 public/static/src/en/animeanyway/icon.png diff --git a/plugins/english/animeAnyway.ts b/plugins/english/animeAnyway.ts new file mode 100644 index 000000000..b3e00b7a2 --- /dev/null +++ b/plugins/english/animeAnyway.ts @@ -0,0 +1,379 @@ +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; +}; + +/** Groups the "Year N Vol. M" family of volumes into one novel per year. */ +type CatalogueEntry = { + novelPath: string; + name: string; + cover: string; + /** volkeywords, ascending by volume number for grouped entries */ + members: 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'; + + 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; + /** novelPath -> real site URL of its newest member volume, for resolveUrl. */ + private novelUrlCache = new Map(); + + /** 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( + /