Browse Source

feat: 扫描歌单
1. 扫描歌单功能
2. 喜爱歌曲功能

kindring 3 months ago
parent
commit
f488f56502

+ 1 - 0
package.json

@@ -35,6 +35,7 @@
   },
   "dependencies": {
     "@headlessui/vue": "^1.7.22",
+    "music-metadata": "^10.6.4",
     "vuedraggable": "^4.1.0"
   }
 }

+ 2 - 0
src/apis/ApiAction.ts

@@ -12,4 +12,6 @@ export enum Music_Actions {
   scan_settings = 'scan_settings',
   scan_music_update = 'scan_music_update',
   scan_music_delete = 'scan_music_delete',
+  scan_music_fetch = 'scan_music_fetch',
+  like_music = 'like_music',
 }

+ 10 - 2
src/apis/musicControl.ts

@@ -1,7 +1,7 @@
 import api from "./baseApi.ts"
-import {ResponseData} from "@/types/apiTypes.ts";
+import {Order, Page, ResponseData} from "@/types/apiTypes.ts";
 import {Music_Actions} from "@/apis/ApiAction.ts";
-import {MusicScanSetting, PlayList} from "@/types/musicType.ts";
+import {MusicInfo, MusicScanSetting, PlayList} from "@/types/musicType.ts";
 
 export async function fetchPlayList(): Promise< ResponseData<PlayList[]> >
 {
@@ -41,3 +41,11 @@ export async function deleteScanConfig(id: number) : Promise<ResponseData<boolea
     let [_callId, promise] = api.sendQuery(Music_Actions.scan_music_delete, id);
     return await promise;
 }
+
+export async function fetchScanMusic(scanId: number, page: number = 1, size: number = 10, 
+                                     orderBy: string = 'id', 
+                                     order: Order = Order.desc): Promise<ResponseData<Page<MusicInfo[]>>>
+{
+    let [_callId, promise] = api.sendQuery(Music_Actions.scan_music_fetch, {scanId, page, size, orderBy, order});
+    return await promise as ResponseData<Page<MusicInfo[]>>;
+}

+ 277 - 22
src/common/db/db_music.ts

@@ -1,12 +1,31 @@
 import Logger from "@/util/logger.ts";
 import { loadDb} from "@/common/db/db.ts";
-import {handle, PromiseResult} from "@/util/promiseHandle.ts";
+import {handle, PromiseResult, ResType} from "@/util/promiseHandle.ts";
 import {AppDbName} from "@/types/appConfig.ts";
 import {Knex} from "knex";
-import {MusicScanSetting, MusicTableName, PlayList} from "@/types/musicType.ts";
+import {MusicInfo, MusicScanSetting, MusicTableName, PlayList} from "@/types/musicType.ts";
+import {Order, Page} from "@/types/apiTypes.ts";
 
 let logger = Logger.logger('music_db', 'info');
 
+const Music_field = [
+    'id',
+    'key',
+    'name',
+    'artists',
+    'album',
+    'cover',
+    'duration',
+    'isLike',
+    'origin',
+    'type',
+    'isLocal',
+    'filePath',
+    'lyricPath',
+    'tags',
+    'playCount',
+    'scanId',
+]
 async function _initScanConfigTable(db : Knex): PromiseResult<boolean> {
     let [err, hasTable] = await handle(
         db.schema.hasTable(MusicTableName.music_scan_setting)
@@ -16,7 +35,7 @@ async function _initScanConfigTable(db : Knex): PromiseResult<boolean> {
         logger.error(`[音频扫描库] ${err.message}`)
         return [new Error('音频扫描库初始化失败'), false]
     }
-    console.log(hasTable)
+
     if (hasTable) {
         return [null, true];
     }
@@ -72,23 +91,6 @@ async function _initPlayListTable(db : Knex): PromiseResult<boolean> {
         logger.error(`[初始化歌单失败] ${createErr.message}`)
         return [createErr, false];
     }
-    // 添加默认歌单
-    await db.insert({
-        name: '我的喜爱',
-        icon: 'favorite',
-        cover: 'favorite',
-        description: '我的喜爱',
-        playCount: 0,
-        trackCount: 0,
-        createTime: Date.now(),
-        isTagSearch: false,
-        lastPlayTime: Date.now(),
-        isSync: false,
-        isPublic: false,
-        isLike: true,
-    }).into(MusicTableName.music_play_list)
-    logger.info('[初始化我的喜爱歌单成功]')
-
     return [null, true];
 }
 
@@ -103,12 +105,23 @@ async function _initSongsTable(db : Knex): PromiseResult<boolean>
         return [new Error('音频表初始化'), false]
     }
     if (hasTable) {
+        if (false)
+        {
+            // 移除旧数据
+            // 更新表字段
+            // await db.schema.alterTable(MusicTableName.music_songs, (table) => {
+            //     // table.string('key')
+            //     table.string('scanId')
+            // })
+            await removeMusicByScanId(0);
+        }
         return [null, true];
     }
     let [createErr, _res] = await handle(
         db.schema.createTable(MusicTableName.music_songs, (table) => {
             logger.info(`[初始化音频库]`)
             table.increments('id').primary()
+            table.string('key')
             table.string('name')
             table.string('artists')
             table.string('album')
@@ -122,6 +135,7 @@ async function _initSongsTable(db : Knex): PromiseResult<boolean>
             table.string('lyricPath')
             table.string('tags')
             table.integer('playCount')
+            table.integer('scanId')
         }))
     if (createErr) {
         createErr = createErr as Error;
@@ -179,7 +193,14 @@ export async function initMusicData() : PromiseResult<boolean>
     [err, res] = await _initPlayListTable(db);
     if (err) {
         err = err as Error;
-        logger.error(`[初始化播放列表库失败] ${err.message}`)
+        logger.error(`[初始化歌单库失败] ${err.message}`)
+        return [err, false];
+    }
+
+    [err, res] = await initDefaultPlayList();
+    if (err) {
+        err = err as Error;
+        logger.error(`[初始化默认播放列表失败] ${err.message}`)
         return [err, false];
     }
     [err, res] = await _initSongsTable(db);
@@ -200,7 +221,47 @@ export async function initMusicData() : PromiseResult<boolean>
     return [null, res]
 }
 
