xref: /MusicFree/src/core/localMusicSheet.ts (revision 2aa881935ca35b8fb1abc4206e0dc35149231456)
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';
17
18let localSheet: IMusic.IMusicItem[] = [];
19const localSheetStateMapper = new StateMapper(() => localSheet);
20
21export async function setup() {
22    const sheet = await getStorage(StorageKeys.LocalMusicSheet);
23    if (sheet) {
24        let validSheet = [];
25        for (let musicItem of sheet) {
26            const localPath = getInternalData<string>(
27                musicItem,
28                InternalDataType.LOCALPATH,
29            );
30            if (localPath && (await FileSystem.exists(localPath))) {
31                validSheet.push(musicItem);
32            }
33        }
34        if (validSheet.length !== sheet.length) {
35            await setStorage(StorageKeys.LocalMusicSheet, validSheet);
36        }
37        localSheet = validSheet;
38    } else {
39        await setStorage(StorageKeys.LocalMusicSheet, []);
40    }
41    localSheetStateMapper.notify();
42}
43
44export async function addMusic(
45    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
46) {
47    if (!Array.isArray(musicItem)) {
48        musicItem = [musicItem];
49    }
50    let newSheet = [...localSheet];
51    musicItem.forEach(mi => {
52        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
53            newSheet.push(mi);
54        }
55    });
56    await setStorage(StorageKeys.LocalMusicSheet, newSheet);
57    localSheet = newSheet;
58    localSheetStateMapper.notify();
59}
60
61function addMusicDraft(musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) {
62    if (!Array.isArray(musicItem)) {
63        musicItem = [musicItem];
64    }
65    let newSheet = [...localSheet];
66    musicItem.forEach(mi => {
67        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
68            newSheet.push(mi);
69        }
70    });
71    localSheet = newSheet;
72    localSheetStateMapper.notify();
73}
74
75async function saveLocalSheet() {
76    await setStorage(StorageKeys.LocalMusicSheet, localSheet);
77}
78
79export async function removeMusic(
80    musicItem: IMusic.IMusicItem,
81    deleteOriginalFile = false,
82) {
83    const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem));
84    let newSheet = [...localSheet];
85    if (idx !== -1) {
86        const localMusicItem = localSheet[idx];
87        newSheet.splice(idx, 1);
88        const localPath =
89            musicItem[internalSerializeKey]?.localPath ??
90            localMusicItem[internalSerializeKey]?.localPath;
91        if (deleteOriginalFile && localPath) {
92            await FileSystem.unlink(localPath);
93        }
94    }
95    localSheet = newSheet;
96    localSheetStateMapper.notify();
97}
98
99function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null {
100    const data = fn.slice(0, fn.lastIndexOf('.')).split('@');
101    const [platform, id, title, artist] = data;
102    if (!platform || !id) {
103        return null;
104    }
105    return {
106        id,
107        platform,
108        title,
109        artist,
110    };
111}
112
113function localMediaFilter(_: FileStat) {
114    return supportLocalMediaType.some(ext => _.filename.endsWith(ext));
115}
116
117let importToken: string | null = null;
118// 获取本地的文件列表
119async function getMusicStats(folderPaths: string[]) {
120    const _importToken = nanoid();
121    importToken = _importToken;
122    const musicList: FileStat[] = [];
123    let peek: string | undefined;
124    let dirFiles: FileStat[] = [];
125    while (folderPaths.length !== 0) {
126        if (importToken !== _importToken) {
127            throw new Error('Import Broken');
128        }
129        peek = folderPaths.shift() as string;
130        try {
131            dirFiles = await FileSystem.statDir(peek);
132        } catch {
133            dirFiles = [];
134        }
135
136        dirFiles.forEach(item => {
137            if (item.type === 'directory' && !folderPaths.includes(item.path)) {
138                folderPaths.push(item.path);
139            } else if (localMediaFilter(item)) {
140                musicList.push(item);
141            }
142        });
143    }
144    return {musicList, token: _importToken};
145}
146
147function cancelImportLocal() {
148    importToken = null;
149}
150
151// 导入本地音乐
152const groupNum = 50;
153async function importLocal(_folderPaths: string[]) {
154    const folderPaths = [..._folderPaths];
155    const {musicList, token} = await getMusicStats(folderPaths);
156    if (token !== importToken) {
157        throw new Error('Import Broken');
158    }
159    // 分组请求,不然序列化可能出问题
160    let metas: IBasicMeta[] = [];
161    const groups = Math.ceil(musicList.length / groupNum);
162    for (let i = 0; i < groups; ++i) {
163        metas = metas.concat(
164            await mp3Util.getMediaMeta(
165                musicList
166                    .slice(i * groupNum, (i + 1) * groupNum)
167                    .map(_ => _.path),
168            ),
169        );
170    }
171    if (token !== importToken) {
172        throw new Error('Import Broken');
173    }
174    const musicItems = await Promise.all(
175        musicList.map(async (musicStat, index) => {
176            let {platform, id, title, artist} =
177                parseFilename(musicStat.filename) ?? {};
178            const meta = metas[index];
179            if (!platform || !id) {
180                platform = '本地';
181                id = await FileSystem.hash(musicStat.path, 'MD5');
182            }
183            return {
184                id,
185                platform,
186                title: title ?? meta?.title ?? musicStat.filename,
187                artist: artist ?? meta?.artist ?? '未知歌手',
188                duration: parseInt(meta?.duration ?? '0') / 1000,
189                album: meta?.album ?? '未知专辑',
190                artwork: '',
191                [internalSerializeKey]: {
192                    localPath: musicStat.path,
193                },
194            };
195        }),
196    );
197    if (token !== importToken) {
198        throw new Error('Import Broken');
199    }
200    addMusic(musicItems);
201}
202
203/** 是否为本地音乐 */
204function isLocalMusic(
205    musicItem: ICommon.IMediaBase | null,
206): IMusic.IMusicItem | undefined {
207    return musicItem
208        ? localSheet.find(_ => isSameMediaItem(_, musicItem))
209        : undefined;
210}
211
212/** 状态-是否为本地音乐 */
213function useIsLocal(musicItem: IMusic.IMusicItem | null) {
214    const localMusicState = localSheetStateMapper.useMappedState();
215    const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem));
216    useEffect(() => {
217        if (!musicItem) {
218            setIsLocal(false);
219        } else {
220            setIsLocal(!!isLocalMusic(musicItem));
221        }
222    }, [localMusicState, musicItem]);
223    return isLocal;
224}
225
226function getMusicList() {
227    return localSheet;
228}
229
230async function updateMusicList(newSheet: IMusic.IMusicItem[]) {
231    const _localSheet = [...newSheet];
232    try {
233        await setStorage(StorageKeys.LocalMusicSheet, _localSheet);
234        localSheet = _localSheet;
235        localSheetStateMapper.notify();
236    } catch {}
237}
238
239const LocalMusicSheet = {
240    setup,
241    addMusic,
242    removeMusic,
243    addMusicDraft,
244    saveLocalSheet,
245    importLocal,
246    cancelImportLocal,
247    isLocalMusic,
248    useIsLocal,
249    getMusicList,
250    useMusicList: localSheetStateMapper.useMappedState,
251    updateMusicList,
252};
253
254export default LocalMusicSheet;
255