async addAlbumArtData(trackUid, albumArtData) {
        this._checkClosed();
        const db = await this.db;
        const tx = db.transaction(ALBUM_ART_OBJECT_STORE_NAME, READ_WRITE);
        const store = tx.objectStore(ALBUM_ART_OBJECT_STORE_NAME);
        const storedData = await iDbPromisify(store.get(IDBKeyRange.only(trackUid)));

        if (storedData && storedData.images && storedData.images.length > 0) {
            const storedImages = storedData.images;
            const newImages = albumArtData.images;

            for (let i = 0; i < newImages.length; ++i) {
                const newImage = newImages[i];
                const {imageType, image} = newImage;
                let shouldBeAdded = true;
                for (let j = 0; j < storedImages.length; ++j) {
                    const storedImage = storedImages[j];
                    if (storedImage.imageType === imageType &&
                        storedImage.image === image) {
                        shouldBeAdded = false;
                        break;
                    }
                }

                if (shouldBeAdded) {
                    storedImages.push(newImage);
                }
            }
            storedData.images = storedImages;
            return iDbPromisify(store.put(storedData));
        } else {
            albumArtData.trackUid = trackUid;
            return iDbPromisify(store.put(albumArtData));
        }
    }
 async consumeTmpFileById(tmpFileId) {
     this._checkClosed();
     const db = await this.db;
     const store = db.transaction(TMP_FILES_OBJECT_STORE_NAME, READ_WRITE).objectStore(TMP_FILES_OBJECT_STORE_NAME);
     const ret = await iDbPromisify(store.get(tmpFileId));
     await iDbPromisify(store.delete(tmpFileId));
     return ret;
 }
 async addTrackInfo(trackUid, trackInfo) {
     this._checkClosed();
     trackInfo.trackUid = trackUid;
     const db = await this.db;
     const transaction = db.transaction(TRACK_INFO_OBJECT_STORE_NAME, READ_WRITE);
     const store = transaction.objectStore(TRACK_INFO_OBJECT_STORE_NAME);
     const previousTrackInfo = await iDbPromisify(store.get(trackUid));
     const newTrackInfo = Object.assign({}, previousTrackInfo || {}, trackInfo);
     await iDbPromisify(store.put(newTrackInfo));
     return newTrackInfo;
 }
 async completeAcoustIdFetchJob(jobId) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction([ACOUST_ID_JOB_OBJECT_STORE_NAME], READ_WRITE);
     const acoustIdStore = tx.objectStore(ACOUST_ID_JOB_OBJECT_STORE_NAME);
     const job = await iDbPromisify(acoustIdStore.get(IDBKeyRange.only(jobId)));
     if (!job) {
         return;
     }
     const jobDeleted = iDbPromisify(acoustIdStore.delete(IDBKeyRange.only(jobId)));
     await jobDeleted;
 }
 async setAcoustIdFetchJobError(jobId, error) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(ACOUST_ID_JOB_OBJECT_STORE_NAME, READ_WRITE);
     const store = tx.objectStore(ACOUST_ID_JOB_OBJECT_STORE_NAME);
     const job = await iDbPromisify(store.get(IDBKeyRange.only(jobId)));
     job.lastTried = new Date();
     job.lastError = {
         message: error && error.message || `${error}`,
         stack: error && error.stack || null
     };
     return iDbPromisify(store.put(job));
 }
    async updateAcoustIdFetchJobState(trackUid, data) {
        this._checkClosed();
        const db = await this.db;
        const tx = db.transaction(ACOUST_ID_JOB_OBJECT_STORE_NAME, READ_WRITE);
        const store = tx.objectStore(ACOUST_ID_JOB_OBJECT_STORE_NAME);
        const uidIndex = store.index(`trackUid`);
        const job = await iDbPromisify(uidIndex.get(IDBKeyRange.only(trackUid)));

        if (!job) {
            return;
        }
        Object.assign(job, data);
        await iDbPromisify(store.put(job));
    }
 async storeLog(message) {
     if (this.isClosedAndEmit()) return null;
     const date = new Date();
     const db = await this.db;
     const store = db.transaction(LOG_OBJECT_STORE_NAME, READ_WRITE).objectStore(LOG_OBJECT_STORE_NAME);
     return iDbPromisify(store.add({message, date}));
 }
 async setLoudnessAnalyzerStateForTrack(trackUid, serializedState) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(LOUDNESS_ANALYZER_SERIALIZED_STATE_STORE_NAME, READ_WRITE);
     const store = tx.objectStore(LOUDNESS_ANALYZER_SERIALIZED_STATE_STORE_NAME);
     return iDbPromisify(store.put({trackUid, serializedState}));
 }
 async getLoudnessAnalyzerStateForTrack(trackUid) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(LOUDNESS_ANALYZER_SERIALIZED_STATE_STORE_NAME, READ_ONLY);
     const store = tx.objectStore(LOUDNESS_ANALYZER_SERIALIZED_STATE_STORE_NAME);
     return iDbPromisify(store.get(trackUid));
 }
 async get(key) {
     if (this.isClosedAndEmit()) return null;
     key = `${key}`;
     const db = await this.db;
     const store = db.transaction(KEY_VALUE_PAIRS_OBJECT_STORE_NAME, READ_ONLY).objectStore(KEY_VALUE_PAIRS_OBJECT_STORE_NAME);
     return iDbPromisify(store.get(key));
 }
 async replaceTrackInfo(trackUid, trackInfo) {
     this._checkClosed();
     trackInfo.trackUid = trackUid;
     const db = await this.db;
     const tx = db.transaction(TRACK_INFO_OBJECT_STORE_NAME, READ_WRITE).objectStore(TRACK_INFO_OBJECT_STORE_NAME);
     return iDbPromisify(tx.put(trackInfo));
 }
    async fileByFileReference(fileReference) {
        this._checkClosed();
        if (fileReference instanceof File) {
            return fileReference;
        } else if (fileReference instanceof ArrayBuffer) {
            const trackUid = fileReference;
            const db = await this.db;
            const tx = db.transaction(TRACK_PAYLOAD_OBJECT_STORE_NAME, READ_ONLY);
            const store = tx.objectStore(TRACK_PAYLOAD_OBJECT_STORE_NAME);
            const result = await iDbPromisify(store.get(IDBKeyRange.only(trackUid)));

            if (!result) {
                return result;
            }

            if (result.payloadType === PAYLOAD_TYPE_FILESYSTEM_FILE) {
                if (await this._canProbablyStoreInFs()) {
                    return this.fs.getFileByTrackUid(trackUid);
                }
                return null;
            }

            return result.file ? result.file : null;
        } else {
            throw new Error(`invalid fileReference`);
        }
    }
    async addSearchIndexEntryForTrackIfNotPresent(entry) {
        this._checkClosed();
        const db = await this.db;
        const tx = db.transaction(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME, READ_WRITE);
        const store = tx.objectStore(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME);
        const {trackUid} = entry;
        const key = IDBKeyRange.only(trackUid);

        const result = await iDbPromisify(store.getKey(key));

        if (result) {
            return;
        }

        await iDbPromisify(store.add(entry));
    }
 async updateSearchIndexEntryForTrack(entry) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME, READ_WRITE);
     const store = tx.objectStore(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME);
     await iDbPromisify(store.put(entry));
 }
 async getAcoustIdFetchJob() {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(ACOUST_ID_JOB_OBJECT_STORE_NAME, READ_ONLY);
     const store = tx.objectStore(ACOUST_ID_JOB_OBJECT_STORE_NAME);
     const index = store.index(`lastTried`);
     return iDbPromisify(index.get(IDBKeyRange.lowerBound(new Date(0))));
 }
    async getAlbumArtData(trackUid, artist, album) {
        this._checkClosed();
        const db = await this.db;
        const tx = db.transaction(ALBUM_ART_OBJECT_STORE_NAME, READ_ONLY);
        const store = tx.objectStore(ALBUM_ART_OBJECT_STORE_NAME);
        const result = await iDbPromisify(store.get(IDBKeyRange.only(trackUid)));

        if (result) {
            return result;
        }

        if (artist && album) {
            const index = store.index(`artistAlbum`);
            return iDbPromisify(index.get(IDBKeyRange.only([artist, album])));
        }
        return null;
    }
 async searchSuffixes(firstSuffixKeyword) {
     this._checkClosed();
     const db = await this.db;
     const tx = db.transaction(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME, READ_ONLY);
     const store = tx.objectStore(TRACK_SEARCH_INDEX_OBJECT_STORE_NAME);
     const index = store.index(`suffixMulti`);
     const key = IDBKeyRange.bound(firstSuffixKeyword, `${firstSuffixKeyword}\uffff`, false, false);
     return iDbPromisify(index.getAll(key));
 }
    async addAcoustIdFetchJob(trackUid, fingerprint, duration, state) {
        this._checkClosed();
        const db = await this.db;
        const tx = db.transaction(ACOUST_ID_JOB_OBJECT_STORE_NAME, READ_WRITE);
        const store = tx.objectStore(ACOUST_ID_JOB_OBJECT_STORE_NAME);
        const uidIndex = store.index(`trackUid`);
        const key = await iDbPromisify(uidIndex.getKey(IDBKeyRange.only(trackUid)));

        if (key) {
            return;
        }

        await iDbPromisify(store.add({
            trackUid, created: new Date(),
            fingerprint, duration, lastError: null,
            lastTried: new Date(0), state
        }));
    }
