1import {internalSerializeKey, StorageKeys} from '@/constants/commonConst'; 2import mp3Util from '@/native/mp3Util'; 3import { 4 getInternalData, 5 InternalDataType, 6 isSameMediaItem, 7} from '@/utils/mediaItem'; 8import StateMapper from '@/utils/stateMapper'; 9import {getStorage, setStorage} from '@/utils/storage'; 10import {nanoid} from 'nanoid'; 11import {useEffect, useState} from 'react'; 12import {FileStat, FileSystem} from 'react-native-file-access'; 13 14let localSheet: IMusic.IMusicItem[] = []; 15const localSheetStateMapper = new StateMapper(() => localSheet); 16 17export async function setup() { 18 const sheet = await getStorage(StorageKeys.LocalMusicSheet); 19 if (sheet) { 20 let validSheet = []; 21 for (let musicItem of sheet) { 22 const localPath = getInternalData<string>( 23 musicItem, 24 InternalDataType.LOCALPATH, 25 ); 26 if (localPath && (await FileSystem.exists(localPath))) { 27 validSheet.push(musicItem); 28 } 29 } 30 if (validSheet.length !== sheet.length) { 31 await setStorage(StorageKeys.LocalMusicSheet, validSheet); 32 } 33 localSheet = validSheet; 34 } else { 35 await setStorage(StorageKeys.LocalMusicSheet, []); 36 } 37 localSheetStateMapper.notify(); 38} 39 40export async function addMusic( 41 musicItem: IMusic.IMusicItem | IMusic.IMusicItem[], 42) { 43 if (!Array.isArray(musicItem)) { 44 musicItem = [musicItem]; 45 } 46 let newSheet = [...localSheet]; 47 musicItem.forEach(mi => { 48 if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) { 49 newSheet.push(mi); 50 } 51 }); 52 await setStorage(StorageKeys.LocalMusicSheet, newSheet); 53 localSheet = newSheet; 54 localSheetStateMapper.notify(); 55} 56 57export async function removeMusic( 58 musicItem: IMusic.IMusicItem, 59 deleteOriginalFile = false, 60) { 61 const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem)); 62 let newSheet = [...localSheet]; 63 if (idx !== -1) { 64 newSheet.splice(idx, 1); 65 if (deleteOriginalFile && musicItem[internalSerializeKey]?.localPath) { 66 await FileSystem.unlink(musicItem[internalSerializeKey].localPath); 67 } 68 } 69 localSheet = newSheet; 70 localSheetStateMapper.notify(); 71} 72 73function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null { 74 const data = fn.slice(0, fn.lastIndexOf('.')).split('@'); 75 const [platform, id, title, artist] = data; 76 if (!platform || !id) { 77 return null; 78 } 79 return { 80 id, 81 platform, 82 title, 83 artist, 84 }; 85} 86 87function localMediaFilter(_: FileStat) { 88 return ( 89 _.filename.endsWith('.mp3') || 90 _.filename.endsWith('.flac') || 91 _.filename.endsWith('.wma') || 92 _.filename.endsWith('.wav') || 93 _.filename.endsWith('.m4a') || 94 _.filename.endsWith('.ogg') || 95 _.filename.endsWith('.acc') 96 ); 97} 98 99let importToken: string | null = null; 100// 获取本地的文件列表 101async function getMusicStats(folderPaths: string[]) { 102 const _importToken = nanoid(); 103 importToken = _importToken; 104 const musicList: FileStat[] = []; 105 let peek: string | undefined; 106 let dirFiles: FileStat[] = []; 107 while (folderPaths.length !== 0) { 108 if (importToken !== _importToken) { 109 throw new Error('Import Broken'); 110 } 111 peek = folderPaths.shift() as string; 112 try { 113 dirFiles = await FileSystem.statDir(peek); 114 } catch { 115 dirFiles = []; 116 } 117 118 dirFiles.forEach(item => { 119 if (item.type === 'directory' && !folderPaths.includes(item.path)) { 120 folderPaths.push(item.path); 121 } else if (localMediaFilter(item)) { 122 musicList.push(item); 123 } 124 }); 125 } 126 return {musicList, token: _importToken}; 127} 128 129function cancelImportLocal() { 130 importToken = null; 131} 132 133// 导入本地音乐 134async function importLocal(_folderPaths: string[]) { 135 const folderPaths = [..._folderPaths]; 136 const {musicList, token} = await getMusicStats(folderPaths); 137 if (token !== importToken) { 138 throw new Error('Import Broken'); 139 } 140 const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path)); 141 if (token !== importToken) { 142 throw new Error('Import Broken'); 143 } 144 const musicItems = await Promise.all( 145 musicList.map(async (musicStat, index) => { 146 let {platform, id, title, artist} = 147 parseFilename(musicStat.filename) ?? {}; 148 const meta = metas[index]; 149 if (!platform || !id) { 150 platform = '本地'; 151 id = await FileSystem.hash(musicStat.path, 'MD5'); 152 } 153 return { 154 id, 155 platform, 156 title: title ?? meta?.title ?? musicStat.filename, 157 artist: artist ?? meta?.artist ?? '未知歌手', 158 duration: parseInt(meta?.duration ?? '0') / 1000, 159 album: meta?.album ?? '未知专辑', 160 artwork: '', 161 [internalSerializeKey]: { 162 localPath: musicStat.path, 163 }, 164 }; 165 }), 166 ); 167 if (token !== importToken) { 168 throw new Error('Import Broken'); 169 } 170 addMusic(musicItems); 171} 172 173/** 是否为本地音乐 */ 174function isLocalMusic( 175 musicItem: ICommon.IMediaBase | null, 176): IMusic.IMusicItem | undefined { 177 return musicItem 178 ? localSheet.find(_ => isSameMediaItem(_, musicItem)) 179 : undefined; 180} 181 182/** 状态-是否为本地音乐 */ 183function useIsLocal(musicItem: IMusic.IMusicItem | null) { 184 const localMusicState = localSheetStateMapper.useMappedState(); 185 const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem)); 186 useEffect(() => { 187 if (!musicItem) { 188 setIsLocal(false); 189 } else { 190 setIsLocal(!!isLocalMusic(musicItem)); 191 } 192 }, [localMusicState, musicItem]); 193 return isLocal; 194} 195 196function getMusicList() { 197 return localSheet; 198} 199 200async function updateMusicList(newSheet: IMusic.IMusicItem[]) { 201 const _localSheet = [...newSheet]; 202 try { 203 await setStorage(StorageKeys.LocalMusicSheet, _localSheet); 204 localSheet = _localSheet; 205 localSheetStateMapper.notify(); 206 } catch {} 207} 208 209const LocalMusicSheet = { 210 setup, 211 addMusic, 212 removeMusic, 213 importLocal, 214 cancelImportLocal, 215 isLocalMusic, 216 useIsLocal, 217 getMusicList, 218 useMusicList: localSheetStateMapper.useMappedState, 219 updateMusicList, 220}; 221 222export default LocalMusicSheet; 223