1import {internalSerializeKey, StorageKeys} from '@/constants/commonConst'; 2import mp3Util, {IBasicMeta} from '@/native/mp3Util'; 3import {errorLog} from '@/utils/log'; 4import { 5 getInternalData, 6 InternalDataType, 7 isSameMediaItem, 8} from '@/utils/mediaItem'; 9import StateMapper from '@/utils/stateMapper'; 10import {getStorage, setStorage} from '@/utils/storage'; 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 57export async function removeMusic( 58 musicItem: IMusic.IMusicItem, 59 deleteOriginalFile = false, 60) { 61 const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem)); 62 let newSheet = [...localSheet]; 63 if (idx !== -1) { 64 newSheet.splice(idx, 1); 65 if (deleteOriginalFile && musicItem[internalSerializeKey]?.localPath) { 66 await FileSystem.unlink(musicItem[internalSerializeKey].localPath); 67 } 68 } 69 localSheet = newSheet; 70 localSheetStateMapper.notify(); 71} 72 73function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null { 74 const data = fn.slice(0, fn.lastIndexOf('.')).split('@'); 75 const [platform, id, title, artist] = data; 76 if (!platform || !id) { 77 return null; 78 } 79 return { 80 id, 81 platform, 82 title, 83 artist, 84 }; 85} 86 87/** 从文件夹导入 */ 88async function importFolder(folderPath: string) { 89 const dirFiles = await FileSystem.statDir(folderPath); 90 const musicFiles = dirFiles.filter( 91 // todo: flac播放没有声音 92 _ => 93 _.type === 'file' && 94 (_.filename.endsWith('.mp3') || _.filename.endsWith('.flac')), 95 ); 96 97 const musicItems: IMusic.IMusicItem[] = await Promise.all( 98 musicFiles.map(async mf => { 99 let {platform, id, title, artist} = 100 parseFilename(mf.filename) ?? {}; 101 102 let meta: IBasicMeta | null; 103 try { 104 meta = await mp3Util.getBasicMeta(mf.path); 105 } catch { 106 meta = null; 107 } 108 if (!platform || !id) { 109 platform = '本地'; 110 id = await FileSystem.hash(mf.path, 'MD5'); 111 } 112 return { 113 id, 114 platform, 115 title: title ?? meta?.title ?? mf.filename, 116 artist: artist ?? meta?.artist ?? '未知歌手', 117 duration: parseInt(meta?.duration ?? '0') / 1000, 118 album: meta?.album ?? '未知专辑', 119 artwork: '', 120 [internalSerializeKey]: { 121 localPath: mf.path, 122 }, 123 }; 124 }), 125 ); 126 addMusic(musicItems); 127} 128 129function localMediaFilter(_: FileStat) { 130 return ( 131 _.filename.endsWith('.mp3') || 132 _.filename.endsWith('.flac') || 133 _.filename.endsWith('.wma') 134 ); 135} 136 137// TODO: 需要支持中断&取消 138async function getMusicFiles(folderPath: string) { 139 try { 140 const dirFiles = await FileSystem.statDir(folderPath); 141 const musicFiles: FileStat[] = []; 142 143 await Promise.all( 144 dirFiles.map(async item => { 145 if (item.type === 'directory') { 146 const res = await getMusicFiles(item.path); 147 musicFiles.push(...res); 148 } else if (localMediaFilter(item)) { 149 musicFiles.push(item); 150 } 151 }), 152 ); 153 return musicFiles; 154 } catch (e: any) { 155 errorLog('获取本地文件失败', e?.message); 156 return []; 157 } 158} 159 160// 导入本地音乐 161async function importLocal(folderPaths: string[]) { 162 const musics = await Promise.all(folderPaths.map(_ => getMusicFiles(_))); 163 const musicList = musics.flat(); 164 const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path)); 165 console.log(metas); 166 const musicItems = await Promise.all( 167 musicList.map(async (musicStat, index) => { 168 let {platform, id, title, artist} = 169 parseFilename(musicStat.filename) ?? {}; 170 const meta = metas[index]; 171 if (!platform || !id) { 172 platform = '本地'; 173 id = await FileSystem.hash(musicStat.path, 'MD5'); 174 } 175 return { 176 id, 177 platform, 178 title: title ?? meta?.title ?? musicStat.filename, 179 artist: artist ?? meta?.artist ?? '未知歌手', 180 duration: parseInt(meta?.duration ?? '0') / 1000, 181 album: meta?.album ?? '未知专辑', 182 artwork: '', 183 [internalSerializeKey]: { 184 localPath: musicStat.path, 185 }, 186 }; 187 }), 188 ); 189 addMusic(musicItems); 190} 191 192/** 是否为本地音乐 */ 193function isLocalMusic( 194 musicItem: ICommon.IMediaBase | null, 195): IMusic.IMusicItem | undefined { 196 return musicItem 197 ? localSheet.find(_ => isSameMediaItem(_, musicItem)) 198 : undefined; 199} 200 201/** 状态-是否为本地音乐 */ 202function useIsLocal(musicItem: IMusic.IMusicItem | null) { 203 const localMusicState = localSheetStateMapper.useMappedState(); 204 const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem)); 205 useEffect(() => { 206 if (!musicItem) { 207 setIsLocal(false); 208 } else { 209 setIsLocal(!!isLocalMusic(musicItem)); 210 } 211 }, [localMusicState, musicItem]); 212 return isLocal; 213} 214 215function getMusicList() { 216 return localSheet; 217} 218 219const LocalMusicSheet = { 220 setup, 221 addMusic, 222 removeMusic, 223 importFolder, 224 importLocal, 225 isLocalMusic, 226 useIsLocal, 227 getMusicList, 228 useMusicList: localSheetStateMapper.useMappedState, 229}; 230 231export default LocalMusicSheet; 232