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} 105 106function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null { 107 const data = fn.slice(0, fn.lastIndexOf('.')).split('@'); 108 const [platform, id, title, artist] = data; 109 if (!platform || !id) { 110 return null; 111 } 112 return { 113 id, 114 platform: platform, 115 title: title ?? '', 116 artist: artist ?? '', 117 }; 118} 119 120function localMediaFilter(_: FileStat) { 121 return supportLocalMediaType.some(ext => _.filename.endsWith(ext)); 122} 123 124let importToken: string | null = null; 125// 获取本地的文件列表 126async function getMusicStats(folderPaths: string[]) { 127 const _importToken = nanoid(); 128 importToken = _importToken; 129 const musicList: FileStat[] = []; 130 let peek: string | undefined; 131 let dirFiles: FileStat[] = []; 132 while (folderPaths.length !== 0) { 133 if (importToken !== _importToken) { 134 throw new Error('Import Broken'); 135 } 136 peek = folderPaths.shift() as string; 137 try { 138 dirFiles = await FileSystem.statDir(peek); 139 } catch { 140 dirFiles = []; 141 } 142 143 dirFiles.forEach(item => { 144 if (item.type === 'directory' && !folderPaths.includes(item.path)) { 145 folderPaths.push(item.path); 146 } else if (localMediaFilter(item)) { 147 musicList.push(item); 148 } 149 }); 150 } 151 return {musicList, token: _importToken}; 152} 153 154function cancelImportLocal() { 155 importToken = null; 156} 157 158// 导入本地音乐 159const groupNum = 25; 160async function importLocal(_folderPaths: string[]) { 161 const folderPaths = [..._folderPaths]; 162 const {musicList, token} = await getMusicStats(folderPaths); 163 if (token !== importToken) { 164 throw new Error('Import Broken'); 165 } 166 // 分组请求,不然序列化可能出问题 167 let metas: IBasicMeta[] = []; 168 const groups = Math.ceil(musicList.length / groupNum); 169 for (let i = 0; i < groups; ++i) { 170 metas = metas.concat( 171 await mp3Util.getMediaMeta( 172 musicList 173 .slice(i * groupNum, (i + 1) * groupNum) 174 .map(_ => _.path), 175 ), 176 ); 177 } 178 if (token !== importToken) { 179 throw new Error('Import Broken'); 180 } 181 const musicItems = await Promise.all( 182 musicList.map(async (musicStat, index) => { 183 let {platform, id, title, artist} = 184 parseFilename(musicStat.filename) ?? {}; 185 const meta = metas[index]; 186 if (!platform || !id) { 187 platform = '本地'; 188 id = await FileSystem.hash(musicStat.path, 'MD5'); 189 } 190 return { 191 id, 192 platform, 193 title: title ?? meta?.title ?? musicStat.filename, 194 artist: artist ?? meta?.artist ?? '未知歌手', 195 duration: parseInt(meta?.duration ?? '0') / 1000, 196 album: meta?.album ?? '未知专辑', 197 artwork: '', 198 [internalSerializeKey]: { 199 localPath: musicStat.path, 200 }, 201 }; 202 }), 203 ); 204 if (token !== importToken) { 205 throw new Error('Import Broken'); 206 } 207 addMusic(musicItems); 208} 209 210/** 是否为本地音乐 */ 211function isLocalMusic( 212 musicItem: ICommon.IMediaBase | null, 213): IMusic.IMusicItem | undefined { 214 return musicItem 215 ? localSheet.find(_ => isSameMediaItem(_, musicItem)) 216 : undefined; 217} 218 219/** 状态-是否为本地音乐 */ 220function useIsLocal(musicItem: IMusic.IMusicItem | null) { 221 const localMusicState = localSheetStateMapper.useMappedState(); 222 const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem)); 223 useEffect(() => { 224 if (!musicItem) { 225 setIsLocal(false); 226 } else { 227 setIsLocal(!!isLocalMusic(musicItem)); 228 } 229 }, [localMusicState, musicItem]); 230 return isLocal; 231} 232 233function getMusicList() { 234 return localSheet; 235} 236 237async function updateMusicList(newSheet: IMusic.IMusicItem[]) { 238 const _localSheet = [...newSheet]; 239 try { 240 await setStorage(StorageKeys.LocalMusicSheet, _localSheet); 241 localSheet = _localSheet; 242 localSheetStateMapper.notify(); 243 } catch {} 244} 245 246const LocalMusicSheet = { 247 setup, 248 addMusic, 249 removeMusic, 250 addMusicDraft, 251 saveLocalSheet, 252 importLocal, 253 cancelImportLocal, 254 isLocalMusic, 255 useIsLocal, 256 getMusicList, 257 useMusicList: localSheetStateMapper.useMappedState, 258 updateMusicList, 259}; 260 261export default LocalMusicSheet; 262