factored out music library index. and more...

* use keese
 * relative file path is no longer the db key
This commit is contained in:
Andrew Kelley 2013-10-04 00:46:05 -04:00
parent 983937132e
commit 1349bf6f91
13 changed files with 646 additions and 878 deletions

View file

@ -9,6 +9,8 @@ var chokidar = require('chokidar');
var shuffle = require('mess');
var mv = require('mv');
var zfill = require('zfill');
var MusicLibraryIndex = require('music-library-index');
var keese = require('keese');
var safePath = require('./safe_path');
module.exports = Player;
@ -37,7 +39,8 @@ function Player(db, musicDirectory) {
EventEmitter.call(this);
this.db = db;
this.musicDirectory = musicDirectory;
this.allDbFiles = {};
this.dbFilesByPath = {};
this.libraryIndex = new MusicLibraryIndex();
this.addQueue = new Pend();
this.addQueue.max = 10;
@ -123,13 +126,18 @@ Player.prototype.initialize = function(cb) {
function onFileMissing(fullPath) {
var relPath = path.relative(self.musicDirectory, fullPath);
self.delDbEntry(relPath);
var dbFile = self.dbFilesByPath[relPath];
if (!dbFile) {
console.warn("File reported deleted that was not in our db:", fullPath);
return;
}
self.delDbEntry(dbFile);
}
function onAddOrChange(fullPath, stat) {
// check the mtime against the mtime of the same file in the db
var relPath = path.relative(self.musicDirectory, fullPath);
var dbFile = self.allDbFiles[relPath];
var dbFile = self.dbFilesByPath[relPath];
var fileMtime = stat.mtime.getTime();
if (dbFile) {
@ -154,8 +162,9 @@ Player.prototype.initialize = function(cb) {
cb();
return;
}
var file = data.key.substring(LIBRARY_KEY_PREFIX.length);
self.allDbFiles[file] = deserializeFileData(data.value);
var dbFile = deserializeFileData(data.value);
self.libraryIndex.addTrack(dbFile);
self.dbFilesByPath[dbFile.file] = dbFile;
});
stream.on('error', function(err) {
stream.removeAllListeners();
@ -168,24 +177,30 @@ Player.prototype.initialize = function(cb) {
}
};
Player.prototype.deleteFile = function(relPath) {
Player.prototype.deleteFile = function(key) {
var self = this;
var fullPath = path.join(self.musicDirectory, relPath);
var dbFile = self.libraryIndex.trackTable[key];
if (!dbFile) {
console.error("Error deleting file - no entry:", key);
return;
}
var fullPath = path.join(self.musicDirectory, dbFile.file);
fs.unlink(fullPath, function(err) {
if (err) {
console.error("Error deleting", relPath, err.stack);
console.error("Error deleting", dbFile.file, err.stack);
}
});
self.delDbEntry(relPath);
self.delDbEntry(dbFile);
};
Player.prototype.delDbEntry = function(relPath) {
Player.prototype.delDbEntry = function(dbFile) {
var self = this;
delete self.allDbFiles[relPath];
self.emit('delete', relPath);
self.db.del(relPath, function(err) {
self.libraryIndex.removeTrack(dbFile.key);
delete self.dbFilesByPath[dbFile.file];
self.emit('delete', dbFile);
self.db.del(LIBRARY_KEY_PREFIX + dbFile.key, function(err) {
if (err) {
console.error("Error deleting db entry", relPath, err.stack);
console.error("Error deleting db entry", dbFile.key, err.stack);
}
});
};
@ -211,7 +226,7 @@ Player.prototype.importFile = function(fullPath, filenameHint, cb) {
groove.open(fullPath, function(err, file) {
if (err) return cb(err);
var newDbFile = grooveFileToDbFile(file, filenameHint);
var suggestedPath = getSuggestedPath(newDbFile, filenameHint);
var suggestedPath = self.getSuggestedPath(newDbFile, filenameHint);
pend.go(testSuggestedPath);
pend.go(function(cb) {
file.close(cb);
@ -248,10 +263,11 @@ Player.prototype.importFile = function(fullPath, filenameHint, cb) {
Player.prototype.persist = function(dbFile, cb) {
cb = cb || logIfError;
var prevDbFile = this.allDbFiles[dbFile.file];
this.allDbFiles[dbFile.file] = dbFile;
var prevDbFile = this.libraryIndex.trackTable[dbFile.key];
this.libraryIndex.addTrack(dbFile);
this.dbFilesByPath[dbFile.file] = dbFile;
this.emit('update', prevDbFile, dbFile);
this.db.put(LIBRARY_KEY_PREFIX + dbFile.file, serializeFileData(dbFile), cb);
this.db.put(LIBRARY_KEY_PREFIX + dbFile.key, serializeFileData(dbFile), cb);
function logIfError(err) {
if (err) {
@ -290,21 +306,23 @@ Player.prototype.queueAddToLibrary = function(relPath, mtime) {
});
};
Player.prototype.appendFiles = function(files, tagAsRandom) {
Player.prototype.appendTracks = function(keys, tagAsRandom) {
var lastTrack = this.tracksInOrder[this.tracksInOrder.length - 1];
var items = {};
var nextSortKey = lastTrack ? lastTrack.sort_key + 1 : 0;
files.forEach(function(file) {
var lastSortKey = lastTrack ? lastTrack.sortKey : keese();
keys.forEach(function(key) {
var id = uuid();
var nextSortKey = keese(lastSortKey, null);
lastSortKey = nextSortKey;
items[id] = {
file: file,
sort_key: nextSortKey++,
key: key,
sortKey: nextSortKey,
};
});
this.addItems(items, tagAsRandom);
};
// items looks like {id: {file, sort_key}}
// items looks like {id: {key, sortKey}}
Player.prototype.addItems = function(items, tagAsRandom) {
var self = this;
tagAsRandom = !!tagAsRandom;
@ -312,10 +330,9 @@ Player.prototype.addItems = function(items, tagAsRandom) {
var item = items[id];
var playlistItem = {
id: id,
file: item.file,
sort_key: item.sort_key,
is_random: tagAsRandom,
time: self.allDbFiles[item.file].time,
key: item.key,
sortKey: item.sortKey,
isRandom: tagAsRandom,
grooveFile: null,
pendingGrooveFile: false,
deleted: false,
@ -340,10 +357,12 @@ Player.prototype.clearPlaylist = function() {
Player.prototype.shufflePlaylist = function() {
shuffle(this.tracksInOrder);
// fix sort_key and index properties
// fix sortKey and index properties
var nextSortKey = keese(null, null);
this.tracksInOrder.forEach(function(track, index) {
track.index = index;
track.sort_key = index;
track.sortKey = nextSortKey;
nextSortKey = keese(nextSortKey, null);
});
playlistChanged(this);
}
@ -355,10 +374,10 @@ Player.prototype.removePlaylistItems = function(ids) {
playlistChanged(this);
}
// items looks like {id: {sort_key}}
// items looks like {id: {sortKey}}
Player.prototype.movePlaylistItems = function(items) {
for (var id in items) {
this.playlist[id].sort_key = items[id].sort_key;
this.playlist[id].sortKey = items[id].sortKey;
}
playlistChanged(this);
}
@ -414,38 +433,40 @@ Player.prototype.stop = function() {
playlistChanged(this);
}
function operatorCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
Player.prototype.getSuggestedPath = function(track, filenameHint) {
var p = "";
if (track.albumArtistName) {
p = path.join(p, safePath(track.albumArtistName));
} else if (track.compilation) {
p = path.join(p, safePath(this.libraryIndex.variousArtistsName));
}
if (track.albumName) {
p = path.join(p, safePath(track.albumName));
}
var t = "";
if (track.track != null) {
t += safePath(zfill(track.track, 2)) + " ";
}
t += safePath(track.name + path.extname(filenameHint));
return path.join(p, t);
}
// TODO: use keese
function generateSortKey(previous_key, next_key){
if (previous_key != null) {
if (next_key != null) {
return (previous_key + next_key) / 2;
} else {
return 0 | previous_key + 1;
}
} else {
if (next_key != null) {
return (0 + next_key) / 2;
} else {
return 1;
}
}
function operatorCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
function disambiguateSortKeys(self) {
var previousUniqueKey = null;
var previousKey = null;
self.tracksInOrder.forEach(function(track, i) {
if (track.sort_key === previousKey) {
if (track.sortKey === previousKey) {
// move the repeat back
track.sort_key = generateSortKey(previousUniqueKey, track.sort_key);
previousUniqueKey = track.sort_key;
track.sortKey = keese(previousUniqueKey, track.sortKey);
previousUniqueKey = track.sortKey;
} else {
previousUniqueKey = previousKey;
previousKey = track.sort_key;
previousKey = track.sortKey;
}
});
}
@ -459,7 +480,7 @@ function cacheTracksArray(self) {
});
function asc(a, b) {
return operatorCompare(a.sort_key, b.sort_key);
return operatorCompare(a.sortKey, b.sortKey);
}
function trackById(id) {
return self.playlist[id];
@ -494,12 +515,13 @@ function playlistChanged(self) {
}
function preloadFile(self, track) {
var fullPath = path.join(self.musicDirectory, track.file);
var relPath = self.libraryIndex.trackTable[track.key].file;
var fullPath = path.join(self.musicDirectory, relPath);
track.pendingGrooveFile = true;
groove.open(fullPath, function(err, file) {
track.pendingGrooveFile = false;
if (err) {
console.error("Error opening", track.file, err.stack);
console.error("Error opening", relPath, err.stack);
return;
}
if (track.deleted) {
@ -627,10 +649,6 @@ function shouldSuppressWarning(filename) {
return IGNORE_OK_EXTS.indexOf(ext) !== -1;
}
function getFile(item) {
return item.file;
}
function closeFile(file) {
file.close(function(err) {
if (err) {
@ -639,38 +657,43 @@ function closeFile(file) {
});
}
function parseIntOrNull(n){
function parseTrackString(trackStr) {
if (!trackStr) return {};
var parts = trackStr.split('/');
if (parts.length > 1) {
return {
track: parseIntOrNull(parts[0]),
trackCount: parseIntOrNull(parts[1]),
};
}
return {
track: parseIntOrNull(parts[0]),
};
}
function parseIntOrNull(n) {
n = parseInt(n, 10);
if (isNaN(n)) n = null;
if (isNaN(n)) return null;
return n;
}
function getSuggestedPath(track, filenameHint) {
var p = "";
if (track.album_artist_name) {
p = path.join(p, safePath(track.album_artist_name));
}
if (track.album_name) {
p = path.join(p, safePath(track.album_name));
}
var t = "";
if (track.track != null) {
t += safePath(zfill(track.track, 2)) + " ";
}
t += safePath(track.name + path.extname(filenameHint));
return path.join(p, t);
}
function grooveFileToDbFile(file, filenameHint) {
var parsedTrack = parseTrackString(file.getMetadata("track"));
var parsedDisc = parseTrackString(file.getMetadata("disc"));
return {
key: uuid(),
name: file.getMetadata("title") || trackNameFromFile(filenameHint),
artist_name: (file.getMetadata("artist") || "").trim(),
artist_disambiguation: "",
album_artist_name: (file.getMetadata("album_artist") || "").trim(),
album_name: (file.getMetadata("album") || "").trim(),
track: parseIntOrNull(file.getMetadata("track")),
time: file.duration(),
artistName: (file.getMetadata("artist") || "").trim(),
albumArtistName: (file.getMetadata("album_artist") || "").trim(),
albumName: (file.getMetadata("album") || "").trim(),
compilation: !!parseInt(file.getMetadata("TCP"), 10),
track: parsedTrack.track,
trackCount: parsedTrack.trackCount,
disc: parsedDisc.disc,
discCount: parsedDisc.discCount,
duration: file.duration(),
year: parseInt(file.getMetadata("date") || "0", 10),
genre: file.getMetadata("genre"),
};
}

View file

@ -33,7 +33,7 @@ var actions = {
'listallinfo': {
permission: 'read',
fn: function(client, msg, cb) {
cb({msg: this.player.allDbFiles});
cb({msg: this.player.libraryIndex.trackTable});
},
},
'move': {
@ -67,7 +67,7 @@ var actions = {
'playid': {
permission: 'control',
fn: function(client, msg, cb) {
this.player.playId(msg.track_id);
this.player.playId(msg.trackId);
cb({});
},
},
@ -112,8 +112,8 @@ var actions = {
volume: this.player.volume,
repeat: this.player.repeat,
state: this.player.isPlaying ? 'play' : 'pause',
track_start_date: this.player.trackStartDate,
paused_time: this.player.pausedTime,
trackStartDate: this.player.trackStartDate,
pausedTime: this.player.pausedTime,
}});
},
},
@ -138,7 +138,7 @@ PlayerServer.prototype.createClient = function(socket, permissions) {
socket.on('request', function(request){
request = JSON.parse(request);
self.request(client, request.cmd, function(arg){
var response = {callback_id: request.callback_id};
var response = {callbackId: request.callbackId};
response.err = arg.err;
response.msg = arg.msg;
socket.emit('PlayerResponse', JSON.stringify(response));
@ -202,10 +202,9 @@ function serializePlaylist(playlist) {
for (var id in playlist) {
var item = playlist[id];
o[id] = {
file: item.file,
sort_key: item.sort_key,
is_random: item.is_random,
time: item.time,
key: item.key,
sortKey: item.sortKey,
isRandom: item.isRandom,
};
}
return o;

View file

@ -10,14 +10,13 @@ function Delete(gb) {
function onSocketConnection(client) {
var self = this;
client.on('DeleteFromLibrary', function(data) {
client.on('DeleteFromLibrary', function(keys) {
if (!client.permissions.admin) {
console.warn("User without admin permission trying to delete songs");
return;
}
var files = JSON.parse(data);
files.forEach(function(file) {
self.gb.player.deleteFile(file);
keys.forEach(function(key) {
self.gb.player.deleteFile(key);
});
});
}

View file

@ -26,11 +26,14 @@ function setup(self) {
self.downloadPath(dlPath, zipName, req, resp);
});
app.post('/download/custom', express.urlencoded(), function(req, resp) {
var reqFiles = req.body.file;
if (!Array.isArray(reqFiles)) {
reqFiles = [reqFiles];
var reqKeys = req.body.key;
if (!Array.isArray(reqKeys)) {
reqKeys = [reqKeys];
}
var files = reqFiles.map(function(f) { return path.join(musicDir, f); });
var files = reqKeys.map(function(key) {
var dbFile = self.gb.player.libraryIndex.trackTable[key];
return dbFile && path.join(musicDir, dbFile.file);
});
var reqZipName = req.body.zipName || "music";
var zipName = safePath(reqZipName.toString()) + ".zip";
self.sendZipOfFiles(zipName, files, req, resp);

View file

@ -76,7 +76,7 @@ DynamicMode.prototype.checkDynamicMode = function() {
allIds[track.id] = true;
if (self.previousIds[track.id] == null) {
// tag any newly queued tracks
var dbFile = player.allDbFiles[track.file];
var dbFile = player.libraryIndex.trackTable[track.key];
dbFile.lastQueueDate = now;
player.persist(dbFile);
}
@ -95,19 +95,19 @@ DynamicMode.prototype.checkDynamicMode = function() {
didAnything = true;
}
var files = self.getRandomSongFiles(addCount);
if (files.length > 0) didAnything = true;
if (didAnything) player.appendFiles(files, true);
var keys = self.getRandomSongKeys(addCount);
if (keys.length > 0) didAnything = true;
if (didAnything) player.appendTracks(keys, true);
};
DynamicMode.prototype.getRandomSongFiles = function(count) {
DynamicMode.prototype.getRandomSongKeys = function(count) {
if (count === 0) return [];
var player = this.gb.player;
var neverQueued = [];
var sometimesQueued = [];
for (var key in player.allDbFiles) {
var dbFile = player.allDbFiles[key];
for (var key in player.libraryIndex.trackTable) {
var dbFile = player.libraryIndex.trackTable[key];
if (dbFile.lastQueueDate == null) {
neverQueued.push(dbFile);
} else {
@ -130,17 +130,17 @@ DynamicMode.prototype.getRandomSongFiles = function(count) {
var totalSize = triangleArea + rectangleArea;
if (totalSize === 0) return [];
// decode indexes through the distribution shape
var files = [];
var keys = [];
for (var i = 0; i < count; i += 1) {
var index = Math.random() * totalSize;
if (index < triangleArea) {
// triangle
files.push(sometimesQueued[Math.floor(Math.sqrt(index))].file);
keys.push(sometimesQueued[Math.floor(Math.sqrt(index))].key);
} else {
files.push(neverQueued[Math.floor((index - triangleArea) / maxWeight)].file);
keys.push(neverQueued[Math.floor((index - triangleArea) / maxWeight)].key);
}
}
return files;
return keys;
};
function broadcastUpdate(self, socket) {

View file

@ -36,7 +36,9 @@
"findit": "~1.1.0",
"archiver": "~0.4.9",
"temp": "~0.6.0",
"uuid": "~1.4.1"
"uuid": "~1.4.1",
"music-library-index": "0.0.1",
"keese": "~1.0.0"
},
"devDependencies": {
"handlebars": "1.0.7",

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,8 @@ var removeDiacritics = require('diacritics').remove;
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var uuid = require('uuid');
var MusicLibraryIndex = require('music-library-index');
var keese = require('keese');
module.exports = PlayerClient;
@ -16,11 +18,7 @@ module.exports = PlayerClient;
*
* */
var PREFIXES_TO_STRIP = [/^\s*the\s+/, /^\s*a\s+/, /^\s*an\s+/];
var VARIOUS_ARTISTS_KEY = "VariousArtists";
var VARIOUS_ARTISTS_NAME = "Various Artists";
var compareSortKeyAndId = makeCompareProps(['sort_key', 'id']);
var compareSortKeyAndId = makeCompareProps(['sortKey', 'id']);
PlayerClient.REPEAT_OFF = 0;
PlayerClient.REPEAT_ALL = 1;
@ -55,66 +53,62 @@ function PlayerClient(socket) {
}
PlayerClient.prototype.handleConnectionStart = function(){
var this$ = this;
var self = this;
this.updateLibrary(function(){
this$.updateStatus();
this$.updatePlaylist();
self.updateStatus();
self.updatePlaylist();
});
};
PlayerClient.prototype.updateLibrary = function(callback){
var this$ = this;
var self = this;
callback = callback || noop;
this.sendCommandName('listallinfo', function(err, track_table){
var res$, _, track, tracks, last_query;
if (err) {
return callback(err);
this.sendCommandName('listallinfo', function(err, trackTable){
if (err) return callback(err);
self.library.clear();
for (var key in trackTable) {
var track = trackTable[key];
self.library.addTrack(track);
}
res$ = [];
for (_ in track_table) {
track = track_table[_];
res$.push(track);
}
tracks = res$;
addSearchTags(tracks);
this$.buildArtistAlbumTree(tracks, this$.library);
this$.have_file_list_cache = true;
last_query = this$.last_query;
this$.last_query = "";
this$.search(last_query);
self.library.rebuild();
self.haveFileListCache = true;
var lastQuery = self.lastQuery;
self.lastQuery = null;
self.search(lastQuery);
callback();
});
};
PlayerClient.prototype.updatePlaylist = function(callback){
var this$ = this;
var self = this;
callback = callback || noop;
this.sendCommandName('playlistinfo', function(err, tracks){
if (err) return callback(err);
this$.clearPlaylist();
self.clearPlaylist();
for (var id in tracks) {
var item = tracks[id];
var track = this$.library.track_table[item.file];
track.time = item.time;
this$.playlist.item_table[id] = {
var track = self.library.trackTable[item.key];
self.playlist.itemTable[id] = {
id: id,
sort_key: item.sort_key,
is_random: item.is_random,
sortKey: item.sortKey,
isRandom: item.isRandom,
track: track,
playlist: this$.playlist,
playlist: self.playlist,
};
}
this$.refreshPlaylistList();
if (this$.status.current_item != null) {
this$.status.current_item = this$.playlist.item_table[this$.status.current_item.id];
self.refreshPlaylistList();
if (self.currentItem != null) {
self.currentItem = self.playlist.itemTable[self.currentItem.id];
}
if (this$.status.current_item != null) {
this$.emit('playlistupdate');
if (self.currentItem != null) {
self.emit('playlistupdate');
callback();
} else {
this$.updateStatus(function(err){
self.updateStatus(function(err){
if (err) {
return callback(err);
}
this$.emit('playlistupdate');
self.emit('playlistupdate');
callback();
});
}
@ -122,168 +116,121 @@ PlayerClient.prototype.updatePlaylist = function(callback){
};
PlayerClient.prototype.updateStatus = function(callback){
var this$ = this;
var self = this;
callback = callback || noop;
this.sendCommandName('status', function(err, o){
var ref$;
if (err) {
return callback(err);
}
ref$ = this$.status;
ref$.volume = o.volume;
ref$.repeat = o.repeat;
ref$.state = o.state;
this$.status.track_start_date = o.track_start_date != null ? new Date(o.track_start_date) : null;
this$.status.paused_time = o.paused_time;
if (err) return callback(err);
self.volume = o.volume;
self.repeat = o.repeat;
self.state = o.state;
self.trackStartDate = o.trackStartDate != null ? new Date(o.trackStartDate) : null;
self.pausedTime = o.pausedTime;
});
this.sendCommandName('currentsong', function(err, id){
if (err) return callback(err);
if (id != null) {
this$.status.current_item = this$.playlist.item_table[id];
if (this$.status.current_item != null) {
this$.status.time = this$.status.current_item.track.time;
this$.emit('statusupdate');
self.currentItem = self.playlist.itemTable[id];
if (self.currentItem != null) {
self.duration = self.currentItem.track.duration;
self.emit('statusupdate');
callback();
} else {
this$.status.current_item = null;
this$.updatePlaylist(function(err){
self.currentItem = null;
self.updatePlaylist(function(err){
if (err) {
return callback(err);
}
this$.emit('statusupdate');
self.emit('statusupdate');
callback();
});
}
} else {
this$.status.current_item = null;
self.currentItem = null;
callback();
this$.emit('statusupdate');
self.emit('statusupdate');
}
});
};
PlayerClient.prototype.search = function(query){
PlayerClient.prototype.search = function(query) {
query = query.trim();
if (query.length === 0) {
this.search_results = this.library;
this.emit('libraryupdate');
this.last_query = query;
return;
}
var words = formatSearchable(query).split(/\s+/);
var words = query.split(/\s+/);
query = words.join(" ");
if (query === this.last_query) {
return;
}
this.last_query = query;
var result = [];
for (var k in this.library.track_table) {
var track = this.library.track_table[k];
var is_match = fn$();
if (is_match) {
result.push(track);
}
}
this.buildArtistAlbumTree(result, this.search_results = {});
if (query === this.lastQuery) return;
this.lastQuery = query;
this.searchResults = this.library.search(query);
this.emit('libraryupdate');
function fn$(){
var i, ref$, len$, word;
for (i = 0, len$ = (ref$ = words).length; i < len$; ++i) {
word = ref$[i];
if (track.search_tags.indexOf(word) === -1) {
return false;
}
}
return true;
}
};
PlayerClient.prototype.getDefaultQueuePosition = function(){
var ref$, previous_key, next_key, start_pos, i, to$, track, sort_key;
previous_key = (ref$ = this.status.current_item) != null ? ref$.sort_key : void 8;
next_key = null;
start_pos = ((ref$ = (ref$ = this.status.current_item) != null ? ref$.pos : void 8) != null ? ref$ : -1) + 1;
for (i = start_pos, to$ = this.playlist.item_list.length; i < to$; ++i) {
track = this.playlist.item_list[i];
sort_key = track.sort_key;
if (track.is_random) {
next_key = sort_key;
PlayerClient.prototype.getDefaultQueuePosition = function() {
var previousKey = this.currentItem && this.currentItem.sortKey;
var nextKey = null;
var startPos = this.currentItem ? this.currentItem.index + 1 : 0;
for (var i = startPos; i < this.playlist.itemList.length; i += 1) {
var track = this.playlist.itemList[i];
var sortKey = track.sortKey;
if (track.isRandom) {
nextKey = sortKey;
break;
}
previous_key = sort_key;
previousKey = sortKey;
}
return {
previous_key: previous_key,
next_key: next_key
previousKey: previousKey,
nextKey: nextKey
};
};
// TODO: use keese
PlayerClient.prototype.generateSortKey = function(previous_key, next_key){
if (previous_key != null) {
if (next_key != null) {
return (previous_key + next_key) / 2;
} else {
return 0 | previous_key + 1;
}
} else {
if (next_key != null) {
return (0 + next_key) / 2;
} else {
return 1;
}
}
};
PlayerClient.prototype.queueTracks = function(keys, previousKey, nextKey) {
if (!keys.length) return;
PlayerClient.prototype.queueFiles = function(files, previous_key, next_key, is_random){
var ref$, items, i, len$, file, sort_key, id;
if (!files.length) {
return;
if (previousKey == null && nextKey == null) {
var defaultPos = this.getDefaultQueuePosition();
previousKey = defaultPos.previousKey;
nextKey = defaultPos.nextKey;
}
is_random = Boolean(is_random);
if (previous_key == null && next_key == null) {
ref$ = this.getDefaultQueuePosition(), previous_key = ref$.previous_key, next_key = ref$.next_key;
}
items = {};
for (i = 0, len$ = files.length; i < len$; ++i) {
file = files[i];
sort_key = this.generateSortKey(previous_key, next_key);
id = uuid();
var items = {};
for (var i = 0; i < keys.length; i += 1) {
var key = keys[i];
var sortKey = keese(previousKey, nextKey);
var id = uuid();
items[id] = {
file: file,
sort_key: sort_key,
is_random: is_random
key: key,
sortKey: sortKey,
};
this.playlist.item_table[id] = {
this.playlist.itemTable[id] = {
id: id,
sort_key: sort_key,
is_random: is_random,
track: this.library.track_table[file]
key: key,
sortKey: sortKey,
isRandom: false,
track: this.library.trackTable[key],
};
previous_key = sort_key;
previousKey = sortKey;
}
this.refreshPlaylistList();
this.sendCommand({
name: 'addid',
items: items
items: items,
});
this.emit('playlistupdate');
};
PlayerClient.prototype.queueFilesNext = function(files){
var curItem = this.status.current_item;
var prevKey = curItem && curItem.sort_key;
PlayerClient.prototype.queueTracksNext = function(keys) {
var prevKey = this.currentItem && this.currentItem.sortKey;
var nextKey = null;
var itemList = this.playlist.item_list;
var itemList = this.playlist.itemList;
for (var i = 0; i < itemList.length; ++i) {
var track = itemList[i];
if (prevKey == null || track.sort_key > prevKey) {
if (nextKey == null || track.sort_key < nextKey) {
nextKey = track.sort_key;
if (prevKey == null || track.sortKey > prevKey) {
if (nextKey == null || track.sortKey < nextKey) {
nextKey = track.sortKey;
}
}
}
this.queueFiles(files, prevKey, nextKey);
this.queueTracks(keys, prevKey, nextKey);
};
PlayerClient.prototype.clear = function(){
@ -298,133 +245,131 @@ PlayerClient.prototype.shuffle = function(){
PlayerClient.prototype.stop = function(){
this.sendCommandName('stop');
this.status.state = "stop";
this.state = "stop";
this.emit('statusupdate');
};
PlayerClient.prototype.play = function(){
this.sendCommandName('play');
if (this.status.state === "pause") {
this.status.track_start_date = elapsedToDate(this.status.paused_time);
this.status.state = "play";
if (this.state === "pause") {
this.trackStartDate = elapsedToDate(this.pausedTime);
this.state = "play";
this.emit('statusupdate');
}
};
PlayerClient.prototype.pause = function(){
this.sendCommandName('pause');
if (this.status.state === "play") {
this.status.paused_time = dateToElapsed(this.status.track_start_date);
this.status.state = "pause";
if (this.state === "play") {
this.pausedTime = dateToElapsed(this.trackStartDate);
this.state = "pause";
this.emit('statusupdate');
}
};
PlayerClient.prototype.next = function(){
var currentItem = this.status.current_item;
var pos = currentItem ? currentItem.pos + 1 : 0;
var index = this.currentItem ? this.currentItem.index + 1 : 0;
// handle the case of Repeat All
if (pos >= this.playlist.item_list.length &&
this.status.repeat === PlayerClient.REPEAT_ALL)
if (index >= this.playlist.itemList.length &&
this.repeat === PlayerClient.REPEAT_ALL)
{
pos = 0;
index = 0;
}
var item = this.playlist.item_list[pos];
var item = this.playlist.itemList[index];
var id = item && item.id;
this.playId(id);
};
PlayerClient.prototype.prev = function(){
var currentItem = this.status.current_item;
var pos = currentItem ? currentItem.pos - 1 : this.playlist.item_list.length - 1;
var index = this.currentItem ? this.currentItem.index - 1 : this.playlist.itemList.length - 1;
// handle case of Repeat All
if (pos < 0 && this.status.repeat === PlayerClient.REPEAT_ALL) {
pos = this.playlist.item_list.length - 1;
if (index < 0 && this.repeat === PlayerClient.REPEAT_ALL) {
index = this.playlist.itemList.length - 1;
}
var item = this.playlist.item_list[pos];
var item = this.playlist.itemList[index];
var id = item && item.id;
this.playId(id);
};
PlayerClient.prototype.playId = function(track_id){
PlayerClient.prototype.playId = function(trackId){
this.sendCommand({
name: 'playid',
track_id: track_id
trackId: trackId
});
this.anticipatePlayId(track_id);
this.anticipatePlayId(trackId);
};
PlayerClient.prototype.moveIds = function(track_ids, previous_key, next_key){
var res$, i, len$, id, track, tracks, items, sort_key;
res$ = [];
for (i = 0, len$ = track_ids.length; i < len$; ++i) {
id = track_ids[i];
if ((track = this.playlist.item_table[id]) != null) {
res$.push(track);
}
PlayerClient.prototype.moveIds = function(trackIds, previousKey, nextKey){
var track, i;
var tracks = [];
for (i = 0; i < trackIds.length; i += 1) {
var id = trackIds[i];
track = this.playlist.itemTable[id];
if (track) tracks.push(track);
}
tracks = res$;
tracks.sort(compareSortKeyAndId);
items = {};
for (i = 0, len$ = tracks.length; i < len$; ++i) {
var items = {};
for (i = 0; i < tracks.length; i += 1) {
track = tracks[i];
sort_key = this.generateSortKey(previous_key, next_key);
items[id] = {
sort_key: sort_key
var sortKey = keese(previousKey, nextKey);
items[track.id] = {
sortKey: sortKey,
};
track.sort_key = sort_key;
previous_key = sort_key;
track.sortKey = sortKey;
previousKey = sortKey;
}
this.refreshPlaylistList();
this.sendCommand({
name: 'move',
items: items
items: items,
});
this.emit('playlistupdate');
};
PlayerClient.prototype.shiftIds = function(track_id_set, offset){
var items, previous_key, next_key, i, ref$, len$, track, sort_key;
items = {};
previous_key = null;
next_key = null;
PlayerClient.prototype.shiftIds = function(trackIdSet, offset){
var i;
var items = {};
var previousKey = null;
var nextKey = null;
var itemList = this.playlist.itemList;
var track, sortKey;
if (offset < 0) {
for (i = 0, len$ = (ref$ = this.playlist.item_list).length; i < len$; ++i) {
track = ref$[i];
if (track.id in track_id_set) {
if (next_key == null) {
for (i = 0; i < itemList.length; i += 1) {
track = itemList[i];
if (track.id in trackIdSet) {
if (nextKey == null) {
continue;
}
sort_key = this.generateSortKey(previous_key, next_key);
sortKey = keese(previousKey, nextKey);
items[track.id] = {
sort_key: sort_key
sortKey: sortKey
};
track.sort_key = sort_key;
track.sortKey = sortKey;
}
previous_key = next_key;
next_key = track.sort_key;
previousKey = nextKey;
nextKey = track.sortKey;
}
} else {
for (i = (ref$ = this.playlist.item_list).length - 1; i >= 0; --i) {
track = ref$[i];
if (track.id in track_id_set) {
if (previous_key == null) {
for (i = itemList.length - 1; i >= 0; i -= 1) {
track = itemList[i];
if (track.id in trackIdSet) {
if (previousKey == null) {
continue;
}
sort_key = this.generateSortKey(previous_key, next_key);
sortKey = keese(previousKey, nextKey);
items[track.id] = {
sort_key: sort_key
sortKey: sortKey
};
track.sort_key = sort_key;
track.sortKey = sortKey;
}
next_key = previous_key;
previous_key = track.sort_key;
nextKey = previousKey;
previousKey = track.sortKey;
}
}
this.refreshPlaylistList();
@ -435,20 +380,18 @@ PlayerClient.prototype.shiftIds = function(track_id_set, offset){
this.emit('playlistupdate');
};
PlayerClient.prototype.removeIds = function(track_ids){
var ids, i, len$, track_id, ref$, item;
if (track_ids.length === 0) {
return;
}
ids = [];
for (i = 0, len$ = track_ids.length; i < len$; ++i) {
track_id = track_ids[i];
if (((ref$ = this.status.current_item) != null ? ref$.id : void 8) === track_id) {
this.status.current_item = null;
PlayerClient.prototype.removeIds = function(trackIds){
if (trackIds.length === 0) return;
var ids = [];
for (var i = 0; i < trackIds.length; i += 1) {
var trackId = trackIds[i];
var currentId = this.currentItem && this.currentItem.id;
if (currentId === trackId) {
this.currentItem = null;
}
ids.push(track_id);
item = this.playlist.item_table[track_id];
delete this.playlist.item_table[item.id];
ids.push(trackId);
var item = this.playlist.itemTable[trackId];
delete this.playlist.itemTable[item.id];
this.refreshPlaylistList();
}
this.sendCommand({
@ -458,15 +401,15 @@ PlayerClient.prototype.removeIds = function(track_ids){
this.emit('playlistupdate');
};
PlayerClient.prototype.seek = function(pos){
PlayerClient.prototype.seek = function(pos) {
pos = parseFloat(pos, 10);
if (pos < 0) pos = 0;
if (pos > this.status.time) pos = this.status.time;
if (pos > this.duration) pos = this.duration;
this.sendCommand({
name: 'seek',
pos: pos
pos: pos,
});
this.status.track_start_date = elapsedToDate(pos);
this.trackStartDate = elapsedToDate(pos);
this.emit('statusupdate');
};
@ -475,12 +418,12 @@ PlayerClient.prototype.setVolume = function(vol){
name: "setvol",
vol: vol,
});
this.status.volume = vol;
this.volume = vol;
this.emit('statusupdate');
};
PlayerClient.prototype.setRepeatMode = function(mode) {
this.status.repeat = mode;
this.repeat = mode;
this.sendCommand({
name: 'repeat',
mode: mode,
@ -489,7 +432,6 @@ PlayerClient.prototype.setRepeatMode = function(mode) {
};
PlayerClient.prototype.authenticate = function(password, callback){
var this$ = this;
callback = callback || noop;
this.sendCommand({
name: 'password',
@ -508,19 +450,20 @@ PlayerClient.prototype.sendCommandName = function(name, cb){
PlayerClient.prototype.sendCommand = function(cmd, cb){
cb = cb || noop;
var callback_id = this.next_response_handler_id++;
this.response_handlers[callback_id] = cb;
var callbackId = this.nextResponseHandlerId++;
this.responseHandlers[callbackId] = cb;
this.socket.emit('request', JSON.stringify({
cmd: cmd,
callback_id: callback_id
callbackId: callbackId
}));
};
PlayerClient.prototype.handleResponse = function(arg){
var err, msg, callback_id, handler;
err = arg.err, msg = arg.msg, callback_id = arg.callback_id;
handler = this.response_handlers[callback_id];
delete this.response_handlers[callback_id];
var err = arg.err;
var msg = arg.msg;
var callbackId = arg.callbackId;
var handler = this.responseHandlers[callbackId];
delete this.responseHandlers[callbackId];
handler(err, msg);
};
@ -534,160 +477,62 @@ PlayerClient.prototype.handleStatus = function(systems){
PlayerClient.prototype.clearPlaylist = function(){
this.playlist = {
item_list: [],
item_table: {},
pos: null,
itemList: [],
itemTable: {},
index: null,
name: null
};
};
PlayerClient.prototype.anticipatePlayId = function(track_id){
var item = this.playlist.item_table[track_id];
this.status.current_item = item;
this.status.state = "play";
this.status.time = item.track.time;
this.status.track_start_date = new Date();
PlayerClient.prototype.anticipatePlayId = function(trackId){
var item = this.playlist.itemTable[trackId];
this.currentItem = item;
this.state = "play";
this.duration = item.track.duration;
this.trackStartDate = new Date();
this.emit('statusupdate');
};
PlayerClient.prototype.anticipateSkip = function(direction){
var ref$, next_item;
next_item = this.playlist.item_list[((ref$ = this.status.current_item) != null ? ref$.pos : void 8) + direction];
if (next_item != null) {
this.anticipatePlayId(next_item.id);
}
};
PlayerClient.prototype.buildArtistAlbumTree = function(tracks, library){
var ref$;
var len$, track, album_key, album, artist_table, k, album_artists, i, ref1$, album_artist_name, artist_key, artist, various_artist;
library.track_table = {};
library.album_table = {};
for (i = 0, len$ = tracks.length; i < len$; ++i) {
track = tracks[i];
library.track_table[track.file] = track;
album_key = albumKey(track);
album = getOrCreate(album_key, library.album_table, fn$);
track.album = album;
album.tracks.push(track);
if (album.year == null) {
album.year = track.year;
}
}
artist_table = {};
for (k in library.album_table) {
album = library.album_table[k];
album_artists = {};
album.tracks.sort(trackComparator);
for (i = 0, len$ = (ref1$ = album.tracks).length; i < len$; ++i) {
track = ref1$[i];
track.pos = i;
album_artist_name = track.album_artist_name;
album_artists[artistKey(album_artist_name)] = true;
album_artists[artistKey(track.artist_name)] = true;
}
if (moreThanOneKey(album_artists)) {
album_artist_name = VARIOUS_ARTISTS_NAME;
artist_key = VARIOUS_ARTISTS_KEY;
for (i = 0, len$ = (ref1$ = album.tracks).length; i < len$; ++i) {
track = ref1$[i];
track.artist_disambiguation = track.artist_name;
}
} else {
artist_key = artistKey(album_artist_name);
}
artist = getOrCreate(artist_key, artist_table, fn1$);
album.artist = artist;
artist.albums.push(album);
}
library.artists = [];
various_artist = null;
for (k in artist_table) {
artist = artist_table[k];
artist.albums.sort(albumComparator);
for (i = 0, len$ = (ref$ = artist.albums).length; i < len$; ++i) {
album = ref$[i];
album.pos = i;
}
if (artist.key === VARIOUS_ARTISTS_KEY) {
various_artist = artist;
} else {
library.artists.push(artist);
}
}
library.artists.sort(artistComparator);
if (various_artist != null) {
library.artists.splice(0, 0, various_artist);
}
for (i = 0, len$ = (ref$ = library.artists).length; i < len$; ++i) {
artist = ref$[i];
artist.pos = i;
}
library.artist_table = artist_table;
function fn$(){
return {
name: track.album_name,
year: track.year,
tracks: [],
key: album_key
};
}
function fn1$(){
return {
name: album_artist_name,
albums: [],
key: artist_key
};
PlayerClient.prototype.anticipateSkip = function(direction) {
if (this.currentItem) {
var nextItem = this.playlist.itemList[this.currentItem.index + direction];
if (nextItem) this.anticipatePlayId(nextItem.id);
}
};
PlayerClient.prototype.refreshPlaylistList = function(){
var ref$;
var id, item, i, len$;
this.playlist.item_list = [];
for (id in this.playlist.item_table) {
item = this.playlist.item_table[id];
this.playlist.itemList = [];
var item;
for (var id in this.playlist.itemTable) {
item = this.playlist.itemTable[id];
item.playlist = this.playlist;
this.playlist.item_list.push(item);
this.playlist.itemList.push(item);
}
this.playlist.item_list.sort(compareSortKeyAndId);
for (i = 0, len$ = (ref$ = this.playlist.item_list).length; i < len$; ++i) {
item = ref$[i];
item.pos = i;
this.playlist.itemList.sort(compareSortKeyAndId);
for (var i = 0; i < this.playlist.itemList.length; i += 1) {
item = this.playlist.itemList[i];
item.index = i;
}
};
PlayerClient.prototype.resetServerState = function(){
this.buffer = "";
this.response_handlers = {};
this.next_response_handler_id = 0;
this.have_file_list_cache = false;
this.library = {
artists: [],
track_table: {}
};
this.search_results = this.library;
this.last_query = "";
this.responseHandlers = {};
this.nextResponseHandlerId = 0;
this.haveFileListCache = false;
this.library = new MusicLibraryIndex({
searchFields: MusicLibraryIndex.defaultSearchFields.concat('file'),
});
this.searchResults = this.library;
this.lastQuery = "";
this.clearPlaylist();
this.status = {
repeat: 0,
current_item: null
};
this.repeat = 0;
this.currentItem = null;
this.stored_playlist_table = {};
this.stored_playlist_item_table = {};
this.stored_playlists = [];
};
function stripPrefixes(str){
var i, ref$, len$, regex;
for (i = 0, len$ = (ref$ = PREFIXES_TO_STRIP).length; i < len$; ++i) {
regex = ref$[i];
str = str.replace(regex, '');
break;
}
return str;
}
function elapsedToDate(elapsed){
return new Date(new Date() - elapsed * 1000);
}
@ -696,106 +541,10 @@ function dateToElapsed(date){
return (new Date() - date) / 1000;
}
function sortableTitle(title){
return stripPrefixes(formatSearchable(title));
}
function titleCompare(a, b){
var _a = sortableTitle(a);
var _b = sortableTitle(b);
if (_a < _b) {
return -1;
} else if (_a > _b) {
return 1;
} else {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
}
function noop(err){
if (err) throw err;
}
function getOrCreate(key, table, initObjFunc){
var result;
result = table[key];
if (result == null) {
result = initObjFunc();
table[key] = result;
}
return result;
}
function trackComparator(a, b){
if (a.track < b.track) {
return -1;
} else if (a.track > b.track) {
return 1;
} else {
return titleCompare(a.name, b.name);
}
}
function albumComparator(a, b){
if (a.year < b.year) {
return -1;
} else if (a.year > b.year) {
return 1;
} else {
return titleCompare(a.name, b.name);
}
}
function artistComparator(a, b){
return titleCompare(a.name, b.name);
}
function albumKey(track){
if (track.album_name) {
return formatSearchable(track.album_name);
} else {
return formatSearchable(track.album_artist_name);
}
}
function artistKey(artist_name){
return formatSearchable(artist_name);
}
function moreThanOneKey(object){
var count = -2;
for (var k in object) {
if (!++count) {
return true;
}
}
return false;
}
function addSearchTags(tracks){
var i, len$, track;
for (i = 0, len$ = tracks.length; i < len$; ++i) {
track = tracks[i];
track.search_tags = formatSearchable([
track.artist_name,
track.album_artist_name,
track.album_name,
track.name,
track.file,
].join("\n"));
}
}
function formatSearchable(str) {
return removeDiacritics(str).toLowerCase();
}
function operatorCompare(a, b){
if (a === b) {
return 0;
@ -808,14 +557,11 @@ function operatorCompare(a, b){
}
function makeCompareProps(props){
return function(a, b){
var i, ref$, len$, prop, result;
for (i = 0, len$ = (ref$ = props).length; i < len$; ++i) {
prop = ref$[i];
result = operatorCompare(a[prop], b[prop]);
if (result) {
return result;
}
return function(a, b) {
for (var i = 0; i < props.length; i += 1) {
var prop = props[i];
var result = operatorCompare(a[prop], b[prop]);
if (result) return result;
}
return 0;
};

View file

@ -57,7 +57,7 @@ function getUrl(){
}
function updatePlayer() {
var should_stream = trying_to_stream && player.status.state === "play";
var should_stream = trying_to_stream && player.state === "play";
if (actually_streaming === should_stream) return;
if (should_stream) {
soundManager.destroySound('stream');

View file

@ -1,17 +1,17 @@
{{#albums}}
{{#albumList}}
<li>
<div class="clickable expandable" id="{{albumid key}}" data-key="{{key}}" data-type="album">
<div class="ui-icon ui-icon-triangle-1-e"></div>
<span>{{#if name}}{{name}}{{else}}[Unknown Album]{{/if}}</span>
</div>
<ul style="display: none;">
{{#tracks}}
{{#trackList}}
<li>
<div class="clickable" id="{{trackid file}}" data-key="{{file}}" data-type="track">
<span>{{#if track}}{{track}}. {{/if}}{{#if artist_disambiguation}}{{artist_disambiguation}} - {{/if}}{{name}}</span>
<div class="clickable" id="{{trackid key}}" data-key="{{key}}" data-type="track">
<span>{{#if track}}{{track}}. {{/if}}{{#if compilation}}{{artistName}} - {{/if}}{{name}}</span>
</div>
</li>
{{/tracks}}
{{/trackList}}
</ul>
</li>
{{/albums}}
{{/albumList}}

View file

@ -1,6 +1,6 @@
{{#if artists}}
{{#if artistList}}
<ul>
{{#artists}}
{{#artistList}}
<li>
<div class="clickable expandable" id="{{artistid key}}" data-key="{{key}}" data-type="artist">
<div class="ui-icon ui-icon-triangle-1-e"></div>
@ -8,11 +8,11 @@
</div>
<ul></ul>
</li>
{{/artists}}
{{/artistList}}
</ul>
{{else}}
<p class="ui-state-highlight ui-corner-all">
<span class="ui-icon ui-icon-info"></span>
<strong>{{empty_library_message}}</strong>
<strong>{{emptyLibraryMessage}}</strong>
</p>
{{/if}}

View file

@ -3,9 +3,9 @@
<div class="pl-item" id="playlist-track-{{id}}" data-id="{{id}}">
<span class="track">{{track.track}}</span>
<span class="title">{{track.name}}</span>
<span class="artist">{{track.artist_name}}</span>
<span class="album">{{track.album_name}}</span>
<span class="time">{{time track.time}}</span>
<span class="artist">{{track.artistName}}</span>
<span class="album">{{track.albumName}}</span>
<span class="time">{{time track.duration}}</span>
</div>
{{/playlist}}
{{/if}}

View file

@ -1,11 +1,11 @@
{{#item_list}}
{{#itemList}}
<li>
<div class="clickable" id="{{storedplaylistitemid id}}" data-key="{{id}}" data-type="stored_playlist_item">
<span>
{{#track}}
{{#if artist_name}}{{artist_name}} - {{/if}}{{name}}
{{#if artistName}}{{artistName}} - {{/if}}{{name}}
{{/track}}
</span>
</div>
</li>
{{/item_list}}
{{/itemList}}