-
+export async function initDefaultPlayList(): PromiseResult<boolean>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('[初始化歌单] 数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), false]
+    }
+    // 判断默认歌单是否存在
+    let [err, res] = await handle(db.select('*').from(MusicTableName.music_play_list).where('isLike', true))
+    if (err) {
+        err = err as Error;
+        logger.error(`[初始化歌单] 检索默认歌单 => ${err.message}`)
+        return [err, false];
+    }
+    res = res as MusicScanSetting[];
+    if (res.length > 0) {
+        return [null, true];
+    }
+    let defaultPlayList = {
+        name: '我的喜爱',
+        icon: 'favorite',
+        cover: 'favorite',
+        description: '我的喜爱',
+        playCount: 0,
+        trackCount: 0,
+        createTime: Date.now(),
+        isTagSearch: false,
+        lastPlayTime: Date.now(),
+        isSync: false,
+        isPublic: false,
+        isLike: true,
+    };
+    [err, res] = await handle(db.insert(defaultPlayList).into(MusicTableName.music_play_list))
+    if (err) {
+        err = err as Error;
+        logger.error(`[初始化我的喜爱歌单失败] ${err.message}`)
+        return [err, false];
+    }
+    logger.info('[初始化我的喜爱歌单成功]')
+    return [null, true];
+}
 
 // 根据扫描地址获取扫描配置
 export async function getScanConfigByPath(path: string, id: number[] = []) : PromiseResult<MusicScanSetting[]>
@@ -341,3 +402,197 @@ export async function addPlayList(playList: PlayList) : PromiseResult<boolean>
     }
     return [null, true];
 }
+
+export async function get_like_playlist(): PromiseResult<PlayList>
+{
+    const __func__ = 'get_like_playlist()'
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error(`${__func__} 数据库初始化失败`)
+        return [new Error('[获取歌单] 音乐数据库初始化失败'), null]
+    }
+    let [err, res] = await handle(
+        db.select('id', 'name', 'icon', 'cover', 'description', 'playCount', 'trackCount', 'createTime', 'isTagSearch', 'lastPlayTime', 'isSync', 'isPublic', 'isLike')
+            .from(MusicTableName.music_play_list)
+            .where('isLike', true)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`${__func__} 获取喜爱的歌单失败 ${err.message}`)
+        return [err, null];
+    }
+    res = res as ResType<PlayList[]>
+    if (!res || res.length === 0) {
+        logger.error(`${__func__} 获取喜爱的歌单失败, 歌单不存在`)
+        return [new Error('[获取歌单] 歌单不存在'), null];
+    }
+    let playList = res[0] as PlayList;
+    return [null, playList];
+}
+
+export async function addPlayListMusic(playlist_id: number, music_id: number) : PromiseResult<boolean>
+{
+    const __func__ = 'addPlayListMusic()'
+    const __key__ = '添加歌曲至播放列表'
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error(`${__func__} 数据库初始化失败`)
+        return [new Error(`${__key__} 音乐数据库初始化失败`), false]
+    }
+    let [err, _res] = await handle(
+        db.insert({
+            playListId: playlist_id,
+            musicId: music_id,
+            order: 0
+        }).into(MusicTableName.music_play_list_songs)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`${__func__} ${__key__} 失败 ${err.message}`)
+        return [err, false];
+    }
+    return [null, true];
+    
+}
+
+export async function getMusicByKey(musicKey: string) : PromiseResult<MusicInfo>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), null]
+    }
+    let [err, res] = await handle(
+        db.select(...Music_field)
+            .from(MusicTableName.music_songs)
+            .where('key', musicKey)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`[获取音乐失败] ${err.message}`)
+        return [err, null];
+    }
+    return [null, res as ResType<MusicInfo>];
+}
+
+export async function addMusic(music: MusicInfo): PromiseResult<boolean>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), false]
+    }
+    let saveMusic = {
+        key: music.key,
+        name: music.name,
+        artists: music.artists.join(','),
+        album: music.album,
+        cover: music.cover,
+        duration: music.duration,
+        isLike: music.isLike,
+        origin: music.origin,
+        type: music.type,
+        isLocal: music.isLocal,
+        filePath: music.filePath,
+        lyricPath: music.lyricPath,
+        tags: music.tags.join(','),
+        playCount:music.playCount,
+        scanId: music.scanId
+    }
+    console.log(saveMusic);
+    let [err, _res] = await handle(
+        db.insert(saveMusic).into(MusicTableName.music_songs)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`[添加音乐失败] ${err.message}`)
+        return [err, false];
+    }
+    return [null, true];
+}
+
+export async function likeMusic(id: number, isLike: boolean) : PromiseResult<boolean>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), false]
+    }
+    let [err, _res] = await handle(
+        db.update({isLike: isLike}).where('id', id)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`[喜欢音乐] ${isLike? '喜欢': '取消喜欢'} ${err.message}`)
+        return [err, false];
+    }
+    return [null, true];
+}
+export async function getMusicsByScanId(scanId: number, page: number = 1, size: number = 10,
+                                        orderBy: string = 'id', order: 'asc' | 'desc' = 'asc')
+    : PromiseResult<Page<MusicInfo[]>>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), null]
+    }
+    let countPromise;
+    let resData: Page<MusicInfo[]> = {
+        total: 0,
+        data: [],
+        page: page,
+        size: size,
+        order: order as Order,
+        sort: orderBy
+    }
+    if (page === 1)
+    {
+        // 第一页尝试获取总数量
+        countPromise = db.count('id as count')
+            .from(MusicTableName.music_songs)
+            .where('scanId', scanId)
+    } else
+    {
+        countPromise = Promise.resolve({count: 0});
+    }
+    let listPromise = db.select(...Music_field)
+        .from(MusicTableName.music_songs)
+        .where('scanId', scanId)
+        .limit(size)
+        .offset((page - 1) * size)
+        .orderBy(orderBy, order)
+
+    let [err, res] = await handle<[{ count: number}, MusicInfo[]]>(Promise.all([countPromise, listPromise]) as Promise<[{ count: number}, MusicInfo[]]>)
+    if (err) {
+        err = err as Error;
+        logger.error(`[获取扫描歌曲] ${err.message}`)
+    }
+    if (!res) {
+        logger.error(`[获取扫描歌曲] 无法获取指定歌单数据`)
+        return [err, resData];
+    }
+
+    resData.total = res[0].count as number;
+    resData.data = res[1] as MusicInfo[];
+    return [err, resData];
+}
+
+// 移除异常歌单
+export async function removeMusicByScanId(scanId: number) : PromiseResult<boolean>
+{
+    let db = loadDb(AppDbName.music_db)
+    if(!db){
+        logger.error('数据库初始化失败')
+        return [new Error('音乐数据库初始化失败'), false]
+    }
+    let [err, _res] = await handle(
+        db.delete().from(MusicTableName.music_songs).where('scanId', scanId)
+    )
+    if (err) {
+        err = err as Error;
+        logger.error(`[移除指定歌单音乐列表失败] ${err.message}`)
+        return [err, false];
+    }
+    return [err, true];
+}

