koel/resources/assets/js/stores/songStore.ts

198 lines
5.7 KiB
TypeScript
Raw Normal View History

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 { merge, orderBy, sumBy, take, unionBy, uniqBy } from 'lodash'
2022-07-24 11:47:18 +00:00
import { reactive, UnwrapNestedRefs, watch } from 'vue'
import { arrayify, logger, secondsToHis, use } from '@/utils'
import { authService, cache, http } from '@/services'
import { albumStore, artistStore, commonStore, overviewStore, playlistStore, preferenceStore } from '@/stores'
2022-04-15 14:24:30 +00:00
2022-07-24 11:47:18 +00:00
export type SongUpdateData = {
title?: string
artist_name?: string
album_name?: string
album_artist_name?: string
track?: number | null
disc?: number | null
lyrics?: string
year?: number | null
genre?: string
2022-04-15 14:24:30 +00:00
}
2022-07-24 11:47:18 +00:00
export interface SongUpdateResult {
2022-04-15 14:24:30 +00:00
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-07-24 11:47:18 +00:00
vault: new Map<string, UnwrapNestedRefs<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
2022-07-24 11:47:18 +00:00
getFormattedLength: (songs: Song | Song[]) => secondsToHis(sumBy(arrayify(songs), 'length')),
2022-04-15 14:24:30 +00:00
byId (id: string) {
const song = this.vault.get(id)
return song?.deleted ? undefined : song
2022-04-15 14:24:30 +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-07-05 21:43:35 +00:00
async resolve (id: string) {
2022-07-24 11:47:18 +00:00
let song = this.byId(id)
2022-07-05 21:43:35 +00:00
2022-07-24 11:47:18 +00:00
if (!song) {
try {
song = this.syncWithVault(await http.get<Song>(`songs/${id}`))[0]
2022-07-24 11:47:18 +00:00
} catch (e) {
logger.error(e)
}
2022-07-05 21:43:35 +00:00
}
2022-07-24 11:47:18 +00:00
return song
2022-07-05 21:43:35 +00:00
},
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.
*/
registerPlay: async (song: Song) => {
const interaction = await http.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
},
scrobble: async (song: Song) => await http.post(`songs/${song.id}/scrobble`, {
2022-07-24 11:47:18 +00:00
timestamp: song.play_start_time
}),
2022-04-15 14:24:30 +00:00
2022-07-24 11:47:18 +00:00
async update (songsToUpdate: Song[], data: SongUpdateData) {
const { songs, artists, albums, removed } = await http.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))
},
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}`,
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) {
2022-07-24 11:47:18 +00:00
merge(local, song)
2022-06-10 10:47:46 +00:00
} else {
local = reactive(song)
2022-07-24 11:47:18 +00:00
local.playback_state = 'Stopped'
this.watchPlayCount(local)
2022-07-24 11:47:18 +00:00
this.vault.set(local.id, local)
2022-04-15 14:24:30 +00:00
}
2022-06-10 10:47:46 +00:00
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
watchPlayCount: (song: UnwrapNestedRefs<Song>) => {
watch(() => song.play_count, () => overviewStore.refresh())
2022-04-15 14:24:30 +00:00
},
async cacheable (key: any, fetcher: Promise<Song[]>) {
const songs = await cache.remember<Song[]>(key, async () => this.syncWithVault(await fetcher))
return songs.filter(song => !song.deleted)
},
2022-07-30 15:08:20 +00:00
async fetchForAlbum (album: Album | number) {
const id = typeof album === 'number' ? album : album.id
return await this.cacheable(['album.songs', id], http.get<Song[]>(`albums/${id}/songs`))
2022-04-15 14:24:30 +00:00
},
2022-07-30 15:08:20 +00:00
async fetchForArtist (artist: Artist | number) {
const id = typeof artist === 'number' ? artist : artist.id
return await this.cacheable(['artist.songs', id], http.get<Song[]>(`artists/${id}/songs`))
2022-06-10 10:47:46 +00:00
},
2022-04-15 14:24:30 +00:00
async fetchForPlaylist (playlist: Playlist | number) {
const id = typeof playlist === 'number' ? playlist : playlist.id
return await this.cacheable(['playlist.songs', id], http.get<Song[]>(`playlists/${id}/songs`))
2022-04-15 14:24:30 +00:00
},
async fetchForPlaylistFolder (folder: PlaylistFolder) {
const songs: Song[] = []
for await (const playlist of playlistStore.byFolder(folder)) {
songs.push(...await songStore.fetchForPlaylist(playlist))
}
return uniqBy(songs, 'id')
},
2022-07-24 11:47:18 +00:00
async paginate (sortField: SongListSortField, sortOrder: SortOrder, page: number) {
const resource = await http.get<PaginatorResource>(
2022-06-10 10:47:46 +00:00
`songs?page=${page}&sort=${sortField}&order=${sortOrder}`
)
2022-04-15 14:24:30 +00:00
2022-07-22 21:56:13 +00:00
this.state.songs = unionBy(this.state.songs, this.syncWithVault(resource.data), 'id')
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()).filter(song => !song.deleted), 'play_count', 'desc'), count)
2022-04-15 14:24:30 +00:00
},
async deleteFromFilesystem (songs: Song[]) {
const ids = songs.map(song => {
// Whenever a vault sync is requested (e.g. upon playlist/album/artist fetching)
// songs marked as "deleted" will be excluded.
song.deleted = true
return song.id
})
await http.delete('songs', { songs: ids })
2022-06-10 10:47:46 +00:00
}
2022-04-15 14:24:30 +00:00
}