VulpesOS/gaia/dev_apps/music-oga/js/db.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

314 lines
10 KiB
JavaScript
Executable file

/* exported Database */
/* global AlbumArt, App, AudioMetadata, ForwardLock, LazyLoader, MediaDB,
Normalizer, TitleBar */
'use strict';
var Database = (function() {
// The MediaDB object that manages the filesystem and the database of metadata
// See init()
var musicdb;
function init() {
// We want to exclude some folders that store ringtones so they don't show
// up in the music app. The regex matches absolute paths starting with a
// volume name (e.g. "/volume-name/Ringtones/") or relative paths starting
// with the excluded folder name (e.g. "Ringtones/").
var excludedFolders = ['Ringtones', 'Notifications', 'Alarms'];
var excludeFilter = new RegExp(
'^(/[^/]*/)?(' + excludedFolders.join('|') + ')/', 'i'
);
// Here we use the mediadb.js which gallery is using (in shared/js/)
// to index our music contents with metadata parsed.
// So the behaviors of musicdb are the same as the MediaDB in gallery
musicdb = new MediaDB('music', metadataParserWrapper, {
indexes: ['metadata.album', 'metadata.artist', 'metadata.title',
'metadata.rated', 'metadata.played', 'date'],
excludeFilter: excludeFilter,
batchSize: 1,
autoscan: false, // We call scan() explicitly after listing music we know
updateRecord: updateRecord,
reparsedRecord: reparsedRecord,
version: 3
});
function metadataParserWrapper(file, onsuccess, onerror) {
var files = ['/js/metadata_scripts.js', '/js/metadata/album_art.js'];
LazyLoader.load(files).then(() => {
return AudioMetadata.parse(file);
}).then((metadata) => {
return AlbumArt.process(file, metadata);
}).then(onsuccess, onerror);
}
var startedReparsing = false;
function updateRecord(record, oldVersion, newVersion) {
if (oldVersion === 2) {
// Version 3 of the music DB changes ID3 parsing, so we need to reparse
// the file from scratch!
record.needsReparse = true;
if (!startedReparsing) {
startedReparsing = true;
App.showOverlay('upgrade');
}
}
return record.metadata;
}
function reparsedRecord(oldMetadata, newMetadata) {
// We assume that updateRecord has already changed oldMetadata if
// necessary. (It's not necessary at the moment).
newMetadata.rated = oldMetadata.rated;
newMetadata.played = oldMetadata.played;
return newMetadata;
}
// show dialog in upgradestart, when it finished, it will turned to ready.
musicdb.onupgrading = function(event) {
App.showOverlay('upgrade');
};
// This is called when DeviceStorage becomes unavailable because the
// sd card is removed or because it is mounted for USB mass storage
// This may be called before onready if it is unavailable to begin with
musicdb.onunavailable = function(event) {
var why;
switch (event.detail) {
case MediaDB.NOCARD:
why = 'nocard';
break;
case MediaDB.UNMOUNTED:
why = 'unmounted';
break;
}
App.dbUnavailable(why);
};
// If the user removed the sdcard (but there is still internal storage)
// we just need to stop playing, we don't have to put up an overlay.
// This event will be followed by deleted events to remove the songs
// that were on the sdcard and are no longer playable.
musicdb.oncardremoved = function() {
App.dbUnavailable('cardremoved');
};
musicdb.onenumerable = startupOnEnumerable;
// Don't refresh the UI on the first onready event, since onenumerable will
// have already handled the refresh.
var refreshOnReady = false;
function startupOnEnumerable() {
App.dbEnumerable(function() {
if (musicdb.state === MediaDB.READY) {
onReady();
} else {
musicdb.onready = onReady;
}
});
}
function onReady() {
App.dbReady(refreshOnReady, function() {
// Start scanning for new music
musicdb.scan();
});
// Subsequent onready events need to refresh the UI.
refreshOnReady = true;
}
var filesDeletedWhileScanning = 0;
var filesFoundWhileScanning = 0;
var filesFoundBatch = 0;
var scanning = false;
var SCAN_UPDATE_BATCH_SIZE = 25; // Redisplay after this many new files
var DELETE_BATCH_TIMEOUT = 500; // Redisplay this long after a delete
var deleteTimer = null;
var firstScanDone = false;
// When musicdb scans, let the user know
musicdb.onscanstart = function() {
scanning = true;
filesFoundWhileScanning = 0;
filesFoundBatch = 0;
filesDeletedWhileScanning = 0;
};
// And hide the throbber when scanning is done
musicdb.onscanend = function() {
scanning = false;
TitleBar.hideScanProgress();
if (filesFoundBatch > 0 || filesDeletedWhileScanning > 0) {
filesFoundWhileScanning = 0;
filesFoundBatch = 0;
filesDeletedWhileScanning = 0;
App.refreshViews();
}
// If this was the first scan after startup, tell the performance monitors
// that we finished loading everything.
if (!firstScanDone) {
firstScanDone = true;
window.performance.mark('fullyLoaded');
}
};
// When MediaDB finds new files, it sends created events. During
// scanning we may get lots of them. Bluetooth file transfer can
// also result in created events. The way the app is currently
// structured, all we can do is rebuild the entire UI with the
// updated list of files. We don't want to do this for every new file
// but we do want to redisplay every so often.
musicdb.oncreated = function(event) {
if (scanning) {
var metadata = event.detail[0].metadata;
var n = event.detail.length;
filesFoundWhileScanning += n;
filesFoundBatch += n;
TitleBar.showScanProgress({
count: filesFoundWhileScanning,
artist: metadata.artist,
title: metadata.title
});
if (filesFoundBatch > SCAN_UPDATE_BATCH_SIZE) {
filesFoundBatch = 0;
App.refreshViews();
}
}
else {
// If we get a created event while we are not scanning, then
// there was probably a new song saved via bluetooth or MMS.
// We don't have any way to be clever about it; we just have to
// redisplay the entire view
App.refreshViews();
}
};
// For deletions, we just set a flag and redisplay when the scan is done.
// This means that there is a longer window of time when the app might
// display music that is no longer available. But the only way to prevent
// this is to refuse to display any music until the scan completes.
musicdb.ondeleted = function(event) {
if (scanning) {
// If we get a deletion during a scan, just note it for processing
// when the scan is over
filesDeletedWhileScanning += event.detail.length;
}
else {
// Otherwise, if we're not scanning, this may be one in a series
// of deletions (we get lots when the sd card is pulled out, for
// example). Don't redisplay the UI right away. Instead, wait until the
// deletions seem to have stopped or paused before updating
if (deleteTimer) {
clearTimeout(deleteTimer);
}
deleteTimer = setTimeout(function() {
deleteTimer = null;
App.refreshViews();
}, DELETE_BATCH_TIMEOUT);
}
};
}
function incrementPlayCount(fileinfo) {
fileinfo.metadata.played++;
musicdb.updateMetadata(fileinfo.name, {played: fileinfo.metadata.played});
}
function setSongRating(fileinfo, rated) {
fileinfo.metadata.rated = rated;
musicdb.updateMetadata(fileinfo.name, {rated: fileinfo.metadata.rated});
}
function getFile(fileinfo, decrypt = false) {
return new Promise((resolve, reject) => {
musicdb.getFile(fileinfo.name, (file) => {
if (file) {
resolve(file);
} else {
reject('unable to get file: ' + fileinfo.name);
}
});
}).then((blob) => {
if (!decrypt || !fileinfo.metadata.locked) {
return blob;
}
return new Promise(function(resolve, reject) {
ForwardLock.getKey(function(secret) {
ForwardLock.unlockBlob(secret, blob, resolve, null, reject);
});
});
});
}
function enumerate(...args) {
return musicdb.enumerate(...args);
}
function enumerateAll(...args) {
return musicdb.enumerateAll(...args);
}
function advancedEnumerate(...args) {
return musicdb.advancedEnumerate(...args);
}
function count(...args) {
return musicdb.count(...args);
}
/**
* Search the music database for a particular query.
*
* @param {String} key The field to query against (e.g. 'title').
* @param {String} query The string to search for.
* @param {Function} callback A function to call with the results of the
* search, called once per result (plus a final call passing `null`).
* @return {Object} The handle for this query.
*/
function search(key, query, callback) {
// Convert to lowercase and replace accented characters.
query = Normalizer.toAscii(query.toLocaleLowerCase());
var direction = (key === 'title') ? 'next' : 'nextunique';
return musicdb.enumerate('metadata.' + key, null, direction, (result) => {
if (result === null) {
callback(result);
return;
}
var resultLowerCased = result.metadata[key].toLocaleLowerCase();
if (Normalizer.toAscii(resultLowerCased).indexOf(query) !== -1) {
callback(result);
}
});
}
function cancelEnumeration(handle) {
musicdb.cancelEnumeration(handle);
}
return {
init: init,
incrementPlayCount: incrementPlayCount,
setSongRating: setSongRating,
getFile: getFile,
enumerate: enumerate,
enumerateAll: enumerateAll,
advancedEnumerate: advancedEnumerate,
count: count,
search: search,
cancelEnumeration: cancelEnumeration,
// This is just here for testing.
get initialScanComplete() {
return musicdb.initialScanComplete;
}
};
})();