// Cấu hình API const API_CONFIG = { baseUrl: 'https://jsonplaceholder.typicode.com', endpoints: { getUrls: '/posts' } };
// Cấu hình IndexedDB const DB_NAME = 'urlCrawlerDB'; const DB_VERSION = 1; const STORES = { SENT_URLS: 'sentUrls', CRAWLED_URLS: 'crawledUrls', PARENT_URLS: 'parentUrls' };
// Khởi tạo IndexedDB let db;
function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => { console.error('Lỗi khi mở cơ sở dữ liệu:', event.target.error); reject(event.target.error);
}; request.onsuccess = (event) => { db = event.target.result; resolve(db);
}; request.onupgradeneeded = (event) => { const db = event.target.result; // Tạo các kho lưu trữ đối tượng if (!db.objectStoreNames.contains(STORES.SENT_URLS)) { db.createObjectStore(STORES.SENT_URLS, { keyPath: 'id', autoIncrement: true }); } if (!db.objectStoreNames.contains(STORES.CRAWLED_URLS)) { db.createObjectStore(STORES.CRAWLED_URLS, { keyPath: 'parentId' }); } if (!db.objectStoreNames.contains(STORES.PARENT_URLS)) { db.createObjectStore(STORES.PARENT_URLS, { keyPath: 'id' }); }
};
}); }
// --- Quản lý Cache URL đã gửi --- async function getSentUrlsCache() { try { const transaction = db.transaction([STORES.SENT_URLS], 'readonly'); const store = transaction.objectStore(STORES.SENT_URLS); const request = store.getAll();
return new Promise((resolve, reject) => { request.onsuccess = () => { const urls = request.result.map(item => item.url); resolve({ urls, lastUpdated: new Date().toISOString() }); }; request.onerror = () => reject(request.error);
});
} catch (e) { console.error('Lỗi khi lấy cache URL đã gửi:', e); return { urls: [], lastUpdated: null }; } }
async function setSentUrlsCache(urls) { try { const transaction = db.transaction([STORES.SENT_URLS], 'readwrite'); const store = transaction.objectStore(STORES.SENT_URLS);
// Xóa dữ liệu hiện có
await new Promise((resolve, reject) => { const clearRequest = store.clear(); clearRequest.onsuccess = resolve; clearRequest.onerror = reject;
}); // Thêm URL mới
const uniqueUrls = Array.from(new Set(urls));
for (const url of uniqueUrls) { await new Promise((resolve, reject) => { const addRequest = store.add({ url, timestamp: new Date().toISOString() }); addRequest.onsuccess = resolve; addRequest.onerror = reject; });
} console.log(`✅ Đã lưu ${urls.length} URLs vào cache gửi đi.`);
} catch (e) { console.error('❌ Lỗi khi lưu cache URL đã gửi:', e); } }
async function addSentUrlsToCache(newUrls) { const currentCache = await getSentUrlsCache(); const updatedUrls = [...currentCache.urls, ...newUrls]; await setSentUrlsCache(updatedUrls); }
async function clearSentUrlsCacheIfOld() { try { const cacheData = await getSentUrlsCache(); if (cacheData && cacheData.lastUpdated) { const lastUpdatedDate = new Date(cacheData.lastUpdated); const today = new Date();
if (lastUpdatedDate.toDateString() !== today.toDateString()) { console.log('🗓️ Cache URL đã gửi cũ, đang xóa...'); const transaction = db.transaction([STORES.SENT_URLS], 'readwrite'); const store = transaction.objectStore(STORES.SENT_URLS); await new Promise((resolve, reject) => { const clearRequest = store.clear(); clearRequest.onsuccess = resolve; clearRequest.onerror = reject; }); console.log('✅ Đã xóa cache URL đã gửi cũ.'); return true; }
}
} catch (e) { console.error('Lỗi khi kiểm tra/xóa cache cũ:', e); } return false; }
// --- Quản lý Cache URL con đã crawl theo Parent ID --- async function getCrawledUrlsByParentIdCache() { try { const transaction = db.transaction([STORES.CRAWLED_URLS], 'readonly'); const store = transaction.objectStore(STORES.CRAWLED_URLS); const request = store.getAll();
return new Promise((resolve, reject) => { request.onsuccess = () => { const urlsByParentId = {}; request.result.forEach(item => { urlsByParentId[item.parentId] = new Set(item.urls); }); resolve({ urlsByParentId, lastUpdated: new Date().toISOString() }); }; request.onerror = () => reject(request.error);
});
} catch (e) { console.error('Lỗi khi lấy cache URL đã crawl theo parent ID:', e); return { urlsByParentId: {}, lastUpdated: null }; } }
async function setCrawledUrlsByParentIdCache(cacheData) { try { const transaction = db.transaction([STORES.CRAWLED_URLS], 'readwrite'); const store = transaction.objectStore(STORES.CRAWLED_URLS);
// Clear existing data
await new Promise((resolve, reject) => { const clearRequest = store.clear(); clearRequest.onsuccess = resolve; clearRequest.onerror = reject;
}); // Add new data
for (const parentId in cacheData.urlsByParentId) { await new Promise((resolve, reject) => { const addRequest = store.add({ parentId, urls: Array.from(cacheData.urlsByParentId[parentId]), timestamp: new Date().toISOString() }); addRequest.onsuccess = resolve; addRequest.onerror = reject; });
} console.log(`✅ Đã lưu cache URL đã crawl theo parent ID.`);
} catch (e) { console.error('❌ Lỗi khi lưu cache URL đã crawl theo parent ID:', e); } }
async function addCrawledUrlsForParentToCache(parentId, newUrls) { const currentCache = await getCrawledUrlsByParentIdCache(); if (!currentCache.urlsByParentId[parentId]) { currentCache.urlsByParentId[parentId] = new Set(); } newUrls.forEach(url => currentCache.urlsByParentId[parentId].add(url)); await setCrawledUrlsByParentIdCache(currentCache); }
async function clearCrawledUrlsByParentIdCacheIfOld() { try { const cacheData = await getCrawledUrlsByParentIdCache(); if (cacheData && cacheData.lastUpdated) { const lastUpdatedDate = new Date(cacheData.lastUpdated); const today = new Date();
if (lastUpdatedDate.toDateString() !== today.toDateString()) { console.log('🗓️ Cache URL đã crawl theo parent ID cũ, đang xóa...'); const transaction = db.transaction([STORES.CRAWLED_URLS], 'readwrite'); const store = transaction.objectStore(STORES.CRAWLED_URLS); await new Promise((resolve, reject) => { const clearRequest = store.clear(); clearRequest.onsuccess = resolve; clearRequest.onerror = reject; }); console.log('✅ Đã xóa cache URL đã crawl theo parent ID cũ.'); return true; }
}
} catch (e) { console.error('Lỗi khi kiểm tra/xóa cache cũ:', e); } return false; }
// --- Parent URL Management --- async function getParentFromCache(id) { try { const transaction = db.transaction([STORES.PARENT_URLS], 'readonly'); const store = transaction.objectStore(STORES.PARENT_URLS); const request = store.get(id);
return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
});
} catch (e) { console.error('Error getting parent from cache:', e); return null; } }
async function updateParentCache(id, childUrls) { try { const parentData = await getParentFromCache(id); if (parentData) { parentData.childUrls = [...new Set([...parentData.childUrls, ...childUrls])]; const transaction = db.transaction([STORES.PARENT_URLS], 'readwrite'); const store = transaction.objectStore(STORES.PARENT_URLS); await new Promise((resolve, reject) => { const putRequest = store.put(parentData); putRequest.onsuccess = resolve; putRequest.onerror = reject; }); } } catch (error) { console.error('Error updating parent cache:', error); } }
// Khởi tạo cơ sở dữ liệu khi extension được tải document.addEventListener('DOMContentLoaded', async () => { try { await initDB(); console.log('✅ Đã khởi tạo IndexedDB thành công'); } catch (error) { console.error('❌ Không thể khởi tạo IndexedDB:', error); } });
async function fetchUrlsFromApi() { const apiUrl = document.getElementById('apiUrl').value.trim(); const output = document.getElementById("output");
if (!apiUrl) { throw new Error('Vui lòng nhập URL API'); }
try { const response = await fetch(apiUrl); if (!response.ok) { throw new Error('Không thể kết nối đến API'); } const data = await response.json();
if (!Array.isArray(data)) { throw new Error('API phải trả về một mảng các object');
} const validUrls = data.filter(item => { try { if (!item.url || !item.id) return false; new URL(item.url); return true; } catch { return false; }
}); if (validUrls.length === 0) { throw new Error('Không tìm thấy URL hợp lệ nào trong dữ liệu API');
} // Lưu thông tin parent vào IndexedDB
for (const item of validUrls) { const transaction = db.transaction([STORES.PARENT_URLS], 'readwrite'); const store = transaction.objectStore(STORES.PARENT_URLS); await store.put({ id: item.id, url: item.url, childUrls: [] });
} return validUrls.map(item => item.url);
} catch (error) {
console.error('Lỗi khi lấy URL từ API:', error);
throw new Error(Lỗi khi lấy dữ liệu từ API: ${error.message}
);
}
}
async function extractLinksFromUrls(urls) { const output = document.getElementById("output"); output.textContent = "🚀 Đang xử lý...";
const allLinks = [];
const processedIds = new Set(); for (const parentUrl of urls) { const parentData = Object.values(JSON.parse(localStorage.getItem('urlToIdMap') || '{}')) .find(data => data.url === parentUrl); if (!parentData || processedIds.has(parentData.id)) { console.log(`Bỏ qua URL ${parentUrl} vì đã xử lý ID ${parentData?.id}`); continue; } await new Promise((resolve) => { chrome.tabs.create({ url: parentUrl, active: false }, (tab) => { const tabId = tab.id; const listener = (updatedTabId, changeInfo) => { if (updatedTabId === tabId && changeInfo.status === "complete") { chrome.scripting.executeScript({ target: { tabId }, func: () => { const links = []; let titles = document.querySelectorAll('.pr-title.js__card-title'); if (titles.length === 0) { const selectors = [ ".product-item-top a[href]", ".product-item a[href]", ".item-title a[href]", ".post-title a[href]", "article a[href]", ".content-wrapper a[href]" ]; for (const selector of selectors) { titles = document.querySelectorAll(selector); if (titles.length > 0) break; } if (titles.length === 0) { const allLinks = document.querySelectorAll('a[href]'); titles = Array.from(allLinks).filter(link => { const href = link.href.toLowerCase(); return (href.includes('/product/') || href.includes('/item/') || href.includes('/post/') || href.includes('/detail/') || href.match(/\d{4,}/)); }); } } titles = Array.from(titles).filter(title => { let element = title; while (element) { if (element.classList && Array.from(element.classList).some(cls => cls.includes('footer'))) { return false; } element = element.parentElement; } return true; }); titles.forEach((title) => { const link = title.closest('a') || title; if (link && link.href && !link.href.includes('#') && !link.href.includes('javascript:') && link.href.startsWith('http')) { links.push(link.href); } }); return Array.from(new Set(links)); } }).then((results) => { const links = results[0].result || []; updateParentCache(parentData.id, links); allLinks.push(...links); if (parentData.id) processedIds.add(parentData.id); chrome.tabs.remove(tabId); chrome.tabs.onUpdated.removeListener(listener); resolve(); }).catch((err) => { console.error("❌ Lỗi:", err); chrome.tabs.remove(tabId); chrome.tabs.onUpdated.removeListener(listener); resolve(); }); } }; chrome.tabs.onUpdated.addListener(listener); }); });
} return allLinks;
}
async function sendDataToApi(links) { const submitApiUrl = document.getElementById('submitApiUrl').value.trim(); const apiMethod = document.getElementById('apiMethod').value; const output = document.getElementById("output");
if (!submitApiUrl) { throw new Error('Vui lòng nhập URL API gửi kết quả');
} try { const urlToIdMap = JSON.parse(localStorage.getItem('urlToIdMap') || '{}'); console.log('URL to id Map:', urlToIdMap); const requestData = { data: links.map(url => ({ url: url, id: urlToIdMap[url] || null })) }; console.log('Request Data:', requestData); const response = await fetch(submitApiUrl, { method: apiMethod, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error('Không thể gửi dữ liệu lên API'); } const result = await response.json(); return result;
} catch (error) { console.error('Error sending data to API:', error); throw new Error(`Lỗi khi gửi dữ liệu lên API: ${error.message}`);
}
}
async function sendLinksToBackend(links, endpoint, method) { const output = document.getElementById("output"); if (!endpoint) { output.textContent = '❌ Vui lòng nhập URL API gửi kết quả.'; return; } try { const parentIds = new Set(); links.forEach(url => { const parentData = Object.values(JSON.parse(localStorage.getItem('urlToIdMap') || '{}')) .find(data => data.childUrls?.includes(url)); if (parentData) parentIds.add(parentData.id); });
const data = Array.from(parentIds).map(id => { const parentData = getParentFromCache(id); return { id: id, urls: parentData?.childUrls || [] }; }); const payload = { data }; console.log('payload gửi backend:', payload); output.textContent = '🔄 Đang gửi dữ liệu về backend...'; const response = await fetch(endpoint, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error('Không thể gửi dữ liệu lên API'); } const result = await response.json(); output.textContent = `✅ Đã gửi dữ liệu về backend thành công!`; return result;
} catch (error) { output.textContent = `❌ Lỗi: ${error.message}`;
}
}
document.getElementById("fetchFromApi").addEventListener("click", async () => { const output = document.getElementById("output"); output.textContent = "🔄 Đang lấy danh sách URL từ API..."; try { // Xóa cache cũ nếu đã qua ngày clearCrawledUrlsByParentIdCacheIfOld();
const apiUrl = document.getElementById('apiUrl').value.trim(); const submitApiUrl = document.getElementById('submitApiUrl').value.trim(); const apiMethod = document.getElementById('apiMethod').value; const filterToday = document.getElementById('filterToday').checked; const dataFromApi = await fetch(apiUrl); const urlObjs = await dataFromApi.json(); if (!Array.isArray(urlObjs) || urlObjs.length === 0) { output.textContent = "⚠️ Không tìm thấy URL nào từ API."; return; } // Lấy toàn bộ cache URL con theo parent ID const allCrawledUrlsCache = getCrawledUrlsByParentIdCache().urlsByParentId; console.log(`📊 Tổng số parent ID có cache: ${Object.keys(allCrawledUrlsCache).length}`); const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); const results = []; // Kết quả để gửi lên API for (const obj of urlObjs) { const { id, url } = obj; output.textContent = `🔄 Đang crawl: ${url}`; // Lấy cache URL con cho ID gốc hiện tại const currentParentCrawledUrls = allCrawledUrlsCache[id] || new Set(); console.log(`📊 Cache cho parent ID ${id}: ${currentParentCrawledUrls.size} URLs`); await new Promise((resolve) => { chrome.tabs.update(tab.id, {url}, () => { chrome.tabs.onUpdated.addListener(function listener(tabId, info) { if (tabId === tab.id && info.status === "complete") { chrome.tabs.onUpdated.removeListener(listener); // Gửi cache URL con CHỈ cho parent ID hiện tại chrome.tabs.sendMessage(tab.id, { action: "START_CRAWL", id, filterToday, crawledUrlsCache: Array.from(currentParentCrawledUrls) // Gửi mảng để serialize }, (response) => { if (response && response.success) { // response.links là mảng các URL con MỚI được tìm thấy cho parent ID này if (response.links && response.links.length > 0) { results.push({ id, urls: response.links }); // Thêm các URL con mới tìm được vào cache cho parent ID hiện tại addCrawledUrlsForParentToCache(id, response.links); } if (response.stoppedEarly) { console.log(`🚩 Dừng crawl sớm cho ${url} (ID: ${id}) do tìm thấy URL đã có trong cache của ID này.`); } } else if (response && response.error) { console.error(`❌ Lỗi crawl ${url} (ID: ${id}): ${response.error}`); } resolve(); }); } }); }); }); } // Tiếp tục gửi dữ liệu đến backend như bình thường if (results.length > 0) { const payload = { data: results }; output.textContent = '🔄 Đang gửi dữ liệu về backend...'; const response = await fetch(submitApiUrl, { method: apiMethod, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error('Không thể gửi dữ liệu lên API'); } output.textContent = `✅ Đã gửi dữ liệu về backend thành công!`; } else { output.textContent = '⚠️ Không có URL mới nào để gửi lên API.'; } } catch (error) { output.textContent = `❌ Lỗi: ${error.message}`;
}
});
// Chức năng tự động lấy dữ liệu let autoFetchInterval = null;
function startAutoFetch() { if (autoFetchInterval) { clearInterval(autoFetchInterval); }
// Kích hoạt lần lấy đầu tiên ngay lập tức
document.getElementById('fetchFromApi').click(); // Đặt khoảng thời gian cho mỗi 90 phút (90 * 60 * 1000 milliseconds)
autoFetchInterval = setInterval(() => { document.getElementById('fetchFromApi').click();
}, 90 * 60 * 1000);
}
function stopAutoFetch() { if (autoFetchInterval) { clearInterval(autoFetchInterval); autoFetchInterval = null; } }
// Thêm sự kiện cho checkbox tự động lấy dữ liệu document.addEventListener('DOMContentLoaded', function() { const autoFetchCheckbox = document.getElementById('autoFetch');
// Tải trạng thái đã lưu
chrome.storage.local.get(['autoFetchEnabled'], function(result) { if (result.autoFetchEnabled) { autoFetchCheckbox.checked = true; startAutoFetch(); }
}); autoFetchCheckbox.addEventListener('change', function() { if (this.checked) { startAutoFetch(); } else { stopAutoFetch(); } // Lưu trạng thái chrome.storage.local.set({ autoFetchEnabled: this.checked });
});
});
cảm ơn