export type CoverRole = "poster" | "square" | "banner" | "foreground" | "other"; export type ImageType = "volume" | "volume_back" | "chapter" | "season" | "banner" | "audiobook" | "other"; export type ContentRating = "safe" | "suggestive" | "erotica" | "pornographic"; export const CONTENT_RATINGS: readonly ContentRating[] = ["safe", "suggestive", "erotica", "pornographic"]; export type Shape = "tall" | "portrait" | "square" | "wide" | "other"; export type ImageExt = "jpg" | "png" | "gif" | "webp"; /** Wayback history: one snapshot per this many months (spec ยง4.3). */ export type Granularity = 1 | 3 | 6 | 12; export const GRANULARITIES: readonly Granularity[] = [1, 3, 6, 12]; export interface SourceSeries { source: string; // Source.id externalId: string; // e.g. Webtoons title_no "95" title: string; language: string; // MangaBaka language code, e.g. "en", "zh-Hant", or "unknown" canonicalUrl: string; archiveUrls: string[]; // URL prefixes (no scheme) to look up in Wayback mature: boolean; flags: Record; // e.g. { canvas: true } } export interface CoverCandidate { url: string; // normalised, hotlinkable role: CoverRole; current: boolean; // shown on the live page now seenAt?: string; // earliest archive timestamp (YYYYMMDDhhmmss) if historical lastSeenAt?: string; // latest archive timestamp after merging originalUrl?: string; // as written in the page (absolute), before normalisation archivedUrl?: string; // Wayback copy of the image itself } export interface InspectedCover { candidate: CoverCandidate; fetchedFrom: string; // URL that served the bytes (candidate.url, or archivedUrl) live: boolean; // true if fetchedFrom === candidate.url width: number; height: number; sha256: string; // lowercase hex contentType: string; // image/jpeg | image/png | image/gif | image/webp ext: ImageExt; bytes: Uint8Array; shape: Shape; type: ImageType; // suggested MangaBaka type }