Commit Graph
70 Commits
Author SHA1 Message Date
jim 21e848efb7 feat: add [X remaining] progress counter to likes mode output 2026-05-19 15:48:18 -05:00
jim 24c5449e0c No longer copies db to output directory 2026-05-18 21:32:01 -05:00
jimandClaude Opus 4.7 c55569eadf chore: log folder transitions during -ingest
The every-50-file progress line wasn't enough to know which blog was
currently being processed on a long run. Now -ingest prints a
"entering folder: <name>" line whenever the source folder changes, and
the every-50 progress line also prefixes the folder name.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-18 15:40:27 -05:00
jimandClaude Opus 4.7 cf3f97ddc4 feat: add single-blog filter to -ingest
`-ingest <blogname>` now restricts the run to one blog's folder, mirroring
the existing -parse <blogname> ergonomics. `-ingest` with no arg still
processes every blog under appSettings:PathTTRoot (or PathInput fallback).

Breaking vs 3aff849: the first positional arg is interpreted as a blog
name, not a path. Configure the root via appSettings:PathTTRoot.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-18 15:36:42 -05:00
jimandClaude Opus 4.7 3aff849216 feat: merge ThreeTxtFileHelper into URLNotesGrabberCORE
Folds the standalone ThreeTxtFileHelper tool into URLNotesGrabberCORE so
text-file ingest/output/correct lives alongside the API scraper. Adds
new flags -ingest, -output, -correct (with -apply), -updatepaths, and a
one-time -importposts <posts.db> migration.

Schema: Blogs.TTFolderPath and Posts.PostType are added by an idempotent
migration. On (BlogName, PostID) collisions, content columns are
overwritten while engagement columns (ByLikes, RootBlogName, RootURL,
HasNotesGathered, NotFound, NotesGatheredDateTime, Likes*) are preserved.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-18 12:43:17 -05:00
jim 5781e121d2 Merge branch 'claude/gifted-dhawan-7e5fc7' 2026-05-16 14:02:07 -05:00
jimandClaude Sonnet 4.6 e27190e4f9 add incremental refresh + cooldown to -likes mode
After backfill completes for a blog, -likes can now pick up only newer
likes instead of being a one-shot pull. Tracks a per-blog
liked_timestamp high-water mark and stops the refresh walk once it
crosses the stored mark. A configurable cooldown (LikesRefreshCooldownDays,
default 7) gates which blogs are re-checked on each run. -force bypasses
the cooldown.

The migration adds three columns to Blogs (LikesNewestTimestamp,
LikesLastRefreshed, LikesLastNewCount) and does a one-time reset of all
likes tracking state so the new high-water mark starts from a clean
baseline. Existing ByLikes posts remain; the UNIQUE constraint absorbs
re-inserts during the first re-backfill.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-16 14:00:45 -05:00
jim 7c667bd579 Merge branch 'claude/eager-hugle-2f3520' 2026-05-15 11:53:34 -05:00
jimandClaude Opus 4.7 2634ff8967 speed up no-args directory import with shared connection + lazy blog cache
AddBlog/AddPost/UpdatePost/UpdatePostSetDate now reuse a single SQLiteConnection
when an import session is active, instead of opening and closing one per call.
AddBlog also short-circuits on an in-memory HashSet of blog names already
attempted this run. Other entry points are unaffected since they never call
BeginImportSession.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-15 11:51:57 -05:00
jim 69ce36a1f5 sleep call on finding note 2026-05-15 11:49:54 -05:00
jim 52293f2021 Merge branch 'claude/goofy-antonelli-73ea26' 2026-05-11 09:09:25 -05:00
jimandClaude Opus 4.7 2b86ce9119 collapse GetPosts SQL log to a single line
Per-iteration call in CollectNotes was dumping ~30 lines of newline-padded
SQL to the console. Replace with a whitespace-collapsed single-line print.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-11 09:09:24 -05:00
jim a5c118ce3b Merge branch 'claude/goofy-antonelli-73ea26' 2026-05-11 09:03:12 -05:00
jimandClaude Opus 4.7 47497d02bf tweak GetPosts beforeDate filter and broaden GetBlogsForLikes
- GetPosts: match NotesGatheredDateTime = 0 instead of IS NULL for the
  beforeDate cutoff.
- GetBlogsForLikes: drop the EXISTS-Posts predicate so blogs with notes
  but no posts rows are still eligible for likes collection.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-11 09:03:01 -05:00
