2022-04-15 14:24:30 +00:00
|
|
|
import isMobile from 'ismobilejs'
|
2022-06-10 10:47:46 +00:00
|
|
|
import slugify from 'slugify'
|
|
|
|
import { orderBy, take, union } from 'lodash'
|
|
|
|
import { reactive, watch } from 'vue'
|
2022-07-04 13:24:02 +00:00
|
|
|
import { arrayify, eventBus, secondsToHis, use } from '@/utils'
|
2022-04-24 17:58:12 +00:00
|
|
|
import { authService, httpService } from '@/services'
|
2022-06-10 10:47:46 +00:00
|
|
|
import { albumStore, artistStore, commonStore, overviewStore, preferenceStore } from '@/stores'
|
2022-07-04 10:38:06 +00:00
|
|
|
import { Cache } from '@/services/cache'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
interface BroadcastSongData {
|
|
|
|
song: {
|
|
|
|
id: string
|
|
|
|
title: string
|
|
|
|
liked: boolean
|
|
|
|
playbackState: PlaybackState
|
|
|
|
album: {
|
2022-04-25 13:07:38 +00:00
|
|
|
id: number
|
2022-04-15 14:24:30 +00:00
|
|
|
name: string
|
|
|
|
cover: string
|
|
|
|
}
|
|
|
|
artist: {
|
2022-04-25 13:07:38 +00:00
|
|
|
id: number
|
2022-04-15 14:24:30 +00:00
|
|
|
name: string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SongUpdateResult {
|
|
|
|
songs: Song[]
|
|
|
|
artists: Artist[]
|
|
|
|
albums: Album[]
|
2022-06-10 10:47:46 +00:00
|
|
|
removed: {
|
|
|
|
albums: Pick<Album, 'id' | 'artist_id' | 'name' | 'cover' | 'created_at'>[]
|
|
|
|
artists: Pick<Artist, 'id' | 'name' | 'image' | 'created_at'>[]
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const songStore = {
|
2022-06-10 10:47:46 +00:00
|
|
|
vault: new Map<string, Song>(),
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-20 09:37:22 +00:00
|
|
|
state: reactive({
|
2022-06-10 10:47:46 +00:00
|
|
|
songs: [] as Song[]
|
2022-04-20 09:37:22 +00:00
|
|
|
}),
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the total duration of some songs.
|
|
|
|
*
|
|
|
|
* @param songs
|
|
|
|
* @param {Boolean} formatted Whether to convert the duration into H:i:s format
|
|
|
|
*/
|
2022-04-24 17:58:12 +00:00
|
|
|
getLength: (songs: Song[], formatted: boolean = false) => {
|
2022-04-15 14:24:30 +00:00
|
|
|
const duration = songs.reduce((length, song) => length + song.length, 0)
|
|
|
|
|
|
|
|
return formatted ? secondsToHis(duration) : duration
|
|
|
|
},
|
|
|
|
|
2022-04-24 17:58:12 +00:00
|
|
|
getFormattedLength (songs: Song[]) {
|
2022-04-20 09:37:22 +00:00
|
|
|
return String(this.getLength(songs, true))
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-04-24 17:58:12 +00:00
|
|
|
byId (id: string) {
|
2022-06-10 10:47:46 +00:00
|
|
|
return this.vault.get(id)
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-04-24 17:58:12 +00:00
|
|
|
byIds (ids: string[]) {
|
2022-04-15 14:24:30 +00:00
|
|
|
const songs = [] as Song[]
|
2022-06-10 10:47:46 +00:00
|
|
|
ids.forEach(id => use(this.byId(id), song => songs.push(song!)))
|
2022-04-15 14:24:30 +00:00
|
|
|
return songs
|
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
byAlbum (album: Album) {
|
|
|
|
return Array.from(this.vault.values()).filter(song => song.album_id === album.id)
|
|
|
|
},
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
/**
|
2022-06-10 10:47:46 +00:00
|
|
|
* Match a title to a song.
|
2022-04-15 14:24:30 +00:00
|
|
|
* Forget about Levenshtein distance, this implementation is good enough.
|
|
|
|
*/
|
2022-06-10 10:47:46 +00:00
|
|
|
match: (title: string, songs: Song[]) => {
|
2022-04-15 14:24:30 +00:00
|
|
|
title = slugify(title.toLowerCase())
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
for (const song of songs) {
|
2022-04-15 14:24:30 +00:00
|
|
|
if (slugify(song.title.toLowerCase()) === title) {
|
|
|
|
return song
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Increase a play count for a song.
|
|
|
|
*/
|
2022-04-24 17:58:12 +00:00
|
|
|
registerPlay: async (song: Song) => {
|
2022-04-24 08:50:45 +00:00
|
|
|
const interaction = await httpService.post<Interaction>('interaction/play', { song: song.id })
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
// Use the data from the server to make sure we don't miss a play from another device.
|
2022-06-10 10:47:46 +00:00
|
|
|
song.play_count = interaction.play_count
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
scrobble: async (song: Song) => await httpService.post(`${song.id}/scrobble`, { timestamp: song.play_start_time }),
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-24 17:58:12 +00:00
|
|
|
async update (songsToUpdate: Song[], data: any) {
|
2022-06-10 10:47:46 +00:00
|
|
|
const { songs, artists, albums, removed } = await httpService.put<SongUpdateResult>('songs', {
|
2022-04-15 14:24:30 +00:00
|
|
|
data,
|
|
|
|
songs: songsToUpdate.map(song => song.id)
|
|
|
|
})
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
this.syncWithVault(songs)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
albumStore.syncWithVault(albums)
|
|
|
|
artistStore.syncWithVault(artists)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
albumStore.removeByIds(removed.albums.map(album => album.id))
|
|
|
|
artistStore.removeByIds(removed.artists.map(artist => artist.id))
|
|
|
|
|
2022-07-04 13:24:02 +00:00
|
|
|
eventBus.emit('SONGS_UPDATED')
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
overviewStore.refresh()
|
|
|
|
},
|
|
|
|
|
|
|
|
getSourceUrl: (song: Song) => {
|
|
|
|
return isMobile.any && preferenceStore.transcodeOnMobile
|
|
|
|
? `${commonStore.state.cdn_url}play/${song.id}/1/128?api_token=${authService.getToken()}`
|
|
|
|
: `${commonStore.state.cdn_url}play/${song.id}?api_token=${authService.getToken()}`
|
|
|
|
},
|
|
|
|
|
|
|
|
getShareableUrl: (song: Song) => `${window.BASE_URL}#!/song/${song.id}`,
|
|
|
|
|
|
|
|
generateDataToBroadcast: (song: Song): BroadcastSongData => ({
|
|
|
|
song: {
|
|
|
|
id: song.id,
|
|
|
|
title: song.title,
|
|
|
|
liked: song.liked,
|
|
|
|
playbackState: song.playback_state || 'Stopped',
|
|
|
|
album: {
|
|
|
|
id: song.album_id,
|
|
|
|
name: song.album_name,
|
|
|
|
cover: song.album_cover
|
|
|
|
},
|
|
|
|
artist: {
|
|
|
|
id: song.artist_id,
|
|
|
|
name: song.artist_name
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
2022-06-10 10:47:46 +00:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
|
|
|
syncWithVault (songs: Song | Song[]) {
|
|
|
|
return arrayify(songs).map(song => {
|
|
|
|
let local = this.byId(song.id)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
if (local) {
|
|
|
|
Object.assign(local, song)
|
|
|
|
} else {
|
|
|
|
song.playback_state = 'Stopped'
|
|
|
|
local = reactive(song)
|
|
|
|
this.trackPlayCount(local!)
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
this.vault.set(song.id, local)
|
|
|
|
return local
|
2022-04-15 14:24:30 +00:00
|
|
|
})
|
2022-06-10 10:47:46 +00:00
|
|
|
},
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
trackPlayCount: (song: Song) => {
|
|
|
|
watch(() => song.play_count, (newCount, oldCount) => {
|
|
|
|
const album = albumStore.byId(song.album_id)
|
|
|
|
album && (album.play_count += (newCount - oldCount))
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
const artist = artistStore.byId(song.artist_id)
|
|
|
|
artist && (artist.play_count += (newCount - oldCount))
|
|
|
|
|
|
|
|
if (song.album_artist_id !== song.artist_id) {
|
|
|
|
const albumArtist = artistStore.byId(song.album_artist_id)
|
|
|
|
albumArtist && (albumArtist.play_count += (newCount - oldCount))
|
|
|
|
}
|
|
|
|
|
|
|
|
overviewStore.refresh()
|
|
|
|
})
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
async fetchForAlbum (album: Album) {
|
2022-07-04 14:18:41 +00:00
|
|
|
return await Cache.resolve<Song[]>(
|
2022-07-04 10:38:06 +00:00
|
|
|
[`album.songs`, album.id],
|
|
|
|
async () => this.syncWithVault(await httpService.get<Song[]>(`albums/${album.id}/songs`))
|
|
|
|
)
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
async fetchForArtist (artist: Artist) {
|
2022-07-04 14:18:41 +00:00
|
|
|
return await Cache.resolve<Song[]>(
|
2022-07-04 10:38:06 +00:00
|
|
|
['artist.songs', artist.id],
|
|
|
|
async () => this.syncWithVault(await httpService.get<Song[]>(`artists/${artist.id}/songs`))
|
|
|
|
)
|
2022-06-10 10:47:46 +00:00
|
|
|
},
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
async fetchForPlaylist (playlist: Playlist) {
|
2022-07-04 14:18:41 +00:00
|
|
|
return await Cache.resolve<Song[]>(
|
2022-07-04 10:38:06 +00:00
|
|
|
[`playlist.songs`, playlist.id],
|
|
|
|
async () => this.syncWithVault(await httpService.get<Song[]>(`playlists/${playlist.id}/songs`))
|
|
|
|
)
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
async fetch (sortField: SongListSortField, sortOrder: SortOrder, page: number) {
|
|
|
|
const resource = await httpService.get<PaginatorResource>(
|
|
|
|
`songs?page=${page}&sort=${sortField}&order=${sortOrder}`
|
|
|
|
)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
this.state.songs = union(this.state.songs, this.syncWithVault(resource.data))
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
return resource.links.next ? ++resource.meta.current_page : null
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
getMostPlayed (count: number) {
|
|
|
|
return take(orderBy(Array.from(this.vault.values()), 'play_count', 'desc'), count)
|
2022-04-15 14:24:30 +00:00
|
|
|
},
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
getRecentlyAdded (count: number) {
|
|
|
|
return take(orderBy(Array.from(this.vault.values()), 'created_at', 'desc'), count)
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|