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>
192 lines
7.5 KiB
TypeScript
192 lines
7.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { parseArchiveFilename, scopedPostId } from './archive-patterns';
|
|
|
|
describe('parseArchiveFilename — Instagram export format', () => {
|
|
it('parses a single-image post', () => {
|
|
expect(parseArchiveFilename('2023-04-19_4utumn07 - CrORBIcJJbM.mp4')).toEqual({
|
|
postId: 'CrORBIcJJbM',
|
|
date: '2023-04-19',
|
|
username: '4utumn07',
|
|
index: 1,
|
|
ext: 'mp4',
|
|
isStory: false,
|
|
dateFromMtime: false,
|
|
});
|
|
});
|
|
|
|
it('parses a carousel slide index', () => {
|
|
const parsed = parseArchiveFilename('2023-04-12_4utumn07 - Cq8LrxSJAJE - 3.jpg');
|
|
expect(parsed).toMatchObject({ postId: 'Cq8LrxSJAJE', index: 3, ext: 'jpg' });
|
|
});
|
|
|
|
it('groups a carousel under one post id', () => {
|
|
const ids = ['1', '2', '3'].map(
|
|
n => parseArchiveFilename(`2023-04-12_user - Cq8LrxSJAJE - ${n}.jpg`)!.postId,
|
|
);
|
|
expect(new Set(ids).size).toBe(1);
|
|
});
|
|
|
|
it('parses caption sidecar files', () => {
|
|
expect(parseArchiveFilename('2023-04-12_4utumn07 - Cq8LrxSJAJE.txt')).toMatchObject({
|
|
postId: 'Cq8LrxSJAJE',
|
|
ext: 'txt',
|
|
});
|
|
});
|
|
|
|
it('flags an explicit story suffix', () => {
|
|
expect(parseArchiveFilename('2023-04-12_user - ABC - story.jpg')?.isStory).toBe(true);
|
|
});
|
|
|
|
it('parses the story sidecar layout (date_user - N - shortcode)', () => {
|
|
// Files in `story - <user>` carry a per-day ordinal before the shortcode.
|
|
const parsed = parseArchiveFilename('2025-10-26_4utumn07 - 2 - DQRuDx9iW5Q.jpg', 'stories');
|
|
expect(parsed).toMatchObject({ date: '2025-10-26', username: '4utumn07', ext: 'jpg' });
|
|
expect(parsed!.postId).toContain('DQRuDx9iW5Q');
|
|
});
|
|
|
|
it('gives each story item a distinct id', () => {
|
|
const a = parseArchiveFilename('2026-08-13_u - 1 - Db-UTJcCUUr.mp4', 'stories')!.postId;
|
|
const b = parseArchiveFilename('2026-08-13_u - 2 - Db-oNJ1CWQ4.mp4', 'stories')!.postId;
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|
|
|
|
describe('parseArchiveFilename — Instaloader format', () => {
|
|
it('parses a timestamped filename', () => {
|
|
expect(parseArchiveFilename('2024-01-01_12-00-00_UTC.jpg')).toMatchObject({
|
|
postId: '2024-01-01_12-00-00_UTC',
|
|
date: '2024-01-01',
|
|
index: 1,
|
|
});
|
|
});
|
|
|
|
it('parses the carousel suffix', () => {
|
|
expect(parseArchiveFilename('2024-01-01_12-00-00_UTC_2.jpg')?.index).toBe(2);
|
|
});
|
|
|
|
it('flags the story suffix', () => {
|
|
expect(parseArchiveFilename('2024-01-01_12-00-00_UTC_story.jpg')?.isStory).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('parseArchiveFilename — story highlights', () => {
|
|
it('parses the dateless highlight layout', () => {
|
|
expect(parseArchiveFilename('4utumn07 - C5dQPEYpd9W.mp4', 'highlight')).toMatchObject({
|
|
postId: 'C5dQPEYpd9W',
|
|
username: '4utumn07',
|
|
ext: 'mp4',
|
|
isStory: false,
|
|
});
|
|
});
|
|
|
|
it('dates a highlight from mtime when the filename has none', () => {
|
|
const mtime = Date.UTC(2024, 4, 17, 12, 0, 0);
|
|
expect(parseArchiveFilename('user - ABC.jpg', 'highlight', mtime)?.date).toBe('2024-05-17');
|
|
});
|
|
|
|
it('leaves the date empty when no mtime is available', () => {
|
|
expect(parseArchiveFilename('user - ABC.jpg', 'highlight')?.date).toBe('');
|
|
});
|
|
|
|
it('does not apply the loose highlight pattern outside highlight directories', () => {
|
|
// Would otherwise swallow ordinary "a - b.jpg" filenames.
|
|
expect(parseArchiveFilename('user - ABC.jpg', 'posts')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseArchiveFilename — non-matching files', () => {
|
|
it.each(['4utumn07.jpg', 'profile_pic.jpg', 'README.md', 'no-separator.png'])(
|
|
'returns null for %s',
|
|
name => expect(parseArchiveFilename(name)).toBeNull(),
|
|
);
|
|
});
|
|
|
|
describe('scopedPostId', () => {
|
|
it('leaves base-profile ids untouched so permalinks keep working', () => {
|
|
expect(scopedPostId('Cq8LrxSJAJE', 'posts')).toBe('Cq8LrxSJAJE');
|
|
});
|
|
|
|
it('namespaces sidecar ids by directory', () => {
|
|
expect(scopedPostId('C5dQ', 'highlight', 'story highlights - u - Sunstory'))
|
|
.toBe('story highlights - u - Sunstory/C5dQ');
|
|
});
|
|
|
|
it('keeps the same shortcode distinct across sources', () => {
|
|
const inPosts = scopedPostId('ABC', 'posts');
|
|
const inHighlight = scopedPostId('ABC', 'highlight', 'story highlights - u - H');
|
|
expect(inPosts).not.toBe(inHighlight);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* gallery-dl is replacing JDownloader as the fetcher (docs/gallery-dl.md).
|
|
* Its naming differs cosmetically, and these cases pin down that the two
|
|
* interoperate so a mixed archive parses identically.
|
|
*/
|
|
describe('gallery-dl / JDownloader naming interop', () => {
|
|
it('treats a single-media post the same with or without an index', () => {
|
|
const jd2 = parseArchiveFilename('2023-04-19_4utumn07 - CrORBIcJJbM.mp4')!;
|
|
const gdl = parseArchiveFilename('2023-04-19_4utumn07 - CrORBIcJJbM - 1.mp4')!;
|
|
expect(jd2.postId).toBe(gdl.postId);
|
|
expect(jd2.index).toBe(gdl.index);
|
|
expect(jd2.index).toBe(1);
|
|
});
|
|
|
|
it('normalises zero-padded carousel indices', () => {
|
|
// JD2 pads to the width of the media count (10+ items -> "01"), and
|
|
// gallery-dl's count can be one higher, so the same post may be padded
|
|
// by one tool and not the other.
|
|
expect(parseArchiveFilename('2024-04-17_4utumn07 - C53YPQzp7Wj - 09.jpg')!.index).toBe(9);
|
|
expect(parseArchiveFilename('2024-04-17_4utumn07 - C53YPQzp7Wj - 9.jpg')!.index).toBe(9);
|
|
expect(parseArchiveFilename('2023-11-03_4utumn07 - CzM8Uf6B6H_ - 01.jpg')!.index).toBe(1);
|
|
});
|
|
|
|
it('reads a gallery-dl story name, which carries a per-item shortcode', () => {
|
|
const p = parseArchiveFilename('2026-08-16_official_band - DcF9OyhBJ1H.jpg', 'stories')!;
|
|
expect(p.postId).toBe('DcF9OyhBJ1H');
|
|
expect(p.date).toBe('2026-08-16');
|
|
});
|
|
|
|
it('gives a dated highlight a real date instead of the mtime fallback', () => {
|
|
const mtime = Date.parse('2026-08-17T00:00:00Z');
|
|
const undated = parseArchiveFilename('4utumn07 - C-IImhvpFuk.jpg', 'highlight', mtime)!;
|
|
const dated = parseArchiveFilename('2024-08-04_4utumn07 - C-IImhvpFuk.jpg', 'highlight', mtime)!;
|
|
// Same item either way, so re-fetching cannot split it into two posts.
|
|
expect(dated.postId).toBe(undated.postId);
|
|
expect(undated.date).toBe('2026-08-17');
|
|
expect(dated.date).toBe('2024-08-04');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Highlights are the only files with no date in the name, so they fall back to
|
|
* mtime — which is when the file was written, not when it was posted. Callers
|
|
* need to know the difference to let a real date win.
|
|
*/
|
|
describe('dateFromMtime', () => {
|
|
const mtime = Date.parse('2026-08-17T00:00:00Z');
|
|
|
|
it('flags an undated highlight name as mtime-dated', () => {
|
|
const p = parseArchiveFilename('4utumn07 - C-IImhvpFuk.jpg', 'highlight', mtime)!;
|
|
expect(p.date).toBe('2026-08-17');
|
|
expect(p.dateFromMtime).toBe(true);
|
|
});
|
|
|
|
it('does not flag a highlight that carries its own date', () => {
|
|
const p = parseArchiveFilename('2024-08-04_4utumn07 - C-IImhvpFuk.jpg', 'highlight', mtime)!;
|
|
expect(p.date).toBe('2024-08-04');
|
|
expect(p.dateFromMtime).toBe(false);
|
|
});
|
|
|
|
it('never flags ordinary post or Instaloader names', () => {
|
|
expect(parseArchiveFilename('2023-04-19_u - ABC.mp4', 'posts', mtime)!.dateFromMtime).toBe(false);
|
|
expect(parseArchiveFilename('2024-01-01_12-00-00_UTC.jpg', 'posts', mtime)!.dateFromMtime).toBe(false);
|
|
});
|
|
|
|
it('leaves the date empty rather than guessing when no mtime is given', () => {
|
|
const p = parseArchiveFilename('4utumn07 - C-IImhvpFuk.jpg', 'highlight')!;
|
|
expect(p.date).toBe('');
|
|
expect(p.dateFromMtime).toBe(false);
|
|
});
|
|
});
|