/ clis / band / posts.js
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  });