Jamstash/app/subsonic/subsonic-service.js
Hyzual d2bdb8eed9 Use angular cache when querying getMusicFolders and getIndexes
It saves us a few requests and helps making Jamstash faster.
It's unlikely that the artists list or the music folders will change during a Jamstash session. It's also an easy cache to reset: the user only has to reload the page.
2015-07-07 22:09:12 +02:00

558 lines
24 KiB
JavaScript

/**
* jamstash.subsonicService Module
*
* Provides access through $http to the Subsonic server's API.
* Also offers more fine-grained functionality that is not part of Subsonic's API.
*/
angular.module('jamstash.subsonic.service', [
'ngLodash',
'jamstash.settings.service',
'jamstash.utils',
'jamstash.model'
])
.factory('subsonic', [
'$rootScope',
'$http',
'$q',
'lodash',
'globals',
'utils',
'map',
function (
$rootScope,
$http,
$q,
_,
globals,
utils,
map
) {
'use strict';
// TODO: Hyz: Remove when refactored
var content = {
album: [],
song: [],
playlists: [],
breadcrumb: [],
playlistsPublic: [],
playlistsGenre: globals.SavedGenres,
selectedAutoAlbum: null,
selectedArtist: null,
selectedAlbum: null,
selectedPlaylist: null,
selectedAutoPlaylist: null,
selectedGenre: null,
selectedPodcast: null
};
var showPlaylist = false;
var subsonicService = {
// TODO: Hyz: Remove when refactored
showIndex: $rootScope.showIndex,
showPlaylist: showPlaylist,
/**
* Handles building the URL with the correct parameters and error-handling while communicating with
* a Subsonic server
* @param {String} partialUrl the last part of the Subsonic URL you want, e.g. 'getStarred.view'. If it does not start with a '/', it will be prefixed
* @param {Object} config optional $http config object. The base settings expected by Subsonic (username, password, etc.) will be overwritten.
* @return {Promise} a Promise that will be resolved if we receive the 'ok' status from Subsonic. Will be rejected otherwise with an object : {'reason': a message that can be displayed to a user, 'httpError': the HTTP error code, 'subsonicError': the error Object sent by Subsonic}
*/
subsonicRequest: function (partialUrl, config) {
var exception = { reason: 'Error when contacting the Subsonic server.' };
var deferred = $q.defer();
var actualUrl = (partialUrl.charAt(0) === '/') ? partialUrl : '/' + partialUrl;
var url = globals.BaseURL() + actualUrl;
// Extend the provided config (if it exists) with our params
// Otherwise we create a config object
var actualConfig = config || {};
actualConfig.params = actualConfig.params || {};
_.extend(actualConfig.params, {
u: globals.settings.Username,
p: globals.settings.Password,
f: globals.settings.Protocol,
v: globals.settings.ApiVersion,
c: globals.settings.ApplicationName
});
actualConfig.timeout = globals.settings.Timeout;
var httpPromise;
if (globals.settings.Protocol === 'jsonp') {
actualConfig.params.callback = 'JSON_CALLBACK';
httpPromise = $http.jsonp(url, actualConfig);
} else {
httpPromise = $http.get(url, actualConfig);
}
httpPromise.success(function (data) {
var subsonicResponse = (data['subsonic-response'] !== undefined) ? data['subsonic-response'] : { status: 'failed' };
if (subsonicResponse.status === 'ok') {
deferred.resolve(subsonicResponse);
} else {
if (subsonicResponse.status === 'failed' && subsonicResponse.error !== undefined) {
exception.subsonicError = subsonicResponse.error;
exception.version = subsonicResponse.version;
}
deferred.reject(exception);
}
}).error(function (data, status) {
exception.httpError = status;
deferred.reject(exception);
});
return deferred.promise;
},
ping: function () {
return subsonicService.subsonicRequest('ping.view');
},
getMusicFolders: function () {
var exception = { reason: 'No music folder found on the Subsonic server.' };
var promise = subsonicService.subsonicRequest('getMusicFolders.view', {
cache: true
}).then(function (subsonicResponse) {
if (subsonicResponse.musicFolders !== undefined && subsonicResponse.musicFolders.musicFolder !== undefined) {
return [].concat(subsonicResponse.musicFolders.musicFolder);
} else {
return $q.reject(exception);
}
});
return promise;
},
getArtists: function (folder) {
var exception = { reason: 'No artist found on the Subsonic server.' };
var params;
if (! isNaN(folder)) {
params = {
musicFolderId: folder
};
}
var promise = subsonicService.subsonicRequest('getIndexes.view', {
cache: true,
params: params
}).then(function (subsonicResponse) {
if (subsonicResponse.indexes !== undefined && (subsonicResponse.indexes.index !== undefined || subsonicResponse.indexes.shortcut !== undefined)) {
// Make sure shortcut, index and each index's artist are arrays
// because Madsonic will return an object when there's only one element
var formattedResponse = {};
formattedResponse.shortcut = [].concat(subsonicResponse.indexes.shortcut);
formattedResponse.index = [].concat(subsonicResponse.indexes.index);
_.map(formattedResponse.index, function (index) {
var formattedIndex = index;
formattedIndex.artist = [].concat(index.artist);
return formattedIndex;
});
return formattedResponse;
} else {
return $q.reject(exception);
}
});
return promise;
},
getAlbumByTag: function (id) { // Gets Album by ID3 tag: NOT Being Used Currently 1/24/2015
var deferred = $q.defer();
$.ajax({
url: globals.BaseURL() + '/getAlbum.view?' + globals.BaseParams() + '&id=' + id,
method: 'GET',
dataType: globals.settings.Protocol,
timeout: globals.settings.Timeout,
success: function (data) {
if (typeof data["subsonic-response"].album != 'undefined') {
content.album = [];
content.song = [];
var items = [];
if (data["subsonic-response"].album.song.length > 0) {
items = data["subsonic-response"].album.song;
} else {
items[0] = data["subsonic-response"].album.song;
}
angular.forEach(items, function (item, key) {
content.song.push(map.mapSong(item));
});
}
deferred.resolve(content);
}
});
return deferred.promise;
},
getSongs: function (id) {
var exception = { reason: 'This directory is empty.' };
var promise = subsonicService.subsonicRequest('getMusicDirectory.view', {
params: {
id: id
}
}).then(function (subsonicResponse) {
if (subsonicResponse.directory.child !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var children = [].concat(subsonicResponse.directory.child);
if (children.length > 0) {
var allChildren = _.partition(children, function (item) {
return item.isDir;
});
return {
directories: map.mapAlbums(allChildren[0]),
songs: map.mapSongs(allChildren[1])
};
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
// This is used when we add or play a directory, so we recursively get all its contents
recursiveGetSongs: function (id) {
var deferred = $q.defer();
// We first use getSongs() to get the contents of the root directory
subsonicService.getSongs(id).then(function (data) {
var directories = data.directories;
var songs = data.songs;
// If there are only songs, we return them immediately: this is a leaf directory and the end of the recursion
if (directories.length === 0) {
deferred.resolve(songs);
} else {
// otherwise, for each directory, we call ourselves
var promises = [];
angular.forEach(directories, function (dir) {
var subdirectoryRequest = subsonicService.recursiveGetSongs(dir.id).then(function (data) {
// This is where we join all the songs together in a single array
return songs.concat(data);
});
promises.push(subdirectoryRequest);
});
// since all of this is asynchronous, we need to wait for all the requests to finish by using $q.all()
var allRequestsFinished = $q.all(promises).then(function (data) {
// and since $q.all() wraps everything in another array, we use flatten() to end up with only one array of songs
return _.flatten(data);
});
deferred.resolve(allRequestsFinished);
}
}, function () {
// Even if getSongs returns an error, we resolve with an empty array. Otherwise one empty directory somewhere
// would keep us from playing all the songs of a directory recursively
deferred.resolve([]);
});
return deferred.promise;
},
getAlbumListBy: function (type, offset) {
var actualOffset = (offset > 0) ? offset : 0;
var exception = { reason: 'No matching albums found on the Subsonic server.' };
var params = {
size: globals.settings.AutoAlbumSize,
type: type,
offset: actualOffset
};
var promise = subsonicService.subsonicRequest('getAlbumList.view', {
params: params
}).then(function (subsonicResponse) {
if (subsonicResponse.albumList.album !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var albumArray = [].concat(subsonicResponse.albumList.album);
if (albumArray.length > 0) {
return map.mapAlbums(albumArray);
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
search: function (query, type) {
if (_([0, 1, 2]).contains(type)) {
var promise = subsonicService.subsonicRequest('search2.view', {
params: {
query: query
}
}).then(function (subsonicResponse) {
var searchResult;
if (! _.isEmpty(subsonicResponse.searchResult2)) {
searchResult = subsonicResponse.searchResult2;
} else if (! _.isEmpty(subsonicResponse.search2)) {
// We also check search2 because Music Cabinet doesn't respond the same thing
// as everyone else...
searchResult = subsonicResponse.search2;
}
if (! _.isEmpty(searchResult)) {
// Make sure that song, album and artist are arrays using concat
// because Madsonic will return an object when there's only one element
switch (type) {
case 0:
if (searchResult.song !== undefined) {
return map.mapSongs([].concat(searchResult.song));
}
break;
case 1:
if (searchResult.album !== undefined) {
return map.mapAlbums([].concat(searchResult.album));
}
break;
case 2:
if (searchResult.artist !== undefined) {
return [].concat(searchResult.artist);
}
break;
}
}
// We end up here for every else
return $q.reject({ reason: 'No results.' });
});
return promise;
} else {
return $q.reject({ reason: 'Wrong search type.' });
}
},
getRandomSongs: function (genre, folder) {
var exception = { reason: 'No songs found on the Subsonic server.' };
var params = {
size: globals.settings.AutoPlaylistSize
};
if (genre !== undefined && genre !== '' && genre !== 'Random') {
params.genre = genre;
}
if (! isNaN(folder)) {
params.musicFolderId = folder;
}
var promise = subsonicService.subsonicRequest('getRandomSongs.view', {
params: params
}).then(function (subsonicResponse) {
if (subsonicResponse.randomSongs !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var songArray = [].concat(subsonicResponse.randomSongs.song);
if (songArray.length > 0) {
return map.mapSongs(songArray);
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
getStarred: function () {
var promise = subsonicService.subsonicRequest('getStarred.view', { cache: true })
.then(function (subsonicResponse) {
if (angular.equals(subsonicResponse.starred, {})) {
return $q.reject({ reason: 'Nothing is starred on the Subsonic server.' });
} else {
return subsonicResponse.starred;
}
});
return promise;
},
getRandomStarredSongs: function () {
var promise = subsonicService.getStarred()
.then(function (starred) {
if (starred.song !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var songArray = [].concat(starred.song);
if (songArray.length > 0) {
// Return random subarray of songs
var songs = [].concat(_.sample(songArray, globals.settings.AutoPlaylistSize));
return map.mapSongs(songs);
}
}
// We end up here for every else
return $q.reject({ reason: 'No starred songs found on the Subsonic server.' });
});
return promise;
},
getPlaylists: function () {
var exception = { reason: 'No playlist found on the Subsonic server.' };
var promise = subsonicService.subsonicRequest('getPlaylists.view')
.then(function (subsonicResponse) {
if (subsonicResponse.playlists.playlist !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var playlistArray = [].concat(subsonicResponse.playlists.playlist);
if (playlistArray.length > 0) {
var allPlaylists = _.partition(playlistArray, function (item) {
return item.owner === globals.settings.Username;
});
return { playlists: allPlaylists[0], playlistsPublic: allPlaylists[1] };
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
getPlaylist: function (id) {
var exception = { reason: 'This playlist is empty.' };
var promise = subsonicService.subsonicRequest('getPlaylist.view', {
params: {
id: id
}
}).then(function (subsonicResponse) {
if (subsonicResponse.playlist.entry !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var entryArray = [].concat(subsonicResponse.playlist.entry);
if (entryArray.length > 0) {
return map.mapSongs(entryArray);
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
newPlaylist: function (name) {
var promise = subsonicService.subsonicRequest('createPlaylist.view', {
params: {
name: name
}
});
return promise;
},
deletePlaylist: function (id) {
var promise = subsonicService.subsonicRequest('deletePlaylist.view', {
params: {
id: id
}
});
return promise;
},
savePlaylist: function (playlistId, songs) {
var params = {
params: {
playlistId: playlistId,
songId: []
}
};
for (var i = 0; i < songs.length; i++) {
params.params.songId.push(songs[i].id);
}
return subsonicService.subsonicRequest('createPlaylist.view', params);
},
//TODO: Hyz: move to controller
songsRemoveSelected: function (songs) {
var deferred = $q.defer();
angular.forEach(songs, function (item, key) {
var index = content.song.indexOf(item)
content.song.splice(index, 1);
});
deferred.resolve(content);
return deferred.promise;
},
getGenres: function () {
var exception = { reason: 'No genre found on the Subsonic server.' };
var promise = subsonicService.subsonicRequest('getGenres.view')
.then(function (subsonicResponse) {
if (subsonicResponse.genres !== undefined && subsonicResponse.genres.genre !== undefined) {
var genreArray = [].concat(subsonicResponse.genres.genre);
if (genreArray.length > 0) {
var stringArray;
if (genreArray[0].value) {
stringArray = _.pluck(genreArray, 'value');
// Of course, Madsonic doesn't return the same thing as Subsonic...
} else if (genreArray[0].content) {
stringArray = _.pluck(genreArray, 'content');
}
return stringArray;
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
getPodcasts: function () {
var exception = { reason: 'No podcast found on the Subsonic server.' };
var promise = subsonicService.subsonicRequest('getPodcasts.view', {
params: {
includeEpisodes: false
}
})
.then(function (subsonicResponse) {
if (subsonicResponse.podcasts !== undefined && subsonicResponse.podcasts.channel !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var channelArray = [].concat(subsonicResponse.podcasts.channel);
if (channelArray.length > 0) {
return channelArray;
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
getPodcast: function (id) {
var exception = { reason: 'This podcast was not found on the Subsonic server.' };
var promise = subsonicService.subsonicRequest('getPodcasts.view', {
params: {
id: id,
includeEpisodes: true
}
}).then(function (subsonicResponse) {
var episodes = [];
if (subsonicResponse.podcasts.channel !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var channelArray = [].concat(subsonicResponse.podcasts.channel);
if (channelArray.length > 0) {
var channel = channelArray[0];
if (channel !== null && channel.id === id) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var episodesArray = [].concat(channel.episode);
episodes = _.filter(episodesArray, function (episode) {
return episode.status === 'completed';
});
if (episodes.length > 0) {
return map.mapPodcasts(episodes);
} else {
return $q.reject({ reason: 'No downloaded episode found for this podcast. Please check the podcast settings.' });
}
}
}
}
// We end up here for every else
return $q.reject(exception);
});
return promise;
},
scrobble: function (song) {
var promise = subsonicService.subsonicRequest('scrobble.view', {
params: {
id: song.id,
submisssion: true
}
}).then(function () {
if (globals.settings.Debug) { console.log('Successfully scrobbled song: ' + song.id); }
return true;
});
return promise;
},
toggleStar: function (item) {
var partialUrl = (item.starred) ? 'unstar.view' : 'star.view';
var promise = subsonicService.subsonicRequest(partialUrl, {
params: {
id: item.id
}
}).then(function () {
return ! item.starred;
});
return promise;
}
// End subsonic
};
return subsonicService;
}]);