feat(book-requests): link releases to their source page and hold imports for review

Torznab and Newznab read the feed's details page, plugins may send infoUrl, and the
search drops any link that could carry a credential. A grab can ask to be held for
review whatever its score, and the held review shows the file's own edition facts,
its cover and the release page. Torznab guids are now digested like Newznab's.

Refs #1438
This commit is contained in:
neon
2026-10-20 16:42:16 +00:00
parent 3c87418416
commit a728bf293b
44 changed files with 25375 additions and 68 deletions
+6
View File
@@ -99,6 +99,12 @@ export interface PluginReleaseCandidate {
downloadUrl?: string;
magnet?: string;
infoHash?: string;
/**
* The release's page on the source, for an approver to read before sending it. An http(s) URL
* that needs no credential: the host drops one carrying the indexer's credential or a key-like
* query parameter rather than showing it. Ignored by hosts older than the field.
*/
infoUrl?: string;
sizeBytes: number | null;
/**
* Null means the source reported no swarm counts, which must not be read as zero: the
+5
View File
@@ -20,6 +20,11 @@ export const SERVER_FEATURES = [
"server-connection-identity",
/** `GET /books/:id/annotations` and `/bookmarks` accept `readerFileId` and return positions for that file. */
"reader-file-positions",
/**
* `POST .../grab` accepts `holdForReview`, release candidates carry `infoUrl`, and a held
* review reports `heldOnRequest`, `releaseInfoUrl`, `edition` and `hasCover` with a cover route.
*/
"book-request-review-hold",
] as const;
export type ServerFeature = (typeof SERVER_FEATURES)[number];
+31
View File
@@ -33,6 +33,26 @@ export interface BookRequestVerification {
rows: BookRequestVerificationRow[];
}
/**
* What the imported file says about itself, for telling one edition from another. The comparison
* is about the work, and two editions of one work agree on its title and author.
*
* Read from the file's own metadata only, never from what a provider matched it to: a provider
* match is itself a guess at the edition, and this is what an approver checks that guess against.
*/
export interface BookRequestReviewEdition {
publisher: string | null;
/** As the file states it, which may be a year, a month or a full date. Shown, never parsed. */
publishedDate: string | null;
language: string | null;
pageCount: number | null;
isbn10: string | null;
narrators: string[];
durationSeconds: number | null;
abridged: boolean | null;
chapterCount: number | null;
}
/** One file of the imported entry. Names only: a dock path is a server path and never leaves. */
export type BookRequestReviewFile = Pick<BookDockUnitFile, "fileName" | "fileSize" | "format" | "role">;
@@ -58,4 +78,15 @@ export interface BookRequestReview {
totalSizeBytes: number | null;
/** False when the request has no library to be filed into, which force-filing cannot invent. */
canFile: boolean;
/**
* Whoever sent the release asked to look at the import before it was filed, so it is held
* whatever the score says. Absent on servers older than the `book-request-review-hold` feature.
*/
heldOnRequest: boolean;
/** The page of the release this import came from, where its source published one. */
releaseInfoUrl: string | null;
/** Null once the dock entry is gone, and on a file that states none of it. */
edition: BookRequestReviewEdition | null;
/** Whether `.../review/cover` will answer with the cover found in the imported file. */
hasCover: boolean;
}
+5
View File
@@ -312,6 +312,11 @@ export interface GrabBookRequestPayload {
torrentFileBase64?: string;
torrentFileName?: string;
downloadClientId?: number;
/**
* Wait in review once the download lands rather than being filed on a passing score. Send only
* to a server advertising `book-request-review-hold`: an older one rejects the unknown field.
*/
holdForReview?: boolean;
}
/**
+6
View File
@@ -411,6 +411,12 @@ export interface ReleaseCandidateItem {
indexerName: string;
guid: string;
title: string;
/**
* The release's own page on its source, where the source publishes one: the description, file
* list and comments an approver checks an edition against. Never a download link, and never a
* link carrying a credential: one that might is dropped on the server rather than shown.
*/
infoUrl: string | null;
sizeBytes: number | null;
/** Null where the indexer did not report a swarm count at all, which is not the same as zero. */
seeders: number | null;