import { AlbumListItem, AlbumWithSongs, Artist, ArtistInfo, HomeLists, mapAlbumID3toAlbumListItem, mapAlbumID3WithSongstoAlbunWithSongs, mapArtistID3toArtist, mapArtistInfo, mapChildToSong, mapPlaylistListItem, mapPlaylistWithSongs, PlaylistListItem, PlaylistWithSongs, SearchResults, } from '@app/models/music' import { Store } from '@app/state/store' import { GetAlbumList2Type, StarParams } from '@app/subsonic/params' import produce from 'immer' import { GetState, SetState } from 'zustand' export type MusicSlice = { // // family-style state // artistInfo: { [id: string]: ArtistInfo } fetchArtistInfo: (id: string) => Promise albumsWithSongs: { [id: string]: AlbumWithSongs } fetchAlbumWithSongs: (id: string) => Promise playlistsWithSongs: { [id: string]: PlaylistWithSongs } fetchPlaylistWithSongs: (id: string) => Promise // // lists-style state // artists: Artist[] artistsUpdating: boolean fetchArtists: () => Promise playlists: PlaylistListItem[] playlistsUpdating: boolean fetchPlaylists: () => Promise albums: AlbumListItem[] albumsUpdating: boolean fetchAlbums: (size?: number, offset?: number) => Promise searchResults: SearchResults searchResultsUpdating: boolean fetchSearchResults: (query: string) => Promise clearSearchResults: () => void homeLists: HomeLists homeListsUpdating: boolean fetchHomeLists: () => Promise clearHomeLists: () => void starredSongs: { [id: string]: boolean } starredAlbums: { [id: string]: boolean } starredArtists: { [id: string]: boolean } starItem: (id: string, type: string, unstar?: boolean) => Promise } export const selectMusic = { fetchArtistInfo: (state: Store) => state.fetchArtistInfo, fetchAlbumWithSongs: (state: Store) => state.fetchAlbumWithSongs, fetchPlaylistWithSongs: (state: Store) => state.fetchPlaylistWithSongs, artists: (store: MusicSlice) => store.artists, artistsUpdating: (store: MusicSlice) => store.artistsUpdating, fetchArtists: (store: MusicSlice) => store.fetchArtists, playlists: (store: MusicSlice) => store.playlists, playlistsUpdating: (store: MusicSlice) => store.playlistsUpdating, fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists, albums: (store: MusicSlice) => store.albums, albumsUpdating: (store: MusicSlice) => store.albumsUpdating, fetchAlbums: (store: MusicSlice) => store.fetchAlbums, searchResults: (store: MusicSlice) => store.searchResults, searchResultsUpdating: (store: MusicSlice) => store.searchResultsUpdating, fetchSearchResults: (store: MusicSlice) => store.fetchSearchResults, clearSearchResults: (store: MusicSlice) => store.clearSearchResults, homeLists: (store: MusicSlice) => store.homeLists, homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating, fetchHomeLists: (store: MusicSlice) => store.fetchHomeLists, clearHomeLists: (store: MusicSlice) => store.clearHomeLists, starItem: (store: MusicSlice) => store.starItem, } function reduceStarred( starredType: { [id: string]: boolean }, items: { id: string; starred?: Date | boolean }[], ): { [id: string]: boolean } { return { ...starredType, ...items.reduce((acc, val) => { acc[val.id] = !!val.starred return acc }, {} as { [id: string]: boolean }), } } export const createMusicSlice = (set: SetState, get: GetState): MusicSlice => ({ artistInfo: {}, fetchArtistInfo: async id => { const client = get().client if (!client) { return undefined } try { const [artistResponse, artistInfoResponse] = await Promise.all([ client.getArtist({ id }), client.getArtistInfo2({ id }), ]) const topSongsResponse = await client.getTopSongs({ artist: artistResponse.data.artist.name, count: 50 }) const artistInfo = mapArtistInfo( artistResponse.data, artistInfoResponse.data.artistInfo, topSongsResponse.data.songs, client, ) set( produce(state => { state.artistInfo[id] = artistInfo state.starredSongs = reduceStarred(state.starredSongs, artistInfo.topSongs) state.starredArtists = reduceStarred(state.starredArtists, [artistInfo]) state.starredAlbums = reduceStarred(state.starredAlbums, artistInfo.albums) }), ) return artistInfo } catch { return undefined } }, albumsWithSongs: {}, fetchAlbumWithSongs: async id => { const client = get().client if (!client) { return undefined } try { const response = await client.getAlbum({ id }) const album = mapAlbumID3WithSongstoAlbunWithSongs(response.data.album, response.data.songs, client) set( produce(state => { state.albumsWithSongs[id] = album state.starredSongs = reduceStarred(state.starredSongs, album.songs) state.starredAlbums = reduceStarred(state.starredAlbums, [album]) }), ) return album } catch { return undefined } }, playlistsWithSongs: {}, fetchPlaylistWithSongs: async id => { const client = get().client if (!client) { return undefined } try { const response = await client.getPlaylist({ id }) const playlist = mapPlaylistWithSongs(response.data.playlist, client) set( produce(state => { state.playlistsWithSongs[id] = playlist state.starredSongs = reduceStarred(state.starredSongs, playlist.songs) }), ) return playlist } catch { return undefined } }, artists: [], artistsUpdating: false, fetchArtists: async () => { const client = get().client if (!client) { return } if (get().artistsUpdating) { return } set({ artistsUpdating: true }) try { const response = await client.getArtists() set( produce(state => { state.artists = response.data.artists.map(mapArtistID3toArtist) state.starredArtists = reduceStarred(state.starredArtists, state.artists) }), ) } finally { set({ artistsUpdating: false }) } }, playlists: [], playlistsUpdating: false, fetchPlaylists: async () => { const client = get().client if (!client) { return } if (get().playlistsUpdating) { return } set({ playlistsUpdating: true }) try { const response = await client.getPlaylists() set({ playlists: response.data.playlists.map(mapPlaylistListItem) }) } finally { set({ playlistsUpdating: false }) } }, albums: [], albumsUpdating: false, fetchAlbums: async (size = 500, offset = 0) => { const client = get().client if (!client) { return } if (get().albumsUpdating) { return } set({ albumsUpdating: true }) try { const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset }) set( produce(state => { state.albums = response.data.albums.map(mapAlbumID3toAlbumListItem) state.starredAlbums = reduceStarred(state.starredAlbums, state.albums) }), ) } finally { set({ albumsUpdating: false }) } }, searchResults: { artists: [], albums: [], songs: [], }, searchResultsUpdating: false, fetchSearchResults: async query => { if (query.length < 2) { return } const client = get().client if (!client) { return } if (get().searchResultsUpdating) { return } set({ searchResultsUpdating: true }) try { const response = await client.search3({ query }) set( produce(state => { state.searchResults = { artists: response.data.artists.map(mapArtistID3toArtist), albums: response.data.albums.map(mapAlbumID3toAlbumListItem), songs: response.data.songs.map(a => mapChildToSong(a, client)), } state.starredSongs = reduceStarred(state.starredSongs, state.searchResults.songs) state.starredArtists = reduceStarred(state.starredArtists, state.searchResults.artists) state.starredAlbums = reduceStarred(state.starredAlbums, state.searchResults.albums) }), ) } finally { set({ searchResultsUpdating: false }) } }, clearSearchResults: () => { set({ searchResults: { artists: [], albums: [], songs: [], }, }) }, homeLists: {}, homeListsUpdating: false, fetchHomeLists: async () => { const client = get().client if (!client) { return } if (get().homeListsUpdating) { return } set({ homeListsUpdating: true }) const types = get().settings.home.lists try { const promises: Promise[] = [] for (const type of types) { promises.push( client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => { set( produce(state => { state.homeLists[type] = response.data.albums.map(mapAlbumID3toAlbumListItem) state.starredAlbums = reduceStarred(state.starredAlbums, state.homeLists[type]) }), ) }), ) } await Promise.all(promises) } finally { set({ homeListsUpdating: false }) } }, clearHomeLists: () => { set({ homeLists: {} }) }, starredSongs: {}, starredAlbums: {}, starredArtists: {}, starItem: async (id, type, unstar = false) => { const client = get().client if (!client) { return } let params: StarParams let setStarred: (starred: boolean) => void switch (type) { case 'song': params = { id } setStarred = starred => { set( produce(state => { state.starredSongs = reduceStarred(state.starredSongs, [{ id, starred }]) }), ) } break case 'album': params = { albumId: id } setStarred = starred => { set( produce(state => { state.starredAlbums = reduceStarred(state.starredAlbums, [{ id, starred }]) }), ) } break case 'artist': params = { artistId: id } setStarred = starred => { set( produce(state => { state.starredArtists = reduceStarred(state.starredArtists, [{ id, starred }]) }), ) } break default: return } try { setStarred(!unstar) if (unstar) { await client.unstar(params) } else { await client.star(params) } } catch { setStarred(unstar) } }, })