'use strict'; class SubtitleDatabase { constructor() { this.db = null; this.isLoading = false; this.isLoaded = false; this.loadPromise = null; this.progressCallback = null; this.version = "1.0.1" } async clearCache() { try { await dbStorage.removeItem("databases", "subtitleDB"); await dbStorage.setItem("databases", "version", this.version); this.db = null; this.isLoaded = false; this.isLoading = false; this.loadPromise = null; return true } catch (error) { throw error; } } getDebugInfo() { return { version: this.version, isLoaded: this.isLoaded, isLoading: this.isLoading, recordCount: this.db ? this.db.length : 0, memoryUsage: this.db ? JSON.stringify(this.db).length : 0, cacheStatus: this.isLoaded ? "\u5df2\u52a0\u8f7d" : this.isLoading ? "\u52a0\u8f7d\u4e2d" : "\u672a\u52a0\u8f7d" } } printDebugInfo() { const info = this.getDebugInfo() } async checkVersion() { try { const storedVersion = await dbStorage.getItem("databases", "version"); if (!storedVersion) { await dbStorage.setItem("databases", "version", this.version); return false } if (storedVersion !== this.version) { await this.clearCache(); await dbStorage.setItem("databases", "version", this.version); return true } return false } catch (error) { return false } } async load(progressCallback = null) { if (this.isLoaded && this.db && this.db.length > 0) return true; if (this.isLoading) return this.loadPromise; this.isLoading = true; this.progressCallback = progressCallback; await this.checkVersion(); try { const cachedDB = await dbStorage.getItem("databases", "subtitleDB"); if (cachedDB) { if (Array.isArray(cachedDB) && cachedDB.length > 0) { this.db = cachedDB; this.isLoaded = true; this.isLoading = false; return true; } } } catch (e) { } this.loadPromise = new Promise(async (resolve, reject) => { try { const checkResponse = await fetch("https://vvdb.cicada000.work/subtitle_db", { method: "HEAD", referrerPolicy: 'no-referrer', mode: 'cors', credentials: 'omit' }); if (!checkResponse.ok) throw new Error(`Database file not found: ${checkResponse.status}`); const response = await fetch("https://vvdb.cicada000.work/subtitle_db", { referrerPolicy: 'no-referrer', mode: 'cors', credentials: 'omit' }); if (!response.ok) throw new Error(`Failed to load database: ${response.status}`); const contentLength = response.headers.get("Content-Length"); const reader = response.body.getReader(); let receivedLength = 0; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; } const chunksAll = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { chunksAll.set(chunk, position); position += chunk.length } const ds = new DecompressionStream("gzip"); const decompressedStream = (new Response(chunksAll)).body.pipeThrough(ds); const decompressedData = await (new Response(decompressedStream)).text(); const parsedData = JSON.parse(decompressedData); if (!parsedData || !Array.isArray(parsedData) || parsedData.length === 0) throw new Error("Invalid or empty database format"); this.db = parsedData; this.isLoaded = true; this.isLoading = false; try { await dbStorage.setItem("databases", "subtitleDB", this.db) } catch (e) { } if (window.subtitleDB !== this) window.subtitleDB = this; resolve(true) } catch (error) { this.isLoading = false; this.isLoaded = false; this.db = null; reject(error) } }); return this.loadPromise } lcsRatio(str1, str2) { str1 = str1.toLowerCase(); str2 = str2.toLowerCase(); if (!str1 || !str2) return 0; const m = str1.length; const n = str2.length; const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0)); for (let i = 1; i <= m; i++)for (let j = 1; j <= n; j++)if (str1[i - 1] === str2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); return dp[m][n] / m * 100 } multiWordLcsRatio(queryWords, text) { text = text.toLowerCase(); const totalQueryLength = queryWords.reduce((sum, word) => sum + word.length, 0); const usedChars = (new Array(text.length)).fill(false); let totalMatched = 0; for (const word of queryWords) { const wordLower = word.toLowerCase(); let foundMatch = false; let startPos = 0; while (true) { const pos = text.indexOf(wordLower, startPos); if (pos === -1) break; const endPos = pos + wordLower.length; let canUse = true; for (let i = pos; i < endPos; i++)if (usedChars[i]) { canUse = false; break } if (canUse) { for (let i = pos; i < endPos; i++)usedChars[i] = true; totalMatched += wordLower.length; foundMatch = true; break } startPos = pos + 1 } } return totalMatched / totalQueryLength * 100 } search(query, minRatio = 50, minSimilarity = 0) { if (!this.isLoaded || !this.db || this.db.length === 0) { return { status: "success", data: [], count: 0, message: "数据库未加载或为空", suggestions: [ "请等待数据库加载完成", "如果问题持续,请刷新页面" ] }; } const startTime = performance.now(); query = query.toLowerCase(); const hasSpaces = query.includes(" ") || query.includes("%20"); const queryWords = hasSpaces ? query.replace(/%20/g, " ").split(/\s+/).filter(Boolean) : [query]; const filteredBySimiliarity = this.db.filter(item => item.s >= minSimilarity); if (window.Worker && queryWords.length > 1) return new Promise(resolve => { const worker = new Worker("search_worker.js"); worker.onmessage = e => { const workerResults = e.data; worker.terminate(); if (workerResults.status === "success") { if (workerResults.data.length === 0) { resolve({ status: "success", data: [], count: 0, message: `未找到与 '${query}' 匹配的结果`, suggestions: [ "检查输入是否正确", `尝试降低最小匹配率(当前:${minRatio}%)`, `尝试降低最小相似度(当前:${minSimilarity})`, "尝试使用更简短的关键词" ] }); } else { resolve(workerResults); } } else { resolve({ status: "error", data: [], count: 0, message: workerResults.message || "搜索失败", suggestions: ["请重试"] }); } }; worker.onerror = (error) => { console.error("Worker error:", error); resolve({ status: "error", message: "Search failed", data: [], count: 0 }); }; worker.postMessage({ db: filteredBySimiliarity, queryWords, minRatio }); }); const results = filteredBySimiliarity.map(item => { const matchRatio = hasSpaces ? this.multiWordLcsRatio(queryWords, item.x) : this.lcsRatio(query, item.x); return { matchRatio, item, exactMatch: queryWords.every(word => item.x.toLowerCase().includes(word)) }; }).filter(result => result.matchRatio >= minRatio); results.sort((a, b) => { if (b.matchRatio !== a.matchRatio) return b.matchRatio - a.matchRatio; return b.exactMatch - a.exactMatch; }); const apiResults = results.map(result => ({ filename: result.item.f, timestamp: result.item.t, similarity: result.item.s, text: result.item.x, match_ratio: result.matchRatio, exact_match: result.exactMatch })); if (apiResults.length === 0) { return { status: "success", data: [{ // 包装在数组中 status: "success", data: [], count: 0, folder: "subtitle", max_results: "unlimited", message: `未找到与 '${query}' 匹配的结果`, suggestions: [ "检查输入是否正确", `尝试降低最小匹配率(当前:${minRatio}%)`, `尝试降低最小相似度(当前:${minSimilarity})`, "尝试使用更简短的关键词" ] }], count: 1 // 数组长度为1 }; } return { status: "success", data: apiResults, count: apiResults.length }; } } window.dbDebug = { clearCache: async () => { try { await window.subtitleDB.clearCache() } catch (error) { } }, info: () => { window.subtitleDB.printDebugInfo() }, reload: async () => { try { await window.subtitleDB.load() } catch (error) { } }, help: () => { } }; window.subtitleDB = new SubtitleDatabase; fetch("https://vvdb.cicada000.work/subtitle_db", { method: "HEAD", referrerPolicy: 'no-referrer', mode: 'cors', credentials: 'omit' }).then(response => { if (response.ok) window.subtitleDB.load().catch(error => { }) }).catch(error => { });