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

331 lines
8.1 KiB
JavaScript
Raw Normal View History

2016-11-26 03:25:35 +00:00
import Vue from 'vue'
import slugify from 'slugify'
2017-04-24 06:38:25 +00:00
import { assign, without, map, take, remove, orderBy, each, unionBy, compact } from 'lodash'
import isMobile from 'ismobilejs'
2015-12-13 04:42:28 +00:00
2016-12-19 05:37:30 +00:00
import { secondsToHis, alerts, pluralize } from '../utils'
2016-11-26 03:25:35 +00:00
import { http, ls } from '../services'
import { sharedStore, favoriteStore, albumStore, artistStore, preferenceStore } from '.'
2016-11-26 03:25:35 +00:00
import stub from '../stubs/song'
2015-12-13 04:42:28 +00:00
2016-06-25 10:15:57 +00:00
export const songStore = {
2016-06-25 16:05:24 +00:00
stub,
albums: [],
cache: {},
2015-12-13 04:42:28 +00:00
2016-06-25 16:05:24 +00:00
state: {
2015-12-13 04:42:28 +00:00
/**
2016-06-25 16:05:24 +00:00
* All songs in the store
*
2016-06-25 16:05:24 +00:00
* @type {Array}
2015-12-13 04:42:28 +00:00
*/
2016-06-25 16:05:24 +00:00
songs: [stub],
2015-12-13 04:42:28 +00:00
2015-12-20 12:17:35 +00:00
/**
* The recently played songs **on the current machine**
*
2016-06-25 16:05:24 +00:00
* @type {Array}
2015-12-20 12:17:35 +00:00
*/
recentlyPlayed: []
2016-06-25 16:05:24 +00:00
},
/**
* Init the store.
*
2017-04-23 16:01:02 +00:00
* @param {Array.<Object>} songs The array of song objects
2016-06-25 16:05:24 +00:00
*/
2017-04-23 16:01:02 +00:00
init (songs) {
this.all = songs
each(this.all, song => this.setupSong(song))
this.state.recentlyPlayed = this.gatherRecentlyPlayedFromLocalStorage()
2016-06-25 16:05:24 +00:00
},
2017-04-23 16:01:02 +00:00
setupSong (song) {
2016-11-26 03:25:35 +00:00
song.fmtLength = secondsToHis(song.length)
2016-06-25 16:05:24 +00:00
2017-04-23 16:01:02 +00:00
const album = albumStore.byId(song.album_id)
const artist = artistStore.byId(song.artist_id)
2017-04-23 16:01:02 +00:00
2016-06-25 16:05:24 +00:00
// Manually set these additional properties to be reactive
2017-04-23 16:01:02 +00:00
Vue.set(song, 'playCount', song.playCount || 0)
2016-11-26 03:25:35 +00:00
Vue.set(song, 'album', album)
2017-04-23 16:01:02 +00:00
Vue.set(song, 'artist', artist)
Vue.set(song, 'liked', song.liked || false)
Vue.set(song, 'lyrics', song.lyrics || null)
Vue.set(song, 'playbackState', song.playbackState || 'stopped')
artist.songs = unionBy(artist.songs || [], [song], 'id')
album.songs = unionBy(album.songs || [], [song], 'id')
// now if the song is part of a compilation album, the album must be added
// into its artist as well
if (album.is_compilation) {
artist.albums = unionBy(artist.albums, [album], 'id')
2016-06-25 16:05:24 +00:00
}
// Cache the song, so that byId() is faster
this.cache[song.id] = song
2017-04-23 16:01:02 +00:00
return song
2016-06-25 16:05:24 +00:00
},
/**
* Initializes the interaction (like/play count) information.
*
* @param {Array.<Object>} interactions The array of interactions of the current user
*/
2016-11-26 03:25:35 +00:00
initInteractions (interactions) {
favoriteStore.clear()
2016-06-25 16:05:24 +00:00
each(interactions, interaction => {
2016-11-26 03:25:35 +00:00
const song = this.byId(interaction.song_id)
2016-06-25 16:05:24 +00:00
if (!song) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
song.liked = interaction.liked
song.playCount = interaction.play_count
song.album.playCount += song.playCount
song.artist.playCount += song.playCount
2016-06-25 16:05:24 +00:00
2017-05-07 17:41:12 +00:00
song.liked && favoriteStore.add(song)
2016-11-26 03:25:35 +00:00
})
2016-06-25 16:05:24 +00:00
},
/**
* Get the total duration of some songs.
*
* @param {Array.<Object>} songs
* @param {Boolean} toHis Whether to convert the duration into H:i:s format
*
* @return {Float|String}
*/
2016-11-26 03:25:35 +00:00
getLength (songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return toHis ? secondsToHis(duration) : duration
2016-06-25 16:05:24 +00:00
},
/**
* Get all songs.
*
* @return {Array.<Object>}
*/
2016-11-26 03:25:35 +00:00
get all () {
return this.state.songs
2016-06-25 16:05:24 +00:00
},
/**
* Set all songs.
*
* @param {Array.<Object>} value
*/
2016-11-26 03:25:35 +00:00
set all (value) {
this.state.songs = value
2016-06-25 16:05:24 +00:00
},
/**
* Get a song by its ID.
*
* @param {String} id
*
* @return {Object}
*/
2016-11-26 03:25:35 +00:00
byId (id) {
return this.cache[id]
2016-06-25 16:05:24 +00:00
},
/**
* Get songs by their IDs.
*
* @param {Array.<String>} ids
*
* @return {Array.<Object>}
*/
2016-11-26 03:25:35 +00:00
byIds (ids) {
return ids.map(id => this.byId(id))
2016-06-25 16:05:24 +00:00
},
/**
* Guess a song by its title and album.
* Forget about Levenshtein distance, this implementation is good enough.
*
* @param {string} title
* @param {Object} album
*
2016-12-01 13:42:00 +00:00
* @return {Object|false}
*/
guess (title, album) {
title = slugify(title.toLowerCase())
let found = false
2017-01-15 16:20:55 +00:00
each(album.songs, song => {
if (slugify(song.title.toLowerCase()) === title) {
found = song
}
})
return found
},
2016-06-25 16:05:24 +00:00
/**
* Increase a play count for a song.
*
* @param {Object} song
*/
2016-11-26 03:25:35 +00:00
registerPlay (song) {
2016-06-27 06:11:35 +00:00
return new Promise((resolve, reject) => {
2016-11-26 03:25:35 +00:00
const oldCount = song.playCount
2016-06-27 06:11:35 +00:00
2017-01-12 16:50:00 +00:00
http.post('interaction/play', { song: song.id }, ({ data }) => {
2016-06-27 06:11:35 +00:00
// Use the data from the server to make sure we don't miss a play from another device.
2017-01-12 16:50:00 +00:00
song.playCount = data.play_count
2016-11-26 03:25:35 +00:00
song.album.playCount += song.playCount - oldCount
song.artist.playCount += song.playCount - oldCount
2016-06-27 06:11:35 +00:00
2017-01-12 16:50:00 +00:00
resolve(data)
2016-12-20 15:44:47 +00:00
}, error => reject(error))
2016-11-26 03:25:35 +00:00
})
2016-06-25 16:05:24 +00:00
},
/**
* Add a song into the "recently played" list.
*
* @param {Object}
*/
addRecentlyPlayed (song) {
2016-06-25 16:05:24 +00:00
// First we make sure that there's no duplicate.
this.state.recentlyPlayed = without(this.state.recentlyPlayed, song)
2016-06-25 16:05:24 +00:00
// Then we prepend the song into the list.
this.state.recentlyPlayed.unshift(song)
// Only take first 7 songs
this.state.recentlyPlayed.splice(7)
// Save to local storage as well
preferenceStore.set('recent-songs', map(this.state.recentlyPlayed, 'id'))
2016-06-25 16:05:24 +00:00
},
/**
* Scrobble a song (using Last.fm).
*
* @param {Object} song
*/
2016-11-26 03:25:35 +00:00
scrobble (song) {
2016-06-27 06:11:35 +00:00
return new Promise((resolve, reject) => {
2017-01-12 16:50:00 +00:00
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, ({ data }) => {
resolve(data)
2016-12-20 15:44:47 +00:00
}, error => reject(error))
2016-06-27 06:11:35 +00:00
})
2016-06-25 16:05:24 +00:00
},
/**
* Update song data.
*
* @param {Array.<Object>} songs An array of song
* @param {Object} data
*/
2016-11-26 03:25:35 +00:00
update (songs, data) {
2016-06-27 06:11:35 +00:00
return new Promise((resolve, reject) => {
http.put('songs', {
data,
2016-11-26 03:25:35 +00:00
songs: map(songs, 'id')
2017-04-23 16:01:02 +00:00
}, ({ data: { songs, artists, albums }}) => {
2017-04-24 06:38:25 +00:00
// Add the artist and album into stores if they're new
each(artists, artist => !artistStore.byId(artist.id) && artistStore.add(artist))
each(albums, album => !albumStore.byId(album.id) && albumStore.add(album))
2017-04-23 16:01:02 +00:00
each(songs, song => {
const originalSong = this.byId(song.id)
if (originalSong.album_id !== song.album_id) {
// album has been changed. Remove the song from its old album.
originalSong.album.songs = without(originalSong.album.songs, originalSong)
}
if (originalSong.artist_id !== song.artist_id) {
2017-04-23 16:01:02 +00:00
// artist has been changed. Remove the song from its old artist
originalSong.artist.songs = without(originalSong.artist.songs, originalSong)
}
assign(originalSong, song)
// re-setup the song
this.setupSong(originalSong)
})
2017-04-24 06:38:25 +00:00
artistStore.compact()
albumStore.compact()
2017-04-23 16:01:02 +00:00
2016-12-19 05:37:30 +00:00
alerts.success(`Updated ${pluralize(songs.length, 'song')}.`)
2016-11-26 03:25:35 +00:00
resolve(songs)
2016-12-20 15:44:47 +00:00
}, error => reject(error))
2016-11-26 03:25:35 +00:00
})
2016-06-25 16:05:24 +00:00
},
/**
* Get a song's playable source URL.
*
* @param {Object} song
*
* @return {string} The source URL, with JWT token appended.
*/
2016-11-26 03:25:35 +00:00
getSourceUrl (song) {
if (isMobile.any && preferenceStore.transcodeOnMobile) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play/1/128?jwt-token=${ls.get('jwt-token')}`
}
2016-11-26 03:25:35 +00:00
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`
2016-06-25 16:05:24 +00:00
},
2016-07-07 13:54:20 +00:00
/**
* Get a song's shareable URL.
* Visiting this URL will automatically queue the song and play it.
*
* @param {Object} song
*
* @return {string}
*/
2016-11-26 03:25:35 +00:00
getShareableUrl (song) {
return `${window.location.origin}/#!/song/${song.id}`
2016-07-07 13:54:20 +00:00
},
2016-06-25 16:05:24 +00:00
/**
* The recently played songs.
* @return {Array.<Object>}
*/
get recentlyPlayed () {
return this.state.recentlyPlayed
},
/**
* Gather the recently played songs from local storage.
2016-06-25 16:05:24 +00:00
* @return {Array.<Object>}
*/
gatherRecentlyPlayedFromLocalStorage () {
return compact(this.byIds(preferenceStore.get('recent-songs') || []))
2016-06-25 16:05:24 +00:00
},
/**
* Get top n most-played songs.
*
* @param {Number} n
*
* @return {Array.<Object>}
*/
2016-11-26 03:25:35 +00:00
getMostPlayed (n = 10) {
const songs = take(orderBy(this.all, 'playCount', 'desc'), n)
2016-06-25 16:05:24 +00:00
// Remove those with playCount=0
2016-11-26 03:25:35 +00:00
remove(songs, song => !song.playCount)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return songs
2016-06-25 16:05:24 +00:00
},
/**
* Get n most recently added songs.
* @param {Number} n
* @return {Array.<Object>}
*/
2016-11-26 03:25:35 +00:00
getRecentlyAdded (n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n)
}
}