Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions plugins/english/animeAnyway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type PortableBlock =

type SanityVolume = {
title: string;
displayTitle?: string;
series?: string;
seriesUsesRootPath?: boolean;
volkeyword: string;
mainImage?: { asset?: { url?: string } };
banner?: { asset?: { url?: string } };
Expand All @@ -35,22 +38,30 @@ type SanityVolume = {

type SanityAllVolumesEntry = {
title: string;
displayTitle?: string;
series?: string;
seriesUsesRootPath?: boolean;
volkeyword: string;
mainImage?: { asset?: { url?: string } };
releaseDate?: string;
};

type NextData<T> = { props: { pageProps: T } };

type HomePageProps = { allVolumes?: SanityAllVolumesEntry[] };
type VolumePageProps = { vol?: SanityVolume; statusCode?: number };
type VolumePageProps = {
vol?: SanityVolume;
volumeProps?: { 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;
path: string;
name: string;
cover: string;
releaseDate: string;
Expand All @@ -68,7 +79,7 @@ class AnimeAnyway implements Plugin.PluginBase {
* 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;
private readonly yearVolumePattern = /^Year\s+\d+\s+Vol(?:ume)?\.?\s*\d+/i;

/**
* Sanity's image CDN URL for a Portable Text image block, which only carries
Expand All @@ -94,12 +105,32 @@ class AnimeAnyway implements Plugin.PluginBase {
return parsed.props?.pageProps;
}

private displayName(title: string) {
private displayName(title: string, displayTitle?: string) {
const volumeTitle = displayTitle?.trim();
if (volumeTitle && volumeTitle.toLowerCase() !== title.toLowerCase()) {
return `${title}: ${volumeTitle}`;
}

return this.yearVolumePattern.test(title)
? `Classroom of the Elite: ${title}`
: title;
}

/** Supports both the legacy pageProps.vol and current volumeProps.vol. */
private getVolume(data?: VolumePageProps) {
return data?.vol ?? data?.volumeProps?.vol;
}

private getVolumePath(volume: {
series?: string;
seriesUsesRootPath?: boolean;
volkeyword: string;
}) {
return volume.series && volume.seriesUsesRootPath === false
? `${volume.series}/${volume.volkeyword}`
: volume.volkeyword;
Comment on lines +129 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New routes lack test coverage
The series-prefixed path depends on seriesUsesRootPath being exactly false, but no fixture-backed test checks that the catalogue produces nibunnoinochi/v1. The new volumeProps.vol parsing branch is untested too. Tests for both payload shapes and routes would catch a site-data mismatch that lint and type-checking cannot.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. This repository has no dedicated unit-test or fixture harness; its documented validation is the live plugin check. I verified the new nibunnoinochi/v1 route and both payload handling paths against the live site successfully. Fixture coverage could be added separately if the project adopts a test framework.

}

/**
* The catalogue is the homepage's own volume list, one novel per volume —
* each is published as its own book on the site, so that's what's exposed
Expand All @@ -113,10 +144,10 @@ class AnimeAnyway implements Plugin.PluginBase {
const volumes = home?.allVolumes ?? [];

return volumes.map(volume => ({
volkeyword: volume.volkeyword,
name: this.displayName(volume.title),
path: this.getVolumePath(volume),
name: this.displayName(volume.title, volume.displayTitle),
cover: volume.mainImage?.asset?.url ?? defaultCover,
releaseDate: '',
releaseDate: volume.releaseDate ?? '',
}));
})();

Expand All @@ -138,14 +169,15 @@ class AnimeAnyway implements Plugin.PluginBase {
let ordered = catalogue;

if (showLatestNovels) {
// Release dates aren't on the homepage listing, only on each volume
// page — the catalogue is small, so fetch them all to sort "latest".
// Older catalogue payloads omit release dates, so fill only those gaps.
await Promise.all(
catalogue.map(async entry => {
const vol = await this.fetchPageData<VolumePageProps>(
entry.volkeyword,
if (entry.releaseDate) return;

const vol = this.getVolume(
await this.fetchPageData<VolumePageProps>(entry.path),
);
entry.releaseDate = vol?.vol?.releaseDate ?? '';
entry.releaseDate = vol?.releaseDate ?? '';
}),
);
ordered = [...catalogue].sort((a, b) =>
Expand All @@ -155,7 +187,7 @@ class AnimeAnyway implements Plugin.PluginBase {

return ordered.map(entry => ({
name: entry.name,
path: entry.volkeyword,
path: entry.path,
cover: entry.cover,
}));
}
Expand Down Expand Up @@ -196,7 +228,7 @@ class AnimeAnyway implements Plugin.PluginBase {
.sort((a, b) => b.score - a.score)
.map(({ entry }) => ({
name: entry.name,
path: entry.volkeyword,
path: entry.path,
cover: entry.cover,
}));
}
Expand Down Expand Up @@ -265,13 +297,13 @@ class AnimeAnyway implements Plugin.PluginBase {
};

const data = await this.fetchPageData<VolumePageProps>(novelPath);
const vol = data?.vol;
const vol = this.getVolume(data);
if (!vol) {
novel.summary = 'This novel is not available on Anime Anyway.';
return novel;
}

novel.name = this.displayName(vol.title);
novel.name = this.displayName(vol.title, vol.displayTitle);
novel.cover = vol.mainImage?.asset?.url ?? defaultCover;
novel.summary = this.plainTextFromPortableText(vol.synopsis);
novel.chapters = (vol.chapters ?? []).map((chapter, index) => ({
Expand Down
36 changes: 36 additions & 0 deletions specs/anime-anyway-site-schema-fix.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Anime Anyway site-schema fix

## Requirements & Goals

- Restore Anime Anyway support in LNReader after the site changed its volume-page payload shape.
- Allow the new `Nibunnoinochi` volume and its first chapter to appear, open, and render in LNReader.
- Preserve compatibility with existing Anime Anyway volumes and the previous payload shape where practical.
- Keep the adapter's existing one-novel-per-volume catalogue behavior.

## Inputs, Outputs & Behavior

- Read the homepage `__NEXT_DATA__` catalogue, including the current `displayTitle` and `releaseDate` fields when present.
- Read volume pages from either the legacy `pageProps.vol` location or the current `pageProps.volumeProps.vol` location.
- Use the series title and volume display title to create a distinct, searchable LNReader novel name. Continue recognizing legacy `Year N Vol. M` titles.
- Use the normalized volume payload to populate the novel cover, synopsis, status, and ordered chapter list.
- Continue reading chapter content from `pageProps.chapter` and resolving chapter URLs using the site's real path format.
- Use homepage release dates for latest sorting and fall back to volume-page release dates when the homepage omits them.

## Edge Cases & Error Handling

- A missing or malformed volume payload must keep the existing unavailable-novel response rather than throwing.
- A volume with no `displayTitle` must still use its legacy title correctly.
- A legacy page with only `pageProps.vol` must continue to parse.
- Missing covers, synopsis, release dates, or chapter arrays must use the adapter's existing defaults and empty results.
- Unknown Portable Text block types must remain safely ignored.
- The adapter must not merge distinct volumes or construct synthetic paths.

## Acceptance Criteria

- [ ] `popularNovels(1, ...)` includes `Nibunnoinochi` Volume 1 with path `nibunnoinochi/v1` and a useful distinct name.
- [ ] `searchNovels('Nibunnoinochi', 1)` returns the new volume.
- [ ] `parseNovel('nibunnoinochi/v1')` returns the new volume name, cover, synopsis, and one chapter named `Prologue: The Place Where I’m to Die`.
- [ ] `parseChapter('nibunnoinochi/v1/prologue')` returns rendered content longer than the live-check minimum.
- [ ] An existing volume such as `y3v4` still returns its full chapter list and latest sorting remains functional.
- [ ] Type-checking, linting/format checks, and the live plugin check pass.
- [ ] The final change is committed with a conventional commit message and prepared for the repository's normal pull-request workflow.
Loading