Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
314 lines
10 KiB
JavaScript
Executable file
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;
|
|
}
|
|
};
|
|
})();
|