From 92a4ada3c2ac8006ec4e8c65114b200eea38402a Mon Sep 17 00:00:00 2001 From: ergosteur <1992147+ergosteur@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:41:23 -0400 Subject: [PATCH] feat: generate JDownloader crawljobs from the archives on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual flow is: paste a profile URL into JDownloader, paste the /reels URL separately (the profile page misses some reels), set the output folder by hand, repeat per profile. scripts/jd2-sync.ts emits one crawljob per source with the folder already pointed at the right directory, so folder-watch picks up the whole batch at once. Profiles and sidecars are derived with the same grouping logic the server uses, so output folders always match what the viewer expects to find. --download-base maps the path for a JDownloader running on another machine (Windows paths included), since it typically runs on a desktop against the share. Directories that aren't Instagram profiles are skipped: an archive root also collects tool output and exports from other services, and pointing a crawl at those spends requests on instagram.com to be told the profile doesn't exist — exactly the traffic worth not spending. Filtering is by username shape, plus --skip and a .jd2ignore file for names that look like usernames but aren't. Defaults are conservative: chunks=1, because multi-chunk ranged requests are the one CDN-side pattern that doesn't resemble a browser, and links park in the LinkGrabber for review rather than auto-starting. Only posts and reels are emitted; highlight URLs need a numeric id and story URLs expire, so those stay manual. Format verified against JDownloader's own explain.txt for the folderwatch extension, read from the daily SVN mirror rather than one of the decade-stale GitHub copies. Also refreshes CLAUDE.md, whose URL-state section still described the query parameters replaced in 1.4.0, and documents the mobile feed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011uBWhwV3wFQ5MBCcMHHem7 --- CLAUDE.md | 17 +++++++++++++---- package.json | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d057716..06fd643 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,8 @@ InstaArchive Viewer is a React 19 + Vite 6 PWA for browsing archived Instagram d - `npm run lint` — type-check only (`tsc --noEmit`) - `npm test` / `npm run test:watch` — vitest - `npx vitest run src/lib/archive-patterns.test.ts` — a single test file +- `npm run jd2 -- --archives --dry-run` — generate JDownloader `.crawljob` + files for every profile on disk (see `scripts/jd2-sync.ts`) Local development usually needs both `npm run dev` and `npm run server`. Local-folder mode works without the backend; server-mode archives do not. @@ -71,14 +73,21 @@ Restoring an archive **rehydrates URLs from `path`**: server archives rebuild HT Images over 1MiB are downscaled in a Web Worker via `OffscreenCanvas`. The queue is **serial on purpose** — decoding several 50MP+ images at once OOMs the tab. `requestThumbnail` must keep a stable identity (it reads cache state through a ref), or every completed thumbnail re-runs the effect in all mounted thumbnails. -### URL state (`src/App.tsx`) +### URL state (`src/App.tsx`, `src/lib/routing.ts`) -App state syncs to `?a=` / `?t=` / `?p=`. Two rules, both learned from real bugs: +Paths mirror Instagram: `//`, `//reels/`, `//p//`. The old `?a=&t=&p=` form is still parsed for existing links but never written. Reserved prefixes (`api`, `archives`, `assets`…) can't be mistaken for a profile name. -- The initial query string is captured into a ref on first render; the URL is rewritten from state as soon as anything loads, so reading `window.location` later sees the rewrite, not the user's link. +A post URL carries no tab, as on Instagram — the tab is re-derived from the post's `source`, so a reel link lands on the Reels tab and pages through reels. Sidecar posts keep directory-scoped ids internally but expose only the shortcode. + +Three rules, all learned from real bugs: + +- The initial route is captured into a ref on first render; the URL is rewritten from state as soon as anything loads, so reading `window.location` later sees the rewrite, not the user's link. - URL writing is gated on `hasInitialLoaded`, otherwise it erases the deep link before the loader consumes it. +- Deep-link resolution waits on the archive fetch having *settled* (`archivesFetched`), not on `isServerMode`, which is still false while the request is in flight. -Deep-link resolution waits on the archive fetch having *settled*, not on `isServerMode` (which is still false while in flight). +### Mobile feed (`src/components/PostFeed.tsx`) + +Below `md`, opening a post renders a scrolling feed page rather than the modal (`useIsMobile` decides). Only a window of posts is mounted; it grows both ways, and prepending corrects `scrollTop` in a `useLayoutEffect` so content doesn't jump. Only the post crossing the viewport centre plays its video and drives the URL. Desktop keeps `PostModal`; both share `MediaCarousel`. ### Backend (`server.ts`) diff --git a/package.json b/package.json index cbc67de..6d81a34 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "clean": "rm -rf dist", "lint": "tsc --noEmit", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "jd2": "tsx scripts/jd2-sync.ts" }, "dependencies": { "@tailwindcss/vite": "^4.1.14",