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