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 _.filename.endsWith('.aac') || 97 _.filename.endsWith('.ape') 98 ); 99} 100 101let importToken: string | null = null; 102// 获取本地的文件列表 103async function getMusicStats(folderPaths: string[]) { 104 const _importToken = nanoid(); 105 importToken = _importToken; 106 const musicList: FileStat[] = []; 107 let peek: string | undefined; 108 let dirFiles: FileStat[] = []; 109 while (folderPaths.length !== 0) { 110 if (importToken !== _importToken) { 111 throw new Error('Import Broken'); 112 } 113 peek = folderPaths.shift() as string; 114 try { 115 dirFiles = await FileSystem.statDir(peek); 116 } catch { 117 dirFiles = []; 118 } 119 120 dirFiles.forEach(item => { 121 if (item.type === 'directory' && !folderPaths.includes(item.path)) { 122 folderPaths.push(item.path); 123 } else if (localMediaFilter(item)) { 124 musicList.push(item); 125 } 126 }); 127 } 128 return {musicList, token: _importToken}; 129} 130 131function cancelImportLocal() { 132 importToken = null; 133} 134 135// 导入本地音乐 136async function importLocal(_folderPaths: string[]) { 137 const folderPaths = [..._folderPaths]; 138 const {musicList, token} = await getMusicStats(folderPaths); 139 if (token !== importToken) { 140 throw new Error('Import Broken'); 141 } 142 const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path)); 143 if (token !== importToken) { 144 throw new Error('Import Broken'); 145 } 146 const musicItems = await Promise.all( 147 musicList.map(async (musicStat, index) => { 148 let {platform, id, title, artist} = 149 parseFilename(musicStat.filename) ?? {}; 150 const meta = metas[index]; 151 if (!platform || !id) { 152 platform = '本地'; 153 id = await FileSystem.hash(musicStat.path, 'MD5'); 154 } 155 return { 156 id, 157 platform, 158 title: title ?? meta?.title ?? musicStat.filename, 159 artist: artist ?? meta?.artist ?? '未知歌手', 160 duration: parseInt(meta?.duration ?? '0') / 1000, 161 album: meta?.album ?? '未知专辑', 162 artwork: '', 163 [internalSerializeKey]: { 164 localPath: musicStat.path, 165 }, 166 }; 167 }), 168 ); 169 if (token !== importToken) { 170 throw new Error('Import Broken'); 171 } 172 addMusic(musicItems); 173} 174 175/** 是否为本地音乐 */ 176function isLocalMusic( 177 musicItem: ICommon.IMediaBase | null, 178): IMusic.IMusicItem | undefined { 179 return musicItem 180 ? localSheet.find(_ => isSameMediaItem(_, musicItem)) 181 : undefined; 182} 183 184/** 状态-是否为本地音乐 */ 185function useIsLocal(musicItem: IMusic.IMusicItem | null) { 186 const localMusicState = localSheetStateMapper.useMappedState(); 187 const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem)); 188 useEffect(() => { 189 if (!musicItem) { 190 setIsLocal(false); 191 } else { 192 setIsLocal(!!isLocalMusic(musicItem)); 193 } 194 }, [localMusicState, musicItem]); 195 return isLocal; 196} 197 198function getMusicList() { 199 return localSheet; 200} 201 202async function updateMusicList(newSheet: IMusic.IMusicItem[]) { 203 const _localSheet = [...newSheet]; 204 try { 205 await setStorage(StorageKeys.LocalMusicSheet, _localSheet); 206 localSheet = _localSheet; 207 localSheetStateMapper.notify(); 208 } catch {} 209} 210 211const LocalMusicSheet = { 212 setup, 213 addMusic, 214 removeMusic, 215 importLocal, 216 cancelImportLocal, 217 isLocalMusic, 218 useIsLocal, 219 getMusicList, 220 useMusicList: localSheetStateMapper.useMappedState, 221 updateMusicList, 222}; 223 224export default LocalMusicSheet; 225