async _fetchCoverArtInfo(cancellationToken, acoustIdResult, trackInfo) {
        const {trackUid} = trackInfo;
        const {album, title} = acoustIdResult;
        let mbid, type;

        if (album && album.mbid) {
            ({mbid, type} = album);
        } else if (title && title.mbid) {
            ({mbid, type} = title);
        } else {
            return false;
        }

        const {artist: taggedArtist,
                album: taggedAlbum} = trackInfo;
        try {
            const response = await ajaxGet(toCorsUrl(`https://coverartarchive.org/${type}/${mbid}`), cancellationToken);
            if (response && response.images && response.images.length > 0) {
                await this.database.addAlbumArtData(trackUid, {
                    trackUid,
                    images: response.images.map(i => Object.assign({[IMAGE_TYPE_KEY]: IMAGE_TYPE_COVERARTARCHIVE}, i)),
                    artist: taggedArtist,
                    album: taggedAlbum
                });
                return true;
            }
        } catch (e) {
            if (e.status !== 404) {
                throw e;
            }
        }
        return false;
    }
    async _fetchAcoustId(cancellationToken, uid, fingerprint, duration) {
        const data = queryString({
            client: `djbbrJFK`,
            format: `json`,
            duration: duration | 0,
            meta: `recordings+releasegroups+compress`,
            fingerprint
        });
        const url = `https://api.acoustId.org/v2/lookup?${data}`;

        let result;
        const fullResponse = await ajaxGet(toCorsUrl(url), cancellationToken);
        if (fullResponse.results && fullResponse.results.length > 0) {
            result = parseAcoustId(fullResponse, duration | 0);
        }
        const trackInfo = await this.getTrackInfoByTrackUid(uid);
        const wasAutogenerated = trackInfo.autogenerated;

        let trackInfoUpdated = false;
        if (result) {
            trackInfo.autogenerated = false;
            const {album: albumResult,
                   title: titleResult,
                   artist: artistResult,
                   albumArtist: albumArtistResult} = result;
            const {name: album} = albumResult || {};
            const {name: title} = titleResult || {};
            const {name: artist} = artistResult || {};
            const {name: albumArtist} = albumArtistResult || {};

            if ((isUnknown(trackInfo.title) || wasAutogenerated) && title) {
                trackInfo.title = title;
                trackInfoUpdated = true;
            }

            if ((isUnknown(trackInfo.album) || wasAutogenerated) && album) {
                trackInfo.album = album;
                trackInfoUpdated = true;
            }

            if ((isUnknown(trackInfo.albumArtist) || wasAutogenerated) && albumArtist) {
                trackInfo.albumArtist = albumArtist;
                trackInfoUpdated = true;
            }

            if ((isUnknown(trackInfo.artist) || wasAutogenerated) && artist) {
                trackInfo.artist = artist;
                trackInfoUpdated = true;
            }
        }

        await this.database.replaceTrackInfo(uid, trackInfo);
        return {
            acoustIdResult: result || null,
            trackInfo,
            trackInfoUpdated
        };
    }
    constructor(wasm, database, searchBackend) {
        super(METADATA_MANAGER_READY_EVENT_NAME, database);
        this._searchBackend = searchBackend;
        this._wasm = wasm;
        this._blobUrls = [];
        this._blobUrlSize = 0;
        this._trackInfoEntriesCount = 0;
        this._kvdb = null;

        this._loudnessAnalyzerStateSerializer = new JobProcessor({jobCallback: this._loudnessAnalysisJob.bind(this)});
        this._acoustIdDataFetcher = new JobProcessor({delay: 1000, jobCallback: this._fetchAcoustIdDataJob.bind(this)});
        this._fingerprinter = new JobProcessor({jobCallback: this._fingerprintJob.bind(this)});
        this._metadataParser = new JobProcessor({jobCallback: this._parseMetadataJob.bind(this), parallelJobs: 8});
        this._coverArtDownloader = new JobProcessor({
            async jobCallback({cancellationToken}, url) {
                while (!cancellationToken.isCancelled()) {
                    try {
                        const result = await ajaxGet(toCorsUrl(url), cancellationToken, {responseType: `blob`});
                        return result;
                    } catch (e) {
                        await delay(10000);
                    }
                }
                return null;
            },
            parallelJobs: 3
        });


        this.actions = {
            setRating({trackUid, rating}) {
                if (!this.canUseDatabase()) return;
                this.database.updateRating(trackUid, rating);
            },

            setSkipCounter({trackUid, counter, lastPlayed}) {
                if (!this.canUseDatabase()) return;
                this.database.updateSkipCounter(trackUid, counter, lastPlayed);
            },

            setPlaythroughCounter({trackUid, counter, lastPlayed}) {
                if (!this.canUseDatabase()) return;
                this.database.updatePlaythroughCounter(trackUid, counter, lastPlayed);
            },

            async getAlbumArt({trackUid, artist, album, preference, requestReason}) {
                if (!this.canUseDatabase()) return;
                const albumArt = await this._getAlbumArt(trackUid, artist, album, preference);
                const result = {albumArt, trackUid, preference, requestReason};
                this.postMessage({type: ALBUM_ART_RESULT_MESSAGE, result});
            },

            async parseMetadata({fileReference}) {
                if (!this.canUseDatabase()) return;
                const trackUid = await fileReferenceToTrackUid(fileReference);
                const result = await this._parseMetadata(trackUid, fileReference);
                if (!result) {
                    return;
                }
                this.postMessage({type: METADATA_RESULT_MESSAGE, result});

                if (result.trackInfo) {
                    if (!result.trackInfo.hasBeenFingerprinted) {
                        this._fingerprinter.postJob(trackUid, fileReference);
                    }

                    if (!result.trackInfo.hasInitialLoudnessInfo) {
                        this._loudnessAnalyzerStateSerializer.postJob(trackUid, fileReference);
                    }
                }

            },

            async getTrackInfoBatch({batch}) {
                if (!this.canUseDatabase()) return;
                const missing = [];
                const trackInfos = await this.database.trackUidsToTrackInfos(batch, missing);
                for (let i = 0; i < trackInfos.length; ++i) {
                    const trackInfo = trackInfos[i];
                    const {trackUid} = trackInfo;
                    if (!trackInfo.hasBeenFingerprinted) {
                        this._fingerprinter.postJob(trackUid, trackUid);
                    }

                    if (!trackInfo.hasInitialLoudnessInfo) {
                        this._loudnessAnalyzerStateSerializer.postJob(trackUid, trackUid);
                    }
                }

                for (let i = 0; i < missing.length; ++i) {
                    this.actions.parseMetadata.call(this, {fileReference: missing[i]});
                }

                this.postMessage({type: TRACKINFO_BATCH_RESULT_MESSAGE, result: {trackInfos}});
            },

            async mapTrackUidsToFiles({trackUids}) {
                if (!this.canUseDatabase()) return;
                const missing = [];
                const files = await this.database.trackUidsToFiles(trackUids, missing);
                this.postMessage({type: UIDS_MAPPED_TO_FILES_MESSAGE, result: {files}});

                for (let i = 0; i < missing.length; ++i) {
                    const trackUid = missing[i];
                    this.postMessage({
                        type: FILE_REFERENCE_UNAVAILABLE_MESSAGE,
                        result: {trackUid}
                    });
                }
            },

            async parseTmpFile({tmpFileId}) {
                if (!this.canUseDatabase()) return;
                await this._checkKvdb();
                const tmpFile = await this._kvdb.consumeTmpFileById(tmpFileId);
                if (!tmpFile) {
                    return;
                }

                const {file} = tmpFile;
                const trackUid = await fileReferenceToTrackUid(file);
                const trackInfo = await this.getTrackInfoByTrackUid(trackUid);
                if (!trackInfo) {
                    const result = await this._parseMetadata(trackUid, file);
                    if (result && result.trackInfo) {
                        if (!result.trackInfo.hasBeenFingerprinted) {
                            this._fingerprinter.postJob(trackUid, trackUid);
                        }

                        if (!result.trackInfo.hasInitialLoudnessInfo) {
                            this._loudnessAnalyzerStateSerializer.postJob(trackUid, trackUid);
                        }
                        this.postMessage({type: NEW_TRACK_FROM_TMP_FILE_MESSAGE, result: {
                            trackInfo: result.trackInfo
                        }});
                    }
                } else {
                    try {
                        await this.database.ensureFileStored(trackUid, file);
                    } catch (e) {
                        if (!this._checkStorageError(e)) {
                            throw e;
                        }
                    }
                }
            }
        };
        this._acoustIdDataFetcher.on(JOB_COMPLETE_EVENT, async (job) => {
            const result = await job.promise;
            if (result !== NO_JOBS_FOUND_TOKEN) {
                this._acoustIdDataFetcher.postJob();
            }
        });
        this._acoustIdDataFetcher.postJob();
        this._metadataParser.on(ALL_JOBS_COMPLETE_EVENT, () => {
            this.postMessage({type: ALL_FILES_PERSISTED_MESSAGE});
        });
        this._updateMediaLibrarySize();
    }