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