reworked fetchAlbumList to remove ui state

refactored home screen to use new method
i broke playing songs somehow, JS thread goes into a loop
This commit is contained in:
austinried
2022-03-18 13:04:20 +09:00
parent 76306f1558
commit 98ef0d1d44
8 changed files with 381 additions and 160 deletions

View File

@@ -48,7 +48,7 @@ export const useFetchList2 = (fetchList: () => Promise<void>, resetList: () => v
} }
export const useFetchPaginatedList = <T>( export const useFetchPaginatedList = <T>(
fetchList: (size?: number, offset?: number) => Promise<T[]>, fetchList: (size: number, offset: number) => Promise<T[]>,
pageSize: number, pageSize: number,
) => { ) => {
const [list, setList] = useState<T[]>([]) const [list, setList] = useState<T[]>([])
@@ -94,32 +94,3 @@ export const useFetchPaginatedList = <T>(
return { list, refreshing, refresh, reset, fetchNextPage } return { list, refreshing, refresh, reset, fetchNextPage }
} }
export const useFetchPaginatedList2 = (fetchNextListPage: () => Promise<void>, resetList: () => void) => {
const [refreshing, setRefreshing] = useState(false)
const refresh = useCallback(async () => {
setRefreshing(true)
resetList()
await fetchNextListPage()
setRefreshing(false)
}, [fetchNextListPage, resetList])
useActiveServerRefresh(
useCallback(async () => {
await refresh()
}, [refresh]),
)
const fetchNextPage = useCallback(async () => {
setRefreshing(true)
await fetchNextListPage()
setRefreshing(false)
}, [fetchNextListPage])
return { refreshing, refresh, fetchNextPage }
}

View File

@@ -63,7 +63,7 @@ export interface Song {
duration?: number duration?: number
starred?: Date starred?: Date
streamUri: string // streamUri: string
coverArt?: string coverArt?: string
} }

View File

