Browse Source

feat: 歌曲扫描检索功能完善
1. 列出扫描到的歌曲
2. 将扫描歌单转移到软件启动后

kindring 2 months ago
parent
commit
02ccd5c863

+ 1 - 0
src/apis/ApiAction.ts

@@ -6,6 +6,7 @@ export enum Magnet_Actions {
 
 
 export enum Music_Actions {
+  music_app_start = 'music_app_start',
   play_list_fetch = 'play_list_fetch',
   scan_music_select = 'scan_music_select',
   scan_music_add = 'scan_music_add',

+ 20 - 4
src/apis/musicControl.ts

@@ -2,7 +2,14 @@ import api from "./baseApi.ts"
 import {Order, Page, ResponseData} from "@/types/apiTypes.ts";
 import {Music_Actions} from "@/apis/ApiAction.ts";
 import {MusicInfo, MusicScanSetting, PlayList} from "@/types/musicType.ts";
+import {promises} from "fs-extra";
 
+export async function musicAppStart()
+{
+    let [_callId, promise] = api.sendQuery(Music_Actions.music_app_start, {});
+    let response = await promise;
+    return response;
+}
 export async function fetchPlayList(): Promise< ResponseData<PlayList[]> >
 {
     let [_callId, promise] = api.sendQuery(Music_Actions.play_list_fetch, {});
@@ -42,10 +49,19 @@ export async function deleteScanConfig(id: number) : Promise<ResponseData<boolea
     return await promise;
 }
 
-export async function fetchScanMusic(scanId: number, page: number = 1, size: number = 10, 
-                                     orderBy: string = 'id', 
+export async function fetchScanMusic(scanId: number, page: number = 1, size: number = 10,
+                                     key: string = '',
+                                     sort: 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});
+    let [_callId, promise] = api.sendQuery(Music_Actions.scan_music_fetch,
+        {data: scanId, page, size, sort, order, key} as Page<number>);
     return await promise as ResponseData<Page<MusicInfo[]>>;
-}
+}
+
+export async function api_likeMusic(musicId: number): Promise<ResponseData<boolean>>
+{
+    let [_callId, promise] = api.sendQuery(Music_Actions.like_music, musicId);
+    return await promise;
+}
+

+ 12 - 3
src/assets/public.css

@@ -53,10 +53,11 @@
     width: 100%;
     height: calc(100% - 160px);
     overflow: hidden;
+    font-size: 0.9em;
 }
 .music-list-head{
-    width: calc(100% - 30px);
-    height: 50px;
+    width: 100%;
+    height: 60px;
     box-sizing: border-box;
     flex-shrink: 0;
 }
@@ -73,16 +74,24 @@
     grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr; /* 设置列的布局 */
     gap: 10px; /* 列之间的间距 */
     width: 100%;
-    height: 50px;
+    height: 60px;
+    padding: 5px 10px;
+    box-sizing: border-box;
 }
 
+.music-list-item .name{
+    font-weight: bold;
+    font-size: 1.1rem;
+}
 
 .cover {
     width: 50px; /* 封面图片固定宽度 */
     height: 50px; /* 和高度保持一致 */
     overflow: hidden; /* 隐藏超出部分 */
+    border-radius: 5px;
 }
 
+
 .cover img {
     width: 100%; /* 图片宽度100% */
     height: auto; /* 高度自适应 */

+ 41 - 12
src/common/db/db_music.ts

@@ -109,11 +109,19 @@ async function _initSongsTable(db : Knex): PromiseResult<boolean>
         {
             // 移除旧数据
             // 更新表字段
+            console.log('修改音频表信息');
+            // 打印表结构
+            // 移除表
+            // 字段修改 scanId 从 string -> integer
+            // await db.schema.alterTable(MusicTableName.music_songs, (table) => {
+            //     table.dropColumn('scanId')
+            // })
+
             // await db.schema.alterTable(MusicTableName.music_songs, (table) => {
             //     // table.string('key')
-            //     table.string('scanId')
+            //     table.integer('scanId')
             // })
-            await removeMusicByScanId(0);
+            await removeMusicByScanId(4);
         }
         return [null, true];
     }
@@ -533,8 +541,19 @@ export async function likeMusic(id: number, isLike: boolean) : PromiseResult<boo
     }
     return [null, true];
 }
