#!/usr/bin/env node /** * dafont.js — ESM scraper/wrapper dafont.com (CLI + module) * * CLI: node dafont.js search|new|top|category|author|font|url * Module: import { search, getFont, getNewFonts, ... } from './dafont.js' */ import * as cheerio from 'cheerio'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; const BASE = 'https://www.dafont.com'; const DL = 'https://dl.dafont.com'; const H = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', Accept: 'text/html', 'Accept-Language': 'en-US,en;q=0.9', }; const get = async (url) => { const r = await fetch(url, { headers: H }); if (!r.ok) throw new Error(`HTTP ${r.status} → ${url}`); return r.text(); }; export const delay = (ms = 1200) => new Promise((r) => setTimeout(r, ms)); const num = (s) => { const m = String(s || '').match(/([\d,.]+)/); return m ? parseInt(m[1].replace(/[.,]/g, ''), 10) : null; }; const abs = (u) => !u ? null : u.startsWith('//') ? 'https:' + u : u.startsWith('http') ? u : BASE + (u.startsWith('/') ? u : '/' + u); const licenseOf = (t) => /100%\s*Free/i.test(t) ? '100% Free' : /Free for personal use/i.test(t) ? 'Free for personal use' : /Public domain|GPL|OFL/i.test(t) ? 'Public domain / GPL / OFL' : /Donationware/i.test(t) ? 'Donationware' : null; /** Parse list pages (new/top/category/author/search) */ function parseList(html) { const $ = cheerio.load(html); const out = [], seen = new Set(); $('div.lv1left').each((_, el) => { const $l = $(el); // slug: dari link .font di dalam, atau dari preview sibling let slug = null; const $a = $l.find('a[href$=".font"]').first(); if ($a.length) slug = ($a.attr('href') || '').replace(/^\//, '').replace(/\.font$/, ''); if (!slug) { const $p = $l.nextAll('div.preview').first().find('a[href$=".font"]').first(); if ($p.length) slug = ($p.attr('href') || '').replace(/^\//, '').replace(/\.font$/, ''); } if (!slug || seen.has(slug)) return; seen.add(slug); // name: strong / highlight / link text const name = $l.find('a[href$=".font"] strong').text().trim() || $l.find('span.highlight').text().trim() || $a.text().trim() || slug; const $auth = $l.find('a[href*=".d"]').first(); const author = $auth.length ? $auth.text().trim() : null; const authorSlug = $auth.length ? ($auth.attr('href') || '').replace(/^\//, '') : null; const $r = $l.nextAll('div.lv1right').first(); const $v = $l.nextAll('div.lv2right').first(); const $d = $l.nextAll('div.dlbox').first(); const $pr = $l.nextAll('div.preview').first(); const cats = []; $r.find('a').each((__, a) => { const t = $(a).text().trim(); if (t && t !== 'help') cats.push(t); }); const light = $v.find('span.light').text() || $v.text(); const downloads = num(light); const y = light.match(/\(([\d,.]+)\s+yesterday\)/i); const downloadsYesterday = y ? num(y[1]) : null; const license = licenseOf($v.find('a.help').text() || $v.text()); const dlHref = $d.find('a.dl').attr('href') || ''; const downloadUrl = abs(dlHref) || `${DL}/dl/?f=${slug.replace(/-/g, '_')}`; let preview = null; const bg = ($pr.attr('style') || '').match(/url\((['"]?)([^)'"]+)\1\)/); if (bg) preview = abs(bg[2]); out.push({ name, slug, url: `${BASE}/${slug}.font`, downloadUrl, preview, downloads, downloadsYesterday, license, author, authorSlug, categories: cats.length ? cats : null, }); }); return out; } export async function search(q, page = 1) { const qs = encodeURIComponent(q.trim()); const url = `${BASE}/search.php?q=${qs}${page > 1 ? `&page=${page}` : ''}`; return parseList(await get(url)); } export async function getNewFonts(page = 1) { return parseList(await get(page === 1 ? `${BASE}/new.php` : `${BASE}/new.php?page=${page}`)); } export async function getTopFonts(page = 1, period = 'yesterday') { let url = `${BASE}/top.php`; if (period === 'alltime') url += '?period=2'; if (page > 1) url += (url.includes('?') ? '&' : '?') + `page=${page}`; return parseList(await get(url)); } export async function getCategoryFonts(catId, page = 1) { const url = `${BASE}/theme.php?cat=${catId}${page > 1 ? `&page=${page}` : ''}`; return parseList(await get(url)); } export async function getAuthorFonts(authorSlug, page = 1) { const s = authorSlug.replace(/^\//, ''); return parseList(await get(page === 1 ? `${BASE}/${s}` : `${BASE}/${s}?page=${page}`)); } export async function getFont(slug) { const clean = slug.replace(/\.font$/, '').replace(/^\//, ''); const url = `${BASE}/${clean}.font`; const $ = cheerio.load(await get(url)); const name = $('div.lv1left a[href$=".font"] strong').first().text().trim() || $('div.lv1left span.highlight').text().trim() || clean; let author = null, authorSlug = null; $('div.lv1left a[href*=".d"]').each((_, el) => { const h = $(el).attr('href') || ''; if (/login|register/i.test(h)) return; author = $(el).text().trim(); authorSlug = h.replace(/^\//, ''); return false; }); const categories = []; $('div.lv1right a').each((_, el) => { const t = $(el).text().trim(); const h = $(el).attr('href') || ''; if (t && t !== 'help') categories.push({ name: t, url: abs(h) }); }); const light = $('div.lv2right span.light').text() || $('div.lv2right').text(); const downloads = num(light); const y = light.match(/\(([\d,.]+)\s+yesterday\)/i); const downloadsYesterday = y ? num(y[1]) : null; const license = licenseOf($('div.lv2right a.help').text() || $('div.lv2right').text()); const dlHref = $('div.dlbox a.dl').attr('href') || ''; const downloadUrl = abs(dlHref) || `${DL}/dl/?f=${clean.replace(/-/g, '_')}`; const files = []; $('span.filename').each((_, el) => { const t = $(el).text().trim(); if (t) files.push(t); }); if (!files.length) $('b').each((_, el) => { const t = $(el).text().trim(); if (/\.(ttf|otf|fon)$/i.test(t)) files.push(t); }); let preview = null; const bg = ($('div.preview').attr('style') || '').match(/url\((['"]?)([^)'"]+)\1\)/); if (bg) preview = abs(bg[2]); const ill = $('img[src*="/img/illustration/"]').attr('src'); const illustration = ill ? abs(ill) : null; let firstSeen = null; const fs = $('div.dfsmall').filter((_, el) => /First seen on DaFont/i.test($(el).text())).first().text(); const fm = fs.match(/First seen on DaFont:\s*(.+)/i); if (fm) firstSeen = fm[1].trim(); // note: ambil div setelah "Note of the author" let note = null; $('div').each((_, el) => { if (/Note of the author/i.test($(el).text()) && $(el).find('i b, b i').length) { const n = $(el).next('div').text().replace(/\s+/g, ' ').trim(); if (n && n.length < 800 && !/Cookies|Privacy Policy/i.test(n)) note = n; return false; } }); return { name, slug: clean, url, downloadUrl, preview, illustration, author, authorSlug, categories, downloads, downloadsYesterday, license, files: [...new Set(files)], firstSeen, note, }; } export function getDownloadUrl(slug) { return `${DL}/dl/?f=${slug.replace(/\.font$/, '').replace(/^\//, '').replace(/-/g, '_')}`; } // ---------- CLI ---------- const isCLI = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isCLI) { const [, , cmd, ...args] = process.argv; const j = (d) => console.log(JSON.stringify(d, null, 2)); const die = (m) => { console.error(m); process.exit(1); }; (async () => { try { switch (cmd) { case 'search': if (!args[0]) die('Usage: node dafont.js search [page]'); j(await search(args[0], +args[1] || 1)); break; case 'new': j(await getNewFonts(+args[0] || 1)); break; case 'top': j(await getTopFonts(+args[0] || 1, args[1] === 'alltime' ? 'alltime' : 'yesterday')); break; case 'category': case 'cat': if (!args[0]) die('Usage: node dafont.js category [page]'); j(await getCategoryFonts(args[0], +args[1] || 1)); break; case 'author': if (!args[0]) die('Usage: node dafont.js author [page]'); j(await getAuthorFonts(args[0], +args[1] || 1)); break; case 'font': case 'detail': if (!args[0]) die('Usage: node dafont.js font '); j(await getFont(args[0])); break; case 'url': if (!args[0]) die('Usage: node dafont.js url '); console.log(getDownloadUrl(args[0])); break; default: console.log(` dafont.js — dafont.com scraper (CLI + module) search [page] new [page] top [page] [yesterday|alltime] category [page] author [page] font url Contoh: node dafont.js search coolvetica node dafont.js new 1 node dafont.js top 1 alltime node dafont.js font super-bouncer `); } } catch (e) { console.error('Error:', e.message); process.exit(1); } })(); }