/*
* TİTCK KÜB / KT toplu PDF indirici
* Kullanım: [url]https://titck.gov.tr/kubkt[/url] sayfasında F12 > Console'a tamamını yapıştırın.
* Chrome / Microsoft Edge (File System Access API) gerekir.
* Dosyalar seçilen ana klasörün KUB ve KT alt klasörlerine ayrı ayrı kaydedilir.
*/
(() => {
'use strict';
if (window.__TITCK_PDF_TOPLU__?.panel?.isConnected) {
window.__TITCK_PDF_TOPLU__.panel.style.display = 'block';
console.info('TİTCK indirme paneli zaten açık.');
return;
}
if (location.hostname !== 'titck.gov.tr' || location.pathname.replace(/\/$/, '') !== '/kubkt') {
alert('Önce [url]https://titck.gov.tr/kubkt[/url] adresini açın.');
return;
}
if (!('showDirectoryPicker' in window)) {
alert('Klasör seçerek kaydetme için güncel Chrome veya Microsoft Edge kullanmalısınız.');
return;
}
const table = document.querySelector('#posts');
const $jq = window.jQuery;
if (!table || !$jq?.fn?.dataTable?.isDataTable(table)) {
alert('DataTables tablosu hazır değil. Sayfa tam yüklendikten sonra tekrar deneyin.');
return;
}
const dt = $jq(table).DataTable();
let rootFolder = null;
let kubFolder = null;
let ktFolder = null;
let running = false;
let paused = false;
let stopRequested = false;
let activeController = null;
let currentPage = 1;
let stats = { saved: 0, skipped: 0, failed: 0, missing: 0, rows: 0 };
const style = document.createElement('style');
style.id = 'titck-pdf-panel-style';
style.textContent = `
#titck-pdf-panel { all: initial; position:fixed; top:18px; right:18px; width:370px;
max-width:calc(100vw - 36px); max-height:calc(100vh - 36px); overflow:auto;
z-index:2147483647; background:#111827; color:#f9fafb; border:1px solid #4b5563;
border-radius:14px; box-shadow:0 18px 55px #0009; font:13px/1.5 Arial,sans-serif;
box-sizing:border-box; padding:18px; }
#titck-pdf-panel * { box-sizing:border-box; font-family:Arial,sans-serif; }
#titck-pdf-panel h2 { margin:0 0 8px; font-size:18px; color:#fff; }
#titck-pdf-panel p { margin:7px 0 12px; font-size:12px; color:#cbd5e1; }
#titck-pdf-panel .trow { display:grid; grid-template-columns:1fr 1fr; gap:9px; margin:10px 0; }
#titck-pdf-panel label { display:block; font-size:12px; color:#e2e8f0; }
#titck-pdf-panel input { display:block; width:100%; margin-top:4px; padding:8px 7px;
font-size:13px; color:#111827; background:#fff; border:1px solid #9ca3af; border-radius:7px; }
#titck-pdf-panel button { border:0; border-radius:8px; background:#374151; color:white;
font-size:12px; font-weight:bold; padding:10px 9px; cursor:pointer; }
#titck-pdf-panel button:hover { filter:brightness(1.18); }
#titck-pdf-panel button:disabled { opacity:.45; cursor:not-allowed; }
#titck-pdf-panel .full { width:100%; }
#titck-pdf-panel .action { display:grid; grid-template-columns:1fr 1fr 1fr; gap:7px; margin:12px 0; }
#titck-pdf-panel #titck-start { background:#047857; }
#titck-pdf-panel #titck-pause { background:#b45309; }
#titck-pdf-panel #titck-stop { background:#b91c1c; }
#titck-pdf-panel #titck-folder { background:#1d4ed8; }
#titck-pdf-panel #titck-log { background:#030712; border:1px solid #374151;
min-height:90px; max-height:180px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere;
color:#d1d5db; font:11px/1.5 Consolas,monospace; padding:8px; border-radius:7px; }
#titck-pdf-panel #titck-stats { white-space:pre-line; margin:10px 0;
font:12px/1.6 Consolas,monospace; color:#bfdbfe; }
#titck-pdf-panel #titck-status { color:#fef08a; font-weight:700; margin:7px 0; }
`;
document.head.appendChild(style);
const panel = document.createElement('section');
panel.id = 'titck-pdf-panel';
panel.innerHTML = `
<h2>📥 TİTCK KÜB / KT İndirici</h2>
<p>Seçtiğiniz klasöre <b>KUB/</b> ve <b>KT/</b> alt klasörleri oluşturulur. Her ilaç için iki ayrı PDF indirilir (sitede varsa).</p>
<button type="button" class="full" id="titck-folder">📁 Kayıt klasörünü seç</button>
<p id="titck-folder-label">Henüz klasör seçilmedi.</p>
<div class="trow">
<label>Başlangıç sayfası<input id="titck-first" type="number" min="1" step="1" value="1"></label>
<label>Bitiş sayfası<input id="titck-last" type="number" min="1" step="1" value="1"></label>
</div>
<div class="trow">
<label>PDF arası bekleme (ms)<input id="titck-pdf-delay" type="number" min="0" step="100" value="1000"></label>
<label>Sayfa arası bekleme (ms)<input id="titck-page-delay" type="number" min="0" step="100" value="1500"></label>
</div>
<div class="trow">
<label>PDF zaman aşımı (sn)<input id="titck-timeout" type="number" min="10" step="10" value="120"></label>
<label>Hata sonrası ek deneme<input id="titck-retries" type="number" min="0" max="8" step="1" value="2"></label>
</div>
<div class="action">
<button type="button" id="titck-start">▶ Başlat</button>
<button type="button" id="titck-pause" disabled>⏸ Duraklat</button>
<button type="button" id="titck-stop" disabled>■ Durdur</button>
</div>
<div id="titck-status">Hazır</div>
<div id="titck-stats"></div>
<div id="titck-log" aria-live="polite"></div>
<p>Not: Sekmeyi kapatmayın. Durdurup yeniden başlattığınızda aynı isimdeki mevcut PDF'ler atlanır.</p>
`;
document.body.appendChild(panel);
const el = id => panel.querySelector('#' + id);
const setStatus = s => { el('titck-status').textContent = s; };
const n = (id, fallback = 0) => {
const v = Number(el(id).value);
return Number.isFinite(v) ? v : fallback;
};
const maxPages = () => Math.max(1, dt.page.info().pages || 1);
el('titck-last').value = maxPages();
const log = message => {
const now = new Date().toLocaleTimeString('tr-TR');
const node = document.createElement('div');
node.textContent = `[${now}] ${message}`;
el('titck-log').appendChild(node);
while (el('titck-log').childNodes.length > 180) el('titck-log').firstChild.remove();
el('titck-log').scrollTop = el('titck-log').scrollHeight;
console.log('[TİTCK]', message);
};
const updateStats = () => {
el('titck-stats').textContent =
`Sayfa: ${currentPage}/${maxPages()}\n` +
`İncelenen ilaç: ${stats.rows}\n` +
`Kaydedilen PDF: ${stats.saved} | Atlanan: ${stats.skipped}\n` +
`Eksik bağlantı: ${stats.missing} | Başarısız: ${stats.failed}`;
};
const updateButtons = () => {
el('titck-start').disabled = running;
el('titck-folder').disabled = running;
el('titck-pause').disabled = !running;
el('titck-stop').disabled = !running;
el('titck-pause').textContent = paused ? '▶ Devam et' : '⏸ Duraklat';
};
updateStats();
el('titck-folder').addEventListener('click', async () => {
try {
rootFolder = await window.showDirectoryPicker({ mode: 'readwrite', id: 'titck-kub-kt' });
kubFolder = await rootFolder.getDirectoryHandle('KUB', { create: true });
ktFolder = await rootFolder.getDirectoryHandle('KT', { create: true });
el('titck-folder-label').textContent = `Seçili klasör: ${rootFolder.name} → KUB / KT`;
log('Kayıt klasörü seçildi: ' + rootFolder.name);
} catch (err) {
if (err.name !== 'AbortError') log('Klasör seçilemedi: ' + err.message);
}
});
function stoppedError() {
const error = new Error('İşlem durduruldu');
error.name = 'StoppedError';
return error;
}
async function checkPauseAndStop() {
if (stopRequested) throw stoppedError();
while (paused && !stopRequested) {
await new Promise(resolve => setTimeout(resolve, 200));
}
if (stopRequested) throw stoppedError();
}
async function waitMs(ms) {
const until = performance.now() + Math.max(0, ms);
do {
await checkPauseAndStop();
const left = until - performance.now();
if (left <= 0) break;
await new Promise(resolve => setTimeout(resolve, Math.min(200, left)));
} while (true);
}
async function gotoPage(zeroBasedPage) {
await checkPauseAndStop();
if (dt.page.info().page === zeroBasedPage) return;
await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error('Tablonun sayfa değiştirmesi zaman aşımına uğradı (60 sn)'));
}, 60000);
function cleanup() {
clearTimeout(timer);
$jq(table).off('draw.dt.titckbulk', onDraw);
}
function onDraw() {
if (dt.page.info().page === zeroBasedPage) {
cleanup();
resolve();
}
}
$jq(table).on('draw.dt.titckbulk', onDraw);
try { dt.page(zeroBasedPage).draw('page'); }
catch (err) { cleanup(); reject(err); }
});
}
function safeFileName(str) {
return String(str || 'Bilinmeyen_ilac')
.normalize('NFC')
.replace(/[\\/:*?"<>|\x00-\x1f]/g, '_')
.replace(/\s+/g, ' ')
.replace(/[. ]+$/g, '')
.slice(0, 105) || 'Bilinmeyen_ilac';
}
function shortHash(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(16).padStart(8, '0');
}
function jobsOnCurrentPage() {
const trList = Array.from(table.tBodies[0]?.querySelectorAll('tr') || []);
const jobs = [];
for (const tr of trList) {
const cells = tr.cells;
if (cells.length < 7) continue;
const drug = cells[0].textContent.trim() || 'Bilinmeyen_ilac';
stats.rows++;
for (const [col, type] of [[5, 'KUB'], [6, 'KT']]) {
const anchor = cells[col].querySelector('a[href]');
if (!anchor) { stats.missing++; continue; }
try {
const url = new URL(anchor.getAttribute('href'), location.href);
if (url.origin !== location.origin || !url.pathname.toLowerCase().endsWith('.pdf')) {
stats.missing++;
log(`PDF bağlantısı desteklenmiyor: ${drug} (${type})`);
continue;
}
const name = `${safeFileName(drug)}_${shortHash(url.href)}.pdf`;
jobs.push({ type, drug, name, url: url.href });
} catch (_) {
stats.missing++;
}
}
}
return jobs;
}
// Büyük PDF'leri RAM'e yüklemeden parça parça dosyaya yazar.
async function fetchAndSave(job, folder) {
const controller = new AbortController();
activeController = controller;
const timeoutMs = Math.max(10, n('titck-timeout', 120)) * 1000;
const timer = setTimeout(() => controller.abort(), timeoutMs);
let writer = null;
let fileWasCreated = false;
let reader = null;
try {
const response = await fetch(job.url, {
method: 'GET', credentials: 'same-origin', mode: 'same-origin',
cache: 'no-store', signal: controller.signal
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.httpStatus = response.status;
throw error;
}
if (!response.body) throw new Error('Sunucu PDF veri akışı döndürmedi');
reader = response.body.getReader();
// Hata / giriş / HTML sayfasını yanlışlıkla .pdf olarak kaydetmeyelim.
const headChunks = [];
let headSize = 0;
while (headSize < 1024) {
const part = await reader.read();
if (part.done) break;
headChunks.push(part.value);
headSize += part.value.byteLength;
}
const prefix = new Uint8Array(Math.min(headSize, 1024));
let offset = 0;
for (const piece of headChunks) {
if (offset >= prefix.length) break;
const chunk = piece.subarray(0, prefix.length - offset);
prefix.set(chunk, offset);
offset += chunk.length;
}
if (!new TextDecoder('latin1').decode(prefix).includes('%PDF-')) {
throw new Error('Gelen içerik PDF değil (HTML/hata yanıtı olabilir)');
}
const file = await folder.getFileHandle(job.name, { create: true });
fileWasCreated = true;
writer = await file.createWritable();
for (const piece of headChunks) await writer.write(piece);
while (true) {
const { value, done } = await reader.read();
if (done) break;
await writer.write(value);
}
await writer.close();
writer = null;
} catch (err) {
if (reader) { try { await reader.cancel(); } catch (_) {} }
if (writer) { try { await writer.abort(); } catch (_) {} }
if (fileWasCreated) {
try { await folder.removeEntry(job.name); } catch (_) {}
}
throw err;
} finally {
clearTimeout(timer);
if (activeController === controller) activeController = null;
}
}
async function saveJob(job) {
const folder = job.type === 'KUB' ? kubFolder : ktFolder;
try {
const existing = await folder.getFileHandle(job.name, { create: false });
if ((await existing.getFile()).size > 0) {
stats.skipped++;
updateStats();
return;
}
} catch (err) {
if (err.name !== 'NotFoundError') throw err;
}
const retries = Math.min(8, Math.max(0, Math.floor(n('titck-retries', 2))));
for (let attempt = 0; attempt <= retries; attempt++) {
await checkPauseAndStop();
try {
await fetchAndSave(job, folder);
stats.saved++;
updateStats();
return;
} catch (err) {
if (stopRequested) throw stoppedError();
const permanent = [400, 401, 403, 404, 410].includes(err.httpStatus);
if (attempt < retries && !permanent) {
const retryWait = err.httpStatus === 429 ? 30000 : 2500 * (attempt + 1);
log(`${job.type} deneme ${attempt + 1} başarısız: ${job.drug} (${err.message}), ${retryWait / 1000} sn sonra tekrar...`);
await waitMs(retryWait);
} else {
stats.failed++;
log(`BAŞARISIZ [${job.type}]: ${job.drug} — ${err.message}`);
updateStats();
return;
}
}
}
}
el('titck-pause').addEventListener('click', () => {
if (!running) return;
paused = !paused;
setStatus(paused ? 'Duraklatıldı (aktif PDF bitince)' : 'Çalışıyor');
updateButtons();
});
el('titck-stop').addEventListener('click', () => {
stopRequested = true;
paused = false;
if (activeController) activeController.abort();
setStatus('Durduruluyor...');
updateButtons();
});
el('titck-start').addEventListener('click', async () => {
if (running) return;
if (!rootFolder || !kubFolder || !ktFolder) {
alert('Önce kayıt klasörünü seçin.');
return;
}
const totalPages = maxPages();
const first = Math.floor(n('titck-first', 1));
const last = Math.floor(n('titck-last', totalPages));
if (first < 1 || last < first || last > totalPages) {
alert(`Sayfa aralığı 1-${totalPages} arasında olmalı.`);
return;
}
if (n('titck-pdf-delay') < 0 || n('titck-page-delay') < 0) {
alert('Bekleme süreleri negatif olamaz.');
return;
}
running = true;
paused = false;
stopRequested = false;
stats = { saved: 0, skipped: 0, failed: 0, missing: 0, rows: 0 };
currentPage = first;
updateButtons();
updateStats();
log(`Başladı: ${first}-${last}. sayfalar, PDF bekleme ${n('titck-pdf-delay')} ms.`);
try {
for (let page = first; page <= last; page++) {
await checkPauseAndStop();
currentPage = page;
setStatus(`Sayfa yükleniyor: ${page}/${last}`);
await gotoPage(page - 1);
await checkPauseAndStop();
const jobs = jobsOnCurrentPage();
log(`Sayfa ${page}/${last}: ${jobs.length} PDF bağlantısı bulundu.`);
updateStats();
for (const job of jobs) {
await checkPauseAndStop();
setStatus(`Sayfa ${page}/${last}: ${job.type} → ${job.drug.slice(0, 45)}`);
const before = stats.skipped;
await saveJob(job);
if (before === stats.skipped) await waitMs(n('titck-pdf-delay', 1000));
}
// Elle devam etmek için başlangıç sayfasını hazır tut.
el('titck-first').value = Math.min(page + 1, last);
if (page < last) await waitMs(n('titck-page-delay', 1500));
}
setStatus(`Tamamlandı. Kaydedilen: ${stats.saved} | Hata: ${stats.failed}`);
log('BİTTİ. Toplam kaydedilen PDF: ' + stats.saved);
} catch (err) {
if (err.name === 'StoppedError' || stopRequested) {
el('titck-first').value = currentPage;
setStatus(`Durduruldu. ${currentPage}. sayfadan yeniden başlayabilirsiniz.`);
log(`Durduruldu. ${currentPage}. sayfadan devam edebilirsiniz; mevcut dosyalar atlanır.`);
} else {
el('titck-first').value = currentPage;
setStatus('Hata oluştu: ' + err.message);
log(`GENEL HATA: ${err.stack || err.message}`);
}
} finally {
running = false;
paused = false;
stopRequested = false;
updateButtons();
updateStats();
}
});
window.__TITCK_PDF_TOPLU__ = { panel, getStats: () => ({ ...stats }), stop: () => el('titck-stop').click() };
log(`Panel hazır. Sitede ${maxPages()} sayfa algılandı.`);
console.info('TİTCK KÜB / KT indirme paneli sayfanın sağ üstünde açıldı.');
})();