posts.js
1 import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors'; 2 import { cli, Strategy } from '@jackwener/opencli/registry'; 3 /** 4 * band posts — List posts from a specific Band. 5 * 6 * Band.us renders the post list in the DOM for logged-in users, so we navigate 7 * directly to the band's post page and extract everything from the DOM — no XHR 8 * interception or home-page detour required. 9 */ 10 cli({ 11 site: 'band', 12 name: 'posts', 13 description: 'List posts from a Band', 14 domain: 'www.band.us', 15 strategy: Strategy.COOKIE, 16 navigateBefore: false, 17 browser: true, 18 args: [ 19 { 20 name: 'band_no', 21 positional: true, 22 required: true, 23 type: 'int', 24 help: 'Band number (get it from: band bands)', 25 }, 26 { name: 'limit', type: 'int', default: 20, help: 'Max results' }, 27 ], 28 columns: ['date', 'author', 'content', 'comments', 'url'], 29 func: async (page, kwargs) => { 30 const bandNo = Number(kwargs.band_no); 31 const limit = Number(kwargs.limit); 32 // Navigate directly to the band's post page — no home-page detour needed. 33 await page.goto(`https://www.band.us/band/${bandNo}/post`); 34 const cookies = await page.getCookies({ domain: 'band.us' }); 35 const isLoggedIn = cookies.some(c => c.name === 'band_session'); 36 if (!isLoggedIn) 37 throw new AuthRequiredError('band.us', 'Not logged in to Band'); 38 // Extract post list from the DOM. Poll until post items appear (React hydration). 39 const posts = await page.evaluate(` 40 (async () => { 41 const sleep = ms => new Promise(r => setTimeout(r, ms)); 42 const norm = s => (s || '').replace(/\\s+/g, ' ').trim(); 43 const limit = ${limit}; 44 45 // Wait up to 9 s for post items to render. 46 for (let i = 0; i < 30; i++) { 47 if (document.querySelector('article.cContentsCard._postMainWrap')) break; 48 await sleep(300); 49 } 50 51 // Band embeds custom <band:mention>, <band:sticker>, etc. tags in content. 52 const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, ''); 53 54 const results = []; 55 const postEls = Array.from( 56 document.querySelectorAll('article.cContentsCard._postMainWrap') 57 ); 58 59 for (const el of postEls) { 60 // URL: first post permalink link (absolute or relative). 61 const linkEl = el.querySelector('a[href*="/post/"]'); 62 const href = linkEl?.getAttribute('href') || ''; 63 if (!href) continue; 64 const url = href.startsWith('http') ? href : 'https://www.band.us' + href; 65 66 // Author name — a.text in the post header area. 67 const author = norm(el.querySelector('a.text')?.textContent); 68 69 // Date / timestamp. 70 const date = norm(el.querySelector('time')?.textContent); 71 72 // Post body text (strip Band markup tags, truncate for listing). 73 const bodyEl = el.querySelector('.postText._postText'); 74 const content = bodyEl 75 ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)).slice(0, 120) 76 : ''; 77 78 // Comment count is in span.count inside the count area. 79 const commentEl = el.querySelector('span.count'); 80 const comments = commentEl ? parseInt((commentEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0; 81 82 if (results.length >= limit) break; 83 results.push({ date, author, content, comments, url }); 84 } 85 86 return results; 87 })() 88 `); 89 if (!posts || posts.length === 0) { 90 throw new EmptyResultError('band posts', 'No posts found in this Band'); 91 } 92 return posts; 93 }, 94 });