@@ -3,17 +3,21 @@ import CoverArt from '@app/components/CoverArt'
import GradientScrollView from '@app/components/GradientScrollView' import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header' import Header from '@app/components/Header'
import NothingHere from '@app/components/NothingHere' import NothingHere from '@app/components/NothingHere'
import { useFetchPaginatedList } from '@app/hooks/list'
import { useActiveServerRefresh } from '@app/hooks/server' import { useActiveServerRefresh } from '@app/hooks/server'
import { AlbumListItem } from '@app/models/music' import { AlbumListItem } from '@app/models/music'
import { Album } from '@app/state/library'
import { selectMusic } from '@app/state/music' import { selectMusic } from '@app/state/music'
import { selectSettings } from '@app/state/settings' import { selectSettings } from '@app/state/settings'
import { useStore } from '@app/state/store' import { Store, useStore } from '@app/state/store'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { GetAlbumListType } from '@app/subsonic/params' import { GetAlbumList2Params, GetAlbumList2TypeBase, GetAlbumListType } from '@app/subsonic/params'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React, { useCallback } from 'react' import produce from 'immer'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native' import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'
import create from 'zustand'
const titles: { [key in GetAlbumListType]?: string } = { const titles: { [key in GetAlbumListType]?: string } = {
recent: 'Recently Played', recent: 'Recently Played',
@@ -49,9 +53,11 @@ const AlbumItem = React.memo<{
}) })
const Category = React.memo<{ const Category = React.memo<{
name?: string type: string
data: AlbumListItem[] }>(({ type }) => {
}>(({ name, data }) => { const list = useHomeStore(useCallback(store => store.lists[type] || [], [type]))
const albums = useStore(useCallback(store => list.map(id => store.entities.albums[id]), [list]))
const Albums = () => ( const Albums = () => (
<ScrollView <ScrollView
horizontal={true} horizontal={true}
@@ -59,7 +65,7 @@ const Category = React.memo<{
overScrollMode={'never'} overScrollMode={'never'}
style={styles.artScroll} style={styles.artScroll}
contentContainerStyle={styles.artScrollContent}> contentContainerStyle={styles.artScrollContent}>
{data.map(album => ( {albums.map(album => (
<AlbumItem key={album.id} album={album} /> <AlbumItem key={album.id} album={album} />
))} ))}
</ScrollView> </ScrollView>
@@ -73,24 +79,55 @@ const Category = React.memo<{
return ( return (
<View style={styles.category}> <View style={styles.category}>
<Header style={styles.header}>{name}</Header> <Header style={styles.header}>{titles[type as GetAlbumListType] || ''}</Header>
{data.length > 0 ? <Albums /> : <Nothing />} {albums.length > 0 ? <Albums /> : <Nothing />}
</View> </View>
) )
}) })
interface HomeState {
lists: { [type: string]: string[] }
setList: (type: string, list: string[]) => void
}
const useHomeStore = create<HomeState>((set, get) => ({
lists: {},
setList: (type, list) => {
set(
produce<HomeState>(state => {
state.lists[type] = list
}),
)
},
}))
const Home = () => { const Home = () => {
const [refreshing, setRefreshing] = useState(false)
const types = useStore(selectSettings.homeLists) const types = useStore(selectSettings.homeLists)
const lists = useStore(selectMusic.homeLists) const fetchAlbumList = useStore(store => store.fetchLibraryAlbumList)
const updating = useStore(selectMusic.homeListsUpdating) const setList = useHomeStore(store => store.setList)
const update = useStore(selectMusic.fetchHomeLists)
const clear = useStore(selectMusic.clearHomeLists) const refresh = useCallback(async () => {
setRefreshing(true)
await Promise.all(
types.map(async type => {
console.log('fetch', type)
const ids = await fetchAlbumList({ type: type as GetAlbumList2TypeBase, size: 20, offset: 0 })
console.log('set', type)
setList(type, ids)
}),
)
setRefreshing(false)
}, [fetchAlbumList, setList, types])
useActiveServerRefresh( useActiveServerRefresh(
useCallback(() => { useCallback(() => {
clear() types.forEach(type => setList(type, []))
update() refresh()
}, [clear, update]), }, [refresh, setList, types]),
) )
return ( return (
@@ -99,15 +136,15 @@ const Home = () => {
contentContainerStyle={styles.scrollContentContainer} contentContainerStyle={styles.scrollContentContainer}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={updating} refreshing={refreshing}
onRefresh={update} onRefresh={refresh}
colors={[colors.accent, colors.accentLow]} colors={[colors.accent, colors.accentLow]}
progressViewOffset={StatusBar.currentHeight} progressViewOffset={StatusBar.currentHeight}
/> />
}> }>
<View style={styles.content}> <View style={styles.content}>
{types.map(type => ( {types.map(type => (
<Category key={type} name={titles[type as GetAlbumListType]} data={type in lists ? lists[type] : []} /> <Category key={type} type={type} />
))} ))}
</View> </View>
</GradientScrollView> </GradientScrollView>

View File

@@ -2,15 +2,16 @@ import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt' import CoverArt from '@app/components/CoverArt'
import FilterButton, { OptionData } from '@app/components/FilterButton' import FilterButton, { OptionData } from '@app/components/FilterButton'
import GradientFlatList from '@app/components/GradientFlatList' import GradientFlatList from '@app/components/GradientFlatList'
import { useFetchPaginatedList2 } from '@app/hooks/list' import { useFetchPaginatedList } from '@app/hooks/list'
import { Album, AlbumListItem } from '@app/models/music' import { Album, AlbumListItem } from '@app/models/music'
import { selectSettings } from '@app/state/settings' import { selectSettings } from '@app/state/settings'
import { Store, useStore } from '@app/state/store' import { Store, useStore } from '@app/state/store'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { GetAlbumList2Type } from '@app/subsonic/params' import { GetAlbumList2Params, GetAlbumList2Type } from '@app/subsonic/params'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React, { useEffect } from 'react' import pick from 'lodash.pick'
import React, { useCallback, useEffect } from 'react'
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native' import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
const AlbumItem = React.memo<{ const AlbumItem = React.memo<{
@@ -55,35 +56,57 @@ const filterOptions: OptionData[] = [
// { text: 'By Genre...', value: 'byGenre' }, // { text: 'By Genre...', value: 'byGenre' },
] ]
const selectAlbumList = (store: Store) => {
return Object.values(store.entities.albumsList)
.flat()
.map(id => store.entities.albums[id])
}
const AlbumsList = () => { const AlbumsList = () => {
const list = useStore(selectAlbumList)
const fetchAlbumsNextPage = useStore(store => store.fetchLibraryAlbumsNextPage)
const resetAlbumsList = useStore(store => store.resetLibraryAlbumsList)
const { refreshing, refresh, fetchNextPage } = useFetchPaginatedList2(fetchAlbumsNextPage, resetAlbumsList)
const filter = useStore(selectSettings.libraryAlbumFilter) const filter = useStore(selectSettings.libraryAlbumFilter)
const setFilter = useStore(selectSettings.setLibraryAlbumFilter) const setFilter = useStore(selectSettings.setLibraryAlbumFilter)
const fetchAlbumList = useStore(store => store.fetchLibraryAlbumList)
const fetchPage = useCallback(
(size: number, offset: number) => {
let params: GetAlbumList2Params
switch (filter.type) {
case 'byYear':
params = {
size,
offset,
type: filter.type,
fromYear: filter.fromYear,
toYear: filter.toYear,
}
break
case 'byGenre':
params = {
size,
offset,
type: filter.type,
genre: filter.genre,
}
break
default:
params = {
size,
offset,
type: filter.type,
}
break
}
return fetchAlbumList(params)
},
[fetchAlbumList, filter.fromYear, filter.genre, filter.toYear, filter.type],
)
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchPage, 300)
const albums = useStore(useCallback(store => list.map(id => store.entities.albums[id]), [list]))
const layout = useWindowDimensions() const layout = useWindowDimensions()
const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2 const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2
const height = size + 36 const height = size + 36
useEffect(() => {
refresh()
}, [refresh, filter])
return ( return (
<View style={styles.container}> <View style={styles.container}>
<GradientFlatList <GradientFlatList
data={list.map(album => ({ album, size, height }))} data={albums.map(album => ({ album, size, height }))}
renderItem={AlbumListRenderItem} renderItem={AlbumListRenderItem}
keyExtractor={item => item.album.id} keyExtractor={item => item.album.id}
numColumns={3} numColumns={3}

View File

@@ -6,12 +6,13 @@ import ListItem from '@app/components/ListItem'
import ListPlayerControls from '@app/components/ListPlayerControls' import ListPlayerControls from '@app/components/ListPlayerControls'
import { useCoverArtFile } from '@app/hooks/cache' import { useCoverArtFile } from '@app/hooks/cache'
import { useAlbumWithSongs, usePlaylistWithSongs } from '@app/hooks/music' import { useAlbumWithSongs, usePlaylistWithSongs } from '@app/hooks/music'
import { AlbumWithSongs, PlaylistWithSongs, Song } from '@app/models/music' import { Album, AlbumWithSongs, PlaylistListItem, PlaylistWithSongs, Song } from '@app/models/music'
import { useStore } from '@app/state/store' import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer' import { selectTrackPlayer } from '@app/state/trackplayer'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import React, { useState } from 'react' import pick from 'lodash.pick'
import React, { useCallback, useEffect, useState } from 'react'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
type SongListType = 'album' | 'playlist' type SongListType = 'album' | 'playlist'
@@ -46,18 +47,19 @@ const SongRenderItem: React.FC<{
const SongListDetails = React.memo<{ const SongListDetails = React.memo<{
title: string title: string
type: SongListType type: SongListType
songList?: AlbumWithSongs | PlaylistWithSongs songList?: Album | PlaylistListItem
songs?: Song[]
subtitle?: string subtitle?: string
}>(({ title, songList, subtitle, type }) => { }>(({ title, songList, songs, subtitle, type }) => {
const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail') const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail')
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined) const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
const setQueue = useStore(selectTrackPlayer.setQueue) const setQueue = useStore(selectTrackPlayer.setQueue)
if (!songList) { if (!songList || !songs) {
return <SongListDetailsFallback /> return <SongListDetailsFallback />
} }
const _songs = [...songList.songs] const _songs = [...songs]
let typeName = '' let typeName = ''
if (type === 'album') { if (type === 'album') {
@@ -106,7 +108,7 @@ const SongListDetails = React.memo<{
<CoverArt type="cover" size="original" coverArt={songList.coverArt} style={styles.cover} /> <CoverArt type="cover" size="original" coverArt={songList.coverArt} style={styles.cover} />
<Text style={styles.title}>{songList.name}</Text> <Text style={styles.title}>{songList.name}</Text>
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : <></>} {subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : <></>}
{songList.songs.length > 0 && ( {songs.length > 0 && (
<ListPlayerControls <ListPlayerControls
style={styles.controls} style={styles.controls}
songs={_songs} songs={_songs}
@@ -135,11 +137,32 @@ const AlbumView = React.memo<{
id: string id: string
title: string title: string
}>(({ id, title }) => { }>(({ id, title }) => {
const album = useAlbumWithSongs(id) // const album = useAlbumWithSongs(id)
const album = useStore(useCallback(store => store.entities.albums[id], [id]))
const songs = useStore(
useCallback(
store => {
const ids = store.entities.albumSongs[id]
return ids ? ids.map(i => store.entities.songs[i]) : undefined
},
[id],
),
)
const fetchAlbum = useStore(store => store.fetchLibraryAlbum)
useEffect(() => {
if (!album || !songs) {
fetchAlbum(id)
}
}, [album, fetchAlbum, id, songs])
return ( return (
<SongListDetails <SongListDetails
title={title} title={title}
songList={album} songList={album}
songs={songs}
subtitle={(album?.artist || '') + (album?.year ? ' • ' + album?.year : '')} subtitle={(album?.artist || '') + (album?.year ? ' • ' + album?.year : '')}
type="album" type="album"
/> />

View File

@@ -1,12 +1,22 @@
import { Store } from '@app/state/store' import { Store } from '@app/state/store'
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements' import {
import { GetAlbumList2Params } from '@app/subsonic/params' AlbumID3Element,
ArtistID3Element,
ArtistInfo2Element,
ChildElement,
PlaylistElement,
} from '@app/subsonic/elements'
import { GetAlbumList2Params, Search3Params, StarParams } from '@app/subsonic/params'
import { import {
GetAlbumList2Response, GetAlbumList2Response,
GetAlbumResponse,
GetArtistInfo2Response, GetArtistInfo2Response,
GetArtistResponse, GetArtistResponse,
GetArtistsResponse, GetArtistsResponse,
GetPlaylistResponse,
GetPlaylistsResponse,
GetTopSongsResponse, GetTopSongsResponse,
Search3Response,
SubsonicResponse, SubsonicResponse,
} from '@app/subsonic/responses' } from '@app/subsonic/responses'
import produce from 'immer' import produce from 'immer'
@@ -54,6 +64,14 @@ export interface Album {
year?: number year?: number
} }
export interface Playlist {
itemType: 'playlist'
id: string
name: string
comment?: string
coverArt?: string
}
export interface Song { export interface Song {
itemType: 'song' itemType: 'song'
id: string id: string
@@ -66,11 +84,15 @@ export interface Song {
discNumber?: number discNumber?: number
duration?: number duration?: number
starred?: Date starred?: Date
// streamUri: string
coverArt?: string coverArt?: string
} }
export interface SearchResults {
artists: string[]
albums: string[]
songs: string[]
}
function mapArtist(artist: ArtistID3Element): Artist { function mapArtist(artist: ArtistID3Element): Artist {
return { return {
itemType: 'artist', itemType: 'artist',
@@ -102,6 +124,16 @@ function mapAlbum(album: AlbumID3Element): Album {
} }
} }
function mapPlaylist(playlist: PlaylistElement): Playlist {
return {
itemType: 'playlist',
id: playlist.id,
name: playlist.name,
comment: playlist.comment,
coverArt: playlist.coverArt,
}
}
function mapSong(song: ChildElement): Song { function mapSong(song: ChildElement): Song {
return { return {
itemType: 'song', itemType: 'song',
@@ -119,6 +151,10 @@ function mapSong(song: ChildElement): Song {
} }
} }
function mapId(entities: { id: string }[]): string[] {
return entities.map(e => e.id)
}
export type LibrarySlice = { export type LibrarySlice = {
entities: { entities: {
artists: ById<Artist> artists: ById<Artist>
@@ -127,9 +163,15 @@ export type LibrarySlice = {
artistNameTopSongs: OneToMany artistNameTopSongs: OneToMany
albums: ById<Album> albums: ById<Album>
albumSongs: OneToMany
// todo: remove these and store in component state
albumsList: PaginatedList albumsList: PaginatedList
albumsListSize: number albumsListSize: number
playlists: ById<Playlist>
playlistSongs: OneToMany
songs: ById<Song> songs: ById<Song>
} }
@@ -138,17 +180,29 @@ export type LibrarySlice = {
fetchLibraryArtists: () => Promise<void> fetchLibraryArtists: () => Promise<void>
fetchLibraryArtist: (id: string) => Promise<void> fetchLibraryArtist: (id: string) => Promise<void>
fetchLibraryArtistInfo: (artistId: string) => Promise<void> fetchLibraryArtistInfo: (artistId: string) => Promise<void>
fetchLibraryArtistTopSongs: (artistName: string) => Promise<void>
resetLibraryArtists: () => void resetLibraryArtists: () => void
fetchLibraryTopSongs: (artistName: string) => Promise<void> fetchLibraryAlbum: (id: string) => Promise<void>
fetchLibraryAlbumsNextPage: () => Promise<void> fetchLibraryPlaylists: () => Promise<void>
resetLibraryAlbumsList: () => void fetchLibraryPlaylist: (id: string) => Promise<void>
fetchLibraryAlbumList: (params: GetAlbumList2Params) => Promise<string[]>
fetchLibrarySearchResults: (params: Search3Params) => Promise<SearchResults>
star: (params: StarParams) => Promise<void>
unstar: (params: StarParams) => Promise<void>
} }
function nextOffest(list: PaginatedList): number { function reduceById<T extends { id: string }>(collection: T[]): ById<T> {
const pages = Object.keys(list).map(k => parseInt(k, 10)) return collection.reduce((acc, value) => {
return pages.length > 0 ? pages.sort((a, b) => a - b)[pages.length - 1] : 0 acc[value.id] = value
return acc
}, {} as ById<T>)
}
function mergeById<T extends { [id: string]: unknown }>(object: T, source: T): void {
merge(object, source)
} }
const defaultEntities = () => ({ const defaultEntities = () => ({
@@ -160,6 +214,10 @@ const defaultEntities = () => ({
albums: {}, albums: {},
albumsList: {}, albumsList: {},
albumsListSize: 300, albumsListSize: 300,
albumSongs: {},
playlists: {},
playlistSongs: {},
songs: {}, songs: {},
}) })
@@ -186,15 +244,13 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
return return
} }
const artists = response.data.artists.reduce((acc, value) => { const artists = response.data.artists.map(mapArtist)
acc[value.id] = mapArtist(value) const artistsById = reduceById(artists)
return acc
}, {} as ById<Artist>)
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.artists = artists state.entities.artists = artistsById
state.entities.artistAlbums = pick(state.entities.artistAlbums, Object.keys(artists)) state.entities.artistAlbums = pick(state.entities.artistAlbums, mapId(artists))
}), }),
) )
}, },
@@ -212,18 +268,15 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
return return
} }
const albums = response.data.albums.reduce((acc, value) => {
acc[value.id] = mapAlbum(value)
return acc
}, {} as ById<Album>)
const artist = mapArtist(response.data.artist) const artist = mapArtist(response.data.artist)
const albums = response.data.albums.map(mapAlbum)
const albumsById = reduceById(albums)
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.artists[id] = artist state.entities.artists[id] = artist
state.entities.artistAlbums[id] = Object.keys(albums) state.entities.artistAlbums[id] = mapId(albums)
merge(state.entities.albums, albums) mergeById(state.entities.albums, albumsById)
}), }),
) )
}, },
@@ -259,7 +312,7 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
) )
}, },
fetchLibraryTopSongs: async artistName => { fetchLibraryArtistTopSongs: async artistName => {
const client = get().client const client = get().client
if (!client) { if (!client) {
return return
@@ -273,80 +326,200 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
} }
const topSongs = response.data.songs.map(mapSong) const topSongs = response.data.songs.map(mapSong)
const topSongsById = reduceById(topSongs)
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
merge(state.entities.songs, topSongs) mergeById(state.entities.songs, topSongsById)
state.entities.artistNameTopSongs[artistName] = topSongs.map(s => s.id) state.entities.artistNameTopSongs[artistName] = mapId(topSongs)
}), }),
) )
}, },
fetchLibraryAlbumsNextPage: async () => { fetchLibraryAlbum: async id => {
const client = get().client const client = get().client
if (!client) { if (!client) {
return return
} }
const filter = get().settings.screens.library.albums let response: SubsonicResponse<GetAlbumResponse>
const size = get().entities.albumsListSize try {
const offset = nextOffest(get().entities.albumsList) response = await client.getAlbum({ id })
} catch {
return
}
let params: GetAlbumList2Params const album = mapAlbum(response.data.album)
switch (filter.type) { const songs = response.data.songs.map(mapSong)
case 'byYear': const songsById = reduceById(songs)
params = {
size, set(
offset, produce<LibrarySlice>(state => {
type: filter.type, state.entities.albums[id] = album
fromYear: filter.fromYear, state.entities.albumSongs[id] = mapId(songs)
toYear: filter.toYear, mergeById(state.entities.songs, songsById)
} }),
break )
case 'byGenre': },
params = {
size, fetchLibraryPlaylists: async () => {
offset, const client = get().client
type: filter.type, if (!client) {
genre: filter.genre, return
} }
break
default: let response: SubsonicResponse<GetPlaylistsResponse>
params = { try {
size, response = await client.getPlaylists()
offset, } catch {
type: filter.type, return
} }
break
const playlists = response.data.playlists.map(mapPlaylist)
const playlistsById = reduceById(playlists)
set(
produce<LibrarySlice>(state => {
state.entities.playlists = playlistsById
state.entities.playlistSongs = pick(state.entities.playlistSongs, mapId(playlists))
}),
)
},
fetchLibraryPlaylist: async id => {
const client = get().client
if (!client) {
return
}
let response: SubsonicResponse<GetPlaylistResponse>
try {
response = await client.getPlaylist({ id })
} catch {
return
}
const playlist = mapPlaylist(response.data.playlist)
const songs = response.data.playlist.songs.map(mapSong)
const songsById = reduceById(songs)
set(
produce<LibrarySlice>(state => {
state.entities.playlists[id] = playlist
state.entities.playlistSongs[id] = mapId(songs)
mergeById(state.entities.songs, songsById)
}),
)
},
fetchLibraryAlbumList: async params => {
const client = get().client
if (!client) {
return []
} }
let response: SubsonicResponse<GetAlbumList2Response> let response: SubsonicResponse<GetAlbumList2Response>
try { try {
response = await client.getAlbumList2(params) response = await client.getAlbumList2(params)
} catch { } catch {
return return []
} }
const albums = response.data.albums.reduce((acc, value) => { const albums = response.data.albums.map(mapAlbum)
acc[value.id] = mapAlbum(value) const albumsById = reduceById(albums)
return acc
}, {} as ById<Album>)
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
if (response.data.albums.length <= 0) { mergeById(state.entities.albums, albumsById)
return }),
)
return mapId(albums)
},
fetchLibrarySearchResults: async params => {
const empty = { artists: [], albums: [], songs: [] }
const client = get().client
if (!client) {
return empty
}
let response: SubsonicResponse<Search3Response>
try {
response = await client.search3(params)
} catch {
return empty
}
const artists = response.data.artists.map(mapArtist)
const artistsById = reduceById(artists)
const albums = response.data.albums.map(mapAlbum)
const albumsById = reduceById(albums)
const songs = response.data.songs.map(mapSong)
const songsById = reduceById(songs)
set(
produce<LibrarySlice>(state => {
mergeById(state.entities.artists, artistsById)
mergeById(state.entities.albums, albumsById)
mergeById(state.entities.songs, songsById)
}),
)
return {
artists: mapId(artists),
albums: mapId(albums),
songs: mapId(songs),
}
},
star: async params => {
const client = get().client
if (!client) {
return
}
try {
await client.star(params)
} catch {
return
}
set(
produce<LibrarySlice>(state => {
if (params.id) {
state.entities.songs[params.id].starred = new Date()
} else if (params.albumId) {
state.entities.albums[params.albumId].starred = new Date()
} else if (params.artistId) {
state.entities.artists[params.artistId].starred = new Date()
} }
merge(state.entities.albums, albums)
state.entities.albumsList[offset + size] = response.data.albums.map(a => a.id)
}), }),
) )
}, },
resetLibraryAlbumsList: () => { unstar: async params => {
const client = get().client
if (!client) {
return
}
try {
await client.unstar(params)
} catch {
return
}
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.albumsList = {} if (params.id) {
state.entities.songs[params.id].starred = undefined
} else if (params.albumId) {
state.entities.albums[params.albumId].starred = undefined
} else if (params.artistId) {
state.entities.artists[params.artistId].starred = undefined
}
}), }),
) )
}, },

