/* * Copyright (C) 2016-2021 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* global * Database */ class DictionaryDatabase { constructor() { this._db = new Database(); this._dbName = 'dict'; this._schemas = new Map(); } // Public async prepare() { await this._db.open( this._dbName, 60, [ { version: 20, stores: { terms: { primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading'] }, kanji: { primaryKey: {autoIncrement: true}, indices: ['dictionary', 'character'] }, tagMeta: { primaryKey: {autoIncrement: true}, indices: ['dictionary'] }, dictionaries: { primaryKey: {autoIncrement: true}, indices: ['title', 'version'] } } }, { version: 30, stores: { termMeta: { primaryKey: {autoIncrement: true}, indices: ['dictionary', 'expression'] }, kanjiMeta: { primaryKey: {autoIncrement: true}, indices: ['dictionary', 'character'] }, tagMeta: { primaryKey: {autoIncrement: true}, indices: ['dictionary', 'name'] } } }, { version: 40, stores: { terms: { primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading', 'sequence'] } } }, { version: 50, stores: { terms: { primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse'] } } }, { version: 60, stores: { media: { primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'path'] } } } ] ); } async close() { this._db.close(); } isPrepared() { return this._db.isOpen(); } async purge() { if (this._db.isOpening()) { throw new Error('Cannot purge database while opening'); } if (this._db.isOpen()) { this._db.close(); } let result = false; try { await Database.deleteDatabase(this._dbName); result = true; } catch (e) { yomichan.logError(e); } await this.prepare(); return result; } async deleteDictionary(dictionaryName, progressSettings, onProgress) { const targets = [ ['dictionaries', 'title'], ['kanji', 'dictionary'], ['kanjiMeta', 'dictionary'], ['terms', 'dictionary'], ['termMeta', 'dictionary'], ['tagMeta', 'dictionary'], ['media', 'dictionary'] ]; const {rate} = progressSettings; const progressData = { count: 0, processed: 0, storeCount: targets.length, storesProcesed: 0 }; const filterKeys = (keys) => { ++progressData.storesProcesed; progressData.count += keys.length; onProgress(progressData); return keys; }; const onProgress2 = () => { const processed = progressData.processed + 1; progressData.processed = processed; if ((processed % rate) === 0 || processed === progressData.count) { onProgress(progressData); } }; const promises = []; for (const [objectStoreName, indexName] of targets) { const query = IDBKeyRange.only(dictionaryName); const promise = this._db.bulkDelete(objectStoreName, indexName, query, filterKeys, onProgress2); promises.push(promise); } await Promise.all(promises); } findTermsBulk(termList, dictionaries, wildcard) { return new Promise((resolve, reject) => { const results = []; const count = termList.length; if (count === 0) { resolve(results); return; } const visited = new Set(); const useWildcard = !!wildcard; const prefixWildcard = wildcard === 'prefix'; const transaction = this._db.transaction(['terms'], 'readonly'); const terms = transaction.objectStore('terms'); const index1 = terms.index(prefixWildcard ? 'expressionReverse' : 'expression'); const index2 = terms.index(prefixWildcard ? 'readingReverse' : 'reading'); const count2 = count * 2; let completeCount = 0; for (let i = 0; i < count; ++i) { const inputIndex = i; const term = prefixWildcard ? stringReverse(termList[i]) : termList[i]; const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term); const onGetAll = (rows) => { for (const row of rows) { if (dictionaries.has(row.dictionary) && !visited.has(row.id)) { visited.add(row.id); results.push(this._createTerm(row, inputIndex)); } } if (++completeCount >= count2) { resolve(results); } }; this._db.getAll(index1, query, onGetAll, reject); this._db.getAll(index2, query, onGetAll, reject); } }); } findTermsExactBulk(termList, readingList, dictionaries) { return new Promise((resolve, reject) => { const results = []; const count = termList.length; if (count === 0) { resolve(results); return; } const transaction = this._db.transaction(['terms'], 'readonly'); const terms = transaction.objectStore('terms'); const index = terms.index('expression'); let completeCount = 0; for (let i = 0; i < count; ++i) { const inputIndex = i; const reading = readingList[i]; const query = IDBKeyRange.only(termList[i]); const onGetAll = (rows) => { for (const row of rows) { if (row.reading === reading && dictionaries.has(row.dictionary)) { results.push(this._createTerm(row, inputIndex)); } } if (++completeCount >= count) { resolve(results); } }; this._db.getAll(index, query, onGetAll, reject); } }); } findTermsBySequenceBulk(sequenceList, mainDictionary) { return new Promise((resolve, reject) => { const results = []; const count = sequenceList.length; if (count === 0) { resolve(results); return; } const transaction = this._db.transaction(['terms'], 'readonly'); const terms = transaction.objectStore('terms'); const index = terms.index('sequence'); let completeCount = 0; for (let i = 0; i < count; ++i) { const inputIndex = i; const query = IDBKeyRange.only(sequenceList[i]); const onGetAll = (rows) => { for (const row of rows) { if (row.dictionary === mainDictionary) { results.push(this._createTerm(row, inputIndex)); } } if (++completeCount >= count) { resolve(results); } }; this._db.getAll(index, query, onGetAll, reject); } }); } findTermMetaBulk(termList, dictionaries) { return this._findGenericBulk('termMeta', 'expression', termList, dictionaries, this._createTermMeta.bind(this)); } findKanjiBulk(kanjiList, dictionaries) { return this._findGenericBulk('kanji', 'character', kanjiList, dictionaries, this._createKanji.bind(this)); } findKanjiMetaBulk(kanjiList, dictionaries) { return this._findGenericBulk('kanjiMeta', 'character', kanjiList, dictionaries, this._createKanjiMeta.bind(this)); } findTagForTitle(name, title) { const query = IDBKeyRange.only(name); return this._db.find('tagMeta', 'name', query, (row) => (row.dictionary === title), null); } getMedia(targets) { return new Promise((resolve, reject) => { const count = targets.length; const results = new Array(count).fill(null); if (count === 0) { resolve(results); return; } let completeCount = 0; const transaction = this._db.transaction(['media'], 'readonly'); const objectStore = transaction.objectStore('media'); const index = objectStore.index('path'); for (let i = 0; i < count; ++i) { const inputIndex = i; const {path, dictionaryName} = targets[i]; const query = IDBKeyRange.only(path); const onGetAll = (rows) => { for (const row of rows) { if (row.dictionary !== dictionaryName) { continue; } results[inputIndex] = this._createMedia(row, inputIndex); } if (++completeCount >= count) { resolve(results); } }; this._db.getAll(index, query, onGetAll, reject); } }); } getDictionaryInfo() { return new Promise((resolve, reject) => { const transaction = this._db.transaction(['dictionaries'], 'readonly'); const objectStore = transaction.objectStore('dictionaries'); this._db.getAll(objectStore, null, resolve, reject); }); } getDictionaryCounts(dictionaryNames, getTotal) { return new Promise((resolve, reject) => { const targets = [ ['kanji', 'dictionary'], ['kanjiMeta', 'dictionary'], ['terms', 'dictionary'], ['termMeta', 'dictionary'], ['tagMeta', 'dictionary'], ['media', 'dictionary'] ]; const objectStoreNames = targets.map(([objectStoreName]) => objectStoreName); const transaction = this._db.transaction(objectStoreNames, 'readonly'); const databaseTargets = targets.map(([objectStoreName, indexName]) => { const objectStore = transaction.objectStore(objectStoreName); const index = objectStore.index(indexName); return {objectStore, index}; }); const countTargets = []; if (getTotal) { for (const {objectStore} of databaseTargets) { countTargets.push([objectStore, null]); } } for (const dictionaryName of dictionaryNames) { const query = IDBKeyRange.only(dictionaryName); for (const {index} of databaseTargets) { countTargets.push([index, query]); } } const onCountComplete = (results) => { const resultCount = results.length; const targetCount = targets.length; const counts = []; for (let i = 0; i < resultCount; i += targetCount) { const countGroup = {}; for (let j = 0; j < targetCount; ++j) { countGroup[targets[j][0]] = results[i + j]; } counts.push(countGroup); } const total = getTotal ? counts.shift() : null; resolve({total, counts}); }; this._db.bulkCount(countTargets, onCountComplete, reject); }); } async dictionaryExists(title) { const query = IDBKeyRange.only(title); const result = await this._db.find('dictionaries', 'title', query); return typeof result !== 'undefined'; } bulkAdd(objectStoreName, items, start, count) { return this._db.bulkAdd(objectStoreName, items, start, count); } // Private async _findGenericBulk(objectStoreName, indexName, indexValueList, dictionaries, createResult) { return new Promise((resolve, reject) => { const results = []; const count = indexValueList.length; if (count === 0) { resolve(results); return; } const transaction = this._db.transaction([objectStoreName], 'readonly'); const terms = transaction.objectStore(objectStoreName); const index = terms.index(indexName); let completeCount = 0; for (let i = 0; i < count; ++i) { const inputIndex = i; const query = IDBKeyRange.only(indexValueList[i]); const onGetAll = (rows) => { for (const row of rows) { if (dictionaries.has(row.dictionary)) { results.push(createResult(row, inputIndex)); } } if (++completeCount >= count) { resolve(results); } }; this._db.getAll(index, query, onGetAll, reject); } }); } _createTerm(row, index) { return { index, expression: row.expression, reading: row.reading, definitionTags: this._splitField(row.definitionTags || row.tags || ''), termTags: this._splitField(row.termTags || ''), rules: this._splitField(row.rules), glossary: row.glossary, score: row.score, dictionary: row.dictionary, id: row.id, sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence }; } _createKanji(row, index) { return { index, character: row.character, onyomi: this._splitField(row.onyomi), kunyomi: this._splitField(row.kunyomi), tags: this._splitField(row.tags), glossary: row.meanings, stats: row.stats, dictionary: row.dictionary }; } _createTermMeta({expression, mode, data, dictionary}, index) { return {expression, mode, data, dictionary, index}; } _createKanjiMeta({character, mode, data, dictionary}, index) { return {character, mode, data, dictionary, index}; } _createMedia(row, index) { return Object.assign({}, row, {index}); } _splitField(field) { return field.length === 0 ? [] : field.split(' '); } }