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