xref: /MusicFree/src/core/localMusicSheet.ts (revision 5353b47372c7f041c41058084167f60418a3c9fc)
18fc75cb2S猫头猫import {
28fc75cb2S猫头猫    internalSerializeKey,
38fc75cb2S猫头猫    StorageKeys,
48fc75cb2S猫头猫    supportLocalMediaType,
58fc75cb2S猫头猫} from '@/constants/commonConst';
68423b2d4S猫头猫import mp3Util, {IBasicMeta} from '@/native/mp3Util';
788772aabS猫头猫import {
888772aabS猫头猫    getInternalData,
988772aabS猫头猫    InternalDataType,
1088772aabS猫头猫    isSameMediaItem,
1188772aabS猫头猫} from '@/utils/mediaItem';
12afb5c234S猫头猫import StateMapper from '@/utils/stateMapper';
13afb5c234S猫头猫import {getStorage, setStorage} from '@/utils/storage';
14806b2764S猫头猫import {nanoid} from 'nanoid';
150e4173cdS猫头猫import {useEffect, useState} from 'react';
16569da2d8S猫头猫import {unlink} from 'react-native-fs';
17*5353b473S猫头猫import {getInfoAsync, readDirectoryAsync} from 'expo-file-system';
18*5353b473S猫头猫import {addFileScheme, getFileName} from '@/utils/fileUtils.ts';
19afb5c234S猫头猫
20afb5c234S猫头猫let localSheet: IMusic.IMusicItem[] = [];
21afb5c234S猫头猫const localSheetStateMapper = new StateMapper(() => localSheet);
22afb5c234S猫头猫
23afb5c234S猫头猫export async function setup() {
2488772aabS猫头猫    const sheet = await getStorage(StorageKeys.LocalMusicSheet);
2588772aabS猫头猫    if (sheet) {
265589cdf3S猫头猫        let validSheet: IMusic.IMusicItem[] = [];
2788772aabS猫头猫        for (let musicItem of sheet) {
2888772aabS猫头猫            const localPath = getInternalData<string>(
2988772aabS猫头猫                musicItem,
3088772aabS猫头猫                InternalDataType.LOCALPATH,
3188772aabS猫头猫            );
32*5353b473S猫头猫            if (localPath && (await getInfoAsync(localPath)).exists) {
3388772aabS猫头猫                validSheet.push(musicItem);
3488772aabS猫头猫            }
3588772aabS猫头猫        }
3688772aabS猫头猫        if (validSheet.length !== sheet.length) {
3788772aabS猫头猫            await setStorage(StorageKeys.LocalMusicSheet, validSheet);
3888772aabS猫头猫        }
3988772aabS猫头猫        localSheet = validSheet;
40afb5c234S猫头猫    } else {
41afb5c234S猫头猫        await setStorage(StorageKeys.LocalMusicSheet, []);
42afb5c234S猫头猫    }
43afb5c234S猫头猫    localSheetStateMapper.notify();
44afb5c234S猫头猫}
45afb5c234S猫头猫
46afb5c234S猫头猫export async function addMusic(
47afb5c234S猫头猫    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
48afb5c234S猫头猫) {
49afb5c234S猫头猫    if (!Array.isArray(musicItem)) {
50afb5c234S猫头猫        musicItem = [musicItem];
51afb5c234S猫头猫    }
52afb5c234S猫头猫    let newSheet = [...localSheet];
53afb5c234S猫头猫    musicItem.forEach(mi => {
54afb5c234S猫头猫        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
55afb5c234S猫头猫            newSheet.push(mi);
56afb5c234S猫头猫        }
57afb5c234S猫头猫    });
58afb5c234S猫头猫    await setStorage(StorageKeys.LocalMusicSheet, newSheet);
59afb5c234S猫头猫    localSheet = newSheet;
60afb5c234S猫头猫    localSheetStateMapper.notify();
61afb5c234S猫头猫}
62afb5c234S猫头猫
63dc160d50S猫头猫function addMusicDraft(musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) {
64dc160d50S猫头猫    if (!Array.isArray(musicItem)) {
65dc160d50S猫头猫        musicItem = [musicItem];
66dc160d50S猫头猫    }
67dc160d50S猫头猫    let newSheet = [...localSheet];
68dc160d50S猫头猫    musicItem.forEach(mi => {
69dc160d50S猫头猫        if (localSheet.findIndex(_ => isSameMediaItem(mi, _)) === -1) {
70dc160d50S猫头猫            newSheet.push(mi);
71dc160d50S猫头猫        }
72dc160d50S猫头猫    });
73dc160d50S猫头猫    localSheet = newSheet;
74dc160d50S猫头猫    localSheetStateMapper.notify();
75dc160d50S猫头猫}
76dc160d50S猫头猫
77dc160d50S猫头猫async function saveLocalSheet() {
78dc160d50S猫头猫    await setStorage(StorageKeys.LocalMusicSheet, localSheet);
79dc160d50S猫头猫}
80dc160d50S猫头猫
81afb5c234S猫头猫export async function removeMusic(
82afb5c234S猫头猫    musicItem: IMusic.IMusicItem,
83afb5c234S猫头猫    deleteOriginalFile = false,
84afb5c234S猫头猫) {
85afb5c234S猫头猫    const idx = localSheet.findIndex(_ => isSameMediaItem(_, musicItem));
86afb5c234S猫头猫    let newSheet = [...localSheet];
87afb5c234S猫头猫    if (idx !== -1) {
88e65882dbS猫头猫        const localMusicItem = localSheet[idx];
89afb5c234S猫头猫        newSheet.splice(idx, 1);
90e65882dbS猫头猫        const localPath =
91e65882dbS猫头猫            musicItem[internalSerializeKey]?.localPath ??
92e65882dbS猫头猫            localMusicItem[internalSerializeKey]?.localPath;
93e65882dbS猫头猫        if (deleteOriginalFile && localPath) {
94483fe585S猫头猫            try {
95569da2d8S猫头猫                await unlink(localPath);
96483fe585S猫头猫            } catch (e: any) {
97483fe585S猫头猫                if (e.message !== 'File does not exist') {
98483fe585S猫头猫                    throw e;
99483fe585S猫头猫                }
100483fe585S猫头猫            }
101afb5c234S猫头猫        }
102afb5c234S猫头猫    }
103afb5c234S猫头猫    localSheet = newSheet;
104afb5c234S猫头猫    localSheetStateMapper.notify();
105cc77e86bS猫头猫    saveLocalSheet();
106afb5c234S猫头猫}
107afb5c234S猫头猫
108afb5c234S猫头猫function parseFilename(fn: string): Partial<IMusic.IMusicItem> | null {
109afb5c234S猫头猫    const data = fn.slice(0, fn.lastIndexOf('.')).split('@');
110afb5c234S猫头猫    const [platform, id, title, artist] = data;
111afb5c234S猫头猫    if (!platform || !id) {
112afb5c234S猫头猫        return null;
113afb5c234S猫头猫    }
114afb5c234S猫头猫    return {
115afb5c234S猫头猫        id,
116f55de8c2S猫头猫        platform: platform,
117f55de8c2S猫头猫        title: title ?? '',
118f55de8c2S猫头猫        artist: artist ?? '',
119afb5c234S猫头猫    };
120afb5c234S猫头猫}
121afb5c234S猫头猫
122*5353b473S猫头猫function localMediaFilter(filename: string) {
123*5353b473S猫头猫    return supportLocalMediaType.some(ext => filename.endsWith(ext));
124cd669353S猫头猫}
125cd669353S猫头猫
126806b2764S猫头猫let importToken: string | null = null;
127806b2764S猫头猫// 获取本地的文件列表
128806b2764S猫头猫async function getMusicStats(folderPaths: string[]) {
129806b2764S猫头猫    const _importToken = nanoid();
130806b2764S猫头猫    importToken = _importToken;
131*5353b473S猫头猫    const musicList: string[] = [];
132806b2764S猫头猫    let peek: string | undefined;
133*5353b473S猫头猫    let dirFiles: string[] = [];
134806b2764S猫头猫    while (folderPaths.length !== 0) {
135806b2764S猫头猫        if (importToken !== _importToken) {
136806b2764S猫头猫            throw new Error('Import Broken');
137806b2764S猫头猫        }
138806b2764S猫头猫        peek = folderPaths.shift() as string;
139cd669353S猫头猫        try {
140*5353b473S猫头猫            dirFiles = await readDirectoryAsync(peek);
141806b2764S猫头猫        } catch {
142806b2764S猫头猫            dirFiles = [];
143806b2764S猫头猫        }
144cd669353S猫头猫
145*5353b473S猫头猫        await Promise.all(
146*5353b473S猫头猫            dirFiles.map(async fileName => {
147*5353b473S猫头猫                const stat = await getInfoAsync(peek + '/' + fileName);
148*5353b473S猫头猫                if (!stat.exists) {
149*5353b473S猫头猫                    return;
150cd669353S猫头猫                }
151*5353b473S猫头猫                if (stat.isDirectory && !folderPaths.includes(stat.uri)) {
152*5353b473S猫头猫                    folderPaths.push(stat.uri);
153*5353b473S猫头猫                } else if (localMediaFilter(stat.uri)) {
154*5353b473S猫头猫                    musicList.push(stat.uri);
155*5353b473S猫头猫                }
156*5353b473S猫头猫            }),
157*5353b473S猫头猫        );
158cd669353S猫头猫    }
159806b2764S猫头猫    return {musicList, token: _importToken};
160806b2764S猫头猫}
161806b2764S猫头猫
162806b2764S猫头猫function cancelImportLocal() {
163806b2764S猫头猫    importToken = null;
164cd669353S猫头猫}
165cd669353S猫头猫
166cd669353S猫头猫// 导入本地音乐
1674173d3ebS猫头猫const groupNum = 25;
168806b2764S猫头猫async function importLocal(_folderPaths: string[]) {
169*5353b473S猫头猫    const folderPaths = [..._folderPaths.map(it => addFileScheme(it))];
170806b2764S猫头猫    const {musicList, token} = await getMusicStats(folderPaths);
171*5353b473S猫头猫    console.log('HI!!!', musicList, folderPaths, _folderPaths);
172806b2764S猫头猫    if (token !== importToken) {
173806b2764S猫头猫        throw new Error('Import Broken');
174806b2764S猫头猫    }
1758423b2d4S猫头猫    // 分组请求,不然序列化可能出问题
1768423b2d4S猫头猫    let metas: IBasicMeta[] = [];
1778423b2d4S猫头猫    const groups = Math.ceil(musicList.length / groupNum);
1788423b2d4S猫头猫    for (let i = 0; i < groups; ++i) {
1798423b2d4S猫头猫        metas = metas.concat(
1808423b2d4S猫头猫            await mp3Util.getMediaMeta(
181*5353b473S猫头猫                musicList.slice(i * groupNum, (i + 1) * groupNum),
1828423b2d4S猫头猫            ),
1838423b2d4S猫头猫        );
1848423b2d4S猫头猫    }
185806b2764S猫头猫    if (token !== importToken) {
186806b2764S猫头猫        throw new Error('Import Broken');
187806b2764S猫头猫    }
188*5353b473S猫头猫    const musicItems: IMusic.IMusicItem[] = await Promise.all(
189*5353b473S猫头猫        musicList.map(async (musicPath, index) => {
190cd669353S猫头猫            let {platform, id, title, artist} =
191*5353b473S猫头猫                parseFilename(getFileName(musicPath, true)) ?? {};
192cd669353S猫头猫            const meta = metas[index];
193cd669353S猫头猫            if (!platform || !id) {
194cd669353S猫头猫                platform = '本地';
195*5353b473S猫头猫                const fileInfo = await getInfoAsync(musicPath, {
196*5353b473S猫头猫                    md5: true,
197*5353b473S猫头猫                });
198*5353b473S猫头猫                id = fileInfo.exists ? fileInfo.md5 : nanoid();
199cd669353S猫头猫            }
200cd669353S猫头猫            return {
201cd669353S猫头猫                id,
202cd669353S猫头猫                platform,
203*5353b473S猫头猫                title: title ?? meta?.title ?? getFileName(musicPath),
204cd669353S猫头猫                artist: artist ?? meta?.artist ?? '未知歌手',
205*5353b473S猫头猫                duration: parseInt(meta?.duration ?? '0', 10) / 1000,
206cd669353S猫头猫                album: meta?.album ?? '未知专辑',
207cd669353S猫头猫                artwork: '',
208cd669353S猫头猫                [internalSerializeKey]: {
209*5353b473S猫头猫                    localPath: musicPath,
210cd669353S猫头猫                },
211*5353b473S猫头猫            } as IMusic.IMusicItem;
212cd669353S猫头猫        }),
213cd669353S猫头猫    );
214806b2764S猫头猫    if (token !== importToken) {
215806b2764S猫头猫        throw new Error('Import Broken');
216806b2764S猫头猫    }
217cd669353S猫头猫    addMusic(musicItems);
218cd669353S猫头猫}
219cd669353S猫头猫
2200e4173cdS猫头猫/** 是否为本地音乐 */
2210e4173cdS猫头猫function isLocalMusic(
2220e4173cdS猫头猫    musicItem: ICommon.IMediaBase | null,
2230e4173cdS猫头猫): IMusic.IMusicItem | undefined {
2240e4173cdS猫头猫    return musicItem
2250e4173cdS猫头猫        ? localSheet.find(_ => isSameMediaItem(_, musicItem))
2260e4173cdS猫头猫        : undefined;
2270e4173cdS猫头猫}
2280e4173cdS猫头猫
2290e4173cdS猫头猫/** 状态-是否为本地音乐 */
2300e4173cdS猫头猫function useIsLocal(musicItem: IMusic.IMusicItem | null) {
2310e4173cdS猫头猫    const localMusicState = localSheetStateMapper.useMappedState();
2320e4173cdS猫头猫    const [isLocal, setIsLocal] = useState<boolean>(!!isLocalMusic(musicItem));
2330e4173cdS猫头猫    useEffect(() => {
2340e4173cdS猫头猫        if (!musicItem) {
2350e4173cdS猫头猫            setIsLocal(false);
2360e4173cdS猫头猫        } else {
2370e4173cdS猫头猫            setIsLocal(!!isLocalMusic(musicItem));
2380e4173cdS猫头猫        }
2390e4173cdS猫头猫    }, [localMusicState, musicItem]);
2400e4173cdS猫头猫    return isLocal;
2410e4173cdS猫头猫}
2420e4173cdS猫头猫
2430224b881S猫头猫function getMusicList() {
2440224b881S猫头猫    return localSheet;
2450224b881S猫头猫}
2460224b881S猫头猫
24754bb1cc8S猫头猫async function updateMusicList(newSheet: IMusic.IMusicItem[]) {
24854bb1cc8S猫头猫    const _localSheet = [...newSheet];
24954bb1cc8S猫头猫    try {
25054bb1cc8S猫头猫        await setStorage(StorageKeys.LocalMusicSheet, _localSheet);
25154bb1cc8S猫头猫        localSheet = _localSheet;
25254bb1cc8S猫头猫        localSheetStateMapper.notify();
25354bb1cc8S猫头猫    } catch {}
25454bb1cc8S猫头猫}
25554bb1cc8S猫头猫
2560e4173cdS猫头猫const LocalMusicSheet = {
2570e4173cdS猫头猫    setup,
2580e4173cdS猫头猫    addMusic,
2590e4173cdS猫头猫    removeMusic,
260dc160d50S猫头猫    addMusicDraft,
261dc160d50S猫头猫    saveLocalSheet,
262cd669353S猫头猫    importLocal,
263806b2764S猫头猫    cancelImportLocal,
2640e4173cdS猫头猫    isLocalMusic,
2650e4173cdS猫头猫    useIsLocal,
2660224b881S猫头猫    getMusicList,
2670e4173cdS猫头猫    useMusicList: localSheetStateMapper.useMappedState,
26854bb1cc8S猫头猫    updateMusicList,
2690e4173cdS猫头猫};
2700e4173cdS猫头猫
2710e4173cdS猫头猫export default LocalMusicSheet;
272