jimandClaude Opus 4.7 5ae1d4feb8 fix: skip API calls when all keys are rate-limited in -collect mode
Pre-flight check on the CollectNotes loop now sleeps until at least one
key recovers instead of issuing a wasted 429-bound request per iteration.
Extracts the countdown into ApiKeyPool.SleepUntilAnyAvailable (30s refresh)
and reuses it in CollectLikes and GrabNotes.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-11 08:37:47 -05:00
jim 2034b43e83 Merge branch 'claude/wizardly-antonelli-9f2795' 2026-05-09 20:24:53 -05:00
jimandClaude Opus 4.7 4f00adcc05 fix: -collect mode now processes all posts instead of exiting after first
Changed CollectNotes from async void to async Task and await it with
.GetAwaiter().GetResult() to match the pattern used by -replies and -likes.
Previously the method would return immediately after the first await,
causing Main to exit before the loop could process more than one post.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-09 20:24:50 -05:00
jim bcbaea8c48 Merge branch 'claude/wizardly-antonelli-9f2795' 2026-05-08 21:42:39 -05:00
jimandClaude Opus 4.7 349b465f8a fix: release 404 posts from -replies queue
Tumblr API 404s were leaving Posts.NotFound=0 and Notes.replyText='.',
so GetRepliesWithFilledText kept re-selecting the same dead post and
the loop spun forever. Mark NotFound=1 and replies '?' on 404.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-08 21:42:30 -05:00
jim acc5e885a8 feat: require PathDB config in appsettings.json 2026-05-08 16:24:43 -05:00
jimandClaude Sonnet 4.6 b68a9833d8 chore: format remaining blog/post log output as <blog>.tumblr.com/post/<id>
Apply the URL format to the remaining 6 console log lines that still
referenced posts in <blog>/<id> form (404 logs, max-page-limit log,
DumpReplies console output).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-07 16:16:29 -05:00
jim 89f2addf01 Merge branch 'claude/awesome-edison-d12b61' 2026-05-07 16:13:30 -05:00
jimandClaude Sonnet 4.6 b963489741 chore: format blog/post log output as <blog>.tumblr.com/post/<id>
Console-friendly URL format for log lines that reference a specific
post — easier to copy/paste into a browser when investigating output.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-07 16:12:00 -05:00
jim d18ab92019 Merge branch 'claude/awesome-edison-d12b61' 2026-05-07 15:58:13 -05:00
jimandClaude Sonnet 4.6 4737baa288 fix: keep '.' as needs-processing sentinel, normalize real-dot replies
Previous fix removed '.' from the GetRepliesWithFilledText SELECT,
which broke processing of legacy null-substitute rows that need to be
re-fetched.

Restored '.' in the SELECT. To avoid the infinite loop when an actual
API reply is the literal string ".", normalize it to ". " (dot +
trailing space) inside UpdateNoteReplyText so the stored value no
longer matches the sentinel.

Also extended the fan-out UPDATE guard to match the SELECT criteria
(NULL / '' / '.') so legacy '.' rows in other reblog copies can be
filled in too. The guard still refuses to overwrite '?' or
already-fetched text.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-07 15:56:53 -05:00
jim 51c40fe2bd Merge branch 'claude/awesome-edison-d12b61' 2026-05-07 15:49:40 -05:00
jimandClaude Sonnet 4.6 51e2a8a1e1 fix: stop infinite loop when reply text is a literal dot
'.' was used as a sentinel for 'not yet fetched' in the SELECT query,
but it is also valid reply text. This caused any post where a reply
text was literally '.' to stay in the work queue forever.

Also fixes the fan-out UPDATE guard: previously it used
IFNULL(replyText, '.') <> @newValue, which would overwrite '?'
(confirmed-empty) rows with '.' when processing a dot reply elsewhere
in the reblog chain, pulling completed posts back into the queue and
causing the remaining counter to increase.

Changes:
- Remove OR replyText = '.' from GetRepliesWithFilledText SELECT
- Restrict UpdateNoteReplyText fan-out to NULL/'' rows only
- Use '?' not '.' as null-coalesce fallback in UpdateNoteReplyText

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-07 15:49:35 -05:00
jim 0213478a40 Merge branch 'claude/epic-bardeen-cbbb7e' into master 2026-05-07 15:02:48 -05:00
jimandClaude Opus 4.7 081528a81a fix: mark replies '?' after pagination yields no reply notes
Previously, the '?' marker only fired when page 1 returned empty notes.
Posts whose page 1 contained only likes/reblogs and whose page 2 came
back empty were never marked, so GetRepliesWithFilledText kept reselecting
them every iteration. Now mark after the pagination loop whenever
rowsUpdated==0 and at least one page returned 200 OK.

UpdateAllNoteReplyTextForPost now returns int so the caller can fold
the count into emptyReplyCount/rowsUpdated for accurate Done logging.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-07 15:01:04 -05:00
jim f2539dc9d0 fix: mark post's replies '?' on confirmed empty notes response
When the API responds 200 OK with notes:[] on the first page,
the post has no conversational notes per Tumblr. Stamp all the
post's reply rows '?' so the per-iteration requery stops handing
this post back forever. Only fires for first-page 200 OK so a
later-page empty (end of pagination) doesn't poison good rows.
2026-05-07 14:25:00 -05:00
jim 2438997c7c chore: untrack and gitignore .claude local settings + worktrees
Local Claude Code config; should not be in the repo.
2026-05-07 14:20:41 -05:00
jim ac79727040 feat: fan reply updates across reblog chains, requery per post
A reply by a given blog at a given timestamp is the same reply
across the original post and every reblog of it. Drop PostID from
the UpdateNoteReplyText WHERE clause so a single API hit fills in
the replyText on every matching row at once.

