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