From 8e86d378d02f03e7ded91f0adf6da4181ec83eef Mon Sep 17 00:00:00 2001 From: alger Date: Mon, 13 Jan 2025 22:13:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E4=BC=98=E5=8C=96=E9=9F=B3?= =?UTF-8?q?=E4=B9=90=E8=A7=A3=E6=9E=90=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E8=AE=B0=E5=BD=95=20=E6=B7=BB=E5=8A=A0=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=BB=9A=E5=8A=A8=E5=8A=A0=E8=BD=BD=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E5=85=B3=E9=97=AD=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- src/main/index.ts | 11 +- src/main/unblockMusic.ts | 175 ++++++++++++++++++-- src/renderer/api/search.ts | 2 + src/renderer/utils/index.ts | 3 + src/renderer/views/search/index.vue | 248 +++++++++++++++++++++++----- src/renderer/views/set/index.vue | 10 +- 7 files changed, 391 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 18fdbf6..2d6c168 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,6 @@ bun.lockb .env.*.local -out \ No newline at end of file +out + +.cursorrules \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 4cedd15..815024f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -24,17 +24,18 @@ let mainWindow: Electron.BrowserWindow; // 初始化应用 function initialize() { - // 初始化各个模块 + // 初始化配置管理 initializeConfig(); - initializeFileManager(); + // 初始化缓存管理 initializeCacheManager(); + // 初始化文件管理 + initializeFileManager(); + // 初始化窗口管理 + initializeWindowManager(); // 创建主窗口 mainWindow = createMainWindow(icon); - // 初始化窗口管理 - initializeWindowManager(); - // 初始化托盘 initializeTray(iconPath, mainWindow); diff --git a/src/main/unblockMusic.ts b/src/main/unblockMusic.ts index 7cfbc8c..54663ed 100644 --- a/src/main/unblockMusic.ts +++ b/src/main/unblockMusic.ts @@ -1,23 +1,162 @@ import match from '@unblockneteasemusic/server'; +import Store from 'electron-store'; -const unblockMusic = async (id: any, songData: any): Promise => { - return new Promise((resolve, reject) => { - match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'], songData) - .then((data) => { - resolve({ - data: { - data, - params: { - id, - type: 'song' - } - } - }); - }) - .catch((err) => { - reject(err); - }); +type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube'; + +interface SongData { + name: string; + artists: Array<{ name: string }>; + album?: { name: string }; +} + +interface ResponseData { + url: string; + br: number; + size: number; + md5?: string; + platform?: Platform; + gain?: number; +} + +interface UnblockResult { + data: { + data: ResponseData; + params: { + id: number; + type: 'song'; + }; + }; +} + +interface CacheData extends UnblockResult { + timestamp: number; +} + +interface CacheStore { + [key: string]: CacheData; +} + +// 初始化缓存存储 +const store = new Store({ + name: 'unblock-cache' +}); + +// 缓存过期时间(24小时) +const CACHE_EXPIRY = 24 * 60 * 60 * 1000; + +/** + * 检查缓存是否有效 + * @param cacheData 缓存数据 + * @returns boolean + */ +const isCacheValid = (cacheData: CacheData | null): boolean => { + if (!cacheData) return false; + const now = Date.now(); + return now - cacheData.timestamp < CACHE_EXPIRY; +}; + +/** + * 从缓存中获取数据 + * @param id 歌曲ID + * @returns CacheData | null + */ +const getFromCache = (id: string | number): CacheData | null => { + const cacheData = store.get(String(id)) as CacheData | null; + if (isCacheValid(cacheData)) { + return cacheData; + } + // 清除过期缓存 + store.delete(String(id)); + return null; +}; + +/** + * 将数据存入缓存 + * @param id 歌曲ID + * @param data 解析结果 + */ +const saveToCache = (id: string | number, data: UnblockResult): void => { + const cacheData: CacheData = { + ...data, + timestamp: Date.now() + }; + store.set(String(id), cacheData); +}; + +/** + * 清理过期缓存 + */ +const cleanExpiredCache = (): void => { + const allData = store.store; + Object.entries(allData).forEach(([id, data]) => { + if (!isCacheValid(data)) { + store.delete(id); + } }); }; -export { unblockMusic }; +/** + * 音乐解析函数 + * @param id 歌曲ID + * @param songData 歌曲信息 + * @param retryCount 重试次数 + * @returns Promise + */ +const unblockMusic = async ( + id: number | string, + songData: SongData, + retryCount = 3 +): Promise => { + // 检查缓存 + const cachedData = getFromCache(id); + if (cachedData) { + return cachedData; + } + + // 所有可用平台 + const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube']; + + const retry = async (attempt: number): Promise => { + try { + const data = await match(parseInt(String(id), 10), platforms, songData); + const result: UnblockResult = { + data: { + data, + params: { + id: parseInt(String(id), 10), + type: 'song' + } + } + }; + + // 保存到缓存 + saveToCache(id, result); + return result; + } catch (err) { + if (attempt < retryCount) { + // 延迟重试,每次重试增加延迟时间 + await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); + return retry(attempt + 1); + } + + // 所有重试都失败后,抛出详细错误 + throw new Error( + `音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}` + ); + } + }; + + return retry(1); +}; + +// 定期清理过期缓存(每小时执行一次) +setInterval(cleanExpiredCache, 60 * 60 * 1000); + +export { + cleanExpiredCache, // 导出清理缓存函数,以便手动调用 + type Platform, + type ResponseData, + type SongData, + unblockMusic, + type UnblockResult +}; diff --git a/src/renderer/api/search.ts b/src/renderer/api/search.ts index 7d6a5df..dbbc81b 100644 --- a/src/renderer/api/search.ts +++ b/src/renderer/api/search.ts @@ -3,6 +3,8 @@ import request from '@/utils/request'; interface IParams { keywords: string; type: number; + limit?: number; + offset?: number; } // 搜索内容 export const getSearch = (params: IParams) => { diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index a1ae092..2a3e937 100644 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -23,6 +23,9 @@ export const setAnimationClass = (type: String) => { }; // 设置动画延时 export const setAnimationDelay = (index: number = 6, time: number = 50) => { + if (store.state.setData?.noAnimate) { + return ''; + } const speed = store.state.setData?.animationSpeed || 1; return `animation-delay:${(index * time) / (speed * 2)}ms`; }; diff --git a/src/renderer/views/search/index.vue b/src/renderer/views/search/index.vue index e3075c3..82752c5 100644 --- a/src/renderer/views/search/index.vue +++ b/src/renderer/views/search/index.vue @@ -29,8 +29,15 @@ class="search-list" :class="setAnimationClass('animate__fadeInUp')" :native-scrollbar="false" + @scroll="handleScroll" > -
{{ hotKeyword }}
+
+ + {{ hotKeyword }} +
+ +
+ + 加载中... +
+
没有更多了
+ + +
@@ -82,6 +124,51 @@ const store = useStore(); const searchDetail = ref(); const searchType = computed(() => store.state.searchType as number); const searchDetailLoading = ref(false); +const searchHistory = ref([]); + +// 添加分页相关的状态 +const ITEMS_PER_PAGE = 30; // 每页数量 +const page = ref(0); +const hasMore = ref(true); +const isLoadingMore = ref(false); +const currentKeyword = ref(''); + +// 从 localStorage 加载搜索历史 +const loadSearchHistory = () => { + const history = localStorage.getItem('searchHistory'); + searchHistory.value = history ? JSON.parse(history) : []; +}; + +// 保存搜索历史 +const saveSearchHistory = (keyword: string) => { + if (!keyword) return; + const history = searchHistory.value; + // 移除重复的关键词 + const index = history.indexOf(keyword); + if (index > -1) { + history.splice(index, 1); + } + // 添加到开头 + history.unshift(keyword); + // 只保留最近的20条记录 + if (history.length > 20) { + history.pop(); + } + searchHistory.value = history; + localStorage.setItem('searchHistory', JSON.stringify(history)); +}; + +// 清空搜索历史 +const clearSearchHistory = () => { + searchHistory.value = []; + localStorage.removeItem('searchHistory'); +}; + +// 删除搜索历史 +const handleCloseSearchHistory = (keyword: string) => { + searchHistory.value = searchHistory.value.filter((item) => item !== keyword); + localStorage.setItem('searchHistory', JSON.stringify(searchHistory.value)); +}; // 热搜列表 const hotSearchData = ref(); @@ -92,6 +179,7 @@ const loadHotSearch = async () => { onMounted(() => { loadHotSearch(); + loadSearchHistory(); loadSearch(route.query.keyword); }); @@ -114,48 +202,103 @@ watch( ); const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value; -const loadSearch = async (keywords: any, type: any = null) => { - hotKeyword.value = keywords; - searchDetail.value = undefined; +const loadSearch = async (keywords: any, type: any = null, isLoadMore = false) => { if (!keywords) return; - searchDetailLoading.value = true; - const { data } = await getSearch({ keywords, type: type || searchType.value }); + if (!isLoadMore) { + hotKeyword.value = keywords; + searchDetail.value = undefined; + page.value = 0; + hasMore.value = true; + currentKeyword.value = keywords; + } else if (isLoadingMore.value || !hasMore.value) { + return; + } - const songs = data.result.songs || []; - const albums = data.result.albums || []; - const mvs = (data.result.mvs || []).map((item: any) => ({ - ...item, - picUrl: item.cover, - playCount: item.playCount, - desc: item.artists.map((artist: any) => artist.name).join('/'), - type: 'mv' - })); + // 保存搜索历史 + if (!isLoadMore) { + saveSearchHistory(keywords); + } - const playlists = (data.result.playlists || []).map((item: any) => ({ - ...item, - picUrl: item.coverImgUrl, - playCount: item.playCount, - desc: item.creator.nickname, - type: 'playlist' - })); + if (isLoadMore) { + isLoadingMore.value = true; + } else { + searchDetailLoading.value = true; + } - // songs map 替换属性 - songs.forEach((item: any) => { - item.picUrl = item.al.picUrl; - item.artists = item.ar; - }); - albums.forEach((item: any) => { - item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`; - }); - searchDetail.value = { - songs, - albums, - mvs, - playlists - }; + try { + const { data } = await getSearch({ + keywords: currentKeyword.value, + type: type || searchType.value, + limit: ITEMS_PER_PAGE, + offset: page.value * ITEMS_PER_PAGE + }); - searchDetailLoading.value = false; + const songs = data.result.songs || []; + const albums = data.result.albums || []; + const mvs = (data.result.mvs || []).map((item: any) => ({ + ...item, + picUrl: item.cover, + playCount: item.playCount, + desc: item.artists.map((artist: any) => artist.name).join('/'), + type: 'mv' + })); + + const playlists = (data.result.playlists || []).map((item: any) => ({ + ...item, + picUrl: item.coverImgUrl, + playCount: item.playCount, + desc: item.creator.nickname, + type: 'playlist' + })); + + // songs map 替换属性 + songs.forEach((item: any) => { + item.picUrl = item.al.picUrl; + item.artists = item.ar; + }); + albums.forEach((item: any) => { + item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`; + }); + + if (isLoadMore && searchDetail.value) { + // 合并数据 + searchDetail.value.songs = [...searchDetail.value.songs, ...songs]; + searchDetail.value.albums = [...searchDetail.value.albums, ...albums]; + searchDetail.value.mvs = [...searchDetail.value.mvs, ...mvs]; + searchDetail.value.playlists = [...searchDetail.value.playlists, ...playlists]; + } else { + searchDetail.value = { + songs, + albums, + mvs, + playlists + }; + } + + // 判断是否还有更多数据 + hasMore.value = + songs.length === ITEMS_PER_PAGE || + albums.length === ITEMS_PER_PAGE || + mvs.length === ITEMS_PER_PAGE || + playlists.length === ITEMS_PER_PAGE; + + page.value++; + } catch (error) { + console.error('搜索失败:', error); + } finally { + searchDetailLoading.value = false; + isLoadingMore.value = false; + } +}; + +// 添加滚动处理函数 +const handleScroll = (e: any) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + // 距离底部100px时加载更多 + if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoadingMore.value && hasMore.value) { + loadSearch(currentKeyword.value, null, true); + } }; watch( @@ -171,6 +314,11 @@ const handlePlay = () => { const tracks = searchDetail.value?.songs || []; store.commit('setPlayList', tracks); }; + +// 点击搜索历史 +const handleSearchHistory = (keyword: string) => { + loadSearch(keyword, 1); +}; diff --git a/src/renderer/views/set/index.vue b/src/renderer/views/set/index.vue index 4240337..2c56cb5 100644 --- a/src/renderer/views/set/index.vue +++ b/src/renderer/views/set/index.vue @@ -32,7 +32,15 @@
动画速度
-
调节动画播放速度
+
+
+ + + + + 是否开启动画 +
+
{{ setData.animationSpeed }}x