()
| 271 | } |
| 272 | |
| 273 | async function queryVizData(): Promise<VizData> { |
| 274 | const db = await openDb(twitterBookmarksIndexPath()); |
| 275 | |
| 276 | try { |
| 277 | const total = db.exec('SELECT COUNT(*) FROM bookmarks')[0]?.values[0]?.[0] as number; |
| 278 | const authors = db.exec('SELECT COUNT(DISTINCT author_handle) FROM bookmarks')[0]?.values[0]?.[0] as number; |
| 279 | const timelineRows = db.exec( |
| 280 | `SELECT author_handle, posted_at, synced_at |
| 281 | FROM bookmarks |
| 282 | WHERE posted_at IS NOT NULL OR synced_at IS NOT NULL OR author_handle IS NOT NULL` |
| 283 | ); |
| 284 | const timelineData = aggregateTimelineData( |
| 285 | (timelineRows[0]?.values ?? []).map((row) => ({ |
| 286 | authorHandle: (row[0] as string) ?? undefined, |
| 287 | postedAt: (row[1] as string) ?? null, |
| 288 | syncedAt: (row[2] as string) ?? null, |
| 289 | })) |
| 290 | ); |
| 291 | |
| 292 | const topAuthorsRows = db.exec( |
| 293 | `SELECT author_handle, COUNT(*) as c FROM bookmarks |
| 294 | WHERE author_handle IS NOT NULL |
| 295 | GROUP BY author_handle ORDER BY c DESC LIMIT 20` |
| 296 | ); |
| 297 | |
| 298 | // Domains from links_json |
| 299 | const domainRows = db.exec( |
| 300 | `SELECT links_json FROM bookmarks WHERE links_json IS NOT NULL AND links_json != '[]'` |
| 301 | ); |
| 302 | const domainCounts = new Map<string, number>(); |
| 303 | for (const row of domainRows[0]?.values ?? []) { |
| 304 | try { |
| 305 | const links = JSON.parse(row[0] as string) as string[]; |
| 306 | for (const link of links) { |
| 307 | const url = typeof link === 'string' ? link : (link as any).expanded_url ?? (link as any).url ?? ''; |
| 308 | try { |
| 309 | const domain = new URL(url).hostname.replace(/^www\./, ''); |
| 310 | if (domain && domain !== 'x.com' && domain !== 't.co') { |
| 311 | domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1); |
| 312 | } |
| 313 | } catch {} |
| 314 | } |
| 315 | } catch {} |
| 316 | } |
| 317 | const topDomains = [...domainCounts.entries()] |
| 318 | .sort((a, b) => b[1] - a[1]) |
| 319 | .slice(0, 12) |
| 320 | .map(([domain, count]) => ({ domain, count })); |
| 321 | |
| 322 | const mediaStats = { |
| 323 | withMedia: db.exec('SELECT COUNT(*) FROM bookmarks WHERE media_count > 0')[0]?.values[0]?.[0] as number, |
| 324 | withLinks: db.exec('SELECT COUNT(*) FROM bookmarks WHERE link_count > 0')[0]?.values[0]?.[0] as number, |
| 325 | total, |
| 326 | }; |
| 327 | |
| 328 | const langRows = db.exec( |
| 329 | `SELECT language, COUNT(*) as c FROM bookmarks WHERE language IS NOT NULL |
| 330 | GROUP BY language ORDER BY c DESC LIMIT 8` |
no test coverage detected