xref: /MusicFree/src/core/localMusicSheet.ts (revision 7993f90e543777feeadd153fa65d719529df316d)
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
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
87function localMediaFilter(_: FileStat) {
88    return (
89        _.filename.endsWith('.mp3') ||
90        _.filename.endsWith('.flac') ||
91        _.filename.endsWith('.wma') ||
92        _.filename.endsWith('.wav') ||
93        _.filename.endsWith('.m4a') ||
94        _.filename.endsWith('.ogg') ||
95        _.filename.endsWith('.acc')
96    );
97}
98
99let importToken: string | null = null;
100// 获取本地的文件列表
101async function getMusicStats(folderPaths: string[]) {
102    const _importToken = nanoid();
103    importToken = _importToken;
104    const musicList: FileStat[] = [];
105    let peek: string | undefined;
106    let dirFiles: FileStat[] = [];
107    while (folderPaths.length !== 0) {
108        if (importToken !== _importToken) {
109            throw new Error('Import Broken');
110        }
111        peek = folderPaths.shift() as string;
112        try {
113            dirFiles = await FileSystem.statDir(peek);
114        } catch {
115            dirFiles = [];
116        }
117
118        dirFiles.forEach(item => {
119            if (item.type === 'directory' && !folderPaths.includes(item.path)) {
120                folderPaths.push(item.path);
121            } else if (localMediaFilter(item)) {
122                musicList.push(item);
123            }
124        });
125    }
126    return {musicList, token: _importToken};
127}
128
129function cancelImportLocal() {
130    importToken = null;
131}
132
133// 导入本地音乐
134async function importLocal(_folderPaths: string[]) {
135    const folderPaths = [..._folderPaths];
136    const {musicList, token} = await getMusicStats(folderPaths);
137    if (token !== importToken) {
138        throw new Error('Import Broken');
139    }
140    const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path));
141    if (token !== importToken) {
142        throw new Error('Import Broken');
143    }
144    const musicItems = await Promise.all(
145        musicList.map(async (musicStat, index) => {
146            let {platform, id, title, artist} =
147                parseFilename(musicStat.filename) ?? {};
148            const meta = metas[index];
149            if (!platform || !id) {
150                platform = '本地';
151                id = await FileSystem.hash(musicStat.path, 'MD5');
152            }
153            return {
154                id,
155                platform,
156                title: title ?? meta?.title ?? musicStat.filename,
157                artist: artist ?? meta?.artist ?? '未知歌手',
158                duration: parseInt(meta?.duration ?? '0') / 1000,
159                album: meta?.album ?? '未知专辑',
160                artwork: '',
161                [internalSerializeKey]: {
162                    localPath: musicStat.path,
163                },
164            };
165        }),
166    );
167    if (token !== importToken) {
168        throw new Error('Import Broken');
169    }
170    addMusic(musicItems);
171}
172
173/** 是否为本地音乐 */
174function isLocalMusic(
175    musicItem: ICommon.IMediaBase | null,
176): IMusic.IMusicItem | undefined {
177    return musicItem
178        ? localSheet.find(_ => isSameMediaItem(_, musicItem))
179        : undefined;
180}
181
182/** 状态-是否为本地音乐 */
183function useIsLocal(musicItem: IMusic.IMusicItem | null) {
184    const localMusicState = localSheetStateMapper.useMappedState();
185    const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem));
186    useEffect(() => {
187        if (!musicItem) {
188            setIsLocal(false);
189        } else {
190            setIsLocal(!!isLocalMusic(musicItem));
191        }
192    }, [localMusicState, musicItem]);
193    return isLocal;
194}
195
196function getMusicList() {
197    return localSheet;
198}
199
200async function updateMusicList(newSheet: IMusic.IMusicItem[]) {
201    const _localSheet = [...newSheet];
202    try {
203        await setStorage(StorageKeys.LocalMusicSheet, _localSheet);
204        localSheet = _localSheet;
205        localSheetStateMapper.notify();
206    } catch {}
207}
208
209const LocalMusicSheet = {
210    setup,
211    addMusic,
212    removeMusic,
213    importLocal,
214    cancelImportLocal,
215    isLocalMusic,
216    useIsLocal,
217    getMusicList,
218    useMusicList: localSheetStateMapper.useMappedState,
219    updateMusicList,
220};
221
222export default LocalMusicSheet;
223