Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
358 lines
12 KiB
JavaScript
Executable file
358 lines
12 KiB
JavaScript
Executable file
/* global AlbumArt, asyncStorage, Database, ImageUtils, LazyLoader */
|
|
/* exported AlbumArtCache */
|
|
'use strict';
|
|
|
|
/**
|
|
* Handle fetching album art and thumbnail management for already-parsed audio
|
|
* files.
|
|
*/
|
|
var AlbumArtCache = (function() {
|
|
|
|
// When we generate our own thumbnails, aim for this size. XXX: This should be
|
|
// improved to pick a size based on the resolution of our device.
|
|
var THUMBNAIL_WIDTH = 300;
|
|
var THUMBNAIL_HEIGHT = 300;
|
|
|
|
function ThumbnailCache() {
|
|
this._pendingThumbnails = {};
|
|
}
|
|
|
|
ThumbnailCache.prototype = {
|
|
/**
|
|
* Check the thumbnail cache (asyncStorage) to see if we already have a
|
|
* thumbnailized version of the album art. If not, create a thumbnail and
|
|
* return it.
|
|
*
|
|
* @param {Object} fileinfo The file's info.
|
|
* @param {String} cacheKey The cache key generated by makeCacheKey.
|
|
* @return {Promise} A promise returning the Blob for the thumbnail.
|
|
*/
|
|
get: function(fileinfo, cacheKey) {
|
|
if (!cacheKey) {
|
|
return this._makeThumbnail(fileinfo, cacheKey);
|
|
}
|
|
|
|
// If we've already requested this thumbnail from asyncStorage, just
|
|
// return the existing Promise.
|
|
if (cacheKey in this._pendingThumbnails) {
|
|
return this._pendingThumbnails[cacheKey];
|
|
}
|
|
|
|
// See if we've saved a blob in asyncStorage. If not, create a
|
|
// thumbnail blob and store it in the cache. In either case, return the
|
|
// blob.
|
|
var promise = new Promise((resolve, reject) => {
|
|
LazyLoader.load('/shared/js/async_storage.js').then(() => {
|
|
asyncStorage.getItem(cacheKey, (blob) => { resolve(blob); });
|
|
});
|
|
}).then((cachedBlob) => {
|
|
return cachedBlob || this._makeThumbnail(fileinfo, cacheKey);
|
|
});
|
|
|
|
this._pendingThumbnails[cacheKey] = promise;
|
|
return promise;
|
|
},
|
|
|
|
/**
|
|
* Create a thumbnail for the album art and store the Blob in our thumbnail
|
|
* cache.
|
|
*
|
|
* @param {Object} fileinfo The file's info.
|
|
* @param {String} cacheKey The cache key generated by makeCacheKey.
|
|
* @return {Promise} A promise returning the thumbnailized Blob.
|
|
*/
|
|
_makeThumbnail: function(fileinfo, cacheKey) {
|
|
// We don't have a saved blob yet, so grab it, thumbnailize it, and store
|
|
// it in asyncStorage.
|
|
return getAlbumArtBlob(fileinfo).then((blob) => {
|
|
return LazyLoader.load([
|
|
'/shared/js/async_storage.js',
|
|
'/shared/js/image_utils.js'
|
|
]).then(() => {
|
|
return ImageUtils.resizeAndCropToCover(
|
|
blob, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT
|
|
);
|
|
});
|
|
}).then((thumbnailBlob) => {
|
|
if (cacheKey) {
|
|
asyncStorage.setItem(cacheKey, thumbnailBlob);
|
|
}
|
|
return thumbnailBlob;
|
|
});
|
|
}
|
|
};
|
|
|
|
// The session caches for thumbnails and full-size images; maps a cache key
|
|
// (see below) to a Promise (to a blob: URL). There's also a long-term cache
|
|
// for thumbnails, backed by asyncStorage.
|
|
var sessionCache = {'thumbnail': {}, 'fullsize': {}};
|
|
var thumbnailCache = new ThumbnailCache();
|
|
|
|
/**
|
|
* Get a URL for a full-sized version of the cover art for a given file (if
|
|
* any).
|
|
*
|
|
* @param {Object} fileinfo The info for the file we want album art for.
|
|
* @param {Boolean} noPlaceholder True if this function should only return an
|
|
* image if there's cover art for the file. Otherwise, this function will
|
|
* return a placeholder image if there's no cover art.
|
|
* @return {Promise} A promise returning either the URL for the cover art, or
|
|
* null.
|
|
*/
|
|
function getFullSizeURL(fileinfo, noPlaceholder) {
|
|
// If there's no picture info in the metadata, return null or a placeholder.
|
|
if (!fileinfo.metadata.picture) {
|
|
return Promise.resolve(
|
|
noPlaceholder ? null : getDefaultCoverURL(fileinfo, 'fullsize')
|
|
);
|
|
}
|
|
|
|
// See if we've already requested a URL for this album. If so, return it.
|
|
var cacheKey = makeCacheKey(fileinfo);
|
|
if (cacheKey && cacheKey in sessionCache.fullsize) {
|
|
return sessionCache.fullsize[cacheKey];
|
|
}
|
|
|
|
// Otherwise, get the blob from the album art, create a URL for it, and
|
|
// return it in the Promise.
|
|
var promise = getAlbumArtBlob(fileinfo).then((blob) => {
|
|
return URL.createObjectURL(blob);
|
|
});
|
|
|
|
if (cacheKey) {
|
|
sessionCache.fullsize[cacheKey] = promise;
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Get a Blob for a full-sized version of the cover art for a given file (if
|
|
* any).
|
|
*
|
|
* @param {Object} fileinfo The info for the file we want album art for.
|
|
* @param {Boolean} noPlaceholder True if this function should only return an
|
|
* image if there's cover art for the file. Otherwise, this function will
|
|
* return a placeholder image if there's no cover art.
|
|
* @return {Promise} A promise returning either the Blob for the cover art, or
|
|
* null.
|
|
*/
|
|
function getFullSizeBlob(fileinfo, noPlaceholder) {
|
|
// If there's no picture info in the metadata, return null or a placeholder.
|
|
if (!fileinfo.metadata.picture) {
|
|
if (noPlaceholder) {
|
|
return Promise.resolve(null);
|
|
}
|
|
return getBlobFromURL(getDefaultCoverURL(fileinfo, 'fullsize'));
|
|
}
|
|
|
|
// Skip the session cache, since we want to return a blob, not a URL.
|
|
|
|
return getAlbumArtBlob(fileinfo);
|
|
}
|
|
|
|
/**
|
|
* Get a URL for a thumbnailized version of the cover art for a given file
|
|
* (if any).
|
|
*
|
|
* @param {Object} fileinfo The info for the file we want album art for.
|
|
* @param {Boolean} noPlaceholder True if this function should only return an
|
|
* image if there's cover art for the file. Otherwise, this function will
|
|
* return a placeholder image if there's no cover art.
|
|
* @return {Promise} A promise returning either the URL for the thumbnail, or
|
|
* null.
|
|
*/
|
|
function getThumbnailURL(fileinfo, noPlaceholder) {
|
|
// If there's no picture info in the metadata, return null or a placeholder.
|
|
if (!fileinfo.metadata.picture) {
|
|
return Promise.resolve(
|
|
noPlaceholder ? null : getDefaultCoverURL(fileinfo, 'thumbnail')
|
|
);
|
|
}
|
|
|
|
// See if we've already requested a URL for this album. If so, return it.
|
|
var cacheKey = makeCacheKey(fileinfo);
|
|
if (cacheKey && cacheKey in sessionCache.thumbnail) {
|
|
return sessionCache.thumbnail[cacheKey];
|
|
}
|
|
|
|
// Otherwise, get the blob from the thumbnail cache, create a URL for it,
|
|
// and return it in the Promise.
|
|
var promise = thumbnailCache.get(fileinfo, cacheKey).then((blob) => {
|
|
return URL.createObjectURL(blob);
|
|
});
|
|
|
|
if (cacheKey) {
|
|
sessionCache.thumbnail[cacheKey] = promise;
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Get a Blob for a thumbnailized version of the cover art for a given file
|
|
* (if any).
|
|
*
|
|
* @param {Object} fileinfo The info for the file we want album art for.
|
|
* @param {Boolean} noPlaceholder True if this function should only return an
|
|
* image if there's cover art for the file. Otherwise, this function will
|
|
* return a placeholder image if there's no cover art.
|
|
* @return {Promise} A promise returning either the Blob for the thumbnail, or
|
|
* null.
|
|
*/
|
|
function getThumbnailBlob(fileinfo, noPlaceholder) {
|
|
// If there's no picture info in the metadata, return null or a placeholder.
|
|
if (!fileinfo.metadata.picture) {
|
|
if (noPlaceholder) {
|
|
return Promise.resolve(null);
|
|
}
|
|
return getBlobFromURL(getDefaultCoverURL(fileinfo, 'thumbnail'));
|
|
}
|
|
|
|
// Skip the session cache, since we want to return a blob, not a URL.
|
|
|
|
return thumbnailCache.get(fileinfo, makeCacheKey(fileinfo));
|
|
}
|
|
|
|
function getDefaultCoverURL(fileinfo, size) {
|
|
var metadata = fileinfo.metadata;
|
|
// If metadata does not contain both album and artist, then use title
|
|
// instead.
|
|
var infoForHash = (!metadata.album && !metadata.artist) ?
|
|
metadata.title : metadata.album + metadata.artist;
|
|
var hashedNumber = (Math.abs(hash(infoForHash)) % 10) + 1;
|
|
var dir = '/img/artwork/' + (size === 'thumbnail' ? 'thumbnails/' : '');
|
|
|
|
return dir + 'artwork-' + hashedNumber + '.jpg';
|
|
}
|
|
|
|
/**
|
|
* Hash a string into an integral value. (This is a Javascript implementation
|
|
* of Java's String.hashCode() method.
|
|
*
|
|
* @param {String} str The string to hash.
|
|
* @return {Number} The hashed value.
|
|
*/
|
|
function hash(str) {
|
|
var hashCode = 0;
|
|
if (str.length === 0) { return hashCode; }
|
|
for (var i = 0; i < str.length; i++) {
|
|
var c = str.charCodeAt(i);
|
|
hashCode = ((hashCode << 5) - hashCode) + c;
|
|
hashCode = hashCode & hashCode; // Convert to 32-bit integer
|
|
}
|
|
return hashCode;
|
|
}
|
|
|
|
/**
|
|
* Create a cache key for this file. The cache key should be unique for each
|
|
* album, and (hopefully) the same for tracks in a given album.
|
|
*
|
|
* @param {Object} fileinfo The file's info.
|
|
* @return {String} A cache key for the file, or null if we couldn't generate
|
|
* one.
|
|
*/
|
|
function makeCacheKey(fileinfo) {
|
|
var metadata = fileinfo.metadata;
|
|
if (metadata.picture.filename) {
|
|
return 'external.' + metadata.picture.filename;
|
|
} else if (metadata.picture.flavor === 'embedded') {
|
|
var album = metadata.album;
|
|
var artist = metadata.artist;
|
|
var size = metadata.picture.end - metadata.picture.start;
|
|
|
|
if (album || artist) {
|
|
return 'thumbnail.' + album + '.' + artist + '.' + size;
|
|
} else {
|
|
return 'thumbnail.' + (fileinfo.name || fileinfo.blob.name);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the (full-size) Blob for this file's album art.
|
|
*
|
|
* @param {Object} fileinfo The file's info.
|
|
* @return {Promise} A promise returning the full-size Blob.
|
|
*/
|
|
function getAlbumArtBlob(fileinfo) {
|
|
var picture = fileinfo.metadata.picture;
|
|
if (picture.blob) {
|
|
// We must have an unsynced picture that came from a temporary blob
|
|
// (i.e. from the open activity or a unit test).
|
|
return Promise.resolve(picture.blob);
|
|
} else if (picture.filename) {
|
|
// Some audio tracks have an external file for their album art, so we
|
|
// need to grab it from deviceStorage. This could also be an unsynced
|
|
// picture that came from a regular file.
|
|
return LazyLoader.load('/js/metadata/album_art.js').then(() => {
|
|
return new Promise((resolve, reject) => {
|
|
var getreq = AlbumArt.pictureStorage.get(picture.filename);
|
|
getreq.onsuccess = function() {
|
|
resolve(this.result);
|
|
};
|
|
getreq.onerror = function() {
|
|
reject(this.error);
|
|
};
|
|
});
|
|
});
|
|
} else if (picture.start) {
|
|
// Other audio tracks have the album art embedded in the file, so we
|
|
// just need to splice out the part we want.
|
|
return getSongBlob(fileinfo).then((blob) => {
|
|
return blob.slice(picture.start, picture.end, picture.type);
|
|
});
|
|
} else {
|
|
// If we got here, something strange happened...
|
|
var err = new Error('unknown picture flavor: ' + picture.flavor);
|
|
console.error(err);
|
|
return Promise.reject(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the Blob for this file (i.e. the audio track itself).
|
|
*
|
|
* @param {Object} fileinfo The file's info.
|
|
* @return {Promise} A promise returning the audio track's Blob.
|
|
*/
|
|
function getSongBlob(fileinfo) {
|
|
if (fileinfo.blob) {
|
|
// This can happen for the open activity.
|
|
return Promise.resolve(fileinfo.blob);
|
|
} else {
|
|
// This is the normal case.
|
|
return Database.getFile(fileinfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the Blob for a URL.
|
|
*
|
|
* @param {String} url The URL to fetch.
|
|
* @return {Promise} A promise returning the Blob.
|
|
*/
|
|
function getBlobFromURL(url) {
|
|
return new Promise((resolve, reject) => {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url, true);
|
|
xhr.responseType = 'blob';
|
|
|
|
xhr.onload = function() {
|
|
resolve(xhr.response);
|
|
};
|
|
// I don't think onerror usually gets called, but let's play it safe.
|
|
xhr.onerror = function() {
|
|
reject(null);
|
|
};
|
|
|
|
xhr.send();
|
|
});
|
|
}
|
|
|
|
return {
|
|
getFullSizeURL: getFullSizeURL,
|
|
getFullSizeBlob: getFullSizeBlob,
|
|
getThumbnailURL: getThumbnailURL,
|
|
getThumbnailBlob: getThumbnailBlob
|
|
};
|
|
})();
|