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