refactored music mapping into state

easier to access client/settings
This commit is contained in:
austinried 2021-08-16 21:32:43 +09:00
parent faabe00c7e
commit 88d0c6089e
6 changed files with 163 additions and 237 deletions

View File

@ -1,14 +1,3 @@
import { SubsonicApiClient } from '@app/subsonic/api'
import {
AlbumID3Element,
ArtistID3Element,
ArtistInfo2Element,
ChildElement,
PlaylistElement,
PlaylistWithSongsElement,
} from '@app/subsonic/elements'
import { GetArtistResponse } from '@app/subsonic/responses'
export interface Artist {
itemType: 'artist'
id: string
@ -115,97 +104,3 @@ export type DownloadedArtist = Artist & {
}
export type DownloadedSong = Song
export function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
return {
itemType: 'artist',
id: artist.id,
name: artist.name,
starred: artist.starred,
coverArt: artist.coverArt,
}
}
export function mapArtistInfo(
artistResponse: GetArtistResponse,
info: ArtistInfo2Element,
topSongs: ChildElement[],
client: SubsonicApiClient,
): ArtistInfo {
const { artist, albums } = artistResponse
const mappedAlbums = albums.map(mapAlbumID3toAlbum)
return {
...mapArtistID3toArtist(artist),
albums: mappedAlbums,
largeImageUrl: info.largeImageUrl,
topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5),
}
}
export function mapAlbumID3toAlbumListItem(album: AlbumID3Element): AlbumListItem {
return {
itemType: 'album',
id: album.id,
name: album.name,
artist: album.artist,
artistId: album.artistId,
starred: album.starred,
coverArt: album.coverArt,
}
}
export function mapAlbumID3toAlbum(album: AlbumID3Element): Album {
return {
...mapAlbumID3toAlbumListItem(album),
coverArt: album.coverArt,
year: album.year,
}
}
export function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
return {
itemType: 'song',
id: child.id,
album: child.album,
albumId: child.albumId,
artist: child.artist,
artistId: child.artistId,
title: child.title,
track: child.track,
duration: child.duration,
starred: child.starred,
coverArt: child.coverArt,
streamUri: client.streamUri({ id: child.id }),
}
}
export function mapAlbumID3WithSongstoAlbumWithSongs(
album: AlbumID3Element,
songs: ChildElement[],
client: SubsonicApiClient,
): AlbumWithSongs {
return {
...mapAlbumID3toAlbum(album),
songs: songs.map(s => mapChildToSong(s, client)),
}
}
export function mapPlaylistListItem(playlist: PlaylistElement): PlaylistListItem {
return {
itemType: 'playlist',
id: playlist.id,
name: playlist.name,
comment: playlist.comment,
coverArt: playlist.coverArt,
}
}
export function mapPlaylistWithSongs(playlist: PlaylistWithSongsElement, client: SubsonicApiClient): PlaylistWithSongs {
return {
...mapPlaylistListItem(playlist),
songs: playlist.songs.map(s => mapChildToSong(s, client)),
coverArt: playlist.coverArt,
}
}

View File

