mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
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:
@@ -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 }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export interface Song {
|
|||||||
duration?: number
|
duration?: number
|
||||||
starred?: Date
|
starred?: Date
|
||||||
|
|
||||||
streamUri: string
|
// streamUri: string
|
||||||
coverArt?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user