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