xref: /MusicFree/src/core/localMusicSheet.ts (revision cd669353b6a483aad2a61863c7df332c267907c6)
10e4173cdS猫头猫import {internalSerializeKey, StorageKeys} from '@/constants/commonConst';
2afb5c234S猫头猫import mp3Util, {IBasicMeta} from '@/native/mp3Util';
3*cd669353S猫头猫import {errorLog} from '@/utils/log';
488772aabS猫头猫import {
588772aabS猫头猫    getInternalData,
688772aabS猫头猫    InternalDataType,
788772aabS猫头猫    isSameMediaItem,
888772aabS猫头猫} from '@/utils/mediaItem';
9afb5c234S猫头猫import StateMapper from '@/utils/stateMapper';
10afb5c234S猫头猫import {getStorage, setStorage} from '@/utils/storage';
110e4173cdS猫头猫import {useEffect, useState} from 'react';
12*cd669353S猫头猫import {FileStat, FileSystem} from 'react-native-file-access';
13afb5c234S猫头猫
14afb5c234S猫头猫let localSheet: IMusic.IMusicItem[] = [];
15afb5c234S猫头猫const localSheetStateMapper = new StateMapper(() => localSheet);
16afb5c234S猫头猫
17afb5c234S猫头猫export async function setup() {
1888772aabS猫头猫    const sheet = await getStorage(StorageKeys.LocalMusicSheet);
1988772aabS猫头猫    if (sheet) {
2088772aabS猫头猫        let validSheet = [];
2188772aabS猫头猫        for (let musicItem of sheet) {
2288772aabS猫头猫            const localPath = getInternalData<string>(
2388772aabS猫头猫                musicItem,
2488772aabS猫头猫                InternalDataType.LOCALPATH,
2588772aabS猫头猫            );
2688772aabS猫头猫            if (localPath && (await FileSystem.exists(localPath))) {
2788772aabS猫头猫                validSheet.push(musicItem);
2888772aabS猫头猫            }
2988772aabS猫头猫        }
3088772aabS猫头猫        if (validSheet.length !== sheet.length) {
3188772aabS猫头猫            await setStorage(StorageKeys.LocalMusicSheet, validSheet);
3288772aabS猫头猫        }
3388772aabS猫头猫        localSheet = validSheet;
34afb5c234S猫头猫    } else {
35afb5c234S猫头猫        await setStorage(StorageKeys.LocalMusicSheet, []);
36afb5c234S猫头猫    }
37afb5c234S猫头猫    localSheetStateMapper.notify();
38afb5c234S猫头猫}
39afb5c234S猫头猫
40afb5c234S猫头猫export async function addMusic(
41afb5c234S猫头猫    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
42afb5c234S猫头猫) {
43afb5c234S猫头猫    if (!Array.isArray(musicItem)) {
44afb5c234S猫头猫        musicItem = [musicItem];
45afb5c234S猫头猫    }
46afb5c234S猫头猫    let newSheet = [...localSheet];
47afb5c234S猫头猫    musicItem.forEach(mi => {
48afb5c234S猫头猫        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
49afb5c234S猫头猫            newSheet.push(mi);
50afb5c234S猫头猫        }
51afb5c234S猫头猫    });
52afb5c234S猫头猫    await setStorage(StorageKeys.LocalMusicSheet, newSheet);
53afb5c234S猫头猫    localSheet = newSheet;
54afb5c234S猫头猫    localSheetStateMapper.notify();
55afb5c234S猫头猫}
56afb5c234S猫头猫
57afb5c234S猫头猫export async function removeMusic(
58afb5c234S猫头猫    musicItem: IMusic.IMusicItem,
59afb5c234S猫头猫    deleteOriginalFile = false,
60afb5c234S猫头猫) {
61afb5c234S猫头猫    const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem));
62afb5c234S猫头猫    let newSheet = [...localSheet];
63afb5c234S猫头猫    if (idx !== -1) {
64afb5c234S猫头猫        newSheet.splice(idx, 1);
650e4173cdS猫头猫        if (deleteOriginalFile && musicItem[internalSerializeKey]?.localPath) {
660e4173cdS猫头猫            await FileSystem.unlink(musicItem[internalSerializeKey].localPath);
67afb5c234S猫头猫        }
68afb5c234S猫头猫    }
69afb5c234S猫头猫    localSheet = newSheet;
70afb5c234S猫头猫    localSheetStateMapper.notify();
71afb5c234S猫头猫}
72afb5c234S猫头猫
73afb5c234S猫头猫function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null {
74afb5c234S猫头猫    const data = fn.slice(0, fn.lastIndexOf('.')).split('@');
75afb5c234S猫头猫    const [platform, id, title, artist] = data;
76afb5c234S猫头猫    if (!platform || !id) {
77afb5c234S猫头猫        return null;
78afb5c234S猫头猫    }
79afb5c234S猫头猫    return {
80afb5c234S猫头猫        id,
81afb5c234S猫头猫        platform,
82afb5c234S猫头猫        title,
83afb5c234S猫头猫        artist,
84afb5c234S猫头猫    };
85afb5c234S猫头猫}
86afb5c234S猫头猫
870e4173cdS猫头猫/** 从文件夹导入 */
880e4173cdS猫头猫async function importFolder(folderPath: string) {
89afb5c234S猫头猫    const dirFiles = await FileSystem.statDir(folderPath);
90afb5c234S猫头猫    const musicFiles = dirFiles.filter(
91ae543cd2S猫头猫        // todo: flac播放没有声音
92ae543cd2S猫头猫        _ =>
93ae543cd2S猫头猫            _.type === 'file' &&
94ae543cd2S猫头猫            (_.filename.endsWith('.mp3') || _.filename.endsWith('.flac')),
95afb5c234S猫头猫    );
96afb5c234S猫头猫
97afb5c234S猫头猫    const musicItems: IMusic.IMusicItem[] = await Promise.all(
98afb5c234S猫头猫        musicFiles.map(async mf => {
99afb5c234S猫头猫            let {platform, id, title, artist} =
100afb5c234S猫头猫                parseFilename(mf.filename) ?? {};
101afb5c234S猫头猫
102afb5c234S猫头猫            let meta: IBasicMeta | null;
103afb5c234S猫头猫            try {
104afb5c234S猫头猫                meta = await mp3Util.getBasicMeta(mf.path);
105afb5c234S猫头猫            } catch {
106afb5c234S猫头猫                meta = null;
107afb5c234S猫头猫            }
108afb5c234S猫头猫            if (!platform || !id) {
109afb5c234S猫头猫                platform = '本地';
110afb5c234S猫头猫                id = await FileSystem.hash(mf.path, 'MD5');
111afb5c234S猫头猫            }
112afb5c234S猫头猫            return {
113afb5c234S猫头猫                id,
114afb5c234S猫头猫                platform,
1152a0db24aS猫头猫                title: title ?? meta?.title ?? mf.filename,
116afb5c234S猫头猫                artist: artist ?? meta?.artist ?? '未知歌手',
117afb5c234S猫头猫                duration: parseInt(meta?.duration ?? '0') / 1000,
1182a0db24aS猫头猫                album: meta?.album ?? '未知专辑',
119afb5c234S猫头猫                artwork: '',
1200e4173cdS猫头猫                [internalSerializeKey]: {
1210e4173cdS猫头猫                    localPath: mf.path,
122afb5c234S猫头猫                },
123afb5c234S猫头猫            };
124afb5c234S猫头猫        }),
125afb5c234S猫头猫    );
126afb5c234S猫头猫    addMusic(musicItems);
127afb5c234S猫头猫}
1280e4173cdS猫头猫
129*cd669353S猫头猫function localMediaFilter(_: FileStat) {
130*cd669353S猫头猫    return (
131*cd669353S猫头猫        _.filename.endsWith('.mp3') ||
132*cd669353S猫头猫        _.filename.endsWith('.flac') ||
133*cd669353S猫头猫        _.filename.endsWith('.wma')
134*cd669353S猫头猫    );
135*cd669353S猫头猫}
136*cd669353S猫头猫
137*cd669353S猫头猫// TODO: 需要支持中断&取消
138*cd669353S猫头猫async function getMusicFiles(folderPath: string) {
139*cd669353S猫头猫    try {
140*cd669353S猫头猫        const dirFiles = await FileSystem.statDir(folderPath);
141*cd669353S猫头猫        const musicFiles: FileStat[] = [];
142*cd669353S猫头猫
143*cd669353S猫头猫        await Promise.all(
144*cd669353S猫头猫            dirFiles.map(async item => {
145*cd669353S猫头猫                if (item.type === 'directory') {
146*cd669353S猫头猫                    const res = await getMusicFiles(item.path);
147*cd669353S猫头猫                    musicFiles.push(...res);
148*cd669353S猫头猫                } else if (localMediaFilter(item)) {
149*cd669353S猫头猫                    musicFiles.push(item);
150*cd669353S猫头猫                }
151*cd669353S猫头猫            }),
152*cd669353S猫头猫        );
153*cd669353S猫头猫        return musicFiles;
154*cd669353S猫头猫    } catch (e: any) {
155*cd669353S猫头猫        errorLog('获取本地文件失败', e?.message);
156*cd669353S猫头猫        return [];
157*cd669353S猫头猫    }
158*cd669353S猫头猫}
159*cd669353S猫头猫
160*cd669353S猫头猫// 导入本地音乐
161*cd669353S猫头猫async function importLocal(folderPaths: string[]) {
162*cd669353S猫头猫    const musics = await Promise.all(folderPaths.map(_ => getMusicFiles(_)));
163*cd669353S猫头猫    const musicList = musics.flat();
164*cd669353S猫头猫    const metas = await mp3Util.getMediaMeta(musicList.map(_ => _.path));
165*cd669353S猫头猫    console.log(metas);
166*cd669353S猫头猫    const musicItems = await Promise.all(
167*cd669353S猫头猫        musicList.map(async (musicStat, index) => {
168*cd669353S猫头猫            let {platform, id, title, artist} =
169*cd669353S猫头猫                parseFilename(musicStat.filename) ?? {};
170*cd669353S猫头猫            const meta = metas[index];
171*cd669353S猫头猫            if (!platform || !id) {
172*cd669353S猫头猫                platform = '本地';
173*cd669353S猫头猫                id = await FileSystem.hash(musicStat.path, 'MD5');
174*cd669353S猫头猫            }
175*cd669353S猫头猫            return {
176*cd669353S猫头猫                id,
177*cd669353S猫头猫                platform,
178*cd669353S猫头猫                title: title ?? meta?.title ?? musicStat.filename,
179*cd669353S猫头猫                artist: artist ?? meta?.artist ?? '未知歌手',
180*cd669353S猫头猫                duration: parseInt(meta?.duration ?? '0') / 1000,
181*cd669353S猫头猫                album: meta?.album ?? '未知专辑',
182*cd669353S猫头猫                artwork: '',
183*cd669353S猫头猫                [internalSerializeKey]: {
184*cd669353S猫头猫                    localPath: musicStat.path,
185*cd669353S猫头猫                },
186*cd669353S猫头猫            };
187*cd669353S猫头猫        }),
188*cd669353S猫头猫    );
189*cd669353S猫头猫    addMusic(musicItems);
190*cd669353S猫头猫}
191*cd669353S猫头猫
1920e4173cdS猫头猫/** 是否为本地音乐 */
1930e4173cdS猫头猫function isLocalMusic(
1940e4173cdS猫头猫    musicItem: ICommon.IMediaBase | null,
1950e4173cdS猫头猫): IMusic.IMusicItem | undefined {
1960e4173cdS猫头猫    return musicItem
1970e4173cdS猫头猫        ? localSheet.find(_ => isSameMediaItem(_, musicItem))
1980e4173cdS猫头猫        : undefined;
1990e4173cdS猫头猫}
2000e4173cdS猫头猫
2010e4173cdS猫头猫/** 状态-是否为本地音乐 */
2020e4173cdS猫头猫function useIsLocal(musicItem: IMusic.IMusicItem | null) {
2030e4173cdS猫头猫    const localMusicState = localSheetStateMapper.useMappedState();
2040e4173cdS猫头猫    const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem));
2050e4173cdS猫头猫    useEffect(() => {
2060e4173cdS猫头猫        if (!musicItem) {
2070e4173cdS猫头猫            setIsLocal(false);
2080e4173cdS猫头猫        } else {
2090e4173cdS猫头猫            setIsLocal(!!isLocalMusic(musicItem));
2100e4173cdS猫头猫        }
2110e4173cdS猫头猫    }, [localMusicState, musicItem]);
2120e4173cdS猫头猫    return isLocal;
2130e4173cdS猫头猫}
2140e4173cdS猫头猫
2150224b881S猫头猫function getMusicList() {
2160224b881S猫头猫    return localSheet;
2170224b881S猫头猫}
2180224b881S猫头猫
2190e4173cdS猫头猫const LocalMusicSheet = {
2200e4173cdS猫头猫    setup,
2210e4173cdS猫头猫    addMusic,
2220e4173cdS猫头猫    removeMusic,
2230e4173cdS猫头猫    importFolder,
224*cd669353S猫头猫    importLocal,
2250e4173cdS猫头猫    isLocalMusic,
2260e4173cdS猫头猫    useIsLocal,
2270224b881S猫头猫    getMusicList,
2280e4173cdS猫头猫    useMusicList: localSheetStateMapper.useMappedState,
2290e4173cdS猫头猫};
2300e4173cdS猫头猫
2310e4173cdS猫头猫export default LocalMusicSheet;
232