/ clis / medium / utils.js
utils.js
 1  import { CommandExecutionError } from '@jackwener/opencli/errors';
 2  export function buildMediumTagUrl(topic) {
 3      return topic ? `https://medium.com/tag/${encodeURIComponent(topic)}` : 'https://medium.com/tag/technology';
 4  }
 5  export function buildMediumSearchUrl(keyword) {
 6      return `https://medium.com/search?q=${encodeURIComponent(keyword)}`;
 7  }
 8  export function buildMediumUserUrl(username) {
 9      return username.startsWith('@') ? `https://medium.com/${username}` : `https://medium.com/@${username}`;
10  }
11  export async function loadMediumPosts(page, url, limit) {
12      if (!page)
13          throw new CommandExecutionError('Browser session required for medium posts');
14      await page.goto(url);
15      await page.wait({ selector: 'article', timeout: 5 });
16      const data = await page.evaluate(`
17      (async () => {
18        await new Promise((resolve) => setTimeout(resolve, 3000));
19  
20        const limit = ${Math.max(1, Math.min(limit, 50))};
21        const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
22        const posts = [];
23        const seen = new Set();
24  
25        for (const article of Array.from(document.querySelectorAll('article'))) {
26          try {
27            const titleEl = article.querySelector('h2, h3, h1');
28            const title = normalize(titleEl?.textContent);
29            if (!title) continue;
30  
31            const linkEl = titleEl?.closest('a') || article.querySelector('a[href*="/@"], a[href*="/p/"]');
32            let url = linkEl?.getAttribute('href') || '';
33            if (!url) continue;
34            if (!url.startsWith('http')) url = 'https://medium.com' + url;
35            if (seen.has(url)) continue;
36  
37            const author = normalize(
38              Array.from(article.querySelectorAll('a[href^="/@"]'))
39                .map((node) => normalize(node.textContent))
40                .find((text) => text && text !== title),
41            );
42  
43            const allText = normalize(article.textContent);
44            const dateEl = article.querySelector('time');
45            const date = normalize(dateEl?.textContent) ||
46              dateEl?.getAttribute('datetime') ||
47              allText.match(/\\b(?:[A-Z][a-z]{2}\\s+\\d{1,2}|\\d+[dhmw]\\s+ago)\\b/)?.[0] ||
48              '';
49  
50            const readTime = allText.match(/(\\d+)\\s*min\\s*read/i)?.[0] || '';
51            const claps = allText.match(/\\b(\\d+(?:\\.\\d+)?[KkMm]?)\\s*claps?\\b/i)?.[1] || '';
52  
53            const description = normalize(
54              Array.from(article.querySelectorAll('h3, p'))
55                .map((node) => normalize(node.textContent))
56                .find((text) => text && text !== title && text !== author && !/member-only story|response icon/i.test(text)),
57            );
58  
59            seen.add(url);
60            posts.push({
61              rank: posts.length + 1,
62              title,
63              author,
64              date,
65              readTime,
66              claps,
67              description: description ? description.slice(0, 150) : '',
68              url,
69            });
70  
71            if (posts.length >= limit) break;
72          } catch {}
73        }
74  
75        return posts;
76      })()
77    `);
78      return Array.isArray(data) ? data : [];
79  }