Nothing in this tool had any memory: every invocation started from zero
and would happily re-enumerate a profile it had listed minutes earlier.
That is what suspended the account -- the listing passes, not the
downloads -- and an aborted run re-enumerating five profiles on restart
was a large part of the bill.
Three changes, in order of how much they save:
- Seeding is now a one-time bootstrap per source. After the first
successful sync the archive DB records everything gallery-dl has seen,
so the source is never probed again. A second full sync costs roughly
half what the first did.
- Stories never seed at all. A story cannot be in the archive before it
is fetched, so there is nothing to seed from, and probing would double
the cost of the cheapest surface we have.
- A source fetched within --min-interval (20h) is refused, and listing
results are cached for --probe-ttl (24h), so a restart mid-run is free
rather than a repeat. --force overrides both.
--only replaces --no-stories and takes any subset of the surfaces, which
is what makes a daily stories-only run possible: one source per profile,
no seeding, a handful of requests. Everything else stays monthly.
Tested with stdlib unittest -- no new dependencies, and it runs anywhere
the sync does. The cases include the aborted-restart scenario, which now
plans zero work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Adds --urls-file so a run can reference a hand-maintained list rather
than repeating --profile, which is how this actually gets used: the
ARTMS accounts now live in artms_account_links.txt at the archive root.
The parser takes what a person would paste. Full URLs, scheme-less URLs
and bare usernames all work; blank lines and # comments are ignored and
duplicates dropped, so the list can be appended to carelessly. Lines that
are not profiles are rejected loudly rather than silently syncing
nothing: an Instagram post URL yields the segment "p", which would
otherwise be treated as a username and create a directory called "p".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gallery-dl skips already-held media either by file existence -- which
requires the archive mounted where it writes -- or by a sqlite
skip-archive, which requires nothing on disk. Using the latter lets the
fetch host write to local disk and rsync afterwards, avoiding tens of
thousands of small writes over CIFS and keeping a mid-sync failure from
leaving partial files on the live Resilio share.
The key is archive_prefix + archive_fmt: the literal "instagram" plus the
per-media numeric pk. Verified against a real run -- a 3-image carousel
produced 3 rows and a re-run skipped every media file.
media_id is absent from our filenames, so the DB cannot be built from
names alone, but the listing pass we already make maps every live item to
its media_id, and a file listing says which we hold. Seeding therefore
costs no extra Instagram requests and no archive content -- the listing
GET /api/archives/:name/files already serves is enough.
Measured on 0ct0ber19: 2275 live items, 2248 seeded, 27 left to fetch --
exactly the media of the two posts added since the last crawl.
The trap worth the comment it carries: posts and reels are filed under
post_shortcode, while stories and highlights use the per-item shortcode
(post_shortcode there is the containing reel's id, shared by every item).
Matching on the wrong field seeded 5 of 2275 rather than failing loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>