View File

@@ -217,14 +217,6 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
let queue = await get().mapSongstoTrackExts(songs) let queue = await get().mapSongstoTrackExts(songs)
try {
for (const t of queue) {
t.url = get().buildStreamUri(t.id)
}
} catch {
return
}
if (shuffled) { if (shuffled) {
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack) const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
set({ shuffleOrder }) set({ shuffleOrder })

View File

@@ -17,19 +17,21 @@ export const selectTrackPlayerMap = {
export const createTrackPlayerMapSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerMapSlice => ({ export const createTrackPlayerMapSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerMapSlice => ({
mapSongtoTrackExt: async song => { mapSongtoTrackExt: async song => {
let artwork = require('@res/fallback.png') let artwork = require('@res/fallback.png')
if (song.coverArt) { // if (song.coverArt) {
const filePath = await get().fetchCoverArtFilePath(song.coverArt) // const filePath = await get().fetchCoverArtFilePath(song.coverArt)
if (filePath) { // if (filePath) {
artwork = filePath // artwork = filePath
} // }
} // }
console.log('mapping', song.title)
return { return {
id: song.id, id: song.id,
title: song.title, title: song.title,
artist: song.artist || 'Unknown Artist', artist: song.artist || 'Unknown Artist',
album: song.album || 'Unknown Album', album: song.album || 'Unknown Album',
url: song.streamUri, url: get().buildStreamUri(song.id),
userAgent, userAgent,
artwork, artwork,
coverArt: song.coverArt, coverArt: song.coverArt,