-export async function getMusicsByScanId(scanId: number, page: number = 1, size: number = 10,
-                                        orderBy: string = 'id', order: 'asc' | 'desc' = 'asc')
+
+
+/**
+ * 根据扫描配置ID获取音乐
+ * @param scanId
+ * @param key
+ * @param page
+ * @param size
+ * @param sort
+ * @param order
+ */
+export async function getMusicsByScanId(scanId: number, key: string = '', page: number = 1, size: number = 10,
+                                        sort: string = 'id', order: 'asc' | 'desc' = 'asc')
     : PromiseResult<Page<MusicInfo[]>>
 {
     let db = loadDb(AppDbName.music_db)
@@ -549,7 +568,8 @@ export async function getMusicsByScanId(scanId: number, page: number = 1, size:
         page: page,
         size: size,
         order: order as Order,
-        sort: orderBy
+        sort: sort,
+        key: key,
     }
     if (page === 1)
     {
@@ -557,18 +577,27 @@ export async function getMusicsByScanId(scanId: number, page: number = 1, size:
         countPromise = db.count('id as count')
             .from(MusicTableName.music_songs)
             .where('scanId', scanId)
+            if (key) {
+                countPromise.andWhere('key', 'like', `%${key}%`)
+            }
     } else
     {
-        countPromise = Promise.resolve({count: 0});
+        countPromise = Promise.resolve([{count: 0}]);
     }
     let listPromise = db.select(...Music_field)
         .from(MusicTableName.music_songs)
         .where('scanId', scanId)
-        .limit(size)
+        if (key) {
+            console.log('key')
+            listPromise.andWhere('key', 'like', `%${key}%`)
+        }
+        listPromise.limit(size)
         .offset((page - 1) * size)
-        .orderBy(orderBy, order)
+        .orderBy(sort, order)
+
 
-    let [err, res] = await handle<[{ count: number}, MusicInfo[]]>(Promise.all([countPromise, listPromise]) as Promise<[{ count: number}, MusicInfo[]]>)
+    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}`)
@@ -577,9 +606,9 @@ export async function getMusicsByScanId(scanId: number, page: number = 1, size:
         logger.error(`[获取扫描歌曲] 无法获取指定歌单数据`)
         return [err, resData];
     }
-
-    resData.total = res[0].count as number;
+    resData.total = res[0][0].count as number;
     resData.data = res[1] as MusicInfo[];
+    console.log(resData);
     return [err, resData];
 }
 
@@ -600,4 +629,4 @@ export async function removeMusicByScanId(scanId: number) : PromiseResult<boolea
         return [err, false];
     }
     return [err, true];
-}
+}

+ 64 - 23
src/components/music/common/scanListInfo.vue

@@ -1,9 +1,13 @@
 <script setup lang="ts">
 import {MusicInfo, MusicScanSetting} from "@/types/musicType.ts";
-  import {PropType, ref} from "vue";
+import {onBeforeMount, PropType, ref, watch} from "vue";
 import LickIcon from "@/components/music/common/lickIcon.vue";
 import IconSvg from "@/components/public/icon/iconSvg.vue";
 import message from "@/components/public/kui/message";
+import {api_likeMusic, fetchScanMusic} from "@/apis/musicControl.ts";
+import {ErrorCode} from "@/types/apiTypes.ts";
+import {secondToTimeStr} from "@/util/time.ts";
+import {music_action_emits, Music_Action_events} from "@/components/music/music_emits.ts";
 
 const props = defineProps({
   scanSetting: {
@@ -12,37 +16,73 @@ const props = defineProps({
   }
 })
 
+
+let scanSetting_id = ref(0)
 const scanCount = ref(0)
-const musicList = ref<MusicInfo[]>([
+const musicList = ref<MusicInfo[]>([])
+const search_key = ref("");
+const search_page = ref(1);
+const page_limit = 10;
+const lock_loading = ref(false);
+async function loadMusic(scanSetting: MusicScanSetting, page: number, key: string = '')
+{
+  console.log("loadMusic");
+  if (lock_loading.value)
+  {
+    message.info("正在加载中,请稍后");
+    return;
+  }
+  lock_loading.value = true;
+  let res = await fetchScanMusic(scanSetting.id, page, page_limit, key)
+  lock_loading.value = false;
+  if (res.code === ErrorCode.success)
   {
-    name: "霜雪千年",
-    artists: ["1"],
-    cover: "1",
-    duration: 1,
-    filePath: "1",
-    id: 1,
-    key: "",
-    isLike: true,
-    isLocal: true,
-    lyricPath: "1",
-    origin: "1",
-    playCount: 1,
-    tags: ["1"],
-    type: 1,
-    album: "1",
-    scanId: 1,
-  },
-])
+    if (page === 1)
+    {
+      scanCount.value = res.data.total?? 0;
+    }
 
+    let pageData = res.data.data ;
+    for ( let i = 0; i < pageData.length; i++)
+    {
+      pageData[i].isLike = !!pageData[i].isLike;
+      pageData[i].isLocal = !!pageData[i].isLocal;
+      musicList.value.push(pageData[i]);
+    }
+  }
+  else {
+    message.error(res.msg);
+  }
+}
+
+watch(()=>props.scanSetting, ()=>{
+  if (scanSetting_id.value !== props.scanSetting.id)
+  {
+    musicList.value = [];
+    loadMusic(props.scanSetting, 1, search_key.value);
+  }
+})
+
+async function loadMore()
+{
+  search_page.value++;
+  await loadMusic(props.scanSetting, search_page.value, search_key.value);
+}
+
+onBeforeMount(()=>{
+  loadMusic(props.scanSetting, search_page.value, search_key.value);
+})
 
 function playMusic(item: MusicInfo) {
   console.log(item);
   message.info(`play ${item.name}`);
+  music_action_emits(Music_Action_events.play_music, item);
 }
 
 function likeMusic(item: MusicInfo) {
   console.log(item);
   message.info(`like ${item.name}`);
+  api_likeMusic(item.id);
 }
 function showMore(item: MusicInfo) {
   console.log(item);
@@ -76,6 +116,7 @@ function showMore(item: MusicInfo) {
         <div class="isLike">喜欢</div>
       </div>
     </div>
+<!--    让下面框在滑动到底部时自动加载下一级数据 -->
     <div class="music-list-con scroll">
       <div v-for="item in musicList"
            class="music-list-item"
@@ -87,7 +128,7 @@ function showMore(item: MusicInfo) {
         <div class="name">{{item.name}}</div>
         <div class="artists">{{item.artists}}</div>
         <div class="origin">{{item.origin}}</div>
-        <div class="duration">{{item.duration}}</div>
+        <div class="duration">{{ secondToTimeStr(item.duration, "m分s秒" )}}</div>
         <lick-icon class="isLike" :like="item.isLike"
                    @click.stop.capture="likeMusic(item)"/>
         <div class="more">
@@ -115,10 +156,10 @@ function showMore(item: MusicInfo) {
   box-sizing: border-box;
   border-radius: 5px;
   margin: 5px auto;
-
+  font-size: 1em;
   background-color: var(--color-background-mute);
 }
-.name {
+.info .name {
   width: 100%;
   height: 40px;
   font-size: 1.5rem;

+ 14 - 1
src/components/music/musicIndex.vue

@@ -5,7 +5,7 @@ import {MusicScanSetting, PlayList} from "@/types/musicType.ts";
 import PlayListInfo from "./common/playListInfo.vue";
 import message from "@/components/public/kui/message";
 import MusicSetting from "@/components/music/common/musicSetting.vue";
-import {fetchPlayList, fetchScanConfig} from "@/apis/musicControl.ts";
+import {fetchPlayList, fetchScanConfig, musicAppStart} from "@/apis/musicControl.ts";
 import {ErrorCode} from "@/types/apiTypes.ts";
 import {Tab, TabGroup, TabList, TabPanel, TabPanels} from "@headlessui/vue";
 import ScanListInfo from "@/components/music/common/scanListInfo.vue";
@@ -57,8 +57,21 @@ async function loadPlayList()
   }
 }
 
+async function startScan()
+{
+  let responseData = await musicAppStart();
+  if (responseData.code === ErrorCode.success)
+  {
+    message.success(`扫描成功`);
+  } else
+  {
+    message.error(responseData.msg);
+  }
+}
+
 onBeforeMount(()=>{
   loadPlayList();
+  startScan();
 })
 
 function showMusicSetting()

+ 13 - 0
src/components/music/music_emits.ts

@@ -0,0 +1,13 @@
+import {MusicInfo} from "@/types/musicType.ts";
+
+export enum Music_Action_events {
+    like_music = 'like_music',
+    play_music = 'play_music',
+    play_next = 'play_next',
+    play_prev = 'play_prev',
+    play_pause = 'play_pause',
+    play_stop = 'play_stop',
+}
+export const music_action_emits = defineEmits<{
+    (e: Music_Action_events.play_music , music: MusicInfo): void,
+}>()

+ 1 - 1
src/main/AppControl.ts

@@ -337,7 +337,7 @@ export async function initApp(appConfig: AppConfig, app: Electron.App) : Promise
     if(err){
         logger.error(`[应用初始化] 初始化音乐库失败: ${err}`);
     }
-    await start_scan();
+    // start_scan();
     if(flag){
         logger.info(`[应用初始化] 初始化音乐库完成`);
     }

+ 4 - 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_like_music, c_load_scan_music,
+    c_fetchPlayList, c_like_music, c_load_scan_music, c_music_appStart,
     c_scanMusicAdd, c_scanMusicDelete,
     c_scanMusicSelect,
     c_scanMusicUpdate,
@@ -23,6 +23,9 @@ export async function apiRouter(requestData: RequestData<any>){
         case Magnet_Actions.magnet_delete:
             responseData = await c_magnet_delete(requestData);
             break;
+        case Music_Actions.music_app_start:
+            responseData = await c_music_appStart(requestData);
+            break;
         case Music_Actions.play_list_fetch:
             responseData = await c_fetchPlayList(requestData);
             break;

+ 21 - 5
src/main/control/magnet/music.ts

@@ -109,6 +109,9 @@ export async function c_scanMusicAdd(requestData: RequestData<MusicScanSetting>)
         return t_gen_res(requestData, ErrorCode.db, '添加扫描设置失败', false)
     }
     res = res as boolean;
+    if (res) {
+        _scan_(scanSetting);
+    }
     return t_res_ok(requestData,  res)
 }
 
@@ -170,9 +173,10 @@ export async function c_load_scan_music(requestData: RequestData<Page<number>>)
 {
     let queryParam = requestData.data;
     let [err, res] = await getMusicsByScanId(queryParam.data,
+        queryParam.key,
         queryParam.page,
         queryParam.size,
-        queryParam.order,
+        queryParam.sort,
         queryParam.order)
     if (err) {
         logger.error(`[音频扫描] 获取扫描到的音频失败 ${err.message}`)
@@ -383,6 +387,7 @@ export async function _scan_(scanSetting: MusicScanSetting)
             catchInfo[filePath] = fs.statSync(filePath).mtimeMs;
             // 将封面文件写入到 本地 文件夹
             let coversDir = path.join(scanSetting.path, './.covers');
+            let coverName = "";
             // 如果文件的路径是scanSetting.path的子目录, 则再.cobers 目录下创建对应的子文件夹
             if (filePath.startsWith(scanSetting.path))
             {
@@ -398,15 +403,18 @@ export async function _scan_(scanSetting: MusicScanSetting)
             let music_name = '';
             if (musicMetaData.common.title)
             {
-                music_name = musicMetaData.common.title + `_${musicMetaData.common.artist??''}`;
+                music_name = musicMetaData.common.title;
+                coverName = music_name + `_${musicMetaData.common.artist??''}`
             } else
             {
                 music_name = path.basename(filePath);
                 // 移除后缀名
                 music_name = music_name.substring(0, music_name.lastIndexOf('.'));
+                coverName = music_name;
             }
+
             // 移除music_name中的特殊字符
-            music_name = music_name.replace(/[\\/:*?"<>|]/g, '');
+            coverName = coverName.replace(/[\\/:*?"<>|]/g, '');
             // let nextId: string = await _next_id(scanSetting.id);
             // 如果是子文件夹. 则创建对应的子目录存放封面
 
@@ -414,18 +422,19 @@ export async function _scan_(scanSetting: MusicScanSetting)
 
             if (musicMetaData.common.picture && musicMetaData.common.picture.length > 0)
             {
-                coverPath = path.join(coversDir, `${music_name}.jpg`);
+                coverPath = path.join(coversDir, `${coverName}.jpg`);
                 // console.log(coverPath)
                 // 判断封面是否存在
                 if (fs.existsSync(coverPath))
                 {
-                    coverPath = path.join(coversDir, `${music_name}_${randomAzStr(4)}.jpg`);
+                    coverPath = path.join(coversDir, `${coverName}_${randomAzStr(4)}.jpg`);
                 }
                 fs.writeFileSync(coverPath, musicMetaData.common.picture[0].data);
                 // console.log("写入成功")
             } else {
                 console.log(`[获取音频文件信息失败] ${filePath}`)
             }
+            console.log(musicMetaData)
             let musicInfo: MusicInfo = {
                 id: -1,
                 key: '',
@@ -481,3 +490,10 @@ export async function start_scan()
 }
 
 
+export async function c_music_appStart(requestData: RequestData<any>)
+{
+    logger.info(`[音乐播放器启动]`)
+    start_scan();
+    return t_res_ok(requestData, true)
+}
+

+ 3 - 1
src/types/apiTypes.ts

@@ -76,13 +76,15 @@ export interface Page<T> {
     // 每页数量
     size: number;
     // 总文件数量
-    total: number;
+    total?: number;
     // 排序字段
     sort: string;
     // 排序方式
     order: Order;
     // 实际数据
     data: T;
+    // 搜索关键字
+    key?: string;
 }
 
 

+ 31 - 11
src/util/time.ts

@@ -9,7 +9,9 @@ export interface Calendar {
     // todo 农历
 }
 
-
+function _tf(i: number): string {
+    return (i < 10 ? '0' : '') + i;
+};
 /**
  * 时间戳转时间字符串
  * @param timestamp - 时间戳
@@ -43,7 +45,27 @@ export function timeStrToTimeStamp(timeStr: string, isSecond: boolean): number {
 }
 
 
-
+/**
+ * 秒数转为格式化字符串
+ * @param seconds - 秒数
+ * @param format - 格式化字符串
+ * @returns {string} - 格式化后的时间字符串
+ */
+export function secondToTimeStr(seconds: number, format: string = 'MM:DD:HH:mm:ss'): string {
+    // 忽略小数部分, 不四舍五入
+    seconds = Math.floor(seconds);
+    return format
+        .replace(/ss/, _tf(seconds % 60))
+        .replace(/s/, String(seconds % 60))
+        .replace(/mm/, _tf(Math.floor(seconds / 60) % 60))
+        .replace(/m/, String(Math.floor(seconds / 60) % 60))
+        .replace(/HH/, _tf(Math.floor(seconds / 3600)))
+        .replace(/H/, String(Math.floor(seconds / 3600)))
+        .replace(/DD/, _tf(Math.floor(seconds / 86400)))
+        .replace(/D/, String(Math.floor(seconds / 86400)))
+        .replace(/MM/, _tf(Math.floor(seconds / 86400)))
+        .replace(/M/, String(Math.floor(seconds / 86400)));
+}
 
 
 /**
@@ -53,20 +75,18 @@ export function timeStrToTimeStamp(timeStr: string, isSecond: boolean): number {
  * @returns {string} - 格式化后的时间字符串
  */
 export function timeFormat(t: Date, format: string): string {
-    let tf = function(i: number) {
-        return (i < 10 ? '0' : '') + i;
-    };
 
-    return format.replace(/yyyy/, tf(t.getFullYear()))
-        .replace(/MM/, tf(t.getMonth() + 1))
+
+    return format.replace(/yyyy/, _tf(t.getFullYear()))
+        .replace(/MM/, _tf(t.getMonth() + 1))
         .replace(/M/, String(t.getMonth() + 1))
-        .replace(/dd/, tf(t.getDate()))
+        .replace(/dd/, _tf(t.getDate()))
         .replace(/d/, String(t.getDate()))
-        .replace(/HH/, tf(t.getHours()))
+        .replace(/HH/, _tf(t.getHours()))
         .replace(/H/, String(t.getHours()))
-        .replace(/mm/, tf(t.getMinutes()))
+        .replace(/mm/, _tf(t.getMinutes()))
         .replace(/m/, String(t.getMinutes()))
-        .replace(/ss/, tf(t.getSeconds()))
+        .replace(/ss/, _tf(t.getSeconds()))
         .replace(/s/, String(t.getSeconds()));
 }