feat(band): add Band.us adapter — bands, posts, mentions, post commands#532
feat(band): add Band.us adapter — bands, posts, mentions, post commands#532kanghouchao wants to merge 21 commits intojackwener:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new band.us adapter to opencli, providing Band membership and content export commands using browser automation and DOM/XHR extraction.
Changes:
- Introduces
band bands,band posts,band post, andband mentionscommands undersrc/clis/band/. - Implements DOM-based extraction for bands/posts/post, and XHR interception for mentions.
- Extends E2E “graceful auth failure” coverage to include the new Band commands.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/browser-auth.test.ts | Adds auth-required graceful-failure tests for the new band commands. |
| src/clis/band/bands.ts | New command to list bands from the Band.us home sidebar DOM. |
| src/clis/band/posts.ts | New command to list posts for a band via direct navigation + DOM parsing. |
| src/clis/band/post.ts | New command to export a full post (plus optional photo download) via DOM parsing. |
| src/clis/band/mentions.ts | New command to fetch @mention notifications via UI actions + XHR interception. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
All 6 review comments have been addressed in d269b5e:
Replied to each comment individually. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- bands: lists all Bands via get_band_list_with_filter intercept
- posts: lists posts from a Band via get_posts_and_announcements intercept
- mentions: shows @mention notifications via get_news intercept
All use Strategy.INTERCEPT since band.us API requires an HMAC md header
generated by its own JS. SPA navigation to /band/{no}/post triggers the
band list and posts APIs; bell + @メンション tab click triggers mentions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix doc comments: Band uses XHR not fetch; clarify INTERCEPT rationale - bands: replace for-loop with flatMap; explain why band page nav is needed - posts: remove item.post ?? item fallback (API always wraps in post); rename finalRequests → requests for consistency; extract stripBandTags helper - mentions: remove redundant ?? defaults (args have defaults defined); fix unreadOnly bug (was not applied to post/comment modes); consolidate Band tag stripping to single regex; cast kwargs types directly instead of converting; add comments explaining last-response strategy and 'referred' filter flag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_posts_and_announcements returns both regular posts and announcements that have different shapes — some lack post_no and wrap differently. Restore item.post ?? item fallback and filter out items with no resolvable identifier to prevent undefined in URLs and empty rows in output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…to download Exports the complete content of a single Band post: - Post body (with Band markup tags stripped) - All comments in chronological order - Photo URLs shown inline, or downloaded with --output <dir> Uses Strategy.INTERCEPT with a broad 'band.us' pattern to capture both the batch request (embedding get_post) and get_comments in one SPA navigation. Responses are identified client-side by shape: batch_result array vs items array with comment_id fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bands, posts, post: navigate directly to target URL instead of home→SPA detour - All three switch from Strategy.INTERCEPT to Strategy.COOKIE with navigateBefore: false (bands uses framework pre-nav to home; posts/post disable it and goto target directly) - DOM extraction polls for specific content elements rather than fixed waits - post: confirm selectors via browser inspection (a.text, time.time, .sCommentList, .sReplyList for nested replies); add --comments flag to skip comment fetch - posts: extract from rendered post list DOM; correct comment item selector (div.cComment) - Fix: post empty-result guard changed from && to handle null data safely - Fix: photo download now checks HTTP status code before piping to avoid writing redirect HTML into image files - Fix: mentions unread client-side filter skipped for 'mentioned' mode since server already filtered via 未確認のみ button click Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- post: replace manual http/https download with shared downloadMedia utility (handles redirects, timeouts, stream errors correctly) - post: fix photo URL resolution to use location.href as base, handling protocol-relative and relative URLs without throwing - post: switch to node:-prefixed imports per repo convention - post/posts: remove redundant ArgumentError guards — framework already validates required args before func() is called - mentions: INTERCEPT strategy is intentional (Band HMAC prevents DOM-only approach for notifications; update PR description to clarify) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bands: tighten href selector to /band/{id}(?:/post)?$ so feed/post-detail
links are excluded; only sidebar navigation links match
- mentions: replace fixed page.wait(2) sleeps with polling on
getInterceptedRequests() — waits up to 8 s per action, exits as soon
as the expected number of captures arrives (avoids flakiness on slow XHR)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bands: use a.bandCover._link + p.uriText + span.member em selectors (previous a[href*="/band/"] + .bandName combo leaked "メンバー" text) - posts: use article.cContentsCard._postMainWrap + span.count selectors (previous li._postListItem selector matched nothing; DOM changed) - mentions: fix page.wait(500) → page.wait(0.5) (was waiting 500s not ms); use timestamp-suffixed URL to force fresh page load each run so the notification panel is closed; fix get_news vs get_news_count capture ambiguity with result_data.news check; replace cumulative waitForCaptures with waitForOneCapture (getInterceptedRequests clears array on each call) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Following up on the two issues flagged in the second review round — both are resolved, but the actual fixes landed differently than my earlier inline replies described (the code was further improved after live testing): 1. After testing, the regex approach still matched non-sidebar links. The final fix uses 2. The |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… locale-dependent text match Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- post: pass browser cookies to downloadMedia so Band's login-protected photo URLs don't fail with 401/403 - post: include photos.length in empty-result guard so photo-only posts are not falsely reported as not found - mentions: accumulate captures across poll iterations so get_news_count responses don't cause early exit before the real get_news arrives - mentions: update docstring to match actual implementation (client-side filtering, no tab-click) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All 4 issues from the third review round have been addressed in 071b647:
|
|
@copilot review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- mentions: fail fast with a clear error when bell button is not found, instead of silently no-op and waiting 8s before EmptyResultError - post: use shared formatCookieHeader() instead of manual cookie string construction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- posts: check limit before push so --limit 0 returns empty result
- post: indent replies proportionally by depth (' '.repeat(depth))
so multi-level threads remain readable in table output
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Pattern now requires /band/{id} or /band/{id}/post (with optional trailing
slash) so deeper paths like /band/{id}/post/{postNo} are excluded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- mentions: guard bell click with a boolean return so a disappearing element throws a clear EmptyResultError instead of a raw TypeError - post: wait for comment list container instead of first .cComment so posts with zero comments don't incur a fixed 6s delay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replaces document.cookie.includes('band_session') with
page.getCookies({ domain: 'band.us' }) so login detection works even
if Band.us marks the session cookie as HttpOnly in the future.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- mentions: replace EmptyResultError with SelectorError for missing/ disappeared bell button — produces a clearer SELECTOR error code - post: assign per-photo filenames using a global index across both download batches so band-hosted and CDN photos don't overwrite each other Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- post: derive file extension from URL path and include in filename (e.g. photo_1.jpg) so downloaded photos have correct extensions - posts: remove dead code guard (!url && !content) — url is always non-empty here since href-empty posts are already skipped above Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Domain-scoped getCookies may omit host-only cookies scoped to www.band.us; using url: 'https://www.band.us' ensures all relevant cookies are included in the auth header for Band-hosted photo downloads. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Required by CI doc-check --strict: every adapter in src/clis/ must have a corresponding docs/adapters/browser/*.md file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
All 4 commands are implemented and tested locally. The implementation went through multiple rounds of automated code review — all flagged issues have been addressed, including:
Ready for review when you have bandwidth. |
Summary
Adds a Band.us (
band.us) adapter with 4 commands:band bands— list all Bands you belong toband posts <band_no>— list posts from a Band (with--limit)band mentions— show notifications where you were @mentioned (supports--filter,--limit,--unread)band post <band_no> <post_no>— export full post content including nested comments, with optional--output <dir>for photo download and--comments falseto skip comment fetchImplementation notes
Band.us signs every API request with a per-request HMAC (
mdheader) generated by its own JavaScript — the header cannot be replicated externally.bands,posts,postuseStrategy.COOKIEwithnavigateBefore: falseand navigate directly to the target URL, extracting everything from the DOM (Band renders full data for logged-in users):bands: framework pre-nav toband.ushome, extract sidebar band list from DOMposts: directgototo/band/{band_no}/post, extract post list from DOMpost: directgototo/band/{band_no}/post/{post_no}, extract post body + nested replies from DOMmentionsusesStrategy.INTERCEPTbecause notification data is not rendered in the DOM — it is fetched via a signed XHR (get_news) that Band's JS generates. The interceptor captures this response after clicking the notification bell, then filters client-side.Comment hierarchy is handled by recursing into
.sReplyList .sCommentList— replies appear astype: replywith a└prefix in table output.Test plan
opencli band bands— lists your bands (requires Band login)opencli band posts <band_no> --limit 5— lists postsopencli band mentions— shows @mention notificationsopencli band post <band_no> <post_no>— exports post with commentsopencli band post <band_no> <post_no> --comments false— skips comment fetchopencli band post <band_no> <post_no> --output /tmp/photos— downloads photosnpx vitest run tests/e2e/browser-auth.test.ts🤖 Generated with Claude Code