+ 1 - 0
src/components/music/common/mSettingScan.vue

@@ -71,6 +71,7 @@ onBeforeMount(()=>{
 
 function editScanHandle(item: MusicScanSetting)
 {
+  console.log('editScanHandle', item)
   kuiDialog.show({scanSetting: {
     id: item.id,
     name: item.name,

+ 1 - 1
src/components/music/dialog/addScan.vue

@@ -40,7 +40,7 @@ const isFileRepeat = ref(false);
 
 onMounted(()=>{
   console.log(props.scanSetting);
-  if ( props.scanSetting && props.scanSetting.id > 0) {
+  if ( props.scanSetting && props.scanSetting.id > -1) {
     isEdit.value = true;
     name.value = props.scanSetting.name;
     dirPath.value = props.scanSetting.path;

+ 12 - 9
src/components/music/musicIndex.vue

@@ -24,7 +24,7 @@ const searchText = ref('');
 
 const playList: Ref<PlayList[]> = ref<PlayList[]>([])
 
-const selectIndex = ref(0);
+const selectPlaylistIndex = ref(0);
 
 const showPlayList = "showPlayList";
 const showSetting = "showSetting";
@@ -40,7 +40,7 @@ const musicViewShow = ref(showPlayList);
 const site_view_key = ref(site_views[0]);
 
 function changePlayList(index: number) {
-  selectIndex.value = index;
+  selectPlaylistIndex.value = index;
   musicViewShow.value = showPlayList;
 }
 
@@ -64,7 +64,7 @@ function showMusicSetting()
 {
   message.info("show music setting");
   musicViewShow.value = showSetting;
-  selectIndex.value = -1;
+  selectPlaylistIndex.value = -1;
 }
 
 
@@ -79,7 +79,7 @@ async function fetchScanSetting()
   }
 }
 
-function changeTab(index: number) {
+async function changeTab(index: number) {
   if (site_view_key.value === site_views[index])
   {
     return;
@@ -88,11 +88,14 @@ function changeTab(index: number) {
   site_view_key.value = site_views[index];
   if (site_view_key.value === site_playList)
   {
-    loadPlayList();
+    await loadPlayList();
+    selectPlaylistIndex.value = 0;
   } else {
     message.info("show scan list");
-    fetchScanSetting();
+    await fetchScanSetting();
+    selectScanIndex.value = 0;
   }
+
 }
 
 
@@ -139,7 +142,7 @@ function changeScanList(index: number)
                 <div
                     v-for="(item, i) in playList"
                     :key="item.id"
-                    :class="`list-item ${i == selectIndex?'select-item':''}` "
+                    :class="`list-item ${i == selectPlaylistIndex?'select-item':''}` "
                     @click="changePlayList(i)"
                 >
                   <div class="icon">
@@ -155,7 +158,7 @@ function changeScanList(index: number)
                     :class="`list-item ${i == selectScanIndex?'select-item':''}` "
                     @click="changeScanList(i)"
                 >
-                  <span>扫描{{item.name}}</span>
+                  <span>{{item.name}}</span>
                 </div>
               </TabPanel>
             </TabPanels>
@@ -168,7 +171,7 @@ function changeScanList(index: number)
         </div>
       </div>
       <div class="play-list-info">
-        <play-list-info v-if="musicViewShow === showPlayList" :play-list="playList[selectIndex]"/>
+        <play-list-info v-if="musicViewShow === showPlayList" :play-list="playList[selectPlaylistIndex]"/>
         <music-setting v-if="musicViewShow === showSetting"
                        :window-id="windowId"/>
       </div>

+ 2 - 0
src/main/AppControl.ts

@@ -13,6 +13,7 @@ import hook from "@/util/hook.ts";
 import Path from "path";
 import {initMusicData} from "@/common/db/db_music.ts";
 import {ResType} from "@/util/promiseHandle.ts";
+import {start_scan} from "@/main/control/magnet/music.ts";
 
 let logger = Logger.logger('controlWindow', 'info');
 
@@ -336,6 +337,7 @@ export async function initApp(appConfig: AppConfig, app: Electron.App) : Promise
     if(err){
         logger.error(`[应用初始化] 初始化音乐库失败: ${err}`);
     }
+    await start_scan();
     if(flag){
         logger.info(`[应用初始化] 初始化音乐库完成`);
     }

+ 7 - 1
src/main/control/api_router.ts

@@ -2,7 +2,7 @@ import {ApiType, ErrorCode, RequestData, ResponseData} from "@/types/apiTypes.ts
 import {Magnet_Actions, Music_Actions} from "@/apis/ApiAction.ts";
 import {c_fetchMagnetList, c_magnet_batch_update, c_magnet_delete} from "@/main/control/magnet/magnet.ts";
 import {
-    c_fetchPlayList,
+    c_fetchPlayList, c_like_music, c_load_scan_music,
     c_scanMusicAdd, c_scanMusicDelete,
     c_scanMusicSelect,
     c_scanMusicUpdate,
@@ -41,6 +41,12 @@ export async function apiRouter(requestData: RequestData<any>){
         case Music_Actions.scan_music_delete:
             responseData = await c_scanMusicDelete(requestData);
             break;
+        case Music_Actions.scan_music_fetch:
+            responseData = await c_load_scan_music(requestData);
+            break;
+        case Music_Actions.like_music:
+            responseData = await c_like_music(requestData);
+            break;
         default:
             responseData = {
                 type: ApiType.res,

+ 307 - 6
src/main/control/magnet/music.ts

@@ -1,19 +1,42 @@
 import {dialog} from "electron"
-import { ErrorCode, RequestData, ResponseData} from "@/types/apiTypes.ts";
+import fs from "fs-extra";
+import {IAudioMetadata, parseFile} from "music-metadata";
+import {ErrorCode, Page, RequestData, ResponseData} from "@/types/apiTypes.ts";
 import Logger from "@/util/logger.ts";
-import {MusicScanSetting} from "@/types/musicType.ts";
+import {MusicInfo, MusicScanSetting, MusicType, param_music_like, PlayList} from "@/types/musicType.ts";
 import {
+    addMusic, addPlayListMusic,
     addScanConfig,
-    deleteScanConfig, getPlayList,
+    deleteScanConfig, get_like_playlist, getMusicByKey, getMusicsByScanId, getPlayList,
     getScanConfig,
-    getScanConfigByPath,
+    getScanConfigByPath, initDefaultPlayList, likeMusic,
     updateScanConfig
 } from "@/common/db/db_music.ts";
-import {ResType} from "@/util/promiseHandle.ts";
+import {handle, PromiseResult, ResType} from "@/util/promiseHandle.ts";
 import {t_gen_res, t_res_ok} from "@/main/tools/ipcRouter.ts";
+import path from "path";
+import {getRandomStr, randomAzStr, randomNumber} from "@/util/random.ts";
+
 let logger = Logger.logger('music', 'info');
 
+const music_ext_list = [
+    '.mp3',
+    '.flac',
+    '.ape',
+    '.wav',
+    '.wma',
+    '.ogg',
+    '.m4a',
+    '.aac',
+    '.wma',
+    '.wav',
+    '.flac',
+    '.m4a',
+]
+
+const scan_task: {[key: number]: any} = {
 
+}
 
 export async function c_fetchPlayList(requestData: RequestData<null>)
 {
@@ -121,7 +144,10 @@ export async function c_scanMusicUpdate(requestData: RequestData<MusicScanSettin
     return t_res_ok(requestData,  res)
 }
 
-
+/**
+ * 删除扫描配置
+ * @param requestData
+ */
 export async function c_scanMusicDelete(requestData: RequestData<number>) : Promise<ResponseData<boolean>>
 {
     let res: ResType<any> = false;
@@ -134,3 +160,278 @@ export async function c_scanMusicDelete(requestData: RequestData<number>) : Prom
     res = res as boolean;
     return t_res_ok(requestData,  res)
 }
+
+/**
+ * 分页查询
+ * @param requestData
+ */
+export async function c_load_scan_music(requestData: RequestData<Page<number>>)
+    : Promise<ResponseData<Page<MusicInfo[]>>>
+{
+    let queryParam = requestData.data;
+    let [err, res] = await getMusicsByScanId(queryParam.data,
+        queryParam.page,
+        queryParam.size,
+        queryParam.order,
+        queryParam.order)
+    if (err) {
+        logger.error(`[音频扫描] 获取扫描到的音频失败 ${err.message}`)
+        return t_gen_res(requestData, ErrorCode.db, '获取扫描到的音频失败', null)
+    }
+    res = res as Page<MusicInfo[]>;
+    return t_res_ok(requestData, res)
+}
+
+
+/**
+ * 喜欢音频
+ * @param requestData
+ */
+export async function c_like_music(requestData: RequestData<param_music_like>) : Promise<ResponseData<boolean>>
+{
+    logger.info(`[喜欢音频] ${requestData.data}`)
+    let likeData = requestData.data;
+    logger.info(`[喜欢音频] ${likeData.musicId} ${likeData.isLike}`)
+    let [err, res] = await get_like_playlist();
+    if (err) {
+        logger.error(`[获取喜欢列表] ${err.message}`)
+        return t_gen_res(requestData, ErrorCode.db, '获取喜欢列表失败', false)
+    }
+    let playList = res as PlayList;
+    if (playList.id === -1) {
+        logger.error(`[添加喜欢列表] 无法找到我喜欢的歌单 ${err.message}`)
+        initDefaultPlayList();
+        return t_gen_res(requestData, ErrorCode.db, '添加喜欢列表失败', false)
+    }
+    let bool: ResType<boolean> = false;
+    // 歌单中添加歌曲
+    [err, bool] = await addPlayListMusic(playList.id, likeData.musicId);
+    if (err) {
+        logger.error(`[添加喜欢列表] ${err.message}`)
+        return t_gen_res(requestData, ErrorCode.db, '添加喜欢列表失败', false)
+    }
+    [err, bool] = await likeMusic(likeData.musicId, likeData.isLike);
+    if (err) {
+        logger.error(`[喜欢音频失败] ${err.message}`)
+        return t_gen_res(requestData, ErrorCode.db, '喜欢音频失败', false)
+    }
+    bool = bool as boolean;
+    return t_res_ok(requestData,  bool)
+}
+
+async function _read_music_info(filePath: string) : PromiseResult<IAudioMetadata>
+{
+    let [err, metadata] = await handle(parseFile(filePath));
+    if (err) {
+        logger.error(`[读取音频文件失败] ${err.message}`)
+        return [err, null];
+    }
+    metadata = metadata as ResType<IAudioMetadata>;
+    return [null, metadata as IAudioMetadata];
+}
+
+
+/**
+ * 获取扫描配置下的音频文件
+ */
+async function _scan_dir(scanSetting: MusicScanSetting, basePath: string = "") :PromiseResult<any>
+{
+    let scanPath = basePath? scanSetting.path : scanSetting.path;
+    let [err, res] = await handle( fs.readdir(scanPath))
+    let fileArray: string[] = [];
+    if (err) {
+        logger.error(`[扫描目录失败] ${err.message}`)
+        return [err, false];
+    }
+    // 排除目录
+    for (let filename of res as string[])
+    {
+        console.log(filename)
+        // 判断类型
+        let filePath = scanSetting.path + '/' + filename;
+        // 排除隐藏文件
+        if (filename.startsWith('.'))
+        {
+            console.log(`[排除隐藏文件] ${filename}`)
+            continue;
+        }
+        if (fs.statSync(filePath).isDirectory())
+        {
+            if (!scanSetting.scanSubDir)
+            {
+                continue;
+            }
+            let _;
+            [err, _] = await _scan_dir(scanSetting, filePath);
+            if (err)
+            {
+                logger.error(`[扫描子目录失败] ${err.message} ${_}`)
+                return [err, false];
+            }
+        }
+
+        // 获取文件后缀
+        let ext = path.extname(filename);
+        ext = ext.toLowerCase();
+        if ( music_ext_list.includes(ext))
+        {
+            fileArray.push(filePath);
+        }
+    }
+    return [err, fileArray];
+}
+// https://www.npmjs.com/package/music-metadata
+
+async function _next_id(scanSetting_id: number):Promise<string>
+{
+    let id = randomAzStr(randomNumber(16));
+    id = `${scanSetting_id}_${id}`
+    let [err, musicInfo] = await getMusicByKey(id);
+    if (err)
+    {
+        logger.error(`[生成id失败] ${err.message}`)
+        return id;
+    }
+    if (!musicInfo)
+    {
+        return id;
+    }
+    // 循环10次生成id
+    for (let i = 0; i < 20; i++)
+    {
+        let next_id = randomAzStr(randomNumber(16 + i));
+        // id 采用此次的前6位 加上新生成的后6位
+        next_id = id.substring(4, 6) + next_id.substring(next_id.length - 6, 6);
+        next_id = getRandomStr(next_id + id, 8);
+        next_id = `${scanSetting_id}_${next_id}`;
+        [err, musicInfo] = await getMusicByKey(id);
+        if (err)
+        {
+            logger.error(`[生成id失败] ${err.message}`)
+            return next_id;
+        }
+        if (!musicInfo)
+        {
+            return next_id;
+        }
+    }
+    return `${scanSetting_id}_${scanSetting_id}_${id.substring(5, 3)}_${id.substring(5, 3)}`;
+}
+
+export async function _scan_(scanSetting: MusicScanSetting)
+{
+
+    if (scanSetting && scanSetting.id && scan_task[scanSetting.id])
+    {
+        logger.error(`[扫描任务重复] ${scanSetting.id}`)
+        return;
+    }
+    scan_task[scanSetting.id] = true;
+    console.log(scanSetting)
+    console.log(scanSetting.path)
+    // 缓存扫描信息
+    let catchPath = path.join(scanSetting.path, './.catch.json')
+    // 判断文件是否存在
+    if ( !fs.existsSync(catchPath) )
+    {
+        // 文件不存在, 创建空文件
+        fs.writeFileSync(catchPath, '{}');
+    }
+    let catchInfo = JSON.parse(fs.readFileSync(catchPath, 'utf-8'));
+    console.log(catchInfo)
+    let [err, fileArray] = await _scan_dir(scanSetting);
+
+    let success_count = 0;
+    if (err) {
+        logger.error(`[扫描目录失败] ${err.message}`)
+        return;
+    }
+    logger.info(`[扫描目录] ${fileArray.length}`)
+
+    for (let filePath of fileArray as string)
+    {
+        // 判断文件是否已经扫描过
+        if (catchInfo[filePath])
+        {
+            // 判断文件最新的时间
+            if (catchInfo[filePath] > fs.statSync(filePath).mtimeMs)
+            {
+                // 文件没有更新, 跳过
+                continue;
+            }
+        }
+        // 尝试获取音频文件
+        let musicMetaData: ResType<IAudioMetadata> = null;
+        [err, musicMetaData] = await _read_music_info(filePath);
+        if (err) {
+            logger.error(`[获取音频文件信息失败] ${err.message} ${filePath}`)
+            continue;
+        }
+        if (musicMetaData)
+        {
+            // 获取文件更新时间
+            catchInfo[filePath] = fs.statSync(filePath).mtimeMs;
+            // 将封面文件写入到 本地 文件夹
+            let coversDir = path.join(scanSetting.path, './.covers');
+            if (!fs.existsSync(coversDir))
+            {
+                fs.mkdirSync(coversDir);
+            }
+            let music_name = musicMetaData.common.title? musicMetaData.common.title : path.basename(filePath);
+            let nextId: string = await _next_id(scanSetting.id);
+            let coverPath = path.join(coversDir, `${nextId}_${music_name}.jpg`);
+            if (musicMetaData.common.picture && musicMetaData.common.picture.length > 0)
+            {
+                fs.writeFileSync(coverPath, musicMetaData.common.picture[0].data);
+            }
+            let musicInfo: MusicInfo = {
+                id: -1,
+                key: nextId,
+                name: music_name,
+                album: musicMetaData.common.album? musicMetaData.common.album : '',
+                artists: musicMetaData.common.artists? musicMetaData.common.artists : [],
+                cover: coverPath,// 存储在本地的封面文件地址, 只存储基础的文件名称, 其它内容基于本地目录进行拼接
+                duration: musicMetaData.format.duration? musicMetaData.format.duration : 0,
+                isLike: false,
+                origin: '',
+                type: MusicType.local,
+                scanId: scanSetting.id,
+                filePath: filePath,
+                isLocal: true,
+                tags: musicMetaData.common.genre? musicMetaData.common.genre : [],
+                playCount: 0,
+                lyricPath: '',
+            };
+            let _bool : ResType<boolean> = false;
+            [err, _bool] = await addMusic(musicInfo) ;
+            if (err)
+            {
+                logger.error(`[添加歌曲失败] ${err.message} ${musicInfo} ${_bool}`)
+                continue;
+            }
+            success_count++;
+        }
+    }
+
+    // 移除扫描任务
+    delete scan_task[scanSetting.id];
+}
+
+export async function start_scan()
+{
+    let [err, scanSettingList] = await getScanConfig();
+    if (err)
+    {
+        logger.error(`[获取扫描配置失败] ${err.message}`)
+        return;
+    }
+    for (let scanSetting of scanSettingList as MusicScanSetting[])
+    {
+        if (scanSetting)
+        {
+            _scan_(scanSetting);
+        }
+    }
+}
+
+

+ 21 - 0
src/types/apiTypes.ts

@@ -23,6 +23,11 @@ export enum ApiType {
     notify = 0xFD,
 }
 
+export enum Order {
+    asc = 'asc',
+    desc = 'desc',
+}
+
 
 // 请求参数
 export interface RequestData<T>
@@ -65,6 +70,22 @@ export interface NotifyData
 }
 
 
+export interface Page<T> {
+    // 请求页数
+    page: number;
+    // 每页数量
+    size: number;
+    // 总文件数量
+    total: number;
+    // 排序字段
+    sort: string;
+    // 排序方式
+    order: Order;
+    // 实际数据
+    data: T;
+}
+
+
 export enum magnet_Actions {
     // 获取列表
     magnet_list = 'magnet_list',

+ 38 - 15
src/types/musicType.ts

@@ -1,5 +1,5 @@
 export interface PlayList {
-    id: string;
+    id: number;
     name: string;   // 歌单名称
     icon: string;   // 歌单icon
     cover: string;  // 封面图片地址
@@ -20,20 +20,38 @@ export enum MusicType{
 }
 
 export interface MusicInfo {
-    id: string;
-    name: string;   // 歌曲名称
-    artists: string[];  // 歌手名称
-    album: string;  // 专辑名称
-    cover: string;  // 歌曲封面图片地址
-    duration: number;   // 歌曲时长 单位: 秒
-    isLike: boolean;  // 是否喜欢
-    origin: string; // 歌曲来源 用于实现远程链接设备获取音频源文件.
-    type: MusicType; // 歌曲类型, 用于区分歌曲源存放位置
-    isLocal: boolean; // 本地是否存在
-    filePath: string; // 文件存放路径
-    lyricPath: string;  // 歌词文件地址
-    tags: string[]; // 歌曲标签
-    playCount: number;  // 播放次数
+    // 自增id
+    id: number;
+    // 唯一建
+    key: string;
+    // 歌曲名称
+    name: string;
+    // 歌手名称
+    artists: string[];
+    // 专辑名称
+    album: string;
+    // 歌曲封面图片地址
+    cover: string;
+    // 歌曲时长 单位: 秒
+    duration: number;
+    // 是否喜欢
+    isLike: boolean;
+    // 歌曲来源 用于实现远程链接设备获取音频源文件.
+    origin: string;
+    // 歌曲类型, 用于区分歌曲源存放位置
+    type: MusicType;
+    // 本地是否存在
+    isLocal: boolean;
+    // 文件存放路径
+    filePath: string;
+    // 歌词文件地址
+    lyricPath: string;
+    // 歌曲标签
+    tags: string[];
+    // 播放次数
+    playCount: number;
+    // 由哪一个扫描配置添加的
+    scanId: number;
 }
 // 歌单音频信息
 export interface PlayListMusicInfo {
@@ -67,3 +85,8 @@ export enum MusicTableName
     music_play_list_songs = 'music_play_list_songs',// 歌单歌曲表
 }
 
+export interface param_music_like
+{
+    musicId: number;
+    isLike: boolean;
+}

+ 27 - 1
src/util/random.ts

@@ -1,5 +1,31 @@
-export function randomId() {
+export function randomId(): string {
     return Math.random().toString(36).substr(4);
 }
 
+// 生成随机id, a-z 0-9 可以生成特定长度的id
+export function randomAzStr(len: number): string {
+    let id = '';
+    for (let i = 0; i < len; i++) {
+        id += Math.random().toString(36).substr(2);
+    }
+    return id;
+}
+
+// 生成指定大小内的随机数字
+export function randomNumber(max: number = 100 , min: number = 0 ): number {
+    return Math.floor(Math.random() * (max - min + 1)) + min;
+}
 
+/**
+ * 获取随机字符串
+ * @param str 基础字符串
+ * @param len 需要获取的字符串长度
+ * @returns 随机字符串
+ */
+export function getRandomStr(str: string, len: number): string {
+    let result = '';
+    for (let i = 0; i < len; i++) {
+        result += str[Math.floor(Math.random() * str.length)];
+    }
+    return result;
+}

+ 36 - 0
test/music_metadata.js

@@ -0,0 +1,36 @@
+//
+// const music_metadata = require('music-metadata')
+// const util = require('util')
+//
+// console.log(music_metadata)
+//
+//
+// async function main() {
+//     try {
+//         const filePath = 'F:\\Music\\ChiliChill - 时光盲盒.flac';
+//         console.log('Parsing metadata...')
+//         const metadata = await music_metadata.loadMusicMetadata(filePath);
+//
+//         // Output the parsed metadata to the console in a readable format
+//         console.log(util.inspect(metadata, { showHidden: false, depth: null }));
+//     } catch (error) {
+//         console.error('Error parsing metadata:', error.message);
+//     }
+// }
+// main()
+
+
+import { parseFile } from 'music-metadata';
+import { inspect } from 'util';
+
+(async () => {
+    try {
+        const filePath = 'F:\\Music\\ChiliChill - 时光盲盒.flac';
+        const metadata = await parseFile(filePath);
+
+        // Output the parsed metadata to the console in a readable format
+        console.log(inspect(metadata, { showHidden: false, depth: null }));
+    } catch (error) {
+        console.error('Error parsing metadata:', error.message);
+    }
+})();

+ 102 - 3
yarn.lock

@@ -947,6 +947,11 @@
   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0"
   integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==
 
+"@sec-ant/readable-stream@^0.4.1":
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c"
+  integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
+
 "@sinclair/typebox@^0.27.8":
   version "0.27.8"
   resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz"
@@ -990,6 +995,11 @@
   dependencies:
     "@tanstack/virtual-core" "3.5.1"
 
+"@tokenizer/token@^0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
+  integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
+
 "@tootallnate/once@2":
   version "2.0.0"
   resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
@@ -2116,7 +2126,7 @@ content-disposition@0.5.4:
   dependencies:
     safe-buffer "5.2.1"
 
-content-type@~1.0.4, content-type@~1.0.5:
+content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5:
   version "1.0.5"
   resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz"
   integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@@ -2204,6 +2214,13 @@ debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, de
   dependencies:
     ms "2.1.2"
 
+debug@^4.3.7:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+  dependencies:
+    ms "^2.1.3"
+
 decompress-response@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz"
@@ -2715,6 +2732,16 @@ fd-slicer@~1.1.0:
   dependencies:
     pend "~1.2.0"
 
+file-type@^19.6.0:
+  version "19.6.0"
+  resolved "https://registry.yarnpkg.com/file-type/-/file-type-19.6.0.tgz#b43d8870453363891884cf5e79bb3e4464f2efd3"
+  integrity sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==
+  dependencies:
+    get-stream "^9.0.1"
+    strtok3 "^9.0.1"
+    token-types "^6.0.0"
+    uint8array-extras "^1.3.0"
+
 file-uri-to-path@1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"
@@ -2908,6 +2935,14 @@ get-stream@^6.0.0:
   resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
+get-stream@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27"
+  integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==
+  dependencies:
+    "@sec-ant/readable-stream" "^0.4.1"
+    is-stream "^4.0.1"
+
 getopts@2.3.0:
   version "2.3.0"
   resolved "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz"
@@ -3150,7 +3185,7 @@ iconv-lite@^0.6.2:
   dependencies:
     safer-buffer ">= 2.1.2 < 3.0.0"
 
-ieee754@^1.1.13:
+ieee754@^1.1.13, ieee754@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -3282,6 +3317,11 @@ is-stream@^2.0.0:
   resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz"
   integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
+is-stream@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b"
+  integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==
+
 is-unicode-supported@^0.1.0:
   version "0.1.0"
   resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz"
@@ -3865,6 +3905,11 @@ lines-and-columns@^1.1.6:
   resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
   integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
 
+link@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/link/-/link-2.1.1.tgz#c5db408c295fcc75c9f7ff44ae62607e9b836dfa"
+  integrity sha512-NV3AUVYBovJ6eVQcTeRoPnZSxzt2LOijNd+ugEZKRy/XeQlpTRhVRkuDv5kOlXwMAUx30vfUc7asRFb9RT65yg==
+
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
@@ -3999,6 +4044,11 @@ media-typer@0.3.0:
   resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
   integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
 
+media-typer@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
+  integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==
+
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
@@ -4181,7 +4231,7 @@ ms@2.1.2, ms@^2.0.0:
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3:
+ms@2.1.3, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -4191,6 +4241,21 @@ muggle-string@^0.3.1:
   resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.3.1.tgz#e524312eb1728c63dd0b2ac49e3282e6ed85963a"
   integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==
 
+music-metadata@^10.6.4:
+  version "10.6.4"
+  resolved "https://registry.yarnpkg.com/music-metadata/-/music-metadata-10.6.4.tgz#0aa628ac02c4c2bb795a134d5277b28422418874"
+  integrity sha512-42ekQ5CRic4Pvw/85FfzMKegeRDHyWBpCjSSI1B9PTGqaevZ17ASA4v4W6MRq1ELC5THn5rD8S+82iPQ6gv6lw==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    content-type "^1.0.5"
+    debug "^4.3.7"
+    file-type "^19.6.0"
+    link "^2.1.1"
+    media-typer "^1.1.0"
+    strtok3 "^10.0.1"
+    token-types "^6.0.0"
+    uint8array-extras "^1.4.0"
+
 mz@^2.7.0:
   version "2.7.0"
   resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
@@ -4462,6 +4527,11 @@ path-to-regexp@0.1.7:
   resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
   integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
 
+peek-readable@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.3.1.tgz#9cc2c275cceda9f3d07a988f4f664c2080387dff"
+  integrity sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==
+
 pend@~1.2.0:
   version "1.2.0"
   resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
@@ -5205,6 +5275,22 @@ strip-json-comments@~2.0.1:
   resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz"
   integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
 
+strtok3@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-10.0.1.tgz#d718fd0be837d95a854123581a080b40a8917caa"
+  integrity sha512-7OOJepVlvlcgjW/fLNCsIqpNleAoi1y0LTRWGnOpABOSpRmw+65HvnruoOCnjpaQ1efnlYpQ/JwHKuaombnuXQ==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    peek-readable "^5.3.1"
+
+strtok3@^9.0.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-9.1.1.tgz#f8feb188b3fcdbf9b8819cc9211a824c3731df38"
+  integrity sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    peek-readable "^5.3.1"
+
 sucrase@^3.32.0:
   version "3.35.0"
   resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz"
@@ -5387,6 +5473,14 @@ toidentifier@1.0.1:
   resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
+token-types@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/token-types/-/token-types-6.0.0.tgz#1ab26be1ef9c434853500c071acfe5c8dd6544a3"
+  integrity sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    ieee754 "^1.2.1"
+
 truncate-utf8-bytes@^1.0.0:
   version "1.0.2"
   resolved "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz"
@@ -5448,6 +5542,11 @@ typescript@^5.0.2, typescript@^5.3.3:
   resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz"
   integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
 
+uint8array-extras@^1.3.0, uint8array-extras@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.4.0.tgz#e42a678a6dd335ec2d21661333ed42f44ae7cc74"
+  integrity sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==
+
 undici-types@~5.26.4:
   version "5.26.5"
   resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"

+ 5 - 0
功能文档.md

@@ -59,3 +59,8 @@
 1. 桌面小组件直接进入
 2. 下方滚动条进入
 
+## 音乐播放器
+### 扫描逻辑设计
+1. 扫描音频文件时,在对应目录下创建扫描文件. 
+其中记录文件的名称, 以及最后修改时间.
+2. 每次开始扫描都先检查其中的内容