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