Pair that with a per-iteration requery (limit 1) of the work list
so posts whose replies were already filled in as a side-effect of
a previous post's update are skipped without burning an API call.
2026-05-07 14:19:54 -05:00
jim 64a914c14d fix: tolerate ~5s timestamp drift in UpdateNoteReplyText
The API returns reply timestamps that are ~1s ahead of what -collect
originally stored, so an exact TimeStamp match in the UPDATE was
hitting zero rows for every note - the API call worked, the reply
text came back, but nothing landed in the DB. Match within +-5s
instead. Also return rowsAffected from UpdateNoteReplyText and
report it separately from notes-seen in the summary so misses are
visible.
2026-05-07 14:10:37 -05:00
jim 1c7b6a1e86 fix: start replies fetch from newest, ignore stale DB timestamp
The DB-derived MAX(N.timestamp)+1 for a post is unreliable - many notes
were stored with the same timestamp (likely crawl time, not the actual
note timestamp), so passing it as before_timestamp excluded all real
replies and returned an empty notes array. Diagnostics showed two
unrelated posts coming back with identical totals (1625/1021/603) and
zero notes.

Now we start with no before_timestamp (newest page) and let pagination
walk backward via each page's last-note timestamp.
2026-05-07 14:00:48 -05:00
jim 1da8a702ba fix: pull reply_text from Tumblr in -replies mode
Wrap FetchAndStoreReplyText in a per-post pagination loop (up to 10
pages, advancing before_timestamp via the last note's timestamp) so
posts with >50 notes are fully walked. Log raw response (meta + first
500 chars of JSON) when a page returns no notes so empty results are
diagnosable. Stop blanket-marking every reply on a post with '?' on
the first empty response - rows stay '.' and remain retry-eligible;
only individual replies that come back with empty reply_text are
marked '?'.
2026-05-07 13:54:54 -05:00
jim b70f356d24 SQLite DB Project File 2026-05-06 16:51:56 -05:00
jimandClaude Haiku 4.5 539994b742 fix: emit per-key colored output in -replies mode on successful API calls
Call ApiKeyPool.MarkAvailable(key) after successful reply fetch in FetchAndStoreReplyText, matching behavior in CollectLikes. This produces the colored '[Pool] Key#N (...) rate-limit cleared' line for each API call, allowing operators to visually track which API key is being used per request.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
2026-05-06 15:27:23 -05:00
jim c39d555d6e 2026-05-06 14:50:30 -05:00
jim a7d175a5a8 feat: enhance replies command progress output 2026-05-06 10:32:49 -05:00
jim 0adaef62b1 fix: update GetRepliesWithFilledText to join Posts table and filter NotFound 2026-05-06 09:50:36 -05:00
jim 1507086269 fix: Correct CollectMissingReplyText to use API key rate limiting 2026-05-06 09:16:36 -05:00
jim 9c77ecce4b fix: color no-replies messages yellow for visual distinction 2026-05-05 18:07:19 -05:00
jim 78e3a070b2 fix: make reply text collection process all remaining posts without limit 2026-05-05 17:57:23 -05:00
jim 9589a9c090 feat: add green color for replies and cyan for reblog comments 2026-05-05 17:43:48 -05:00
jim abdfe30203 Uses mode=conversation to fetch only notes with text
 Captures replies and reblogs with comment
 Ignores rollup_notes field (as requested)
 Maintains rate limiting and error handling
 Console output shows both reply and reblog comment
2026-05-05 11:49:10 -05:00
jim 354bb9cc8e Additional fixes 2026-05-05 00:04:40 -05:00
jim 5e62b58261 feat: add API key color assignment, auto-persist, AGENTS.md 2026-05-04 10:33:46 -05:00
jim 34e5745e99 Fixed bugs on API key pool 2026-05-03 13:59:51 -05:00
jim 6cec7afb53 Changes Summary
appsettings.json — Added PoolEnabled to each API section:
- TumblrApi → true
- TumblrApi3 → false
- TumblrApi4 → true
DataAccess.cs — Added:
- ApiKeyConfig class — holds credentials + metadata per key
- ApiKeyPool class — manages pool with round-robin rotation, SQLite-backed state (ApiKeyPoolState, ApiKeyPoolMeta tables)
  - GetCurrentKey() — returns next key, skipping rate-limited ones, falls back to earliest-recovery if all are throttled
  - MarkRateLimited(key, retryUntil) / MarkAvailable(key) — persists state
  - Initialize() — discovers pool-enabled keys, detects single-key override mode
- Refactored APIAccess.GrabNotes(), GrabPostWithReplies(), GrabLikes() to accept ApiKeyConfig param and log [Key#N]
Program.cs — Updated:
- Tracks apiExplicitlySet flag from -api/-api3/-api4
- Initializes ApiKeyPool at startup (pool mode or single-key override)
- All 3 API callers updated to use pool rotation + 429 handling
Startup Output
- Pool mode: [Pool] Active keys: Key#1=TumblrApi, Key#2=TumblrApi4
- Single-key: [Pool] Single-key mode: TumblrApi3
2026-05-03 13:31:28 -05:00
jim de96c7c1c5 Updated db location logic 2026-05-03 08:41:21 -05:00