feat: live registrar pricing endpoint and frontend integration
This commit is contained in:
parent
1049ccf5a7
commit
16b49ad833
148
api.php
148
api.php
@ -18,6 +18,71 @@ try {
|
|||||||
|
|
||||||
$action = $_GET['action'] ?? 'domains';
|
$action = $_GET['action'] ?? 'domains';
|
||||||
|
|
||||||
|
function dh_strip_html_text(string $html): string {
|
||||||
|
$html = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', ' ', $html);
|
||||||
|
$html = preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', ' ', $html);
|
||||||
|
$text = strip_tags($html ?? '');
|
||||||
|
$text = preg_replace('/\s+/u', ' ', $text ?? '');
|
||||||
|
return trim($text ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dh_fetch_text(string $url): ?string {
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'timeout' => 12,
|
||||||
|
'user_agent' => 'Mozilla/5.0 DomainHunterPanel/1.0',
|
||||||
|
'follow_location' => 1,
|
||||||
|
],
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => true,
|
||||||
|
'verify_peer_name' => true,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$raw = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($raw === false) return null;
|
||||||
|
return dh_strip_html_text($raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dh_find_price(string $text, string $tld, string $kind): ?float {
|
||||||
|
$hintsRegister = ['rejestr', 'rejestracja', 'new', 'first year', '1 rok'];
|
||||||
|
$hintsRenew = ['odnow', 'renew', 'renewal', 'kolejny', 'next year', '2 rok'];
|
||||||
|
$hints = $kind === 'renew' ? $hintsRenew : $hintsRegister;
|
||||||
|
|
||||||
|
$token = '.' . strtolower($tld);
|
||||||
|
$len = mb_strlen($text, 'UTF-8');
|
||||||
|
|
||||||
|
$pos = 0;
|
||||||
|
while (($idx = mb_stripos($text, $token, $pos, 'UTF-8')) !== false) {
|
||||||
|
$start = max(0, $idx - 240);
|
||||||
|
$chunk = mb_substr($text, $start, 520, 'UTF-8');
|
||||||
|
$chunkLow = mb_strtolower($chunk, 'UTF-8');
|
||||||
|
$ok = false;
|
||||||
|
foreach ($hints as $h) {
|
||||||
|
if (mb_strpos($chunkLow, $h, 0, 'UTF-8') !== false) { $ok = true; break; }
|
||||||
|
}
|
||||||
|
if ($ok && preg_match('/(\d{1,4}(?:[\.,]\d{1,2})?)\s*(zł|pln|eur|€|usd|\$)/iu', $chunkLow, $m)) {
|
||||||
|
return floatval(str_replace(',', '.', $m[1]));
|
||||||
|
}
|
||||||
|
$pos = $idx + mb_strlen($token, 'UTF-8');
|
||||||
|
if ($pos >= $len) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($hints as $h) {
|
||||||
|
$pos = 0;
|
||||||
|
while (($idx = mb_stripos($text, $h, $pos, 'UTF-8')) !== false) {
|
||||||
|
$start = max(0, $idx - 100);
|
||||||
|
$chunk = mb_substr($text, $start, 320, 'UTF-8');
|
||||||
|
if (preg_match('/(\d{1,4}(?:[\.,]\d{1,2})?)\s*(zł|pln|eur|€|usd|\$)/iu', $chunk, $m)) {
|
||||||
|
return floatval(str_replace(',', '.', $m[1]));
|
||||||
|
}
|
||||||
|
$pos = $idx + mb_strlen($h, 'UTF-8');
|
||||||
|
if ($pos >= $len) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($action === 'domains') {
|
if ($action === 'domains') {
|
||||||
$q = trim($_GET['q'] ?? '');
|
$q = trim($_GET['q'] ?? '');
|
||||||
$tld = trim($_GET['tld'] ?? '');
|
$tld = trim($_GET['tld'] ?? '');
|
||||||
@ -82,6 +147,89 @@ if ($action === 'domains') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'live_prices') {
|
||||||
|
$domain = trim($_GET['domain'] ?? '');
|
||||||
|
$tld = strtolower(trim($_GET['tld'] ?? ''));
|
||||||
|
if ($domain !== '' && $tld === '' && strpos($domain, '.') !== false) {
|
||||||
|
$parts = explode('.', $domain);
|
||||||
|
$tld = strtolower(end($parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tld === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'domain or tld required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$regPath = __DIR__ . '/data/registrars.json';
|
||||||
|
if (!file_exists($regPath)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'registrars.json missing']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$regData = json_decode(file_get_contents($regPath), true) ?: [];
|
||||||
|
$cachePath = __DIR__ . '/data/live-price-cache.json';
|
||||||
|
$cacheTtl = 1800;
|
||||||
|
$cache = [];
|
||||||
|
if (file_exists($cachePath)) {
|
||||||
|
$cache = json_decode(file_get_contents($cachePath), true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach (($regData['registrars'] ?? []) as $reg) {
|
||||||
|
$name = $reg['name'] ?? '';
|
||||||
|
if ($name === '') continue;
|
||||||
|
$autoUrl = $reg['autoPricing'][$tld]['url'] ?? null;
|
||||||
|
$baseUrl = $reg['url'] ?? null;
|
||||||
|
$key = strtolower($name) . '|' . $tld;
|
||||||
|
|
||||||
|
$entry = $cache[$key] ?? null;
|
||||||
|
if (!$entry || (($entry['ts'] ?? 0) + $cacheTtl < $now)) {
|
||||||
|
$register = null;
|
||||||
|
$renew = null;
|
||||||
|
$sourceUrl = $autoUrl ?: $baseUrl;
|
||||||
|
if ($sourceUrl) {
|
||||||
|
$text = dh_fetch_text($sourceUrl);
|
||||||
|
if ($text !== null) {
|
||||||
|
$register = dh_find_price($text, $tld, 'register');
|
||||||
|
$renew = dh_find_price($text, $tld, 'renew');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entry = [
|
||||||
|
'register_price' => $register,
|
||||||
|
'renew_price' => $renew,
|
||||||
|
'source_url' => $sourceUrl,
|
||||||
|
'ts' => $now,
|
||||||
|
];
|
||||||
|
$cache[$key] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items[] = [
|
||||||
|
'registrar' => $name,
|
||||||
|
'tld' => $tld,
|
||||||
|
'register_price' => $entry['register_price'] ?? null,
|
||||||
|
'renew_price' => $entry['renew_price'] ?? null,
|
||||||
|
'source_url' => $entry['source_url'] ?? null,
|
||||||
|
'live' => true,
|
||||||
|
'checked_at' => gmdate('c', intval($entry['ts'] ?? $now)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@file_put_contents($cachePath, json_encode($cache, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'domain' => $domain,
|
||||||
|
'tld' => $tld,
|
||||||
|
'cached_seconds' => $cacheTtl,
|
||||||
|
'items' => $items,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($action === 'prices') {
|
if ($action === 'prices') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$raw = json_decode(file_get_contents('php://input'), true) ?: [];
|
$raw = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
|||||||
65
index.html
65
index.html
@ -34,7 +34,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>Domain Hunter Panel</h1>
|
<h1>Domain Hunter Panel</h1>
|
||||||
<p class="muted">Dynamicznie z DB: domeny z Mongo + status nowości w kolejnych skanach.</p>
|
<p class="muted">Domeny z Mongo + ceny live per TLD pobierane dynamicznie z rejestratorów (z krótkim cache).</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Filtr domen</h2>
|
<h2>Filtr domen</h2>
|
||||||
@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let domains = [];
|
let domains = [];
|
||||||
let priceItems = [];
|
let livePriceByTld = {};
|
||||||
|
|
||||||
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; }
|
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; }
|
||||||
|
|
||||||
@ -84,24 +84,37 @@ function registrarDomainUrl(name, domain){
|
|||||||
return domainCheckUrl(domain);
|
return domainCheckUrl(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPrices(){
|
async function loadLivePricesForDomains(domainsList){
|
||||||
const u = new URL('./api.php', location.href);
|
const tlds = [...new Set((domainsList||[]).map(x => x.tld).filter(Boolean))];
|
||||||
u.searchParams.set('action','prices');
|
const next = {};
|
||||||
const d = await (await fetch(u)).json();
|
for (const tld of tlds){
|
||||||
priceItems = d.items || [];
|
const u = new URL('./api.php', location.href);
|
||||||
|
u.searchParams.set('action','live_prices');
|
||||||
|
u.searchParams.set('tld', tld);
|
||||||
|
try {
|
||||||
|
const d = await (await fetch(u)).json();
|
||||||
|
next[tld] = (d.items || []).map(x => ({
|
||||||
|
registrar: x.registrar,
|
||||||
|
register: x.register_price,
|
||||||
|
renew: x.renew_price,
|
||||||
|
twoYear: (x.register_price !== null && x.renew_price !== null) ? (Number(x.register_price) + Number(x.renew_price)) : null,
|
||||||
|
checkedAt: x.checked_at || null,
|
||||||
|
live: true
|
||||||
|
}));
|
||||||
|
} catch(e){
|
||||||
|
next[tld] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
livePriceByTld = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function topOffersByTld(tld){
|
function topOffersByTld(tld){
|
||||||
const offers = priceItems
|
const offers = (livePriceByTld[tld] || []).slice();
|
||||||
.filter(x => x.tld === tld && x.register_price !== null && x.renew_price !== null)
|
offers.sort((a,b)=>{
|
||||||
.map(x => ({
|
const av = a.twoYear === null ? Number.POSITIVE_INFINITY : a.twoYear;
|
||||||
registrar: x.registrar,
|
const bv = b.twoYear === null ? Number.POSITIVE_INFINITY : b.twoYear;
|
||||||
register: Number(x.register_price),
|
return av - bv;
|
||||||
renew: Number(x.renew_price),
|
});
|
||||||
twoYear: Number(x.register_price) + Number(x.renew_price)
|
|
||||||
}))
|
|
||||||
.filter(x => !Number.isNaN(x.register) && !Number.isNaN(x.renew))
|
|
||||||
.sort((a,b) => a.twoYear - b.twoYear);
|
|
||||||
return offers.slice(0,3);
|
return offers.slice(0,3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,9 +142,13 @@ function renderRows(){
|
|||||||
const offersHtml = offers.length
|
const offersHtml = offers.length
|
||||||
? offers.map(o => {
|
? offers.map(o => {
|
||||||
const link = registrarDomainUrl(o.registrar, d.domain);
|
const link = registrarDomainUrl(o.registrar, d.domain);
|
||||||
return `<div><a href='${link}' target='_blank'>${o.registrar}</a>: reg ${money(o.register)} / odn ${money(o.renew)} <span class='muted'>(2l: ${money(o.twoYear)})</span></div>`;
|
const hasPrice = o.register !== null && o.renew !== null;
|
||||||
|
if (hasPrice) {
|
||||||
|
return `<div><a href='${link}' target='_blank'>${o.registrar}</a>: reg ${money(o.register)} / odn ${money(o.renew)} <span class='muted'>(2l: ${money(o.twoYear)})</span></div>`;
|
||||||
|
}
|
||||||
|
return `<div><a href='${link}' target='_blank'>${o.registrar}</a>: <span class='muted'>cena live niedostępna — kliknij, aby sprawdzić dla ${d.domain}</span></div>`;
|
||||||
}).join('')
|
}).join('')
|
||||||
: `<span class='muted'>Brak danych cenowych dla .${d.tld}</span>`;
|
: `<span class='muted'>Brak danych live dla .${d.tld}</span>`;
|
||||||
|
|
||||||
const llm = (d.llm_score!==null && d.llm_score!==undefined) ? `${Number(d.llm_score).toFixed(1)} ${d.decision ? `(${d.decision})` : ''}` : '—';
|
const llm = (d.llm_score!==null && d.llm_score!==undefined) ? `${Number(d.llm_score).toFixed(1)} ${d.decision ? `(${d.decision})` : ''}` : '—';
|
||||||
tr.innerHTML = `<td><a href='${checkUrl}' target='_blank'><b>${d.domain}</b></a> ${d.is_new ? `<span class='chip'>NEW</span>` : ''}</td>
|
tr.innerHTML = `<td><a href='${checkUrl}' target='_blank'><b>${d.domain}</b></a> ${d.is_new ? `<span class='chip'>NEW</span>` : ''}</td>
|
||||||
@ -146,7 +163,7 @@ function renderRows(){
|
|||||||
|
|
||||||
async function refreshAll(){
|
async function refreshAll(){
|
||||||
await loadDomains();
|
await loadDomains();
|
||||||
await loadPrices();
|
await loadLivePricesForDomains(domains);
|
||||||
renderRows();
|
renderRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +176,7 @@ function setupAutoRefresh(){
|
|||||||
if(enabled){
|
if(enabled){
|
||||||
autoTimer = setInterval(async()=>{
|
autoTimer = setInterval(async()=>{
|
||||||
await loadDomains();
|
await loadDomains();
|
||||||
await loadPrices();
|
await loadLivePricesForDomains(domains);
|
||||||
renderRows();
|
renderRows();
|
||||||
}, 60000);
|
}, 60000);
|
||||||
}
|
}
|
||||||
@ -168,10 +185,10 @@ function setupAutoRefresh(){
|
|||||||
document.getElementById('reload').addEventListener('click', refreshAll);
|
document.getElementById('reload').addEventListener('click', refreshAll);
|
||||||
document.getElementById('q').addEventListener('input', async()=>{
|
document.getElementById('q').addEventListener('input', async()=>{
|
||||||
clearTimeout(qTimer);
|
clearTimeout(qTimer);
|
||||||
qTimer = setTimeout(async()=>{ await loadDomains(); renderRows(); }, 250);
|
qTimer = setTimeout(async()=>{ await loadDomains(); await loadLivePricesForDomains(domains); renderRows(); }, 250);
|
||||||
});
|
});
|
||||||
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); renderRows();});
|
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
|
||||||
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); renderRows();});
|
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
|
||||||
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
|
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
|
||||||
|
|
||||||
refreshAll();
|
refreshAll();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user