commit 0a834cb9f6e92b5f63a14f125e08103484114108 Author: Developer Date: Tue Mar 10 23:12:18 2026 +0800 v1.2 原始版本 - UI重设计前的备份 diff --git a/background.js b/background.js new file mode 100644 index 0000000..00c2857 --- /dev/null +++ b/background.js @@ -0,0 +1,1165 @@ +var STORAGE_KEY_PREFIX = 'magnet-progress-state:'; +var MAX_PROGRESS_ITEMS = 2000; +var SESSION_MARKER_KEY = 'magnet-progress-session-marker-v1'; +var CACHE_DB_NAME = 'magnet-thread-cache'; +var CACHE_DB_VERSION = 3; +var COVERAGE_LIMIT_PER_FORUM = 12; +var isSessionReady = false; +var isPreparingSession = false; +var sessionReadyCallbacks = []; + +function requestToPromise(request) { + return new Promise(function(resolve, reject) { + request.onsuccess = function(event) { + resolve(event.target.result); + }; + request.onerror = function(event) { + reject(event.target.error || new Error('IndexedDB 请求失败')); + }; + }); +} + +function transactionDone(transaction) { + return new Promise(function(resolve, reject) { + transaction.oncomplete = function() { + resolve(); + }; + transaction.onerror = function() { + reject(transaction.error || new Error('IndexedDB 事务失败')); + }; + transaction.onabort = function() { + reject(transaction.error || new Error('IndexedDB 事务已中止')); + }; + }); +} + +function openCacheDb() { + return new Promise(function(resolve, reject) { + var request = indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION); + + request.onupgradeneeded = function(event) { + var db = event.target.result; + + if (!db.objectStoreNames.contains('threads')) { + var threadStore = db.createObjectStore('threads', { keyPath: 'cacheKey' }); + threadStore.createIndex('forumKey', 'forumKey', { unique: false }); + threadStore.createIndex('lastSeenAt', 'lastSeenAt', { unique: false }); + threadStore.createIndex('forumKeyLastSeenAt', ['forumKey', 'lastSeenAt'], { unique: false }); + } else { + var existingThreadStore = request.transaction.objectStore('threads'); + if (!existingThreadStore.indexNames.contains('forumKeyLastSeenAt')) { + existingThreadStore.createIndex('forumKeyLastSeenAt', ['forumKey', 'lastSeenAt'], { unique: false }); + } + } + + if (!db.objectStoreNames.contains('coverages')) { + var coverageStore = db.createObjectStore('coverages', { keyPath: 'coverageKey' }); + coverageStore.createIndex('forumKey', 'forumKey', { unique: false }); + coverageStore.createIndex('crawledAt', 'crawledAt', { unique: false }); + coverageStore.createIndex('forumKeyCrawledAt', ['forumKey', 'crawledAt'], { unique: false }); + } else { + var existingCoverageStore = request.transaction.objectStore('coverages'); + if (!existingCoverageStore.indexNames.contains('forumKeyCrawledAt')) { + existingCoverageStore.createIndex('forumKeyCrawledAt', ['forumKey', 'crawledAt'], { unique: false }); + } + } + + if (!db.objectStoreNames.contains('pageCoverages')) { + var pageCoverageStore = db.createObjectStore('pageCoverages', { keyPath: 'coverageKey' }); + pageCoverageStore.createIndex('forumKey', 'forumKey', { unique: false }); + pageCoverageStore.createIndex('crawledAt', 'crawledAt', { unique: false }); + pageCoverageStore.createIndex('forumKeyCrawledAt', ['forumKey', 'crawledAt'], { unique: false }); + } else { + var existingPageCoverageStore = request.transaction.objectStore('pageCoverages'); + if (!existingPageCoverageStore.indexNames.contains('forumKeyCrawledAt')) { + existingPageCoverageStore.createIndex('forumKeyCrawledAt', ['forumKey', 'crawledAt'], { unique: false }); + } + } + }; + + request.onsuccess = function(event) { + resolve(event.target.result); + }; + + request.onerror = function(event) { + reject(event.target.error || new Error('无法打开缓存数据库')); + }; + }); +} + +function normalizeUrl(url) { + if (typeof url !== 'string' || !url) { + return ''; + } + + try { + var parsed = new URL(url); + parsed.hash = ''; + return parsed.href; + } catch (e) { + return url; + } +} + +function getThreadKeyFromUrl(url) { + var normalized = normalizeUrl(url); + if (!normalized) { + return ''; + } + + var match = normalized.match(/thread-(\d+)-/i) || normalized.match(/[?&]tid=(\d+)/i); + return match ? match[1] : normalized; +} + +function normalizeTitle(title) { + return String(title || '').replace(/\s+/g, ' ').trim(); +} + +function normalizeMagnets(magnets) { + var seen = Object.create(null); + var result = []; + + if (!Array.isArray(magnets)) { + return result; + } + + magnets.forEach(function(magnet) { + if (typeof magnet !== 'string' || !magnet) { + return; + } + + if (seen[magnet]) { + return; + } + + seen[magnet] = true; + result.push(magnet); + }); + + return result; +} + +function normalizeThreadEntry(thread, forumKey, crawledAt) { + if (!thread || typeof thread !== 'object') { + return null; + } + + var normalizedUrl = normalizeUrl(thread.url); + var normalizedTitle = normalizeTitle(thread.title); + var threadKey = typeof thread.threadKey === 'string' && thread.threadKey + ? thread.threadKey + : getThreadKeyFromUrl(normalizedUrl); + + if (!forumKey || !normalizedUrl || !threadKey) { + return null; + } + + return { + cacheKey: forumKey + '::' + threadKey, + forumKey: forumKey, + threadKey: threadKey, + url: normalizedUrl, + title: normalizedTitle, + titleNorm: normalizedTitle.toLowerCase(), + lastSeenAt: crawledAt, + firstSeenAt: crawledAt, + magnets: normalizeMagnets(thread.magnets), + magnetCount: Array.isArray(thread.magnets) ? normalizeMagnets(thread.magnets).length : 0, + lastMagnetSyncAt: Array.isArray(thread.magnets) && thread.magnets.length > 0 ? crawledAt : 0 + }; +} + +function getCoverageKey(forumKey, startPage, endPage) { + return forumKey + '::' + startPage + '-' + endPage; +} + +function getPageCoverageKey(forumKey, page) { + return forumKey + '::page::' + page; +} + +async function upsertThreadEntries(threadStore, forumKey, rawThreads, crawledAt) { + var seenThreadKeys = Object.create(null); + var threadKeys = []; + + for (var threadIndex = 0; threadIndex < rawThreads.length; threadIndex++) { + var thread = rawThreads[threadIndex]; + var normalized = normalizeThreadEntry(thread, forumKey, crawledAt); + if (!normalized || seenThreadKeys[normalized.cacheKey]) { + continue; + } + + seenThreadKeys[normalized.cacheKey] = true; + threadKeys.push(normalized.cacheKey); + + var existingRecord = await requestToPromise(threadStore.get(normalized.cacheKey)); + var mergedMagnets = existingRecord && Array.isArray(existingRecord.magnets) + ? existingRecord.magnets + : normalized.magnets; + var mergedLastMagnetSyncAt = existingRecord && Number(existingRecord.lastMagnetSyncAt) + ? Number(existingRecord.lastMagnetSyncAt) + : normalized.lastMagnetSyncAt; + + threadStore.put({ + cacheKey: normalized.cacheKey, + forumKey: normalized.forumKey, + threadKey: normalized.threadKey, + url: normalized.url, + title: normalized.title, + titleNorm: normalized.titleNorm, + firstSeenAt: existingRecord && Number(existingRecord.firstSeenAt) + ? Number(existingRecord.firstSeenAt) + : normalized.firstSeenAt, + lastSeenAt: normalized.lastSeenAt, + magnets: normalizeMagnets(mergedMagnets), + magnetCount: normalizeMagnets(mergedMagnets).length, + lastMagnetSyncAt: mergedLastMagnetSyncAt + }); + } + + return threadKeys; +} + +async function getAssembledPageCoverage(db, forumKey, startPage, endPage) { + var pageTx = db.transaction('pageCoverages', 'readonly'); + var pageStore = pageTx.objectStore('pageCoverages'); + var pageRequests = []; + + for (var page = startPage; page <= endPage; page++) { + pageRequests.push(requestToPromise(pageStore.get(getPageCoverageKey(forumKey, page)))); + } + + var pageRecords = await Promise.all(pageRequests); + await transactionDone(pageTx); + + if (pageRecords.some(function(record) { return !record; })) { + return null; + } + + var merged = Object.create(null); + var mergedThreadKeys = []; + var latestCrawledAt = 0; + + pageRecords.forEach(function(record) { + latestCrawledAt = Math.max(latestCrawledAt, Number(record.crawledAt || 0)); + (Array.isArray(record.threadKeys) ? record.threadKeys : []).forEach(function(threadKey) { + if (merged[threadKey]) { + return; + } + merged[threadKey] = true; + mergedThreadKeys.push(threadKey); + }); + }); + + return getCoverageWithThreads(db, { + forumKey: forumKey, + startPage: startPage, + endPage: endPage, + crawledAt: latestCrawledAt, + strategy: 'assembled_pages', + frontRefreshPages: 0, + threadKeys: mergedThreadKeys + }); +} + +async function getCoverageWithThreads(db, coverageRecord) { + if (!coverageRecord) { + return null; + } + + var threadKeys = Array.isArray(coverageRecord.threadKeys) ? coverageRecord.threadKeys : []; + var threads = []; + + if (threadKeys.length > 0) { + var threadTx = db.transaction('threads', 'readonly'); + var threadStore = threadTx.objectStore('threads'); + var threadRequests = threadKeys.map(function(threadKey) { + return requestToPromise(threadStore.get(threadKey)); + }); + var threadResults = await Promise.all(threadRequests); + await transactionDone(threadTx); + + threads = threadResults.filter(function(record) { + return !!record; + }).map(function(record) { + return { + url: record.url, + title: record.title, + threadKey: record.threadKey + }; + }); + } + + return { + forumKey: coverageRecord.forumKey, + startPage: coverageRecord.startPage, + endPage: coverageRecord.endPage, + crawledAt: coverageRecord.crawledAt, + strategy: coverageRecord.strategy || 'full_live', + frontRefreshPages: coverageRecord.frontRefreshPages || 0, + threads: threads + }; +} + +async function listForumCoverages(db, forumKey) { + return new Promise(function(resolve, reject) { + var tx = db.transaction('coverages', 'readonly'); + var store = tx.objectStore('coverages'); + var index = store.index('forumKey'); + var request = index.openCursor(IDBKeyRange.only(forumKey)); + var results = []; + + request.onsuccess = function(event) { + var cursor = event.target.result; + if (!cursor) { + resolve(results); + return; + } + + results.push(cursor.value); + cursor.continue(); + }; + + request.onerror = function(event) { + reject(event.target.error || new Error('读取覆盖快照失败')); + }; + }); +} + +async function pruneOldCoverages(db, forumKey) { + var coverages = await listForumCoverages(db, forumKey); + if (coverages.length <= COVERAGE_LIMIT_PER_FORUM) { + return; + } + + coverages.sort(function(a, b) { + return Number(b.crawledAt || 0) - Number(a.crawledAt || 0); + }); + + var stale = coverages.slice(COVERAGE_LIMIT_PER_FORUM); + if (stale.length === 0) { + return; + } + + var tx = db.transaction('coverages', 'readwrite'); + var store = tx.objectStore('coverages'); + stale.forEach(function(record) { + store.delete(record.coverageKey); + }); + await transactionDone(tx); +} + +async function saveCoverageSnapshot(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var startPage = Math.max(1, Number(payload.startPage) || 1); + var endPage = Math.max(startPage, Number(payload.endPage) || startPage); + var crawledAt = Number(payload.crawledAt) || Date.now(); + var frontRefreshPages = Math.max(0, Number(payload.frontRefreshPages) || 0); + var strategy = typeof payload.strategy === 'string' ? payload.strategy : 'full_live'; + var rawThreads = Array.isArray(payload.threads) ? payload.threads : []; + + var db = await openCacheDb(); + try { + var tx = db.transaction(['threads', 'coverages'], 'readwrite'); + var threadStore = tx.objectStore('threads'); + var coverageStore = tx.objectStore('coverages'); + var threadKeys = await upsertThreadEntries(threadStore, forumKey, rawThreads, crawledAt); + + coverageStore.put({ + coverageKey: getCoverageKey(forumKey, startPage, endPage), + forumKey: forumKey, + startPage: startPage, + endPage: endPage, + crawledAt: crawledAt, + strategy: strategy, + frontRefreshPages: frontRefreshPages, + threadKeys: threadKeys, + threadCount: threadKeys.length + }); + + await transactionDone(tx); + await pruneOldCoverages(db, forumKey); + + return { + ok: true, + threadCount: threadKeys.length, + coverageKey: getCoverageKey(forumKey, startPage, endPage) + }; + } finally { + db.close(); + } +} + +async function savePageCoverageSnapshot(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var page = Math.max(1, Number(payload.page) || 1); + var crawledAt = Number(payload.crawledAt) || Date.now(); + var rawThreads = Array.isArray(payload.threads) ? payload.threads : []; + + var db = await openCacheDb(); + try { + var tx = db.transaction(['threads', 'pageCoverages'], 'readwrite'); + var threadStore = tx.objectStore('threads'); + var pageCoverageStore = tx.objectStore('pageCoverages'); + var threadKeys = await upsertThreadEntries(threadStore, forumKey, rawThreads, crawledAt); + + pageCoverageStore.put({ + coverageKey: getPageCoverageKey(forumKey, page), + forumKey: forumKey, + page: page, + crawledAt: crawledAt, + threadKeys: threadKeys, + threadCount: threadKeys.length + }); + + await transactionDone(tx); + + return { + ok: true, + page: page, + threadCount: threadKeys.length, + coverageKey: getPageCoverageKey(forumKey, page) + }; + } finally { + db.close(); + } +} + +async function getCachedThreadMagnets(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var rawThreads = Array.isArray(payload.threads) ? payload.threads : []; + var db = await openCacheDb(); + + try { + var tx = db.transaction('threads', 'readonly'); + var threadStore = tx.objectStore('threads'); + var results = []; + + for (var threadIndex = 0; threadIndex < rawThreads.length; threadIndex++) { + var thread = rawThreads[threadIndex]; + var normalized = normalizeThreadEntry(thread, forumKey, Date.now()); + if (!normalized) { + continue; + } + + var record = await requestToPromise(threadStore.get(normalized.cacheKey)); + if (!record) { + results.push({ + threadKey: normalized.threadKey, + url: normalized.url, + title: normalized.title, + magnets: [] + }); + continue; + } + + results.push({ + threadKey: record.threadKey, + url: record.url, + title: record.title, + magnets: normalizeMagnets(record.magnets), + lastMagnetSyncAt: Number(record.lastMagnetSyncAt) || 0 + }); + } + + await transactionDone(tx); + + return { + ok: true, + threads: results + }; + } finally { + db.close(); + } +} + +async function saveThreadMagnets(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var rawThreads = Array.isArray(payload.threads) ? payload.threads : []; + var syncedAt = Number(payload.syncedAt) || Date.now(); + var db = await openCacheDb(); + + try { + var tx = db.transaction('threads', 'readwrite'); + var threadStore = tx.objectStore('threads'); + var savedCount = 0; + + for (var threadIndex = 0; threadIndex < rawThreads.length; threadIndex++) { + var thread = rawThreads[threadIndex]; + var normalized = normalizeThreadEntry(thread, forumKey, syncedAt); + if (!normalized) { + continue; + } + + var existingRecord = await requestToPromise(threadStore.get(normalized.cacheKey)); + var normalizedMagnets = normalizeMagnets(thread.magnets); + var finalMagnets = normalizedMagnets.length > 0 + ? normalizedMagnets + : (existingRecord && Array.isArray(existingRecord.magnets) ? normalizeMagnets(existingRecord.magnets) : []); + + threadStore.put({ + cacheKey: normalized.cacheKey, + forumKey: normalized.forumKey, + threadKey: normalized.threadKey, + url: normalized.url, + title: normalized.title || (existingRecord ? existingRecord.title : ''), + titleNorm: normalizeTitle(normalized.title || (existingRecord ? existingRecord.title : '')).toLowerCase(), + firstSeenAt: existingRecord && Number(existingRecord.firstSeenAt) + ? Number(existingRecord.firstSeenAt) + : normalized.firstSeenAt, + lastSeenAt: existingRecord && Number(existingRecord.lastSeenAt) + ? Number(existingRecord.lastSeenAt) + : normalized.lastSeenAt, + magnets: finalMagnets, + magnetCount: finalMagnets.length, + lastMagnetSyncAt: finalMagnets.length > 0 ? syncedAt : (existingRecord && Number(existingRecord.lastMagnetSyncAt) ? Number(existingRecord.lastMagnetSyncAt) : 0) + }); + + savedCount += 1; + } + + await transactionDone(tx); + + return { + ok: true, + savedCount: savedCount + }; + } finally { + db.close(); + } +} + +function estimateTextSize(value) { + try { + return JSON.stringify(value).length; + } catch (e) { + return 0; + } +} + +async function getStorageEstimate() { + if (!navigator.storage || !navigator.storage.estimate) { + return null; + } + + try { + return await navigator.storage.estimate(); + } catch (e) { + return null; + } +} + +async function readRecentThreadRecords(db, forumKey, limit) { + var tx = db.transaction('threads', 'readonly'); + var store = tx.objectStore('threads'); + var index = forumKey ? store.index('forumKeyLastSeenAt') : store.index('lastSeenAt'); + var range = forumKey + ? IDBKeyRange.bound([forumKey, 0], [forumKey, Number.MAX_SAFE_INTEGER]) + : null; + var request = index.openCursor(range, 'prev'); + + return new Promise(function(resolve, reject) { + var items = []; + + request.onsuccess = function(event) { + var cursor = event.target.result; + if (!cursor || items.length >= limit) { + resolve(items); + return; + } + + items.push(cursor.value); + cursor.continue(); + }; + + request.onerror = function(event) { + reject(event.target.error || new Error('读取最近帖子缓存失败')); + }; + }).finally(function() { + return transactionDone(tx).catch(function() { + return null; + }); + }); +} + +async function readRecentCoverageRecords(db, forumKey, limit) { + var tx = db.transaction('coverages', 'readonly'); + var store = tx.objectStore('coverages'); + var index = forumKey ? store.index('forumKeyCrawledAt') : store.index('crawledAt'); + var range = forumKey + ? IDBKeyRange.bound([forumKey, 0], [forumKey, Number.MAX_SAFE_INTEGER]) + : null; + var request = index.openCursor(range, 'prev'); + + return new Promise(function(resolve, reject) { + var items = []; + + request.onsuccess = function(event) { + var cursor = event.target.result; + if (!cursor || items.length >= limit) { + resolve(items); + return; + } + + items.push(cursor.value); + cursor.continue(); + }; + + request.onerror = function(event) { + reject(event.target.error || new Error('读取最近范围快照失败')); + }; + }).finally(function() { + return transactionDone(tx).catch(function() { + return null; + }); + }); +} + +async function getCacheOverview(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var limit = Math.max(1, Math.min(Number(payload.limit) || 12, 50)); + var db = await openCacheDb(); + + try { + var threadTx = db.transaction('threads', 'readonly'); + var coverageTx = db.transaction(['coverages', 'pageCoverages'], 'readonly'); + var threadStore = threadTx.objectStore('threads'); + var coverageStore = coverageTx.objectStore('coverages'); + var pageCoverageStore = coverageTx.objectStore('pageCoverages'); + var threadForumIndex = threadStore.index('forumKey'); + var coverageForumIndex = coverageStore.index('forumKey'); + var pageCoverageForumIndex = pageCoverageStore.index('forumKey'); + + var totals = await Promise.all([ + requestToPromise(threadStore.count()), + requestToPromise(coverageStore.count()), + requestToPromise(pageCoverageStore.count()), + forumKey ? requestToPromise(threadForumIndex.count(IDBKeyRange.only(forumKey))) : Promise.resolve(0), + forumKey ? requestToPromise(coverageForumIndex.count(IDBKeyRange.only(forumKey))) : Promise.resolve(0), + forumKey ? requestToPromise(pageCoverageForumIndex.count(IDBKeyRange.only(forumKey))) : Promise.resolve(0) + ]); + + await Promise.all([transactionDone(threadTx), transactionDone(coverageTx)]); + + var recentThreads = await readRecentThreadRecords(db, forumKey, limit); + var recentCoverages = await readRecentCoverageRecords(db, forumKey, Math.min(limit, 8)); + var usedBytes = recentThreads.reduce(function(sum, item) { + return sum + estimateTextSize(item); + }, 0); + var storageEstimate = await getStorageEstimate(); + var magnetCachedThreads = recentThreads.filter(function(item) { + return Number(item.magnetCount || 0) > 0; + }).length; + var magnetCount = recentThreads.reduce(function(sum, item) { + return sum + Number(item.magnetCount || 0); + }, 0); + + return { + ok: true, + summary: { + forumKey: forumKey, + totalThreadCount: Number(totals[0] || 0), + totalCoverageCount: Number(totals[1] || 0), + totalPageCoverageCount: Number(totals[2] || 0), + forumThreadCount: Number(totals[3] || 0), + forumCoverageCount: Number(totals[4] || 0), + forumPageCoverageCount: Number(totals[5] || 0), + sampleBytes: usedBytes, + storageUsage: storageEstimate && Number(storageEstimate.usage) ? Number(storageEstimate.usage) : 0, + storageQuota: storageEstimate && Number(storageEstimate.quota) ? Number(storageEstimate.quota) : 0, + recentMagnetCachedThreads: magnetCachedThreads, + recentMagnetCount: magnetCount + }, + recentThreads: recentThreads.map(function(item) { + return { + title: item.title, + url: item.url, + threadKey: item.threadKey, + magnetCount: Number(item.magnetCount || 0), + lastSeenAt: Number(item.lastSeenAt || 0), + lastMagnetSyncAt: Number(item.lastMagnetSyncAt || 0) + }; + }), + recentCoverages: recentCoverages.map(function(item) { + return { + forumKey: item.forumKey, + startPage: Number(item.startPage || 0), + endPage: Number(item.endPage || 0), + threadCount: Number(item.threadCount || 0), + crawledAt: Number(item.crawledAt || 0), + strategy: item.strategy || 'full_live' + }; + }) + }; + } finally { + db.close(); + } +} + +async function clearCacheData() { + var db = await openCacheDb(); + + try { + var tx = db.transaction(['threads', 'coverages', 'pageCoverages'], 'readwrite'); + tx.objectStore('threads').clear(); + tx.objectStore('coverages').clear(); + tx.objectStore('pageCoverages').clear(); + await transactionDone(tx); + + return { ok: true }; + } finally { + db.close(); + } +} + +async function getCachedCoveragePlan(payload) { + var forumKey = typeof payload.forumKey === 'string' ? payload.forumKey : ''; + var startPage = Math.max(1, Number(payload.startPage) || 1); + var endPage = Math.max(startPage, Number(payload.endPage) || startPage); + var frontRefreshPages = Math.max(0, Number(payload.frontRefreshPages) || 0); + + var db = await openCacheDb(); + try { + var exactKey = getCoverageKey(forumKey, startPage, endPage); + var exactTx = db.transaction('coverages', 'readonly'); + var exactStore = exactTx.objectStore('coverages'); + var exactRecord = await requestToPromise(exactStore.get(exactKey)); + await transactionDone(exactTx); + + var exactCoverage = exactRecord ? await getCoverageWithThreads(db, exactRecord) : null; + if (!exactCoverage) { + exactCoverage = await getAssembledPageCoverage(db, forumKey, startPage, endPage); + } + if (exactCoverage) { + return { + ok: true, + exactCoverage: exactCoverage, + shiftedCoverage: null + }; + } + + var shiftedCoverage = null; + if (startPage === 1 && frontRefreshPages > 0) { + var coverages = await listForumCoverages(db, forumKey); + coverages = coverages.filter(function(record) { + return Number(record.startPage) === 1 && Number(record.endPage) >= 1; + }).sort(function(a, b) { + var crawlDiff = Number(b.crawledAt || 0) - Number(a.crawledAt || 0); + if (crawlDiff !== 0) { + return crawlDiff; + } + return Number(b.endPage || 0) - Number(a.endPage || 0); + }); + + if (coverages.length > 0) { + var bestCoverage = coverages[0]; + var hydratedCoverage = await getCoverageWithThreads(db, bestCoverage); + if (hydratedCoverage && Array.isArray(hydratedCoverage.threads) && hydratedCoverage.threads.length > 0) { + shiftedCoverage = { + forumKey: hydratedCoverage.forumKey, + sourceStartPage: hydratedCoverage.startPage, + sourceEndPage: hydratedCoverage.endPage, + crawledAt: hydratedCoverage.crawledAt, + strategy: hydratedCoverage.strategy, + threads: hydratedCoverage.threads, + reusedStartPage: Math.min(endPage, startPage + frontRefreshPages), + reusedEndPage: Math.min(endPage, bestCoverage.endPage + frontRefreshPages) + }; + + if (shiftedCoverage.reusedStartPage > shiftedCoverage.reusedEndPage) { + shiftedCoverage = null; + } + } + } + } + + return { + ok: true, + exactCoverage: null, + shiftedCoverage: shiftedCoverage + }; + } finally { + db.close(); + } +} + +function isProgressStateKey(key) { + return typeof key === 'string' && key.indexOf(STORAGE_KEY_PREFIX) === 0; +} + +function flushSessionReadyCallbacks() { + var callbacks = sessionReadyCallbacks.slice(); + sessionReadyCallbacks = []; + callbacks.forEach(function(callback) { + try { + callback(); + } catch (e) { + // ignore callback errors + } + }); +} + +function finishSessionPrepare() { + isSessionReady = true; + isPreparingSession = false; + flushSessionReadyCallbacks(); +} + +function clearAllProgressStates(done) { + chrome.storage.local.get(null, function(result) { + if (chrome.runtime.lastError) { + done(); + return; + } + + var keys = Object.keys(result || {}).filter(isProgressStateKey); + if (keys.length === 0) { + done(); + return; + } + + chrome.storage.local.remove(keys, function() { + done(); + }); + }); +} + +function ensureSessionReady(callback) { + if (isSessionReady) { + callback(); + return; + } + + sessionReadyCallbacks.push(callback); + if (isPreparingSession) { + return; + } + + isPreparingSession = true; + if (!chrome.storage || !chrome.storage.session) { + finishSessionPrepare(); + return; + } + + chrome.storage.session.get(SESSION_MARKER_KEY, function(result) { + if (chrome.runtime.lastError) { + finishSessionPrepare(); + return; + } + + if (result && result[SESSION_MARKER_KEY]) { + finishSessionPrepare(); + return; + } + + clearAllProgressStates(function() { + var marker = {}; + marker[SESSION_MARKER_KEY] = Date.now(); + chrome.storage.session.set(marker, function() { + finishSessionPrepare(); + }); + }); + }); +} + +function clampTimeout(timeoutMs) { + var parsed = Number(timeoutMs); + if (!Number.isFinite(parsed)) return 15000; + return Math.max(3000, Math.min(parsed, 30000)); +} + +function getStorageKeyBySender(sender) { + if (!sender || !sender.tab || typeof sender.tab.id !== 'number') { + return null; + } + return STORAGE_KEY_PREFIX + sender.tab.id; +} + +function normalizeProgressState(state) { + if (!state || typeof state !== 'object') { + return null; + } + + var speed = typeof state.speedMode === 'string' ? state.speedMode : 'fast'; + if (speed !== 'slow' && speed !== 'medium' && speed !== 'fast' && speed !== 'ultrafast') { + speed = 'fast'; + } + + var startPage = Number(state.startPage); + var endPage = Number(state.endPage); + if (!Number.isFinite(startPage) || startPage < 1) startPage = 1; + if (!Number.isFinite(endPage) || endPage < 1) endPage = startPage; + + var resumeFromPage = Number(state.resumeFromPage); + if (!Number.isFinite(resumeFromPage) || resumeFromPage < 1) { + resumeFromPage = startPage; + } + + var isRunning = !!state.isRunning; + var stoppedByUser = !!state.stoppedByUser; + + var linkSet = new Set(); + var links = []; + if (Array.isArray(state.links)) { + state.links.forEach(function(link) { + if (typeof link !== 'string' || !link) return; + if (linkSet.has(link)) return; + linkSet.add(link); + links.push(link); + }); + } + + var items = []; + if (Array.isArray(state.items)) { + state.items.forEach(function(item) { + if (!item || typeof item !== 'object') return; + var link = typeof item.link === 'string' ? item.link : ''; + if (!link) return; + var title = typeof item.title === 'string' ? item.title : '恢复记录'; + if (!linkSet.has(link)) { + linkSet.add(link); + links.push(link); + } + items.push({ + title: title.slice(0, 200), + link: link + }); + }); + } + + if (links.length > MAX_PROGRESS_ITEMS) { + links = links.slice(links.length - MAX_PROGRESS_ITEMS); + } + if (items.length > MAX_PROGRESS_ITEMS) { + items = items.slice(items.length - MAX_PROGRESS_ITEMS); + } + + return { + links: links, + items: items, + keyword: typeof state.keyword === 'string' ? state.keyword.slice(0, 200) : '', + speedMode: speed, + startPage: startPage, + endPage: endPage, + resumeFromPage: resumeFromPage, + isRunning: isRunning, + stoppedByUser: stoppedByUser, + statusText: typeof state.statusText === 'string' ? state.statusText.slice(0, 300) : '', + statusType: typeof state.statusType === 'string' ? state.statusType.slice(0, 30) : '', + pageUrl: typeof state.pageUrl === 'string' ? state.pageUrl.slice(0, 1000) : '', + updatedAt: Date.now() + }; +} + +function fetchPageHtml(url, timeoutMs) { + var controller = new AbortController(); + var realTimeout = clampTimeout(timeoutMs); + var timer = setTimeout(function() { + controller.abort(); + }, realTimeout); + + return fetch(url, { signal: controller.signal }) + .then(function(response) { + if (!response.ok) { + throw new Error('HTTP ' + response.status + ' ' + response.statusText); + } + return response.text(); + }) + .catch(function(err) { + if (err && err.name === 'AbortError') { + throw new Error('请求超时(' + realTimeout + 'ms)'); + } + throw err; + }) + .finally(function() { + clearTimeout(timer); + }); +} + +function extractMagnetsFromHtml(html) { + const magnetPattern = /magnet:\?xt=urn:btih:[a-fA-F0-9]{32,40}/gi; + const magnets = html.match(magnetPattern) || []; + return [...new Set(magnets)]; +} + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'cacheSaveCoverage') { + saveCoverageSnapshot(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheGetCoveragePlan') { + getCachedCoveragePlan(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheGetThreadMagnets') { + getCachedThreadMagnets(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheSaveThreadMagnets') { + saveThreadMagnets(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheSavePageCoverage') { + savePageCoverageSnapshot(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheGetOverview') { + getCacheOverview(request) + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'cacheClearAll') { + clearCacheData() + .then(function(result) { + sendResponse(result); + }) + .catch(function(err) { + sendResponse({ ok: false, error: err && err.message ? err.message : String(err) }); + }); + + return true; + } + + if (request.action === 'saveProgressState') { + ensureSessionReady(function() { + const saveKey = getStorageKeyBySender(sender); + if (!saveKey) { + sendResponse({ ok: false, error: '无法识别标签页' }); + return; + } + + const normalizedState = normalizeProgressState(request.state); + if (!normalizedState) { + sendResponse({ ok: false, error: '状态数据无效' }); + return; + } + + chrome.storage.local.set({ [saveKey]: normalizedState }, function() { + if (chrome.runtime.lastError) { + sendResponse({ ok: false, error: chrome.runtime.lastError.message }); + return; + } + sendResponse({ ok: true }); + }); + }); + return true; + } + + if (request.action === 'loadProgressState') { + ensureSessionReady(function() { + const loadKey = getStorageKeyBySender(sender); + if (!loadKey) { + sendResponse({ ok: false, error: '无法识别标签页', state: null }); + return; + } + + chrome.storage.local.get(loadKey, function(result) { + if (chrome.runtime.lastError) { + sendResponse({ ok: false, error: chrome.runtime.lastError.message, state: null }); + return; + } + sendResponse({ ok: true, state: result && result[loadKey] ? result[loadKey] : null }); + }); + }); + return true; + } + + if (request.action === 'clearProgressState') { + ensureSessionReady(function() { + const clearKey = getStorageKeyBySender(sender); + if (!clearKey) { + sendResponse({ ok: false, error: '无法识别标签页' }); + return; + } + + chrome.storage.local.remove(clearKey, function() { + if (chrome.runtime.lastError) { + sendResponse({ ok: false, error: chrome.runtime.lastError.message }); + return; + } + sendResponse({ ok: true }); + }); + }); + return true; + } + + if (request.action === 'fetchHtml') { + fetchPageHtml(request.url, request.timeoutMs) + .then(html => { + sendResponse({ html: html }); + }) + .catch(err => { + sendResponse({ html: '', error: err.message }); + }); + return true; + } + + if (request.action === 'openAndFetch') { + fetchPageHtml(request.url, request.timeoutMs) + .then(html => { + sendResponse({ magnets: extractMagnetsFromHtml(html) }); + }) + .catch(err => { + sendResponse({ magnets: [], error: err.message }); + }); + + return true; + } + + return false; +}); + +if (chrome.tabs && chrome.tabs.onRemoved) { + chrome.tabs.onRemoved.addListener(function(tabId) { + var key = STORAGE_KEY_PREFIX + tabId; + chrome.storage.local.remove(key, function() { + // ignore cleanup errors + }); + }); +} diff --git a/content.js b/content.js new file mode 100644 index 0000000..afba523 --- /dev/null +++ b/content.js @@ -0,0 +1,1800 @@ +(function() { + if (window.magnetPluginInitialized) return; + window.magnetPluginInitialized = true; + + var DEBUG_MODE = localStorage.getItem('magnetDebugMode') === 'true'; + function log(msg) { if (DEBUG_MODE) console.log('[MagnetPlugin] ' + msg); } + + function escapeHtml(text) { + return String(text || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function ensurePanelStyles() { + if (document.getElementById('magnet-panel-style')) { + return; + } + + var style = document.createElement('style'); + style.id = 'magnet-panel-style'; + style.textContent = [ + '#magnet-float-ball{position:fixed;bottom:18px;right:18px;width:56px;height:56px;background:linear-gradient(135deg,#2e7d32,#43a047);border-radius:18px;box-shadow:0 16px 30px rgba(46,125,50,.35);z-index:2147483647;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px;font-family:"Trebuchet MS","Segoe UI","Microsoft YaHei",sans-serif !important;transition:transform .18s ease,box-shadow .18s ease;}', + '#magnet-float-ball:hover{transform:translateY(-2px);box-shadow:0 18px 34px rgba(46,125,50,.42);}', + '#magnet-floating-panel{position:fixed;right:16px;bottom:16px;width:min(760px,calc(100vw - 20px));height:min(82vh,820px);background:linear-gradient(180deg,#f9fbff 0%,#f3f6fb 100%);border:1px solid rgba(133,151,178,.25);border-radius:18px;box-shadow:0 24px 54px rgba(15,23,42,.24);z-index:2147483647;font-family:"Trebuchet MS","Segoe UI","Microsoft YaHei",sans-serif;font-size:13px;color:#122033;display:none;flex-direction:column;overflow:hidden;backdrop-filter:blur(10px);}', + '#magnet-floating-panel .magnet-panel-header{display:flex;justify-content:space-between;align-items:flex-start;padding:18px 20px 14px;background:linear-gradient(135deg,#1f5f3e 0%,#2f7c52 58%,#4caf50 100%);color:#fff;gap:14px;}', + '#magnet-floating-panel .magnet-panel-brand{display:flex;flex-direction:column;gap:4px;min-width:0;}', + '#magnet-floating-panel .magnet-panel-title{font-size:20px;font-weight:700;letter-spacing:.3px;}', + '#magnet-floating-panel .magnet-panel-subtitle{font-size:12px;opacity:.88;line-height:1.45;}', + '#magnet-floating-panel .magnet-panel-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}', + '#magnet-floating-panel .magnet-panel-switch{padding:8px 14px;border:none;border-radius:999px;background:rgba(255,255,255,.16);color:#eff8f0;cursor:pointer;font-size:12px;font-weight:700;transition:background .18s ease,transform .18s ease;}', + '#magnet-floating-panel .magnet-panel-switch:hover{background:rgba(255,255,255,.24);transform:translateY(-1px);}', + '#magnet-floating-panel .magnet-panel-switch.is-active{background:#fff;color:#1f5f3e;box-shadow:0 10px 20px rgba(14,30,37,.18);}', + '#magnet-floating-panel .magnet-panel-close{width:34px;height:34px;border:none;border-radius:12px;background:rgba(255,255,255,.16);color:#fff;cursor:pointer;font-size:18px;line-height:1;}', + '#magnet-floating-panel .magnet-panel-close:hover{background:rgba(255,255,255,.24);}', + '#magnet-settings{padding:16px 18px;background:rgba(255,255,255,.88);border-bottom:1px solid rgba(214,223,235,.95);display:flex;flex-direction:column;gap:10px;}', + '#magnet-floating-panel .magnet-control-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}', + '#magnet-floating-panel .magnet-control-row > *{min-width:0;}', + '#magnet-floating-panel .magnet-panel-content{flex:1;min-height:0;padding:16px 18px;background:linear-gradient(180deg,rgba(255,255,255,.78),rgba(247,250,255,.95));}', + '#magnet-floating-panel .magnet-view{display:none;height:100%;flex-direction:column;gap:12px;min-height:0;}', + '#magnet-floating-panel .magnet-view.is-active{display:flex;}', + '#magnet-floating-panel .magnet-view-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 14px;background:#fff;border:1px solid #e4ebf5;border-radius:14px;box-shadow:0 10px 20px rgba(31,48,74,.06);}', + '#magnet-floating-panel .magnet-view-toolbar-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}', + '#magnet-floating-panel .magnet-view-title{font-size:14px;font-weight:700;color:#19324d;}', + '#magnet-floating-panel .magnet-view-meta{font-size:12px;color:#5d6b82;}', + '#magnet-list{display:flex;flex-direction:column;gap:10px;min-height:0;overflow-y:auto;padding-right:4px;}', + '#magnet-cache-panel{flex:1;min-height:0;overflow-y:auto;padding-right:4px;}', + '#magnet-floating-panel .magnet-panel-footer{padding:14px 18px 18px;background:rgba(255,255,255,.96);border-top:1px solid rgba(214,223,235,.95);display:flex;flex-direction:column;gap:10px;}', + '#magnet-status{padding:10px 12px;border-radius:12px;font-size:12px;line-height:1.5;}', + '#magnet-copy-all{width:100%;padding:12px 14px;background:linear-gradient(135deg,#1e88e5,#42a5f5);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 22px rgba(30,136,229,.25);}', + '#magnet-copy-all:hover{filter:brightness(1.03);}', + '.magnet-item{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;background:#fff;border:1px solid #e3eaf2;border-radius:14px;box-shadow:0 10px 18px rgba(31,48,74,.06);}', + '.magnet-title{flex:1;cursor:pointer;color:#20324d;min-width:0;font-size:12px;line-height:1.55;word-break:break-all;font-weight:600;}', + '.magnet-copy-btn{padding:8px 12px;background:linear-gradient(135deg,#43a047,#66bb6a);color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:11px;font-weight:700;white-space:nowrap;flex-shrink:0;}', + '.magnet-copy-btn:hover{filter:brightness(1.03);}', + '.magnet-cache-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px;}', + '.magnet-cache-card{padding:12px;background:#fff;border:1px solid #e4ebf5;border-radius:14px;box-shadow:0 10px 18px rgba(31,48,74,.05);}', + '.magnet-cache-card-label{font-size:11px;color:#6b778c;margin-bottom:6px;}', + '.magnet-cache-card-value{font-size:18px;font-weight:700;color:#163049;}', + '.magnet-cache-section{margin-top:12px;}', + '.magnet-cache-section-title{font-size:12px;font-weight:700;color:#5c6b82;margin-bottom:8px;}', + '.magnet-cache-entry{padding:10px 12px;margin-top:8px;background:#fff;border:1px solid #e4ebf5;border-radius:12px;box-shadow:0 8px 14px rgba(31,48,74,.05);}', + '.magnet-cache-entry-title{font-size:12px;font-weight:700;color:#1b314b;line-height:1.5;word-break:break-all;}', + '.magnet-cache-entry-meta{font-size:11px;color:#6a768d;margin-top:4px;line-height:1.45;}', + '@media (max-width: 900px){#magnet-floating-panel{right:10px;bottom:10px;width:calc(100vw - 20px);height:min(84vh,760px);}#magnet-floating-panel .magnet-panel-header{padding:16px 16px 12px;}#magnet-settings,#magnet-floating-panel .magnet-panel-content,#magnet-floating-panel .magnet-panel-footer{padding-left:14px;padding-right:14px;}}' + ].join(''); + document.head.appendChild(style); + } + + function setPanelView(viewName) { + var resultsView = document.getElementById('magnet-results-view'); + var cacheView = document.getElementById('magnet-cache-view'); + var resultsBtn = document.getElementById('magnet-view-results'); + var cacheBtn = document.getElementById('magnet-view-cache'); + + if (!resultsView || !cacheView || !resultsBtn || !cacheBtn) { + return; + } + + var showCache = viewName === 'cache'; + resultsView.classList.toggle('is-active', !showCache); + cacheView.classList.toggle('is-active', showCache); + resultsBtn.classList.toggle('is-active', !showCache); + cacheBtn.classList.toggle('is-active', showCache); + } + + function createFloatingPanel() { + var existing = document.getElementById('magnet-floating-panel'); + if (existing) return existing; + + ensurePanelStyles(); + + var ball = document.createElement('div'); + ball.id = 'magnet-float-ball'; + ball.innerHTML = '⚡'; + ball.oncontextmenu = function(e) { + e.preventDefault(); + var menu = document.getElementById('magnet-debug-menu'); + if (menu) menu.remove(); + menu = document.createElement('div'); + menu.id = 'magnet-debug-menu'; + menu.style.cssText = 'position:fixed;bottom:80px;right:20px;background:#fff;border-radius:4px;box-shadow:0 2px 10px rgba(0,0,0,0.3);z-index:999999;padding:5px 0;font-size:12px'; + menu.innerHTML = ''; + document.body.appendChild(menu); + menu.querySelector('input').onchange = function() { + DEBUG_MODE = this.checked; + localStorage.setItem('magnetDebugMode', DEBUG_MODE); + }; + setTimeout(function() { + document.addEventListener('click', function hideMenu() { + menu.remove(); + document.removeEventListener('click', hideMenu); + }); + }, 100); + }; + document.body.appendChild(ball); + + var panel = document.createElement('div'); + panel.id = 'magnet-floating-panel'; + panel.innerHTML = '
磁力链接助手
更大的面板、更清晰的缓存视图、智能增量搜索
搜索结果
当前关键词命中的磁链会显示在这里
0 条磁链
缓存总览
查看当前板块与全部缓存的占用、快照和最近命中
'; + document.body.appendChild(panel); + + setPanelView('results'); + + ball.onclick = function() { + panel.style.display = 'flex'; + ball.style.display = 'none'; + }; + + var resultsSwitch = panel.querySelector('#magnet-view-results'); + if (resultsSwitch) { + resultsSwitch.onclick = function() { + setPanelView('results'); + }; + } + + var cacheSwitch = panel.querySelector('#magnet-view-cache'); + if (cacheSwitch) { + cacheSwitch.onclick = function() { + setPanelView('cache'); + refreshCacheOverview({ showStatus: true }); + }; + } + + var refreshCacheBtn = panel.querySelector('#magnet-refresh-cache'); + if (refreshCacheBtn) { + refreshCacheBtn.onclick = function() { + refreshCacheOverview({ showStatus: true }); + }; + } + + var clearCacheInlineBtn = panel.querySelector('#magnet-clear-cache-inline'); + if (clearCacheInlineBtn) { + clearCacheInlineBtn.onclick = clearAllCacheWithConfirm; + } + + var closeBtn = panel.querySelector('.magnet-panel-close'); + if (closeBtn) closeBtn.onclick = function() { + panel.style.display = 'none'; + ball.style.display = 'flex'; + }; + + var copyAllBtn = panel.querySelector('#magnet-copy-all'); + if (copyAllBtn) { + copyAllBtn.onclick = function() { + var links = allMagnetLinks.length > 0 + ? allMagnetLinks.slice() + : Array.from(document.querySelectorAll('.magnet-item .magnet-copy-btn')) + .map(function(btn) { return btn.getAttribute('data-magnet'); }) + .filter(function(link) { return !!link; }); + if (links.length === 0) { + alert('暂无可复制的磁力链接'); + return; + } + var allLinks = links.join('\n'); + navigator.clipboard.writeText(allLinks) + .then(function() { + alert('已复制 ' + links.length + ' 个磁力链接!'); + }) + .catch(function(err) { + var errorMsg = err && err.message ? err.message : '复制失败'; + alert('复制失败:' + errorMsg); + }); + }; + } + + return panel; + } + + function isListPage() { + return document.querySelector('#threadlisttableid') !== null; + } + + function getCurrentPage() { + var match = window.location.href.match(/[?&]page=(\d+)/); + if (match) return parseInt(match[1]); + match = window.location.href.match(/forum-\d+-(\d+)\.html/); + if (match) return parseInt(match[1]); + return 1; + } + + function getForumOrigin(rawUrl) { + var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href; + var match = sourceUrl.match(/^(https?:\/\/[^\/]+)\/?/i); + if (match) return match[1]; + return window.location.origin || 'https://www.sehuatang.net'; + } + + function getForumIdFromUrl(rawUrl) { + var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href; + var match = sourceUrl.match(/[?&]fid=(\d+)/i); + if (match) return match[1]; + + match = sourceUrl.match(/forum-(\d+)(?:-|\.html)/i); + if (match) return match[1]; + + return '2'; + } + + function getForumKey() { + return getForumOrigin(window.location.href) + '|fid:' + getForumIdFromUrl(window.location.href); + } + + function getBaseUrl() { + return getForumOrigin(window.location.href) + '/forum-' + getForumIdFromUrl(window.location.href) + '-'; + } + + function normalizeThreadUrl(url) { + if (typeof url !== 'string' || !url) { + return ''; + } + + try { + var parsed = new URL(url, window.location.origin); + parsed.hash = ''; + return parsed.href; + } catch (e) { + return url; + } + } + + function getThreadKeyFromUrl(url) { + var normalizedUrl = normalizeThreadUrl(url); + if (!normalizedUrl) { + return ''; + } + + var match = normalizedUrl.match(/thread-(\d+)-/i) || normalizedUrl.match(/[?&]tid=(\d+)/i); + return match ? match[1] : normalizedUrl; + } + + function normalizeCachedThreads(threads) { + var result = []; + var seen = Object.create(null); + (Array.isArray(threads) ? threads : []).forEach(function(thread) { + if (!thread || typeof thread !== 'object') return; + + var normalizedUrl = normalizeThreadUrl(thread.url); + var normalizedTitle = typeof thread.title === 'string' + ? thread.title.replace(/\s+/g, ' ').trim() + : ''; + var threadKey = typeof thread.threadKey === 'string' && thread.threadKey + ? thread.threadKey + : getThreadKeyFromUrl(normalizedUrl); + + if (!normalizedUrl || !threadKey || seen[threadKey]) return; + + seen[threadKey] = true; + result.push({ + url: normalizedUrl, + title: normalizedTitle, + threadKey: threadKey + }); + }); + return result; + } + + function normalizeMagnetList(magnets) { + var seen = Object.create(null); + var result = []; + + (Array.isArray(magnets) ? magnets : []).forEach(function(magnet) { + if (typeof magnet !== 'string' || !magnet) return; + if (seen[magnet]) return; + seen[magnet] = true; + result.push(magnet); + }); + + return result; + } + + function extractThreadsFromHtml(html) { + var parser = new DOMParser(); + var doc = parser.parseFromString(html, 'text/html'); + var threads = []; + var tbodies = doc.querySelectorAll('#threadlisttableid tbody[id^="normalthread_"]'); + tbodies.forEach(function(tbody) { + var link = tbody.querySelector('th a[href*="thread-"]'); + var title = tbody.querySelector('th a.xst') || tbody.querySelector('th .xst'); + var titleText = title ? title.textContent : ''; + var normalizedUrl = link && link.href ? normalizeThreadUrl(link.href) : ''; + if (normalizedUrl) { + threads.push({ + url: normalizedUrl, + title: titleText, + threadKey: getThreadKeyFromUrl(normalizedUrl) + }); + } + }); + return normalizeCachedThreads(threads); + } + + function updateStatus(message, type) { + var status = document.getElementById('magnet-status'); + if (status) { + status.textContent = message; + status.setAttribute('data-type', type || ''); + status.style.background = '#e3f2fd'; + status.style.color = '#1976D2'; + if (type === 'loading') { + status.style.background = '#fff3e0'; + status.style.color = '#f57c00'; + } else if (type === 'error') { + status.style.background = '#ffebee'; + status.style.color = '#c62828'; + } else if (type === 'done') { + status.style.background = '#e8f5e9'; + status.style.color = '#388e3c'; + } + + if (type && type !== 'loading') { + scheduleStatePersist(); + } + } + } + + function updateCount(count) { + var countEl = document.getElementById('magnet-count-num'); + if (countEl) countEl.textContent = count; + } + + function clearMagnetList(skipPersist) { + var list = document.getElementById('magnet-list'); + if (list) list.innerHTML = ''; + allMagnetLinks = []; + allMagnetRecords = []; + magnetRecordMap = Object.create(null); + updateCount(0); + + if (!skipPersist) { + scheduleStatePersist(); + } + } + + function addMagnetItem(title, link, options) { + var list = document.getElementById('magnet-list'); + if (!list) return; + + var safeTitle = typeof title === 'string' ? title : String(title || ''); + var safeLink = typeof link === 'string' ? link : String(link || ''); + if (!safeLink) return; + + if (magnetRecordMap[safeLink]) { + return; + } + + magnetRecordMap[safeLink] = safeTitle || '恢复记录'; + allMagnetRecords.push({ + title: magnetRecordMap[safeLink], + link: safeLink + }); + allMagnetLinks.push(safeLink); + + var item = document.createElement('div'); + item.className = 'magnet-item'; + + var titleEl = document.createElement('span'); + titleEl.className = 'magnet-title'; + titleEl.title = safeTitle; + titleEl.textContent = safeTitle; + + var copyBtn = document.createElement('button'); + copyBtn.className = 'magnet-copy-btn'; + copyBtn.setAttribute('data-magnet', safeLink); + copyBtn.textContent = '复制'; + + titleEl.onclick = function() { + navigator.clipboard.writeText(safeLink) + .then(function() { + titleEl.textContent = '已复制: ' + safeTitle.substring(0, 20) + '...'; + setTimeout(function() { + titleEl.textContent = safeTitle; + }, 1500); + }) + .catch(function(err) { + var errorMsg = err && err.message ? err.message : '复制失败'; + log('标题复制失败: ' + errorMsg); + updateStatus('复制失败,请检查剪贴板权限', 'error'); + }); + }; + + copyBtn.onclick = function() { + navigator.clipboard.writeText(safeLink) + .then(function() { + copyBtn.textContent = '已复制'; + setTimeout(function() { + copyBtn.textContent = '复制'; + }, 1000); + }) + .catch(function(err) { + var errorMsg = err && err.message ? err.message : '复制失败'; + log('按钮复制失败: ' + errorMsg); + updateStatus('复制失败,请检查剪贴板权限', 'error'); + }); + }; + + item.appendChild(titleEl); + item.appendChild(copyBtn); + + list.appendChild(item); + setPanelView('results'); + updateCount(list.children.length); + + if (!options || !options.skipPersist) { + scheduleStatePersist(); + } + } + + function extractMagnets() { + var magnetPattern = /magnet:\?xt=urn:btih:[a-fA-F0-9]{32,40}/gi; + var links = new Set(); + + function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + var matches = node.textContent.match(magnetPattern); + if (matches) matches.forEach(function(m) { links.add(m); }); + } else if (node.nodeType === Node.ELEMENT_NODE) { + var tag = node.tagName.toLowerCase(); + if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') { + node.childNodes.forEach(walk); + } + } + } + walk(document.body); + document.querySelectorAll('a[href^="magnet:"]').forEach(function(a) { links.add(a.href); }); + + return Array.from(links); + } + + var speedMode = 'fast'; + var speedConfig = { + slow: { + thread: [300, 600], + page: [500, 900], + concurrency: 1, + fetchTimeout: 25000, + messageTimeout: 30000 + }, + medium: { + thread: [80, 180], + page: [150, 300], + concurrency: 2, + fetchTimeout: 18000, + messageTimeout: 22000 + }, + fast: { + thread: [0, 60], + page: [60, 150], + concurrency: 4, + fetchTimeout: 14000, + messageTimeout: 18000 + }, + ultrafast: { + thread: [0, 20], + page: [20, 80], + concurrency: 6, + fetchTimeout: 10000, + messageTimeout: 14000 + } + }; + + function getSpeedProfile() { + return speedConfig[speedMode] || speedConfig.fast; + } + + function getSpeedDelay(type) { + var profile = getSpeedProfile(); + var range = profile[type] || [0, 0]; + return Math.random() * (range[1] - range[0]) + range[0]; + } + + function getThreadConcurrency() { + return Math.max(1, getSpeedProfile().concurrency || 1); + } + + function getFetchTimeout() { + return getSpeedProfile().fetchTimeout || 15000; + } + + function getMessageTimeout() { + return getSpeedProfile().messageTimeout || 20000; + } + + function sleep(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } + + function sendRuntimeMessage(payload, timeoutMs) { + return new Promise(function(resolve, reject) { + if (!chrome.runtime || !chrome.runtime.id) { + reject(new Error('扩展已失效,请刷新页面')); + return; + } + + var finished = false; + var safeTimeoutMs = Number(timeoutMs); + if (!Number.isFinite(safeTimeoutMs) || safeTimeoutMs <= 0) { + safeTimeoutMs = 15000; + } + var timer = setTimeout(function() { + if (finished) return; + finished = true; + reject(new Error('请求超时,请稍后重试')); + }, safeTimeoutMs); + + try { + chrome.runtime.sendMessage(payload, function(response) { + if (finished) return; + finished = true; + clearTimeout(timer); + + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + + resolve(response); + }); + } catch (err) { + if (finished) return; + finished = true; + clearTimeout(timer); + reject(err); + } + }); + } + + function buildKeywordList(keyword) { + return String(keyword || '') + .split(',') + .map(function(item) { return item.trim(); }) + .filter(function(item) { return !!item; }); + } + + function filterThreadsByKeywords(threadList, keywords) { + var normalizedThreads = normalizeCachedThreads(threadList); + if (!keywords || keywords.length === 0) { + return normalizedThreads; + } + + return normalizedThreads.filter(function(thread) { + return keywords.some(function(keyword) { + return thread.title.indexOf(keyword) !== -1; + }); + }); + } + + function mergeCoverageThreads(targetMap, threads) { + normalizeCachedThreads(threads).forEach(function(thread) { + if (!targetMap[thread.threadKey]) { + targetMap[thread.threadKey] = thread; + return; + } + + if (!targetMap[thread.threadKey].title && thread.title) { + targetMap[thread.threadKey].title = thread.title; + } + if (!targetMap[thread.threadKey].url && thread.url) { + targetMap[thread.threadKey].url = thread.url; + } + }); + } + + function coverageMapToList(targetMap) { + return Object.keys(targetMap).map(function(threadKey) { + return targetMap[threadKey]; + }); + } + + function getSmartFrontRefreshPages(startPage, endPage) { + if (startPage !== 1) { + return 0; + } + + return Math.min(20, Math.max(0, endPage - startPage + 1)); + } + + async function getCachedCoveragePlan(forumKey, startPage, endPage, frontRefreshPages) { + try { + var response = await sendRuntimeMessage({ + action: 'cacheGetCoveragePlan', + forumKey: forumKey, + startPage: startPage, + endPage: endPage, + frontRefreshPages: frontRefreshPages + }, 8000); + + if (!response || !response.ok) { + log('读取缓存计划失败: ' + (response && response.error ? response.error : '空响应')); + return null; + } + + return response; + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('读取缓存计划异常: ' + errorMsg); + return null; + } + } + + async function saveCoverageSnapshot(forumKey, startPage, endPage, threads, frontRefreshPages, strategy) { + var normalizedThreads = normalizeCachedThreads(threads); + if (!forumKey || normalizedThreads.length === 0) { + return; + } + + try { + var response = await sendRuntimeMessage({ + action: 'cacheSaveCoverage', + forumKey: forumKey, + startPage: startPage, + endPage: endPage, + frontRefreshPages: frontRefreshPages, + strategy: strategy, + crawledAt: Date.now(), + threads: normalizedThreads + }, 12000); + + if (!response || !response.ok) { + log('保存范围缓存失败: ' + (response && response.error ? response.error : '空响应')); + } + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('保存范围缓存异常: ' + errorMsg); + } + } + + async function savePageCoverageSnapshot(forumKey, page, threads) { + var normalizedThreads = normalizeCachedThreads(threads); + + try { + var response = await sendRuntimeMessage({ + action: 'cacheSavePageCoverage', + forumKey: forumKey, + page: page, + crawledAt: Date.now(), + threads: normalizedThreads + }, 12000); + + if (!response || !response.ok) { + log('保存页缓存失败: ' + (response && response.error ? response.error : '空响应')); + } + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('保存页缓存异常: ' + errorMsg); + } + } + + async function getCachedThreadMagnets(forumKey, threads) { + var normalizedThreads = normalizeCachedThreads(threads); + if (!forumKey || normalizedThreads.length === 0) { + return []; + } + + try { + var response = await sendRuntimeMessage({ + action: 'cacheGetThreadMagnets', + forumKey: forumKey, + threads: normalizedThreads + }, 12000); + + if (!response || !response.ok || !Array.isArray(response.threads)) { + log('读取帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应')); + return []; + } + + return response.threads.map(function(thread) { + return { + threadKey: typeof thread.threadKey === 'string' ? thread.threadKey : getThreadKeyFromUrl(thread.url), + url: normalizeThreadUrl(thread.url), + title: typeof thread.title === 'string' ? thread.title : '', + magnets: normalizeMagnetList(thread.magnets) + }; + }); + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('读取帖子磁链缓存异常: ' + errorMsg); + return []; + } + } + + async function saveThreadMagnetsToCache(forumKey, threads) { + var normalizedThreads = []; + + (Array.isArray(threads) ? threads : []).forEach(function(thread) { + if (!thread || typeof thread !== 'object') { + return; + } + + var normalizedUrl = normalizeThreadUrl(thread.url); + var threadKey = typeof thread.threadKey === 'string' && thread.threadKey + ? thread.threadKey + : getThreadKeyFromUrl(normalizedUrl); + var magnets = normalizeMagnetList(thread.magnets); + + if (!normalizedUrl || !threadKey || magnets.length === 0) { + return; + } + + normalizedThreads.push({ + threadKey: threadKey, + url: normalizedUrl, + title: typeof thread.title === 'string' ? thread.title : '', + magnets: magnets + }); + }); + + if (!forumKey || normalizedThreads.length === 0) { + return; + } + + try { + var response = await sendRuntimeMessage({ + action: 'cacheSaveThreadMagnets', + forumKey: forumKey, + syncedAt: Date.now(), + threads: normalizedThreads + }, 12000); + + if (!response || !response.ok) { + log('保存帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应')); + } + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('保存帖子磁链缓存异常: ' + errorMsg); + } + } + + function formatTimeLabel(timestamp) { + var value = Number(timestamp); + if (!value) { + return '无'; + } + + var date = new Date(value); + var month = String(date.getMonth() + 1).padStart(2, '0'); + var day = String(date.getDate()).padStart(2, '0'); + var hour = String(date.getHours()).padStart(2, '0'); + var minute = String(date.getMinutes()).padStart(2, '0'); + return month + '-' + day + ' ' + hour + ':' + minute; + } + + function formatBytesLabel(bytes) { + var size = Number(bytes); + if (!Number.isFinite(size) || size <= 0) { + return '0 B'; + } + + if (size < 1024) { + return size + ' B'; + } + + if (size < 1024 * 1024) { + return (size / 1024).toFixed(1) + ' KB'; + } + + return (size / (1024 * 1024)).toFixed(2) + ' MB'; + } + + async function getCacheOverview(forumKey) { + try { + var response = await sendRuntimeMessage({ + action: 'cacheGetOverview', + forumKey: forumKey, + limit: 12 + }, 12000); + + if (!response || !response.ok) { + log('读取缓存总览失败: ' + (response && response.error ? response.error : '空响应')); + return null; + } + + return response; + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('读取缓存总览异常: ' + errorMsg); + return null; + } + } + + function renderCacheOverview(cacheOverview) { + var panel = document.getElementById('magnet-cache-panel'); + if (!panel) { + return; + } + + if (!cacheOverview || !cacheOverview.summary) { + panel.innerHTML = '
缓存状态
暂无缓存数据
'; + return; + } + + var summary = cacheOverview.summary; + var recentThreads = Array.isArray(cacheOverview.recentThreads) ? cacheOverview.recentThreads : []; + var recentCoverages = Array.isArray(cacheOverview.recentCoverages) ? cacheOverview.recentCoverages : []; + + var html = ''; + html += '
说明
'; + html += '
'; + html += '
当前板块唯一帖子
' + summary.forumThreadCount + '
'; + html += '
全部唯一帖子
' + summary.totalThreadCount + '
'; + html += '
磁链缓存
' + summary.recentMagnetCount + '
'; + html += '
存储占用
' + formatBytesLabel(summary.storageUsage) + '
'; + html += '
'; + + html += '
最近缓存帖子
'; + if (recentThreads.length === 0) { + html += '
提示
'; + } else { + recentThreads.forEach(function(item) { + var title = item.title || '未命名帖子'; + html += '
'; + html += '
' + escapeHtml(title) + '
'; + html += ''; + html += '
'; + }); + } + html += '
'; + + html += '
最近范围快照
'; + if (recentCoverages.length === 0) { + html += '
提示
'; + } else { + recentCoverages.forEach(function(item) { + html += '
'; + html += '
页码 ' + item.startPage + '-' + item.endPage + '
'; + html += ''; + html += '
'; + }); + } + html += '
'; + + panel.innerHTML = html; + } + + async function refreshCacheOverview(options) { + options = options || {}; + var cachePanel = document.getElementById('magnet-cache-panel'); + if (cachePanel) { + cachePanel.innerHTML = '
正在读取缓存...
'; + } + + var overview = await getCacheOverview(getForumKey()); + renderCacheOverview(overview); + + if (options.showStatus) { + if (overview && overview.summary) { + updateStatus('已加载缓存:当前板块唯一帖子 ' + overview.summary.forumThreadCount + ' 帖,当前页缓存 ' + Number(overview.summary.forumPageCoverageCount || 0) + ' 页', 'done'); + } else { + updateStatus('读取缓存失败,请稍后重试', 'error'); + } + } + + return overview; + } + + async function clearAllCacheWithConfirm() { + if (!confirm('确认清空全部缓存吗?这不会删除当前页面已显示的结果。')) { + return; + } + + if (!confirm('二次确认:真的要清空全部标题缓存、范围快照和磁链缓存吗?')) { + return; + } + + updateStatus('正在清空缓存...', 'loading'); + + try { + var response = await sendRuntimeMessage({ action: 'cacheClearAll' }, 12000); + if (!response || !response.ok) { + updateStatus('清空缓存失败', 'error'); + return; + } + + await refreshCacheOverview(); + updateStatus('已清空全部缓存', 'done'); + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('清空缓存异常: ' + errorMsg); + updateStatus('清空缓存失败:' + errorMsg, 'error'); + } + } + + function toggleCachePanel() { + var cacheView = document.getElementById('magnet-cache-view'); + var resultsView = document.getElementById('magnet-results-view'); + if (!cacheView || !resultsView) { + return; + } + + var isVisible = cacheView.classList.contains('is-active'); + setPanelView(isVisible ? 'results' : 'cache'); + + if (!isVisible) { + refreshCacheOverview({ showStatus: true }); + } + } + + async function applyCachedMagnetHits(pageLabel, filteredThreads, context) { + var cachedEntries = await getCachedThreadMagnets(context.forumKey, filteredThreads); + var cachedMap = Object.create(null); + var threadsToFetch = []; + var cachedThreadCount = 0; + var cachedMagnetCount = 0; + + cachedEntries.forEach(function(entry) { + if (!entry || !entry.threadKey) return; + cachedMap[entry.threadKey] = entry; + }); + + normalizeCachedThreads(filteredThreads).forEach(function(thread) { + var threadKey = thread.threadKey || getThreadKeyFromUrl(thread.url); + var cachedEntry = threadKey ? cachedMap[threadKey] : null; + var cachedMagnets = cachedEntry ? normalizeMagnetList(cachedEntry.magnets) : []; + + if (cachedMagnets.length === 0) { + if (!context.processedThreadKeys[threadKey]) { + threadsToFetch.push(thread); + } + return; + } + + if (context.processedThreadKeys[threadKey]) { + return; + } + + context.processedThreadKeys[threadKey] = true; + cachedThreadCount += 1; + context.totalFetched += 1; + + cachedMagnets.forEach(function(magnet) { + if (context.allMagnets.has(magnet)) { + return; + } + + context.allMagnets.add(magnet); + cachedMagnetCount += 1; + addMagnetItem(thread.title || cachedEntry.title || '缓存帖子', magnet); + }); + }); + + if (cachedThreadCount > 0) { + updateStatus( + '正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,直接读取缓存磁链 ' + cachedThreadCount + ' 帖(' + cachedMagnetCount + ' 条)', + 'loading' + ); + } + + return { + threadsToFetch: threadsToFetch, + cachedThreadCount: cachedThreadCount, + cachedMagnetCount: cachedMagnetCount, + pageLabel: pageLabel + }; + } + + var STATE_BACKUP_KEY = 'magnetPluginStateBackupV1'; + var MAX_STATE_ITEMS = 2000; + var statePersistTimer = null; + var hasRestoredProgress = false; + var lastRestoredState = null; + var manualClearRequested = false; + + var isFetching = false; + var stopFetching = false; + var allMagnetLinks = []; + var allMagnetRecords = []; + var magnetRecordMap = Object.create(null); + var progressRuntime = { + isRunning: false, + stoppedByUser: false, + startPage: 1, + endPage: 1, + resumeFromPage: 1, + keyword: '', + speedMode: 'fast' + }; + + function resetProgressRuntimeToIdle() { + var startInput = document.getElementById('page-start'); + var endInput = document.getElementById('page-end'); + var keywordInput = document.getElementById('keyword-input'); + var currentStart = startInput ? parseInt(startInput.value, 10) || 1 : 1; + var currentEnd = endInput ? parseInt(endInput.value, 10) || currentStart : currentStart; + + progressRuntime.isRunning = false; + progressRuntime.stoppedByUser = true; + progressRuntime.startPage = currentStart; + progressRuntime.endPage = Math.max(currentStart, currentEnd); + progressRuntime.resumeFromPage = currentStart; + progressRuntime.keyword = keywordInput ? keywordInput.value.trim() : ''; + progressRuntime.speedMode = speedConfig[speedMode] ? speedMode : 'fast'; + } + + function clearSessionBackupState() { + try { + sessionStorage.removeItem(STATE_BACKUP_KEY); + } catch (e) { + log('清理会话备份失败: ' + e); + } + } + + async function clearAllResults() { + manualClearRequested = true; + stopFetching = true; + resetProgressRuntimeToIdle(); + clearMagnetList(true); + clearSessionBackupState(); + + try { + await sendRuntimeMessage({ action: 'clearProgressState' }, 6000); + } catch (e) { + var clearErrorMsg = e && e.message ? e.message : String(e); + log('远端清理失败: ' + clearErrorMsg); + } + + if (!isFetching) { + manualClearRequested = false; + updateStatus('已手动清空结果', 'done'); + scheduleStatePersist(); + } else { + updateStatus('正在停止任务并清空结果...', 'loading'); + } + } + + function buildStateSnapshot() { + var keywordInput = document.getElementById('keyword-input'); + var startInput = document.getElementById('page-start'); + var endInput = document.getElementById('page-end'); + var speedSelect = document.getElementById('speed-select'); + var statusEl = document.getElementById('magnet-status'); + + var startPage = startInput ? parseInt(startInput.value, 10) || 1 : 1; + var endPage = endInput ? parseInt(endInput.value, 10) || startPage : startPage; + var resumeFromPage = Number(progressRuntime.resumeFromPage); + if (!Number.isFinite(resumeFromPage) || resumeFromPage < 1) { + resumeFromPage = startPage; + } + + return { + links: allMagnetLinks.slice(-MAX_STATE_ITEMS), + items: allMagnetRecords.slice(-MAX_STATE_ITEMS), + keyword: keywordInput ? keywordInput.value.trim() : '', + speedMode: speedConfig[speedMode] ? speedMode : 'fast', + startPage: startPage, + endPage: endPage, + resumeFromPage: resumeFromPage, + isRunning: !!progressRuntime.isRunning, + stoppedByUser: !!progressRuntime.stoppedByUser, + statusText: statusEl ? statusEl.textContent || '' : '', + statusType: statusEl ? statusEl.getAttribute('data-type') || '' : '', + pageUrl: window.location.href, + updatedAt: Date.now() + }; + } + + function saveStateToSessionBackup(snapshot) { + try { + sessionStorage.setItem(STATE_BACKUP_KEY, JSON.stringify(snapshot)); + } catch (e) { + log('保存会话备份失败: ' + e); + } + } + + function readStateFromSessionBackup() { + try { + var raw = sessionStorage.getItem(STATE_BACKUP_KEY); + if (!raw) return null; + return JSON.parse(raw); + } catch (e) { + log('读取会话备份失败: ' + e); + return null; + } + } + + function applyStateSnapshot(snapshot) { + if (!snapshot || typeof snapshot !== 'object') { + return false; + } + + var speedSelect = document.getElementById('speed-select'); + if (speedSelect && snapshot.speedMode && speedConfig[snapshot.speedMode]) { + speedSelect.value = snapshot.speedMode; + speedMode = snapshot.speedMode; + } + + var keywordInput = document.getElementById('keyword-input'); + if (keywordInput && typeof snapshot.keyword === 'string') { + keywordInput.value = snapshot.keyword; + } + + var startInput = document.getElementById('page-start'); + if (startInput && Number.isFinite(Number(snapshot.startPage))) { + startInput.value = Math.max(1, Number(snapshot.startPage)); + } + + var endInput = document.getElementById('page-end'); + if (endInput && Number.isFinite(Number(snapshot.endPage))) { + endInput.value = Math.max(1, Number(snapshot.endPage)); + } + + progressRuntime.startPage = Number.isFinite(Number(snapshot.startPage)) + ? Math.max(1, Number(snapshot.startPage)) + : 1; + progressRuntime.endPage = Number.isFinite(Number(snapshot.endPage)) + ? Math.max(progressRuntime.startPage, Number(snapshot.endPage)) + : progressRuntime.startPage; + progressRuntime.resumeFromPage = Number.isFinite(Number(snapshot.resumeFromPage)) + ? Math.max(1, Number(snapshot.resumeFromPage)) + : progressRuntime.startPage; + progressRuntime.isRunning = !!snapshot.isRunning; + progressRuntime.stoppedByUser = !!snapshot.stoppedByUser; + progressRuntime.keyword = typeof snapshot.keyword === 'string' ? snapshot.keyword : ''; + progressRuntime.speedMode = speedConfig[snapshot.speedMode] ? snapshot.speedMode : 'fast'; + + var records = Array.isArray(snapshot.items) && snapshot.items.length > 0 + ? snapshot.items + : (Array.isArray(snapshot.links) + ? snapshot.links.map(function(link) { + return { + title: '恢复记录', + link: link + }; + }) + : []); + + clearMagnetList(true); + records.forEach(function(record) { + if (!record || typeof record !== 'object') return; + if (typeof record.link !== 'string' || !record.link) return; + var title = typeof record.title === 'string' ? record.title : '恢复记录'; + addMagnetItem(title, record.link, { skipPersist: true }); + }); + + if (allMagnetLinks.length > 0) { + var statusText = typeof snapshot.statusText === 'string' && snapshot.statusText + ? '已恢复: ' + snapshot.statusText + : '已恢复上次抓取记录,共' + allMagnetLinks.length + '条'; + updateStatus(statusText, snapshot.statusType || 'done'); + return true; + } + + return false; + } + + function saveStateNow() { + var snapshot = buildStateSnapshot(); + saveStateToSessionBackup(snapshot); + + sendRuntimeMessage({ + action: 'saveProgressState', + state: snapshot + }, 6000).catch(function(e) { + var errorMsg = e && e.message ? e.message : String(e); + log('远端状态保存失败: ' + errorMsg); + }); + } + + function scheduleStatePersist() { + if (statePersistTimer) { + clearTimeout(statePersistTimer); + } + + statePersistTimer = setTimeout(function() { + statePersistTimer = null; + saveStateNow(); + }, 500); + } + + async function restoreProgressState() { + if (hasRestoredProgress) { + return { + restored: allMagnetLinks.length > 0, + state: lastRestoredState + }; + } + hasRestoredProgress = true; + + var sessionState = readStateFromSessionBackup(); + var sessionStateTime = sessionState && Number.isFinite(Number(sessionState.updatedAt)) + ? Number(sessionState.updatedAt) + : 0; + var remoteState = null; + var remoteStateTime = 0; + + try { + var response = await sendRuntimeMessage({ action: 'loadProgressState' }, 6000); + if (response && response.ok && response.state) { + remoteState = response.state; + remoteStateTime = Number.isFinite(Number(response.state.updatedAt)) + ? Number(response.state.updatedAt) + : 0; + } + } catch (e) { + var errorMsg = e && e.message ? e.message : String(e); + log('远端状态恢复失败: ' + errorMsg); + } + + var chosenState = null; + if (sessionState) { + chosenState = sessionState; + } + if (remoteState && (!chosenState || remoteStateTime >= sessionStateTime)) { + chosenState = remoteState; + } + + lastRestoredState = chosenState; + var restored = chosenState ? applyStateSnapshot(chosenState) : false; + + return { + restored: restored, + state: chosenState + }; + } + + window.addEventListener('pagehide', function() { + if (statePersistTimer) { + clearTimeout(statePersistTimer); + statePersistTimer = null; + } + saveStateNow(); + }); + + async function fetchThreadsInParallel(page, filteredThreads, allMagnets, processedThreadKeys, options) { + if (!filteredThreads || filteredThreads.length === 0) { + return 0; + } + + options = options || {}; + + var concurrency = Math.min(getThreadConcurrency(), filteredThreads.length); + var cursor = 0; + var fetchedCount = 0; + var cachePayload = []; + + async function worker() { + while (!stopFetching) { + var currentIndex = cursor; + cursor += 1; + if (currentIndex >= filteredThreads.length) { + return; + } + + var threadItem = filteredThreads[currentIndex]; + var threadUrl = threadItem.url; + var threadTitle = threadItem.title || '未命名帖子'; + var threadKey = threadItem.threadKey || getThreadKeyFromUrl(threadUrl); + + if (processedThreadKeys && threadKey) { + if (processedThreadKeys[threadKey]) { + continue; + } + processedThreadKeys[threadKey] = true; + } + + updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading'); + + try { + var threadResponse = await sendRuntimeMessage({ + action: 'openAndFetch', + url: threadUrl, + timeoutMs: getFetchTimeout() + }, getMessageTimeout()); + + if (threadResponse && threadResponse.error) { + log('帖子请求失败: ' + threadUrl + ' - ' + threadResponse.error); + } else if (threadResponse && threadResponse.magnets) { + var threadMagnets = normalizeMagnetList(threadResponse.magnets); + if (threadMagnets.length > 0 && options.forumKey) { + cachePayload.push({ + threadKey: threadKey, + url: threadUrl, + title: threadTitle, + magnets: threadMagnets + }); + } + + threadMagnets.forEach(function(m) { + if (!allMagnets.has(m)) { + allMagnets.add(m); + addMagnetItem(threadTitle, m); + } + }); + } + } catch (e) { + var threadErrorMsg = e && e.message ? e.message : String(e); + log('获取帖子失败: ' + threadUrl + ' - ' + threadErrorMsg); + } + + fetchedCount += 1; + var delay = getSpeedDelay('thread'); + if (delay > 0) { + await sleep(delay); + } + } + } + + var workers = []; + for (var workerIndex = 0; workerIndex < concurrency; workerIndex++) { + workers.push(worker()); + } + + await Promise.all(workers); + + if (options.forumKey && cachePayload.length > 0) { + await saveThreadMagnetsToCache(options.forumKey, cachePayload); + } + + return fetchedCount; + } + + async function processCachedThreadBatch(pageLabel, threads, context, statusText) { + var normalizedThreads = normalizeCachedThreads(threads); + if (normalizedThreads.length === 0) { + return; + } + + if (statusText) { + updateStatus(statusText, 'loading'); + } + + mergeCoverageThreads(context.coverageThreadMap, normalizedThreads); + var filteredThreads = filterThreadsByKeywords(normalizedThreads, context.keywords); + context.matchedThreads += filteredThreads.length; + + if (filteredThreads.length === 0) { + updateStatus('正在搜索缓存:已检查缓存标题 ' + normalizedThreads.length + ' 帖,当前关键词未命中', 'loading'); + return; + } + + var cacheResult = await applyCachedMagnetHits(pageLabel, filteredThreads, context); + if (cacheResult.threadsToFetch.length === 0) { + updateStatus('正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,结果已全部由缓存秒出', 'loading'); + return; + } + + updateStatus( + '正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖', + 'loading' + ); + context.totalFetched += await fetchThreadsInParallel( + pageLabel, + cacheResult.threadsToFetch, + context.allMagnets, + context.processedThreadKeys, + { forumKey: context.forumKey } + ); + } + + async function fetchLivePageRange(startPage, endPage, context) { + if (startPage > endPage) { + return; + } + + for (var page = startPage; page <= endPage; page++) { + if (stopFetching) break; + + progressRuntime.resumeFromPage = page; + scheduleStatePersist(); + + if (!chrome.runtime || !chrome.runtime.id) { + updateStatus('扩展已失效,请刷新页面', 'error'); + break; + } + + updateStatus('第' + page + '/' + context.normalizedEnd + '页...', 'loading'); + + var pageUrl = context.baseUrl + page + '.html'; + try { + var response = await sendRuntimeMessage({ + action: 'fetchHtml', + url: pageUrl, + timeoutMs: getFetchTimeout() + }, getMessageTimeout()); + + if (!response || response.error) { + context.failedPages += 1; + progressRuntime.resumeFromPage = page + 1; + scheduleStatePersist(); + log('获取第 ' + page + ' 页失败: ' + (response && response.error ? response.error : '空响应')); + continue; + } + + if (!response.html) { + progressRuntime.resumeFromPage = page + 1; + scheduleStatePersist(); + continue; + } + + var threadList = extractThreadsFromHtml(response.html); + mergeCoverageThreads(context.coverageThreadMap, threadList); + await savePageCoverageSnapshot(context.forumKey, page, threadList); + + var filteredThreads = filterThreadsByKeywords(threadList, context.keywords); + context.matchedThreads += filteredThreads.length; + var cacheResult = await applyCachedMagnetHits(page, filteredThreads, context); + if (cacheResult.threadsToFetch.length > 0) { + if (cacheResult.cachedThreadCount > 0) { + updateStatus( + '第' + page + '/' + context.normalizedEnd + '页:缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖', + 'loading' + ); + } + + context.totalFetched += await fetchThreadsInParallel( + page, + cacheResult.threadsToFetch, + context.allMagnets, + context.processedThreadKeys, + { forumKey: context.forumKey } + ); + } + progressRuntime.resumeFromPage = page + 1; + scheduleStatePersist(); + } catch (e) { + context.failedPages += 1; + progressRuntime.resumeFromPage = page + 1; + scheduleStatePersist(); + var pageErrorMsg = e && e.message ? e.message : String(e); + log('获取第 ' + page + ' 页异常: ' + pageErrorMsg); + } + + if (stopFetching) break; + + var pageDelay = getSpeedDelay('page'); + if (pageDelay > 0) { + await sleep(pageDelay); + } + } + } + + async function fetchFromPage(startPage, endPage, options) { + if (isFetching) return; + + options = options || {}; + var preserveExisting = !!options.preserveExisting; + var earlyStart = Math.max(1, parseInt(startPage, 10) || 1); + var earlyEnd = Math.max(earlyStart, parseInt(endPage, 10) || earlyStart); + + isFetching = true; + stopFetching = false; + progressRuntime.isRunning = true; + progressRuntime.stoppedByUser = false; + progressRuntime.startPage = earlyStart; + progressRuntime.endPage = earlyEnd; + progressRuntime.resumeFromPage = earlyStart; + + var panel = createFloatingPanel(); + var ball = document.getElementById('magnet-float-ball'); + panel.style.display = 'flex'; + if (ball) ball.style.display = 'none'; + if (!preserveExisting) { + clearMagnetList(true); + } + + try { + var normalizedStart = Math.max(1, parseInt(startPage, 10) || 1); + var normalizedEnd = Math.max(normalizedStart, parseInt(endPage, 10) || normalizedStart); + + var baseUrl = getBaseUrl(); + var keywordInput = document.getElementById('keyword-input'); + var speedSelect = document.getElementById('speed-select'); + + var keyword = typeof options.keyword === 'string' + ? options.keyword.trim() + : (keywordInput ? keywordInput.value.trim() : ''); + + var selectedSpeed = typeof options.speedMode === 'string' + ? options.speedMode + : (speedSelect ? speedSelect.value : 'fast'); + + speedMode = speedConfig[selectedSpeed] ? selectedSpeed : 'fast'; + if (speedSelect) { + speedSelect.value = speedMode; + } + if (keywordInput) { + keywordInput.value = keyword; + } + + progressRuntime.isRunning = true; + progressRuntime.stoppedByUser = false; + progressRuntime.startPage = normalizedStart; + progressRuntime.endPage = normalizedEnd; + progressRuntime.resumeFromPage = normalizedStart; + progressRuntime.keyword = keyword; + progressRuntime.speedMode = speedMode; + + if (!baseUrl) { + updateStatus('无法获取页面URL', 'error'); + progressRuntime.isRunning = false; + scheduleStatePersist(); + return; + } + + log('开始获取第 ' + normalizedStart + ' 到 ' + normalizedEnd + ' 页, 关键词: ' + keyword + ', 速度:' + speedMode + ', 并发:' + getThreadConcurrency()); + if (preserveExisting && allMagnetLinks.length > 0) { + updateStatus('检测到未完成任务,已恢复' + allMagnetLinks.length + '条,继续抓取...', 'loading'); + } else { + updateStatus('开始获取...', 'loading'); + } + scheduleStatePersist(); + + var forumKey = getForumKey(); + var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd); + var searchContext = { + forumKey: forumKey, + baseUrl: baseUrl, + normalizedEnd: normalizedEnd, + keywords: buildKeywordList(keyword), + coverageThreadMap: Object.create(null), + processedThreadKeys: Object.create(null), + allMagnets: new Set(allMagnetLinks), + matchedThreads: 0, + totalFetched: 0, + failedPages: 0 + }; + var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages); + var cacheStrategy = 'full_live'; + + if (cachePlan && cachePlan.exactCoverage && Array.isArray(cachePlan.exactCoverage.threads) && cachePlan.exactCoverage.threads.length > 0) { + cacheStrategy = 'exact_cache'; + await processCachedThreadBatch('缓存', cachePlan.exactCoverage.threads, searchContext, '命中当前板块缓存,直接搜索...'); + progressRuntime.resumeFromPage = normalizedEnd + 1; + scheduleStatePersist(); + } else { + var refreshedFrontEnd = normalizedStart - 1; + if (frontRefreshPages > 0) { + refreshedFrontEnd = normalizedStart + frontRefreshPages - 1; + updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading'); + await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext); + } + + var shiftedReuseEnd = refreshedFrontEnd; + if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) { + cacheStrategy = 'smart_incremental'; + shiftedReuseEnd = Math.max(shiftedReuseEnd, Math.min(normalizedEnd, Number(cachePlan.shiftedCoverage.reusedEndPage) || refreshedFrontEnd)); + await processCachedThreadBatch( + String(cachePlan.shiftedCoverage.reusedStartPage) + '-' + String(shiftedReuseEnd), + cachePlan.shiftedCoverage.threads, + searchContext, + '智能增量:复用当前板块历史缓存 ' + cachePlan.shiftedCoverage.reusedStartPage + '-' + shiftedReuseEnd + ' 页' + ); + progressRuntime.resumeFromPage = shiftedReuseEnd + 1; + scheduleStatePersist(); + } + + var liveTailStart = Math.max(normalizedStart, shiftedReuseEnd + 1); + if (!stopFetching && liveTailStart <= normalizedEnd) { + if (liveTailStart > normalizedStart) { + updateStatus('智能增量:补抓未覆盖页 ' + liveTailStart + '-' + normalizedEnd, 'loading'); + } + await fetchLivePageRange(liveTailStart, normalizedEnd, searchContext); + } + + if (!stopFetching) { + await saveCoverageSnapshot( + forumKey, + normalizedStart, + normalizedEnd, + coverageMapToList(searchContext.coverageThreadMap), + frontRefreshPages, + cacheStrategy + ); + } + } + + progressRuntime.isRunning = false; + + if (manualClearRequested) { + manualClearRequested = false; + clearMagnetList(true); + clearSessionBackupState(); + resetProgressRuntimeToIdle(); + updateStatus('已手动清空结果', 'done'); + scheduleStatePersist(); + return; + } + + var keywordMsg = keyword ? ' (关键词:' + keyword + ' 匹配:' + searchContext.matchedThreads + '帖)' : ''; + var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : ''; + if (stopFetching) { + progressRuntime.stoppedByUser = true; + updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error'); + } else { + progressRuntime.stoppedByUser = false; + progressRuntime.resumeFromPage = normalizedEnd + 1; + updateStatus('完成! 共' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'done'); + } + + scheduleStatePersist(); + } finally { + if (ball) ball.style.display = 'flex'; + isFetching = false; + } + } + + function fetchAllMagnets() { + var startPage = parseInt(document.getElementById('page-start').value, 10) || 1; + var endPage = parseInt(document.getElementById('page-end').value, 10) || 1; + if (endPage < startPage) { + var temp = startPage; + startPage = endPage; + endPage = temp; + } + fetchFromPage(startPage, endPage); + } + + function stopFetch() { + stopFetching = true; + progressRuntime.isRunning = false; + progressRuntime.stoppedByUser = true; + scheduleStatePersist(); + } + + async function initializePluginUi() { + createFloatingPanel(); + var panel = document.getElementById('magnet-floating-panel'); + if (!panel) return; + + var settingsArea = panel.querySelector('#magnet-settings'); + if (!settingsArea) return; + + if (isListPage() && !document.getElementById('keyword-input')) { + var currentPage = getCurrentPage(); + + var keywordDiv = document.createElement('div'); + keywordDiv.className = 'magnet-control-row'; + keywordDiv.innerHTML = ''; + + var pageRange = document.createElement('div'); + pageRange.className = 'magnet-control-row'; + pageRange.style.cssText = 'font-size:12px;color:#4e5d73'; + pageRange.innerHTML = '页码范围'; + + var btnContainer = document.createElement('div'); + btnContainer.className = 'magnet-control-row'; + + var btn = document.createElement('button'); + btn.textContent = '开始'; + btn.style.cssText = 'flex:1;padding:11px 14px;background:linear-gradient(135deg,#fb8c00,#ffb300);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 20px rgba(251,140,0,.24)'; + btn.onclick = fetchAllMagnets; + + var stopBtn = document.createElement('button'); + stopBtn.textContent = '停止'; + stopBtn.style.cssText = 'padding:11px 14px;background:linear-gradient(135deg,#e53935,#f4511e);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700'; + stopBtn.onclick = stopFetch; + + var clearBtn = document.createElement('button'); + clearBtn.textContent = '清结果'; + clearBtn.style.cssText = 'padding:11px 14px;background:#8d98a8;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700'; + clearBtn.onclick = clearAllResults; + + btnContainer.appendChild(btn); + btnContainer.appendChild(stopBtn); + btnContainer.appendChild(clearBtn); + + var speedDiv = document.createElement('div'); + speedDiv.className = 'magnet-control-row'; + speedDiv.style.cssText = 'font-size:12px;color:#4e5d73'; + speedDiv.innerHTML = '抓取速度'; + + settingsArea.appendChild(keywordDiv); + settingsArea.appendChild(pageRange); + settingsArea.appendChild(btnContainer); + settingsArea.appendChild(speedDiv); + } + + if (!document.getElementById('magnet-status')) { + var footer = panel.querySelector('.magnet-panel-footer'); + if (footer) { + var statusText = document.createElement('div'); + statusText.id = 'magnet-status'; + statusText.style.cssText = 'margin-bottom:8px;padding:6px;background:#e3f2fd;border-radius:4px;font-size:11px;color:#1976D2'; + footer.insertBefore(statusText, footer.firstChild); + } + } + + var restoreResult = await restoreProgressState(); + var restored = !!(restoreResult && restoreResult.restored); + var restoredState = restoreResult ? restoreResult.state : null; + if (isListPage()) { + var resumePage = Number.isFinite(Number(restoredState && restoredState.resumeFromPage)) + ? Math.max(1, Number(restoredState.resumeFromPage)) + : Math.max(1, Number(restoredState && restoredState.startPage) || 1); + var resumeEndPage = Number.isFinite(Number(restoredState && restoredState.endPage)) + ? Math.max(1, Number(restoredState.endPage)) + : resumePage; + + var statusType = typeof (restoredState && restoredState.statusType) === 'string' + ? restoredState.statusType + : ''; + var statusText = typeof (restoredState && restoredState.statusText) === 'string' + ? restoredState.statusText + : ''; + + var legacyRunningHint = statusType === 'loading' || /正在|开始|第\d+\/\d+页/.test(statusText); + + var shouldAutoResume = !!( + restoredState && + (restoredState.isRunning || legacyRunningHint) && + !restoredState.stoppedByUser && + resumePage <= resumeEndPage + ); + if (shouldAutoResume && !isFetching) { + + updateStatus('检测到未完成任务,正在自动继续...', 'loading'); + fetchFromPage(resumePage, resumeEndPage, { + preserveExisting: true, + keyword: typeof restoredState.keyword === 'string' ? restoredState.keyword : '', + speedMode: typeof restoredState.speedMode === 'string' ? restoredState.speedMode : 'fast' + }); + return; + } + + if (!restored) { + updateStatus('输入页码范围获取磁力', ''); + } + return; + } + + var pageTitle = document.title || '当前帖子'; + var magnets = extractMagnets(); + if (magnets.length > 0) { + magnets.forEach(function(m) { addMagnetItem(pageTitle, m); }); + updateStatus('找到 ' + magnets.length + ' 个磁力', 'done'); + } else if (!restored) { + updateStatus('未找到磁力链接', 'error'); + } + } + + var currentUrl = window.location.hostname; + if (currentUrl.includes('sehuatang')) { + initializePluginUi().catch(function(e) { + log('初始化失败: ' + e); + }); + + setInterval(function() { + var panelExists = !!document.getElementById('magnet-floating-panel'); + var ballExists = !!document.getElementById('magnet-float-ball'); + if (panelExists && ballExists) { + return; + } + + hasRestoredProgress = false; + initializePluginUi().catch(function(e) { + log('自动恢复UI失败: ' + e); + }); + }, 2500); + } + + chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { + if (request.action === 'getMagnets') { + var magnets = extractMagnets(); + sendResponse({ + magnets: magnets, + found: magnets.length > 0, + count: magnets.length + }); + return; + } + + if (request.action === 'copyMagnet') { + var magnetsToCopy = extractMagnets(); + if (magnetsToCopy.length === 0) { + sendResponse({ found: false, count: 0 }); + return; + } + + navigator.clipboard.writeText(magnetsToCopy.join('\n')) + .then(function() { + sendResponse({ found: true, count: magnetsToCopy.length }); + }) + .catch(function(err) { + var errorMsg = err && err.message ? err.message : '复制失败'; + sendResponse({ found: false, count: magnetsToCopy.length, error: errorMsg }); + }); + + return true; + } + }); +})(); diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..1f083e5 Binary files /dev/null and b/icon.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..5d44c18 --- /dev/null +++ b/manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "涩花塘磁力助手", + "version": "1.2", + "description": "一键获取当前页所有帖子的磁力链接", + "permissions": [ + "activeTab", + "clipboardWrite", + "storage", + "unlimitedStorage" + ], + "host_permissions": [ + "http://sehuatang.net/*", + "http://www.sehuatang.net/*", + "http://sehuatang.org/*", + "http://www.sehuatang.org/*", + "https://sehuatang.net/*", + "https://www.sehuatang.net/*", + "https://sehuatang.org/*", + "https://www.sehuatang.org/*" + ], + "content_scripts": [ + { + "matches": [ + "http://sehuatang.net/*", + "http://www.sehuatang.net/*", + "http://sehuatang.org/*", + "http://www.sehuatang.org/*", + "https://sehuatang.net/*", + "https://www.sehuatang.net/*", + "https://sehuatang.org/*", + "https://www.sehuatang.org/*" + ], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_title": "涩花塘磁力助手", + "default_popup": "popup.html" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..891ef6d --- /dev/null +++ b/popup.html @@ -0,0 +1,27 @@ + + + + + + +

磁力链接复制助手

+

在论坛页面提取当前页磁力链接并复制到剪贴板

+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..7dbbf1d --- /dev/null +++ b/popup.js @@ -0,0 +1,33 @@ +document.getElementById('copyBtn').addEventListener('click', function() { + chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { + var activeTab = tabs && tabs[0]; + if (!activeTab || typeof activeTab.id !== 'number') { + alert('未找到当前标签页'); + return; + } + + chrome.tabs.sendMessage(activeTab.id, { action: 'getMagnets' }, function(response) { + if (chrome.runtime.lastError) { + alert('当前页面不支持,请在涩花塘论坛页面使用'); + return; + } + + var magnets = response && Array.isArray(response.magnets) ? response.magnets : []; + magnets = Array.from(new Set(magnets)); + + if (magnets.length === 0) { + alert('当前页面未找到磁力链接'); + return; + } + + navigator.clipboard.writeText(magnets.join('\n')) + .then(function() { + alert('已复制 ' + magnets.length + ' 个磁力链接!'); + }) + .catch(function(err) { + var errorMsg = err && err.message ? err.message : '复制失败'; + alert('复制失败:' + errorMsg); + }); + }); + }); +});