Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/mangaplus/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mangaplus",
"version": "1.2.0",
"version": "1.2.1",
"publoader_api": "^2.0.0",
"runtime": "node",
"entrypoint": "index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion src/mangaplus/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "publoader-ext-mangaplus",
"version": "1.2.0",
"version": "1.2.1",
"private": true,
"type": "module",
"description": "publoader extension for MangaPlus by Shueisha (extension API v2)",
Expand Down
27 changes: 27 additions & 0 deletions src/mangaplus/src/expiry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { hasExpired } from "./normalise.ts";

const NOW = 1_757_332_800; // 2026-09-08T12:00:00Z, epoch seconds.

test("a stated expiry in the past has closed", () => {
assert.equal(hasExpired(NOW - 86_400, NOW), true);
});

test("a stated expiry in the future has not", () => {
assert.equal(hasExpired(NOW + 86_400, NOW), false);
});

test("an absent expiry never expires", () => {
// The regression this guards: proto3 drops zero values, so a chapter that
// never rotates out arrives with endTimeStamp absent. It used to be stood in
// as DEFAULT_TIMESTAMP (epoch second 1), which read as "expired in 1970" --
// skipping the chapter from every upload, and, once the platform started
// hard-deleting still-listed expired chapters as paywalled, marking the
// permanently-free chapters for deletion.
assert.equal(hasExpired(null, NOW), false);
});

test("an expiry exactly at now has not closed yet", () => {
assert.equal(hasExpired(NOW, NOW), false);
});
14 changes: 11 additions & 3 deletions src/mangaplus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type PbTitle,
} from "./proto";
import {
hasExpired,
normaliseChapters,
numberWordsPattern,
type OverrideOptions,
Expand Down Expand Up @@ -187,7 +188,7 @@ function isoFromEpoch(seconds: number): string | null {
function toChapterInput(chapter: RawChapter): ChapterInput {
return {
chapterTimestamp: isoFromEpoch(chapter.chapterTimestamp),
chapterExpire: isoFromEpoch(chapter.chapterExpire),
chapterExpire: chapter.chapterExpire === null ? null : isoFromEpoch(chapter.chapterExpire),
chapterLanguage: chapter.chapterLanguage,
chapterNumber: chapter.chapterNumber,
chapterTitle: chapter.chapterTitle,
Expand Down Expand Up @@ -310,7 +311,7 @@ class MangaPlus implements ExtensionRuntime {
// MangaPlus rotates free chapters out; anything already expired, or
// older than the update window, is not a new upload.
if (postedChapterIds.has(chapter.chapterId)) continue;
if (chapter.chapterExpire < now) continue;
if (hasExpired(chapter.chapterExpire, now)) continue;
if (chapter.chapterTimestamp < now - UPDATE_WINDOW_SECONDS) continue;
updatedChapters.push(chapter);
}
Expand Down Expand Up @@ -880,7 +881,14 @@ class MangaPlus implements ExtensionRuntime {
chapterId,
chapterUrl: CHAPTER_URL(chapterId),
chapterTimestamp: chapter.startTimeStamp ?? DEFAULT_TIMESTAMP,
chapterExpire: chapter.endTimeStamp ?? DEFAULT_TIMESTAMP,
// null, NOT DEFAULT_TIMESTAMP: a missing endTimeStamp means "no expiry
// known", and proto3 drops zero values, so a chapter that never expires
// arrives here with the field absent. Standing that in as epoch second
// 1 reads as "expired in 1970" — which skipped the chapter from every
// upload below, and now that the platform deletes still-listed expired
// chapters as paywalled, would delete exactly the chapters that are
// free forever. Unknown has to stay unknown.
chapterExpire: chapter.endTimeStamp ?? null,
chapterTitle: chapter.subTitle ?? null,
chapterNumber: chapter.name ?? null,
chapterLanguage: manga.language,
Expand Down
19 changes: 18 additions & 1 deletion src/mangaplus/src/normalise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export interface RawChapter {
listSlot?: "first" | "mid" | "last";
chapterUrl: string;
chapterTimestamp: number;
chapterExpire: number;
/** null when the publisher states no expiry, i.e. the chapter stays free. */
chapterExpire: number | null;
chapterTitle: string | null;
chapterNumber: string | null;
chapterLanguage: string;
Expand Down Expand Up @@ -82,6 +83,22 @@ export interface OverrideOptions {
verify_pages?: boolean;
}

/**
* Has the publisher's free window closed?
*
* Only a stated expiry can close it. A null expiry means MANGA Plus named no
* end date — proto3 drops zero values, so a chapter that never rotates out
* arrives with the field absent — and "unknown" must not read as "expired".
* Getting this backwards skips the chapter from every upload, and, because the
* platform now hard-deletes still-listed expired chapters as paywalled, would
* delete the chapters that are free forever.
*
* Both arguments are epoch SECONDS, matching the wire format.
*/
export function hasExpired(expire: number | null, now: number): boolean {
return expire !== null && expire < now;
}

/** publoader's chapter-number validator (utils/utils.py). */
const CHAPTER_NUMBER_RE = /^(0|[1-9]\d*)((\.\d+){1,2})?[a-z]?$/i;

Expand Down
Loading