@ -4,17 +4,9 @@ import {
Artist,
ArtistInfo,
HomeLists,
mapAlbumID3toAlbumListItem,
mapAlbumID3WithSongstoAlbumWithSongs,
mapArtistID3toArtist,
mapArtistInfo,
mapChildToSong,
mapPlaylistListItem,
mapPlaylistWithSongs,
PlaylistListItem,
PlaylistWithSongs,
SearchResults,
Song,
StarrableItemType,
} from '@app/models/music'
import { Store } from '@app/state/store'
@ -72,7 +64,6 @@ export type MusicSlice = {
albumIdCoverArtRequests: { [id: string]: Promise<void> }
fetchAlbumCoverArt: (id: string) => Promise<void>
getAlbumCoverArt: (id: string | undefined) => Promise<string | undefined>
mapSongCoverArtFromAlbum: (songs: Song[]) => Promise<Song[]>
}
export const selectMusic = {
@ -133,15 +124,12 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
client.getArtistInfo2({ id }),
])
const topSongsResponse = await client.getTopSongs({ artist: artistResponse.data.artist.name, count: 50 })
const artistInfo = mapArtistInfo(
const artistInfo = await get().mapArtistInfo(
artistResponse.data,
artistInfoResponse.data.artistInfo,
topSongsResponse.data.songs,
client,
)
artistInfo.topSongs = await get().mapSongCoverArtFromAlbum(artistInfo.topSongs)
set(
produce<MusicSlice>(state => {
state.artistInfo[id] = artistInfo
@ -167,9 +155,7 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.getAlbum({ id })
const album = mapAlbumID3WithSongstoAlbumWithSongs(response.data.album, response.data.songs, client)
album.songs = await get().mapSongCoverArtFromAlbum(album.songs)
const album = await get().mapAlbumID3WithSongstoAlbumWithSongs(response.data.album, response.data.songs)
set(
produce<MusicSlice>(state => {
@ -194,9 +180,7 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.getPlaylist({ id })
const playlist = mapPlaylistWithSongs(response.data.playlist, client)
playlist.songs = await get().mapSongCoverArtFromAlbum(playlist.songs)
const playlist = await get().mapPlaylistWithSongs(response.data.playlist)
set(
produce<MusicSlice>(state => {
@ -226,9 +210,11 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.getArtists()
const artists = response.data.artists.map(get().mapArtistID3toArtist)
set(
produce<MusicSlice>(state => {
state.artists = response.data.artists.map(mapArtistID3toArtist)
state.artists = artists
state.starredArtists = reduceStarred(state.starredArtists, state.artists)
}),
)
@ -253,7 +239,8 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.getPlaylists()
set({ playlists: response.data.playlists.map(mapPlaylistListItem) })
const playlists = response.data.playlists.map(get().mapPlaylistListItem)
set({ playlists })
} finally {
set({ playlistsUpdating: false })
}
@ -275,9 +262,10 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset })
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
set(
produce<MusicSlice>(state => {
state.albums = response.data.albums.map(mapAlbumID3toAlbumListItem)
state.albums = albums
state.starredAlbums = reduceStarred(state.starredAlbums, state.albums)
}),
)
@ -311,14 +299,14 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
try {
const response = await client.search3({ query })
const songs = await get().mapSongCoverArtFromAlbum(response.data.songs.map(a => mapChildToSong(a, client)))
const artists = response.data.artists.map(get().mapArtistID3toArtist)
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
const songs = await get().mapChildrenToSongs(response.data.songs)
set(
produce<MusicSlice>(state => {
state.searchResults = {
artists: response.data.artists.map(mapArtistID3toArtist),
albums: response.data.albums.map(mapAlbumID3toAlbumListItem),
songs: songs,
}
state.searchResults = { artists, albums, songs }
state.starredSongs = reduceStarred(state.starredSongs, state.searchResults.songs)
state.starredArtists = reduceStarred(state.starredArtists, state.searchResults.artists)
state.starredAlbums = reduceStarred(state.starredAlbums, state.searchResults.albums)
@ -359,9 +347,10 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
for (const type of types) {
promises.push(
client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => {
const list = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
set(
produce<MusicSlice>(state => {
state.homeLists[type] = response.data.albums.map(mapAlbumID3toAlbumListItem)
state.homeLists[type] = list
state.starredAlbums = reduceStarred(state.starredAlbums, state.homeLists[type])
}),
)
@ -492,15 +481,4 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
await get().fetchAlbumCoverArt(id)
return get().albumIdCoverArt[id]
},
mapSongCoverArtFromAlbum: async songs => {
const mapped: Song[] = []
for (const s of songs) {
mapped.push({
...s,
coverArt: await get().getAlbumCoverArt(s.albumId),
})
}
return mapped
},
})

142
app/state/musicmap.ts Normal file
View File

@ -0,0 +1,142 @@
import {
AlbumListItem,
AlbumWithSongs,
Artist,
ArtistInfo,
PlaylistListItem,
PlaylistWithSongs,
Song,
} from '@app/models/music'
import {
AlbumID3Element,
ArtistID3Element,
ArtistInfo2Element,
ChildElement,
PlaylistElement,
PlaylistWithSongsElement,
} from '@app/subsonic/elements'
import { GetArtistResponse } from '@app/subsonic/responses'
import { GetState, SetState } from 'zustand'
import { Store } from './store'
export type MusicMapSlice = {
mapChildToSong: (child: ChildElement) => Promise<Song>
mapChildrenToSongs: (children: ChildElement[]) => Promise<Song[]>
mapArtistID3toArtist: (artist: ArtistID3Element) => Artist
mapArtistInfo: (
artistResponse: GetArtistResponse,
info: ArtistInfo2Element,
topSongs: ChildElement[],
) => Promise<ArtistInfo>
mapAlbumID3toAlbumListItem: (album: AlbumID3Element) => AlbumListItem
mapAlbumID3toAlbum: (album: AlbumID3Element) => AlbumListItem
mapAlbumID3WithSongstoAlbumWithSongs: (album: AlbumID3Element, songs: ChildElement[]) => Promise<AlbumWithSongs>
mapPlaylistListItem: (playlist: PlaylistElement) => PlaylistListItem
mapPlaylistWithSongs: (playlist: PlaylistWithSongsElement) => Promise<PlaylistWithSongs>
}
class NoClientError extends Error {
constructor() {
super('no client in state')
}
}
export const createMusicMapSlice = (set: SetState<Store>, get: GetState<Store>): MusicMapSlice => ({
mapChildToSong: async child => {
const client = get().client
if (!client) {
throw new NoClientError()
}
return {
itemType: 'song',
id: child.id,
album: child.album,
albumId: child.albumId,
artist: child.artist,
artistId: child.artistId,
title: child.title,
track: child.track,
duration: child.duration,
starred: child.starred,
coverArt: await get().getAlbumCoverArt(child.albumId),
streamUri: client.streamUri({ id: child.id }),
}
},
mapChildrenToSongs: async children => {
const songMaps: Promise<Song>[] = []
for (const child of children) {
songMaps.push(get().mapChildToSong(child))
}
return await Promise.all(songMaps)
},
mapArtistID3toArtist: artist => {
return {
itemType: 'artist',
id: artist.id,
name: artist.name,
starred: artist.starred,
coverArt: artist.coverArt,
}
},
mapArtistInfo: async (artistResponse, info, topSongs) => {
const { artist, albums } = artistResponse
const mappedAlbums = albums.map(get().mapAlbumID3toAlbum)
return {
...get().mapArtistID3toArtist(artist),
albums: mappedAlbums,
largeImageUrl: info.largeImageUrl,
topSongs: (await get().mapChildrenToSongs(topSongs)).slice(0, 5),
}
},
mapAlbumID3toAlbumListItem: album => {
return {
itemType: 'album',
id: album.id,
name: album.name,
artist: album.artist,
artistId: album.artistId,
starred: album.starred,
coverArt: album.coverArt,
}
},
mapAlbumID3toAlbum: album => {
return {
...get().mapAlbumID3toAlbumListItem(album),
coverArt: album.coverArt,
year: album.year,
}
},
mapAlbumID3WithSongstoAlbumWithSongs: async (album, songs) => {
return {
...get().mapAlbumID3toAlbum(album),
songs: await get().mapChildrenToSongs(songs),
}
},
mapPlaylistListItem: playlist => {
return {
itemType: 'playlist',
id: playlist.id,
name: playlist.name,
comment: playlist.comment,
coverArt: playlist.coverArt,
}
},
mapPlaylistWithSongs: async playlist => {
return {
...get().mapPlaylistListItem(playlist),
songs: await get().mapChildrenToSongs(playlist.songs),
coverArt: playlist.coverArt,
}
},
})

View File

@ -4,10 +4,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import create from 'zustand'
import { persist, StateStorage } from 'zustand/middleware'
import { CacheSlice, createCacheSlice } from './cache'
import { createMusicMapSlice, MusicMapSlice } from './musicmap'
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
export type Store = SettingsSlice &
MusicSlice &
MusicMapSlice &
TrackPlayerSlice &
CacheSlice & {
hydrated: boolean
@ -37,6 +39,7 @@ export const useStore = create<Store>(
(set, get) => ({
...createSettingsSlice(set, get),
...createMusicSlice(set, get),
...createMusicMapSlice(set, get),
...createTrackPlayerSlice(set, get),
...createCacheSlice(set, get),

View File

@ -1,54 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
export async function getItem(key: string): Promise<any | null> {
try {
const item = await AsyncStorage.getItem(key)
return item ? JSON.parse(item) : null
} catch (e) {
console.error(`getItem error (key: ${key})`, e)
return null
}
}
export async function multiGet(keys: string[]): Promise<[string, any | null][]> {
try {
const items = await AsyncStorage.multiGet(keys)
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null])
} catch (e) {
console.error('multiGet error', e)
return []
}
}
export async function setItem(key: string, item: any): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(item))
} catch (e) {
console.error(`setItem error (key: ${key})`, e)
}
}
export async function multiSet(items: string[][]): Promise<void> {
try {
await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]))
} catch (e) {
console.error('multiSet error', e)
}
}
export async function getAllKeys(): Promise<string[]> {
try {
return await AsyncStorage.getAllKeys()
} catch (e) {
console.error('getAllKeys error', e)
return []
}
}
export async function multiRemove(keys: string[]): Promise<void> {
try {
await AsyncStorage.multiRemove(keys)
} catch (e) {
console.error('multiRemove error', e)
}
}

View File

@ -1,38 +0,0 @@
import { DownloadedSong } from '@app/models/music'
import { getItem, multiGet, multiSet } from '@app/storage/asyncstorage'
const key = {
downloadedSongKeys: '@downloadedSongKeys',
downloadedAlbumKeys: '@downloadedAlbumKeys',
downloadedArtistKeys: '@downloadedArtistKeys',
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
}
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
const keysItem = await getItem(key.downloadedSongKeys)
const keys: string[] = keysItem ? JSON.parse(keysItem) : []
const items = await multiGet(keys)
return items.map(x => {
const parsed = JSON.parse(x[1] as string)
return {
id: x[0],
type: 'song',
...parsed,
}
})
}
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
await multiSet([
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
...items.map(x => [
x.id,
JSON.stringify({
name: x.name,
album: x.album,
artist: x.artist,
}),
]),
])
}