fix: stop showing a highlight item twice under two naming conventions
JDownloader wrote story-shaped names for highlights during one period of
its life, so the same item exists on disk as both
0ct0ber19 - C5dQPEYpd9W.mp4
2024-04-07_0ct0ber19 - 01 - C5dQPEYpd9W.mp4
which parsed to the ids "C5dQPEYpd9W" and "01 - C5dQPEYpd9W" -- two posts
for one item. The leading ordinal is a position within a day's stories
and carries nothing the shortcode does not, so story and highlight ids
drop it. Post ids are untouched, since those are permalinks.
Measured on the two real files, same archive, cache cleared between:
without the fix the profile reads "Heestory - 2 items", with it
"Heestory - 1 item".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseArchiveFilename, scopedPostId } from './archive-patterns';
|
||||
import { canonicalItemId, parseArchiveFilename, scopedPostId } from './archive-patterns';
|
||||
|
||||
describe('parseArchiveFilename — Instagram export format', () => {
|
||||
it('parses a single-image post', () => {
|
||||
@@ -189,3 +189,39 @@ describe('dateFromMtime', () => {
|
||||
expect(p.dateFromMtime).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* JDownloader wrote story-shaped names for highlights during one period, so
|
||||
* the same item exists under two conventions. They must be one post.
|
||||
*/
|
||||
describe('canonicalItemId', () => {
|
||||
it('collapses the two highlight naming conventions onto one id', () => {
|
||||
const dir = 'story highlights - 4utumn07 - Sunstory';
|
||||
const undated = parseArchiveFilename('4utumn07 - C5dQPEYpd9W.mp4', 'highlight', 1)!;
|
||||
const dated = parseArchiveFilename('2024-04-07_4utumn07 - 01 - C5dQPEYpd9W.mp4', 'highlight')!;
|
||||
expect(scopedPostId(dated.postId, 'highlight', dir))
|
||||
.toBe(scopedPostId(undated.postId, 'highlight', dir));
|
||||
});
|
||||
|
||||
it('does the same for stories', () => {
|
||||
const a = parseArchiveFilename('2025-10-26_u - 2 - DQRuDx9iW5Q.jpg', 'stories')!;
|
||||
expect(scopedPostId(a.postId, 'stories', 'story - u')).toBe('story - u/DQRuDx9iW5Q');
|
||||
});
|
||||
|
||||
it('keeps distinct story items distinct', () => {
|
||||
const a = parseArchiveFilename('2026-08-13_u - 1 - Db-UTJcCUUr.mp4', 'stories')!;
|
||||
const b = parseArchiveFilename('2026-08-13_u - 2 - Db-oNJ1CWQ4.mp4', 'stories')!;
|
||||
expect(scopedPostId(a.postId, 'stories', 'story - u'))
|
||||
.not.toBe(scopedPostId(b.postId, 'stories', 'story - u'));
|
||||
});
|
||||
|
||||
it('leaves a shortcode that merely starts with digits alone', () => {
|
||||
expect(canonicalItemId('4utumn07')).toBe('4utumn07');
|
||||
expect(canonicalItemId('C5dQPEYpd9W')).toBe('C5dQPEYpd9W');
|
||||
expect(canonicalItemId('12345')).toBe('12345');
|
||||
});
|
||||
|
||||
it('does not touch posts, whose ids are permalinks', () => {
|
||||
expect(scopedPostId('01 - ABC', 'posts')).toBe('01 - ABC');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,12 +100,36 @@ export const parseArchiveFilename = (
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A leading per-day ordinal on a story or highlight id: `01 - C5dQPEYpd9W`.
|
||||
*
|
||||
* JDownloader wrote story-shaped names for highlights during one period of its
|
||||
* life, so the same item exists as both `user - CODE.jpg` and
|
||||
* `date_user - 01 - CODE.jpg`. Those parse to different ids and the viewer
|
||||
* shows the item twice. The ordinal carries no information the shortcode does
|
||||
* not — it is a position within a day's stories, and the shortcode is already
|
||||
* unique — so it is dropped.
|
||||
*/
|
||||
const LEADING_ORDINAL = /^\d+ - (?=[A-Za-z0-9_-]+$)/;
|
||||
|
||||
/** Strip the ordinal so both naming conventions land on the same post. */
|
||||
export const canonicalItemId = (postId: string): string =>
|
||||
postId.replace(LEADING_ORDINAL, '');
|
||||
|
||||
/**
|
||||
* Namespace a post ID by its source directory.
|
||||
*
|
||||
* Base-profile IDs are left untouched so existing permalinks keep working;
|
||||
* sidecar IDs are prefixed so a shortcode appearing in both the profile and a
|
||||
* highlight stays two distinct posts.
|
||||
*
|
||||
* Story and highlight ids are canonicalised first, so an item fetched under
|
||||
* two different naming conventions is one post rather than two.
|
||||
*/
|
||||
export const scopedPostId = (postId: string, kind: SourceKind, dir?: string): string =>
|
||||
kind === 'posts' ? postId : `${dir ?? kind}/${postId}`;
|
||||
export const scopedPostId = (postId: string, kind: SourceKind, dir?: string): string => {
|
||||
if (kind === 'posts') return postId;
|
||||
const id = (kind === 'stories' || kind === 'highlight')
|
||||
? canonicalItemId(postId)
|
||||
: postId;
|
||||
return `${dir ?? kind}/${id}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user