The .json sidecars published with the ARTMS fetch were inert: the scanner fed them through the Instaloader path, where `node.edge_media_to_caption` and `checkIsStory`'s `product_type` are both absent, so nothing happened. They are now recognised structurally -- flat, with post_shortcode and type, and none of the markers the other two JSON shapes carry -- and used for three things: - `type` sets post.isReel, which post-tabs prefers over every fallback. This is Instagram's own classification and it disagrees with ours a lot: of 781 items in "official_artms - reels", the sidecars say only 360 are reels. The other 421 are feed videos the clips endpoint returns via include_feed_video, and the directory-based rule counted them all. - `description` fills the caption where no .txt exists. - `date` dates a post whose filename could not. Also fixes date precedence. Only JDownloader highlights lack a date in the filename, so parseArchiveFilename now marks those as mtime-derived and the scanner lets any real date replace them -- previously the date depended on which file the scan reached first. Verified against real published files: a directory of three type=post and three type=reel renders 6 in the grid and exactly the 3 reels in the Reels tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
export interface MediaFile {
|
|
name: string;
|
|
/**
|
|
* Path relative to the archive root (matching webkitRelativePath for local
|
|
* folders). Unlike `url`, this survives a page reload, so it is what the
|
|
* cache persists and what URLs are rehydrated from.
|
|
*/
|
|
path: string;
|
|
url: string;
|
|
type: 'image' | 'video';
|
|
index: number;
|
|
size?: number;
|
|
}
|
|
|
|
/**
|
|
* Which sidecar directory a post came from. Archives store reels, stories and
|
|
* each story highlight in directories alongside the base profile; the viewer
|
|
* folds them into one profile and routes them by kind.
|
|
*/
|
|
export type SourceKind = 'posts' | 'reels' | 'stories' | 'highlight';
|
|
|
|
export interface ArchiveSource {
|
|
kind: SourceKind;
|
|
/** Directory name relative to the archives root. */
|
|
dir: string;
|
|
/** Highlight title, for kind === 'highlight'. */
|
|
title?: string;
|
|
}
|
|
|
|
export interface Post {
|
|
id: string;
|
|
date: string;
|
|
username: string;
|
|
caption: string;
|
|
media: MediaFile[];
|
|
thumbnail: string;
|
|
isStory?: boolean;
|
|
/** Defaults to 'posts' for archives without sidecar directories. */
|
|
source?: SourceKind;
|
|
/**
|
|
* Instagram's own answer to "is this a reel", from a gallery-dl `.json`
|
|
* sidecar. Undefined when the archive carries no such sidecar, which is when
|
|
* the viewer has to fall back to guessing — see src/lib/post-tabs.ts.
|
|
*/
|
|
isReel?: boolean;
|
|
/** Highlight this post belongs to, for source === 'highlight'. */
|
|
highlightTitle?: string;
|
|
}
|
|
|
|
/**
|
|
* Common interface for both local File objects and remote server-side files.
|
|
*/
|
|
export interface ArchiveFile {
|
|
name: string;
|
|
webkitRelativePath: string;
|
|
size: number;
|
|
text(): Promise<string>;
|
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
url?: string;
|
|
/**
|
|
* A URL pointing at this file's contents. Local files mint a disk-backed
|
|
* blob: URL (no data is read into memory); remote files return their HTTP URL.
|
|
*/
|
|
createObjectUrl(mimeHint?: string): string;
|
|
/** True when createObjectUrl() returns a blob: URL that must be revoked. */
|
|
readonly revocable: boolean;
|
|
/** Which sidecar directory this file came from, when known. */
|
|
source?: ArchiveSource;
|
|
/** Last-modified time (ms). Used to date items whose filename has no date. */
|
|
mtime?: number;
|
|
}
|
|
|
|
export interface ServerArchive {
|
|
name: string;
|
|
thumbnail: string;
|
|
path: string;
|
|
/** Null until the server has indexed this profile. */
|
|
fileCount: number | null;
|
|
/**
|
|
* Directory-mtime signature. Cheap for the server to compute and sufficient
|
|
* to detect changes, unlike a file count that would require a full walk.
|
|
*/
|
|
signature?: string;
|
|
/** Base profile plus any sidecar directories folded into it. */
|
|
sources?: ArchiveSource[];
|
|
}
|
|
|
|
/** One entry from GET /api/archives/:name/files. */
|
|
export interface ServerArchiveFile {
|
|
path: string;
|
|
size: number;
|
|
mtime: number;
|
|
kind: SourceKind;
|
|
title?: string;
|
|
}
|
|
|
|
export interface ProfileMetadata {
|
|
username: string;
|
|
fullName: string;
|
|
bio: string;
|
|
followerCount: number;
|
|
followingCount: number;
|
|
externalUrl: string;
|
|
profilePic: string | null;
|
|
allProfilePics: string[];
|
|
}
|
|
|
|
/** Shape of an archive entry persisted in IndexedDB. */
|
|
export interface CacheData {
|
|
name: string;
|
|
isLocal: boolean;
|
|
fileCount: number;
|
|
/** Server archives: the signature this entry was built from. */
|
|
signature?: string;
|
|
posts: Post[];
|
|
stories: Post[];
|
|
/** Story-highlight items, grouped by `highlightTitle`. */
|
|
highlights?: Post[];
|
|
profileMetadata: ProfileMetadata;
|
|
timestamp: number;
|
|
/**
|
|
* Local archives only: whether a FileSystemDirectoryHandle was stored
|
|
* alongside this entry, meaning media URLs can be rehydrated without
|
|
* re-prompting for the folder.
|
|
*/
|
|
hasDirectoryHandle?: boolean;
|
|
/** Path of the profile picture, for rehydration (local archives). */
|
|
profilePicPath?: string;
|
|
}
|