mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 11:29:42 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ad32e8ea | ||
|
|
ca3213d0b3 | ||
|
|
7c7455786e | ||
|
|
68fb9bcec5 | ||
|
|
54e2842b1b | ||
|
|
ce743e1b65 | ||
|
|
32c9fdbfeb | ||
|
|
9df236b2e0 | ||
|
|
0988c71282 | ||
|
|
60881f7f48 | ||
|
|
775f87aa86 | ||
|
|
b1c471f15c | ||
|
|
f7ecfa1fa9 | ||
|
|
d44be6022a | ||
|
|
0c512bccff | ||
|
|
b07cc2359a | ||
|
|
46756a8b09 | ||
|
|
deb73fa789 | ||
|
|
910ab1ff10 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm i -g yarn
|
||||
yarn install # 安装项目依赖
|
||||
yarn # 安装项目依赖
|
||||
|
||||
- name: Build Electron App for windows
|
||||
if: matrix.os == 'windows-latest' # 只在Windows上运行
|
||||
|
||||
11
README.md
11
README.md
@@ -295,7 +295,11 @@ CeruMuisc/
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有技术问题或合作意向(仅限技术交流),请通过 Gitee 私信联系项目维护者。
|
||||
如有技术问题或合作意向
|
||||
可通过如下方式联系
|
||||
- QQ: 2115295703
|
||||
- 微信:13600973542
|
||||
- 邮箱:sqj@shiqianjiang.cn
|
||||
|
||||
## 项目开发者
|
||||
|
||||
@@ -357,8 +361,3 @@ CeruMuisc/
|
||||
|
||||
若您认可本项目的技术价值,欢迎通过以下方式支持开发者(仅用于项目技术维护与迭代):
|
||||
<img src="assets/image-20250827175356006.png" alt="赞助方式1" style="zoom:33%;" /><img src="assets/image-20250827175547444.png" alt="赞助方式2" style="zoom: 33%;" />
|
||||
|
||||
## 联系
|
||||
|
||||
关于项目问题也可联系
|
||||
邮箱:sqj@shiqianjiang.cn
|
||||
|
||||
BIN
docs/assets/image-20251003173109619.png
Normal file
BIN
docs/assets/image-20251003173109619.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 958 KiB |
BIN
docs/assets/image-20251003173141699.png
Normal file
BIN
docs/assets/image-20251003173141699.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1020 KiB |
BIN
docs/assets/image-20251003173654569.png
Normal file
BIN
docs/assets/image-20251003173654569.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 870 KiB |
@@ -5,12 +5,16 @@
|
||||
| 昵称 | 赞助金额 |
|
||||
| :-------------------------: | :------: |
|
||||
| **群友**:可惜情绪落泪零碎 | 6.66 |
|
||||
| **群友**:🍀 | 5 |
|
||||
| **群友**:🍀 | 5 |
|
||||
| **群友**:涟漪 | 50 |
|
||||
| **作者朋友** | 188 |
|
||||
| **群友**:我叫阿狸 | 3 |
|
||||
| RiseSun | 9.9 |
|
||||
| **b站小友**:光牙阿普斯木兰 | 5 |
|
||||
| 青禾 | 8.8 |
|
||||
| li peng | 200 |
|
||||
| **群友**:XIZ | 3 |
|
||||
| YL | 10 |
|
||||
| **群友**:way1437 | 50 |
|
||||
|
||||
据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn
|
||||
|
||||
@@ -46,7 +46,10 @@ features:
|
||||
|
||||
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,**仅提供插件运行框架与播放功能**,不直接存储、提供任何音乐源文件。用户需通过自行选择、安装合规插件获取音乐相关数据,项目旨在为开发者提供桌面应用技术实践与学习案例,为用户提供合规的音乐播放工具框架。
|
||||
|
||||
<img src="./assets/image-20250827175023917.png" alt="image-20250827175023917" style="zoom: 33%;" /><img src="./assets/image-20250827175109430.png" alt="image-20250827175109430" style="zoom:33%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 2rem">
|
||||
<img src="./assets/image-20251003173109619.png" alt="image-20251003173109619"/>
|
||||
<img src= "./assets/image-20251003173654569.png">
|
||||
</div>
|
||||
|
||||
## 技术栈
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ asar: true
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!website/*'
|
||||
- '!scripts/*'
|
||||
- '!assets/*'
|
||||
- '!docs/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md,.idea,.kiro,.codebuddy}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
@@ -81,4 +85,4 @@ publish:
|
||||
provider: generic
|
||||
url: https://update.ceru.shiqianjiang.cn
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
@@ -8,7 +8,6 @@ import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver'
|
||||
import wasm from 'vite-plugin-wasm'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
@@ -16,6 +15,14 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@common': resolve('src/common')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/main/index.ts'),
|
||||
lyric: resolve(__dirname, 'src/web/lyric.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
|
||||
15117
package-lock.json
generated
15117
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ceru-music",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.7",
|
||||
"description": "一款简洁优雅的音乐播放器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "sqj,wldss,star",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/needle": "^3.3.0",
|
||||
"NeteaseCloudMusicApi": "^4.27.0",
|
||||
"animate.css": "^4.1.1",
|
||||
@@ -57,8 +57,7 @@
|
||||
"dompurify": "^3.2.6",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.3.9",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"howler": "^2.2.4",
|
||||
"hpagent": "^1.2.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"jss": "^10.10.0",
|
||||
@@ -69,9 +68,9 @@
|
||||
"mitt": "^3.0.1",
|
||||
"needle": "^3.3.1",
|
||||
"node-fetch": "2",
|
||||
"node-id3": "^0.2.9",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"pinia-plugin-persistedstate": "4.5.0",
|
||||
"tdesign-icons-vue-next": "^0.4.1",
|
||||
"tdesign-vue-next": "^1.15.2",
|
||||
"vue-router": "^4.5.1",
|
||||
|
||||
10
src/common/types/config.ts
Normal file
10
src/common/types/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface lyricConfig {
|
||||
fontSize: number
|
||||
mainColor: string
|
||||
shadowColor: string
|
||||
// 窗口位置
|
||||
x?: number
|
||||
y?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
92
src/common/utils/quality.ts
Normal file
92
src/common/utils/quality.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export const QUALITY_ORDER = [
|
||||
'master',
|
||||
'atmos_plus',
|
||||
'atmos',
|
||||
'hires',
|
||||
'flac24bit',
|
||||
'flac',
|
||||
'320k',
|
||||
'192k',
|
||||
'128k'
|
||||
] as const
|
||||
|
||||
export type KnownQuality = (typeof QUALITY_ORDER)[number]
|
||||
export type QualityInput = KnownQuality | string | { type: string; size?: string }
|
||||
|
||||
const DISPLAY_NAME_MAP: Record<string, string> = {
|
||||
'128k': '标准',
|
||||
'192k': '高品',
|
||||
'320k': '超高',
|
||||
flac: '无损',
|
||||
flac24bit: '超高解析',
|
||||
hires: '高清臻音',
|
||||
atmos: '全景环绕',
|
||||
atmos_plus: '全景增强',
|
||||
master: '超清母带'
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一获取音质中文显示名称
|
||||
*/
|
||||
export function getQualityDisplayName(quality: QualityInput | null | undefined): string {
|
||||
if (!quality) return ''
|
||||
const type = typeof quality === 'object' ? (quality as any).type : quality
|
||||
return DISPLAY_NAME_MAP[type] || String(type || '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个音质优先级(返回负数表示 a 优于 b)
|
||||
*/
|
||||
export function compareQuality(aType: string, bType: string): number {
|
||||
const ia = QUALITY_ORDER.indexOf(aType as KnownQuality)
|
||||
const ib = QUALITY_ORDER.indexOf(bType as KnownQuality)
|
||||
const va = ia === -1 ? QUALITY_ORDER.length : ia
|
||||
const vb = ib === -1 ? QUALITY_ORDER.length : ib
|
||||
return va - vb
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 types,兼容 string 与 {type,size}
|
||||
*/
|
||||
export function normalizeTypes(
|
||||
types: Array<string | { type: string; size?: string }> | null | undefined
|
||||
): string[] {
|
||||
if (!types || !Array.isArray(types)) return []
|
||||
return types
|
||||
.map((t) => (typeof t === 'object' ? (t as any).type : t))
|
||||
.filter((t): t is string => Boolean(t))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组中最高音质类型
|
||||
*/
|
||||
export function getHighestQualityType(
|
||||
types: Array<string | { type: string; size?: string }> | null | undefined
|
||||
): string | null {
|
||||
const arr = normalizeTypes(types)
|
||||
if (!arr.length) return null
|
||||
return arr.sort(compareQuality)[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建并按优先级排序的 [{type, size}] 列表
|
||||
* 支持传入:
|
||||
* - 数组:[{type,size}]
|
||||
* - _types 映射:{ [type]: { size } }
|
||||
*/
|
||||
export function buildQualityFormats(
|
||||
input:
|
||||
| Array<{ type: string; size?: string }>
|
||||
| Record<string, { size?: string }>
|
||||
| null
|
||||
| undefined
|
||||
): Array<{ type: string; size?: string }> {
|
||||
if (!input) return []
|
||||
let list: Array<{ type: string; size?: string }>
|
||||
if (Array.isArray(input)) {
|
||||
list = input.map((i) => ({ type: i.type, size: i.size }))
|
||||
} else {
|
||||
list = Object.keys(input).map((k) => ({ type: k, size: input[k]?.size }))
|
||||
}
|
||||
return list.sort((a, b) => compareQuality(a.type, b.type))
|
||||
}
|
||||
105
src/main/events/index.ts
Normal file
105
src/main/events/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import InitPluginService from './plugins'
|
||||
import '../services/musicSdk/index'
|
||||
import aiEvents from '../events/ai'
|
||||
import { app, powerSaveBlocker } from 'electron'
|
||||
import { type BrowserWindow, ipcMain } from 'electron'
|
||||
export default function InitEventServices(mainWindow: BrowserWindow) {
|
||||
InitPluginService()
|
||||
aiEvents(mainWindow)
|
||||
basisEvent(mainWindow)
|
||||
}
|
||||
|
||||
function basisEvent(mainWindow: BrowserWindow) {
|
||||
let psbId: number | null = null
|
||||
// 复用主进程创建的托盘
|
||||
let tray: any = (global as any).__ceru_tray__ || null
|
||||
let isQuitting = false
|
||||
// 托盘菜单与图标由主进程统一创建,这里不再重复创建
|
||||
// 播放/暂停由主进程托盘菜单触发 'music-control' 事件
|
||||
|
||||
// 应用退出前的清理
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true
|
||||
})
|
||||
|
||||
// 窗口控制 IPC 处理
|
||||
ipcMain.on('window-minimize', () => {
|
||||
mainWindow.minimize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-maximize', () => {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize()
|
||||
} else {
|
||||
mainWindow.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
mainWindow.close()
|
||||
})
|
||||
|
||||
// Mini 模式 IPC 处理 - 最小化到系统托盘
|
||||
ipcMain.on('window-mini-mode', (_, isMini) => {
|
||||
if (mainWindow) {
|
||||
if (isMini) {
|
||||
// 进入 Mini 模式:隐藏窗口到系统托盘
|
||||
mainWindow.hide()
|
||||
// 显示托盘通知(可选)
|
||||
tray = (global as any).__ceru_tray__ || tray
|
||||
if (tray && tray.displayBalloon) {
|
||||
tray.displayBalloon({
|
||||
title: '澜音 Music',
|
||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 退出 Mini 模式:显示窗口
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 全屏模式 IPC 处理
|
||||
ipcMain.on('window-toggle-fullscreen', () => {
|
||||
const isFullScreen = mainWindow.isFullScreen()
|
||||
mainWindow.setFullScreen(!isFullScreen)
|
||||
})
|
||||
// 阻止窗口关闭,改为隐藏到系统托盘
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow?.hide()
|
||||
|
||||
// 显示托盘通知
|
||||
tray = (global as any).__ceru_tray__ || tray
|
||||
if (tray && tray.displayBalloon) {
|
||||
tray.displayBalloon({
|
||||
title: 'Ceru Music',
|
||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// 阻止系统息屏 IPC(开启/关闭)
|
||||
ipcMain.handle('power-save-blocker:start', () => {
|
||||
if (psbId == null) {
|
||||
psbId = powerSaveBlocker.start('prevent-display-sleep')
|
||||
}
|
||||
return psbId
|
||||
})
|
||||
|
||||
ipcMain.handle('power-save-blocker:stop', () => {
|
||||
if (psbId != null && powerSaveBlocker.isStarted(psbId)) {
|
||||
powerSaveBlocker.stop(psbId)
|
||||
}
|
||||
psbId = null
|
||||
return true
|
||||
})
|
||||
|
||||
// 获取应用版本号
|
||||
ipcMain.handle('get-app-version', () => {
|
||||
return app.getVersion()
|
||||
})
|
||||
}
|
||||
154
src/main/events/lyric.ts
Normal file
154
src/main/events/lyric.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { BrowserWindow, ipcMain, screen } from 'electron'
|
||||
import { isAbsolute, relative, resolve } from 'path'
|
||||
import { lyricConfig } from '@common/types/config'
|
||||
import { configManager } from '../services/ConfigManager'
|
||||
|
||||
import lyricWindow from '../windows/lyric-window'
|
||||
|
||||
const lyricStore = {
|
||||
get: () =>
|
||||
configManager.get<lyricConfig>('lyric', {
|
||||
fontSize: 30,
|
||||
mainColor: '#73BCFC',
|
||||
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 180
|
||||
}),
|
||||
set: (value: lyricConfig) => configManager.set<lyricConfig>('lyric', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词相关 IPC
|
||||
*/
|
||||
const initLyricIpc = (mainWin?: BrowserWindow | null): void => {
|
||||
// const mainWin = mainWindow.getWin()
|
||||
const lyricWin = lyricWindow.getWin()
|
||||
|
||||
// 切换桌面歌词
|
||||
ipcMain.on('change-desktop-lyric', (_event, val: boolean) => {
|
||||
if (val) {
|
||||
lyricWin?.show()
|
||||
lyricWin?.setAlwaysOnTop(true, 'screen-saver')
|
||||
} else lyricWin?.hide()
|
||||
})
|
||||
ipcMain.on('win-show', () => {
|
||||
mainWin?.show()
|
||||
})
|
||||
// 音乐名称更改
|
||||
ipcMain.on('play-song-change', (_, title) => {
|
||||
if (!title) return
|
||||
lyricWin?.webContents.send('play-song-change', title)
|
||||
})
|
||||
|
||||
// 音乐歌词更改
|
||||
ipcMain.on('play-lyric-change', (_, lyricData) => {
|
||||
if (!lyricData) return
|
||||
lyricWin?.webContents.send('play-lyric-change', lyricData)
|
||||
})
|
||||
|
||||
// 播放状态更改(播放/暂停)
|
||||
ipcMain.on('play-status-change', (_, status: boolean) => {
|
||||
lyricWin?.webContents.send('play-status-change', status)
|
||||
})
|
||||
|
||||
// 获取窗口位置
|
||||
ipcMain.handle('get-window-bounds', () => {
|
||||
return lyricWin?.getBounds()
|
||||
})
|
||||
// 同步获取窗口位置(回退)
|
||||
ipcMain.on('get-window-bounds-sync', (event) => {
|
||||
event.returnValue = lyricWin?.getBounds()
|
||||
})
|
||||
|
||||
// 获取屏幕尺寸
|
||||
ipcMain.handle('get-screen-size', () => {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
return { width, height }
|
||||
})
|
||||
// 同步获取屏幕尺寸(回退)
|
||||
ipcMain.on('get-screen-size-sync', (event) => {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
event.returnValue = { width, height }
|
||||
})
|
||||
|
||||
// 移动窗口
|
||||
ipcMain.on('move-window', (_, x, y, width, height) => {
|
||||
lyricWin?.setBounds({ x, y, width, height })
|
||||
// 保存配置
|
||||
lyricStore.set({ ...lyricStore.get(), x, y, width, height })
|
||||
// 保持置顶
|
||||
lyricWin?.setAlwaysOnTop(true, 'screen-saver')
|
||||
})
|
||||
|
||||
// 更新高度
|
||||
ipcMain.on('update-window-height', (_, height) => {
|
||||
if (!lyricWin) return
|
||||
const { width } = lyricWin.getBounds()
|
||||
|
||||
// 更新窗口高度
|
||||
lyricWin.setBounds({ width, height })
|
||||
})
|
||||
|
||||
// 获取配置
|
||||
ipcMain.handle('get-desktop-lyric-option', () => {
|
||||
return lyricStore.get()
|
||||
})
|
||||
// 同步获取配置(用于 invoke 不可用的回退)
|
||||
ipcMain.on('get-desktop-lyric-option-sync', (event) => {
|
||||
event.returnValue = lyricStore.get()
|
||||
})
|
||||
|
||||
// 保存配置
|
||||
ipcMain.on('set-desktop-lyric-option', (_, option, callback: boolean = false) => {
|
||||
lyricStore.set(option)
|
||||
// 触发窗口更新
|
||||
if (callback && lyricWin) {
|
||||
lyricWin.webContents.send('desktop-lyric-option-change', option)
|
||||
}
|
||||
mainWin?.webContents.send('desktop-lyric-option-change', option)
|
||||
})
|
||||
|
||||
// 发送主程序事件
|
||||
ipcMain.on('send-main-event', (_, name, val) => {
|
||||
mainWin?.webContents.send(name, val)
|
||||
})
|
||||
|
||||
// 关闭桌面歌词
|
||||
ipcMain.on('closeDesktopLyric', () => {
|
||||
lyricWin?.hide()
|
||||
mainWin?.webContents.send('closeDesktopLyric')
|
||||
})
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
let lyricLockState = false
|
||||
ipcMain.on('toogleDesktopLyricLock', (_, isLock: boolean) => {
|
||||
if (!lyricWin) return
|
||||
lyricLockState = !!isLock
|
||||
// 是否穿透
|
||||
if (lyricLockState) {
|
||||
lyricWin.setIgnoreMouseEvents(true, { forward: true })
|
||||
} else {
|
||||
lyricWin.setIgnoreMouseEvents(false)
|
||||
}
|
||||
// 广播到桌面歌词窗口与主窗口,保持两端状态一致
|
||||
lyricWin.webContents.send('toogleDesktopLyricLock', lyricLockState)
|
||||
mainWin?.webContents.send('toogleDesktopLyricLock', lyricLockState)
|
||||
})
|
||||
|
||||
// 查询当前桌面歌词锁定状态
|
||||
ipcMain.handle('get-lyric-lock-state', () => lyricLockState)
|
||||
|
||||
// 检查是否是子文件夹
|
||||
ipcMain.handle('check-if-subfolder', (_, localFilesPath: string[], selectedDir: string) => {
|
||||
const resolvedSelectedDir = resolve(selectedDir)
|
||||
const allPaths = localFilesPath.map((p) => resolve(p))
|
||||
return allPaths.some((existingPath) => {
|
||||
const relativePath = relative(existingPath, resolvedSelectedDir)
|
||||
return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default initLyricIpc
|
||||
@@ -97,7 +97,7 @@ function getDefaultMessage(type: string, data: any, pluginName: string): string
|
||||
*/
|
||||
export function sendPluginNotice(noticeData: PluginNoticeData, pluginName?: string): void {
|
||||
try {
|
||||
console.log(`[CeruMusic] 插件通知: ${noticeData.type}`, noticeData.data)
|
||||
// console.log(`[CeruMusic] 插件通知: ${noticeData.type}`, noticeData.data)
|
||||
|
||||
// 获取主窗口实例
|
||||
const mainWindow = BrowserWindow.getAllWindows().find((win) => !win.isDestroyed())
|
||||
|
||||
80
src/main/events/plugins.ts
Normal file
80
src/main/events/plugins.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import pluginService from '../services/plugin'
|
||||
function PluginEvent() {
|
||||
ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.selectAndAddPlugin(type)
|
||||
} catch (error: any) {
|
||||
console.error('Error selecting and adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-downloadAndAddPlugin', async (_, url, type): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.downloadAndAddPlugin(url, type)
|
||||
} catch (error: any) {
|
||||
console.error('Error downloading and adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.addPlugin(pluginCode, pluginName)
|
||||
} catch (error: any) {
|
||||
console.error('Error adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-getPluginById', async (_, id): Promise<any> => {
|
||||
try {
|
||||
return pluginService.getPluginById(id)
|
||||
} catch (error: any) {
|
||||
console.error('Error getting plugin by id:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-loadAllPlugins', async (): Promise<any> => {
|
||||
try {
|
||||
// 使用新的 getPluginsList 方法,但保持 API 兼容性
|
||||
return await pluginService.getPluginsList()
|
||||
} catch (error: any) {
|
||||
console.error('Error loading all plugins:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-getPluginLog', async (_, pluginId): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.getPluginLog(pluginId)
|
||||
} catch (error: any) {
|
||||
console.error('Error getting plugin log:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.uninstallPlugin(pluginId)
|
||||
} catch (error: any) {
|
||||
console.error('Error uninstalling plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default function InitPluginService() {
|
||||
setTimeout(async () => {
|
||||
// 初始化插件系统
|
||||
try {
|
||||
await pluginService.initializePlugins()
|
||||
PluginEvent()
|
||||
console.log('插件系统初始化完成')
|
||||
} catch (error) {
|
||||
console.error('插件系统初始化失败:', error)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import ManageSongList, { SongListError } from '../services/songList/ManageSongList'
|
||||
import type { SongList, Songs } from '@common/types/songList'
|
||||
import { configManager } from '../services/ConfigManager'
|
||||
|
||||
// 创建新歌单
|
||||
ipcMain.handle(
|
||||
@@ -21,6 +22,31 @@ ipcMain.handle(
|
||||
}
|
||||
)
|
||||
|
||||
// 喜欢歌单ID持久化
|
||||
ipcMain.handle('songlist:get-favorites-id', async () => {
|
||||
try {
|
||||
const id = configManager.get<string>('favoritesHashId', '')
|
||||
return { success: true, data: id || null }
|
||||
} catch (error) {
|
||||
console.error('获取喜欢歌单ID失败:', error)
|
||||
return { success: false, error: '获取喜欢歌单ID失败' }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('songlist:set-favorites-id', async (_, id: string) => {
|
||||
try {
|
||||
if (!id || typeof id !== 'string' || !id.trim()) {
|
||||
return { success: false, error: '无效的歌单ID' }
|
||||
}
|
||||
configManager.set('favoritesHashId', id.trim())
|
||||
const ok = configManager.saveConfig()
|
||||
return { success: ok, message: ok ? '已保存喜欢歌单ID' : '保存失败' }
|
||||
} catch (error) {
|
||||
console.error('设置喜欢歌单ID失败:', error)
|
||||
return { success: false, error: '设置喜欢歌单ID失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 获取所有歌单
|
||||
ipcMain.handle('songlist:get-all', async () => {
|
||||
try {
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { app, shell, BrowserWindow, ipcMain, Tray, Menu, screen } from 'electron'
|
||||
import {
|
||||
app,
|
||||
shell,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
screen,
|
||||
Rectangle,
|
||||
Display,
|
||||
Tray,
|
||||
Menu
|
||||
} from 'electron'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import icon from '../../resources/logo.png?asset'
|
||||
import path from 'node:path'
|
||||
import pluginService from './services/plugin'
|
||||
import aiEvents from './events/ai'
|
||||
import './services/musicSdk/index'
|
||||
import InitEventServices from './events'
|
||||
|
||||
import lyricWindow from './windows/lyric-window'
|
||||
|
||||
import './events/musicCache'
|
||||
import './events/songList'
|
||||
import './events/directorySettings'
|
||||
import './events/pluginNotice'
|
||||
import initLyricIpc from './events/lyric'
|
||||
|
||||
// 获取单实例锁
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
@@ -25,27 +42,34 @@ if (!gotTheLock) {
|
||||
})
|
||||
}
|
||||
|
||||
// import wy from './utils/musicSdk/wy/index'
|
||||
// import kg from './utils/musicSdk/kg/index'
|
||||
// wy.hotSearch.getList().then((res) => {
|
||||
// console.log(res)
|
||||
// })
|
||||
// kg.hotSearch.getList().then((res) => {
|
||||
// console.log(res)
|
||||
// })
|
||||
let tray: Tray | null = null
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let isQuitting = false
|
||||
let tray: Tray | null = null
|
||||
let trayLyricLocked = false
|
||||
|
||||
function createTray(): void {
|
||||
// 创建系统托盘
|
||||
const trayIconPath = path.join(__dirname, '../../resources/logo.png')
|
||||
tray = new Tray(trayIconPath)
|
||||
function updateTrayMenu() {
|
||||
const lyricWin = lyricWindow.getWin()
|
||||
const isVisible = !!lyricWin && lyricWin.isVisible()
|
||||
const toggleLyricLabel = isVisible ? '隐藏桌面歌词' : '显示桌面歌词'
|
||||
const toggleLockLabel = trayLyricLocked ? '解锁桌面歌词' : '锁定桌面歌词'
|
||||
|
||||
// 创建托盘菜单
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示窗口',
|
||||
label: toggleLyricLabel,
|
||||
click: () => {
|
||||
const target = !isVisible
|
||||
ipcMain.emit('change-desktop-lyric', null, target)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: toggleLockLabel,
|
||||
click: () => {
|
||||
const next = !trayLyricLocked
|
||||
ipcMain.emit('toogleDesktopLyricLock', null, next)
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '显示主窗口',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show()
|
||||
@@ -56,8 +80,6 @@ function createTray(): void {
|
||||
{
|
||||
label: '播放/暂停',
|
||||
click: () => {
|
||||
// 这里可以添加播放控制逻辑
|
||||
console.log('music-control')
|
||||
mainWindow?.webContents.send('music-control')
|
||||
}
|
||||
},
|
||||
@@ -65,46 +87,102 @@ function createTray(): void {
|
||||
{
|
||||
label: '退出',
|
||||
click: () => {
|
||||
isQuitting = true
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
tray?.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
function setupTray() {
|
||||
// 全局单例防重复(热重载/多次执行保护)
|
||||
const g: any = global as any
|
||||
if (g.__ceru_tray__) {
|
||||
try {
|
||||
g.__ceru_tray__.destroy()
|
||||
} catch {}
|
||||
g.__ceru_tray__ = null
|
||||
}
|
||||
if (tray) {
|
||||
try {
|
||||
tray.destroy()
|
||||
} catch {}
|
||||
tray = null
|
||||
}
|
||||
|
||||
const iconPath = path.join(__dirname, '../../resources/logo.ico')
|
||||
tray = new Tray(iconPath)
|
||||
tray.setToolTip('Ceru Music')
|
||||
updateTrayMenu()
|
||||
|
||||
// 双击托盘图标显示窗口
|
||||
// 左键单击切换主窗口显示
|
||||
tray.on('click', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
if (!mainWindow) return
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// 防重复注册 IPC 监听(仅注册一次)
|
||||
if (!g.__ceru_tray_ipc_bound__) {
|
||||
ipcMain.on('toogleDesktopLyricLock', (_e, isLock: boolean) => {
|
||||
trayLyricLocked = !!isLock
|
||||
updateTrayMenu()
|
||||
})
|
||||
ipcMain.on('change-desktop-lyric', () => {
|
||||
updateTrayMenu()
|
||||
})
|
||||
g.__ceru_tray_ipc_bound__ = true
|
||||
}
|
||||
|
||||
// 记录全局托盘句柄
|
||||
g.__ceru_tray__ = tray
|
||||
|
||||
app.once('before-quit', () => {
|
||||
try {
|
||||
tray?.destroy()
|
||||
} catch {}
|
||||
tray = null
|
||||
g.__ceru_tray__ = null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据窗口当前所在的显示器,动态更新窗口的最大尺寸限制。
|
||||
* 这样可以确保窗口在任何显示器上都能正常最大化或全屏。
|
||||
* @param {BrowserWindow} win - 要更新的窗口实例
|
||||
*/
|
||||
function updateWindowMaxLimits(win: BrowserWindow | null): void {
|
||||
if (!win) return
|
||||
|
||||
// 1. 获取窗口的当前边界 (bounds)
|
||||
const currentBounds: Rectangle = win.getBounds()
|
||||
|
||||
// 2. 查找包含该边界的显示器
|
||||
const currentDisplay: Display = screen.getDisplayMatching(currentBounds)
|
||||
|
||||
// 3. 获取该显示器的完整尺寸 (full screen size)
|
||||
const { width: currentScreenWidth, height: currentScreenHeight } = currentDisplay.size
|
||||
|
||||
// 4. 应用新的最大尺寸限制
|
||||
// 移除 maxWidth/maxHeight 上的硬限制,使其能够最大化到当前屏幕的尺寸。
|
||||
// 注意:设置为 0, 0 意味着没有最小限制,我们只关注最大限制。
|
||||
win.setMaximumSize(currentScreenWidth, currentScreenHeight)
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
// return
|
||||
// 获取保存的窗口位置和大小
|
||||
const savedBounds = configManager.getWindowBounds()
|
||||
|
||||
// 获取屏幕尺寸
|
||||
const primaryDisplay = screen.getPrimaryDisplay()
|
||||
// 使用完整屏幕尺寸而不是工作区域,以支持真正的全屏模式
|
||||
const { width: screenWidth, height: screenHeight } = primaryDisplay.size
|
||||
|
||||
// 默认窗口配置
|
||||
const defaultOptions = {
|
||||
width: 1100,
|
||||
height: 750,
|
||||
minWidth: 1100,
|
||||
minHeight: 670,
|
||||
maxWidth: screenWidth,
|
||||
maxHeight: screenHeight,
|
||||
show: false,
|
||||
center: !savedBounds, // 如果有保存的位置,则不居中
|
||||
autoHideMenuBar: true,
|
||||
@@ -130,24 +208,30 @@ function createWindow(): void {
|
||||
mainWindow = new BrowserWindow(defaultOptions)
|
||||
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
||||
|
||||
// 监听窗口移动和调整大小事件,保存窗口位置和大小
|
||||
// ⚠️ 关键修改 2: 监听 'moved' 事件,动态更新最大尺寸
|
||||
mainWindow.on('moved', () => {
|
||||
// 当窗口移动时,确保最大尺寸限制随屏幕变化
|
||||
updateWindowMaxLimits(mainWindow)
|
||||
|
||||
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||
const bounds = mainWindow.getBounds()
|
||||
configManager.saveWindowBounds(bounds)
|
||||
}
|
||||
})
|
||||
|
||||
// ⚠️ 关键修改 3: 窗口创建后立即应用一次最大尺寸限制
|
||||
updateWindowMaxLimits(mainWindow)
|
||||
|
||||
mainWindow.on('resized', () => {
|
||||
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||
const bounds = mainWindow.getBounds()
|
||||
|
||||
// 获取当前屏幕尺寸
|
||||
const { screen } = require('electron')
|
||||
// 获取当前屏幕尺寸 (已在文件顶部导入 screen,无需 require)
|
||||
const currentDisplay = screen.getDisplayMatching(bounds)
|
||||
// 使用 workAreaSize 避免窗口超出任务栏/Dock
|
||||
const { width: screenWidth, height: screenHeight } = currentDisplay.workAreaSize
|
||||
|
||||
// 确保窗口不超过屏幕尺寸
|
||||
// 确保窗口不超过屏幕工作区域尺寸
|
||||
let needResize = false
|
||||
const newBounds = { ...bounds }
|
||||
|
||||
@@ -173,28 +257,11 @@ function createWindow(): void {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow?.show()
|
||||
})
|
||||
|
||||
// 阻止窗口关闭,改为隐藏到系统托盘
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow?.hide()
|
||||
|
||||
// 显示托盘通知
|
||||
if (tray) {
|
||||
tray.displayBalloon({
|
||||
iconType: 'info',
|
||||
title: 'Ceru Music',
|
||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url).then()
|
||||
return { action: 'deny' }
|
||||
})
|
||||
InitEventServices(mainWindow)
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
@@ -205,80 +272,6 @@ function createWindow(): void {
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.selectAndAddPlugin(type)
|
||||
} catch (error: any) {
|
||||
console.error('Error selecting and adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-downloadAndAddPlugin', async (_, url, type): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.downloadAndAddPlugin(url, type)
|
||||
} catch (error: any) {
|
||||
console.error('Error downloading and adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.addPlugin(pluginCode, pluginName)
|
||||
} catch (error: any) {
|
||||
console.error('Error adding plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-getPluginById', async (_, id): Promise<any> => {
|
||||
try {
|
||||
return pluginService.getPluginById(id)
|
||||
} catch (error: any) {
|
||||
console.error('Error getting plugin by id:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-loadAllPlugins', async (): Promise<any> => {
|
||||
try {
|
||||
// 使用新的 getPluginsList 方法,但保持 API 兼容性
|
||||
return await pluginService.getPluginsList()
|
||||
} catch (error: any) {
|
||||
console.error('Error loading all plugins:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-getPluginLog', async (_, pluginId): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.getPluginLog(pluginId)
|
||||
} catch (error: any) {
|
||||
console.error('Error getting plugin log:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise<any> => {
|
||||
try {
|
||||
return await pluginService.uninstallPlugin(pluginId)
|
||||
} catch (error: any) {
|
||||
console.error('Error uninstalling plugin:', error)
|
||||
return { error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
// 获取应用版本号
|
||||
ipcMain.handle('get-app-version', () => {
|
||||
return app.getVersion()
|
||||
})
|
||||
|
||||
aiEvents(mainWindow)
|
||||
import './events/musicCache'
|
||||
import './events/songList'
|
||||
import './events/directorySettings'
|
||||
import './events/pluginNotice'
|
||||
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -295,16 +288,6 @@ app.whenReady().then(() => {
|
||||
app.setName('澜音')
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
// 初始化插件系统
|
||||
try {
|
||||
await pluginService.initializePlugins()
|
||||
console.log('插件系统初始化完成')
|
||||
} catch (error) {
|
||||
console.error('插件系统初始化失败:', error)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
@@ -312,63 +295,11 @@ app.whenReady().then(() => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
// 窗口控制 IPC 处理
|
||||
ipcMain.on('window-minimize', () => {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
if (window) {
|
||||
window.minimize()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('window-maximize', () => {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
if (window) {
|
||||
if (window.isMaximized()) {
|
||||
window.unmaximize()
|
||||
} else {
|
||||
window.maximize()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
if (window) {
|
||||
window.close()
|
||||
}
|
||||
})
|
||||
|
||||
// Mini 模式 IPC 处理 - 最小化到系统托盘
|
||||
ipcMain.on('window-mini-mode', (_, isMini) => {
|
||||
if (mainWindow) {
|
||||
if (isMini) {
|
||||
// 进入 Mini 模式:隐藏窗口到系统托盘
|
||||
mainWindow.hide()
|
||||
// 显示托盘通知(可选)
|
||||
if (tray) {
|
||||
tray.displayBalloon({
|
||||
title: '澜音 Music',
|
||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 退出 Mini 模式:显示窗口
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 全屏模式 IPC 处理
|
||||
ipcMain.on('window-toggle-fullscreen', () => {
|
||||
if (mainWindow) {
|
||||
const isFullScreen = mainWindow.isFullScreen()
|
||||
mainWindow.setFullScreen(!isFullScreen)
|
||||
}
|
||||
})
|
||||
|
||||
createWindow()
|
||||
createTray()
|
||||
lyricWindow.create()
|
||||
initLyricIpc(mainWindow)
|
||||
// 仅在主进程初始化一次托盘
|
||||
setupTray()
|
||||
|
||||
// 注册自动更新事件
|
||||
registerAutoUpdateEvents()
|
||||
@@ -394,13 +325,7 @@ app.whenReady().then(() => {
|
||||
|
||||
// 当所有窗口关闭时不退出应用,因为我们有系统托盘
|
||||
app.on('window-all-closed', () => {
|
||||
// 在 macOS 上,应用通常会保持活跃状态
|
||||
// 在其他平台上,我们也保持应用运行,因为有系统托盘
|
||||
})
|
||||
|
||||
// 应用退出前的清理
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true
|
||||
// 保持应用常驻,通过系统托盘管理
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
@@ -408,57 +333,14 @@ app.on('before-quit', () => {
|
||||
|
||||
let ping: NodeJS.Timeout
|
||||
function startPing() {
|
||||
let interval = 3000
|
||||
|
||||
// 已迁移到 Howler,不再使用 DOM <audio> 轮询。
|
||||
// 如需主进程感知播放结束,请在渲染进程通过 IPC 主动通知。
|
||||
if (ping) {
|
||||
clearInterval(ping)
|
||||
}
|
||||
ping = setInterval(() => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents
|
||||
.executeJavaScript(
|
||||
`
|
||||
(function() {
|
||||
const audio = document.getElementById("globaAudio");
|
||||
if(!audio) return { playing:false, ended: false };
|
||||
|
||||
if(audio.ended) return { playing:false, ended: true };
|
||||
|
||||
return { playing: !audio.paused, ended: false, currentTime: audio.currentTime, duration: audio.duration };
|
||||
})()
|
||||
`
|
||||
)
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
if (res.duration - res.currentTime <= 20) {
|
||||
clearInterval(ping)
|
||||
interval = 500
|
||||
ping = setInterval(() => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents
|
||||
.executeJavaScript(
|
||||
`
|
||||
(function() {
|
||||
const audio = document.getElementById("globaAudio");
|
||||
if(!audio) return { playing:false, ended: false };
|
||||
|
||||
if(audio.ended) return { playing:false, ended: true };
|
||||
|
||||
return { playing: !audio.paused, ended: false, currentTime: audio.currentTime, duration: audio.duration };
|
||||
})()
|
||||
`
|
||||
)
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
if (res && res.ended) {
|
||||
mainWindow?.webContents.send('song-ended')
|
||||
console.log('next song')
|
||||
clearInterval(ping)
|
||||
}
|
||||
})
|
||||
.catch((err) => console.warn(err))
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
})
|
||||
.catch((err) => console.warn(err))
|
||||
}
|
||||
}, interval)
|
||||
// 保留占位,避免调用方报错;不再做任何轮询。
|
||||
// 可在此处监听自定义 IPC 事件以扩展行为。
|
||||
clearInterval(ping)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
47
src/main/logger/index.ts
Normal file
47
src/main/logger/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 日志输出
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
// 日志文件路径
|
||||
const logDir = join(app.getPath('logs'))
|
||||
// 是否存在日志目录
|
||||
if (!existsSync(logDir)) mkdirSync(logDir)
|
||||
|
||||
// 获取日期 - YYYY-MM-DD
|
||||
const dateString = new Date().toISOString().slice(0, 10)
|
||||
const logFilePath = join(logDir, `${dateString}.log`)
|
||||
console.log(logFilePath, '546444444444444444444444444444444444')
|
||||
|
||||
// 配置日志系统
|
||||
log.transports.console.useStyles = true // 颜色输出
|
||||
log.transports.file.level = 'info' // 仅记录 info 及以上级别
|
||||
log.transports.file.resolvePathFn = (): string => logFilePath // 日志文件路径
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024 // 文件最大 2MB
|
||||
|
||||
// 日志格式化
|
||||
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
|
||||
|
||||
// 绑定默认事件
|
||||
const defaultLog = log.scope('default')
|
||||
console.log = defaultLog.log
|
||||
console.info = defaultLog.info
|
||||
console.warn = defaultLog.warn
|
||||
console.error = defaultLog.error
|
||||
|
||||
// 分作用域导出
|
||||
export { defaultLog }
|
||||
export const ipcLog = log.scope('ipc')
|
||||
export const trayLog = log.scope('tray')
|
||||
export const thumbarLog = log.scope('thumbar')
|
||||
export const storeLog = log.scope('store')
|
||||
export const updateLog = log.scope('update')
|
||||
export const systemLog = log.scope('system')
|
||||
export const configLog = log.scope('config')
|
||||
export const windowsLog = log.scope('windows')
|
||||
export const processLog = log.scope('process')
|
||||
export const preloadLog = log.scope('preload')
|
||||
export const rendererLog = log.scope('renderer')
|
||||
export const shortcutLog = log.scope('shortcut')
|
||||
export const serverLog = log.scope('server')
|
||||
@@ -60,6 +60,7 @@ export class ConfigManager {
|
||||
// 设置配置项
|
||||
public set<T>(key: string, value: T): void {
|
||||
this.config[key] = value
|
||||
this.saveConfig()
|
||||
}
|
||||
|
||||
// 删除配置项
|
||||
|
||||
@@ -493,7 +493,7 @@ class CeruMusicPluginHost {
|
||||
}, timeout)
|
||||
|
||||
try {
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 发起请求: ${options.method || 'GET'} ${url}`)
|
||||
// console.log(`${CONSTANTS.LOG_PREFIX} 发起请求: ${options.method || 'GET'} ${url}`)
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'GET',
|
||||
@@ -504,7 +504,7 @@ class CeruMusicPluginHost {
|
||||
const response = await fetch(url, fetchOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求响应: ${response.status} ${response.statusText}`)
|
||||
// console.log(`${CONSTANTS.LOG_PREFIX} 请求响应: ${response.status} ${response.statusText}`)
|
||||
|
||||
const body = await this._parseResponseBody(response)
|
||||
const headers = this._extractHeaders(response)
|
||||
@@ -515,11 +515,11 @@ class CeruMusicPluginHost {
|
||||
headers
|
||||
}
|
||||
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求完成:`, {
|
||||
url,
|
||||
status: response.status,
|
||||
bodyType: typeof body
|
||||
})
|
||||
// console.log(`${CONSTANTS.LOG_PREFIX} 请求完成:`, {
|
||||
// url,
|
||||
// status: response.status,
|
||||
// bodyType: typeof body
|
||||
// })
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import NodeID3 from 'node-id3'
|
||||
import ffmpegStatic from 'ffmpeg-static'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
import { File, Picture, Id3v2Settings } from 'node-taglib-sharp'
|
||||
import path from 'node:path'
|
||||
import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
@@ -75,6 +73,32 @@ function formatTimestamp(timeMs: number): string {
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
|
||||
}
|
||||
|
||||
// 根据图片 URL 和 Content-Type 解析扩展名,默认返回 .jpg
|
||||
function resolveCoverExt(imgUrl: string, contentType?: string): string {
|
||||
const validExts = new Set(['.jpg', '.jpeg', '.png', '.webp', '.bmp'])
|
||||
let urlExt: string | undefined
|
||||
try {
|
||||
const pathname = new URL(imgUrl).pathname
|
||||
const i = pathname.lastIndexOf('.')
|
||||
if (i !== -1) {
|
||||
urlExt = pathname.substring(i).toLowerCase()
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (urlExt && validExts.has(urlExt)) {
|
||||
return urlExt === '.jpeg' ? '.jpg' : urlExt
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
if (contentType.includes('image/png')) return '.png'
|
||||
if (contentType.includes('image/webp')) return '.webp'
|
||||
if (contentType.includes('image/jpeg') || contentType.includes('image/jpg')) return '.jpg'
|
||||
if (contentType.includes('image/bmp')) return '.bmp'
|
||||
}
|
||||
|
||||
return '.jpg'
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换新格式:[开始时间,持续时间](开始时间,字符持续时间,0)字符
|
||||
*/
|
||||
@@ -156,265 +180,6 @@ function convertOldFormat(timestamp: string, content: string): string {
|
||||
return `[${timestamp}]${convertedContent}`
|
||||
}
|
||||
|
||||
// 写入音频标签的辅助函数
|
||||
async function writeAudioTags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||
try {
|
||||
console.log('开始写入音频标签:', filePath, tagWriteOptions, songInfo)
|
||||
|
||||
// 获取文件扩展名来判断格式
|
||||
const fileExtension = path.extname(filePath).toLowerCase().substring(1)
|
||||
console.log('文件格式:', fileExtension)
|
||||
|
||||
// 根据文件格式选择不同的标签写入方法
|
||||
if (fileExtension === 'mp3') {
|
||||
await writeMP3Tags(filePath, songInfo, tagWriteOptions)
|
||||
} else if (['flac', 'ogg', 'opus'].includes(fileExtension)) {
|
||||
await writeVorbisCommentTags(filePath, songInfo, tagWriteOptions)
|
||||
} else if (['m4a', 'mp4', 'aac'].includes(fileExtension)) {
|
||||
await writeMP4Tags(filePath, songInfo, tagWriteOptions)
|
||||
} else {
|
||||
console.warn('不支持的音频格式:', fileExtension)
|
||||
// 尝试使用 NodeID3 作为后备方案
|
||||
await writeMP3Tags(filePath, songInfo, tagWriteOptions)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('写入音频标签时发生错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MP3 格式标签写入
|
||||
async function writeMP3Tags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||
const tags: any = {}
|
||||
|
||||
// 写入基础信息
|
||||
if (tagWriteOptions.basicInfo) {
|
||||
tags.title = songInfo.name || ''
|
||||
tags.artist = songInfo.singer || ''
|
||||
tags.album = songInfo.albumName || ''
|
||||
tags.year = songInfo.year || ''
|
||||
tags.genre = songInfo.genre || ''
|
||||
}
|
||||
|
||||
// 写入歌词
|
||||
if (tagWriteOptions.lyrics && songInfo.lrc) {
|
||||
const convertedLrc = convertLrcFormat(songInfo.lrc)
|
||||
tags.unsynchronisedLyrics = {
|
||||
language: 'chi',
|
||||
shortText: 'Lyrics',
|
||||
text: convertedLrc
|
||||
}
|
||||
}
|
||||
|
||||
// 写入封面
|
||||
if (tagWriteOptions.cover && songInfo.img) {
|
||||
try {
|
||||
const coverResponse = await axios({
|
||||
method: 'GET',
|
||||
url: songInfo.img,
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (coverResponse.data) {
|
||||
tags.image = {
|
||||
mime: 'image/jpeg',
|
||||
type: {
|
||||
id: 3,
|
||||
name: 'front cover'
|
||||
},
|
||||
description: 'Cover',
|
||||
imageBuffer: Buffer.from(coverResponse.data)
|
||||
}
|
||||
}
|
||||
} catch (coverError) {
|
||||
console.warn('获取封面失败:', coverError)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入标签到文件
|
||||
if (Object.keys(tags).length > 0) {
|
||||
const success = NodeID3.write(tags, filePath)
|
||||
if (success) {
|
||||
console.log('MP3音频标签写入成功:', filePath)
|
||||
} else {
|
||||
console.warn('MP3音频标签写入失败:', filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FLAC/OGG 格式标签写入 (使用 Vorbis Comment)
|
||||
async function writeVorbisCommentTags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||
try {
|
||||
console.log('开始写入 FLAC 标签:', filePath)
|
||||
|
||||
// 准备新的标签数据
|
||||
const newTags: any = {}
|
||||
|
||||
// 写入基础信息
|
||||
if (tagWriteOptions.basicInfo) {
|
||||
if (songInfo.name) newTags.TITLE = songInfo.name
|
||||
if (songInfo.singer) newTags.ARTIST = songInfo.singer
|
||||
if (songInfo.albumName) newTags.ALBUM = songInfo.albumName
|
||||
if (songInfo.year) newTags.DATE = songInfo.year.toString()
|
||||
if (songInfo.genre) newTags.GENRE = songInfo.genre
|
||||
}
|
||||
|
||||
// 写入歌词
|
||||
if (tagWriteOptions.lyrics && songInfo.lrc) {
|
||||
const convertedLrc = convertLrcFormat(songInfo.lrc)
|
||||
newTags.LYRICS = convertedLrc
|
||||
}
|
||||
|
||||
console.log('准备写入的标签:', newTags)
|
||||
|
||||
// 使用 ffmpeg-static 写入 FLAC 标签
|
||||
if (path.extname(filePath).toLowerCase() === '.flac') {
|
||||
await writeFLACTagsWithFFmpeg(filePath, newTags, songInfo, tagWriteOptions)
|
||||
} else {
|
||||
console.warn('暂不支持该格式的标签写入:', path.extname(filePath))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('写入 Vorbis Comment 标签失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 fluent-ffmpeg 写入 FLAC 标签
|
||||
async function writeFLACTagsWithFFmpeg(
|
||||
filePath: string,
|
||||
tags: any,
|
||||
songInfo: any,
|
||||
tagWriteOptions: any
|
||||
) {
|
||||
let tempOutputPath: string | null = null
|
||||
let tempCoverPath: string | null = null
|
||||
|
||||
try {
|
||||
if (!ffmpegStatic) {
|
||||
throw new Error('ffmpeg-static 不可用')
|
||||
}
|
||||
ffmpeg.setFfmpegPath(ffmpegStatic.replace('app.asar', 'app.asar.unpacked'))
|
||||
|
||||
// 创建临时输出文件
|
||||
tempOutputPath = filePath + '.temp.flac'
|
||||
|
||||
// 创建 fluent-ffmpeg 实例
|
||||
let command = ffmpeg(filePath)
|
||||
.audioCodec('copy') // 复制音频编解码器,不重新编码
|
||||
.output(tempOutputPath)
|
||||
|
||||
// 添加元数据标签
|
||||
for (const [key, value] of Object.entries(tags)) {
|
||||
if (value) {
|
||||
// fluent-ffmpeg 会自动处理特殊字符转义
|
||||
command = command.outputOptions(['-metadata', `${key}=${value}`])
|
||||
}
|
||||
}
|
||||
|
||||
// 处理封面
|
||||
if (tagWriteOptions.cover && songInfo.img) {
|
||||
try {
|
||||
console.log('开始下载封面:', songInfo.img)
|
||||
const coverResponse = await axios({
|
||||
method: 'GET',
|
||||
url: songInfo.img,
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (coverResponse.data) {
|
||||
// 保存临时封面文件
|
||||
tempCoverPath = path.join(path.dirname(filePath), 'temp_cover.jpg')
|
||||
await fsPromise.writeFile(tempCoverPath, Buffer.from(coverResponse.data))
|
||||
|
||||
// 添加封面作为输入
|
||||
command = command.input(tempCoverPath).outputOptions([
|
||||
'-map',
|
||||
'0:a', // 映射原始文件的音频流
|
||||
'-map',
|
||||
'1:v', // 映射封面的视频流
|
||||
'-c:v',
|
||||
'copy', // 复制视频编解码器
|
||||
'-disposition:v:0',
|
||||
'attached_pic' // 设置为附加图片
|
||||
])
|
||||
|
||||
console.log('封面已添加到命令中')
|
||||
}
|
||||
} catch (coverError) {
|
||||
console.warn(
|
||||
'下载封面失败,跳过封面写入:',
|
||||
coverError instanceof Error ? coverError.message : coverError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行 ffmpeg 命令
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
command
|
||||
.on('start', () => {
|
||||
console.log('执行 ffmpeg 命令')
|
||||
})
|
||||
.on('progress', (progress) => {
|
||||
if (progress.percent) {
|
||||
console.log('处理进度:', Math.round(progress.percent) + '%')
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
console.log('ffmpeg 处理完成')
|
||||
resolve()
|
||||
})
|
||||
.on('error', (err, _, stderr) => {
|
||||
console.error('ffmpeg 错误:', err.message)
|
||||
if (stderr) {
|
||||
console.error('ffmpeg stderr:', stderr)
|
||||
}
|
||||
reject(new Error(`ffmpeg 处理失败: ${err.message}`))
|
||||
})
|
||||
.run()
|
||||
})
|
||||
|
||||
// 检查临时文件是否创建成功
|
||||
if (!fs.existsSync(tempOutputPath)) {
|
||||
throw new Error('ffmpeg 未能创建输出文件')
|
||||
}
|
||||
|
||||
// 替换原文件
|
||||
await fsPromise.rename(tempOutputPath, filePath)
|
||||
tempOutputPath = null // 标记已处理,避免重复清理
|
||||
|
||||
console.log('使用 fluent-ffmpeg 写入 FLAC 标签成功:', filePath)
|
||||
} catch (error) {
|
||||
console.error('使用 fluent-ffmpeg 写入 FLAC 标签失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
// 清理所有临时文件
|
||||
const filesToClean = [tempOutputPath, tempCoverPath].filter(Boolean) as string[]
|
||||
|
||||
for (const tempFile of filesToClean) {
|
||||
try {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
await fsPromise.unlink(tempFile)
|
||||
console.log('已清理临时文件:', tempFile)
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
'清理临时文件失败:',
|
||||
tempFile,
|
||||
cleanupError instanceof Error ? cleanupError.message : cleanupError
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MP4/M4A 格式标签写入
|
||||
async function writeMP4Tags(filePath: string, _songInfo: any, _tagWriteOptions: any) {
|
||||
console.log('MP4/M4A 格式标签写入暂未实现:', filePath)
|
||||
// 可以使用 ffmpeg 或其他工具实现
|
||||
}
|
||||
|
||||
// 获取自定义下载目录
|
||||
const getDownloadDirectory = (): string => {
|
||||
// 使用配置管理服务获取下载目录
|
||||
@@ -492,13 +257,71 @@ export default async function download(
|
||||
delete fileLock[songPath]
|
||||
}
|
||||
|
||||
// 写入标签信息
|
||||
// 写入标签信息(使用 node-taglib-sharp)
|
||||
if (tagWriteOptions && fs.existsSync(songPath)) {
|
||||
try {
|
||||
await writeAudioTags(songPath, songInfo, tagWriteOptions)
|
||||
const baseName = path.basename(songPath, path.extname(songPath))
|
||||
const dirName = path.dirname(songPath)
|
||||
let coverExt = '.jpg'
|
||||
let coverPath = path.join(dirName, `${baseName}${coverExt}`)
|
||||
let coverDownloaded = false
|
||||
|
||||
// 下载封面(仅当启用且有URL)
|
||||
if (tagWriteOptions.cover && songInfo?.img) {
|
||||
try {
|
||||
const coverRes = await axios.get(songInfo.img, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
const ct =
|
||||
(coverRes.headers && (coverRes.headers['content-type'] as string | undefined)) ||
|
||||
undefined
|
||||
coverExt = resolveCoverExt(songInfo.img, ct)
|
||||
coverPath = path.join(dirName, `${baseName}${coverExt}`)
|
||||
await fsPromise.writeFile(coverPath, Buffer.from(coverRes.data))
|
||||
coverDownloaded = true
|
||||
} catch (e) {
|
||||
console.warn('下载封面失败:', e instanceof Error ? e.message : e)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取歌曲文件并设置标签
|
||||
const songFile = File.createFromPath(songPath)
|
||||
|
||||
// 使用默认 ID3v2.3
|
||||
Id3v2Settings.forceDefaultVersion = true
|
||||
Id3v2Settings.defaultVersion = 3
|
||||
|
||||
songFile.tag.title = songInfo?.name || '未知曲目'
|
||||
songFile.tag.album = songInfo?.albumName || '未知专辑'
|
||||
const artists = songInfo?.singer ? [songInfo.singer] : ['未知艺术家']
|
||||
songFile.tag.performers = artists
|
||||
songFile.tag.albumArtists = artists
|
||||
// 写入歌词(转换为标准 LRC)
|
||||
if (tagWriteOptions.lyrics && songInfo?.lrc) {
|
||||
const convertedLrc = convertLrcFormat(songInfo.lrc)
|
||||
songFile.tag.lyrics = convertedLrc
|
||||
}
|
||||
|
||||
// 写入封面
|
||||
if (tagWriteOptions.cover && coverDownloaded) {
|
||||
const songCover = Picture.fromPath(coverPath)
|
||||
songFile.tag.pictures = [songCover]
|
||||
}
|
||||
|
||||
// 保存并释放
|
||||
songFile.save()
|
||||
songFile.dispose()
|
||||
|
||||
// 删除临时封面
|
||||
if (coverDownloaded) {
|
||||
try {
|
||||
await fsPromise.unlink(coverPath)
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('写入音频标签失败:', error)
|
||||
throw ffmpegStatic
|
||||
console.warn('写入音乐元信息失败:', error)
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeName, formatPlayTime, sizeFormate } from '../index'
|
||||
import { decodeName, formatPlayTime } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||
|
||||
export default {
|
||||
limit: 30,
|
||||
@@ -9,87 +10,72 @@ export default {
|
||||
allPage: 1,
|
||||
musicSearch(str, page, limit) {
|
||||
const searchRequest = httpFetch(
|
||||
`https://songsearch.kugou.com/song_search_v2?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1`
|
||||
`https://songsearch.kugou.com/song_search_v2?keyword=${encodeURIComponent(
|
||||
str
|
||||
)}&page=${page}&pagesize=${limit}&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1`
|
||||
)
|
||||
return searchRequest.promise.then(({ body }) => body)
|
||||
},
|
||||
filterData(rawData) {
|
||||
const types = []
|
||||
const _types = {}
|
||||
if (rawData.FileSize !== 0) {
|
||||
const size = sizeFormate(rawData.FileSize)
|
||||
types.push({ type: '128k', size, hash: rawData.FileHash })
|
||||
_types['128k'] = {
|
||||
size,
|
||||
hash: rawData.FileHash
|
||||
}
|
||||
}
|
||||
if (rawData.HQFileSize !== 0) {
|
||||
const size = sizeFormate(rawData.HQFileSize)
|
||||
types.push({ type: '320k', size, hash: rawData.HQFileHash })
|
||||
_types['320k'] = {
|
||||
size,
|
||||
hash: rawData.HQFileHash
|
||||
}
|
||||
}
|
||||
if (rawData.SQFileSize !== 0) {
|
||||
const size = sizeFormate(rawData.SQFileSize)
|
||||
types.push({ type: 'flac', size, hash: rawData.SQFileHash })
|
||||
_types.flac = {
|
||||
size,
|
||||
hash: rawData.SQFileHash
|
||||
}
|
||||
}
|
||||
if (rawData.ResFileSize !== 0) {
|
||||
const size = sizeFormate(rawData.ResFileSize)
|
||||
types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
hash: rawData.ResFileHash
|
||||
}
|
||||
}
|
||||
return {
|
||||
singer: decodeName(formatSingerName(rawData.Singers, 'name')),
|
||||
name: decodeName(rawData.SongName),
|
||||
albumName: decodeName(rawData.AlbumName),
|
||||
albumId: rawData.AlbumID,
|
||||
songmid: rawData.Audioid,
|
||||
source: 'kg',
|
||||
interval: formatPlayTime(rawData.Duration),
|
||||
_interval: rawData.Duration,
|
||||
img: null,
|
||||
lrc: null,
|
||||
otherSource: null,
|
||||
hash: rawData.FileHash,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
}
|
||||
},
|
||||
handleResult(rawData) {
|
||||
const ids = new Set()
|
||||
const list = []
|
||||
async handleResult(rawData) {
|
||||
let ids = new Set()
|
||||
const items = []
|
||||
|
||||
rawData.forEach((item) => {
|
||||
const key = item.Audioid + item.FileHash
|
||||
if (ids.has(key)) return
|
||||
ids.add(key)
|
||||
list.push(this.filterData(item))
|
||||
for (const childItem of item.Grp) {
|
||||
const key = item.Audioid + item.FileHash
|
||||
if (ids.has(key)) continue
|
||||
if (!ids.has(key)) {
|
||||
ids.add(key)
|
||||
list.push(this.filterData(childItem))
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
for (const childItem of item.Grp || []) {
|
||||
const childKey = childItem.Audioid + childItem.FileHash
|
||||
if (!ids.has(childKey)) {
|
||||
ids.add(childKey)
|
||||
items.push(childItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const hashList = items.map((item) => item.FileHash)
|
||||
|
||||
let qualityInfoMap = {}
|
||||
try {
|
||||
const qualityInfoRequest = getBatchMusicQualityInfo(hashList)
|
||||
qualityInfoMap = await qualityInfoRequest.promise
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch quality info:', error)
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
const { types = [], _types = {} } = qualityInfoMap[item.FileHash] || {}
|
||||
|
||||
return {
|
||||
singer: decodeName(formatSingerName(item.Singers, 'name')),
|
||||
name: decodeName(item.SongName),
|
||||
albumName: decodeName(item.AlbumName),
|
||||
albumId: item.AlbumID,
|
||||
songmid: item.Audioid,
|
||||
source: 'kg',
|
||||
interval: formatPlayTime(item.Duration),
|
||||
_interval: item.Duration,
|
||||
img: null,
|
||||
lrc: null,
|
||||
otherSource: null,
|
||||
hash: item.FileHash,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {},
|
||||
}
|
||||
})
|
||||
return list
|
||||
},
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then((result) => {
|
||||
|
||||
return this.musicSearch(str, page, limit).then(async (result) => {
|
||||
if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum)
|
||||
const list = this.handleResult(result.data.lists)
|
||||
|
||||
let list = await this.handleResult(result.data.lists)
|
||||
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
@@ -102,8 +88,8 @@ export default {
|
||||
allPage: this.allPage,
|
||||
limit,
|
||||
total: this.total,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
190
src/main/utils/musicSdk/kg/quality_detail.js
Normal file
190
src/main/utils/musicSdk/kg/quality_detail.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { dnsLookup } from '../utils'
|
||||
import { headers, timeout } from '../options'
|
||||
import { sizeFormate, decodeName, formatPlayTime } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
console.log(headers);
|
||||
|
||||
export const getBatchMusicQualityInfo = (hashList) => {
|
||||
const resources = hashList.map((hash) => ({
|
||||
id: 0,
|
||||
type: 'audio',
|
||||
hash,
|
||||
}))
|
||||
|
||||
const requestObj = httpFetch(
|
||||
`https://gateway.kugou.com/goodsmstore/v1/get_res_privilege?appid=1005&clientver=20049&clienttime=${Date.now()}&mid=NeZha`,
|
||||
{
|
||||
method: 'post',
|
||||
timeout,
|
||||
headers,
|
||||
body: {
|
||||
behavior: 'play',
|
||||
clientver: '20049',
|
||||
resource: resources,
|
||||
area_code: '1',
|
||||
quality: '128',
|
||||
qualities: [
|
||||
'128',
|
||||
'320',
|
||||
'flac',
|
||||
'high',
|
||||
'dolby',
|
||||
'viper_atmos',
|
||||
'viper_tape',
|
||||
'viper_clear',
|
||||
],
|
||||
},
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
}
|
||||
)
|
||||
|
||||
const qualityInfoMap = {}
|
||||
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
|
||||
if (statusCode != 200 || body.error_code != 0)
|
||||
return Promise.reject(new Error('获取音质信息失败'))
|
||||
|
||||
body.data.forEach((songData, index) => {
|
||||
const hash = hashList[index]
|
||||
const types = []
|
||||
const _types = {}
|
||||
|
||||
if (!songData || !songData.relate_goods) return
|
||||
|
||||
for (const quality_data of songData.relate_goods) {
|
||||
if (quality_data.quality === '128') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: '128k', size, hash: quality_data.hash })
|
||||
_types['128k'] = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
if (quality_data.quality === '320') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: '320k', size, hash: quality_data.hash })
|
||||
_types['320k'] = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
if (quality_data.quality === 'flac') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: 'flac', size, hash: quality_data.hash })
|
||||
_types.flac = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
if (quality_data.quality === 'high') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: 'hires', size, hash: quality_data.hash })
|
||||
_types.hires = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
if (quality_data.quality === 'viper_clear') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: 'master', size, hash: quality_data.hash })
|
||||
_types.master = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
if (quality_data.quality === 'viper_atmos') {
|
||||
let size = sizeFormate(quality_data.info.filesize)
|
||||
types.push({ type: 'atmos', size, hash: quality_data.hash })
|
||||
_types.atmos = {
|
||||
size,
|
||||
hash: quality_data.hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qualityInfoMap[hash] = { types, _types }
|
||||
})
|
||||
|
||||
return qualityInfoMap
|
||||
})
|
||||
|
||||
return requestObj
|
||||
}
|
||||
|
||||
export const getHashFromItem = (item) => {
|
||||
if (item.hash) return item.hash
|
||||
if (item.FileHash) return item.FileHash
|
||||
if (item.audio_info && item.audio_info.hash) return item.audio_info.hash
|
||||
return null
|
||||
}
|
||||
|
||||
export const filterData = async (rawList, options = {}) => {
|
||||
let processedList = rawList
|
||||
|
||||
if (options.removeDuplicates) {
|
||||
let ids = new Set()
|
||||
processedList = rawList.filter((item) => {
|
||||
if (!item) return false
|
||||
const audioId = item.audio_info?.audio_id || item.audio_id
|
||||
if (ids.has(audioId)) return false
|
||||
ids.add(audioId)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const hashList = processedList.map((item) => getHashFromItem(item)).filter((hash) => hash)
|
||||
|
||||
const qualityInfoRequest = getBatchMusicQualityInfo(hashList)
|
||||
let qualityInfoMap = {}
|
||||
|
||||
try {
|
||||
qualityInfoMap = await qualityInfoRequest.promise
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch quality info:', error)
|
||||
}
|
||||
|
||||
return processedList.map((item) => {
|
||||
const hash = getHashFromItem(item)
|
||||
const { types = [], _types = {} } = qualityInfoMap[hash] || {}
|
||||
|
||||
if (item.audio_info) {
|
||||
return {
|
||||
name: decodeName(item.songname),
|
||||
singer: decodeName(item.author_name),
|
||||
albumName: decodeName(item.album_info?.album_name || item.remark),
|
||||
albumId: item.album_info.album_id,
|
||||
songmid: item.audio_info.audio_id,
|
||||
source: 'kg',
|
||||
interval: options.fix
|
||||
? formatPlayTime(parseInt(item.audio_info.timelength) / 1000)
|
||||
: formatPlayTime(parseInt(item.audio_info.timelength)),
|
||||
img: null,
|
||||
lrc: null,
|
||||
hash: item.audio_info.hash,
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: decodeName(item.songname),
|
||||
singer: decodeName(item.singername) || formatSingerName(item.authors, 'author_name'),
|
||||
albumName: decodeName(item.album_name || item.remark),
|
||||
albumId: item.album_id,
|
||||
songmid: item.audio_id,
|
||||
source: 'kg',
|
||||
interval: options.fix ? formatPlayTime(item.duration / 1000) : formatPlayTime(item.duration),
|
||||
img: null,
|
||||
lrc: null,
|
||||
hash: item.hash,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {},
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
||||
import { decodeName, dateFormat, formatPlayCount } from '../../index'
|
||||
import './vendors/infSign.min'
|
||||
|
||||
import { signatureParams } from './util'
|
||||
import { filterData } from './quality_detail'
|
||||
|
||||
const handleSignature = (id, page, limit) =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -14,7 +14,7 @@ const handleSignature = (id, page, limit) =>
|
||||
isCDN: !0,
|
||||
callback(i) {
|
||||
resolve(i.signature)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -27,36 +27,36 @@ export default {
|
||||
listDetailLimit: 10000,
|
||||
currentTagInfo: {
|
||||
id: undefined,
|
||||
info: undefined
|
||||
info: undefined,
|
||||
},
|
||||
sortList: [
|
||||
{
|
||||
name: '推荐',
|
||||
id: '5'
|
||||
id: '5',
|
||||
},
|
||||
{
|
||||
name: '最热',
|
||||
id: '6'
|
||||
id: '6',
|
||||
},
|
||||
{
|
||||
name: '最新',
|
||||
id: '7'
|
||||
id: '7',
|
||||
},
|
||||
{
|
||||
name: '热藏',
|
||||
id: '3'
|
||||
id: '3',
|
||||
},
|
||||
{
|
||||
name: '飙升',
|
||||
id: '8'
|
||||
}
|
||||
id: '8',
|
||||
},
|
||||
],
|
||||
cache: new Map(),
|
||||
regExps: {
|
||||
listData: /global\.data = (\[.+\]);/,
|
||||
listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/,
|
||||
// https://www.kugou.com/yy/special/single/1067062.html
|
||||
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/
|
||||
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/,
|
||||
},
|
||||
parseHtmlDesc(html) {
|
||||
const prefix = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
|
||||
@@ -71,18 +71,17 @@ export default {
|
||||
if (tryNum > 2) throw new Error('try max num')
|
||||
|
||||
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
|
||||
const listData = body.match(this.regExps.listData)
|
||||
const listInfo = body.match(this.regExps.listInfo)
|
||||
let listData = body.match(this.regExps.listData)
|
||||
let listInfo = body.match(this.regExps.listInfo)
|
||||
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
|
||||
const list = await this.getMusicInfos(JSON.parse(listData[1]))
|
||||
// listData = this.filterData(JSON.parse(listData[1]))
|
||||
let list = await this.getMusicInfos(JSON.parse(listData[1]))
|
||||
let name
|
||||
let pic
|
||||
if (listInfo) {
|
||||
name = listInfo[1]
|
||||
pic = listInfo[2]
|
||||
}
|
||||
const desc = this.parseHtmlDesc(body)
|
||||
let desc = this.parseHtmlDesc(body)
|
||||
|
||||
return {
|
||||
list,
|
||||
@@ -93,10 +92,10 @@ export default {
|
||||
info: {
|
||||
name,
|
||||
img: pic,
|
||||
desc
|
||||
desc,
|
||||
// author: body.result.info.userinfo.username,
|
||||
// play_count: formatPlayCount(body.result.listen_num),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
getInfoUrl(tagId) {
|
||||
@@ -116,11 +115,11 @@ export default {
|
||||
const result = []
|
||||
if (rawData.status !== 1) return result
|
||||
for (const key of Object.keys(rawData.data)) {
|
||||
const tag = rawData.data[key]
|
||||
let tag = rawData.data[key]
|
||||
result.push({
|
||||
id: tag.special_id,
|
||||
name: tag.special_name,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -135,8 +134,8 @@ export default {
|
||||
parent_name: tag.pname,
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
source: 'kg'
|
||||
}))
|
||||
source: 'kg',
|
||||
})),
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -159,7 +158,7 @@ export default {
|
||||
{
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': 'KuGou2012-8275-web_browser_event_handler'
|
||||
'User-Agent': 'KuGou2012-8275-web_browser_event_handler',
|
||||
},
|
||||
body: {
|
||||
appid: 1001,
|
||||
@@ -170,8 +169,8 @@ export default {
|
||||
platform: 'pc',
|
||||
userid: '262643156',
|
||||
return_min: 6,
|
||||
return_max: 15
|
||||
}
|
||||
return_max: 15,
|
||||
},
|
||||
}
|
||||
)
|
||||
return this._requestObj_listRecommend.promise.then(({ body }) => {
|
||||
@@ -190,7 +189,7 @@ export default {
|
||||
total: item.songcount,
|
||||
grade: item.grade,
|
||||
desc: item.intro,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -219,7 +218,7 @@ export default {
|
||||
},
|
||||
|
||||
createTask(hashs) {
|
||||
const data = {
|
||||
let data = {
|
||||
area_code: '1',
|
||||
show_privilege: 1,
|
||||
show_album_info: '1',
|
||||
@@ -230,16 +229,16 @@ export default {
|
||||
dfid: '-',
|
||||
clienttime: Date.now(),
|
||||
key: 'OIlwieks28dk2k092lksi2UIkp',
|
||||
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname'
|
||||
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname',
|
||||
}
|
||||
let list = hashs
|
||||
const tasks = []
|
||||
let tasks = []
|
||||
while (list.length) {
|
||||
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
|
||||
if (list.length < 100) break
|
||||
list = list.slice(100)
|
||||
}
|
||||
const url = 'http://gateway.kugou.com/v2/album_audio/audio'
|
||||
let url = 'http://gateway.kugou.com/v2/album_audio/audio'
|
||||
return tasks.map((task) =>
|
||||
this.createHttp(url, {
|
||||
method: 'POST',
|
||||
@@ -250,13 +249,13 @@ export default {
|
||||
'KG-Fake': '0',
|
||||
'KG-RF': '00869891',
|
||||
'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi',
|
||||
'x-router': 'kmr.service.kugou.com'
|
||||
}
|
||||
'x-router': 'kmr.service.kugou.com',
|
||||
},
|
||||
}).then((data) => data.map((s) => s[0]))
|
||||
)
|
||||
},
|
||||
async getMusicInfos(list) {
|
||||
return this.filterData2(
|
||||
return await this.filterData(
|
||||
await Promise.all(
|
||||
this.createTask(this.deDuplication(list).map((item) => ({ hash: item.hash })))
|
||||
).then(([...datas]) => datas.flat())
|
||||
@@ -269,7 +268,7 @@ export default {
|
||||
headers: {
|
||||
'KG-RC': 1,
|
||||
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
||||
'User-Agent': ''
|
||||
'User-Agent': '',
|
||||
},
|
||||
body: {
|
||||
appid: 1001,
|
||||
@@ -277,13 +276,13 @@ export default {
|
||||
mid: '21511157a05844bd085308bc76ef3343',
|
||||
clienttime: 640612895,
|
||||
key: '36164c4015e704673c588ee202b9ecb8',
|
||||
data: id
|
||||
}
|
||||
data: id,
|
||||
},
|
||||
})
|
||||
// console.log(songInfo)
|
||||
// type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列
|
||||
let songList
|
||||
const info = songInfo.info
|
||||
let info = songInfo.info
|
||||
switch (info.type) {
|
||||
case 2:
|
||||
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
|
||||
@@ -299,7 +298,7 @@ export default {
|
||||
headers: {
|
||||
'KG-RC': 1,
|
||||
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
||||
'User-Agent': ''
|
||||
'User-Agent': '',
|
||||
},
|
||||
body: {
|
||||
appid: 1001,
|
||||
@@ -313,13 +312,13 @@ export default {
|
||||
userid: info.userid,
|
||||
collect_type: 0,
|
||||
page: 1,
|
||||
pagesize: info.count
|
||||
}
|
||||
}
|
||||
pagesize: info.count,
|
||||
},
|
||||
},
|
||||
})
|
||||
// console.log(songList)
|
||||
}
|
||||
const list = await this.getMusicInfos(songList || songInfo.list)
|
||||
let list = await this.getMusicInfos(songList || songInfo.list)
|
||||
return {
|
||||
list,
|
||||
page: 1,
|
||||
@@ -330,9 +329,9 @@ export default {
|
||||
name: info.name,
|
||||
img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img,
|
||||
// desc: body.result.info.list_desc,
|
||||
author: info.username
|
||||
author: info.username,
|
||||
// play_count: formatPlayCount(info.count),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -342,8 +341,8 @@ export default {
|
||||
{
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'
|
||||
}
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
||||
},
|
||||
}
|
||||
)
|
||||
if (!songInfo.list) {
|
||||
@@ -354,7 +353,7 @@ export default {
|
||||
this.getUserListDetail5(chain)
|
||||
)
|
||||
}
|
||||
const list = await this.getMusicInfos(songInfo.list)
|
||||
let list = await this.getMusicInfos(songInfo.list)
|
||||
// console.log(info, songInfo)
|
||||
return {
|
||||
list,
|
||||
@@ -366,14 +365,14 @@ export default {
|
||||
name: songInfo.info.name,
|
||||
img: songInfo.info.img,
|
||||
// desc: body.result.info.list_desc,
|
||||
author: songInfo.info.username
|
||||
author: songInfo.info.username,
|
||||
// play_count: formatPlayCount(info.count),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
deDuplication(datas) {
|
||||
const ids = new Set()
|
||||
let ids = new Set()
|
||||
return datas.filter(({ hash }) => {
|
||||
if (ids.has(hash)) return false
|
||||
ids.add(hash)
|
||||
@@ -388,29 +387,25 @@ export default {
|
||||
data: [
|
||||
{
|
||||
id: gcid,
|
||||
id_type: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
const result = await this.createHttp(
|
||||
`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36',
|
||||
Referer: 'https://m.kugou.com/'
|
||||
id_type: 2,
|
||||
},
|
||||
body
|
||||
}
|
||||
)
|
||||
],
|
||||
}
|
||||
const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36',
|
||||
Referer: 'https://m.kugou.com/',
|
||||
},
|
||||
body,
|
||||
})
|
||||
return result.list[0].global_collection_id
|
||||
},
|
||||
|
||||
async getUserListDetailByLink({ info }, link) {
|
||||
const listInfo = info['0']
|
||||
let listInfo = info['0']
|
||||
let total = listInfo.count
|
||||
const tasks = []
|
||||
let tasks = []
|
||||
let page = 0
|
||||
while (total) {
|
||||
const limit = total > 90 ? 90 : total
|
||||
@@ -423,8 +418,8 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
||||
Referer: link
|
||||
}
|
||||
Referer: link,
|
||||
},
|
||||
}
|
||||
).then((data) => data.list.info)
|
||||
)
|
||||
@@ -442,13 +437,13 @@ export default {
|
||||
name: listInfo.name,
|
||||
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
|
||||
// desc: body.result.info.list_desc,
|
||||
author: listInfo.list_create_username
|
||||
author: listInfo.list_create_username,
|
||||
// play_count: formatPlayCount(listInfo.count),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
createGetListDetail2Task(id, total) {
|
||||
const tasks = []
|
||||
let tasks = []
|
||||
let page = 0
|
||||
while (total) {
|
||||
const limit = total > 300 ? 300 : total
|
||||
@@ -464,7 +459,10 @@ export default {
|
||||
'&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-'
|
||||
tasks.push(
|
||||
this.createHttp(
|
||||
`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`,
|
||||
`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(
|
||||
params,
|
||||
'web'
|
||||
)}`,
|
||||
{
|
||||
headers: {
|
||||
mid: '1586163263991',
|
||||
@@ -472,8 +470,8 @@ export default {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
dfid: '-',
|
||||
clienttime: '1586163263991'
|
||||
}
|
||||
clienttime: '1586163263991',
|
||||
},
|
||||
}
|
||||
).then((data) => data.info)
|
||||
)
|
||||
@@ -481,14 +479,17 @@ export default {
|
||||
return Promise.all(tasks).then(([...datas]) => datas.flat())
|
||||
},
|
||||
async getUserListDetail2(global_collection_id) {
|
||||
const id = global_collection_id
|
||||
let id = global_collection_id
|
||||
if (id.length > 1000) throw new Error('get list error')
|
||||
const params =
|
||||
'appid=1058&specialid=0&global_specialid=' +
|
||||
id +
|
||||
'&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
|
||||
const info = await this.createHttp(
|
||||
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`,
|
||||
let info = await this.createHttp(
|
||||
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(
|
||||
params,
|
||||
'web'
|
||||
)}`,
|
||||
{
|
||||
headers: {
|
||||
mid: '1586163242519',
|
||||
@@ -496,12 +497,12 @@ export default {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
dfid: '-',
|
||||
clienttime: '1586163242519'
|
||||
}
|
||||
clienttime: '1586163242519',
|
||||
},
|
||||
}
|
||||
)
|
||||
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
|
||||
const list = await this.getMusicInfos(songInfo)
|
||||
let list = await this.getMusicInfos(songInfo)
|
||||
// console.log(info, songInfo, list)
|
||||
return {
|
||||
list,
|
||||
@@ -514,8 +515,8 @@ export default {
|
||||
img: info.imgurl && info.imgurl.replace('{size}', 240),
|
||||
desc: info.intro,
|
||||
author: info.nickname,
|
||||
play_count: formatPlayCount(info.playcount)
|
||||
}
|
||||
play_count: formatPlayCount(info.playcount),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -524,8 +525,8 @@ export default {
|
||||
const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
}
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
}).promise
|
||||
let result = body.match(/var\sphpParam\s=\s({.+?});/)
|
||||
if (result) result = JSON.parse(result[1])
|
||||
@@ -534,13 +535,13 @@ export default {
|
||||
},
|
||||
|
||||
async getUserListDetailByPcChain(chain) {
|
||||
const key = `${chain}_pc_list`
|
||||
let key = `${chain}_pc_list`
|
||||
if (this.cache.has(key)) return this.cache.get(key)
|
||||
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
|
||||
}
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
||||
},
|
||||
}).promise
|
||||
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
|
||||
if (result) result = JSON.parse(result[1])
|
||||
@@ -554,7 +555,7 @@ export default {
|
||||
const limit = 100
|
||||
const [listInfo, list] = await Promise.all([
|
||||
this.getListInfoByChain(chain),
|
||||
this.getUserListDetailById(songInfo.id, page, limit)
|
||||
this.getUserListDetailById(songInfo.id, page, limit),
|
||||
])
|
||||
return {
|
||||
list: list || [],
|
||||
@@ -566,16 +567,16 @@ export default {
|
||||
name: listInfo.specialname,
|
||||
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
||||
// desc: body.result.info.list_desc,
|
||||
author: listInfo.nickname
|
||||
author: listInfo.nickname,
|
||||
// play_count: formatPlayCount(info.count),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
async getUserListDetail5(chain) {
|
||||
const [listInfo, list] = await Promise.all([
|
||||
this.getListInfoByChain(chain),
|
||||
this.getUserListDetailByPcChain(chain)
|
||||
this.getUserListDetailByPcChain(chain),
|
||||
])
|
||||
return {
|
||||
list: list || [],
|
||||
@@ -587,28 +588,28 @@ export default {
|
||||
name: listInfo.specialname,
|
||||
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
||||
// desc: body.result.info.list_desc,
|
||||
author: listInfo.nickname
|
||||
author: listInfo.nickname,
|
||||
// play_count: formatPlayCount(info.count),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
async getUserListDetailById(id, page, limit) {
|
||||
const signature = await handleSignature(id, page, limit)
|
||||
const info = await this.createHttp(
|
||||
let info = await this.createHttp(
|
||||
`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`,
|
||||
{
|
||||
headers: {
|
||||
Referer: 'https://m3ws.kugou.com/share/index.php',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
dfid: '-'
|
||||
}
|
||||
dfid: '-',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// console.log(info)
|
||||
const result = await this.getMusicInfos(info.info)
|
||||
let result = await this.getMusicInfos(info.info)
|
||||
// console.log(info, songInfo)
|
||||
return result
|
||||
},
|
||||
@@ -616,19 +617,15 @@ export default {
|
||||
async getUserListDetail(link, page, retryNum = 0) {
|
||||
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
|
||||
if (link.includes('#')) link = link.replace(/#.*$/, '')
|
||||
if (link.includes('global_collection_id'))
|
||||
return this.getUserListDetail2(
|
||||
link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
|
||||
)
|
||||
if (link.includes('global_collection_id')) return this.getUserListDetail2(link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
|
||||
if (link.includes('gcid_')) {
|
||||
const gcid = link.match(/gcid_\w+/)?.[0]
|
||||
let gcid = link.match(/gcid_\w+/)?.[0]
|
||||
if (gcid) {
|
||||
const global_collection_id = await this.decodeGcid(gcid)
|
||||
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
||||
}
|
||||
}
|
||||
if (link.includes('chain='))
|
||||
return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
||||
if (link.includes('chain=')) return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
||||
if (link.includes('.html')) {
|
||||
if (link.includes('zlist.html')) {
|
||||
link = link.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
||||
@@ -650,34 +647,27 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
||||
Referer: link
|
||||
}
|
||||
Referer: link,
|
||||
},
|
||||
})
|
||||
const {
|
||||
headers: { location },
|
||||
statusCode,
|
||||
body
|
||||
body,
|
||||
} = await requestObj_listDetailLink.promise
|
||||
// console.log(body, location)
|
||||
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
|
||||
if (location) {
|
||||
// console.log(location)
|
||||
if (location.includes('global_collection_id'))
|
||||
return this.getUserListDetail2(
|
||||
location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
|
||||
)
|
||||
if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
|
||||
if (location.includes('gcid_')) {
|
||||
const gcid = link.match(/gcid_\w+/)?.[0]
|
||||
let gcid = link.match(/gcid_\w+/)?.[0]
|
||||
if (gcid) {
|
||||
const global_collection_id = await this.decodeGcid(gcid)
|
||||
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
||||
}
|
||||
}
|
||||
if (location.includes('chain='))
|
||||
return this.getUserListDetail3(
|
||||
location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'),
|
||||
page
|
||||
)
|
||||
if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
||||
if (location.includes('.html')) {
|
||||
if (location.includes('zlist.html')) {
|
||||
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
||||
@@ -698,7 +688,7 @@ export default {
|
||||
// console.log('location', location)
|
||||
return this.getUserListDetail(location, page, ++retryNum)
|
||||
}
|
||||
if (typeof body === 'string') {
|
||||
if (typeof body == 'string') {
|
||||
let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1]
|
||||
if (!global_collection_id) {
|
||||
let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1]
|
||||
@@ -729,184 +719,9 @@ export default {
|
||||
|
||||
return this.getListDetailBySpecialId(id, page)
|
||||
},
|
||||
filterData(rawList) {
|
||||
// console.log(rawList)
|
||||
return rawList.map((item) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
if (item.filesize !== 0) {
|
||||
const size = sizeFormate(item.filesize)
|
||||
types.push({ type: '128k', size, hash: item.hash })
|
||||
_types['128k'] = {
|
||||
size,
|
||||
hash: item.hash
|
||||
}
|
||||
}
|
||||
if (item.filesize_320 !== 0) {
|
||||
const size = sizeFormate(item.filesize_320)
|
||||
types.push({ type: '320k', size, hash: item.hash_320 })
|
||||
_types['320k'] = {
|
||||
size,
|
||||
hash: item.hash_320
|
||||
}
|
||||
}
|
||||
if (item.filesize_ape !== 0) {
|
||||
const size = sizeFormate(item.filesize_ape)
|
||||
types.push({ type: 'ape', size, hash: item.hash_ape })
|
||||
_types.ape = {
|
||||
size,
|
||||
hash: item.hash_ape
|
||||
}
|
||||
}
|
||||
if (item.filesize_flac !== 0) {
|
||||
const size = sizeFormate(item.filesize_flac)
|
||||
types.push({ type: 'flac', size, hash: item.hash_flac })
|
||||
_types.flac = {
|
||||
size,
|
||||
hash: item.hash_flac
|
||||
}
|
||||
}
|
||||
return {
|
||||
singer: decodeName(item.singername),
|
||||
name: decodeName(item.songname),
|
||||
albumName: decodeName(item.album_name),
|
||||
albumId: item.album_id,
|
||||
songmid: item.audio_id,
|
||||
source: 'kg',
|
||||
interval: formatPlayTime(item.duration / 1000),
|
||||
img: null,
|
||||
lrc: null,
|
||||
hash: item.hash,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
}
|
||||
})
|
||||
},
|
||||
// getSinger(singers) {
|
||||
// let arr = []
|
||||
// singers?.forEach(singer => {
|
||||
// arr.push(singer.name)
|
||||
// })
|
||||
// return arr.join('、')
|
||||
// },
|
||||
// v9 API
|
||||
// filterDatav9(rawList) {
|
||||
// console.log(rawList)
|
||||
// return rawList.map(item => {
|
||||
// const types = []
|
||||
// const _types = {}
|
||||
// item.relate_goods.forEach(qualityObj => {
|
||||
// if (qualityObj.level === 2) {
|
||||
// let size = sizeFormate(qualityObj.size)
|
||||
// types.push({ type: '128k', size, hash: qualityObj.hash })
|
||||
// _types['128k'] = {
|
||||
// size,
|
||||
// hash: qualityObj.hash,
|
||||
// }
|
||||
// } else if (qualityObj.level === 4) {
|
||||
// let size = sizeFormate(qualityObj.size)
|
||||
// types.push({ type: '320k', size, hash: qualityObj.hash })
|
||||
// _types['320k'] = {
|
||||
// size,
|
||||
// hash: qualityObj.hash,
|
||||
// }
|
||||
// } else if (qualityObj.level === 5) {
|
||||
// let size = sizeFormate(qualityObj.size)
|
||||
// types.push({ type: 'flac', size, hash: qualityObj.hash })
|
||||
// _types.flac = {
|
||||
// size,
|
||||
// hash: qualityObj.hash,
|
||||
// }
|
||||
// } else if (qualityObj.level === 6) {
|
||||
// let size = sizeFormate(qualityObj.size)
|
||||
// types.push({ type: 'flac24bit', size, hash: qualityObj.hash })
|
||||
// _types.flac24bit = {
|
||||
// size,
|
||||
// hash: qualityObj.hash,
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// const nameInfo = item.name.split(' - ')
|
||||
// return {
|
||||
// singer: this.getSinger(item.singerinfo),
|
||||
// name: decodeName((nameInfo[1] ?? nameInfo[0]).trim()),
|
||||
// albumName: decodeName(item.albuminfo.name),
|
||||
// albumId: item.albuminfo.id,
|
||||
// songmid: item.audio_id,
|
||||
// source: 'kg',
|
||||
// interval: formatPlayTime(item.timelen / 1000),
|
||||
// img: null,
|
||||
// lrc: null,
|
||||
// hash: item.hash,
|
||||
// types,
|
||||
// _types,
|
||||
// typeUrl: {},
|
||||
// }
|
||||
// })
|
||||
// },
|
||||
|
||||
// hash list filter
|
||||
filterData2(rawList) {
|
||||
// console.log(rawList)
|
||||
const ids = new Set()
|
||||
const list = []
|
||||
rawList.forEach((item) => {
|
||||
if (!item) return
|
||||
if (ids.has(item.audio_info.audio_id)) return
|
||||
ids.add(item.audio_info.audio_id)
|
||||
const types = []
|
||||
const _types = {}
|
||||
if (item.audio_info.filesize !== '0') {
|
||||
const size = sizeFormate(parseInt(item.audio_info.filesize))
|
||||
types.push({ type: '128k', size, hash: item.audio_info.hash })
|
||||
_types['128k'] = {
|
||||
size,
|
||||
hash: item.audio_info.hash
|
||||
}
|
||||
}
|
||||
if (item.audio_info.filesize_320 !== '0') {
|
||||
const size = sizeFormate(parseInt(item.audio_info.filesize_320))
|
||||
types.push({ type: '320k', size, hash: item.audio_info.hash_320 })
|
||||
_types['320k'] = {
|
||||
size,
|
||||
hash: item.audio_info.hash_320
|
||||
}
|
||||
}
|
||||
if (item.audio_info.filesize_flac !== '0') {
|
||||
const size = sizeFormate(parseInt(item.audio_info.filesize_flac))
|
||||
types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })
|
||||
_types.flac = {
|
||||
size,
|
||||
hash: item.audio_info.hash_flac
|
||||
}
|
||||
}
|
||||
if (item.audio_info.filesize_high !== '0') {
|
||||
const size = sizeFormate(parseInt(item.audio_info.filesize_high))
|
||||
types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
hash: item.audio_info.hash_high
|
||||
}
|
||||
}
|
||||
list.push({
|
||||
singer: decodeName(item.author_name),
|
||||
name: decodeName(item.songname),
|
||||
albumName: decodeName(item.album_info.album_name),
|
||||
albumId: item.album_info.album_id,
|
||||
songmid: item.audio_info.audio_id,
|
||||
source: 'kg',
|
||||
interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000),
|
||||
img: null,
|
||||
lrc: null,
|
||||
hash: item.audio_info.hash,
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
})
|
||||
})
|
||||
return list
|
||||
async filterData(rawList) {
|
||||
return await filterData(rawList, { removeDuplicates: true, fix: true })
|
||||
},
|
||||
|
||||
// 获取列表信息
|
||||
@@ -920,14 +735,14 @@ export default {
|
||||
limit: body.data.params.pagesize,
|
||||
page: body.data.params.p,
|
||||
total: body.data.params.total,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取列表数据
|
||||
getList(sortId, tagId, page) {
|
||||
const tasks = [this.getSongList(sortId, tagId, page)]
|
||||
let tasks = [this.getSongList(sortId, tagId, page)]
|
||||
tasks.push(
|
||||
this.currentTagInfo.id === tagId
|
||||
? Promise.resolve(this.currentTagInfo.info)
|
||||
@@ -943,7 +758,7 @@ export default {
|
||||
if (recommendList) list.unshift(...recommendList)
|
||||
return {
|
||||
list,
|
||||
...info
|
||||
...info,
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -958,13 +773,13 @@ export default {
|
||||
return {
|
||||
hotTag: this.filterInfoHotTag(body.data.hotTag),
|
||||
tags: this.filterTagInfo(body.data.tagids),
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getDetailPageUrl(id) {
|
||||
if (typeof id === 'string') {
|
||||
if (typeof id == 'string') {
|
||||
if (/^https?:\/\//.test(id)) return id
|
||||
id = id.replace('id_', '')
|
||||
}
|
||||
@@ -975,7 +790,9 @@ export default {
|
||||
// http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0
|
||||
// return httpFetch(`http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5`)
|
||||
return httpFetch(
|
||||
`http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2`
|
||||
`http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(
|
||||
text
|
||||
)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2`
|
||||
).promise.then(({ body }) => {
|
||||
if (body.errcode != 0) throw new Error('filed')
|
||||
// console.log(body.data.info)
|
||||
@@ -991,17 +808,17 @@ export default {
|
||||
grade: item.grade,
|
||||
desc: item.intro,
|
||||
total: item.songcount,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: body.data.total,
|
||||
source: 'kg'
|
||||
source: 'kg',
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
// getTags
|
||||
// getListDetail
|
||||
// getListDetail
|
||||
@@ -1,13 +1,13 @@
|
||||
// import '../../polyfill/array.find'
|
||||
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, decodeName } from '../index'
|
||||
import { formatPlayTime, decodeName } from '../../index'
|
||||
// import { debug } from '../../utils/env'
|
||||
import { formatSinger } from './util'
|
||||
|
||||
export default {
|
||||
regExps: {
|
||||
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/
|
||||
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/,
|
||||
},
|
||||
limit: 30,
|
||||
total: 0,
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
// console.log(rawData)
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const info = rawData[i]
|
||||
const songId = info.MUSICRID.replace('MUSIC_', '')
|
||||
let songId = info.MUSICRID.replace('MUSIC_', '')
|
||||
// const format = (info.FORMATS || info.formats).split('|')
|
||||
|
||||
if (!info.N_MINFO) {
|
||||
@@ -43,33 +43,39 @@ export default {
|
||||
const types = []
|
||||
const _types = {}
|
||||
|
||||
const infoArr = info.N_MINFO.split(';')
|
||||
let infoArr = info.N_MINFO.split(';')
|
||||
for (let info of infoArr) {
|
||||
info = info.match(this.regExps.mInfo)
|
||||
if (info) {
|
||||
switch (info[2]) {
|
||||
case '20900':
|
||||
types.push({ type: 'master', size: info[4] })
|
||||
_types.master = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '4000':
|
||||
types.push({ type: 'flac24bit', size: info[4] })
|
||||
_types.flac24bit = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
types.push({ type: 'hires', size: info[4] })
|
||||
_types.hires = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '2000':
|
||||
types.push({ type: 'flac', size: info[4] })
|
||||
_types.flac = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '320':
|
||||
types.push({ type: '320k', size: info[4] })
|
||||
_types['320k'] = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '128':
|
||||
types.push({ type: '128k', size: info[4] })
|
||||
_types['128k'] = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -77,7 +83,7 @@ export default {
|
||||
}
|
||||
types.reverse()
|
||||
|
||||
const interval = parseInt(info.DURATION)
|
||||
let interval = parseInt(info.DURATION)
|
||||
|
||||
result.push({
|
||||
name: decodeName(info.SONGNAME),
|
||||
@@ -95,7 +101,7 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
})
|
||||
}
|
||||
// console.log(result)
|
||||
@@ -109,7 +115,7 @@ export default {
|
||||
// console.log(result)
|
||||
if (!result || (result.TOTAL !== '0' && result.SHOW === '0'))
|
||||
return this.search(str, page, limit, ++retryNum)
|
||||
const list = this.handleResult(result.abslist)
|
||||
let list = this.handleResult(result.abslist)
|
||||
|
||||
if (list == null) return this.search(str, page, limit, ++retryNum)
|
||||
|
||||
@@ -122,8 +128,8 @@ export default {
|
||||
allPage: this.allPage,
|
||||
total: this.total,
|
||||
limit,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, decodeName } from '../index'
|
||||
import { formatPlayTime, decodeName } from '../../index'
|
||||
import { formatSinger, objStr2JSON } from './util'
|
||||
import album from './album'
|
||||
|
||||
@@ -13,18 +13,18 @@ export default {
|
||||
sortList: [
|
||||
{
|
||||
name: '最新',
|
||||
id: 'new'
|
||||
id: 'new',
|
||||
},
|
||||
{
|
||||
name: '最热',
|
||||
id: 'hot'
|
||||
}
|
||||
id: 'hot',
|
||||
},
|
||||
],
|
||||
regExps: {
|
||||
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/,
|
||||
// http://www.kuwo.cn/playlist_detail/2886046289
|
||||
// https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend
|
||||
listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/
|
||||
listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/,
|
||||
},
|
||||
tagsUrl:
|
||||
'http://wapi.kuwo.cn/api/pc/classify/playlist/getTagList?cmd=rcm_keyword_playlist&user=0&prod=kwplayer_pc_9.0.5.0&vipver=9.0.5.0&source=kwplayer_pc_9.0.5.0&loginUid=0&loginSid=0&appUid=76039576',
|
||||
@@ -43,7 +43,9 @@ export default {
|
||||
},
|
||||
getListDetailUrl(id, page) {
|
||||
// http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2858093057&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1
|
||||
return `http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}&rn=${this.limit_song}&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1`
|
||||
return `http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}&rn=${
|
||||
this.limit_song
|
||||
}&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1`
|
||||
// http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=140&prod=pc
|
||||
},
|
||||
|
||||
@@ -72,7 +74,7 @@ export default {
|
||||
return rawList.map((item) => ({
|
||||
id: `${item.id}-${item.digest}`,
|
||||
name: item.name,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}))
|
||||
},
|
||||
filterTagInfo(rawList) {
|
||||
@@ -83,8 +85,8 @@ export default {
|
||||
parent_name: type.name,
|
||||
id: `${item.id}-${item.digest}`,
|
||||
name: item.name,
|
||||
source: 'kw'
|
||||
}))
|
||||
source: 'kw',
|
||||
})),
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -95,7 +97,7 @@ export default {
|
||||
let id
|
||||
let type
|
||||
if (tagId) {
|
||||
const arr = tagId.split('-')
|
||||
let arr = tagId.split('-')
|
||||
id = arr[0]
|
||||
type = arr[1]
|
||||
} else {
|
||||
@@ -110,7 +112,7 @@ export default {
|
||||
total: body.data.total,
|
||||
page: body.data.pn,
|
||||
limit: body.data.rn,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}
|
||||
} else if (!body.length) {
|
||||
return this.getList(sortId, tagId, page, ++tryNum)
|
||||
@@ -120,7 +122,7 @@ export default {
|
||||
total: 1000,
|
||||
page,
|
||||
limit: 1000,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -145,7 +147,7 @@ export default {
|
||||
img: item.img,
|
||||
grade: item.favorcnt / 10,
|
||||
desc: item.desc,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}))
|
||||
},
|
||||
filterList2(rawData) {
|
||||
@@ -164,7 +166,7 @@ export default {
|
||||
img: item.img,
|
||||
grade: item.favorcnt && item.favorcnt / 10,
|
||||
desc: item.desc,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}))
|
||||
)
|
||||
})
|
||||
@@ -188,8 +190,8 @@ export default {
|
||||
img: body.pic,
|
||||
desc: body.info,
|
||||
author: body.uname,
|
||||
play_count: this.formatPlayCount(body.playnum)
|
||||
}
|
||||
play_count: this.formatPlayCount(body.playnum),
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -207,7 +209,9 @@ export default {
|
||||
getListDetailDigest5Music(id, page, tryNum = 0) {
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
const requestObj = httpFetch(
|
||||
`http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}}&rn=${this.limit_song}&encode=utf-8&keyset=pl2012&identity=kuwo&pcmp4=1`
|
||||
`http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}}&rn=${
|
||||
this.limit_song
|
||||
}&encode=utf-8&keyset=pl2012&identity=kuwo&pcmp4=1`
|
||||
)
|
||||
return requestObj.promise.then(({ body }) => {
|
||||
// console.log(body)
|
||||
@@ -223,8 +227,8 @@ export default {
|
||||
img: body.pic,
|
||||
desc: body.info,
|
||||
author: body.uname,
|
||||
play_count: this.formatPlayCount(body.playnum)
|
||||
}
|
||||
play_count: this.formatPlayCount(body.playnum),
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -235,33 +239,33 @@ export default {
|
||||
|
||||
filterBDListDetail(rawList) {
|
||||
return rawList.map((item) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
for (const info of item.audios) {
|
||||
let types = []
|
||||
let _types = {}
|
||||
for (let info of item.audios) {
|
||||
info.size = info.size?.toLocaleUpperCase()
|
||||
switch (info.bitrate) {
|
||||
case '4000':
|
||||
types.push({ type: 'flac24bit', size: info.size })
|
||||
_types.flac24bit = {
|
||||
size: info.size
|
||||
size: info.size,
|
||||
}
|
||||
break
|
||||
case '2000':
|
||||
types.push({ type: 'flac', size: info.size })
|
||||
_types.flac = {
|
||||
size: info.size
|
||||
size: info.size,
|
||||
}
|
||||
break
|
||||
case '320':
|
||||
types.push({ type: '320k', size: info.size })
|
||||
_types['320k'] = {
|
||||
size: info.size
|
||||
size: info.size,
|
||||
}
|
||||
break
|
||||
case '128':
|
||||
types.push({ type: '128k', size: info.size })
|
||||
_types['128k'] = {
|
||||
size: info.size
|
||||
size: info.size,
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -282,7 +286,7 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -299,8 +303,8 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
||||
plat: 'h5'
|
||||
}
|
||||
plat: 'h5',
|
||||
},
|
||||
}
|
||||
).promise.catch(() => ({ code: 0 }))
|
||||
|
||||
@@ -311,7 +315,7 @@ export default {
|
||||
img: infoData.data.pic,
|
||||
desc: infoData.data.description,
|
||||
author: infoData.data.creatorName,
|
||||
play_count: infoData.data.playNum
|
||||
play_count: infoData.data.playNum,
|
||||
}
|
||||
},
|
||||
async getListDetailMusicListByBDUserPub(id) {
|
||||
@@ -321,8 +325,8 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
||||
plat: 'h5'
|
||||
}
|
||||
plat: 'h5',
|
||||
},
|
||||
}
|
||||
).promise.catch(() => ({ code: 0 }))
|
||||
|
||||
@@ -334,18 +338,20 @@ export default {
|
||||
img: infoData.data.userInfo.headImg,
|
||||
desc: '',
|
||||
author: infoData.data.userInfo.nickname,
|
||||
play_count: ''
|
||||
play_count: '',
|
||||
}
|
||||
},
|
||||
async getListDetailMusicListByBDList(id, source, page, tryNum = 0) {
|
||||
const { body: listData } = await httpFetch(
|
||||
`https://bd-api.kuwo.cn/api/service/playlist/${id}/musicList?reqId=${this.getReqId()}&source=${source}&pn=${page}&rn=${this.limit_song}`,
|
||||
`https://bd-api.kuwo.cn/api/service/playlist/${id}/musicList?reqId=${this.getReqId()}&source=${source}&pn=${page}&rn=${
|
||||
this.limit_song
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
||||
plat: 'h5'
|
||||
}
|
||||
plat: 'h5',
|
||||
},
|
||||
}
|
||||
).promise.catch(() => {
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
@@ -359,7 +365,7 @@ export default {
|
||||
page,
|
||||
limit: listData.data.pageSize,
|
||||
total: listData.data.total,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}
|
||||
},
|
||||
async getListDetailMusicListByBD(id, page) {
|
||||
@@ -383,7 +389,7 @@ export default {
|
||||
img: '',
|
||||
desc: '',
|
||||
author: '',
|
||||
play_count: ''
|
||||
play_count: '',
|
||||
}
|
||||
// console.log(listData)
|
||||
return listData
|
||||
@@ -415,35 +421,53 @@ export default {
|
||||
filterListDetail(rawData) {
|
||||
// console.log(rawData)
|
||||
return rawData.map((item) => {
|
||||
const infoArr = item.N_MINFO.split(';')
|
||||
const types = []
|
||||
const _types = {}
|
||||
let infoArr = item.N_MINFO.split(';')
|
||||
let types = []
|
||||
let _types = {}
|
||||
for (let info of infoArr) {
|
||||
info = info.match(this.regExps.mInfo)
|
||||
if (info) {
|
||||
switch (info[2]) {
|
||||
case '20900':
|
||||
types.push({ type: 'master', size: info[4] })
|
||||
_types.master = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '20501':
|
||||
types.push({ type: 'atmos_plus', size: info[4] })
|
||||
_types.atmos_plus = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '20201':
|
||||
types.push({ type: 'atmos', size: info[4] })
|
||||
_types.atmos = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '4000':
|
||||
types.push({ type: 'flac24bit', size: info[4] })
|
||||
types.push({ type: 'hires', size: info[4] })
|
||||
_types.flac24bit = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '2000':
|
||||
types.push({ type: 'flac', size: info[4] })
|
||||
_types.flac = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '320':
|
||||
types.push({ type: '320k', size: info[4] })
|
||||
_types['320k'] = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
case '128':
|
||||
types.push({ type: '128k', size: info[4] })
|
||||
_types['128k'] = {
|
||||
size: info[4].toLocaleUpperCase()
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -464,7 +488,7 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -472,13 +496,13 @@ export default {
|
||||
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||
tags,
|
||||
hotTag,
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}))
|
||||
},
|
||||
getDetailPageUrl(id) {
|
||||
if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
||||
else if (/^digest-/.test(id)) {
|
||||
const result = id.split('__')
|
||||
let result = id.split('__')
|
||||
id = result[1]
|
||||
}
|
||||
return `http://www.kuwo.cn/playlist_detail/${id}`
|
||||
@@ -486,7 +510,9 @@ export default {
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
return httpFetch(
|
||||
`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${page - 1}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`
|
||||
`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${
|
||||
page - 1
|
||||
}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`
|
||||
).promise.then(({ body }) => {
|
||||
body = objStr2JSON(body)
|
||||
// console.log(body)
|
||||
@@ -501,17 +527,17 @@ export default {
|
||||
// time: item.publish_time,
|
||||
img: item.pic,
|
||||
desc: decodeName(item.intro),
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: parseInt(body.TOTAL),
|
||||
source: 'kw'
|
||||
source: 'kw',
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
// getTags
|
||||
// getListDetail
|
||||
// getListDetail
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { sizeFormate, formatPlayTime } from '../index'
|
||||
import { sizeFormate, formatPlayTime } from '../../index'
|
||||
import { toMD5, formatSingerName } from '../utils'
|
||||
|
||||
export const createSignature = (time, str) => {
|
||||
@@ -17,100 +17,6 @@ export default {
|
||||
page: 0,
|
||||
allPage: 1,
|
||||
|
||||
// 旧版API
|
||||
// musicSearch(str, page, limit) {
|
||||
// const searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, {
|
||||
// searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, {
|
||||
// searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
|
||||
// searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
|
||||
// // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
|
||||
// headers: {
|
||||
// // sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
|
||||
// // timestamp: 1578225871982,
|
||||
// // appId: 'yyapp2',
|
||||
// // mode: 'android',
|
||||
// // ua: 'Android_migu',
|
||||
// // version: '6.9.4',
|
||||
// osVersion: 'android 7.0',
|
||||
// 'User-Agent': 'okhttp/3.9.1',
|
||||
// },
|
||||
// })
|
||||
// // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
|
||||
// return searchRequest.promise.then(({ body }) => body)
|
||||
// },
|
||||
// handleResult(rawData) {
|
||||
// // console.log(rawData)
|
||||
// let ids = new Set()
|
||||
// const list = []
|
||||
// rawData.forEach(item => {
|
||||
// if (ids.has(item.id)) return
|
||||
// ids.add(item.id)
|
||||
// const types = []
|
||||
// const _types = {}
|
||||
// item.newRateFormats && item.newRateFormats.forEach(type => {
|
||||
// let size
|
||||
// switch (type.formatType) {
|
||||
// case 'PQ':
|
||||
// size = sizeFormate(type.size ?? type.androidSize)
|
||||
// types.push({ type: '128k', size })
|
||||
// _types['128k'] = {
|
||||
// size,
|
||||
// }
|
||||
// break
|
||||
// case 'HQ':
|
||||
// size = sizeFormate(type.size ?? type.androidSize)
|
||||
// types.push({ type: '320k', size })
|
||||
// _types['320k'] = {
|
||||
// size,
|
||||
// }
|
||||
// break
|
||||
// case 'SQ':
|
||||
// size = sizeFormate(type.size ?? type.androidSize)
|
||||
// types.push({ type: 'flac', size })
|
||||
// _types.flac = {
|
||||
// size,
|
||||
// }
|
||||
// break
|
||||
// case 'ZQ':
|
||||
// size = sizeFormate(type.size ?? type.androidSize)
|
||||
// types.push({ type: 'flac24bit', size })
|
||||
// _types.flac24bit = {
|
||||
// size,
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// })
|
||||
|
||||
// const albumNInfo = item.albums && item.albums.length
|
||||
// ? {
|
||||
// id: item.albums[0].id,
|
||||
// name: item.albums[0].name,
|
||||
// }
|
||||
// : {}
|
||||
|
||||
// list.push({
|
||||
// singer: this.getSinger(item.singers),
|
||||
// name: item.name,
|
||||
// albumName: albumNInfo.name,
|
||||
// albumId: albumNInfo.id,
|
||||
// songmid: item.songId,
|
||||
// copyrightId: item.copyrightId,
|
||||
// source: 'mg',
|
||||
// interval: null,
|
||||
// img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null,
|
||||
// lrc: null,
|
||||
// lrcUrl: item.lyricUrl,
|
||||
// mrcUrl: item.mrcurl,
|
||||
// trcUrl: item.trcUrl,
|
||||
// otherSource: null,
|
||||
// types,
|
||||
// _types,
|
||||
// typeUrl: {},
|
||||
// })
|
||||
// })
|
||||
// return list
|
||||
// },
|
||||
|
||||
musicSearch(str, page, limit) {
|
||||
const time = Date.now().toString()
|
||||
const signData = createSignature(time, str)
|
||||
@@ -124,8 +30,8 @@ export default {
|
||||
sign: signData.sign,
|
||||
channel: '0146921',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'
|
||||
}
|
||||
'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
},
|
||||
}
|
||||
)
|
||||
return searchRequest.promise.then(({ body }) => body)
|
||||
@@ -150,28 +56,28 @@ export default {
|
||||
size = sizeFormate(type.asize ?? type.isize)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
break
|
||||
case 'HQ':
|
||||
size = sizeFormate(type.asize ?? type.isize)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
break
|
||||
case 'SQ':
|
||||
size = sizeFormate(type.asize ?? type.isize)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
break
|
||||
case 'ZQ24':
|
||||
size = sizeFormate(type.asize ?? type.isize)
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
types.push({ type: 'hires', size })
|
||||
_types.hires = {
|
||||
size,
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -196,7 +102,7 @@ export default {
|
||||
trcUrl: data.trcUrl,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -212,7 +118,7 @@ export default {
|
||||
return Promise.reject(new Error(result ? result.info : '搜索失败'))
|
||||
const songResultData = result.songResultData || { resultList: [], totalCount: 0 }
|
||||
|
||||
const list = this.filterData(songResultData.resultList)
|
||||
let list = this.filterData(songResultData.resultList)
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = parseInt(songResultData.totalCount)
|
||||
@@ -224,8 +130,8 @@ export default {
|
||||
allPage: this.allPage,
|
||||
limit,
|
||||
total: this.total,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { dateFormat, formatPlayCount } from '../index'
|
||||
import { dateFormat, formatPlayCount } from '../../index'
|
||||
import { filterMusicInfoList } from './musicInfo'
|
||||
import { createSignature } from './musicSearch'
|
||||
import { createHttpFetch } from './utils/index'
|
||||
@@ -17,14 +17,14 @@ export default {
|
||||
sortList: [
|
||||
{
|
||||
name: '推荐',
|
||||
id: '15127315'
|
||||
id: '15127315',
|
||||
// id: '1',
|
||||
},
|
||||
{
|
||||
name: '最新',
|
||||
id: '15127272'
|
||||
id: '15127272',
|
||||
// id: '2',
|
||||
}
|
||||
},
|
||||
],
|
||||
regExps: {
|
||||
list: /<li><div class="thumb">.+?<\/li>/g,
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
/.+data-original="(.+?)".*data-id="(\d+)".*<div class="song-list-name"><a\s.*?>(.+?)<\/a>.+<i class="iconfont cf-bofangliang"><\/i>(.+?)<\/div>/,
|
||||
|
||||
// https://music.migu.cn/v3/music/playlist/161044573?page=1
|
||||
listDetailLink: /^.+\/playlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/
|
||||
listDetailLink: /^.+\/playlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/,
|
||||
},
|
||||
tagsUrl: 'https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/musiclistplaza-taglist/release',
|
||||
// tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0',
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
defaultHeaders: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
|
||||
Referer: 'https://m.music.migu.cn/'
|
||||
Referer: 'https://m.music.migu.cn/',
|
||||
// language: 'Chinese',
|
||||
// ua: 'Android_migu',
|
||||
// mode: 'android',
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
} else if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
||||
|
||||
const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), {
|
||||
headers: this.defaultHeaders
|
||||
headers: this.defaultHeaders,
|
||||
})
|
||||
return requestObj_listDetail.promise.then(({ body }) => {
|
||||
if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
page,
|
||||
limit: this.limit_song,
|
||||
total: body.totalCount,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
const requestObj_listDetailInfo = httpFetch(
|
||||
`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`,
|
||||
{
|
||||
headers: this.defaultHeaders
|
||||
headers: this.defaultHeaders,
|
||||
}
|
||||
)
|
||||
return requestObj_listDetailInfo.promise.then(({ body }) => {
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
img: body.data.imgItem.img,
|
||||
desc: body.data.summary,
|
||||
author: body.data.ownerName,
|
||||
play_count: formatPlayCount(body.data.opNumItem.playNum)
|
||||
play_count: formatPlayCount(body.data.opNumItem.playNum),
|
||||
})
|
||||
return cachedDetailInfo
|
||||
})
|
||||
@@ -122,12 +122,12 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
||||
Referer: link
|
||||
}
|
||||
Referer: link,
|
||||
},
|
||||
})
|
||||
const {
|
||||
headers: { location },
|
||||
statusCode
|
||||
statusCode,
|
||||
} = await requestObj_listDetailLink.promise
|
||||
// console.log(body, location)
|
||||
if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
|
||||
return Promise.all([
|
||||
this.getListDetailList(id, page, retryNum),
|
||||
this.getListDetailInfo(id, retryNum)
|
||||
this.getListDetailInfo(id, retryNum),
|
||||
]).then(([listData, info]) => {
|
||||
listData.info = info
|
||||
return listData
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {
|
||||
headers: this.defaultHeaders
|
||||
headers: this.defaultHeaders,
|
||||
// headers: {
|
||||
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
|
||||
// timestamp: 1578225871982,
|
||||
@@ -205,7 +205,7 @@ export default {
|
||||
total: parseInt(body.retMsg.countSize),
|
||||
page,
|
||||
limit: this.limit_list,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}
|
||||
})
|
||||
// return this._requestObj_list.promise.then(({ body }) => {
|
||||
@@ -233,7 +233,7 @@ export default {
|
||||
grade: item.grade,
|
||||
total: item.contentCount,
|
||||
desc: item.summary,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -254,7 +254,7 @@ export default {
|
||||
hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({
|
||||
id,
|
||||
name,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
})),
|
||||
tags: rawList.slice(1).map(({ header, content }) => ({
|
||||
name: header.title,
|
||||
@@ -263,10 +263,10 @@ export default {
|
||||
// parent_name: objectInfo.columnTitle,
|
||||
id,
|
||||
name,
|
||||
source: 'mg'
|
||||
}))
|
||||
source: 'mg',
|
||||
})),
|
||||
})),
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}
|
||||
// return {
|
||||
// hotTag: rawList[0].objectInfo.contents.map(item => ({
|
||||
@@ -313,7 +313,7 @@ export default {
|
||||
name: item.name,
|
||||
img: item.musicListPicUrl,
|
||||
total: item.musicNum,
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
})
|
||||
})
|
||||
return list
|
||||
@@ -331,8 +331,8 @@ export default {
|
||||
sign: signResult.sign,
|
||||
channel: '0146921',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'
|
||||
}
|
||||
'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
},
|
||||
}
|
||||
).then((body) => {
|
||||
if (!body.songListResultData) throw new Error('get song list faild.')
|
||||
@@ -342,12 +342,12 @@ export default {
|
||||
list,
|
||||
limit,
|
||||
total: parseInt(body.songListResultData.totalCount),
|
||||
source: 'mg'
|
||||
source: 'mg',
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
// getTags
|
||||
// getListDetail
|
||||
// getListDetail
|
||||
@@ -1,8 +1,7 @@
|
||||
export const bHh = '624868746c'
|
||||
|
||||
export const headers = {
|
||||
'User-Agent': 'lx-music request',
|
||||
[bHh]: [bHh]
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
}
|
||||
|
||||
export const timeout = 15000
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, sizeFormate } from '../index'
|
||||
import { formatPlayTime, sizeFormate } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
|
||||
export default {
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
const searchRequest = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)'
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
},
|
||||
body: {
|
||||
comm: {
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
phonetype: '0',
|
||||
devicelevel: '31',
|
||||
tmeAppID: 'qqmusiclight',
|
||||
nettype: 'NETWORK_WIFI'
|
||||
nettype: 'NETWORK_WIFI',
|
||||
},
|
||||
req: {
|
||||
module: 'music.search.SearchCgiService',
|
||||
@@ -37,10 +37,10 @@ export default {
|
||||
num_per_page: limit,
|
||||
page_num: page,
|
||||
nqc_flag: 0,
|
||||
grp: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
grp: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)
|
||||
return searchRequest.promise.then(({ body }) => {
|
||||
@@ -56,35 +56,56 @@ export default {
|
||||
rawList.forEach((item) => {
|
||||
if (!item.file?.media_mid) return
|
||||
|
||||
const types = []
|
||||
const _types = {}
|
||||
let types = []
|
||||
let _types = {}
|
||||
const file = item.file
|
||||
if (file.size_128mp3 != 0) {
|
||||
const size = sizeFormate(file.size_128mp3)
|
||||
let size = sizeFormate(file.size_128mp3)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_320mp3 !== 0) {
|
||||
const size = sizeFormate(file.size_320mp3)
|
||||
let size = sizeFormate(file.size_320mp3)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_flac !== 0) {
|
||||
const size = sizeFormate(file.size_flac)
|
||||
let size = sizeFormate(file.size_flac)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_hires !== 0) {
|
||||
const size = sizeFormate(file.size_hires)
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
let size = sizeFormate(file.size_hires)
|
||||
types.push({ type: 'hires', size })
|
||||
_types.hires = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_new[1] !== 0) {
|
||||
let size = sizeFormate(file.size_new[1])
|
||||
types.push({ type: 'atmos', size })
|
||||
_types.atmos = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_new[2] !== 0) {
|
||||
let size = sizeFormate(file.size_new[2])
|
||||
types.push({ type: 'atmos_plus', size })
|
||||
_types.atmos_plus = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (file.size_new[0] !== 0) {
|
||||
let size = sizeFormate(file.size_new[0])
|
||||
types.push({ type: 'master', size })
|
||||
_types.master = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
// types.reverse()
|
||||
@@ -113,7 +134,7 @@ export default {
|
||||
: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
})
|
||||
})
|
||||
// console.log(list)
|
||||
@@ -123,7 +144,7 @@ export default {
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then(({ body, meta }) => {
|
||||
const list = this.handleResult(body.item_song)
|
||||
let list = this.handleResult(body.item_song)
|
||||
|
||||
this.total = meta.estimate_sum
|
||||
this.page = page
|
||||
@@ -134,8 +155,8 @@ export default {
|
||||
allPage: this.allPage,
|
||||
limit,
|
||||
total: this.total,
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
86
src/main/utils/musicSdk/tx/quality_detail.js
Normal file
86
src/main/utils/musicSdk/tx/quality_detail.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { sizeFormate } from '../../index'
|
||||
|
||||
export const getBatchMusicQualityInfo = (songList) => {
|
||||
const songIds = songList.map((item) => item.id)
|
||||
|
||||
const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
},
|
||||
body: {
|
||||
comm: {
|
||||
ct: '19',
|
||||
cv: '1859',
|
||||
uin: '0',
|
||||
},
|
||||
req: {
|
||||
module: 'music.trackInfo.UniformRuleCtrl',
|
||||
method: 'CgiGetTrackInfo',
|
||||
param: {
|
||||
types: Array(songIds.length).fill(1),
|
||||
ids: songIds,
|
||||
ctx: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const qualityInfoMap = {}
|
||||
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode != 200 || body.code != 0) return Promise.reject(new Error('获取音质信息失败'))
|
||||
|
||||
// Process each track from the response
|
||||
body.req.data.tracks.forEach((track) => {
|
||||
const file = track.file
|
||||
const songId = track.id
|
||||
|
||||
const types = []
|
||||
const _types = {}
|
||||
|
||||
if (file.size_128mp3 != 0) {
|
||||
let size = sizeFormate(file.size_128mp3)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = { size }
|
||||
}
|
||||
if (file.size_320mp3 !== 0) {
|
||||
let size = sizeFormate(file.size_320mp3)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = { size }
|
||||
}
|
||||
if (file.size_flac !== 0) {
|
||||
let size = sizeFormate(file.size_flac)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = { size }
|
||||
}
|
||||
if (file.size_hires !== 0) {
|
||||
let size = sizeFormate(file.size_hires)
|
||||
types.push({ type: 'hires', size })
|
||||
_types.hires = { size }
|
||||
}
|
||||
if (file.size_new[1] !== 0) {
|
||||
let size = sizeFormate(file.size_new[1])
|
||||
types.push({ type: 'atmos', size })
|
||||
_types.atmos = { size }
|
||||
}
|
||||
if (file.size_new[2] !== 0) {
|
||||
let size = sizeFormate(file.size_new[2])
|
||||
types.push({ type: 'atmos_plus', size })
|
||||
_types.atmos_plus = { size }
|
||||
}
|
||||
if (file.size_new[0] !== 0) {
|
||||
let size = sizeFormate(file.size_new[0])
|
||||
types.push({ type: 'master', size })
|
||||
_types.master = { size }
|
||||
}
|
||||
|
||||
qualityInfoMap[songId] = { types, _types }
|
||||
})
|
||||
|
||||
return qualityInfoMap
|
||||
})
|
||||
|
||||
return requestObj
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
||||
import { decodeName, formatPlayTime, dateFormat, formatPlayCount } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||
|
||||
export default {
|
||||
_requestObj_tags: null,
|
||||
@@ -12,12 +13,12 @@ export default {
|
||||
sortList: [
|
||||
{
|
||||
name: '最热',
|
||||
id: 5
|
||||
id: 5,
|
||||
},
|
||||
{
|
||||
name: '最新',
|
||||
id: 2
|
||||
}
|
||||
id: 2,
|
||||
},
|
||||
],
|
||||
regExps: {
|
||||
hotTagHtml: /class="c_bg_link js_tag_item" data-id="\w+">.+?<\/a>/g,
|
||||
@@ -26,7 +27,7 @@ export default {
|
||||
// https://y.qq.com/n/yqq/playlist/7217720898.html
|
||||
// https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=9050006&id=7217720898&ADTAG=qfshare
|
||||
listDetailLink: /\/playlist\/(\d+)/,
|
||||
listDetailLink2: /id=(\d+)/
|
||||
listDetailLink2: /id=(\d+)/,
|
||||
},
|
||||
tagsUrl:
|
||||
'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=%7B%22tags%22%3A%7B%22method%22%3A%22get_all_categories%22%2C%22param%22%3A%7B%22qq%22%3A%22%22%7D%2C%22module%22%3A%22playlist.PlaylistAllCategoriesServer%22%7D%7D',
|
||||
@@ -45,10 +46,10 @@ export default {
|
||||
category_id: id,
|
||||
size: this.limit_list,
|
||||
page: page - 1,
|
||||
use_page: 1
|
||||
use_page: 1,
|
||||
},
|
||||
module: 'playlist.PlayListCategoryServer'
|
||||
}
|
||||
module: 'playlist.PlayListCategoryServer',
|
||||
},
|
||||
})
|
||||
)}`
|
||||
}
|
||||
@@ -62,10 +63,10 @@ export default {
|
||||
sin: this.limit_list * (page - 1),
|
||||
size: this.limit_list,
|
||||
order: sortId,
|
||||
cur_page: page
|
||||
cur_page: page,
|
||||
},
|
||||
module: 'playlist.PlayListPlazaServer'
|
||||
}
|
||||
module: 'playlist.PlayListPlazaServer',
|
||||
},
|
||||
})
|
||||
)}`
|
||||
},
|
||||
@@ -95,17 +96,17 @@ export default {
|
||||
})
|
||||
},
|
||||
filterInfoHotTag(html) {
|
||||
const hotTag = html.match(this.regExps.hotTagHtml)
|
||||
let hotTag = html.match(this.regExps.hotTagHtml)
|
||||
const hotTags = []
|
||||
if (!hotTag) return hotTags
|
||||
|
||||
hotTag.forEach((tagHtml) => {
|
||||
const result = tagHtml.match(this.regExps.hotTag)
|
||||
let result = tagHtml.match(this.regExps.hotTag)
|
||||
if (!result) return
|
||||
hotTags.push({
|
||||
id: parseInt(result[1]),
|
||||
name: result[2],
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
})
|
||||
})
|
||||
return hotTags
|
||||
@@ -118,8 +119,8 @@ export default {
|
||||
parent_name: type.group_name,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
source: 'tx'
|
||||
}))
|
||||
source: 'tx',
|
||||
})),
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -130,7 +131,9 @@ export default {
|
||||
this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page))
|
||||
// console.log(this.getListUrl(sortId, tagId, page))
|
||||
return this._requestObj_list.promise.then(({ body }) => {
|
||||
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
|
||||
if (body.code !== this.successCode) {
|
||||
return this.getList(sortId, tagId, page, ++tryNum)
|
||||
}
|
||||
return tagId
|
||||
? this.filterList2(body.playlist.data, page)
|
||||
: this.filterList(body.playlist.data, page)
|
||||
@@ -149,12 +152,12 @@ export default {
|
||||
// grade: item.favorcnt / 10,
|
||||
total: item.song_ids?.length,
|
||||
desc: decodeName(item.desc).replace(/<br>/g, '\n'),
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
})),
|
||||
total: data.total,
|
||||
page,
|
||||
limit: this.limit_list,
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
}
|
||||
},
|
||||
filterList2({ content }, page) {
|
||||
@@ -169,12 +172,12 @@ export default {
|
||||
img: basic.cover.medium_url || basic.cover.default_url,
|
||||
// grade: basic.favorcnt / 10,
|
||||
desc: decodeName(basic.desc).replace(/<br>/g, '\n'),
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
})),
|
||||
total: content.total_cnt,
|
||||
page,
|
||||
limit: this.limit_list,
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,7 +187,7 @@ export default {
|
||||
const requestObj_listDetailLink = httpFetch(link)
|
||||
const {
|
||||
headers: { location },
|
||||
statusCode
|
||||
statusCode,
|
||||
} = await requestObj_listDetailLink.promise
|
||||
// console.log(headers)
|
||||
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
||||
@@ -202,7 +205,6 @@ export default {
|
||||
if (!result) throw new Error('failed')
|
||||
}
|
||||
id = result[1]
|
||||
// console.log(id)
|
||||
}
|
||||
return id
|
||||
},
|
||||
@@ -215,15 +217,16 @@ export default {
|
||||
const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), {
|
||||
headers: {
|
||||
Origin: 'https://y.qq.com',
|
||||
Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`
|
||||
}
|
||||
Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`,
|
||||
},
|
||||
})
|
||||
const { body } = await requestObj_listDetail.promise
|
||||
|
||||
console.log(body);
|
||||
|
||||
if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)
|
||||
const cdlist = body.cdlist[0]
|
||||
return {
|
||||
list: this.filterListDetail(cdlist.songlist),
|
||||
list: await this.filterListDetail(cdlist.songlist),
|
||||
page: 1,
|
||||
limit: cdlist.songlist.length + 1,
|
||||
total: cdlist.songlist.length,
|
||||
@@ -233,44 +236,23 @@ export default {
|
||||
img: cdlist.logo,
|
||||
desc: decodeName(cdlist.desc).replace(/<br>/g, '\n'),
|
||||
author: cdlist.nickname,
|
||||
play_count: formatPlayCount(cdlist.visitnum)
|
||||
}
|
||||
play_count: formatPlayCount(cdlist.visitnum),
|
||||
},
|
||||
}
|
||||
},
|
||||
filterListDetail(rawList) {
|
||||
// console.log(rawList)
|
||||
async filterListDetail(rawList) {
|
||||
const qualityInfoRequest = getBatchMusicQualityInfo(rawList)
|
||||
let qualityInfoMap = {}
|
||||
|
||||
try {
|
||||
qualityInfoMap = await qualityInfoRequest.promise
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch quality info:', error)
|
||||
}
|
||||
|
||||
return rawList.map((item) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
if (item.file.size_128mp3 !== 0) {
|
||||
const size = sizeFormate(item.file.size_128mp3)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
}
|
||||
}
|
||||
if (item.file.size_320mp3 !== 0) {
|
||||
const size = sizeFormate(item.file.size_320mp3)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
}
|
||||
}
|
||||
if (item.file.size_flac !== 0) {
|
||||
const size = sizeFormate(item.file.size_flac)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
}
|
||||
}
|
||||
if (item.file.size_hires !== 0) {
|
||||
const size = sizeFormate(item.file.size_hires)
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
}
|
||||
}
|
||||
// types.reverse()
|
||||
const { types = [], _types = {} } = qualityInfoMap[item.id] || {}
|
||||
|
||||
return {
|
||||
singer: formatSingerName(item.singer, 'name'),
|
||||
name: item.title,
|
||||
@@ -292,7 +274,7 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -300,7 +282,7 @@ export default {
|
||||
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||
tags,
|
||||
hotTag,
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -313,12 +295,16 @@ export default {
|
||||
search(text, page, limit = 20, retryNum = 0) {
|
||||
if (retryNum > 5) throw new Error('max retry')
|
||||
return httpFetch(
|
||||
`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${page - 1}&num_per_page=${limit}&format=json&query=${encodeURIComponent(text)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`,
|
||||
`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${
|
||||
page - 1
|
||||
}&num_per_page=${limit}&format=json&query=${encodeURIComponent(
|
||||
text
|
||||
)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
Referer: 'http://y.qq.com/portal/search.html'
|
||||
}
|
||||
Referer: 'http://y.qq.com/portal/search.html',
|
||||
},
|
||||
}
|
||||
).promise.then(({ body }) => {
|
||||
if (body.code != 0) return this.search(text, page, limit, ++retryNum)
|
||||
@@ -335,17 +321,17 @@ export default {
|
||||
// grade: item.favorcnt / 10,
|
||||
total: item.song_count,
|
||||
desc: decodeName(decodeName(item.introduction)).replace(/<br>/g, '\n'),
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: body.data.sum,
|
||||
source: 'tx'
|
||||
source: 'tx',
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
// getTags
|
||||
// getListDetail
|
||||
// getListDetail
|
||||
@@ -10,16 +10,13 @@ export const getHostIp = (hostname) => {
|
||||
if (typeof result === 'object') return result
|
||||
if (result === true) return
|
||||
ipMap.set(hostname, true)
|
||||
// console.log(hostname)
|
||||
dns.lookup(
|
||||
hostname,
|
||||
{
|
||||
// family: 4,
|
||||
all: false
|
||||
all: false,
|
||||
},
|
||||
(err, address, family) => {
|
||||
if (err) return console.log(err)
|
||||
// console.log(address, family)
|
||||
ipMap.set(hostname, { address, family })
|
||||
}
|
||||
)
|
||||
@@ -42,11 +39,11 @@ export const formatSingerName = (singers, nameKey = 'name', join = '、') => {
|
||||
if (Array.isArray(singers)) {
|
||||
const singer = []
|
||||
singers.forEach((item) => {
|
||||
const name = item[nameKey]
|
||||
let name = item[nameKey]
|
||||
if (!name) return
|
||||
singer.push(name)
|
||||
})
|
||||
return decodeName(singer.join(join))
|
||||
}
|
||||
return decodeName(String(singers ?? ''))
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,23 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { weapi } from './utils/crypto'
|
||||
import { formatPlayTime, sizeFormate } from '../index'
|
||||
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
|
||||
import { formatPlayTime } from '../../index'
|
||||
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||
|
||||
export default {
|
||||
getSinger(singers) {
|
||||
const arr = []
|
||||
let arr = []
|
||||
singers?.forEach((singer) => {
|
||||
arr.push(singer.name)
|
||||
})
|
||||
return arr.join('、')
|
||||
},
|
||||
filterList({ songs, privileges }) {
|
||||
// console.log(songs, privileges)
|
||||
async filterList({ songs, privileges }) {
|
||||
const list = []
|
||||
const idList = songs.map((item) => item.id)
|
||||
const qualityInfoMap = await getBatchMusicQualityInfo(idList)
|
||||
|
||||
songs.forEach((item, index) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
let size
|
||||
let privilege = privileges[index]
|
||||
if (privilege.id !== item.id) privilege = privileges.find((p) => p.id === item.id)
|
||||
if (!privilege) return
|
||||
|
||||
if (privilege.maxBrLevel == 'hires') {
|
||||
size = item.hr ? sizeFormate(item.hr.size) : null
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
}
|
||||
}
|
||||
switch (privilege.maxbr) {
|
||||
case 999000:
|
||||
size = item.sq ? sizeFormate(item.sq.size) : null
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
}
|
||||
case 320000:
|
||||
size = item.h ? sizeFormate(item.h.size) : null
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
}
|
||||
case 192000:
|
||||
case 128000:
|
||||
size = item.l ? sizeFormate(item.l.size) : null
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
types.reverse()
|
||||
const { types, _types } = qualityInfoMap[item.id] || { types: [], _types: {} }
|
||||
|
||||
if (item.pc) {
|
||||
list.push({
|
||||
@@ -67,7 +33,7 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
})
|
||||
} else {
|
||||
list.push({
|
||||
@@ -83,11 +49,10 @@ export default {
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
typeUrl: {},
|
||||
})
|
||||
}
|
||||
})
|
||||
// console.log(list)
|
||||
return list
|
||||
},
|
||||
async getList(ids = [], retryNum = 0) {
|
||||
@@ -98,16 +63,15 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
origin: 'https://music.163.com'
|
||||
origin: 'https://music.163.com',
|
||||
},
|
||||
form: weapi({
|
||||
c: '[' + ids.map((id) => '{"id":' + id + '}').join(',') + ']',
|
||||
ids: '[' + ids.join(',') + ']'
|
||||
})
|
||||
ids: '[' + ids.join(',') + ']',
|
||||
}),
|
||||
})
|
||||
const { body, statusCode } = await requestObj.promise
|
||||
if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')
|
||||
// console.log(body)
|
||||
return { source: 'wy', list: this.filterList(body) }
|
||||
}
|
||||
}
|
||||
return { source: 'wy', list: await this.filterList(body) }
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
// import { httpFetch } from '../../request'
|
||||
// import { weapi } from './utils/crypto'
|
||||
import { sizeFormate, formatPlayTime } from '../index'
|
||||
// import musicDetailApi from './musicDetail'
|
||||
import { httpFetch } from '../../request'
|
||||
import { sizeFormate, formatPlayTime } from '../../index'
|
||||
import { eapiRequest } from './utils/index'
|
||||
|
||||
export default {
|
||||
@@ -9,101 +7,129 @@ export default {
|
||||
total: 0,
|
||||
page: 0,
|
||||
allPage: 1,
|
||||
|
||||
musicSearch(str, page, limit) {
|
||||
const searchRequest = eapiRequest('/api/cloudsearch/pc', {
|
||||
s: str,
|
||||
type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||
limit,
|
||||
total: page == 1,
|
||||
offset: limit * (page - 1)
|
||||
offset: limit * (page - 1),
|
||||
})
|
||||
return searchRequest.promise.then(({ body }) => body)
|
||||
},
|
||||
|
||||
getSinger(singers) {
|
||||
const arr = []
|
||||
singers.forEach((singer) => {
|
||||
arr.push(singer.name)
|
||||
})
|
||||
return arr.join('、')
|
||||
return singers.map((singer) => singer.name).join('、')
|
||||
},
|
||||
|
||||
handleResult(rawList) {
|
||||
// console.log(rawList)
|
||||
if (!rawList) return []
|
||||
return rawList.map((item) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
let size
|
||||
|
||||
if (item.privilege.maxBrLevel == 'hires') {
|
||||
size = item.hr ? sizeFormate(item.hr.size) : null
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
return Promise.all(
|
||||
rawList.map(async (item) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
let size
|
||||
|
||||
try {
|
||||
const requestObj = httpFetch(
|
||||
`https://music.163.com/api/song/music/detail/get?songId=${item.id}`,
|
||||
{
|
||||
method: 'get',
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
origin: 'https://music.163.com',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { body, statusCode } = await requestObj.promise
|
||||
|
||||
if (statusCode !== 200 || !body || body.code !== 200) {
|
||||
throw new Error('Failed to get song quality information')
|
||||
}
|
||||
|
||||
if (body.data.jm && body.data.jm.size) {
|
||||
size = sizeFormate(body.data.jm.size)
|
||||
types.push({ type: 'master', size })
|
||||
_types.master = { size }
|
||||
}
|
||||
if (body.data.db && body.data.db.size) {
|
||||
size = sizeFormate(body.data.db.size)
|
||||
types.push({ type: 'dolby', size })
|
||||
_types.dolby = { size }
|
||||
}
|
||||
if (body.data.hr && body.data.hr.size) {
|
||||
size = sizeFormate(body.data.hr.size)
|
||||
types.push({ type: 'hires', size })
|
||||
_types.hires = { size }
|
||||
}
|
||||
if (body.data.sq && body.data.sq.size) {
|
||||
size = sizeFormate(body.data.sq.size)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = { size }
|
||||
}
|
||||
if (body.data.h && body.data.h.size) {
|
||||
size = sizeFormate(body.data.h.size)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = { size }
|
||||
}
|
||||
if (body.data.m && body.data.m.size) {
|
||||
size = sizeFormate(body.data.m.size)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = { size }
|
||||
} else if (body.data.l && body.data.l.size) {
|
||||
size = sizeFormate(body.data.l.size)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = { size }
|
||||
}
|
||||
|
||||
types.reverse()
|
||||
|
||||
return {
|
||||
singer: this.getSinger(item.ar),
|
||||
name: item.name,
|
||||
albumName: item.al.name,
|
||||
albumId: item.al.id,
|
||||
source: 'wy',
|
||||
interval: formatPlayTime(item.dt / 1000),
|
||||
songmid: item.id,
|
||||
img: item.al.picUrl,
|
||||
lrc: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
switch (item.privilege.maxbr) {
|
||||
case 999000:
|
||||
size = item.sq ? sizeFormate(item.sq.size) : null
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
}
|
||||
case 320000:
|
||||
size = item.h ? sizeFormate(item.h.size) : null
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
}
|
||||
case 192000:
|
||||
case 128000:
|
||||
size = item.l ? sizeFormate(item.l.size) : null
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
types.reverse()
|
||||
|
||||
return {
|
||||
singer: this.getSinger(item.ar),
|
||||
name: item.name,
|
||||
albumName: item.al.name,
|
||||
albumId: item.al.id,
|
||||
source: 'wy',
|
||||
interval: formatPlayTime(item.dt / 1000),
|
||||
songmid: item.id,
|
||||
img: item.al.picUrl,
|
||||
lrc: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
return this.musicSearch(str, page, limit).then((result) => {
|
||||
// console.log(result)
|
||||
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
|
||||
const list = this.handleResult(result.result.songs || [])
|
||||
// console.log(list)
|
||||
return this.handleResult(result.result.songs || []).then((list) => {
|
||||
if (!list || list.length === 0) return this.search(str, page, limit, retryNum)
|
||||
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
this.total = result.result.songCount || 0
|
||||
this.page = page
|
||||
this.allPage = Math.ceil(this.total / this.limit)
|
||||
|
||||
this.total = result.result.songCount || 0
|
||||
this.page = page
|
||||
this.allPage = Math.ceil(this.total / this.limit)
|
||||
|
||||
return {
|
||||
list,
|
||||
allPage: this.allPage,
|
||||
limit: this.limit,
|
||||
total: this.total,
|
||||
source: 'wy'
|
||||
}
|
||||
// return result.data
|
||||
return {
|
||||
list,
|
||||
allPage: this.allPage,
|
||||
limit: this.limit,
|
||||
total: this.total,
|
||||
source: 'wy',
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
91
src/main/utils/musicSdk/wy/quality_detail.js
Normal file
91
src/main/utils/musicSdk/wy/quality_detail.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { sizeFormate } from '../../index'
|
||||
|
||||
export const getMusicQualityInfo = (id) => {
|
||||
|
||||
const requestObj = httpFetch(`https://music.163.com/api/song/music/detail/get?songId=${id}`, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
origin: 'https://music.163.com',
|
||||
},
|
||||
})
|
||||
|
||||
const types = []
|
||||
const _types = {}
|
||||
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode != 200 && body.code != 200) return Promise.reject(new Error('获取音质信息失败' + id))
|
||||
|
||||
const data = body.data
|
||||
|
||||
types.length = 0
|
||||
Object.keys(_types).forEach((key) => delete _types[key])
|
||||
|
||||
if (data.l != null && data.l.size != null) {
|
||||
let size = sizeFormate(data.l.size)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = { size }
|
||||
} else if (data.m != null && data.m.size != null) {
|
||||
let size = sizeFormate(data.m.size)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = { size }
|
||||
}
|
||||
|
||||
if (data.h != null && data.h.size != null) {
|
||||
let size = sizeFormate(data.h.size)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = { size }
|
||||
}
|
||||
|
||||
if (data.sq != null && data.sq.size != null) {
|
||||
let size = sizeFormate(data.sq.size)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = { size }
|
||||
}
|
||||
|
||||
if (data.hr != null && data.hr.size != null) {
|
||||
let size = sizeFormate(data.hr.size)
|
||||
types.push({ type: 'hires', size })
|
||||
_types.hires = { size }
|
||||
}
|
||||
|
||||
if (data.jm != null && data.jm.size != null) {
|
||||
let size = sizeFormate(data.jm.size)
|
||||
types.push({ type: 'master', size })
|
||||
_types.master = { size }
|
||||
}
|
||||
|
||||
if (data.je != null && data.je.size != null) {
|
||||
let size = sizeFormate(data.je.size)
|
||||
types.push({ type: 'atmos', size })
|
||||
_types.atmos = { size }
|
||||
}
|
||||
|
||||
return { types: [...types], _types: { ..._types } }
|
||||
})
|
||||
|
||||
return { requestObj, types, _types }
|
||||
}
|
||||
|
||||
export const getBatchMusicQualityInfo = async (idList) => {
|
||||
const ids = idList.filter((id) => id)
|
||||
|
||||
const qualityPromises = ids.map((id) => {
|
||||
const result = getMusicQualityInfo(id)
|
||||
return result.requestObj.promise.catch((err) => {
|
||||
console.error(`获取歌曲 ${id} 音质信息失败:`, err)
|
||||
return { types: [], _types: {} }
|
||||
})
|
||||
})
|
||||
|
||||
const qualityResults = await Promise.all(qualityPromises)
|
||||
|
||||
const qualityInfoMap = {}
|
||||
ids.forEach((id, index) => {
|
||||
qualityInfoMap[id] = qualityResults[index] || { types: [], _types: {} }
|
||||
})
|
||||
|
||||
return qualityInfoMap
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { weapi, linuxapi } from './utils/crypto'
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
||||
import { /* formatPlayTime, */ dateFormat, formatPlayCount } from '../../index'
|
||||
import musicDetailApi from './musicDetail'
|
||||
import { eapiRequest } from './utils/index'
|
||||
import { formatSingerName } from '../utils'
|
||||
// import { formatSingerName } from '../utils'
|
||||
|
||||
export default {
|
||||
_requestObj_tags: null,
|
||||
@@ -16,16 +16,12 @@ export default {
|
||||
sortList: [
|
||||
{
|
||||
name: '最热',
|
||||
id: 'hot'
|
||||
}
|
||||
// {
|
||||
// name: '最新',
|
||||
// id: 'new',
|
||||
// },
|
||||
id: 'hot',
|
||||
},
|
||||
],
|
||||
regExps: {
|
||||
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
|
||||
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/
|
||||
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/,
|
||||
},
|
||||
|
||||
async handleParseId(link, retryNum = 0) {
|
||||
@@ -34,9 +30,8 @@ export default {
|
||||
const requestObj_listDetailLink = httpFetch(link)
|
||||
const {
|
||||
headers: { location },
|
||||
statusCode
|
||||
statusCode,
|
||||
} = await requestObj_listDetailLink.promise
|
||||
// console.log(statusCode)
|
||||
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
||||
const url = location == null ? link : location
|
||||
return this.regExps.listDetailLink.test(url)
|
||||
@@ -59,13 +54,11 @@ export default {
|
||||
} else {
|
||||
id = await this.handleParseId(id)
|
||||
}
|
||||
// console.log(id)
|
||||
}
|
||||
return { id, cookie }
|
||||
},
|
||||
async getListDetail(rawId, page, tryNum = 0) {
|
||||
// 获取歌曲列表内的音乐
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
if (tryNum > 1000) return Promise.reject(new Error('try max num'))
|
||||
|
||||
const { id, cookie } = await this.getListId(rawId)
|
||||
if (cookie) this.cookie = cookie
|
||||
@@ -75,7 +68,7 @@ export default {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
Cookie: this.cookie
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
form: linuxapi({
|
||||
method: 'POST',
|
||||
@@ -83,36 +76,30 @@ export default {
|
||||
params: {
|
||||
id,
|
||||
n: this.limit_song,
|
||||
s: 8
|
||||
}
|
||||
})
|
||||
s: 8,
|
||||
},
|
||||
}),
|
||||
})
|
||||
const { statusCode, body } = await requestObj_listDetail.promise
|
||||
if (statusCode !== 200 || body.code !== this.successCode)
|
||||
return this.getListDetail(id, page, ++tryNum)
|
||||
const limit = 1000
|
||||
const rangeStart = (page - 1) * limit
|
||||
// console.log(body)
|
||||
let limit = 50
|
||||
let rangeStart = (page - 1) * limit
|
||||
let list
|
||||
if (body.playlist.trackIds.length == body.privileges.length) {
|
||||
list = this.filterListDetail(body)
|
||||
} else {
|
||||
try {
|
||||
list = (
|
||||
await musicDetailApi.getList(
|
||||
body.playlist.trackIds.slice(rangeStart, limit * page).map((trackId) => trackId.id)
|
||||
)
|
||||
).list
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (err.message == 'try max num') {
|
||||
throw err
|
||||
} else {
|
||||
return this.getListDetail(id, page, ++tryNum)
|
||||
}
|
||||
try {
|
||||
list = (
|
||||
await musicDetailApi.getList(
|
||||
body.playlist.trackIds.slice(rangeStart, limit * page).map((trackId) => trackId.id)
|
||||
)
|
||||
).list
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (err.message == 'try max num') {
|
||||
throw err
|
||||
} else {
|
||||
return this.getListDetail(id, page, ++tryNum)
|
||||
}
|
||||
}
|
||||
// console.log(list)
|
||||
return {
|
||||
list,
|
||||
page,
|
||||
@@ -124,119 +111,79 @@ export default {
|
||||
name: body.playlist.name,
|
||||
img: body.playlist.coverImgUrl,
|
||||
desc: body.playlist.description,
|
||||
author: body.playlist.creator.nickname
|
||||
}
|
||||
author: body.playlist.creator.nickname,
|
||||
},
|
||||
}
|
||||
},
|
||||
filterListDetail({ playlist: { tracks }, privileges }) {
|
||||
// console.log(tracks, privileges)
|
||||
const list = []
|
||||
tracks.forEach((item, index) => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
let size
|
||||
let privilege = privileges[index]
|
||||
if (privilege.id !== item.id) privilege = privileges.find((p) => p.id === item.id)
|
||||
if (!privilege) return
|
||||
|
||||
if (privilege.maxBrLevel == 'hires') {
|
||||
size = item.hr ? sizeFormate(item.hr.size) : null
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size
|
||||
}
|
||||
}
|
||||
switch (privilege.maxbr) {
|
||||
case 999000:
|
||||
size = null
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size
|
||||
}
|
||||
// filterListDetail({ playlist: { tracks } }) {
|
||||
// const list = []
|
||||
// tracks.forEach((item) => {
|
||||
// const types = []
|
||||
// const _types = {}
|
||||
|
||||
case 320000:
|
||||
size = item.h ? sizeFormate(item.h.size) : null
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
}
|
||||
// if (item.pc) {
|
||||
// list.push({
|
||||
// singer: item.pc.ar ?? '',
|
||||
// name: item.pc.sn ?? '',
|
||||
// albumName: item.pc.alb ?? '',
|
||||
// albumId: item.al?.id,
|
||||
// source: 'wy',
|
||||
// interval: formatPlayTime(item.dt / 1000),
|
||||
// songmid: item.id,
|
||||
// img: item.al?.picUrl ?? '',
|
||||
// lrc: null,
|
||||
// otherSource: null,
|
||||
// types,
|
||||
// _types,
|
||||
// typeUrl: {},
|
||||
// })
|
||||
// } else {
|
||||
// list.push({
|
||||
// singer: formatSingerName(item.ar, 'name'),
|
||||
// name: item.name ?? '',
|
||||
// albumName: item.al?.name,
|
||||
// albumId: item.al?.id,
|
||||
// source: 'wy',
|
||||
// interval: formatPlayTime(item.dt / 1000),
|
||||
// songmid: item.id,
|
||||
// img: item.al?.picUrl,
|
||||
// lrc: null,
|
||||
// otherSource: null,
|
||||
// types,
|
||||
// _types,
|
||||
// typeUrl: {},
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// return list
|
||||
// },
|
||||
|
||||
case 192000:
|
||||
case 128000:
|
||||
size = item.l ? sizeFormate(item.l.size) : null
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
types.reverse()
|
||||
|
||||
if (item.pc) {
|
||||
list.push({
|
||||
singer: item.pc.ar ?? '',
|
||||
name: item.pc.sn ?? '',
|
||||
albumName: item.pc.alb ?? '',
|
||||
albumId: item.al?.id,
|
||||
source: 'wy',
|
||||
interval: formatPlayTime(item.dt / 1000),
|
||||
songmid: item.id,
|
||||
img: item.al?.picUrl ?? '',
|
||||
lrc: null,
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
})
|
||||
} else {
|
||||
list.push({
|
||||
singer: formatSingerName(item.ar, 'name'),
|
||||
name: item.name ?? '',
|
||||
albumName: item.al?.name,
|
||||
albumId: item.al?.id,
|
||||
source: 'wy',
|
||||
interval: formatPlayTime(item.dt / 1000),
|
||||
songmid: item.id,
|
||||
img: item.al?.picUrl,
|
||||
lrc: null,
|
||||
otherSource: null,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {}
|
||||
})
|
||||
}
|
||||
})
|
||||
return list
|
||||
},
|
||||
|
||||
// 获取列表数据
|
||||
getList(sortId, tagId, page, tryNum = 0) {
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
||||
this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {
|
||||
method: 'post',
|
||||
form: weapi({
|
||||
cat: tagId || '全部', // 全部,华语,欧美,日语,韩语,粤语,小语种,流行,摇滚,民谣,电子,舞曲,说唱,轻音乐,爵士,乡村,R&B/Soul,古典,民族,英伦,金属,朋克,蓝调,雷鬼,世界音乐,拉丁,另类/独立,New Age,古风,后摇,Bossa Nova,清晨,夜晚,学习,工作,午休,下午茶,地铁,驾车,运动,旅行,散步,酒吧,怀旧,清新,浪漫,性感,伤感,治愈,放松,孤独,感动,兴奋,快乐,安静,思念,影视原声,ACG,儿童,校园,游戏,70后,80后,90后,网络歌曲,KTV,经典,翻唱,吉他,钢琴,器乐,榜单,00后
|
||||
order: sortId, // hot,new
|
||||
cat: tagId || '全部',
|
||||
order: sortId,
|
||||
limit: this.limit_list,
|
||||
offset: this.limit_list * (page - 1),
|
||||
total: true
|
||||
})
|
||||
total: true,
|
||||
}),
|
||||
})
|
||||
return this._requestObj_list.promise.then(({ body }) => {
|
||||
// console.log(body)
|
||||
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
|
||||
return {
|
||||
list: this.filterList(body.playlists),
|
||||
total: parseInt(body.total),
|
||||
page,
|
||||
limit: this.limit_list,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
}
|
||||
})
|
||||
},
|
||||
filterList(rawData) {
|
||||
// console.log(rawData)
|
||||
return rawData.map((item) => ({
|
||||
play_count: formatPlayCount(item.playCount),
|
||||
id: String(item.id),
|
||||
@@ -247,20 +194,18 @@ export default {
|
||||
grade: item.grade,
|
||||
total: item.trackCount,
|
||||
desc: item.description,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
}))
|
||||
},
|
||||
|
||||
// 获取标签
|
||||
getTag(tryNum = 0) {
|
||||
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
this._requestObj_tags = httpFetch('https://music.163.com/weapi/playlist/catalogue', {
|
||||
method: 'post',
|
||||
form: weapi({})
|
||||
form: weapi({}),
|
||||
})
|
||||
return this._requestObj_tags.promise.then(({ body }) => {
|
||||
// console.log(JSON.stringify(body))
|
||||
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
||||
return this.filterTagInfo(body)
|
||||
})
|
||||
@@ -274,7 +219,7 @@ export default {
|
||||
parent_name: categories[item.category],
|
||||
id: item.name,
|
||||
name: item.name,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,22 +228,20 @@ export default {
|
||||
list.push({
|
||||
name: categories[key],
|
||||
list: subList[key],
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
})
|
||||
}
|
||||
return list
|
||||
},
|
||||
|
||||
// 获取热门标签
|
||||
getHotTag(tryNum = 0) {
|
||||
if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
this._requestObj_hotTags = httpFetch('https://music.163.com/weapi/playlist/hottags', {
|
||||
method: 'post',
|
||||
form: weapi({})
|
||||
form: weapi({}),
|
||||
})
|
||||
return this._requestObj_hotTags.promise.then(({ body }) => {
|
||||
// console.log(JSON.stringify(body))
|
||||
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
||||
return this.filterHotTagInfo(body.tags)
|
||||
})
|
||||
@@ -307,7 +250,7 @@ export default {
|
||||
return rawList.map((item) => ({
|
||||
id: item.playlistTag.name,
|
||||
name: item.playlistTag.name,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -315,7 +258,7 @@ export default {
|
||||
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||
tags,
|
||||
hotTag,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -327,23 +270,18 @@ export default {
|
||||
search(text, page, limit = 20) {
|
||||
return eapiRequest('/api/cloudsearch/pc', {
|
||||
s: text,
|
||||
type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||
type: 1000,
|
||||
limit,
|
||||
total: page == 1,
|
||||
offset: limit * (page - 1)
|
||||
offset: limit * (page - 1),
|
||||
}).promise.then(({ body }) => {
|
||||
if (body.code != this.successCode) throw new Error('filed')
|
||||
// console.log(body)
|
||||
return {
|
||||
list: this.filterList(body.result.playlists),
|
||||
limit,
|
||||
total: body.result.playlistCount,
|
||||
source: 'wy'
|
||||
source: 'wy',
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// getList
|
||||
// getTags
|
||||
// getListDetail
|
||||
},
|
||||
}
|
||||
45
src/main/windows/index.ts
Normal file
45
src/main/windows/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, app } from 'electron'
|
||||
import { windowsLog } from '../logger'
|
||||
import { join } from 'path'
|
||||
import icon from '../../../resources/logo.png?asset'
|
||||
|
||||
export const createWindow = (
|
||||
options: BrowserWindowConstructorOptions = {}
|
||||
): BrowserWindow | null => {
|
||||
try {
|
||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||
title: app.getName(),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frame: false, // 创建后是否显示窗口
|
||||
center: true, // 窗口居中
|
||||
icon, // 窗口图标
|
||||
autoHideMenuBar: true, // 隐藏菜单栏
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
// 禁用渲染器沙盒
|
||||
sandbox: false,
|
||||
// 禁用同源策略
|
||||
webSecurity: false,
|
||||
// 允许 HTTP
|
||||
allowRunningInsecureContent: true,
|
||||
// 禁用拼写检查
|
||||
spellcheck: false,
|
||||
// 启用 Node.js
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
// 关闭上下文隔离,确保在窗口中注入 window.electron
|
||||
contextIsolation: false,
|
||||
backgroundThrottling: false
|
||||
}
|
||||
}
|
||||
// 合并参数
|
||||
options = Object.assign(defaultOptions, options)
|
||||
// 创建窗口
|
||||
const win = new BrowserWindow(options)
|
||||
return win
|
||||
} catch (error) {
|
||||
windowsLog.error(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
92
src/main/windows/lyric-window.ts
Normal file
92
src/main/windows/lyric-window.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { BrowserWindow, screen } from 'electron'
|
||||
import { createWindow } from './index'
|
||||
import { configManager } from '../services/ConfigManager'
|
||||
import { join } from 'path'
|
||||
import { lyricConfig } from '@common/types/config'
|
||||
|
||||
const lyricStore = {
|
||||
get: () =>
|
||||
configManager.get<lyricConfig>('lyric', {
|
||||
fontSize: 30,
|
||||
mainColor: '#73BCFC',
|
||||
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 180
|
||||
}),
|
||||
set: (value: lyricConfig) => configManager.set<lyricConfig>('lyric', value)
|
||||
}
|
||||
|
||||
class LyricWindow {
|
||||
private win: BrowserWindow | null = null
|
||||
constructor() {}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return
|
||||
// 歌词窗口缩放
|
||||
this.win?.on('resized', () => {
|
||||
const bounds = this.win?.getBounds()
|
||||
if (bounds) {
|
||||
const { width, height } = bounds
|
||||
console.log('歌词窗口缩放:', width, height);
|
||||
|
||||
lyricStore.set({
|
||||
...lyricStore.get(),
|
||||
width,
|
||||
height
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 创建主窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
const { width, height, x, y } = lyricStore.get()
|
||||
this.win = createWindow({
|
||||
width: width || 800,
|
||||
height: height || 180,
|
||||
minWidth: 440,
|
||||
minHeight: 120,
|
||||
maxWidth: 1600,
|
||||
maxHeight: 300,
|
||||
show: false,
|
||||
// 窗口位置
|
||||
x,
|
||||
y,
|
||||
transparent: true,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
alwaysOnTop: true,
|
||||
resizable: true,
|
||||
movable: true,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false
|
||||
})
|
||||
if (!this.win) return null
|
||||
// 加载地址(开发环境用项目根目录,生产用打包后的相对路径)
|
||||
this.win.loadFile(join(__dirname, '../main/src/web/lyric.html'))
|
||||
// 窗口事件
|
||||
this.event()
|
||||
return this.win
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
return this.win
|
||||
}
|
||||
}
|
||||
|
||||
export default new LyricWindow()
|
||||
2
src/preload/index.d.ts
vendored
2
src/preload/index.d.ts
vendored
@@ -53,6 +53,8 @@ interface CustomAPI {
|
||||
validateIntegrity: (hashId: string) => Promise<any>
|
||||
repairData: (hashId: string) => Promise<any>
|
||||
forceSave: (hashId: string) => Promise<any>
|
||||
getFavoritesId: () => Promise<any>
|
||||
setFavoritesId: (favoritesId: string) => Promise<any>
|
||||
}
|
||||
|
||||
ai: {
|
||||
|
||||
@@ -8,6 +8,11 @@ const api = {
|
||||
console.log('preload: 发送 window-minimize 事件')
|
||||
ipcRenderer.send('window-minimize')
|
||||
},
|
||||
// 阻止系统息屏
|
||||
powerSaveBlocker: {
|
||||
start: () => ipcRenderer.invoke('power-save-blocker:start'),
|
||||
stop: () => ipcRenderer.invoke('power-save-blocker:stop')
|
||||
},
|
||||
maximize: () => {
|
||||
console.log('preload: 发送 window-maximize 事件')
|
||||
ipcRenderer.send('window-maximize')
|
||||
@@ -111,7 +116,11 @@ const api = {
|
||||
validateIntegrity: (hashId: string) =>
|
||||
ipcRenderer.invoke('songlist:validate-integrity', hashId),
|
||||
repairData: (hashId: string) => ipcRenderer.invoke('songlist:repair-data', hashId),
|
||||
forceSave: (hashId: string) => ipcRenderer.invoke('songlist:force-save', hashId)
|
||||
forceSave: (hashId: string) => ipcRenderer.invoke('songlist:force-save', hashId),
|
||||
|
||||
// 喜欢歌单ID持久化
|
||||
getFavoritesId: () => ipcRenderer.invoke('songlist:get-favorites-id'),
|
||||
setFavoritesId: (id: string) => ipcRenderer.invoke('songlist:set-favorites-id', id)
|
||||
},
|
||||
|
||||
getUserConfig: () => ipcRenderer.invoke('get-user-config'),
|
||||
@@ -198,14 +207,14 @@ const api = {
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ipcRenderer })
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI
|
||||
window.electron = { ...electronAPI, ipcRenderer }
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
|
||||
144
src/renderer/auto-imports.d.ts
vendored
144
src/renderer/auto-imports.d.ts
vendored
@@ -7,90 +7,72 @@
|
||||
export {}
|
||||
declare global {
|
||||
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
||||
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||
const computed: (typeof import('vue'))['computed']
|
||||
const createApp: (typeof import('vue'))['createApp']
|
||||
const customRef: (typeof import('vue'))['customRef']
|
||||
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||
const effectScope: (typeof import('vue'))['effectScope']
|
||||
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||
const getCurrentWatcher: (typeof import('vue'))['getCurrentWatcher']
|
||||
const h: (typeof import('vue'))['h']
|
||||
const inject: (typeof import('vue'))['inject']
|
||||
const isProxy: (typeof import('vue'))['isProxy']
|
||||
const isReactive: (typeof import('vue'))['isReactive']
|
||||
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||
const isRef: (typeof import('vue'))['isRef']
|
||||
const isShallow: (typeof import('vue'))['isShallow']
|
||||
const markRaw: (typeof import('vue'))['markRaw']
|
||||
const nextTick: (typeof import('vue'))['nextTick']
|
||||
const onActivated: (typeof import('vue'))['onActivated']
|
||||
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||
const onMounted: (typeof import('vue'))['onMounted']
|
||||
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
||||
const provide: (typeof import('vue'))['provide']
|
||||
const reactive: (typeof import('vue'))['reactive']
|
||||
const readonly: (typeof import('vue'))['readonly']
|
||||
const ref: (typeof import('vue'))['ref']
|
||||
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||
const toRaw: (typeof import('vue'))['toRaw']
|
||||
const toRef: (typeof import('vue'))['toRef']
|
||||
const toRefs: (typeof import('vue'))['toRefs']
|
||||
const toValue: (typeof import('vue'))['toValue']
|
||||
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||
const unref: (typeof import('vue'))['unref']
|
||||
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||
const useDialog: (typeof import('naive-ui'))['useDialog']
|
||||
const useId: (typeof import('vue'))['useId']
|
||||
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar']
|
||||
const useMessage: (typeof import('naive-ui'))['useMessage']
|
||||
const useModel: (typeof import('vue'))['useModel']
|
||||
const useNotification: (typeof import('naive-ui'))['useNotification']
|
||||
const useSlots: (typeof import('vue'))['useSlots']
|
||||
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
||||
const watch: (typeof import('vue'))['watch']
|
||||
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isShallow: typeof import('vue')['isShallow']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type {
|
||||
Component,
|
||||
Slot,
|
||||
Slots,
|
||||
ComponentPublicInstance,
|
||||
ComputedRef,
|
||||
DirectiveBinding,
|
||||
ExtractDefaultPropTypes,
|
||||
ExtractPropTypes,
|
||||
ExtractPublicPropTypes,
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
MaybeRef,
|
||||
MaybeRefOrGetter,
|
||||
VNode,
|
||||
WritableComputedRef
|
||||
} from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
3
src/renderer/components.d.ts
vendored
3
src/renderer/components.d.ts
vendored
@@ -12,6 +12,7 @@ declare module 'vue' {
|
||||
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
||||
ContextMenu: typeof import('./src/components/ContextMenu/ContextMenu.vue')['default']
|
||||
Demo: typeof import('./src/components/ContextMenu/demo.vue')['default']
|
||||
DesktopLyricStyle: typeof import('./src/components/Settings/DesktopLyricStyle.vue')['default']
|
||||
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||
@@ -40,6 +41,7 @@ declare module 'vue' {
|
||||
TButton: typeof import('tdesign-vue-next')['Button']
|
||||
TCard: typeof import('tdesign-vue-next')['Card']
|
||||
TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
|
||||
TColorPicker: typeof import('tdesign-vue-next')['ColorPicker']
|
||||
TContent: typeof import('tdesign-vue-next')['Content']
|
||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||
TDivider: typeof import('tdesign-vue-next')['Divider']
|
||||
@@ -50,6 +52,7 @@ declare module 'vue' {
|
||||
TIcon: typeof import('tdesign-vue-next')['Icon']
|
||||
TImage: typeof import('tdesign-vue-next')['Image']
|
||||
TInput: typeof import('tdesign-vue-next')['Input']
|
||||
TInputNumber: typeof import('tdesign-vue-next')['InputNumber']
|
||||
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
||||
TLayout: typeof import('tdesign-vue-next')['Layout']
|
||||
TLoading: typeof import('tdesign-vue-next')['Loading']
|
||||
|
||||
@@ -31,6 +31,15 @@ onMounted(() => {
|
||||
syncNaiveTheme()
|
||||
window.addEventListener('theme-changed', () => syncNaiveTheme())
|
||||
|
||||
// 全局监听来自主进程的播放控制事件,确保路由切换也可响应
|
||||
const forward = (name: string, val?: any) => {
|
||||
window.dispatchEvent(new CustomEvent('global-music-control', { detail: { name, val } }))
|
||||
}
|
||||
window.electron?.ipcRenderer?.on?.('play', () => forward('play'))
|
||||
window.electron?.ipcRenderer?.on?.('pause', () => forward('pause'))
|
||||
window.electron?.ipcRenderer?.on?.('playPrev', () => forward('playPrev'))
|
||||
window.electron?.ipcRenderer?.on?.('playNext', () => forward('playNext'))
|
||||
|
||||
// 应用启动后延迟3秒检查更新,避免影响启动速度
|
||||
setTimeout(() => {
|
||||
checkForUpdates()
|
||||
|
||||
3
src/renderer/src/assets/icons/lyricOpen.svg
Normal file
3
src/renderer/src/assets/icons/lyricOpen.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg class="lyrics-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M192 240a112 112 0 0 1 111.616 102.784l0.384 9.216V832a16 16 0 0 0 12.352 15.552L320 848h66.816a48 48 0 0 1 6.528 95.552l-6.528 0.448H320a112 112 0 0 1-111.616-102.784L208 832V352a16 16 0 0 0-12.352-15.552L192 336H128a48 48 0 0 1-6.528-95.552L128 240h64z m640-157.568a112 112 0 0 1 111.616 102.848l0.384 9.152V832a112 112 0 0 1-102.784 111.616L832 944h-67.84a48 48 0 0 1-6.464-95.552l6.464-0.448H832a16 16 0 0 0 15.552-12.352L848 832V194.432a16 16 0 0 0-12.352-15.552L832 178.432H480a48 48 0 0 1-6.528-95.552l6.528-0.448H832z m-160 315.136c61.824 0 112 50.112 112 112v147.648a112 112 0 0 1-112 112h-128a112 112 0 0 1-112-112V509.568c0-61.888 50.176-112 112-112z m0 96h-128a16 16 0 0 0-16 16v147.648c0 8.832 7.168 16 16 16h128a16 16 0 0 0 16-16V509.568a16 16 0 0 0-16-16z m64-253.568a48 48 0 0 1 6.528 95.552l-6.528 0.448h-256a48 48 0 0 1-6.528-95.552L480 240h256zM256 82.432a48 48 0 0 1 6.528 95.616L256 178.432H128a48 48 0 0 1-6.528-95.552L128 82.432h128z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -234,6 +234,8 @@
|
||||
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
||||
--song-list-quality-bg: #fff7e6;
|
||||
--song-list-quality-color: #fa8c16;
|
||||
--song-list-source-bg: #f3feff;
|
||||
--song-list-source-color: #00d4e3;
|
||||
|
||||
/* Search 页面专用变量 - 亮色主题 */
|
||||
--search-bg: var(--theme-bg-tertiary);
|
||||
@@ -597,6 +599,8 @@
|
||||
--song-list-btn-bg-hover: var(--td-brand-color-light);
|
||||
--song-list-quality-bg: #3a2a1a;
|
||||
--song-list-quality-color: #fa8c16;
|
||||
--song-list-source-bg: #343939;
|
||||
--song-list-source-color: #00eeff;
|
||||
|
||||
/* Search 页面专用变量 - 暗色主题 */
|
||||
--search-bg: var(--theme-bg-tertiary);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="virtual-scroll-content" :style="{ transform: `translateY(${offsetY}px)` }">
|
||||
<div
|
||||
v-for="(song, index) in visibleItems"
|
||||
:key="song.id || song.songmid"
|
||||
:key="`${song.source || ''}-${song.songmid}-${song.albumId || ''}-${index}`"
|
||||
class="song-item"
|
||||
@mouseenter="hoveredSong = song.id || song.songmid"
|
||||
@mouseleave="hoveredSong = null"
|
||||
@@ -44,6 +44,9 @@
|
||||
<span v-if="song.types && song.types.length > 0" class="quality-tag">
|
||||
{{ getQualityDisplayName(song.types[song.types.length - 1]) }}
|
||||
</span>
|
||||
<span v-if="song.source" class="source-tag">
|
||||
{{ song.source }}
|
||||
</span>
|
||||
{{ song.singer }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,8 +61,17 @@
|
||||
|
||||
<!-- 喜欢按钮 -->
|
||||
<div class="col-like">
|
||||
<button class="action-btn like-btn" @click.stop>
|
||||
<i class="icon-heart"></i>
|
||||
<button
|
||||
class="action-btn like-btn"
|
||||
title="喜欢/取消喜欢"
|
||||
@click.stop="onToggleLike(song)"
|
||||
>
|
||||
<HeartIcon
|
||||
:fill-color="isLiked(song) ? ['#e5484d', '#e5484d'] : ''"
|
||||
:stroke-color="isLiked(song) ? [] : [contrastTextColor, contrastTextColor]"
|
||||
:stroke-width="isLiked(song) ? 0 : 2"
|
||||
size="18"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +122,8 @@ import {
|
||||
PlayCircleIcon,
|
||||
AddIcon,
|
||||
FolderIcon,
|
||||
DeleteIcon
|
||||
DeleteIcon,
|
||||
HeartIcon
|
||||
} from 'tdesign-icons-vue-next'
|
||||
import ContextMenu from '../ContextMenu/ContextMenu.vue'
|
||||
import { createMenuItem, createSeparator, calculateMenuPosition } from '../ContextMenu/utils'
|
||||
@@ -290,8 +303,9 @@ const getQualityDisplayName = (quality: any) => {
|
||||
|
||||
// 处理滚动事件
|
||||
const onScroll = (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
scrollTop.value = target.scrollTop
|
||||
const target = event.target as HTMLElement | null
|
||||
// 兼容程序触发的假事件,target 可能为 null
|
||||
scrollTop.value = target?.scrollTop ?? scrollContainer.value?.scrollTop ?? 0
|
||||
emit('scroll', event)
|
||||
}
|
||||
|
||||
@@ -405,6 +419,89 @@ const loadPlaylists = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// === 喜欢功能(列表内心形) ===
|
||||
const favoritesId = ref<string | null>(null)
|
||||
const likedSet = ref<Set<string | number>>(new Set())
|
||||
const contrastTextColor = 'var(--song-list-btn-color)'
|
||||
|
||||
const loadFavorites = async () => {
|
||||
try {
|
||||
const favIdRes = await (window as any).api?.songList?.getFavoritesId?.()
|
||||
const id: string | null = (favIdRes && favIdRes.data) || null
|
||||
favoritesId.value = id
|
||||
if (!id) {
|
||||
likedSet.value = new Set()
|
||||
return
|
||||
}
|
||||
const existsRes = await songListAPI.exists(id)
|
||||
if (!existsRes.success || !existsRes.data) {
|
||||
favoritesId.value = null
|
||||
likedSet.value = new Set()
|
||||
return
|
||||
}
|
||||
const songsRes = await songListAPI.getSongs(id)
|
||||
if (songsRes.success && Array.isArray(songsRes.data)) {
|
||||
likedSet.value = new Set(songsRes.data.map((s: any) => s.songmid))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载“我的喜欢”失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const isLiked = (song: Song) => likedSet.value.has(song.songmid)
|
||||
|
||||
const ensureFavoritesId = async (): Promise<string | null> => {
|
||||
if (favoritesId.value) {
|
||||
const existsRes = await songListAPI.exists(favoritesId.value)
|
||||
if (existsRes.success && existsRes.data) return favoritesId.value
|
||||
favoritesId.value = null
|
||||
}
|
||||
const searchRes = await songListAPI.search('我的喜欢', 'local')
|
||||
if (searchRes.success && Array.isArray(searchRes.data)) {
|
||||
const exact = searchRes.data.find((pl) => pl.name === '我的喜欢' && pl.source === 'local')
|
||||
if (exact?.id) {
|
||||
favoritesId.value = exact.id
|
||||
await (window as any).api?.songList?.setFavoritesId?.(favoritesId.value)
|
||||
return favoritesId.value
|
||||
}
|
||||
}
|
||||
const createRes = await songListAPI.create('我的喜欢', '', 'local')
|
||||
if (!createRes.success || !createRes.data?.id) {
|
||||
MessagePlugin.error(createRes.error || '创建“我的喜欢”失败')
|
||||
return null
|
||||
}
|
||||
favoritesId.value = createRes.data.id
|
||||
await (window as any).api?.songList?.setFavoritesId?.(favoritesId.value)
|
||||
return favoritesId.value
|
||||
}
|
||||
|
||||
const onToggleLike = async (song: Song) => {
|
||||
try {
|
||||
const id = await ensureFavoritesId()
|
||||
if (!id) return
|
||||
if (isLiked(song)) {
|
||||
const removeRes = await songListAPI.removeSong(id, song.songmid)
|
||||
if (removeRes.success && removeRes.data) {
|
||||
likedSet.value.delete(song.songmid)
|
||||
// MessagePlugin.success('已取消喜欢')
|
||||
} else {
|
||||
MessagePlugin.error(removeRes.error || '取消喜欢失败')
|
||||
}
|
||||
} else {
|
||||
const addRes = await songListAPI.addSongs(id, [toRaw(song) as any])
|
||||
if (addRes.success) {
|
||||
likedSet.value.add(song.songmid)
|
||||
// MessagePlugin.success('已添加到“我的喜欢”')
|
||||
} else {
|
||||
MessagePlugin.error(addRes.error || '添加到“我的喜欢”失败')
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('切换喜欢失败:', e)
|
||||
MessagePlugin.error(e?.message || '操作失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加歌曲到歌单
|
||||
const handleAddToSongList = async (song: Song, playlist: SongList) => {
|
||||
try {
|
||||
@@ -420,7 +517,7 @@ const handleAddToSongList = async (song: Song, playlist: SongList) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 组件挂载后触发一次重新计算
|
||||
nextTick(() => {
|
||||
if (scrollContainer.value) {
|
||||
@@ -431,10 +528,15 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 加载歌单列表
|
||||
loadPlaylists()
|
||||
await loadPlaylists()
|
||||
// 预加载“我的喜欢”集合(确保方法存在于当前文件作用域)
|
||||
await loadFavorites()
|
||||
|
||||
// 监听歌单变化事件
|
||||
window.addEventListener('playlist-updated', loadPlaylists)
|
||||
window.addEventListener('playlist-updated', async () => {
|
||||
await loadPlaylists()
|
||||
await loadFavorites()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -660,6 +762,14 @@ onUnmounted(() => {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
.source-tag {
|
||||
background: var(--song-list-source-bg);
|
||||
color: var(--song-list-source-color);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ const emit = defineEmits<{
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const animationId = ref<number>()
|
||||
const analyser = ref<AnalyserNode>()
|
||||
// 节流渲染,目标 ~30fps
|
||||
const lastFrameTime = ref(0)
|
||||
const dataArray = ref<Uint8Array>()
|
||||
const resizeObserver = ref<ResizeObserver>()
|
||||
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
|
||||
@@ -75,93 +77,87 @@ const initAudioAnalyser = () => {
|
||||
}
|
||||
|
||||
// 绘制可视化
|
||||
const draw = () => {
|
||||
const draw = (ts?: number) => {
|
||||
if (!canvasRef.value || !analyser.value || !dataArray.value) return
|
||||
|
||||
// 帧率节流 ~30fps
|
||||
const now = ts ?? performance.now()
|
||||
if (now - lastFrameTime.value < 33) {
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
return
|
||||
}
|
||||
lastFrameTime.value = now
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
if (!ctx) {
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取频域数据或生成模拟数据
|
||||
if (analyser.value && dataArray.value) {
|
||||
// 有真实音频分析器,获取真实数据
|
||||
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
|
||||
} else {
|
||||
// 没有音频分析器,生成模拟数据
|
||||
const time = Date.now() * 0.001
|
||||
const time = now * 0.001
|
||||
for (let i = 0; i < dataArray.value.length; i++) {
|
||||
// 生成基于时间的模拟频谱数据
|
||||
const frequency = i / dataArray.value.length
|
||||
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
|
||||
const bass = Math.sin(time * 4) * 0.3 + 0.7 // 低频变化
|
||||
const bass = Math.sin(time * 4) * 0.3 + 0.7
|
||||
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
|
||||
}
|
||||
}
|
||||
|
||||
// 计算低频音量 (80hz-120hz 范围)
|
||||
// 假设采样率为 44100Hz,fftSize 为 256,则每个频率 bin 约为 172Hz
|
||||
// 80-120Hz 大约对应前 1-2 个 bin
|
||||
const lowFreqStart = 0
|
||||
const lowFreqEnd = Math.min(3, dataArray.value.length) // 取前几个低频 bin
|
||||
// 计算低频音量(前 3 个 bin)
|
||||
let lowFreqSum = 0
|
||||
for (let i = lowFreqStart; i < lowFreqEnd; i++) {
|
||||
lowFreqSum += dataArray.value[i]
|
||||
}
|
||||
const lowFreqVolume = lowFreqSum / (lowFreqEnd - lowFreqStart) / 255
|
||||
const lowBins = Math.min(3, dataArray.value.length)
|
||||
for (let i = 0; i < lowBins; i++) lowFreqSum += dataArray.value[i]
|
||||
emit('lowFreqUpdate', lowFreqSum / lowBins / 255)
|
||||
|
||||
// 发送低频音量给父组件
|
||||
emit('lowFreqUpdate', lowFreqVolume)
|
||||
|
||||
// 完全清空画布
|
||||
// 清屏
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 如果有背景色,再填充背景
|
||||
// 背景
|
||||
if (props.backgroundColor !== 'transparent') {
|
||||
ctx.fillStyle = props.backgroundColor
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// 使用容器的实际尺寸进行计算,因为 ctx 已经缩放过了
|
||||
// 计算尺寸
|
||||
const container = canvas.parentElement
|
||||
if (!container) return
|
||||
|
||||
if (!container) {
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
return
|
||||
}
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const canvasWidth = containerRect.width
|
||||
const canvasHeight = props.height
|
||||
|
||||
// 计算对称柱状图参数
|
||||
// 柱状参数
|
||||
const halfBarCount = Math.floor(props.barCount / 2)
|
||||
const barWidth = canvasWidth / 2 / halfBarCount
|
||||
const maxBarHeight = canvasHeight * 0.9
|
||||
const centerX = canvasWidth / 2
|
||||
|
||||
// 绘制左右对称的频谱柱状图
|
||||
// 每帧仅创建一次渐变(自底向上),减少对象分配
|
||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0)
|
||||
gradient.addColorStop(0, props.color)
|
||||
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
// 绘制对称频谱
|
||||
for (let i = 0; i < halfBarCount; i++) {
|
||||
// 增强低频响应,让可视化更敏感
|
||||
let barHeight = (dataArray.value[i] / 255) * maxBarHeight
|
||||
|
||||
// 对数据进行增强处理,让变化更明显
|
||||
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
|
||||
|
||||
const y = canvasHeight - barHeight
|
||||
|
||||
// 创建渐变色
|
||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, y)
|
||||
gradient.addColorStop(0, props.color)
|
||||
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
// 绘制左侧柱状图(从中心向左)
|
||||
const leftX = centerX - (i + 1) * barWidth
|
||||
ctx.fillRect(leftX, y, barWidth, barHeight)
|
||||
|
||||
// 绘制右侧柱状图(从中心向右)
|
||||
const rightX = centerX + i * barWidth
|
||||
ctx.fillRect(rightX, y, barWidth, barHeight)
|
||||
}
|
||||
|
||||
// 继续动画
|
||||
if (props.show && Audio.value.isPlay) {
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
}
|
||||
@@ -286,6 +282,10 @@ onBeforeUnmount(() => {
|
||||
analyser.value.disconnect()
|
||||
analyser.value = undefined
|
||||
}
|
||||
// 通知管理器移除对该分析器的引用,防止 Map 持有导致 GC 不回收
|
||||
try {
|
||||
audioManager.removeAnalyser(componentId.value)
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.warn('清理音频资源时出错:', error)
|
||||
}
|
||||
|
||||
@@ -92,111 +92,211 @@ const state = reactive({
|
||||
// 监听歌曲ID变化,获取歌词
|
||||
watch(
|
||||
() => props.songId,
|
||||
async (newId) => {
|
||||
async (newId, _oldId, onCleanup) => {
|
||||
if (!newId || !props.songInfo) return
|
||||
let lyricText = ''
|
||||
let parsedLyrics: LyricLine[] = []
|
||||
// 创建一个符合 MusicItem 接口的对象,只包含必要的基本属性
|
||||
// 竞态与取消控制,防止内存泄漏与过期结果覆盖
|
||||
let active = true
|
||||
const abort = new AbortController()
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
abort.abort()
|
||||
})
|
||||
|
||||
// 工具函数:清洗响应式对象,避免序列化问题
|
||||
const getCleanSongInfo = () => JSON.parse(JSON.stringify(toRaw(props.songInfo)))
|
||||
|
||||
// 工具函数:按来源解析逐字歌词
|
||||
const parseCrLyricBySource = (source: string, text: string): LyricLine[] => {
|
||||
return source === 'tx' ? parseQrc(text) : parseYrc(text)
|
||||
}
|
||||
|
||||
// 工具函数:合并翻译到主歌词
|
||||
const mergeTranslation = (base: LyricLine[], tlyric?: string): LyricLine[] => {
|
||||
if (!tlyric || base.length === 0) return base
|
||||
|
||||
const translated = parseLrc(tlyric)
|
||||
if (!translated || translated.length === 0) return base
|
||||
|
||||
// 将译文按 startTime-endTime 建立索引,便于精确匹配
|
||||
const keyOf = (s: number, e: number) => `${s}-${e}`
|
||||
const joinWords = (line: LyricLine) => (line.words || []).map((w) => w.word).join('')
|
||||
|
||||
const tMap = new Map<string, LyricLine>()
|
||||
for (const tl of translated) {
|
||||
tMap.set(keyOf(tl.startTime, tl.endTime), tl)
|
||||
}
|
||||
|
||||
// 动态容差:与行时长相关,避免长/短行同一阈值导致误配
|
||||
const baseTolerance = 300 // 上限
|
||||
const ratioTolerance = 0.4 // 与行时长的比例
|
||||
|
||||
// 锚点对齐 + 顺序映射:以第一行为锚点,后续按索引顺序插入译文
|
||||
const translatedSorted = translated.slice().sort((a, b) => a.startTime - b.startTime)
|
||||
|
||||
if (base.length > 0) {
|
||||
const firstBase = base[0]
|
||||
const firstDuration = Math.max(1, firstBase.endTime - firstBase.startTime)
|
||||
const firstTol = Math.min(baseTolerance, firstDuration * ratioTolerance)
|
||||
|
||||
// 在容差内寻找与第一行起始时间最接近的译文行作为锚点
|
||||
let anchorIndex: number | null = null
|
||||
let bestDiff = Number.POSITIVE_INFINITY
|
||||
for (let i = 0; i < translatedSorted.length; i++) {
|
||||
const diff = Math.abs(translatedSorted[i].startTime - firstBase.startTime)
|
||||
if (diff <= firstTol && diff < bestDiff) {
|
||||
bestDiff = diff
|
||||
anchorIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if (anchorIndex !== null) {
|
||||
// 从锚点开始顺序映射
|
||||
let j = anchorIndex
|
||||
for (let i = 0; i < base.length && j < translatedSorted.length; i++, j++) {
|
||||
const bl = base[i]
|
||||
const tl = translatedSorted[j]
|
||||
if (tl.words[0].word === '//' || !bl.words[0].word) continue
|
||||
const text = joinWords(tl)
|
||||
if (text) bl.translatedLyric = text
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// 未找到锚点:保持原样
|
||||
return base
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否为网易云音乐,只有网易云才使用ttml接口
|
||||
const isNetease =
|
||||
props.songInfo && 'source' in props.songInfo && props.songInfo.source === 'wy'
|
||||
const songinfo: any = _.cloneDeep(toRaw(props.songInfo))
|
||||
console.log(songinfo)
|
||||
if (isNetease) {
|
||||
// 网易云音乐优先尝试ttml接口
|
||||
const source =
|
||||
props.songInfo && 'source' in props.songInfo ? (props.songInfo as any).source : 'kg'
|
||||
let parsedLyrics: LyricLine[] = []
|
||||
|
||||
if (source === 'wy' || source === 'tx') {
|
||||
// 网易云 / QQ 音乐:优先尝试 TTML
|
||||
try {
|
||||
const res = (await (
|
||||
await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`)
|
||||
).text()) as any
|
||||
const res = await (
|
||||
await fetch(
|
||||
`https://amll-ttml-db.stevexmh.net/${source === 'wy' ? 'ncm' : 'qq'}/${newId}`,
|
||||
{
|
||||
signal: abort.signal
|
||||
})
|
||||
).text()
|
||||
if (!active) return
|
||||
if (!res || res.length < 100) throw new Error('ttml 无歌词')
|
||||
parsedLyrics = parseTTML(res).lines
|
||||
console.log('搜索到ttml歌词', parsedLyrics)
|
||||
} catch {
|
||||
// ttml失败后使用新的歌词API
|
||||
// 回退到统一歌词 API
|
||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||
source: 'wy',
|
||||
songInfo: songinfo
|
||||
source,
|
||||
songInfo: _.cloneDeep(toRaw(props.songInfo)) as any
|
||||
})
|
||||
console.log('网易云歌词数据:', lyricData)
|
||||
if (!active) return
|
||||
|
||||
if (lyricData.crlyric) {
|
||||
// 使用逐字歌词
|
||||
lyricText = lyricData.crlyric
|
||||
console.log('网易云逐字歌词', lyricText)
|
||||
parsedLyrics = parseYrc(lyricText)
|
||||
console.log('使用网易云逐字歌词', parsedLyrics)
|
||||
} else if (lyricData.lyric) {
|
||||
lyricText = lyricData.lyric
|
||||
parsedLyrics = parseLrc(lyricText)
|
||||
console.log('使用网易云普通歌词', parsedLyrics)
|
||||
if (lyricData?.crlyric) {
|
||||
parsedLyrics = parseCrLyricBySource(source, lyricData.crlyric)
|
||||
} else if (lyricData?.lyric) {
|
||||
parsedLyrics = parseLrc(lyricData.lyric)
|
||||
}
|
||||
|
||||
if (lyricData.tlyric) {
|
||||
const translatedline = parseLrc(lyricData.tlyric)
|
||||
console.log('网易云翻译歌词:', translatedline)
|
||||
for (let i = 0; i < parsedLyrics.length; i++) {
|
||||
if (translatedline[i] && translatedline[i].words[0]) {
|
||||
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
|
||||
}
|
||||
} else {
|
||||
// 其他音乐平台直接使用新的歌词API
|
||||
const source = props.songInfo && 'source' in props.songInfo ? props.songInfo.source : 'kg'
|
||||
// 创建一个纯净的对象,避免Vue响应式对象序列化问题
|
||||
const cleanSongInfo = JSON.parse(JSON.stringify(toRaw(props.songInfo)))
|
||||
// 其他来源:直接统一歌词 API
|
||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||
source: source,
|
||||
songInfo: cleanSongInfo
|
||||
source,
|
||||
songInfo: getCleanSongInfo()
|
||||
})
|
||||
console.log(`${source}歌词数据:`, lyricData)
|
||||
if (!active) return
|
||||
|
||||
if (lyricData.crlyric) {
|
||||
// 使用逐字歌词
|
||||
lyricText = lyricData.crlyric
|
||||
if (source === 'tx') {
|
||||
parsedLyrics = parseQrc(lyricText)
|
||||
} else {
|
||||
parsedLyrics = parseYrc(lyricText)
|
||||
}
|
||||
console.log(`使用${source}逐字歌词`, parsedLyrics)
|
||||
} else if (lyricData.lyric) {
|
||||
lyricText = lyricData.lyric
|
||||
parsedLyrics = parseLrc(lyricText)
|
||||
console.log(`使用${source}普通歌词`, parsedLyrics)
|
||||
if (lyricData?.crlyric) {
|
||||
parsedLyrics = parseCrLyricBySource(source, lyricData.crlyric)
|
||||
} else if (lyricData?.lyric) {
|
||||
parsedLyrics = parseLrc(lyricData.lyric)
|
||||
}
|
||||
|
||||
if (lyricData.tlyric) {
|
||||
const translatedline = parseLrc(lyricData.tlyric)
|
||||
console.log(`${source}翻译歌词:`, translatedline)
|
||||
for (let i = 0; i < parsedLyrics.length; i++) {
|
||||
if (translatedline[i] && translatedline[i].words[0]) {
|
||||
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedLyrics.length > 0) {
|
||||
state.lyricLines = parsedLyrics
|
||||
console.log('歌词加载成功', parsedLyrics.length)
|
||||
} else {
|
||||
state.lyricLines = []
|
||||
console.log('未找到歌词或解析失败')
|
||||
parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
|
||||
}
|
||||
if (!active) return
|
||||
state.lyricLines = parsedLyrics.length > 0 ? parsedLyrics : []
|
||||
} catch (error) {
|
||||
console.error('获取歌词失败:', error)
|
||||
// 若已无效或已清理,避免写入与持有引用
|
||||
if (!active) return
|
||||
state.lyricLines = []
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 桌面歌词联动:构建歌词负载、计算当前行并通过 IPC 推送
|
||||
const buildLyricPayload = (lines: LyricLine[]) =>
|
||||
(lines || []).map((l) => ({
|
||||
content: (l.words || []).map((w) => w.word).join(''),
|
||||
tran: l.translatedLyric || ''
|
||||
}))
|
||||
|
||||
const lastLyricIndex = ref(-1)
|
||||
const computeLyricIndex = (timeMs: number, lines: LyricLine[]) => {
|
||||
if (!lines || lines.length === 0) return -1
|
||||
const t = timeMs
|
||||
const i = lines.findIndex((l) => t >= l.startTime && t < l.endTime)
|
||||
if (i !== -1) return i
|
||||
for (let j = lines.length - 1; j >= 0; j--) {
|
||||
if (t >= lines[j].startTime) return j
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// 歌词集合变化时,先推一次集合,index 为 -1(由窗口自行处理占位)
|
||||
watch(
|
||||
() => state.lyricLines,
|
||||
(lines) => {
|
||||
const payload = { index: -1, lyric: buildLyricPayload(lines) }
|
||||
;(window as any)?.electron?.ipcRenderer?.send?.('play-lyric-change', payload)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 当前时间变化时,计算当前行并推送
|
||||
watch(
|
||||
() => state.currentTime,
|
||||
(ms) => {
|
||||
const idx = computeLyricIndex(ms, state.lyricLines)
|
||||
if (idx !== lastLyricIndex.value) {
|
||||
lastLyricIndex.value = idx
|
||||
const payload = { index: idx, lyric: buildLyricPayload(state.lyricLines) }
|
||||
;(window as any)?.electron?.ipcRenderer?.send?.('play-lyric-change', payload)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 播放状态推送(用于窗口播放/暂停按钮联动)
|
||||
watch(
|
||||
() => Audio.value.isPlay,
|
||||
(playing) => {
|
||||
;(window as any)?.electron?.ipcRenderer?.send?.('play-status-change', playing)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 歌曲标题推送
|
||||
watch(
|
||||
() => props.songInfo,
|
||||
(info) => {
|
||||
try {
|
||||
const name = (info as any)?.name || ''
|
||||
const artist = (info as any)?.singer || ''
|
||||
const title = [name, artist].filter(Boolean).join(' - ')
|
||||
if (title) (window as any)?.electron?.ipcRenderer?.send?.('play-song-change', title)
|
||||
} catch {}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
|
||||
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
|
||||
|
||||
// 订阅音频事件,保持数据同步
|
||||
const unsubscribeTimeUpdate = ref<(() => void) | undefined>(undefined)
|
||||
const unsubscribePlay = ref<(() => void) | undefined>(undefined)
|
||||
@@ -214,24 +314,51 @@ const useBlackText = ref(false)
|
||||
async function updateTextColor() {
|
||||
try {
|
||||
useBlackText.value = await shouldUseBlackText(actualCoverImage.value)
|
||||
console.log('使用黑色文本:', useBlackText.value)
|
||||
} catch (error) {
|
||||
console.error('获取对比色失败:', error)
|
||||
useBlackText.value = false // 默认使用白色文本
|
||||
}
|
||||
}
|
||||
|
||||
const jumpTime = (e) => {
|
||||
if (Audio.value.audio) Audio.value.audio.currentTime = e.line.getLine().startTime / 1000
|
||||
}
|
||||
// 监听封面图片变化
|
||||
watch(() => actualCoverImage.value, updateTextColor, { immediate: true })
|
||||
|
||||
// 在全屏播放显示时阻止系统息屏
|
||||
const blockerActive = ref(false)
|
||||
watch(
|
||||
() => props.show,
|
||||
async (visible) => {
|
||||
try {
|
||||
if (visible && !blockerActive.value) {
|
||||
await (window as any)?.api?.powerSaveBlocker?.start?.()
|
||||
blockerActive.value = true
|
||||
} else if (!visible && blockerActive.value) {
|
||||
await (window as any)?.api?.powerSaveBlocker?.stop?.()
|
||||
blockerActive.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('powerSaveBlocker 切换失败:', e)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
updateTextColor()
|
||||
console.log('组件挂载完成', bgRef.value, lyricPlayerRef.value)
|
||||
})
|
||||
|
||||
// 组件卸载前清理订阅
|
||||
onBeforeUnmount(() => {
|
||||
onBeforeUnmount(async () => {
|
||||
// 组件卸载时确保恢复系统息屏
|
||||
if (blockerActive.value) {
|
||||
try {
|
||||
await (window as any)?.api?.powerSaveBlocker?.stop?.()
|
||||
} catch {}
|
||||
blockerActive.value = false
|
||||
}
|
||||
// 取消订阅以防止内存泄漏
|
||||
if (unsubscribeTimeUpdate.value) {
|
||||
unsubscribeTimeUpdate.value()
|
||||
@@ -239,6 +366,8 @@ onBeforeUnmount(() => {
|
||||
if (unsubscribePlay.value) {
|
||||
unsubscribePlay.value()
|
||||
}
|
||||
bgRef.value?.bgRender?.dispose()
|
||||
lyricPlayerRef.value?.lyricPlayer?.dispose()
|
||||
})
|
||||
|
||||
// 监听音频URL变化
|
||||
@@ -299,7 +428,7 @@ const lyricTranslateY = computed(() => {
|
||||
:album-is-video="false"
|
||||
:fps="30"
|
||||
:flow-speed="4"
|
||||
:has-lyric="state.lyricLines.length > 10 && playSetting.getBgPlaying"
|
||||
:has-lyric="state.lyricLines.length > 10"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1"
|
||||
/>
|
||||
<!-- 全屏按钮 -->
|
||||
@@ -365,11 +494,7 @@ const lyricTranslateY = computed(() => {
|
||||
class="lyric-player"
|
||||
:enable-spring="playSetting.getisJumpLyric"
|
||||
:enable-scale="playSetting.getisJumpLyric"
|
||||
@line-click="
|
||||
(e) => {
|
||||
if (Audio.audio) Audio.audio.currentTime = e.line.getLine().startTime / 1000
|
||||
}
|
||||
"
|
||||
@line-click="jumpTime"
|
||||
>
|
||||
</LyricPlayer>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, provide, ref, onActivated, onDeactivated } from 'vue'
|
||||
import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
watch,
|
||||
nextTick
|
||||
} from 'vue'
|
||||
import { ControlAudioStore } from '@renderer/store/ControlAudio'
|
||||
|
||||
const audioStore = ControlAudioStore()
|
||||
@@ -17,6 +26,26 @@ onMounted(() => {
|
||||
// window.api.ping(handleEnded)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听 URL 变化,先重置旧音频再加载新音频,避免旧解码/缓冲滞留
|
||||
*/
|
||||
watch(
|
||||
() => audioStore.Audio.url,
|
||||
async (newUrl) => {
|
||||
const a = audioMeta.value
|
||||
if (!a) return
|
||||
try {
|
||||
a.pause()
|
||||
} catch {}
|
||||
a.removeAttribute('src')
|
||||
a.load()
|
||||
await nextTick()
|
||||
// 模板绑定会把 src 更新为 newUrl,这里再触发一次 load
|
||||
if (newUrl) {
|
||||
a.load()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 组件被激活时(从缓存中恢复)
|
||||
onActivated(() => {
|
||||
console.log('音频组件被激活')
|
||||
@@ -71,22 +100,29 @@ const handlePlay = (): void => {
|
||||
audioStore.publish('play')
|
||||
}
|
||||
|
||||
let rafId: number | null = null
|
||||
const startSetupInterval = (): void => {
|
||||
if (rafId !== null) return
|
||||
const onFrame = () => {
|
||||
if (audioMeta.value && !audioMeta.value.paused) {
|
||||
audioStore.publish('timeupdate')
|
||||
|
||||
audioStore.setCurrentTime((audioMeta.value && audioMeta.value.currentTime) || 0)
|
||||
|
||||
requestAnimationFrame(onFrame)
|
||||
}
|
||||
rafId = requestAnimationFrame(onFrame)
|
||||
}
|
||||
requestAnimationFrame(onFrame)
|
||||
rafId = requestAnimationFrame(onFrame)
|
||||
}
|
||||
|
||||
const handlePause = (): void => {
|
||||
audioStore.Audio.isPlay = false
|
||||
audioStore.publish('pause')
|
||||
// 停止单实例 rAF
|
||||
if (rafId !== null) {
|
||||
try {
|
||||
cancelAnimationFrame(rafId)
|
||||
} catch {}
|
||||
rafId = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (event: Event): void => {
|
||||
@@ -112,8 +148,23 @@ const handleCanPlay = (): void => {
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时清空所有订阅者
|
||||
window.api.pingService.stop()
|
||||
|
||||
try {
|
||||
window.api.pingService.stop()
|
||||
} catch {}
|
||||
// 停止 rAF
|
||||
if (rafId !== null) {
|
||||
try {
|
||||
cancelAnimationFrame(rafId)
|
||||
} catch {}
|
||||
rafId = null
|
||||
}
|
||||
if (audioMeta.value) {
|
||||
try {
|
||||
audioMeta.value.pause()
|
||||
} catch {}
|
||||
audioMeta.value.removeAttribute('src')
|
||||
audioMeta.value.load()
|
||||
}
|
||||
audioStore.clearAllSubscribers()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -29,13 +29,110 @@ import {
|
||||
} from '@renderer/utils/playlist/playlistManager'
|
||||
import mediaSessionController from '@renderer/utils/audio/useSmtc'
|
||||
import defaultCoverImg from '/default-cover.png'
|
||||
import { downloadSingleSong } from '@renderer/utils/audio/download'
|
||||
import { HeartIcon, DownloadIcon, CheckIcon, LockOnIcon } from 'tdesign-icons-vue-next'
|
||||
import _ from 'lodash'
|
||||
import { songListAPI } from '@renderer/api/songList'
|
||||
|
||||
const controlAudio = ControlAudioStore()
|
||||
const localUserStore = LocalUserDetailStore()
|
||||
const { Audio } = storeToRefs(controlAudio)
|
||||
const { list, userInfo } = storeToRefs(localUserStore)
|
||||
const { setCurrentTime, start, stop, setVolume, setUrl } = controlAudio
|
||||
|
||||
// 当前歌曲是否已在“我的喜欢”
|
||||
const likeState = ref(false)
|
||||
const isLiked = computed(() => likeState.value)
|
||||
|
||||
const refreshLikeState = async () => {
|
||||
try {
|
||||
if (!userInfo.value.lastPlaySongId) {
|
||||
likeState.value = false
|
||||
return
|
||||
}
|
||||
const favIdRes = await window.api.songList.getFavoritesId()
|
||||
const favoritesId: string | null = (favIdRes && favIdRes.data) || null
|
||||
if (!favoritesId) {
|
||||
likeState.value = false
|
||||
return
|
||||
}
|
||||
const hasRes = await songListAPI.hasSong(favoritesId, userInfo.value.lastPlaySongId)
|
||||
likeState.value = !!(hasRes.success && hasRes.data)
|
||||
} catch {
|
||||
likeState.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userInfo.value.lastPlaySongId,
|
||||
() => refreshLikeState()
|
||||
)
|
||||
onMounted(() => refreshLikeState())
|
||||
const showFullPlay = ref(false)
|
||||
// 桌面歌词开关与锁定状态
|
||||
const desktopLyricOpen = ref(false)
|
||||
const desktopLyricLocked = ref(false)
|
||||
|
||||
// 桌面歌词按钮逻辑:
|
||||
// - 若未打开:打开桌面歌词
|
||||
// - 若已打开且锁定:先解锁,不关闭
|
||||
// - 若已打开且未锁定:关闭桌面歌词
|
||||
const toggleDesktopLyric = async () => {
|
||||
try {
|
||||
if (!desktopLyricOpen.value) {
|
||||
window.electron?.ipcRenderer?.send?.('change-desktop-lyric', true)
|
||||
desktopLyricOpen.value = true
|
||||
// 恢复最新锁定状态
|
||||
const lock = await window.electron?.ipcRenderer?.invoke?.('get-lyric-lock-state')
|
||||
desktopLyricLocked.value = !!lock
|
||||
return
|
||||
}
|
||||
// 已打开
|
||||
const lock = await window.electron?.ipcRenderer?.invoke?.('get-lyric-lock-state')
|
||||
desktopLyricLocked.value = !!lock
|
||||
if (desktopLyricLocked.value) {
|
||||
// 先解锁,本次不关闭
|
||||
window.electron?.ipcRenderer?.send?.('toogleDesktopLyricLock', false)
|
||||
desktopLyricLocked.value = false
|
||||
return
|
||||
}
|
||||
// 未锁定则关闭
|
||||
window.electron?.ipcRenderer?.send?.('change-desktop-lyric', false)
|
||||
desktopLyricOpen.value = false
|
||||
} catch (e) {
|
||||
console.error('切换桌面歌词失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听来自主进程的锁定状态广播
|
||||
window.electron?.ipcRenderer?.on?.('toogleDesktopLyricLock', (_, lock) => {
|
||||
desktopLyricLocked.value = !!lock
|
||||
})
|
||||
// 监听主进程通知关闭桌面歌词
|
||||
window.electron?.ipcRenderer?.on?.('closeDesktopLyric', () => {
|
||||
desktopLyricOpen.value = false
|
||||
desktopLyricLocked.value = false
|
||||
})
|
||||
|
||||
window.addEventListener('global-music-control', (e: any) => {
|
||||
const name = e?.detail?.name
|
||||
console.log(name);
|
||||
switch (name) {
|
||||
case 'play':
|
||||
handlePlay()
|
||||
break
|
||||
case 'pause':
|
||||
handlePause()
|
||||
break
|
||||
case 'playPrev':
|
||||
playPrevious()
|
||||
break
|
||||
case 'playNext':
|
||||
playNext()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', KeyEvent)
|
||||
// 处理最小化右键的事件
|
||||
const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
|
||||
@@ -136,38 +233,45 @@ const playSong = async (song: SongList) => {
|
||||
// 设置加载状态
|
||||
isLoadingSong.value = true
|
||||
|
||||
// 检查是否需要恢复播放位置(历史播放)
|
||||
// 检查是否需要恢复播放位置(历史播放)
|
||||
const isHistoryPlay =
|
||||
song.songmid === userInfo.value.lastPlaySongId &&
|
||||
userInfo.value.currentTime !== undefined &&
|
||||
userInfo.value.currentTime > 0
|
||||
|
||||
if (isHistoryPlay && userInfo.value.currentTime !== undefined) {
|
||||
pendingRestorePosition = userInfo.value.currentTime
|
||||
pendingRestoreSongId = song.songmid
|
||||
console.log(`准备恢复播放位置: ${pendingRestorePosition}秒`)
|
||||
// 清除历史位置,避免重复恢复
|
||||
// 清除历史位置,避免重复恢复
|
||||
userInfo.value.currentTime = 0
|
||||
} else {
|
||||
pendingRestorePosition = 0
|
||||
pendingRestoreSongId = null
|
||||
}
|
||||
|
||||
// 更新当前播放歌曲ID
|
||||
// 立刻暂停当前播放 - 不等待渐变
|
||||
if (Audio.value.isPlay && Audio.value.audio) {
|
||||
Audio.value.isPlay = false
|
||||
Audio.value.audio.pause()
|
||||
// 恢复音量,避免下次播放音量为0
|
||||
Audio.value.audio.volume = Audio.value.volume / 100
|
||||
}
|
||||
|
||||
// 立刻更新 UI 到新歌曲
|
||||
songInfo.value.name = song.name
|
||||
songInfo.value.singer = song.singer
|
||||
songInfo.value.albumName = song.albumName
|
||||
songInfo.value.img = song.img
|
||||
userInfo.value.lastPlaySongId = song.songmid
|
||||
|
||||
// 如果播放列表是打开的,滚动到当前播放歌曲
|
||||
// 如果播放列表是打开的,滚动到当前播放歌曲
|
||||
if (showPlaylist.value) {
|
||||
nextTick(() => {
|
||||
playlistDrawerRef.value?.scrollToCurrentSong()
|
||||
})
|
||||
}
|
||||
|
||||
// 更新歌曲信息并触发主题色更新
|
||||
songInfo.value.name = song.name
|
||||
songInfo.value.singer = song.singer
|
||||
songInfo.value.albumName = song.albumName
|
||||
songInfo.value.img = song.img
|
||||
|
||||
// 更新媒体会话元数据
|
||||
mediaSessionController.updateMetadata({
|
||||
title: song.name,
|
||||
@@ -176,68 +280,85 @@ const playSong = async (song: SongList) => {
|
||||
artworkUrl: song.img || defaultCoverImg
|
||||
})
|
||||
|
||||
// 确保主题色更新
|
||||
|
||||
// 尝试获取 URL
|
||||
let urlToPlay = ''
|
||||
|
||||
// 获取URL
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
urlToPlay = await getSongRealUrl(toRaw(song))
|
||||
} catch (error) {
|
||||
throw error
|
||||
} catch (error: any) {
|
||||
console.error('获取歌曲 URL 失败,播放下一首原歌曲:', error)
|
||||
isLoadingSong.value = false
|
||||
tryAutoNext('获取歌曲 URL 失败')
|
||||
return
|
||||
}
|
||||
|
||||
// 先停止当前播放
|
||||
if (Audio.value.isPlay) {
|
||||
const stopResult = stop()
|
||||
if (stopResult && typeof stopResult.then === 'function') {
|
||||
await stopResult
|
||||
}
|
||||
// 在切换前彻底重置旧音频,释放缓冲与解码器
|
||||
if (Audio.value.audio) {
|
||||
const a = Audio.value.audio
|
||||
try {
|
||||
a.pause()
|
||||
} catch {}
|
||||
a.removeAttribute('src')
|
||||
a.load()
|
||||
}
|
||||
|
||||
// 设置URL(这会触发音频重新加载)
|
||||
// 设置 URL(这会触发音频重新加载)
|
||||
setUrl(urlToPlay)
|
||||
|
||||
// 等待音频准备就绪
|
||||
await waitForAudioReady()
|
||||
await setColor()
|
||||
songInfo.value = {
|
||||
...song
|
||||
}
|
||||
// // 短暂延迟确保音频状态稳定
|
||||
// await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 开始播放
|
||||
try {
|
||||
start()
|
||||
} catch (error) {
|
||||
console.error('启动播放失败:', error)
|
||||
// 如果是 AbortError,尝试重新播放
|
||||
if ((error as { name: string }).name === 'AbortError') {
|
||||
console.log('检测到 AbortError,尝试重新播放...')
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
try {
|
||||
const retryResult = start()
|
||||
if (retryResult && typeof retryResult.then === 'function') {
|
||||
await retryResult
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error('重试播放也失败:', retryError)
|
||||
throw retryError
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
// 更新完整歌曲信息
|
||||
songInfo.value = { ...song }
|
||||
|
||||
/**
|
||||
* 提前关闭加载状态
|
||||
* 这样UI不会卡在“加载中”,用户能立刻看到播放键切换
|
||||
*/
|
||||
isLoadingSong.value = false
|
||||
|
||||
/**
|
||||
* 异步开始播放(不await,以免阻塞UI)
|
||||
*/
|
||||
start()
|
||||
.catch(async (error: any) => {
|
||||
console.error('启动播放失败:', error)
|
||||
tryAutoNext('启动播放失败')
|
||||
})
|
||||
.then(() => {
|
||||
autoNextCount.value = 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 注册事件监听,确保浏览器播放事件触发时同步关闭loading
|
||||
* (多一道保险)
|
||||
*/
|
||||
if (Audio.value.audio) {
|
||||
Audio.value.audio.addEventListener(
|
||||
'playing',
|
||||
() => {
|
||||
isLoadingSong.value = false
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
Audio.value.audio.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
isLoadingSong.value = false
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('播放歌曲失败:', error)
|
||||
MessagePlugin.error('播放失败,原因:' + error.message)
|
||||
console.error('播放歌曲失败(外层捕获):', error)
|
||||
tryAutoNext('播放歌曲失败')
|
||||
// MessagePlugin.error('播放失败,原因:' + error.message)
|
||||
isLoadingSong.value = false
|
||||
} finally {
|
||||
// 无论成功还是失败,都清除加载状态
|
||||
// 最后的保险,确保加载状态一定会被关闭
|
||||
isLoadingSong.value = false
|
||||
}
|
||||
}
|
||||
|
||||
provide('PlaySong', playSong)
|
||||
// 歌曲信息
|
||||
const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
|
||||
@@ -246,6 +367,23 @@ const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
|
||||
// 歌曲加载状态
|
||||
const isLoadingSong = ref(false)
|
||||
|
||||
// 自动下一首次数限制:不超过当前列表的30%
|
||||
const autoNextCount = ref(0)
|
||||
const getAutoNextLimit = () => Math.max(1, Math.floor(list.value.length * 0.3))
|
||||
const tryAutoNext = (reason: string) => {
|
||||
const limit = getAutoNextLimit()
|
||||
MessagePlugin.error(`自动跳过当前歌曲:原因:${reason}`)
|
||||
|
||||
if (autoNextCount.value >= limit && autoNextCount.value > 2) {
|
||||
MessagePlugin.error(
|
||||
`自动下一首失败:超过当前列表30%限制(${autoNextCount.value}/${limit})。原因:${reason}`
|
||||
)
|
||||
return
|
||||
}
|
||||
autoNextCount.value++
|
||||
playNext()
|
||||
}
|
||||
|
||||
// 更新播放模式
|
||||
const updatePlayMode = () => {
|
||||
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]
|
||||
@@ -321,6 +459,19 @@ const handleVolumeDragEnd = () => {
|
||||
window.removeEventListener('mouseup', handleVolumeDragEnd)
|
||||
}
|
||||
|
||||
const handleVolumeWheel = (event: WheelEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
const volumeStep = event.deltaY > 0 ? -5 : 5
|
||||
const updatedVolume = Math.max(0, Math.min(100, volumeValue.value + volumeStep))
|
||||
|
||||
if (updatedVolume === volumeValue.value) {
|
||||
return
|
||||
}
|
||||
|
||||
volumeValue.value = updatedVolume
|
||||
}
|
||||
|
||||
// 播放列表相关
|
||||
const showPlaylist = ref(false)
|
||||
const playlistDrawerRef = ref<InstanceType<typeof PlaylistDrawer> | null>(null)
|
||||
@@ -579,6 +730,86 @@ const toggleFullPlay = () => {
|
||||
showFullPlay.value = !showFullPlay.value
|
||||
}
|
||||
|
||||
// 左侧操作:喜欢/取消喜欢(支持切换)
|
||||
const onToggleLike = async () => {
|
||||
try {
|
||||
// 获取当前播放歌曲对象
|
||||
const currentSong = list.value.find((s) => s.songmid === userInfo.value.lastPlaySongId)
|
||||
if (!currentSong) {
|
||||
MessagePlugin.warning('当前没有正在播放的歌曲')
|
||||
return
|
||||
}
|
||||
|
||||
// 读取持久化的“我的喜欢”歌单ID
|
||||
const favIdRes = await window.api.songList.getFavoritesId()
|
||||
let favoritesId: string | null = (favIdRes && favIdRes.data) || null
|
||||
|
||||
// 如果已有ID但歌单不存在,则置空
|
||||
if (favoritesId) {
|
||||
const existsRes = await songListAPI.exists(favoritesId)
|
||||
if (!existsRes.success || !existsRes.data) {
|
||||
favoritesId = null
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有ID,尝试查找同名歌单;找不到则创建
|
||||
if (!favoritesId) {
|
||||
const searchRes = await songListAPI.search('我的喜欢', 'local')
|
||||
if (searchRes.success && Array.isArray(searchRes.data)) {
|
||||
const exact = searchRes.data.find((pl) => pl.name === '我的喜欢' && pl.source === 'local')
|
||||
favoritesId = exact?.id || null
|
||||
}
|
||||
if (!favoritesId) {
|
||||
const createRes = await songListAPI.create('我的喜欢', '', 'local')
|
||||
if (!createRes.success || !createRes.data?.id) {
|
||||
MessagePlugin.error(createRes.error || '创建“我的喜欢”失败')
|
||||
return
|
||||
}
|
||||
favoritesId = createRes.data.id
|
||||
}
|
||||
// 持久化ID到主进程配置
|
||||
await window.api.songList.setFavoritesId(favoritesId)
|
||||
}
|
||||
|
||||
// 根据当前状态决定添加或移除
|
||||
if (likeState.value) {
|
||||
const removeRes = await songListAPI.removeSong(
|
||||
favoritesId!,
|
||||
userInfo.value.lastPlaySongId as any
|
||||
)
|
||||
if (removeRes.success && removeRes.data) {
|
||||
likeState.value = false
|
||||
// MessagePlugin.success('已取消喜欢')
|
||||
} else {
|
||||
MessagePlugin.error(removeRes.error || '取消喜欢失败')
|
||||
}
|
||||
} else {
|
||||
const addRes = await songListAPI.addSongs(favoritesId!, [
|
||||
_.cloneDeep(toRaw(currentSong)) as any
|
||||
])
|
||||
if (addRes.success) {
|
||||
likeState.value = true
|
||||
// MessagePlugin.success('已添加到“我的喜欢”')
|
||||
} else {
|
||||
MessagePlugin.error(addRes.error || '添加到“我的喜欢”失败')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('切换喜欢状态失败:', error)
|
||||
MessagePlugin.error('操作失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const onDownload = async () => {
|
||||
try {
|
||||
await downloadSingleSong(_.cloneDeep(toRaw(songInfo.value)) as any)
|
||||
MessagePlugin.success('开始下载当前歌曲')
|
||||
} catch (e: any) {
|
||||
console.error('下载失败:', e)
|
||||
MessagePlugin.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条相关
|
||||
const progressRef = ref<HTMLDivElement | null>(null)
|
||||
const isDraggingProgress = ref(false)
|
||||
@@ -823,6 +1054,36 @@ watch(showFullPlay, (val) => {
|
||||
<div class="song-name">{{ songInfo.name }}</div>
|
||||
<div class="artist-name">{{ songInfo.singer }}</div>
|
||||
</div>
|
||||
|
||||
<div class="left-actions">
|
||||
<t-tooltip :content="isLiked ? '已喜欢' : '喜欢'">
|
||||
<t-button
|
||||
class="control-btn"
|
||||
variant="text"
|
||||
shape="circle"
|
||||
:disabled="!songInfo.songmid"
|
||||
@click.stop="onToggleLike"
|
||||
>
|
||||
<heart-icon
|
||||
:fill-color="isLiked ? ['#FF7878', '#FF7878'] : ''"
|
||||
:stroke-color="isLiked ? [] : [contrastTextColor, contrastTextColor]"
|
||||
:stroke-width="isLiked ? 0 : 2"
|
||||
size="18"
|
||||
/>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
<t-tooltip content="下载">
|
||||
<t-button
|
||||
class="control-btn"
|
||||
variant="text"
|
||||
shape="circle"
|
||||
:disabled="!songInfo.songmid"
|
||||
@click.stop="onDownload"
|
||||
>
|
||||
<DownloadIcon size="18" />
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:播放控制 -->
|
||||
@@ -868,6 +1129,7 @@ watch(showFullPlay, (val) => {
|
||||
class="volume-control"
|
||||
@mouseenter="showVolumeSlider = true"
|
||||
@mouseleave="showVolumeSlider = false"
|
||||
@wheel.prevent="handleVolumeWheel"
|
||||
>
|
||||
<button class="control-btn">
|
||||
<shengyin style="width: 1.5em; height: 1.5em" />
|
||||
@@ -893,6 +1155,29 @@ watch(showFullPlay, (val) => {
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 桌面歌词开关按钮 -->
|
||||
<t-tooltip
|
||||
:content="
|
||||
desktopLyricOpen ? (desktopLyricLocked ? '解锁歌词' : '关闭桌面歌词') : '打开桌面歌词'
|
||||
"
|
||||
>
|
||||
<t-button
|
||||
class="control-btn lyric-btn"
|
||||
shape="circle"
|
||||
variant="text"
|
||||
:disabled="!songInfo.songmid"
|
||||
@click.stop="toggleDesktopLyric"
|
||||
>
|
||||
<SvgIcon name="lyricOpen" size="18"></SvgIcon>
|
||||
<transition name="fade" mode="out-in">
|
||||
<template v-if="desktopLyricOpen">
|
||||
<LockOnIcon v-if="desktopLyricLocked" key="lock" class="lyric-lock" size="8" />
|
||||
<CheckIcon v-else key="check" class="lyric-check" size="8" />
|
||||
</template>
|
||||
</transition>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 播放列表按钮 -->
|
||||
<t-tooltip content="播放列表">
|
||||
<n-badge :value="list.length" :max="99" color="#bbb">
|
||||
@@ -1135,6 +1420,38 @@ watch(showFullPlay, (val) => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧操作按钮 */
|
||||
.left-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
|
||||
.control-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: v-bind(contrastTextColor);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: v-bind(hoverColor);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 中间:播放控制 */
|
||||
.center-controls {
|
||||
display: flex;
|
||||
@@ -1214,6 +1531,7 @@ watch(showFullPlay, (val) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
@@ -1222,6 +1540,17 @@ watch(showFullPlay, (val) => {
|
||||
&:hover {
|
||||
color: v-bind(hoverColor);
|
||||
}
|
||||
|
||||
&.lyric-btn .lyric-check,
|
||||
&.lyric-btn .lyric-lock {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
color: v-bind(maincolor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
src/renderer/src/components/Settings/DesktopLyricStyle.vue
Normal file
219
src/renderer/src/components/Settings/DesktopLyricStyle.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
interface LyricOption {
|
||||
fontSize: number
|
||||
mainColor: string
|
||||
shadowColor: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const option = ref<LyricOption>({
|
||||
fontSize: 30,
|
||||
mainColor: '#73BCFC',
|
||||
shadowColor: 'rgba(255, 255, 255, 0.5)',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 800,
|
||||
height: 180
|
||||
})
|
||||
|
||||
const shadowRgb = ref<{ r: number; g: number; b: number }>({ r: 255, g: 255, b: 255 })
|
||||
const mainHex = ref<string>('#73BCFC')
|
||||
const shadowColorStr = ref<string>('rgba(255, 255, 255, 0.5')
|
||||
|
||||
const parseColorToRgb = (input: string) => {
|
||||
const rgbaMatch = input?.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i)
|
||||
if (rgbaMatch) {
|
||||
return { r: Number(rgbaMatch[1]), g: Number(rgbaMatch[2]), b: Number(rgbaMatch[3]) }
|
||||
}
|
||||
const hexMatch = input?.match(/^#([0-9a-f]{6})$/i)
|
||||
if (hexMatch) {
|
||||
const hex = hexMatch[1]
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16)
|
||||
}
|
||||
}
|
||||
return { r: 255, g: 255, b: 255 }
|
||||
}
|
||||
|
||||
const onMainColorChange = (val: string) => {
|
||||
mainHex.value = val
|
||||
option.value.mainColor = val
|
||||
}
|
||||
const onShadowColorChange = (val: string) => {
|
||||
shadowColorStr.value = val
|
||||
option.value.shadowColor = val
|
||||
}
|
||||
|
||||
const original = ref<LyricOption | null>(null)
|
||||
|
||||
const loadOption = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await window.electron.ipcRenderer.invoke('get-desktop-lyric-option')
|
||||
if (res) {
|
||||
option.value = { ...option.value, ...res }
|
||||
original.value = { ...option.value }
|
||||
mainHex.value = option.value.mainColor || '#73BCFC'
|
||||
shadowColorStr.value = option.value.shadowColor || 'rgba(255,255,255,0.5)'
|
||||
shadowRgb.value = parseColorToRgb(shadowColorStr.value)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载桌面歌词配置失败:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const applyOption = () => {
|
||||
saving.value = true
|
||||
try {
|
||||
// 传入 callback=true 让桌面歌词窗口即时更新
|
||||
const payload = { ...option.value, shadowColor: shadowColorStr.value }
|
||||
window.electron.ipcRenderer.send('set-desktop-lyric-option', payload, true)
|
||||
} finally {
|
||||
setTimeout(() => (saving.value = false), 200)
|
||||
}
|
||||
}
|
||||
|
||||
const resetOption = () => {
|
||||
if (!original.value) return
|
||||
option.value = { ...original.value }
|
||||
applyOption()
|
||||
}
|
||||
|
||||
const toggleDesktopLyric = (enabled: boolean) => {
|
||||
window.electron.ipcRenderer.send('change-desktop-lyric', enabled)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOption()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lyric-style">
|
||||
<div class="header">
|
||||
<h3>桌面歌词样式</h3>
|
||||
<p>自定义桌面歌词的字体大小、颜色与阴影效果,并可预览与即时应用。</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="row">
|
||||
<div class="field">
|
||||
<label>字体大小(px)</label>
|
||||
<t-input-number v-model="option.fontSize" :min="12" :max="96" :step="1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>主颜色</label>
|
||||
<t-color-picker
|
||||
v-model="mainHex"
|
||||
:color-modes="['monochrome']"
|
||||
format="HEX"
|
||||
@change="onMainColorChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>阴影颜色</label>
|
||||
<t-color-picker
|
||||
v-model="shadowColorStr"
|
||||
:color-modes="['monochrome']"
|
||||
format="RGBA"
|
||||
:enable-alpha="true"
|
||||
@change="onShadowColorChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="field">
|
||||
<label>宽度</label>
|
||||
<t-input-number v-model="option.width" :min="300" :max="1600" :step="10" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>高度</label>
|
||||
<t-input-number v-model="option.height" :min="100" :max="600" :step="10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<t-button :loading="loading" theme="default" variant="outline" @click="loadOption"
|
||||
>刷新</t-button
|
||||
>
|
||||
<t-button :loading="saving" theme="primary" @click="applyOption">应用到桌面歌词</t-button>
|
||||
<t-button theme="default" @click="resetOption">还原</t-button>
|
||||
<t-switch @change="toggleDesktopLyric($event as boolean)">显示桌面歌词</t-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<div
|
||||
class="preview-lyric"
|
||||
:style="{
|
||||
fontSize: option.fontSize + 'px',
|
||||
color: mainHex,
|
||||
textShadow: `0 0 6px ${shadowColorStr}`
|
||||
}"
|
||||
>
|
||||
这是桌面歌词预览
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lyric-style {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.header h3 {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.color-input {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--td-border-level-1-color);
|
||||
border-radius: var(--td-radius-small);
|
||||
background: var(--td-bg-color-container);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.preview {
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--td-border-level-1-color);
|
||||
border-radius: var(--td-radius-medium);
|
||||
background: var(--settings-preview-bg);
|
||||
}
|
||||
.preview-lyric {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -606,8 +606,8 @@ const handleSuggestionSelect = (suggestion: any, _type: any) => {
|
||||
|
||||
.mainContent {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
// overflow-y: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 0;
|
||||
/* 确保flex子元素能够正确计算高度 */
|
||||
|
||||
@@ -67,18 +67,11 @@ function setAnimate(routerObj: RouteRecordRaw[]) {
|
||||
}
|
||||
}
|
||||
setAnimate(routes)
|
||||
|
||||
const option: RouterOptions = {
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
scrollBehavior(_to_, _from_, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
routes
|
||||
}
|
||||
|
||||
const router = createRouter(option)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -4,6 +4,8 @@ class AudioManager {
|
||||
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
|
||||
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
|
||||
private analysers = new Map<string, AnalyserNode>()
|
||||
// 为每个 audioElement 复用一个分流器,避免重复断开重连主链路
|
||||
private splitters = new WeakMap<HTMLAudioElement, GainNode>()
|
||||
|
||||
static getInstance(): AudioManager {
|
||||
if (!AudioManager.instance) {
|
||||
@@ -60,16 +62,19 @@ class AudioManager {
|
||||
analyser.fftSize = fftSize
|
||||
analyser.smoothingTimeConstant = 0.6
|
||||
|
||||
// 创建增益节点作为中介,避免直接断开主音频链
|
||||
const gainNode = context.createGain()
|
||||
gainNode.gain.value = 1.0
|
||||
// 复用每个 audioElement 的分流器:source -> splitter -> destination
|
||||
let splitter = this.splitters.get(audioElement)
|
||||
if (!splitter) {
|
||||
splitter = context.createGain()
|
||||
splitter.gain.value = 1.0
|
||||
// 仅第一次建立主链路,不要断开已有连接,避免累积
|
||||
source.connect(splitter)
|
||||
splitter.connect(context.destination)
|
||||
this.splitters.set(audioElement, splitter)
|
||||
}
|
||||
|
||||
// 连接:source -> gainNode -> analyser
|
||||
// -> destination (保持音频播放)
|
||||
source.disconnect() // 先断开所有连接
|
||||
source.connect(gainNode)
|
||||
gainNode.connect(context.destination) // 确保音频继续播放
|
||||
gainNode.connect(analyser) // 连接到分析器
|
||||
// 将分析器挂到分流器上,不影响主链路
|
||||
splitter.connect(analyser)
|
||||
|
||||
// 存储分析器引用
|
||||
this.analysers.set(id, analyser)
|
||||
@@ -104,6 +109,15 @@ class AudioManager {
|
||||
context.close()
|
||||
}
|
||||
|
||||
// 断开并移除分流器
|
||||
const splitter = this.splitters.get(audioElement)
|
||||
if (splitter) {
|
||||
try {
|
||||
splitter.disconnect()
|
||||
} catch {}
|
||||
this.splitters.delete(audioElement)
|
||||
}
|
||||
|
||||
this.audioSources.delete(audioElement)
|
||||
this.audioContexts.delete(audioElement)
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@ import { NotifyPlugin, MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
import { useSettingsStore } from '@renderer/store/Settings'
|
||||
import { toRaw, h } from 'vue'
|
||||
import {
|
||||
QUALITY_ORDER,
|
||||
getQualityDisplayName,
|
||||
buildQualityFormats,
|
||||
getHighestQualityType,
|
||||
compareQuality,
|
||||
type KnownQuality
|
||||
} from '@common/utils/quality'
|
||||
|
||||
interface MusicItem {
|
||||
singer: string
|
||||
@@ -18,44 +26,17 @@ interface MusicItem {
|
||||
typeUrl: Record<string, any>
|
||||
}
|
||||
|
||||
const qualityMap: Record<string, string> = {
|
||||
'128k': '标准音质',
|
||||
'192k': '高品音质',
|
||||
'320k': '超高品质',
|
||||
flac: '无损音质',
|
||||
flac24bit: '超高解析',
|
||||
hires: '高清臻音',
|
||||
atmos: '全景环绕',
|
||||
master: '超清母带'
|
||||
}
|
||||
const qualityKey = Object.keys(qualityMap)
|
||||
|
||||
// 创建音质选择弹窗
|
||||
function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const LocalUserDetail = LocalUserDetailStore()
|
||||
|
||||
// 获取歌曲支持的音质列表
|
||||
const availableQualities = songInfo.types || []
|
||||
|
||||
// 检查用户设置的音质是否为特殊音质
|
||||
const isSpecialQuality = ['hires', 'atmos', 'master'].includes(userQuality)
|
||||
|
||||
// 如果是特殊音质且用户支持,添加到选项中(不管歌曲是否有这个音质)
|
||||
const availableQualities = buildQualityFormats(songInfo.types || [])
|
||||
// 展示全部音质,但对超出用户最高音质的项做禁用呈现
|
||||
const userMaxIndex = QUALITY_ORDER.indexOf(userQuality as KnownQuality)
|
||||
const qualityOptions = [...availableQualities]
|
||||
if (isSpecialQuality && LocalUserDetail.userSource.quality === userQuality) {
|
||||
const hasSpecialQuality = availableQualities.some((q) => q.type === userQuality)
|
||||
if (!hasSpecialQuality) {
|
||||
qualityOptions.push({ type: userQuality, size: '源站无法得知此音质的文件大小' })
|
||||
}
|
||||
}
|
||||
|
||||
// 按音质优先级排序
|
||||
qualityOptions.sort((a, b) => {
|
||||
const aIndex = qualityKey.indexOf(a.type)
|
||||
const bIndex = qualityKey.indexOf(b.type)
|
||||
return bIndex - aIndex // 降序排列,高音质在前
|
||||
})
|
||||
// 按音质优先级排序(高→低)
|
||||
qualityOptions.sort((a, b) => compareQuality(a.type, b.type))
|
||||
|
||||
const dialog = DialogPlugin.confirm({
|
||||
header: '选择下载音质(可滚动)',
|
||||
@@ -80,35 +61,48 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
||||
msOverflowStyle: 'none'
|
||||
}
|
||||
},
|
||||
qualityOptions.map((quality) =>
|
||||
h(
|
||||
qualityOptions.map((quality) => {
|
||||
const idx = QUALITY_ORDER.indexOf(quality.type as KnownQuality)
|
||||
const disabled = userMaxIndex !== -1 && idx !== -1 && idx < userMaxIndex
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
key: quality.type,
|
||||
class: 'quality-item',
|
||||
title: disabled ? '超出你的最高音质设置,已禁用' : undefined,
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
margin: '8px 0',
|
||||
border: '1px solid #e7e7e7',
|
||||
border: '1px solid ' + (disabled ? '#f0f0f0' : '#e7e7e7'),
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
backgroundColor: quality.type === userQuality ? '#e6f7ff' : '#fff'
|
||||
backgroundColor:
|
||||
quality.type === userQuality ? (disabled ? '#f5faff' : '#e6f7ff') : '#fff',
|
||||
opacity: disabled ? 0.55 : 1
|
||||
},
|
||||
onClick: () => {
|
||||
if (disabled) return
|
||||
dialog.destroy()
|
||||
resolve(quality.type)
|
||||
},
|
||||
onMouseenter: (e: MouseEvent) => {
|
||||
if (disabled) return
|
||||
const target = e.target as HTMLElement
|
||||
target.style.backgroundColor = '#f0f9ff'
|
||||
target.style.borderColor = '#1890ff'
|
||||
},
|
||||
onMouseleave: (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (disabled) {
|
||||
target.style.backgroundColor =
|
||||
quality.type === userQuality ? '#f5faff' : '#fff'
|
||||
target.style.borderColor = '#f0f0f0'
|
||||
return
|
||||
}
|
||||
target.style.backgroundColor =
|
||||
quality.type === userQuality ? '#e6f7ff' : '#fff'
|
||||
target.style.borderColor = '#e7e7e7'
|
||||
@@ -122,17 +116,22 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
||||
style: {
|
||||
fontWeight: '500',
|
||||
fontSize: '14px',
|
||||
color: quality.type === userQuality ? '#1890ff' : '#333'
|
||||
color:
|
||||
quality.type === userQuality
|
||||
? disabled
|
||||
? '#8fbfff'
|
||||
: '#1890ff'
|
||||
: '#333'
|
||||
}
|
||||
},
|
||||
qualityMap[quality.type] || quality.type
|
||||
getQualityDisplayName(quality.type)
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
color: disabled ? '#bbb' : '#999',
|
||||
marginTop: '2px'
|
||||
}
|
||||
},
|
||||
@@ -145,7 +144,7 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
||||
class: 'quality-size',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
color: disabled ? '#999' : '#666',
|
||||
fontWeight: '500'
|
||||
}
|
||||
},
|
||||
@@ -153,7 +152,7 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
]
|
||||
),
|
||||
@@ -166,6 +165,7 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
||||
|
||||
async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||
try {
|
||||
console.log('开始下载', toRaw(songInfo))
|
||||
const LocalUserDetail = LocalUserDetailStore()
|
||||
const userQuality = LocalUserDetail.userSource.quality as string
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -186,68 +186,20 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
let quality = selectedQuality
|
||||
const isSpecialQuality = ['hires', 'atmos', 'master'].includes(quality)
|
||||
|
||||
// 如果选择的是特殊音质,先尝试下载
|
||||
if (isSpecialQuality) {
|
||||
try {
|
||||
console.log(`尝试下载特殊音质: ${quality} - ${qualityMap[quality]}`)
|
||||
const tip = MessagePlugin.success('开始下载歌曲:' + songInfo.name)
|
||||
|
||||
const specialResult = await window.api.music.requestSdk('downloadSingleSong', {
|
||||
pluginId: LocalUserDetail.userSource.pluginId?.toString() || '',
|
||||
source: songInfo.source,
|
||||
quality,
|
||||
songInfo: toRaw(songInfo) as any,
|
||||
tagWriteOptions: toRaw(settingsStore.settings.tagWriteOptions)
|
||||
})
|
||||
|
||||
;(await tip).close()
|
||||
|
||||
// 如果成功获取特殊音质链接,处理结果并返回
|
||||
if (specialResult) {
|
||||
if (!Object.hasOwn(specialResult, 'path')) {
|
||||
MessagePlugin.info(specialResult.message)
|
||||
} else {
|
||||
await NotifyPlugin.success({
|
||||
title: '下载成功',
|
||||
content: `${specialResult.message} 保存位置: ${specialResult.path}`
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`下载${qualityMap[quality]}音质失败,重新选择音质`)
|
||||
MessagePlugin.error('该音质下载失败,请重新选择音质')
|
||||
|
||||
// 特殊音质下载失败,重新弹出选择框
|
||||
const retryQuality = await createQualityDialog(songInfo, userQuality)
|
||||
if (!retryQuality) {
|
||||
return
|
||||
}
|
||||
quality = retryQuality
|
||||
} catch (specialError) {
|
||||
console.log(`下载${qualityMap[quality]}音质出错:`, specialError)
|
||||
MessagePlugin.error('该音质下载失败,请重新选择音质')
|
||||
|
||||
// 特殊音质下载出错,重新弹出选择框
|
||||
const retryQuality = await createQualityDialog(songInfo, userQuality)
|
||||
if (!retryQuality) {
|
||||
return
|
||||
}
|
||||
quality = retryQuality
|
||||
}
|
||||
}
|
||||
let quality = selectedQuality as string
|
||||
|
||||
// 检查选择的音质是否超出歌曲支持的最高音质
|
||||
const songMaxQuality = songInfo.types[songInfo.types.length - 1]?.type
|
||||
if (songMaxQuality && qualityKey.indexOf(quality) > qualityKey.indexOf(songMaxQuality)) {
|
||||
const songMaxQuality = getHighestQualityType(songInfo.types)
|
||||
if (
|
||||
songMaxQuality &&
|
||||
QUALITY_ORDER.indexOf(quality as KnownQuality) <
|
||||
QUALITY_ORDER.indexOf(songMaxQuality as KnownQuality)
|
||||
) {
|
||||
quality = songMaxQuality
|
||||
MessagePlugin.warning(`所选音质不可用,已自动调整为: ${qualityMap[quality]}`)
|
||||
MessagePlugin.warning(`所选音质不可用,已自动调整为: ${getQualityDisplayName(quality)}`)
|
||||
}
|
||||
|
||||
console.log(`使用音质下载: ${quality} - ${qualityMap[quality]}`)
|
||||
console.log(`使用音质下载: ${quality} - ${getQualityDisplayName(quality)}`)
|
||||
const tip = MessagePlugin.success('开始下载歌曲:' + songInfo.name)
|
||||
|
||||
const result = await window.api.music.requestSdk('downloadSingleSong', {
|
||||
@@ -255,7 +207,8 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||
source: songInfo.source,
|
||||
quality,
|
||||
songInfo: toRaw(songInfo) as any,
|
||||
tagWriteOptions: toRaw(settingsStore.settings.tagWriteOptions)
|
||||
tagWriteOptions: toRaw(settingsStore.settings.tagWriteOptions),
|
||||
isCache: true
|
||||
})
|
||||
|
||||
;(await tip).close()
|
||||
|
||||
@@ -42,38 +42,6 @@ export async function getSongRealUrl(song: SongList): Promise<string> {
|
||||
const settingsStore = useSettingsStore()
|
||||
const isCache = settingsStore.settings.autoCacheMusic ?? true
|
||||
|
||||
// 检查是否为特殊音质(高清臻音、全景环绕或超清母带)
|
||||
const isSpecialQuality = ['hires', 'atmos', 'master'].includes(quality)
|
||||
|
||||
// 如果是特殊音质,先尝试获取对应链接
|
||||
if (isSpecialQuality) {
|
||||
try {
|
||||
console.log(`尝试获取特殊音质: ${quality} - ${qualityMap[quality]}`)
|
||||
const specialUrlData = await window.api.music.requestSdk('getMusicUrl', {
|
||||
pluginId: LocalUserDetail.userSource.pluginId as unknown as string,
|
||||
source: song.source,
|
||||
songInfo: song as any,
|
||||
quality,
|
||||
isCache
|
||||
})
|
||||
|
||||
// 如果成功获取特殊音质链接,直接返回
|
||||
if (
|
||||
typeof specialUrlData === 'string' ||
|
||||
(typeof specialUrlData === 'object' && !specialUrlData.error)
|
||||
) {
|
||||
console.log(`成功获取${qualityMap[quality]}链接`)
|
||||
return specialUrlData as string
|
||||
}
|
||||
|
||||
console.log(`获取${qualityMap[quality]}链接失败,回退到标准逻辑`)
|
||||
// 如果获取特殊音质失败,继续执行原有逻辑
|
||||
} catch (specialError) {
|
||||
console.log(`获取${qualityMap[quality]}链接出错,回退到标准逻辑:`, specialError)
|
||||
// 特殊音质获取失败,继续执行原有逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 原有逻辑:检查歌曲支持的最高音质
|
||||
if (
|
||||
qualityKey.indexOf(quality) >
|
||||
@@ -115,16 +83,34 @@ export async function addToPlaylistAndPlay(
|
||||
playSongCallback: (song: SongList) => Promise<void>
|
||||
) {
|
||||
try {
|
||||
// 使用store的方法添加歌曲到第一位
|
||||
localUserStore.addSongToFirst(song)
|
||||
// 获取当前正在播放的歌曲索引
|
||||
const currentId = localUserStore.userInfo?.lastPlaySongId
|
||||
const currentIndex =
|
||||
currentId !== undefined && currentId !== null
|
||||
? localUserStore.list.findIndex((item: SongList) => item.songmid === currentId)
|
||||
: -1
|
||||
|
||||
// 播放歌曲 - 确保正确处理Promise
|
||||
// 如果目标歌曲已在列表中,先移除以避免重复
|
||||
const existingIndex = localUserStore.list.findIndex(
|
||||
(item: SongList) => item.songmid === song.songmid
|
||||
)
|
||||
if (existingIndex !== -1) {
|
||||
localUserStore.list.splice(existingIndex, 1)
|
||||
}
|
||||
|
||||
if (currentIndex !== -1) {
|
||||
// 正在播放:插入到当前歌曲的下一首
|
||||
localUserStore.list.splice(currentIndex + 1, 0, song)
|
||||
} else {
|
||||
// 未在播放:添加到第一位
|
||||
localUserStore.addSongToFirst(song)
|
||||
}
|
||||
|
||||
// 播放插入的歌曲
|
||||
const playResult = playSongCallback(song)
|
||||
if (playResult && typeof playResult.then === 'function') {
|
||||
await playResult
|
||||
}
|
||||
|
||||
// await MessagePlugin.success('已添加到播放列表并开始播放')
|
||||
} catch (error: any) {
|
||||
console.error('播放失败:', error)
|
||||
if (error.message) {
|
||||
|
||||
@@ -23,10 +23,3 @@
|
||||
<PlayMusic />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.animate__animated {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -221,6 +221,8 @@ onUnmounted(() => {
|
||||
.find-container {
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, toRaw, computed } from 'vue'
|
||||
import { ref, onMounted, toRaw, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
@@ -27,6 +27,10 @@ const LocalUserDetail = LocalUserDetailStore()
|
||||
// 响应式状态
|
||||
const songs = ref<MusicItem[]>([])
|
||||
const loading = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 50
|
||||
const currentSong = ref<MusicItem | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const playlistInfo = ref({
|
||||
@@ -60,8 +64,8 @@ const fetchPlaylistSongs = async () => {
|
||||
// 处理本地歌单
|
||||
await fetchLocalPlaylistSongs()
|
||||
} else {
|
||||
// 处理网络歌单
|
||||
await fetchNetworkPlaylistSongs()
|
||||
// 处理网络歌单(重置并加载第一页)
|
||||
await fetchNetworkPlaylistSongs(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌单歌曲失败:', error)
|
||||
@@ -116,22 +120,43 @@ const fetchLocalPlaylistSongs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络歌单歌曲
|
||||
const fetchNetworkPlaylistSongs = async () => {
|
||||
/**
|
||||
* 获取网络歌单歌曲,支持重置与分页追加
|
||||
* @param reset 是否重置为第一页
|
||||
*/
|
||||
const fetchNetworkPlaylistSongs = async (reset = false) => {
|
||||
try {
|
||||
// 调用API获取歌单详情和歌曲列表
|
||||
// 并发保护:首次加载使用 loading,分页加载使用 loadingMore
|
||||
if ((reset && !loading.value) || (!reset && loadingMore.value)) return
|
||||
|
||||
if (reset) {
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
songs.value = []
|
||||
loading.value = true
|
||||
} else {
|
||||
if (!hasMore.value) return
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
const result = (await window.api.music.requestSdk('getPlaylistDetail', {
|
||||
source: playlistInfo.value.source,
|
||||
id: playlistInfo.value.id,
|
||||
page: 1
|
||||
page: currentPage.value
|
||||
})) as any
|
||||
const limit = Number(result?.limit ?? pageSize)
|
||||
|
||||
console.log(result)
|
||||
if (result && result.list) {
|
||||
songs.value = result.list
|
||||
if (result && Array.isArray(result.list)) {
|
||||
const newList = result.list
|
||||
|
||||
// 获取歌曲封面
|
||||
setPic(0, playlistInfo.value.source)
|
||||
if (reset) {
|
||||
songs.value = newList
|
||||
} else {
|
||||
songs.value = [...songs.value, ...newList]
|
||||
}
|
||||
|
||||
// 获取新增歌曲封面
|
||||
setPic((currentPage.value - 1) * limit, playlistInfo.value.source)
|
||||
|
||||
// 如果API返回了歌单详细信息,更新歌单信息
|
||||
if (result.info) {
|
||||
@@ -143,10 +168,28 @@ const fetchNetworkPlaylistSongs = async () => {
|
||||
total: result.info.total || playlistInfo.value.total
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分页状态
|
||||
currentPage.value += 1
|
||||
const total = result.info?.total ?? playlistInfo.value.total ?? 0
|
||||
if (total) {
|
||||
hasMore.value = songs.value.length < total
|
||||
} else {
|
||||
hasMore.value = newList.length >= limit
|
||||
}
|
||||
} else {
|
||||
hasMore.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取网络歌单失败:', error)
|
||||
songs.value = []
|
||||
if (reset) songs.value = []
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
if (reset) {
|
||||
loading.value = false
|
||||
} else {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,45 +432,44 @@ const handleShufflePlaylist = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
// 滚动事件处理
|
||||
/**
|
||||
* 滚动事件处理:更新头部紧凑状态,并在接近底部时触发分页加载
|
||||
*/
|
||||
const handleScroll = (event?: Event) => {
|
||||
let scrollTop = 0
|
||||
let scrollHeight = 0
|
||||
let clientHeight = 0
|
||||
|
||||
if (event && event.target) {
|
||||
scrollTop = (event.target as HTMLElement).scrollTop
|
||||
const target = event.target as HTMLElement
|
||||
scrollTop = target.scrollTop
|
||||
scrollHeight = target.scrollHeight
|
||||
clientHeight = target.clientHeight
|
||||
} else if (scrollContainer.value) {
|
||||
scrollTop = scrollContainer.value.scrollTop
|
||||
scrollHeight = scrollContainer.value.scrollHeight
|
||||
clientHeight = scrollContainer.value.clientHeight
|
||||
}
|
||||
|
||||
scrollY.value = scrollTop
|
||||
// 当滚动超过100px时,启用紧凑模式
|
||||
isHeaderCompact.value = scrollY.value > 100
|
||||
|
||||
// 触底加载(参考 search.vue)
|
||||
if (
|
||||
scrollHeight > 0 &&
|
||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||
!loadingMore.value &&
|
||||
hasMore.value &&
|
||||
!isLocalPlaylist.value
|
||||
) {
|
||||
fetchNetworkPlaylistSongs(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchPlaylistSongs()
|
||||
|
||||
// 延迟添加滚动事件监听,等待 SongVirtualList 组件渲染完成
|
||||
setTimeout(() => {
|
||||
// 查找 SongVirtualList 内部的虚拟滚动容器
|
||||
const virtualListContainer = document.querySelector('.virtual-scroll-container')
|
||||
|
||||
if (virtualListContainer) {
|
||||
scrollContainer.value = virtualListContainer as HTMLElement
|
||||
virtualListContainer.addEventListener('scroll', handleScroll, { passive: true })
|
||||
console.log('滚动监听器已添加到:', virtualListContainer)
|
||||
} else {
|
||||
console.warn('未找到虚拟滚动容器')
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
onUnmounted(() => {
|
||||
if (scrollContainer.value) {
|
||||
scrollContainer.value.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ const localSongs = ref<LocalSong[]>([
|
||||
// 歌单列表
|
||||
const playlists = ref<SongList[]>([])
|
||||
const loading = ref(false)
|
||||
// 喜欢歌单ID(用于排序与标记)
|
||||
const favoritesId = ref<string | null>(null)
|
||||
|
||||
// 对话框状态
|
||||
const showCreatePlaylistDialog = ref(false)
|
||||
@@ -192,6 +194,18 @@ const loadPlaylists = async () => {
|
||||
const result = await songListAPI.getAll()
|
||||
if (result.success) {
|
||||
playlists.value = result.data || []
|
||||
// 读取“我的喜欢”ID并置顶与标记
|
||||
try {
|
||||
const favRes = await (window as any).api?.songList?.getFavoritesId?.()
|
||||
favoritesId.value = (favRes && favRes.data) || null
|
||||
if (favoritesId.value) {
|
||||
const idx = playlists.value.findIndex((p) => p.id === favoritesId.value)
|
||||
if (idx > 0) {
|
||||
const fav = playlists.value.splice(idx, 1)[0]
|
||||
playlists.value.unshift(fav)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
MessagePlugin.error(result.error || '加载歌单失败')
|
||||
}
|
||||
@@ -517,7 +531,7 @@ const setPicForPlaylist = async (songs: any[], source: string) => {
|
||||
// 处理网络歌单导入
|
||||
const handleNetworkPlaylistImport = async (input: string) => {
|
||||
try {
|
||||
const load1 = MessagePlugin.loading('正在解析歌单链接...')
|
||||
const load1 = MessagePlugin.loading('正在解析歌单链接...', 0)
|
||||
|
||||
let playlistId: string = ''
|
||||
let platformName: string = ''
|
||||
@@ -541,38 +555,51 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
||||
}
|
||||
platformName = '网易云音乐'
|
||||
} else if (importPlatformType.value === 'tx') {
|
||||
// QQ音乐歌单ID解析 - 支持多种链接格式
|
||||
const qqPlaylistRegexes = [
|
||||
// 标准歌单链接
|
||||
/(?:y\.qq\.com\/n\/ryqq\/playlist\/|music\.qq\.com\/.*[?&]id=|playlist[?&]id=)(\d+)/i,
|
||||
// 分享链接格式
|
||||
/(?:i\.y\.qq\.com\/n2\/m\/share\/details\/taoge\.html.*[?&]id=)(\d+)/i,
|
||||
// 其他可能的分享格式
|
||||
/(?:c\.y\.qq\.com\/base\/fcgi-bin\/u\?.*__=)(\d+)/i,
|
||||
// 手机版链接
|
||||
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i,
|
||||
// 通用ID提取 - 匹配 id= 或 &id= 参数
|
||||
/[?&]id=(\d+)/i
|
||||
]
|
||||
// QQ音乐歌单ID解析:优先通过 SDK 解析,失败再回退到正则
|
||||
let parsedId = ''
|
||||
try {
|
||||
const parsed: any = await window.api.music.requestSdk('parsePlaylistId', {
|
||||
source: 'tx',
|
||||
url: input
|
||||
})
|
||||
console.log('QQ音乐歌单解析结果', parsed)
|
||||
if (parsed) parsedId = parsed
|
||||
} catch (e) {}
|
||||
|
||||
let match: RegExpMatchArray | null = null
|
||||
for (const regex of qqPlaylistRegexes) {
|
||||
match = input.match(regex)
|
||||
if (match && match[1]) {
|
||||
playlistId = match[1]
|
||||
break
|
||||
if (parsedId) {
|
||||
playlistId = parsedId
|
||||
} else {
|
||||
const qqPlaylistRegexes = [
|
||||
// 标准歌单链接(强烈推荐)
|
||||
/(?:y\.qq\.com\/n\/ryqq\/playlist\/|music\.qq\.com\/.*[?&]id=|playlist[?&]id=)(\d+)/i,
|
||||
// 分享链接格式
|
||||
/(?:i\.y\.qq\.com\/n2\/m\/share\/details\/taoge\.html.*[?&]id=)(\d+)/i,
|
||||
// 其他可能的分享格式 https:\/\/c\d+\.y\.qq\.com\/base\/fcgi-bin\/u\?.*__=([A-Za-z0-9]+)/i,
|
||||
// 手机版链接
|
||||
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i,
|
||||
// 通用ID提取 - 匹配 id= 或 &id= 参数
|
||||
/[?&]id=(\d+)/i
|
||||
]
|
||||
|
||||
let match: RegExpMatchArray | null = null
|
||||
for (const regex of qqPlaylistRegexes) {
|
||||
match = input.match(regex)
|
||||
if (match && match[1]) {
|
||||
playlistId = match[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!match || !match[1]) {
|
||||
// 检查是否直接输入的是纯数字ID
|
||||
const numericMatch = input.match(/^\d+$/)
|
||||
if (numericMatch) {
|
||||
playlistId = input
|
||||
} else {
|
||||
MessagePlugin.error('无法识别的QQ音乐歌单链接或ID格式,请检查链接是否正确')
|
||||
load1.then((res) => res.close())
|
||||
return
|
||||
if (!match || !match[1]) {
|
||||
// 检查是否直接输入的是纯数字ID
|
||||
const numericMatch = input.match(/^\d+$/)
|
||||
if (numericMatch) {
|
||||
playlistId = input
|
||||
} else {
|
||||
MessagePlugin.error('无法识别的QQ音乐歌单链接或ID格式,请检查链接是否正确')
|
||||
load1.then((res) => res.close())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
platformName = 'QQ音乐'
|
||||
@@ -680,18 +707,11 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证歌单ID是否有效
|
||||
if (!playlistId || playlistId.length < 6) {
|
||||
MessagePlugin.error('歌单ID格式不正确')
|
||||
load1.then((res) => res.close())
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭加载提示
|
||||
load1.then((res) => res.close())
|
||||
|
||||
// 获取歌单详情
|
||||
const load2 = MessagePlugin.loading('正在获取歌单信息...')
|
||||
const load2 = MessagePlugin.loading('正在获取歌单信息,请不要离开页面...', 0)
|
||||
|
||||
const getListDetail = async (page: number) => {
|
||||
let detailResult: any
|
||||
@@ -701,6 +721,7 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
||||
id: playlistId,
|
||||
page: page
|
||||
})) as any
|
||||
console.log('list', detailResult)
|
||||
} catch {
|
||||
MessagePlugin.error(`获取${platformName}歌单详情失败:歌曲信息可能有误`)
|
||||
load2.then((res) => res.close())
|
||||
@@ -728,6 +749,7 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (detailResult.total < songs.length) break
|
||||
page++
|
||||
const { list: songsList } = await getListDetail(page)
|
||||
if (!(songsList && songsList.length)) {
|
||||
@@ -1022,6 +1044,14 @@ onMounted(() => {
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-name" :title="playlist.name" @click="viewPlaylist(playlist)">
|
||||
{{ playlist.name }}
|
||||
<t-tag
|
||||
v-if="playlist.id === favoritesId"
|
||||
theme="danger"
|
||||
variant="light-outline"
|
||||
size="small"
|
||||
style="margin-left: 6px"
|
||||
>我的喜欢</t-tag
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="playlist.description"
|
||||
@@ -1441,7 +1471,8 @@ onMounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
width: 100%;
|
||||
// height: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.local-container {
|
||||
padding: 2rem;
|
||||
@@ -1774,7 +1805,7 @@ onMounted(() => {
|
||||
|
||||
.playlist-cover {
|
||||
height: 180px;
|
||||
background: linear-gradient(135deg, var(--td-brand-color-4) 0%, var(--td-brand-color-6) 100%);
|
||||
background: #e4e4e4;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -210,7 +210,9 @@ const formatPlayTime = (timeStr: string): string => {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
// min-height: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
|
||||
@@ -233,6 +233,7 @@ const handleScroll = (event: Event) => {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
|
||||
@@ -18,6 +18,7 @@ import DirectorySettings from '@renderer/components/Settings/DirectorySettings.v
|
||||
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
||||
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
||||
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
||||
import DesktopLyricStyle from '@renderer/components/Settings/DesktopLyricStyle.vue'
|
||||
import Versions from '@renderer/components/Versions.vue'
|
||||
import { useAutoUpdate } from '@renderer/composables/useAutoUpdate'
|
||||
import { playSetting as usePlaySetting } from '@renderer/store/playSetting'
|
||||
@@ -419,6 +420,10 @@ const getTagOptionsStatus = () => {
|
||||
<h3>应用主题色</h3>
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<DesktopLyricStyle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 功能设置 -->
|
||||
|
||||
541
src/web/lyric.html
Normal file
541
src/web/lyric.html
Normal file
@@ -0,0 +1,541 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>澜音 - 桌面歌词</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-size: 30;
|
||||
--main-color: #73BCFC;
|
||||
--shadow-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--main-color);
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
cursor: move;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
.meta {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tools {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.lock-lyric {
|
||||
cursor: none;
|
||||
/* 鼠标穿透 */
|
||||
pointer-events: none;
|
||||
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
.meta {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tools {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
gap: 8px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
background-color 0.3s;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#song-artist {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
margin: 12px;
|
||||
z-index: 1;
|
||||
max-width: 100%;
|
||||
pointer-events: auto;
|
||||
|
||||
#lyric-text {
|
||||
font-size: calc(var(--font-size) * 1px);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#lyric-tran {
|
||||
font-size: calc(var(--font-size) * 1px - 5px);
|
||||
margin-top: 8px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 4px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 0 4px var(--shadow-color);
|
||||
transition: opacity 0.3s;
|
||||
/* animation: 15s wordsLoop linear infinite normal; */
|
||||
}
|
||||
|
||||
@keyframes wordsLoop {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="meta">
|
||||
<span id="song-name">CeruMusic(澜音)</span>
|
||||
<span id="song-artist">未知艺术家</span>
|
||||
</div>
|
||||
<div class="tools" id="tools">
|
||||
<div id="show-app" class="item" title="打开应用">
|
||||
<svg
|
||||
width="1200"
|
||||
height="1200"
|
||||
viewBox="0 0 1200 1200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="1200" height="1200" rx="379" fill="white" />
|
||||
<path
|
||||
d="M957.362 204.197C728.535 260.695 763.039 192.264 634.41 175.368C451.817 151.501 504.125 315.925 504.125 315.925L630.545 673.497C591.211 654.805 544.287 643.928 494.188 643.928C353.275 643.928 239 729.467 239 834.964C239 940.567 353.137 1026 494.188 1026C635.1 1026 749.375 940.461 749.375 834.964C749.375 832.218 749.237 829.473 749.099 826.727C749.513 825.988 749.789 825.143 750.065 824.087C757.932 789.449 634.272 348.345 634.272 348.345C634.272 348.345 764.971 401.886 860.89 351.936C971.163 294.699 964.953 202.402 957.362 204.197Z"
|
||||
fill="url(#paint0_linear_4_16)"
|
||||
stroke="#29293A"
|
||||
stroke-opacity="0.23"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_4_16"
|
||||
x1="678.412"
|
||||
y1="-1151.29"
|
||||
x2="796.511"
|
||||
y2="832.071"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.572115" stop-color="#B8F1ED" />
|
||||
<stop offset="0.9999" stop-color="#B8F1CC" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="font-size-reduce" class="item" title="缩小字体">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="font-size-add" class="item" title="放大字体">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="play-prev" class="item" title="上一首">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1m3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07a1 1 0 0 0 0 1.64"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 播放暂停 -->
|
||||
<div id="pause" class="item hidden" title="暂停">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2m6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="play" class="item" title="播放">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18a1 1 0 0 0 0-1.69L9.54 5.98A.998.998 0 0 0 8 6.82"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="play-next" class="item" title="下一首">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82M16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 锁定 -->
|
||||
<div id="lock-lyric" class="item" title="锁定/解锁">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 关闭 -->
|
||||
<div id="close-lyric" class="item" title="关闭">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main id="lyric-content">
|
||||
<span id="lyric-text">该歌曲暂无歌词</span>
|
||||
<span id="lyric-tran"></span>
|
||||
</main>
|
||||
<script>
|
||||
class LyricsWindow {
|
||||
constructor() {
|
||||
// 获取元素
|
||||
this.songNameDom = document.getElementById('song-name')
|
||||
this.songArtistDom = document.getElementById('song-artist')
|
||||
this.lyricContentDom = document.getElementById('lyric-content')
|
||||
this.lyricTextDom = document.getElementById('lyric-text')
|
||||
this.lyricTranDom = document.getElementById('lyric-tran')
|
||||
this.pauseDom = document.getElementById('pause')
|
||||
this.playDom = document.getElementById('play')
|
||||
// 窗口位置
|
||||
this.isDragging = false
|
||||
this.startX = 0
|
||||
this.startY = 0
|
||||
this.startWinX = 0
|
||||
this.startWinY = 0
|
||||
this.winWidth = 0
|
||||
this.winHeight = 0
|
||||
// 临时变量
|
||||
// this.lyricIndex = -1;
|
||||
// 初始化
|
||||
this.restoreOptions()
|
||||
this.menuClick()
|
||||
this.setupIPCListeners()
|
||||
this.setupWindowDragListeners()
|
||||
this.setupMutationObserver()
|
||||
}
|
||||
// 歌词切换动画
|
||||
updateLyrics(content = '纯音乐,请欣赏', translation = '') {
|
||||
// document.startViewTransition(() => {
|
||||
// this.lyricTextDom.innerHTML = content;
|
||||
// this.lyricTranDom.innerHTML = translation;
|
||||
// });
|
||||
this.lyricTextDom.innerHTML = content
|
||||
this.lyricTranDom.innerHTML = translation
|
||||
}
|
||||
// 获取配置
|
||||
async restoreOptions() {
|
||||
try {
|
||||
const defaultOptions = await window.electron.ipcRenderer.invoke(
|
||||
'get-desktop-lyric-option'
|
||||
)
|
||||
if (defaultOptions) this.changeOptions(defaultOptions)
|
||||
return defaultOptions
|
||||
} catch (error) {
|
||||
console.error('Failed to restore options:', error)
|
||||
}
|
||||
}
|
||||
// 修改配置
|
||||
changeOptions(options, callback = true) {
|
||||
if (!options) return
|
||||
const { fontSize, mainColor, shadowColor } = options
|
||||
document.documentElement.style.setProperty('--font-size', fontSize)
|
||||
document.documentElement.style.setProperty('--main-color', mainColor)
|
||||
document.documentElement.style.setProperty('--shadow-color', shadowColor)
|
||||
if (callback) window.electron.ipcRenderer.send('set-desktop-lyric-option', options)
|
||||
}
|
||||
// 菜单点击事件
|
||||
menuClick() {
|
||||
const toolsDom = document.getElementById('tools')
|
||||
if (!toolsDom) return
|
||||
// 菜单项点击
|
||||
toolsDom.addEventListener('click', async (event) => {
|
||||
const target = event.target.closest('div')
|
||||
if (!target) return
|
||||
console.log(target)
|
||||
const id = target.id
|
||||
if (!id) return
|
||||
// 获取配置
|
||||
const options = await this.restoreOptions()
|
||||
switch (id) {
|
||||
case 'show-app': {
|
||||
window.electron.ipcRenderer.send('win-show')
|
||||
break
|
||||
}
|
||||
case 'font-size-add': {
|
||||
let fontSize = options.fontSize
|
||||
if (fontSize < 60) {
|
||||
fontSize++
|
||||
this.changeOptions({ ...options, fontSize })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'font-size-reduce': {
|
||||
let fontSize = options.fontSize
|
||||
if (fontSize > 10) {
|
||||
fontSize--
|
||||
this.changeOptions({ ...options, fontSize })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'play': {
|
||||
window.electron.ipcRenderer.send('send-main-event', 'play')
|
||||
break
|
||||
}
|
||||
case 'pause': {
|
||||
window.electron.ipcRenderer.send('send-main-event', 'pause')
|
||||
break
|
||||
}
|
||||
case 'play-prev': {
|
||||
window.electron.ipcRenderer.send('send-main-event', 'playPrev')
|
||||
break
|
||||
}
|
||||
case 'play-next': {
|
||||
window.electron.ipcRenderer.send('send-main-event', 'playNext')
|
||||
break
|
||||
}
|
||||
case 'close-lyric': {
|
||||
window.electron.ipcRenderer.send('closeDesktopLyric')
|
||||
break
|
||||
}
|
||||
case 'lock-lyric': {
|
||||
const locked = !document.body.classList.contains('lock-lyric')
|
||||
document.body.classList.toggle('lock-lyric', locked)
|
||||
window.electron.ipcRenderer.send('toogleDesktopLyricLock', locked)
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
// 监听 IPC 事件
|
||||
setupIPCListeners() {
|
||||
window.electron.ipcRenderer.on('play-song-change', (_, title) => {
|
||||
if (!title) return
|
||||
const [songName, songArtist] = title.split(' - ')
|
||||
this.songNameDom.innerHTML = songName
|
||||
this.songArtistDom.innerHTML = songArtist
|
||||
this.updateLyrics(title)
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('play-lyric-change', (_, lyricData) => {
|
||||
if (!lyricData) return
|
||||
this.parsedLyricsData(lyricData)
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('play-status-change', (_, status) => {
|
||||
this.playDom.classList.toggle('hidden', status)
|
||||
this.pauseDom.classList.toggle('hidden', !status)
|
||||
})
|
||||
|
||||
// 配置变化
|
||||
window.electron.ipcRenderer.on('desktop-lyric-option-change', (_, options) => {
|
||||
this.changeOptions(options, false)
|
||||
})
|
||||
|
||||
// 歌词锁定(仅更新样式,不再回传,避免事件循环)
|
||||
window.electron.ipcRenderer.on('toogleDesktopLyricLock', (_, lock) => {
|
||||
document.body.classList.toggle('lock-lyric', lock)
|
||||
})
|
||||
}
|
||||
// 解析歌词
|
||||
parsedLyricsData(lyricData) {
|
||||
if (!this.lyricContentDom || !this.lyricTextDom) return
|
||||
const { index, lyric } = lyricData
|
||||
// 更换文字
|
||||
if (!lyric || index < 0) {
|
||||
if (lyric.length === 0) this.updateLyrics()
|
||||
} else {
|
||||
const { content, tran } = lyric[index]
|
||||
this.updateLyrics(content, tran || '')
|
||||
}
|
||||
}
|
||||
// 拖拽窗口
|
||||
setupWindowDragListeners() {
|
||||
document.addEventListener('mousedown', this.startDrag.bind(this))
|
||||
document.addEventListener('mousemove', this.dragWindow.bind(this))
|
||||
document.addEventListener('mouseup', this.endDrag.bind(this))
|
||||
}
|
||||
// 开始拖拽
|
||||
async startDrag(event) {
|
||||
this.isDragging = true
|
||||
const { screenX, screenY } = event
|
||||
const {
|
||||
x: winX,
|
||||
y: winY,
|
||||
width,
|
||||
height
|
||||
} = await window.electron.ipcRenderer.invoke('get-window-bounds')
|
||||
this.startX = screenX
|
||||
this.startY = screenY
|
||||
this.startWinX = winX
|
||||
this.startWinY = winY
|
||||
this.winWidth = width
|
||||
this.winHeight = height
|
||||
}
|
||||
// 拖拽
|
||||
async dragWindow(event) {
|
||||
if (!this.isDragging) return
|
||||
const { screenX, screenY } = event
|
||||
let newWinX = this.startWinX + (screenX - this.startX)
|
||||
let newWinY = this.startWinY + (screenY - this.startY)
|
||||
const { width: screenWidth, height: screenHeight } =
|
||||
await window.electron.ipcRenderer.invoke('get-screen-size')
|
||||
newWinX = Math.max(0, Math.min(screenWidth - this.winWidth, newWinX))
|
||||
newWinY = Math.max(0, Math.min(screenHeight - this.winHeight, newWinY))
|
||||
window.electron.ipcRenderer.send(
|
||||
'move-window',
|
||||
newWinX,
|
||||
newWinY,
|
||||
this.winWidth,
|
||||
this.winHeight
|
||||
)
|
||||
}
|
||||
// 结束拖拽
|
||||
endDrag() {
|
||||
this.isDragging = false
|
||||
}
|
||||
// 更新高度
|
||||
updateWindowHeight() {
|
||||
const bodyHeight = document.body.scrollHeight
|
||||
window.electron.ipcRenderer.send('update-window-height', bodyHeight)
|
||||
}
|
||||
// 动态监听高度
|
||||
setupMutationObserver() {
|
||||
const observer = new MutationObserver(this.updateWindowHeight.bind(this))
|
||||
observer.observe(document.body, { childList: true, subtree: true, attributes: true })
|
||||
this.updateWindowHeight()
|
||||
}
|
||||
}
|
||||
|
||||
new LyricsWindow()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user