const fieldUpdater = function(...fieldNames) {
    return {
        async method(trackUid, ...values) {
            this._checkClosed();
            const db = await this.db;
            const tx = db.transaction(TRACK_INFO_OBJECT_STORE_NAME, READ_WRITE);
            const store = tx.objectStore(TRACK_INFO_OBJECT_STORE_NAME);
            let data = await iDbPromisify(store.get(trackUid));
            data = Object(data);
            data.trackUid = trackUid;
            for (let i = 0; i < fieldNames.length; ++i) {
                const name = fieldNames[i];
                const value = values[i];
                data[name] = value;
            }
            return iDbPromisify(store.put(data));
        }
    };
};
 async addTmpFile(file, source) {
     this._checkClosed();
     const db = await this.db;
     const store = db.transaction(TMP_FILES_OBJECT_STORE_NAME, READ_WRITE).objectStore(TMP_FILES_OBJECT_STORE_NAME);
     const obj = {
         created: new Date(),
         file,
         [SOURCE_KEY]: source
     };
     const ret = await iDbPromisify(store.add(obj));
     return ret;
 }
    async getAll() {
        if (this.isClosedAndEmit()) return {};
        const db = await this.db;
        const store = db.transaction(KEY_VALUE_PAIRS_OBJECT_STORE_NAME, READ_ONLY).objectStore(KEY_VALUE_PAIRS_OBJECT_STORE_NAME);
        const keyValuePairs = await iDbPromisify(store.getAll());

        const ret = Object.create(null);
        keyValuePairs.forEach((pair) => {
            ret[pair.key] = pair.value;
        });
        return ret;
    }
 getKeySetter(key) {
     if (!this.keySetters[key]) {
         const keySetter = {
             async method(value) {
                 this._checkClosed();
                 const db = await this.db;
                 const transaction = db.transaction(KEY_VALUE_PAIRS_OBJECT_STORE_NAME, READ_WRITE);
                 const store = transaction.objectStore(KEY_VALUE_PAIRS_OBJECT_STORE_NAME);
                 const existingData = await iDbPromisify(store.get(key));
                 if (existingData) {
                     existingData.value = value;
                     return iDbPromisify(store.put(existingData));
                 } else {
                     const data = {key, value};
                     return iDbPromisify(store.add(data));
                 }
             }
         };
         this.keySetters[key] = keySetter.method;
     }
     return this.keySetters[key];
 }
    constructor() {
        super();
        this._closed = false;
        /* eslint-disable no-undef */
        const request = indexedDB.open(NAME, VERSION);
        /* eslint-enable no-undef */
        this.db = iDbPromisify(request);
        request.onupgradeneeded = (event) => {
            applyStoreSpec(event.target.transaction, objectStoreSpec);
        };
        this.initialValues = this.getAll();
        this.keySetters = Object.create(null);

        this._uiLogRef = self.uiLog;
        self.uiLog = this.uiLogOverwrite.bind(this);
        this._lastLog = null;
        this._setHandlers();
    }
    constructor() {
        this._closed = false;
        const request = indexedDB.open(NAME, VERSION);
        this.db = iDbPromisify(request);
        this.fs = new FileSystemWrapper();
        request.onupgradeneeded = (event) => {
            const {target} = event;
            const {transaction} = target;
            const stores = applyStoreSpec(transaction, objectStoreSpec);
            if (event.oldVersion < DATA_WIPE_VERSION) {
                for (const key of Object.keys(stores)) {
                    stores[key].clear();
                }
            }

        };
        this._setHandlers();
        this._usageAndQuota = null;
    }
 async getTrackInfoByTrackUid(trackUid) {
     this._checkClosed();
     const db = await this.db;
     const store = db.transaction(TRACK_INFO_OBJECT_STORE_NAME, READ_ONLY).objectStore(TRACK_INFO_OBJECT_STORE_NAME);
     return iDbPromisify(store.get(trackUid));
 }
 async clearTmpFiles() {
     this._checkClosed();
     const db = await this.db;
     const store = db.transaction(TMP_FILES_OBJECT_STORE_NAME, READ_WRITE).objectStore(TMP_FILES_OBJECT_STORE_NAME);
     await iDbPromisify(store.clear());
 }
    async ensureFileStored(trackUid, fileReference) {
        this._checkClosed();
        if (fileReference instanceof ArrayBuffer) {
            return false;
        } else if (fileReference instanceof File) {

            const db = await this.db;
            let tx = db.transaction(TRACK_PAYLOAD_OBJECT_STORE_NAME, READ_ONLY);
            let store = tx.objectStore(TRACK_PAYLOAD_OBJECT_STORE_NAME);
            const result = await iDbPromisify(store.get(trackUid));

            if (result) {
                return false;
            }

            let fsPath = null;
            let canStoreInFs = true;
            if (await this._canProbablyStoreInFs()) {
                try {
                    fsPath = await this.fs.storeFileByTrackUid(trackUid, fileReference);
                } catch (e) {
                    if (e.name !== QUOTA_EXCEEDED_ERROR) {
                        throw e;
                    }
                    canStoreInFs = false;
                }
            }

            if (canStoreInFs && !fsPath) {
                return false;
            }

            const data = canStoreInFs ? {
                payloadType: PAYLOAD_TYPE_FILESYSTEM_FILE,
                trackUid,
                originalLastModified: fileReference.lastModified,
                originalName: fileReference.name,
                originalSize: fileReference.size,
                originalType: fileReference.type,
                fileSystemPath: fsPath
            } : {
                payloadType: PAYLOAD_TYPE_INDEXED_DB_FILE,
                file: fileReference,
                trackUid
            };

            tx = db.transaction(TRACK_PAYLOAD_OBJECT_STORE_NAME, READ_WRITE);
            store = tx.objectStore(TRACK_PAYLOAD_OBJECT_STORE_NAME);

            try {
                await iDbPromisify(store.add(data));
                return true;
            } catch (e) {
                if (e.name !== CONSTRAINT_ERROR) {
                    throw e;
                }
                return false;
            }
        } else {
            throw new Error(`invalid fileReference`);
        }
    }
 async getTmpFileById(tmpFileId) {
     this._checkClosed();
     const db = await this.db;
     const store = db.transaction(TMP_FILES_OBJECT_STORE_NAME, READ_ONLY).objectStore(TMP_FILES_OBJECT_STORE_NAME);
     return iDbPromisify(store.get(tmpFileId));
 }