1import {internalSerializeKey, StorageKeys} from '@/constants/commonConst'; 2import mp3Util 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// 导入本地音乐 159async function importLocal(_folderPaths: string[]) { 160 const folderPaths = [..._folderPaths]; 161 const {musicList, token} = await getMusicStats(folderPaths); 162 if (token !== importToken) { 163 throw new Error('Import Broken'); 164 } 165 const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path)); 166 if (token !== importToken) { 167 throw new Error('Import Broken'); 168 } 169 const musicItems = await Promise.all( 170 musicList.map(async (musicStat, index) => { 171 let {platform, id, title, artist} = 172 parseFilename(musicStat.filename) ?? {}; 173 const meta = metas[index]; 174 if (!platform || !id) { 175 platform = '本地'; 176 id = await FileSystem.hash(musicStat.path, 'MD5'); 177 } 178 return { 179 id, 180 platform, 181 title: title ?? meta?.title ?? musicStat.filename, 182 artist: artist ?? meta?.artist ?? '未知歌手', 183 duration: parseInt(meta?.duration ?? '0') / 1000, 184 album: meta?.album ?? '未知专辑', 185 artwork: '', 186 [internalSerializeKey]: { 187 localPath: musicStat.path, 188 }, 189 }; 190 }), 191 ); 192 if (token !== importToken) { 193 throw new Error('Import Broken'); 194 } 195 addMusic(musicItems); 196} 197 198/** 是否为本地音乐 */ 199function isLocalMusic( 200 musicItem: ICommon.IMediaBase | null, 201): IMusic.IMusicItem | undefined { 202 return musicItem 203 ? localSheet.find(_ => isSameMediaItem(_, musicItem)) 204 : undefined; 205} 206 207/** 状态-是否为本地音乐 */ 208function useIsLocal(musicItem: IMusic.IMusicItem | null) { 209 const localMusicState = localSheetStateMapper.useMappedState(); 210 const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem)); 211 useEffect(() => { 212 if (!musicItem) { 213 setIsLocal(false); 214 } else { 215 setIsLocal(!!isLocalMusic(musicItem)); 216 } 217 }, [localMusicState, musicItem]); 218 return isLocal; 219} 220 221function getMusicList() { 222 return localSheet; 223} 224 225async function updateMusicList(newSheet: IMusic.IMusicItem[]) { 226 const _localSheet = [...newSheet]; 227 try { 228 await setStorage(StorageKeys.LocalMusicSheet, _localSheet); 229 localSheet = _localSheet; 230 localSheetStateMapper.notify(); 231 } catch {} 232} 233 234const LocalMusicSheet = { 235 setup, 236 addMusic, 237 removeMusic, 238 addMusicDraft, 239 saveLocalSheet, 240 importLocal, 241 cancelImportLocal, 242 isLocalMusic, 243 useIsLocal, 244 getMusicList, 245 useMusicList: localSheetStateMapper.useMappedState, 246 updateMusicList, 247}; 248 249export default LocalMusicSheet; 250