Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
550 lines
12 KiB
JavaScript
Executable file
550 lines
12 KiB
JavaScript
Executable file
/* global AlbumArtCache, AudioMetadata, Database, LazyLoader, NFCShare,
|
|
PlaybackQueue, Remote, bridge, navigateToURL, onSearchOpen,
|
|
onSearchClose */
|
|
'use strict';
|
|
|
|
var audio = null;
|
|
var queueSettings = null;
|
|
var remote = null;
|
|
var nfcShare = null;
|
|
var currentFilePath = null;
|
|
var currentQueue = null;
|
|
var isInterrupted = false;
|
|
var isFastSeeking = false;
|
|
var isStopped = true;
|
|
var externalFile = null;
|
|
|
|
var service = bridge.service('music-service')
|
|
.method('play', play)
|
|
.method('pause', pause)
|
|
.method('seek', seek)
|
|
.method('startFastSeek', startFastSeek)
|
|
.method('stopFastSeek', stopFastSeek)
|
|
.method('getPlaybackStatus', getPlaybackStatus)
|
|
|
|
.method('currentSong', currentSong)
|
|
.method('previousSong', previousSong)
|
|
.method('nextSong', nextSong)
|
|
.method('queueAlbum', queueAlbum)
|
|
.method('queueArtist', queueArtist)
|
|
.method('queuePlaylist', queuePlaylist)
|
|
.method('queueSong', queueSong)
|
|
.method('setRepeatSetting', setRepeatSetting)
|
|
.method('setShuffleSetting', setShuffleSetting)
|
|
|
|
.method('getAlbums', getAlbums)
|
|
.method('getAlbum', getAlbum)
|
|
|
|
.method('getArtists', getArtists)
|
|
.method('getArtist', getArtist)
|
|
|
|
.method('getPlaylists', getPlaylists)
|
|
.method('getPlaylist', getPlaylist)
|
|
|
|
.method('getSongs', getSongs)
|
|
.method('getSongCount', getSongCount)
|
|
.method('getSong', getSong)
|
|
.method('getSongFile', getSongFile)
|
|
.method('setSongRating', setSongRating)
|
|
.method('search', search)
|
|
|
|
.method('getSongArtwork', getSongArtwork)
|
|
.method('getSongThumbnail', getSongThumbnail)
|
|
.method('getSongArtworkURL', getSongArtworkURL)
|
|
.method('getSongThumbnailURL', getSongThumbnailURL)
|
|
|
|
.method('share', share)
|
|
.method('openExternalFile', openExternalFile)
|
|
.method('enableNFC', enableNFC)
|
|
|
|
.method('getDatabaseStatus', getDatabaseStatus)
|
|
|
|
.method('navigate', navigate)
|
|
.method('searchOpen', searchOpen)
|
|
.method('searchClose', searchClose)
|
|
|
|
.listen()
|
|
.listen(new BroadcastChannel('music-service'));
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
audio = document.getElementById('audio');
|
|
|
|
audio.addEventListener('loadeddata', function() {
|
|
URL.revokeObjectURL(audio.src);
|
|
});
|
|
|
|
audio.addEventListener('play', function() {
|
|
service.broadcast('play');
|
|
});
|
|
|
|
audio.addEventListener('pause', function() {
|
|
service.broadcast('pause');
|
|
});
|
|
|
|
audio.addEventListener('durationchange', function() {
|
|
service.broadcast('durationChange', audio.duration);
|
|
});
|
|
|
|
audio.addEventListener('timeupdate', function() {
|
|
service.broadcast('elapsedTimeChange', audio.currentTime);
|
|
});
|
|
|
|
audio.addEventListener('ended', function() {
|
|
nextSong(true);
|
|
});
|
|
|
|
audio.addEventListener('mozinterruptbegin', function() {
|
|
isInterrupted = true;
|
|
|
|
service.broadcast('interruptBegin');
|
|
});
|
|
|
|
audio.addEventListener('mozinterruptend', function() {
|
|
isInterrupted = false;
|
|
|
|
service.broadcast('interruptEnd');
|
|
});
|
|
});
|
|
|
|
function play(filePath) {
|
|
loadRemote();
|
|
|
|
if (!filePath) {
|
|
audio.play();
|
|
return;
|
|
}
|
|
|
|
getSongFile(filePath).then((file) => {
|
|
if (isStopped) {
|
|
return;
|
|
}
|
|
|
|
currentFilePath = filePath;
|
|
|
|
audio.src = null;
|
|
audio.load();
|
|
|
|
audio.mozAudioChannelType = 'content';
|
|
audio.src = URL.createObjectURL(file);
|
|
audio.load();
|
|
audio.play();
|
|
|
|
getSong(filePath).then((song) => {
|
|
Database.incrementPlayCount(song);
|
|
});
|
|
|
|
service.broadcast('songChange');
|
|
});
|
|
}
|
|
|
|
function pause() {
|
|
audio.pause();
|
|
}
|
|
|
|
function stop() {
|
|
isStopped = true;
|
|
|
|
audio.pause();
|
|
|
|
currentFilePath = null;
|
|
currentQueue = null;
|
|
|
|
audio.src = null;
|
|
audio.load();
|
|
|
|
service.broadcast('stop');
|
|
}
|
|
|
|
var seekTimeout;
|
|
|
|
function seek(time) {
|
|
if (audio.seeking) {
|
|
clearTimeout(seekTimeout);
|
|
seekTimeout = setTimeout(() => seek(time), 50);
|
|
return;
|
|
}
|
|
|
|
audio.fastSeek(parseInt(time, 10));
|
|
}
|
|
|
|
function startFastSeek(reverse) {
|
|
if (isFastSeeking) {
|
|
return;
|
|
}
|
|
|
|
reverse = reverse === true || reverse === 'reverse';
|
|
|
|
isFastSeeking = true;
|
|
|
|
function fastSeek() {
|
|
if (!isFastSeeking) {
|
|
audio.volume = 1;
|
|
return;
|
|
}
|
|
|
|
audio.volume = 0.5;
|
|
seek(audio.currentTime + (reverse ? -5 : 5));
|
|
setTimeout(fastSeek, 100);
|
|
}
|
|
|
|
fastSeek();
|
|
}
|
|
|
|
function stopFastSeek() {
|
|
isFastSeeking = false;
|
|
}
|
|
|
|
function getPlaybackStatus() {
|
|
return loadQueueSettings().then(() => {
|
|
return {
|
|
queueIndex: currentQueue ? currentQueue.index : -1,
|
|
queueRawIndex: currentQueue ? currentQueue.rawIndex : -1,
|
|
queueLength: currentQueue ? currentQueue.length : -1,
|
|
repeat: PlaybackQueue.repeat,
|
|
shuffle: PlaybackQueue.shuffle ? 1 : 0,
|
|
filePath: currentFilePath,
|
|
stopped: isStopped,
|
|
paused: audio.paused,
|
|
duration: audio.duration,
|
|
elapsedTime: audio.currentTime,
|
|
isInterrupted: isInterrupted,
|
|
isFastSeeking: isFastSeeking
|
|
};
|
|
});
|
|
}
|
|
|
|
function currentSong() {
|
|
if (!currentQueue) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
return currentQueue.current();
|
|
}
|
|
|
|
function previousSong() {
|
|
if (!currentQueue) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
currentQueue.previous();
|
|
|
|
return currentSong().then(song => play(song.name));
|
|
}
|
|
|
|
function nextSong(automatic = false) {
|
|
if (!currentQueue) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
var hasNextSong = currentQueue.next(automatic);
|
|
if (!hasNextSong && !externalFile) {
|
|
return Promise.resolve(stop());
|
|
}
|
|
|
|
return currentSong().then(song => play(song.name));
|
|
}
|
|
|
|
function loadQueueSettings() {
|
|
if (!queueSettings) {
|
|
queueSettings = LazyLoader.load('/js/queue.js').then(() => {
|
|
return PlaybackQueue.loadSettings();
|
|
});
|
|
}
|
|
|
|
return queueSettings;
|
|
}
|
|
|
|
function loadRemote() {
|
|
if (!remote) {
|
|
remote = LazyLoader.load('/js/remote.js').then(() => {
|
|
return Remote;
|
|
});
|
|
}
|
|
|
|
return remote;
|
|
}
|
|
|
|
function loadNFCShare() {
|
|
if (!nfcShare) {
|
|
nfcShare = LazyLoader.load('/js/nfc_share.js').then(() => {
|
|
return NFCShare;
|
|
});
|
|
}
|
|
|
|
return nfcShare;
|
|
}
|
|
|
|
function queueArtist(filePath) {
|
|
return loadQueueSettings().then(() => {
|
|
return getArtist(filePath).then((songs) => {
|
|
var index = songs.findIndex(song => song.name === filePath);
|
|
currentQueue = new PlaybackQueue.StaticQueue(songs, index);
|
|
|
|
return currentSong().then((song) => {
|
|
isStopped = false;
|
|
play(song.name);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function queueAlbum(filePath) {
|
|
return loadQueueSettings().then(() => {
|
|
return getAlbum(filePath).then((songs) => {
|
|
var index = songs.findIndex(song => song.name === filePath);
|
|
currentQueue = new PlaybackQueue.StaticQueue(songs, index);
|
|
|
|
return currentSong().then((song) => {
|
|
isStopped = false;
|
|
play(song.name);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function queuePlaylist(id, filePath) {
|
|
return loadQueueSettings().then(() => {
|
|
return getPlaylist(id).then((songs) => {
|
|
var playlist = Database.playlists.find(playlist => playlist.id === id);
|
|
return setShuffleSetting(playlist.shuffle).then(() => {
|
|
var index = filePath ?
|
|
songs.findIndex(song => song.name === filePath) :
|
|
(playlist.shuffle ? Math.floor(Math.random() * songs.length) : 0);
|
|
currentQueue = new PlaybackQueue.StaticQueue(songs, index);
|
|
|
|
return currentSong().then((song) => {
|
|
isStopped = false;
|
|
play(song.name);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function queueSong(filePath) {
|
|
return loadQueueSettings().then(() => {
|
|
return getSongs().then((songs) => {
|
|
var index = songs.findIndex(song => song.name === filePath);
|
|
currentQueue = new PlaybackQueue.StaticQueue(songs, index);
|
|
|
|
return currentSong().then((song) => {
|
|
isStopped = false;
|
|
play(song.name);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function setRepeatSetting(repeat) {
|
|
repeat = parseInt(repeat, 10) || 0;
|
|
return loadQueueSettings().then(() => PlaybackQueue.repeat = repeat);
|
|
}
|
|
|
|
function setShuffleSetting(shuffle) {
|
|
if (typeof shuffle !== 'boolean') {
|
|
shuffle = shuffle !== 'false' && parseInt(shuffle || 0, 10) !== 0;
|
|
}
|
|
|
|
return loadQueueSettings().then(() => PlaybackQueue.shuffle = shuffle);
|
|
}
|
|
|
|
function getPlaylists() {
|
|
return Promise.resolve(Database.playlists);
|
|
}
|
|
|
|
function getPlaylist(id) {
|
|
var playlist = Database.playlists.find(playlist => playlist.id === id);
|
|
|
|
return new Promise((resolve) => {
|
|
Database.enumerateAll(playlist.index, null, playlist.direction, (songs) => {
|
|
resolve(songs);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getArtists() {
|
|
return Database.artists();
|
|
}
|
|
|
|
function getAlbums() {
|
|
return Database.albums();
|
|
}
|
|
|
|
function getSongs() {
|
|
return Database.songs();
|
|
}
|
|
|
|
function getSongCount() {
|
|
return Database.totalCount();
|
|
}
|
|
|
|
function getArtist(filePath) {
|
|
return getSong(filePath).then((song) => {
|
|
return Database.artist(song.metadata.artist);
|
|
});
|
|
}
|
|
|
|
function getAlbum(filePath) {
|
|
return getSong(filePath).then((song) => {
|
|
return Database.album(song.metadata.album);
|
|
});
|
|
}
|
|
|
|
function getSong(filePath) {
|
|
if (externalFile && filePath === externalFile.name) {
|
|
return Promise.resolve(externalFile);
|
|
}
|
|
|
|
return Database.getFileInfo(filePath);
|
|
}
|
|
|
|
function getSongFile(filePath) {
|
|
if (externalFile && filePath === externalFile.name) {
|
|
return Promise.resolve(externalFile.file);
|
|
}
|
|
|
|
return getSong(filePath).then((song) => {
|
|
return Database.getFile(song);
|
|
});
|
|
}
|
|
|
|
function getSongArtwork(filePath) {
|
|
return LazyLoader.load('/js/metadata/album_art_cache.js').then(() => {
|
|
return getSong(filePath).then((song) => {
|
|
return AlbumArtCache.getFullSizeBlob(song);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getSongThumbnail(filePath) {
|
|
return LazyLoader.load('/js/metadata/album_art_cache.js').then(() => {
|
|
return getSong(filePath).then((song) => {
|
|
return AlbumArtCache.getThumbnailBlob(song);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getSongArtworkURL(filePath) {
|
|
return LazyLoader.load('/js/metadata/album_art_cache.js').then(() => {
|
|
return getSong(filePath).then((song) => {
|
|
return AlbumArtCache.getFullSizeURL(song);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getSongThumbnailURL(filePath) {
|
|
return LazyLoader.load('/js/metadata/album_art_cache.js').then(() => {
|
|
return getSong(filePath).then((song) => {
|
|
return AlbumArtCache.getThumbnailURL(song);
|
|
});
|
|
});
|
|
}
|
|
|
|
function setSongRating(rating, filePath) {
|
|
rating = parseInt(rating, 10) || 0;
|
|
return getSong(filePath).then((song) => {
|
|
return Database.setSongRating(song, rating);
|
|
});
|
|
}
|
|
|
|
function search(key, query) {
|
|
return new Promise((resolve) => {
|
|
var results = [];
|
|
|
|
Database.search(key, query, (result) => {
|
|
if (result === null) {
|
|
resolve(results);
|
|
} else {
|
|
results.push(result);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function getDatabaseStatus() {
|
|
return Promise.resolve(Database.status);
|
|
}
|
|
|
|
function navigate(url) {
|
|
navigateToURL(url);
|
|
}
|
|
|
|
function searchOpen() {
|
|
onSearchOpen();
|
|
}
|
|
|
|
function searchClose() {
|
|
onSearchClose();
|
|
}
|
|
|
|
function share(filePath) {
|
|
return getSong(filePath).then((song) => {
|
|
if (song.metadata.locked || !window.MozActivity) {
|
|
return;
|
|
}
|
|
|
|
return Promise.all([
|
|
getSongFile(filePath),
|
|
getSongThumbnail(filePath)
|
|
]).then(([file, thumbnail]) => {
|
|
var path = song.name;
|
|
var filename = path.substring(path.lastIndexOf('/') + 1);
|
|
|
|
return new window.MozActivity({
|
|
name: 'share',
|
|
data: {
|
|
type: 'audio/*',
|
|
number: 1,
|
|
blobs: [file],
|
|
filenames: [filename],
|
|
filepaths: [path],
|
|
metadata: [{
|
|
title: song.metadata.title,
|
|
artist: song.metadata.artist,
|
|
album: song.metadata.album,
|
|
picture: thumbnail
|
|
}]
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function openExternalFile(file, filename = null) {
|
|
var scripts = [
|
|
'/js/metadata/metadata_scripts.js',
|
|
'/js/metadata/album_art.js'
|
|
];
|
|
|
|
return loadQueueSettings().then(() => {
|
|
return LazyLoader.load(scripts);
|
|
}).then(() => {
|
|
return AudioMetadata.parse(file, filename);
|
|
}).then((metadata) => {
|
|
externalFile = {
|
|
file: file,
|
|
name: file.name || URL.createObjectURL(file),
|
|
metadata: metadata
|
|
};
|
|
|
|
currentQueue = new PlaybackQueue.StaticQueue([externalFile]);
|
|
|
|
return currentSong();
|
|
}).then((song) => {
|
|
isStopped = false;
|
|
play(song.name);
|
|
});
|
|
}
|
|
|
|
function enableNFC(enabled) {
|
|
// This function is a no-op until we enable NFC once.
|
|
if (!nfcShare && !enabled) {
|
|
return;
|
|
}
|
|
|
|
loadNFCShare().then(() => {
|
|
NFCShare.enabled = enabled;
|
|
});
|
|
}
|
|
|
|
Database.init();
|