koel/resources/assets/js/stores/song.js

421 lines
12 KiB
JavaScript
Raw Normal View History

2016-02-09 04:57:08 +00:00
import Vue from 'vue';
2016-04-05 07:38:10 +00:00
import { without, map, take, remove, orderBy, each } from 'lodash';
2015-12-13 04:42:28 +00:00
import http from '../services/http';
2016-04-10 09:51:06 +00:00
import { secondsToHis } from '../services/utils';
2015-12-13 04:42:28 +00:00
import stub from '../stubs/song';
import favoriteStore from './favorite';
import sharedStore from './shared';
2015-12-22 17:14:47 +00:00
import userStore from './user';
2016-03-05 09:01:12 +00:00
import albumStore from './album';
import artistStore from './artist';
import ls from '../services/ls';
2015-12-13 04:42:28 +00:00
export default {
stub,
albums: [],
2016-01-16 05:55:16 +00:00
cache: {},
2015-12-13 04:42:28 +00:00
state: {
/**
* All songs in the store
*
* @type {Array}
*/
2015-12-13 04:42:28 +00:00
songs: [stub],
/**
* The recently played songs **in the current session**
*
* @type {Array}
*/
recent: [],
2015-12-13 04:42:28 +00:00
},
/**
* Init the store.
*
2016-01-17 14:26:24 +00:00
* @param {Array.<Object>} albums The array of albums to extract our songs from
2015-12-13 04:42:28 +00:00
*/
2015-12-22 17:46:54 +00:00
init(albums) {
2015-12-13 04:42:28 +00:00
// Iterate through the albums. With each, add its songs into our master song list.
2016-04-05 07:38:10 +00:00
this.all = albums.reduce((songs, album) => {
2015-12-13 04:42:28 +00:00
// While doing so, we populate some other information into the songs as well.
2016-04-05 07:38:10 +00:00
each(album.songs, song => {
2016-04-10 09:51:06 +00:00
song.fmtLength = secondsToHis(song.length);
2015-12-13 04:42:28 +00:00
2016-02-10 16:54:55 +00:00
// Manually set these additional properties to be reactive
Vue.set(song, 'playCount', 0);
Vue.set(song, 'album', album);
Vue.set(song, 'liked', false);
2016-02-08 13:14:51 +00:00
Vue.set(song, 'lyrics', null);
Vue.set(song, 'playbackState', 'stopped');
2016-01-16 05:55:16 +00:00
// Cache the song, so that byId() is faster
this.cache[song.id] = song;
2015-12-13 04:42:28 +00:00
});
2015-12-13 04:42:28 +00:00
return songs.concat(album.songs);
}, []);
},
2015-12-22 17:46:54 +00:00
/**
* Initializes the interaction (like/play count) information.
*
2016-01-17 14:26:24 +00:00
* @param {Array.<Object>} interactions The array of interactions of the current user
2015-12-22 17:46:54 +00:00
*/
initInteractions(interactions) {
2015-12-30 04:14:47 +00:00
favoriteStore.clear();
2016-04-05 07:38:10 +00:00
each(interactions, interaction => {
2016-03-28 13:38:14 +00:00
const song = this.byId(interaction.song_id);
2015-12-22 17:46:54 +00:00
if (!song) {
return;
}
song.liked = interaction.liked;
song.playCount = interaction.play_count;
song.album.playCount += song.playCount;
song.album.artist.playCount += song.playCount;
2015-12-22 17:46:54 +00:00
if (song.liked) {
favoriteStore.add(song);
}
});
},
2016-01-14 08:02:59 +00:00
/**
* Get the total duration of some songs.
*
2016-01-17 14:26:24 +00:00
* @param {Array.<Object>} songs
2016-01-19 11:00:23 +00:00
* @param {Boolean} toHis Whether to convert the duration into H:i:s format
*
2016-01-17 14:26:24 +00:00
* @return {Float|String}
2016-01-14 08:02:59 +00:00
*/
getLength(songs, toHis) {
2016-03-28 13:38:14 +00:00
const duration = songs.reduce((length, song) => length + song.length, 0);
2016-01-14 08:02:59 +00:00
if (toHis) {
2016-04-10 09:51:06 +00:00
return secondsToHis(duration);
2016-01-14 08:02:59 +00:00
}
return duration;
},
2015-12-13 04:42:28 +00:00
/**
* Get all songs.
2015-12-22 17:46:54 +00:00
*
2016-01-17 14:26:24 +00:00
* @return {Array.<Object>}
2015-12-13 04:42:28 +00:00
*/
2016-03-18 04:45:12 +00:00
get all() {
2015-12-13 04:42:28 +00:00
return this.state.songs;
},
2016-04-05 07:38:10 +00:00
/**
* Set all songs.
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.songs = value;
},
2015-12-13 04:42:28 +00:00
/**
2016-01-07 09:03:38 +00:00
* Get a song by its ID.
*
2016-01-17 14:26:24 +00:00
* @param {String} id
*
2015-12-22 17:46:54 +00:00
* @return {Object}
2015-12-13 04:42:28 +00:00
*/
byId(id) {
2016-01-16 05:55:16 +00:00
return this.cache[id];
2015-12-13 04:42:28 +00:00
},
/**
2016-01-07 09:03:38 +00:00
* Get songs by their IDs.
*
2016-01-17 14:26:24 +00:00
* @param {Array.<String>} ids
*
2016-01-17 14:26:24 +00:00
* @return {Array.<Object>}
2015-12-13 04:42:28 +00:00
*/
byIds(ids) {
2016-02-13 16:59:57 +00:00
return ids.map(id => this.byId(id));
2015-12-13 04:42:28 +00:00
},
/**
* Increase a play count for a song.
*
* @param {Object} song
* @param {?Function} cb
*/
registerPlay(song, cb = null) {
2016-03-28 13:38:14 +00:00
const oldCount = song.playCount;
http.post('interaction/play', { song: song.id }, response => {
// Use the data from the server to make sure we don't miss a play from another device.
song.playCount = response.data.play_count;
song.album.playCount += song.playCount - oldCount;
song.album.artist.playCount += song.playCount - oldCount;
if (cb) {
cb();
}
});
},
/**
* Add a song into the "recently played" list.
*
* @param {Object}
2015-12-13 04:42:28 +00:00
*/
addRecent(song) {
// First we make sure that there's no duplicate.
this.state.recent = without(this.state.recent, song);
// Then we prepend the song into the list.
this.state.recent.unshift(song);
2015-12-13 04:42:28 +00:00
},
/**
* Get extra song information (lyrics, artist info, album info).
*
* @param {Object} song
* @param {?Function} cb
2015-12-13 04:42:28 +00:00
*/
getInfo(song, cb = null) {
2016-02-08 13:14:51 +00:00
// Check if the song's info has been retrieved before.
2016-03-06 05:02:03 +00:00
if (song.infoRetrieved) {
2015-12-13 04:42:28 +00:00
if (cb) {
cb();
}
2015-12-13 04:42:28 +00:00
return;
}
2016-02-29 16:50:25 +00:00
http.get(`${song.id}/info`, response => {
2016-03-28 13:38:14 +00:00
const data = response.data;
2016-02-29 16:50:25 +00:00
song.lyrics = data.lyrics;
// If the artist image is not in a nice form, don't use it.
if (data.artist_info && typeof data.artist_info.image !== 'string') {
data.artist_info.image = null;
}
song.album.artist.info = data.artist_info;
// Set the artist image on the client side to the retrieved image from server.
if (data.artist_info.image) {
song.album.artist.image = data.artist_info.image;
}
// Convert the duration into i:s
if (data.album_info && data.album_info.tracks) {
2016-04-10 09:51:06 +00:00
each(data.album_info.tracks, track => track.fmtLength = secondsToHis(track.length));
}
// If the album cover is not in a nice form, don't use it.
if (data.album_info && typeof data.album_info.image !== 'string') {
data.album_info.image = null;
}
song.album.info = data.album_info;
2015-12-13 04:42:28 +00:00
// Set the album on the client side to the retrieved image from server.
if (data.album_info.cover) {
song.album.cover = data.album_info.cover;
}
2016-03-06 05:02:03 +00:00
song.infoRetrieved = true;
2015-12-13 04:42:28 +00:00
if (cb) {
cb();
}
});
},
2015-12-20 12:17:35 +00:00
/**
2016-01-07 09:03:38 +00:00
* Scrobble a song (using Last.fm).
*
2016-01-07 09:03:38 +00:00
* @param {Object} song
* @param {?Function} cb
2015-12-20 12:17:35 +00:00
*/
scrobble(song, cb = null) {
2016-03-18 04:45:12 +00:00
if (!window.useLastfm || !userStore.current.preferences.lastfm_session_key) {
2015-12-20 12:17:35 +00:00
return;
}
http.post(`${song.id}/scrobble/${song.playStartTime}`, () => {
if (cb) {
cb();
}
});
},
2016-03-05 09:01:12 +00:00
/**
* Update song data.
*
* @param {Array.<Object>} songs An array of song
* @param {Object} data
* @param {?Function} successCb
* @param {?Function} errorCb
*/
update(songs, data, successCb = null, errorCb = null) {
2016-03-18 04:45:12 +00:00
if (!userStore.current.is_admin) {
2016-03-05 09:01:12 +00:00
return;
}
http.put('songs', {
data,
2016-03-31 08:58:46 +00:00
songs: map(songs, 'id'),
2016-03-05 09:01:12 +00:00
}, response => {
2016-04-05 07:38:10 +00:00
each(response.data, song => this.syncUpdatedSong(song));
2016-03-05 09:01:12 +00:00
if (successCb) {
successCb();
}
}, () => {
if (errorCb) {
errorCb();
}
});
},
/**
* Sync an updated song into our current library.
*
* This is one of the most ugly functions I've written, if not the worst itself.
* Sorry, future me.
* Sorry guys.
* Forgive me.
*
* @param {Object} updatedSong The updated song, with albums and whatnot.
*
* @return {?Object} The updated song.
*/
syncUpdatedSong(updatedSong) {
// Cases:
// 1. Album doesn't change (and then, artist doesn't either)
// 2. Album changes (note that a new album might have been created) and
// 2.a. Artist remains the same.
// 2.b. Artist changes as well. Note that an artist might have been created.
// Find the original song,
2016-03-28 13:38:14 +00:00
const originalSong = this.byId(updatedSong.id);
2016-03-05 09:01:12 +00:00
if (!originalSong) {
return;
}
// and keep track of original album/artist.
2016-03-28 13:38:14 +00:00
const originalAlbumId = originalSong.album.id;
const originalArtistId = originalSong.album.artist.id;
2016-03-05 09:01:12 +00:00
2016-03-28 12:16:03 +00:00
// First, we update the title, lyrics, and track #
2016-03-05 09:01:12 +00:00
originalSong.title = updatedSong.title;
originalSong.lyrics = updatedSong.lyrics;
2016-03-28 12:16:03 +00:00
originalSong.track = updatedSong.track;
2016-03-05 09:01:12 +00:00
if (updatedSong.album.id === originalAlbumId) { // case 1
// Nothing to do
} else { // case 2
// First, remove it from its old album
albumStore.removeSongsFromAlbum(originalSong.album, originalSong);
2016-03-28 13:38:14 +00:00
const existingAlbum = albumStore.byId(updatedSong.album.id);
const newAlbumCreated = !existingAlbum;
2016-03-05 09:01:12 +00:00
if (!newAlbumCreated) {
// The song changed to an existing album. We now add it to such album.
albumStore.addSongsIntoAlbum(existingAlbum, originalSong);
} else {
// A new album was created. We:
// - Add the new album into our collection
// - Add the song into it
albumStore.addSongsIntoAlbum(updatedSong.album, originalSong);
2016-04-05 07:38:10 +00:00
albumStore.add(updatedSong.album);
2016-03-05 09:01:12 +00:00
}
if (updatedSong.album.artist.id === originalArtistId) { // case 2.a
// Same artist, but what if the album is new?
if (newAlbumCreated) {
artistStore.addAlbumsIntoArtist(artistStore.byId(originalArtistId), updatedSong.album);
}
} else { // case 2.b
// The artist changes.
2016-03-28 13:38:14 +00:00
const existingArtist = artistStore.byId(updatedSong.album.artist.id);
2016-03-05 09:01:12 +00:00
if (!existingArtist) {
// New artist created. We:
// - Add the album into it, because now it MUST BE a new album
// (there's no "new artist with existing album" in our system).
// - Add the new artist into our collection
artistStore.addAlbumsIntoArtist(updatedSong.album.artist, updatedSong.album);
2016-04-05 07:38:10 +00:00
artistStore.add(updatedSong.album.artist);
2016-03-05 09:01:12 +00:00
}
}
// As a last step, we purify our library of empty albums/artists.
if (albumStore.isAlbumEmpty(albumStore.byId(originalAlbumId))) {
albumStore.remove(albumStore.byId(originalAlbumId));
}
if (artistStore.isArtistEmpty(artistStore.byId(originalArtistId))) {
artistStore.remove(artistStore.byId(originalArtistId));
}
2016-03-06 05:02:03 +00:00
// Now we make sure the next call to info() get the refreshed, correct info.
originalSong.infoRetrieved = false;
2016-03-05 09:01:12 +00:00
}
2016-03-06 05:02:03 +00:00
return originalSong;
2016-03-05 09:01:12 +00:00
},
/**
* Get a song's playable source URL.
*
* @param {Object} song
*
* @return {string} The source URL, with JWT token appended.
*/
getSourceUrl(song) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`;
},
/**
* Get the last n recently played songs.
*
* @param {Number} n
*
* @return {Array.<Object>}
*/
getRecent(n = 10) {
return take(this.state.recent, n);
},
/**
* Get top n most-played songs.
*
* @param {Number} n
*
* @return {Array.<Object>}
*/
getMostPlayed(n = 10) {
2016-04-05 07:38:10 +00:00
const songs = take(orderBy(this.all, 'playCount', 'desc'), n);
// Remove those with playCount=0
remove(songs, song => !song.playCount);
return songs;
},
/**
* Called when the application is tore down.
* Reset stuff.
*/
teardown() {
this.state.recent = [];
},
2015-12-13 04:42:28 +00:00
};