1import {internalSerializeKey, StorageKeys} from '@/constants/commonConst'; 2import mp3Util, {IBasicMeta} from '@/native/mp3Util'; 3import { 4 getInternalData, 5 InternalDataType, 6 isSameMediaItem, 7} from '@/utils/mediaItem'; 8import StateMapper from '@/utils/stateMapper'; 9import {getStorage, setStorage} from '@/utils/storage'; 10import {nanoid} from 'nanoid'; 11import {useEffect, useState} from 'react'; 12import {FileStat, FileSystem} from 'react-native-file-access'; 13 14let localSheet: IMusic.IMusicItem[] = []; 15const localSheetStateMapper = new StateMapper(() => localSheet); 16 17export async function setup() { 18 const sheet = await getStorage(StorageKeys.LocalMusicSheet); 19 if (sheet) { 20 let validSheet = []; 21 for (let musicItem of sheet) { 22 const localPath = getInternalData<string>( 23 musicItem, 24 InternalDataType.LOCALPATH, 25 ); 26 if (localPath && (await FileSystem.exists(localPath))) { 27 validSheet.push(musicItem); 28 } 29 } 30 if (validSheet.length !== sheet.length) { 31 await setStorage(StorageKeys.LocalMusicSheet, validSheet); 32 } 33 localSheet = validSheet; 34 } else { 35 await setStorage(StorageKeys.LocalMusicSheet, []); 36 } 37 localSheetStateMapper.notify(); 38} 39 40export async function addMusic( 41 musicItem: IMusic.IMusicItem | IMusic.IMusicItem[], 42) { 43 if (!Array.isArray(musicItem)) { 44 musicItem = [musicItem]; 45 } 46 let newSheet = [...localSheet]; 47 musicItem.forEach(mi => { 48 if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) { 49 newSheet.push(mi); 50 } 51 }); 52 await setStorage(StorageKeys.LocalMusicSheet, newSheet); 53 localSheet = newSheet; 54 localSheetStateMapper.notify(); 55} 56 57function addMusicDraft(musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) { 58 if (!Array.isArray(musicItem)) { 59 musicItem = [musicItem]; 60 } 61 let newSheet = [...localSheet]; 62 musicItem.forEach(mi => { 63 if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) { 64 newSheet.push(mi); 65 } 66 }); 67 localSheet = newSheet; 68 localSheetStateMapper.notify(); 69} 70 71async function saveLocalSheet() { 72 await setStorage(StorageKeys.LocalMusicSheet, localSheet); 73} 74 75export async function removeMusic( 76 musicItem: IMusic.IMusicItem, 77 deleteOriginalFile = false, 78) { 79 const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem)); 80 let newSheet = [...localSheet]; 81 if (idx !== -1) { 82 const localMusicItem = localSheet[idx]; 83 newSheet.splice(idx, 1); 84 const localPath = 85 musicItem[internalSerializeKey]?.localPath ?? 86 localMusicItem[internalSerializeKey]?.localPath; 87 if (deleteOriginalFile && localPath) { 88 await FileSystem.unlink(localPath); 89 } 90 } 91 localSheet = newSheet; 92 localSheetStateMapper.notify(); 93} 94 95function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null { 96 const data = fn.slice(0, fn.lastIndexOf('.')).split('@'); 97 const [platform, id, title, artist] = data; 98 if (!platform || !id) { 99 return null; 100 } 101 return { 102 id, 103 platform, 104 title, 105 artist, 106 }; 107} 108 109function localMediaFilter(_: FileStat) { 110 return ( 111 _.filename.endsWith('.mp3') || 112 _.filename.endsWith('.flac') || 113 _.filename.endsWith('.wma') || 114 _.filename.endsWith('.wav') || 115 _.filename.endsWith('.m4a') || 116 _.filename.endsWith('.ogg') || 117 _.filename.endsWith('.acc') || 118 _.filename.endsWith('.aac') || 119 _.filename.endsWith('.ape') || 120 _.filename.endsWith('.m4s') 121 ); 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 = 200; 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