From 88d0c6089ebf9a29fece937f6f5cf49891a61ca7 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 16 Aug 2021 21:32:43 +0900 Subject: [PATCH] refactored music mapping into state easier to access client/settings --- app/models/music.ts | 105 -------------------------- app/state/music.ts | 58 +++++---------- app/state/musicmap.ts | 142 ++++++++++++++++++++++++++++++++++++ app/state/store.ts | 3 + app/storage/asyncstorage.ts | 54 -------------- app/storage/music.ts | 38 ---------- 6 files changed, 163 insertions(+), 237 deletions(-) create mode 100644 app/state/musicmap.ts delete mode 100644 app/storage/asyncstorage.ts delete mode 100644 app/storage/music.ts diff --git a/app/models/music.ts b/app/models/music.ts index ccdeab8..f058150 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -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, - } -} diff --git a/app/state/music.ts b/app/state/music.ts index 48710ca..4eed195 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -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 } fetchAlbumCoverArt: (id: string) => Promise getAlbumCoverArt: (id: string | undefined) => Promise - mapSongCoverArtFromAlbum: (songs: Song[]) => Promise } export const selectMusic = { @@ -133,15 +124,12 @@ export const createMusicSlice = (set: SetState, get: GetState): 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(state => { state.artistInfo[id] = artistInfo @@ -167,9 +155,7 @@ export const createMusicSlice = (set: SetState, get: GetState): 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(state => { @@ -194,9 +180,7 @@ export const createMusicSlice = (set: SetState, get: GetState): 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(state => { @@ -226,9 +210,11 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu try { const response = await client.getArtists() + const artists = response.data.artists.map(get().mapArtistID3toArtist) + set( produce(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, get: GetState): 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, get: GetState): Mu try { const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset }) + const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem) set( produce(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, get: GetState): 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(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, get: GetState): 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(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, get: GetState): 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 - }, }) diff --git a/app/state/musicmap.ts b/app/state/musicmap.ts new file mode 100644 index 0000000..9365831 --- /dev/null +++ b/app/state/musicmap.ts @@ -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 + mapChildrenToSongs: (children: ChildElement[]) => Promise + mapArtistID3toArtist: (artist: ArtistID3Element) => Artist + mapArtistInfo: ( + artistResponse: GetArtistResponse, + info: ArtistInfo2Element, + topSongs: ChildElement[], + ) => Promise + mapAlbumID3toAlbumListItem: (album: AlbumID3Element) => AlbumListItem + mapAlbumID3toAlbum: (album: AlbumID3Element) => AlbumListItem + mapAlbumID3WithSongstoAlbumWithSongs: (album: AlbumID3Element, songs: ChildElement[]) => Promise + mapPlaylistListItem: (playlist: PlaylistElement) => PlaylistListItem + mapPlaylistWithSongs: (playlist: PlaylistWithSongsElement) => Promise +} + +class NoClientError extends Error { + constructor() { + super('no client in state') + } +} + +export const createMusicMapSlice = (set: SetState, get: GetState): 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[] = [] + 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, + } + }, +}) diff --git a/app/state/store.ts b/app/state/store.ts index a5cf63e..30e4757 100644 --- a/app/state/store.ts +++ b/app/state/store.ts @@ -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( (set, get) => ({ ...createSettingsSlice(set, get), ...createMusicSlice(set, get), + ...createMusicMapSlice(set, get), ...createTrackPlayerSlice(set, get), ...createCacheSlice(set, get), diff --git a/app/storage/asyncstorage.ts b/app/storage/asyncstorage.ts deleted file mode 100644 index 66a8899..0000000 --- a/app/storage/asyncstorage.ts +++ /dev/null @@ -1,54 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage' - -export async function getItem(key: string): Promise { - 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 { - try { - await AsyncStorage.setItem(key, JSON.stringify(item)) - } catch (e) { - console.error(`setItem error (key: ${key})`, e) - } -} - -export async function multiSet(items: string[][]): Promise { - 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 { - try { - return await AsyncStorage.getAllKeys() - } catch (e) { - console.error('getAllKeys error', e) - return [] - } -} - -export async function multiRemove(keys: string[]): Promise { - try { - await AsyncStorage.multiRemove(keys) - } catch (e) { - console.error('multiRemove error', e) - } -} diff --git a/app/storage/music.ts b/app/storage/music.ts deleted file mode 100644 index c4b9924..0000000 --- a/app/storage/music.ts +++ /dev/null @@ -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 { - 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 { - 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, - }), - ]), - ]) -}