xref: /MusicFree/src/core/localMusicSheet.ts (revision 9501a53846abeb6e52d388c9c5031f73671944ef)
1import {internalSerializeKey, StorageKeys} from '@/constants/commonConst';
2import mp3Util, {IBasicMeta} 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 {useEffect, useState} from 'react';
11import {FileSystem} from 'react-native-file-access';
12
13let localSheet: IMusic.IMusicItem[] = [];
14const localSheetStateMapper = new StateMapper(() => localSheet);
15
16export async function setup() {
17    const sheet = await getStorage(StorageKeys.LocalMusicSheet);
18    if (sheet) {
19        let validSheet = [];
20        for (let musicItem of sheet) {
21            const localPath = getInternalData<string>(
22                musicItem,
23                InternalDataType.LOCALPATH,
24            );
25            if (localPath && (await FileSystem.exists(localPath))) {
26                validSheet.push(musicItem);
27            }
28        }
29        if (validSheet.length !== sheet.length) {
30            await setStorage(StorageKeys.LocalMusicSheet, validSheet);
31        }
32        localSheet = validSheet;
33    } else {
34        await setStorage(StorageKeys.LocalMusicSheet, []);
35    }
36    localSheetStateMapper.notify();
37}
38
39export async function addMusic(
40    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
41) {
42    if (!Array.isArray(musicItem)) {
43        musicItem = [musicItem];
44    }
45    let newSheet = [...localSheet];
46    musicItem.forEach(mi => {
47        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
48            newSheet.push(mi);
49        }
50    });
51    await setStorage(StorageKeys.LocalMusicSheet, newSheet);
52    localSheet = newSheet;
53    localSheetStateMapper.notify();
54}
55
56export async function removeMusic(
57    musicItem: IMusic.IMusicItem,
58    deleteOriginalFile = false,
59) {
60    const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem));
61    let newSheet = [...localSheet];
62    if (idx !== -1) {
63        newSheet.splice(idx, 1);
64        if (deleteOriginalFile && musicItem[internalSerializeKey]?.localPath) {
65            await FileSystem.unlink(musicItem[internalSerializeKey].localPath);
66        }
67    }
68    localSheet = newSheet;
69    localSheetStateMapper.notify();
70}
71
72function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null {
73    const data = fn.slice(0, fn.lastIndexOf('.')).split('@');
74    const [platform, id, title, artist] = data;
75    if (!platform || !id) {
76        return null;
77    }
78    return {
79        id,
80        platform,
81        title,
82        artist,
83    };
84}
85
86/** 从文件夹导入 */
87async function importFolder(folderPath: string) {
88    const dirFiles = await FileSystem.statDir(folderPath);
89    const musicFiles = dirFiles.filter(
90        _ => _.type === 'file' && _.filename.endsWith('.mp3'),
91    );
92
93    const musicItems: IMusic.IMusicItem[] = await Promise.all(
94        musicFiles.map(async mf => {
95            let {platform, id, title, artist} =
96                parseFilename(mf.filename) ?? {};
97
98            let meta: IBasicMeta | null;
99            try {
100                meta = await mp3Util.getBasicMeta(mf.path);
101            } catch {
102                meta = null;
103            }
104            if (!platform || !id) {
105                platform = '本地';
106                id = await FileSystem.hash(mf.path, 'MD5');
107            }
108            return {
109                id,
110                platform,
111                title: title ?? meta?.title ?? '未知名称',
112                artist: artist ?? meta?.artist ?? '未知歌手',
113                duration: parseInt(meta?.duration ?? '0') / 1000,
114                album: meta?.album ?? '',
115                artwork: '',
116                [internalSerializeKey]: {
117                    localPath: mf.path,
118                },
119            };
120        }),
121    );
122    addMusic(musicItems);
123}
124
125/** 是否为本地音乐 */
126function isLocalMusic(
127    musicItem: ICommon.IMediaBase | null,
128): IMusic.IMusicItem | undefined {
129    return musicItem
130        ? localSheet.find(_ => isSameMediaItem(_, musicItem))
131        : undefined;
132}
133
134/** 状态-是否为本地音乐 */
135function useIsLocal(musicItem: IMusic.IMusicItem | null) {
136    const localMusicState = localSheetStateMapper.useMappedState();
137    const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem));
138    useEffect(() => {
139        if (!musicItem) {
140            setIsLocal(false);
141        } else {
142            setIsLocal(!!isLocalMusic(musicItem));
143        }
144    }, [localMusicState, musicItem]);
145    return isLocal;
146}
147
148function getMusicList() {
149    return localSheet;
150}
151
152const LocalMusicSheet = {
153    setup,
154    addMusic,
155    removeMusic,
156    importFolder,
157    isLocalMusic,
158    useIsLocal,
159    getMusicList,
160    useMusicList: localSheetStateMapper.useMappedState,
161};
162
163export default LocalMusicSheet;
164