mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 19:37:38 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce743e1b65 | ||
|
|
32c9fdbfeb | ||
|
|
9df236b2e0 | ||
|
|
0988c71282 | ||
|
|
60881f7f48 | ||
|
|
775f87aa86 | ||
|
|
b1c471f15c | ||
|
|
f7ecfa1fa9 | ||
|
|
d44be6022a | ||
|
|
0c512bccff |
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%;" />
|
<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 |
@@ -14,5 +14,7 @@
|
|||||||
| 青禾 | 8.8 |
|
| 青禾 | 8.8 |
|
||||||
| li peng | 200 |
|
| li peng | 200 |
|
||||||
| **群友**:XIZ | 3 |
|
| **群友**:XIZ | 3 |
|
||||||
|
| YL | 10 |
|
||||||
|
| **群友**:way1437 | 50 |
|
||||||
|
|
||||||
据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn
|
据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ features:
|
|||||||
|
|
||||||
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,**仅提供插件运行框架与播放功能**,不直接存储、提供任何音乐源文件。用户需通过自行选择、安装合规插件获取音乐相关数据,项目旨在为开发者提供桌面应用技术实践与学习案例,为用户提供合规的音乐播放工具框架。
|
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>
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ceru-music",
|
"name": "ceru-music",
|
||||||
"version": "1.4.1",
|
"version": "1.4.6",
|
||||||
"description": "一款简洁优雅的音乐播放器",
|
"description": "一款简洁优雅的音乐播放器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "sqj,wldss,star",
|
"author": "sqj,wldss,star",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||||
"@pixi/filter-color-matrix": "^7.4.3",
|
"@pixi/filter-color-matrix": "^7.4.3",
|
||||||
"@pixi/sprite": "^7.4.3",
|
"@pixi/sprite": "^7.4.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.27",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/needle": "^3.3.0",
|
"@types/needle": "^3.3.0",
|
||||||
"NeteaseCloudMusicApi": "^4.27.0",
|
"NeteaseCloudMusicApi": "^4.27.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@@ -57,8 +57,7 @@
|
|||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"howler": "^2.2.4",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
|
||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"iconv-lite": "^0.7.0",
|
"iconv-lite": "^0.7.0",
|
||||||
"jss": "^10.10.0",
|
"jss": "^10.10.0",
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"needle": "^3.3.1",
|
"needle": "^3.3.1",
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"node-id3": "^0.2.9",
|
"node-taglib-sharp": "^6.0.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"tdesign-icons-vue-next": "^0.4.1",
|
"tdesign-icons-vue-next": "^0.4.1",
|
||||||
|
|||||||
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))
|
||||||
|
}
|
||||||
150
src/main/events/index.ts
Normal file
150
src/main/events/index.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import InitPluginService from './plugins'
|
||||||
|
import '../services/musicSdk/index'
|
||||||
|
import aiEvents from '../events/ai'
|
||||||
|
import { app, powerSaveBlocker, Menu } from 'electron'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { type BrowserWindow, Tray, 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: Tray | null = null
|
||||||
|
let isQuitting = false
|
||||||
|
function createTray(): void {
|
||||||
|
// 创建系统托盘
|
||||||
|
const trayIconPath = path.join(__dirname, '../../resources/logo.png')
|
||||||
|
tray = new Tray(trayIconPath)
|
||||||
|
|
||||||
|
// 创建托盘菜单
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示窗口',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '播放/暂停',
|
||||||
|
click: () => {
|
||||||
|
// 这里可以添加播放控制逻辑
|
||||||
|
console.log('music-control')
|
||||||
|
mainWindow?.webContents.send('music-control')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
isQuitting = true
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
tray.setContextMenu(contextMenu)
|
||||||
|
tray.setToolTip('Ceru Music')
|
||||||
|
|
||||||
|
// 单击托盘图标显示窗口
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide()
|
||||||
|
} else {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
createTray()
|
||||||
|
// 应用退出前的清理
|
||||||
|
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()
|
||||||
|
// 显示托盘通知(可选)
|
||||||
|
if (tray) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 显示托盘通知
|
||||||
|
if (tray) {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
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 { ipcMain } from 'electron'
|
||||||
import ManageSongList, { SongListError } from '../services/songList/ManageSongList'
|
import ManageSongList, { SongListError } from '../services/songList/ManageSongList'
|
||||||
import type { SongList, Songs } from '@common/types/songList'
|
import type { SongList, Songs } from '@common/types/songList'
|
||||||
|
import { configManager } from '../services/ConfigManager'
|
||||||
|
|
||||||
// 创建新歌单
|
// 创建新歌单
|
||||||
ipcMain.handle(
|
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 () => {
|
ipcMain.handle('songlist:get-all', async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { app, shell, BrowserWindow, ipcMain, Tray, Menu, screen, powerSaveBlocker } from 'electron'
|
import { app, shell, BrowserWindow, ipcMain, screen, Rectangle, Display } from 'electron'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/logo.png?asset'
|
import icon from '../../resources/logo.png?asset'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import pluginService from './services/plugin'
|
import InitEventServices from './events'
|
||||||
import aiEvents from './events/ai'
|
|
||||||
import './services/musicSdk/index'
|
import './events/musicCache'
|
||||||
|
import './events/songList'
|
||||||
|
import './events/directorySettings'
|
||||||
|
import './events/pluginNotice'
|
||||||
|
|
||||||
// 获取单实例锁
|
// 获取单实例锁
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
@@ -25,87 +29,44 @@ 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 psbId: number | null = null
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let isQuitting = false
|
|
||||||
|
|
||||||
function createTray(): void {
|
/**
|
||||||
// 创建系统托盘
|
* 根据窗口当前所在的显示器,动态更新窗口的最大尺寸限制。
|
||||||
const trayIconPath = path.join(__dirname, '../../resources/logo.png')
|
* 这样可以确保窗口在任何显示器上都能正常最大化或全屏。
|
||||||
tray = new Tray(trayIconPath)
|
* @param {BrowserWindow} win - 要更新的窗口实例
|
||||||
|
*/
|
||||||
|
function updateWindowMaxLimits(win: BrowserWindow | null): void {
|
||||||
|
if (!win) return
|
||||||
|
|
||||||
// 创建托盘菜单
|
// 1. 获取窗口的当前边界 (bounds)
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const currentBounds: Rectangle = win.getBounds()
|
||||||
{
|
|
||||||
label: '显示窗口',
|
|
||||||
click: () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '播放/暂停',
|
|
||||||
click: () => {
|
|
||||||
// 这里可以添加播放控制逻辑
|
|
||||||
console.log('music-control')
|
|
||||||
mainWindow?.webContents.send('music-control')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
isQuitting = true
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
tray.setContextMenu(contextMenu)
|
// 2. 查找包含该边界的显示器
|
||||||
tray.setToolTip('Ceru Music')
|
const currentDisplay: Display = screen.getDisplayMatching(currentBounds)
|
||||||
|
|
||||||
// 双击托盘图标显示窗口
|
// 3. 获取该显示器的完整尺寸 (full screen size)
|
||||||
tray.on('click', () => {
|
const { width: currentScreenWidth, height: currentScreenHeight } = currentDisplay.size
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isVisible()) {
|
// 4. 应用新的最大尺寸限制
|
||||||
mainWindow.hide()
|
// 移除 maxWidth/maxHeight 上的硬限制,使其能够最大化到当前屏幕的尺寸。
|
||||||
} else {
|
// 注意:设置为 0, 0 意味着没有最小限制,我们只关注最大限制。
|
||||||
mainWindow.show()
|
win.setMaximumSize(currentScreenWidth, currentScreenHeight)
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// return
|
|
||||||
// 获取保存的窗口位置和大小
|
// 获取保存的窗口位置和大小
|
||||||
const savedBounds = configManager.getWindowBounds()
|
const savedBounds = configManager.getWindowBounds()
|
||||||
|
|
||||||
// 获取屏幕尺寸
|
|
||||||
const primaryDisplay = screen.getPrimaryDisplay()
|
|
||||||
// 使用完整屏幕尺寸而不是工作区域,以支持真正的全屏模式
|
|
||||||
const { width: screenWidth, height: screenHeight } = primaryDisplay.size
|
|
||||||
|
|
||||||
// 默认窗口配置
|
// 默认窗口配置
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
width: 1100,
|
width: 1100,
|
||||||
height: 750,
|
height: 750,
|
||||||
minWidth: 1100,
|
minWidth: 1100,
|
||||||
minHeight: 670,
|
minHeight: 670,
|
||||||
maxWidth: screenWidth,
|
// ⚠️ 关键修改 1: 移除 maxWidth 和 maxHeight 的硬编码限制
|
||||||
maxHeight: screenHeight,
|
// maxWidth: screenWidth,
|
||||||
|
// maxHeight: screenHeight,
|
||||||
show: false,
|
show: false,
|
||||||
center: !savedBounds, // 如果有保存的位置,则不居中
|
center: !savedBounds, // 如果有保存的位置,则不居中
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
@@ -131,24 +92,30 @@ function createWindow(): void {
|
|||||||
mainWindow = new BrowserWindow(defaultOptions)
|
mainWindow = new BrowserWindow(defaultOptions)
|
||||||
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
||||||
|
|
||||||
// 监听窗口移动和调整大小事件,保存窗口位置和大小
|
// ⚠️ 关键修改 2: 监听 'moved' 事件,动态更新最大尺寸
|
||||||
mainWindow.on('moved', () => {
|
mainWindow.on('moved', () => {
|
||||||
|
// 当窗口移动时,确保最大尺寸限制随屏幕变化
|
||||||
|
updateWindowMaxLimits(mainWindow)
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||||
const bounds = mainWindow.getBounds()
|
const bounds = mainWindow.getBounds()
|
||||||
configManager.saveWindowBounds(bounds)
|
configManager.saveWindowBounds(bounds)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ⚠️ 关键修改 3: 窗口创建后立即应用一次最大尺寸限制
|
||||||
|
updateWindowMaxLimits(mainWindow)
|
||||||
|
|
||||||
mainWindow.on('resized', () => {
|
mainWindow.on('resized', () => {
|
||||||
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||||
const bounds = mainWindow.getBounds()
|
const bounds = mainWindow.getBounds()
|
||||||
|
|
||||||
// 获取当前屏幕尺寸
|
// 获取当前屏幕尺寸 (已在文件顶部导入 screen,无需 require)
|
||||||
const { screen } = require('electron')
|
|
||||||
const currentDisplay = screen.getDisplayMatching(bounds)
|
const currentDisplay = screen.getDisplayMatching(bounds)
|
||||||
|
// 使用 workAreaSize 避免窗口超出任务栏/Dock
|
||||||
const { width: screenWidth, height: screenHeight } = currentDisplay.workAreaSize
|
const { width: screenWidth, height: screenHeight } = currentDisplay.workAreaSize
|
||||||
|
|
||||||
// 确保窗口不超过屏幕尺寸
|
// 确保窗口不超过屏幕工作区域尺寸
|
||||||
let needResize = false
|
let needResize = false
|
||||||
const newBounds = { ...bounds }
|
const newBounds = { ...bounds }
|
||||||
|
|
||||||
@@ -174,29 +141,11 @@ function createWindow(): void {
|
|||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow?.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) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url).then()
|
shell.openExternal(details.url).then()
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
InitEventServices(mainWindow)
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
@@ -206,80 +155,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'
|
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
@@ -296,16 +171,6 @@ app.whenReady().then(() => {
|
|||||||
app.setName('澜音')
|
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
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
@@ -313,79 +178,7 @@ app.whenReady().then(() => {
|
|||||||
optimizer.watchWindowShortcuts(window)
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 阻止系统息屏 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
|
|
||||||
})
|
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
createTray()
|
|
||||||
|
|
||||||
// 注册自动更新事件
|
// 注册自动更新事件
|
||||||
registerAutoUpdateEvents()
|
registerAutoUpdateEvents()
|
||||||
@@ -415,67 +208,19 @@ app.on('window-all-closed', () => {
|
|||||||
// 在其他平台上,我们也保持应用运行,因为有系统托盘
|
// 在其他平台上,我们也保持应用运行,因为有系统托盘
|
||||||
})
|
})
|
||||||
|
|
||||||
// 应用退出前的清理
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
isQuitting = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// In this file you can include the rest of your app's specific main process
|
// In this file you can include the rest of your app's specific main process
|
||||||
// code. You can also put them in separate files and require them here.
|
// code. You can also put them in separate files and require them here.
|
||||||
|
|
||||||
let ping: NodeJS.Timeout
|
let ping: NodeJS.Timeout
|
||||||
function startPing() {
|
function startPing() {
|
||||||
let interval = 3000
|
// 已迁移到 Howler,不再使用 DOM <audio> 轮询。
|
||||||
|
// 如需主进程感知播放结束,请在渲染进程通过 IPC 主动通知。
|
||||||
ping = setInterval(() => {
|
if (ping) {
|
||||||
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)
|
clearInterval(ping)
|
||||||
}
|
}
|
||||||
})
|
ping = setInterval(() => {
|
||||||
.catch((err) => console.warn(err))
|
// 保留占位,避免调用方报错;不再做任何轮询。
|
||||||
}
|
// 可在此处监听自定义 IPC 事件以扩展行为。
|
||||||
}, interval)
|
clearInterval(ping)
|
||||||
}
|
}, 1000)
|
||||||
})
|
|
||||||
.catch((err) => console.warn(err))
|
|
||||||
}
|
|
||||||
}, interval)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import NodeID3 from 'node-id3'
|
import { File, Picture, Id3v2Settings } from 'node-taglib-sharp'
|
||||||
import ffmpegStatic from 'ffmpeg-static'
|
|
||||||
import ffmpeg from 'fluent-ffmpeg'
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import fs from 'fs'
|
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')}`
|
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)字符
|
* 转换新格式:[开始时间,持续时间](开始时间,字符持续时间,0)字符
|
||||||
*/
|
*/
|
||||||
@@ -156,265 +180,6 @@ function convertOldFormat(timestamp: string, content: string): string {
|
|||||||
return `[${timestamp}]${convertedContent}`
|
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 => {
|
const getDownloadDirectory = (): string => {
|
||||||
// 使用配置管理服务获取下载目录
|
// 使用配置管理服务获取下载目录
|
||||||
@@ -492,13 +257,71 @@ export default async function download(
|
|||||||
delete fileLock[songPath]
|
delete fileLock[songPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入标签信息
|
// 写入标签信息(使用 node-taglib-sharp)
|
||||||
if (tagWriteOptions && fs.existsSync(songPath)) {
|
if (tagWriteOptions && fs.existsSync(songPath)) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.warn('写入音频标签失败:', error)
|
console.warn('写入音乐元信息失败:', error)
|
||||||
throw ffmpegStatic
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { decodeName, formatPlayTime, sizeFormate } from '../index'
|
import { decodeName, formatPlayTime } from '../../index'
|
||||||
import { formatSingerName } from '../utils'
|
import { formatSingerName } from '../utils'
|
||||||
|
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
limit: 30,
|
limit: 30,
|
||||||
@@ -9,87 +10,72 @@ export default {
|
|||||||
allPage: 1,
|
allPage: 1,
|
||||||
musicSearch(str, page, limit) {
|
musicSearch(str, page, limit) {
|
||||||
const searchRequest = httpFetch(
|
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)
|
return searchRequest.promise.then(({ body }) => body)
|
||||||
},
|
},
|
||||||
filterData(rawData) {
|
async handleResult(rawData) {
|
||||||
const types = []
|
let ids = new Set()
|
||||||
const _types = {}
|
const items = []
|
||||||
if (rawData.FileSize !== 0) {
|
|
||||||
const size = sizeFormate(rawData.FileSize)
|
rawData.forEach((item) => {
|
||||||
types.push({ type: '128k', size, hash: rawData.FileHash })
|
const key = item.Audioid + item.FileHash
|
||||||
_types['128k'] = {
|
if (!ids.has(key)) {
|
||||||
size,
|
ids.add(key)
|
||||||
hash: rawData.FileHash
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rawData.HQFileSize !== 0) {
|
})
|
||||||
const size = sizeFormate(rawData.HQFileSize)
|
|
||||||
types.push({ type: '320k', size, hash: rawData.HQFileHash })
|
const hashList = items.map((item) => item.FileHash)
|
||||||
_types['320k'] = {
|
|
||||||
size,
|
let qualityInfoMap = {}
|
||||||
hash: rawData.HQFileHash
|
try {
|
||||||
}
|
const qualityInfoRequest = getBatchMusicQualityInfo(hashList)
|
||||||
}
|
qualityInfoMap = await qualityInfoRequest.promise
|
||||||
if (rawData.SQFileSize !== 0) {
|
} catch (error) {
|
||||||
const size = sizeFormate(rawData.SQFileSize)
|
console.error('Failed to fetch quality info:', error)
|
||||||
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 items.map((item) => {
|
||||||
|
const { types = [], _types = {} } = qualityInfoMap[item.FileHash] || {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
singer: decodeName(formatSingerName(rawData.Singers, 'name')),
|
singer: decodeName(formatSingerName(item.Singers, 'name')),
|
||||||
name: decodeName(rawData.SongName),
|
name: decodeName(item.SongName),
|
||||||
albumName: decodeName(rawData.AlbumName),
|
albumName: decodeName(item.AlbumName),
|
||||||
albumId: rawData.AlbumID,
|
albumId: item.AlbumID,
|
||||||
songmid: rawData.Audioid,
|
songmid: item.Audioid,
|
||||||
source: 'kg',
|
source: 'kg',
|
||||||
interval: formatPlayTime(rawData.Duration),
|
interval: formatPlayTime(item.Duration),
|
||||||
_interval: rawData.Duration,
|
_interval: item.Duration,
|
||||||
img: null,
|
img: null,
|
||||||
lrc: null,
|
lrc: null,
|
||||||
otherSource: null,
|
otherSource: null,
|
||||||
hash: rawData.FileHash,
|
hash: item.FileHash,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
}
|
|
||||||
},
|
|
||||||
handleResult(rawData) {
|
|
||||||
const ids = new Set()
|
|
||||||
const list = []
|
|
||||||
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
|
|
||||||
ids.add(key)
|
|
||||||
list.push(this.filterData(childItem))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return list
|
|
||||||
},
|
},
|
||||||
search(str, page = 1, limit, retryNum = 0) {
|
search(str, page = 1, limit, retryNum = 0) {
|
||||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||||
if (limit == null) limit = this.limit
|
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)
|
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)
|
if (list == null) return this.search(str, page, limit, retryNum)
|
||||||
|
|
||||||
@@ -102,8 +88,8 @@ export default {
|
|||||||
allPage: this.allPage,
|
allPage: this.allPage,
|
||||||
limit,
|
limit,
|
||||||
total: this.total,
|
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 { httpFetch } from '../../request'
|
||||||
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
import { decodeName, dateFormat, formatPlayCount } from '../../index'
|
||||||
import './vendors/infSign.min'
|
import './vendors/infSign.min'
|
||||||
|
|
||||||
import { signatureParams } from './util'
|
import { signatureParams } from './util'
|
||||||
|
import { filterData } from './quality_detail'
|
||||||
|
|
||||||
const handleSignature = (id, page, limit) =>
|
const handleSignature = (id, page, limit) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@@ -14,7 +14,7 @@ const handleSignature = (id, page, limit) =>
|
|||||||
isCDN: !0,
|
isCDN: !0,
|
||||||
callback(i) {
|
callback(i) {
|
||||||
resolve(i.signature)
|
resolve(i.signature)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -27,36 +27,36 @@ export default {
|
|||||||
listDetailLimit: 10000,
|
listDetailLimit: 10000,
|
||||||
currentTagInfo: {
|
currentTagInfo: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
info: undefined
|
info: undefined,
|
||||||
},
|
},
|
||||||
sortList: [
|
sortList: [
|
||||||
{
|
{
|
||||||
name: '推荐',
|
name: '推荐',
|
||||||
id: '5'
|
id: '5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '最热',
|
name: '最热',
|
||||||
id: '6'
|
id: '6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '最新',
|
name: '最新',
|
||||||
id: '7'
|
id: '7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '热藏',
|
name: '热藏',
|
||||||
id: '3'
|
id: '3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '飙升',
|
name: '飙升',
|
||||||
id: '8'
|
id: '8',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
cache: new Map(),
|
cache: new Map(),
|
||||||
regExps: {
|
regExps: {
|
||||||
listData: /global\.data = (\[.+\]);/,
|
listData: /global\.data = (\[.+\]);/,
|
||||||
listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/,
|
listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/,
|
||||||
// https://www.kugou.com/yy/special/single/1067062.html
|
// https://www.kugou.com/yy/special/single/1067062.html
|
||||||
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/
|
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/,
|
||||||
},
|
},
|
||||||
parseHtmlDesc(html) {
|
parseHtmlDesc(html) {
|
||||||
const prefix = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
|
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')
|
if (tryNum > 2) throw new Error('try max num')
|
||||||
|
|
||||||
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
|
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
|
||||||
const listData = body.match(this.regExps.listData)
|
let listData = body.match(this.regExps.listData)
|
||||||
const listInfo = body.match(this.regExps.listInfo)
|
let listInfo = body.match(this.regExps.listInfo)
|
||||||
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
|
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
|
||||||
const list = await this.getMusicInfos(JSON.parse(listData[1]))
|
let list = await this.getMusicInfos(JSON.parse(listData[1]))
|
||||||
// listData = this.filterData(JSON.parse(listData[1]))
|
|
||||||
let name
|
let name
|
||||||
let pic
|
let pic
|
||||||
if (listInfo) {
|
if (listInfo) {
|
||||||
name = listInfo[1]
|
name = listInfo[1]
|
||||||
pic = listInfo[2]
|
pic = listInfo[2]
|
||||||
}
|
}
|
||||||
const desc = this.parseHtmlDesc(body)
|
let desc = this.parseHtmlDesc(body)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
@@ -93,10 +92,10 @@ export default {
|
|||||||
info: {
|
info: {
|
||||||
name,
|
name,
|
||||||
img: pic,
|
img: pic,
|
||||||
desc
|
desc,
|
||||||
// author: body.result.info.userinfo.username,
|
// author: body.result.info.userinfo.username,
|
||||||
// play_count: formatPlayCount(body.result.listen_num),
|
// play_count: formatPlayCount(body.result.listen_num),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getInfoUrl(tagId) {
|
getInfoUrl(tagId) {
|
||||||
@@ -116,11 +115,11 @@ export default {
|
|||||||
const result = []
|
const result = []
|
||||||
if (rawData.status !== 1) return result
|
if (rawData.status !== 1) return result
|
||||||
for (const key of Object.keys(rawData.data)) {
|
for (const key of Object.keys(rawData.data)) {
|
||||||
const tag = rawData.data[key]
|
let tag = rawData.data[key]
|
||||||
result.push({
|
result.push({
|
||||||
id: tag.special_id,
|
id: tag.special_id,
|
||||||
name: tag.special_name,
|
name: tag.special_name,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -135,8 +134,8 @@ export default {
|
|||||||
parent_name: tag.pname,
|
parent_name: tag.pname,
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}))
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -159,7 +158,7 @@ export default {
|
|||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'KuGou2012-8275-web_browser_event_handler'
|
'User-Agent': 'KuGou2012-8275-web_browser_event_handler',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
appid: 1001,
|
appid: 1001,
|
||||||
@@ -170,8 +169,8 @@ export default {
|
|||||||
platform: 'pc',
|
platform: 'pc',
|
||||||
userid: '262643156',
|
userid: '262643156',
|
||||||
return_min: 6,
|
return_min: 6,
|
||||||
return_max: 15
|
return_max: 15,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return this._requestObj_listRecommend.promise.then(({ body }) => {
|
return this._requestObj_listRecommend.promise.then(({ body }) => {
|
||||||
@@ -190,7 +189,7 @@ export default {
|
|||||||
total: item.songcount,
|
total: item.songcount,
|
||||||
grade: item.grade,
|
grade: item.grade,
|
||||||
desc: item.intro,
|
desc: item.intro,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -219,7 +218,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
createTask(hashs) {
|
createTask(hashs) {
|
||||||
const data = {
|
let data = {
|
||||||
area_code: '1',
|
area_code: '1',
|
||||||
show_privilege: 1,
|
show_privilege: 1,
|
||||||
show_album_info: '1',
|
show_album_info: '1',
|
||||||
@@ -230,16 +229,16 @@ export default {
|
|||||||
dfid: '-',
|
dfid: '-',
|
||||||
clienttime: Date.now(),
|
clienttime: Date.now(),
|
||||||
key: 'OIlwieks28dk2k092lksi2UIkp',
|
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
|
let list = hashs
|
||||||
const tasks = []
|
let tasks = []
|
||||||
while (list.length) {
|
while (list.length) {
|
||||||
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
|
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
|
||||||
if (list.length < 100) break
|
if (list.length < 100) break
|
||||||
list = list.slice(100)
|
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) =>
|
return tasks.map((task) =>
|
||||||
this.createHttp(url, {
|
this.createHttp(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -250,13 +249,13 @@ export default {
|
|||||||
'KG-Fake': '0',
|
'KG-Fake': '0',
|
||||||
'KG-RF': '00869891',
|
'KG-RF': '00869891',
|
||||||
'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi',
|
'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]))
|
}).then((data) => data.map((s) => s[0]))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
async getMusicInfos(list) {
|
async getMusicInfos(list) {
|
||||||
return this.filterData2(
|
return await this.filterData(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.createTask(this.deDuplication(list).map((item) => ({ hash: item.hash })))
|
this.createTask(this.deDuplication(list).map((item) => ({ hash: item.hash })))
|
||||||
).then(([...datas]) => datas.flat())
|
).then(([...datas]) => datas.flat())
|
||||||
@@ -269,7 +268,7 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'KG-RC': 1,
|
'KG-RC': 1,
|
||||||
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
||||||
'User-Agent': ''
|
'User-Agent': '',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
appid: 1001,
|
appid: 1001,
|
||||||
@@ -277,13 +276,13 @@ export default {
|
|||||||
mid: '21511157a05844bd085308bc76ef3343',
|
mid: '21511157a05844bd085308bc76ef3343',
|
||||||
clienttime: 640612895,
|
clienttime: 640612895,
|
||||||
key: '36164c4015e704673c588ee202b9ecb8',
|
key: '36164c4015e704673c588ee202b9ecb8',
|
||||||
data: id
|
data: id,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
// console.log(songInfo)
|
// console.log(songInfo)
|
||||||
// type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列
|
// type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列
|
||||||
let songList
|
let songList
|
||||||
const info = songInfo.info
|
let info = songInfo.info
|
||||||
switch (info.type) {
|
switch (info.type) {
|
||||||
case 2:
|
case 2:
|
||||||
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
|
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
|
||||||
@@ -299,7 +298,7 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'KG-RC': 1,
|
'KG-RC': 1,
|
||||||
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
'KG-THash': 'network_super_call.cpp:3676261689:379',
|
||||||
'User-Agent': ''
|
'User-Agent': '',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
appid: 1001,
|
appid: 1001,
|
||||||
@@ -313,13 +312,13 @@ export default {
|
|||||||
userid: info.userid,
|
userid: info.userid,
|
||||||
collect_type: 0,
|
collect_type: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
pagesize: info.count
|
pagesize: info.count,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
// console.log(songList)
|
// console.log(songList)
|
||||||
}
|
}
|
||||||
const list = await this.getMusicInfos(songList || songInfo.list)
|
let list = await this.getMusicInfos(songList || songInfo.list)
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -330,9 +329,9 @@ export default {
|
|||||||
name: info.name,
|
name: info.name,
|
||||||
img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img,
|
img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img,
|
||||||
// desc: body.result.info.list_desc,
|
// desc: body.result.info.list_desc,
|
||||||
author: info.username
|
author: info.username,
|
||||||
// play_count: formatPlayCount(info.count),
|
// play_count: formatPlayCount(info.count),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -342,8 +341,8 @@ export default {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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) {
|
if (!songInfo.list) {
|
||||||
@@ -354,7 +353,7 @@ export default {
|
|||||||
this.getUserListDetail5(chain)
|
this.getUserListDetail5(chain)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const list = await this.getMusicInfos(songInfo.list)
|
let list = await this.getMusicInfos(songInfo.list)
|
||||||
// console.log(info, songInfo)
|
// console.log(info, songInfo)
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
@@ -366,14 +365,14 @@ export default {
|
|||||||
name: songInfo.info.name,
|
name: songInfo.info.name,
|
||||||
img: songInfo.info.img,
|
img: songInfo.info.img,
|
||||||
// desc: body.result.info.list_desc,
|
// desc: body.result.info.list_desc,
|
||||||
author: songInfo.info.username
|
author: songInfo.info.username,
|
||||||
// play_count: formatPlayCount(info.count),
|
// play_count: formatPlayCount(info.count),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deDuplication(datas) {
|
deDuplication(datas) {
|
||||||
const ids = new Set()
|
let ids = new Set()
|
||||||
return datas.filter(({ hash }) => {
|
return datas.filter(({ hash }) => {
|
||||||
if (ids.has(hash)) return false
|
if (ids.has(hash)) return false
|
||||||
ids.add(hash)
|
ids.add(hash)
|
||||||
@@ -388,29 +387,25 @@ export default {
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: gcid,
|
id: gcid,
|
||||||
id_type: 2
|
id_type: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, {
|
||||||
}
|
|
||||||
const result = await this.createHttp(
|
|
||||||
`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
'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/',
|
||||||
Referer: 'https://m.kugou.com/'
|
|
||||||
},
|
},
|
||||||
body
|
body,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return result.list[0].global_collection_id
|
return result.list[0].global_collection_id
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserListDetailByLink({ info }, link) {
|
async getUserListDetailByLink({ info }, link) {
|
||||||
const listInfo = info['0']
|
let listInfo = info['0']
|
||||||
let total = listInfo.count
|
let total = listInfo.count
|
||||||
const tasks = []
|
let tasks = []
|
||||||
let page = 0
|
let page = 0
|
||||||
while (total) {
|
while (total) {
|
||||||
const limit = total > 90 ? 90 : total
|
const limit = total > 90 ? 90 : total
|
||||||
@@ -423,8 +418,8 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
Referer: link
|
Referer: link,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).then((data) => data.list.info)
|
).then((data) => data.list.info)
|
||||||
)
|
)
|
||||||
@@ -442,13 +437,13 @@ export default {
|
|||||||
name: listInfo.name,
|
name: listInfo.name,
|
||||||
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
|
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
|
||||||
// desc: body.result.info.list_desc,
|
// desc: body.result.info.list_desc,
|
||||||
author: listInfo.list_create_username
|
author: listInfo.list_create_username,
|
||||||
// play_count: formatPlayCount(listInfo.count),
|
// play_count: formatPlayCount(listInfo.count),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createGetListDetail2Task(id, total) {
|
createGetListDetail2Task(id, total) {
|
||||||
const tasks = []
|
let tasks = []
|
||||||
let page = 0
|
let page = 0
|
||||||
while (total) {
|
while (total) {
|
||||||
const limit = total > 300 ? 300 : total
|
const limit = total > 300 ? 300 : total
|
||||||
@@ -464,7 +459,10 @@ export default {
|
|||||||
'&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-'
|
'&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-'
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.createHttp(
|
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: {
|
headers: {
|
||||||
mid: '1586163263991',
|
mid: '1586163263991',
|
||||||
@@ -472,8 +470,8 @@ export default {
|
|||||||
'User-Agent':
|
'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',
|
'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: '-',
|
||||||
clienttime: '1586163263991'
|
clienttime: '1586163263991',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).then((data) => data.info)
|
).then((data) => data.info)
|
||||||
)
|
)
|
||||||
@@ -481,14 +479,17 @@ export default {
|
|||||||
return Promise.all(tasks).then(([...datas]) => datas.flat())
|
return Promise.all(tasks).then(([...datas]) => datas.flat())
|
||||||
},
|
},
|
||||||
async getUserListDetail2(global_collection_id) {
|
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')
|
if (id.length > 1000) throw new Error('get list error')
|
||||||
const params =
|
const params =
|
||||||
'appid=1058&specialid=0&global_specialid=' +
|
'appid=1058&specialid=0&global_specialid=' +
|
||||||
id +
|
id +
|
||||||
'&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
|
'&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
|
||||||
const info = await this.createHttp(
|
let info = await this.createHttp(
|
||||||
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`,
|
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(
|
||||||
|
params,
|
||||||
|
'web'
|
||||||
|
)}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
mid: '1586163242519',
|
mid: '1586163242519',
|
||||||
@@ -496,12 +497,12 @@ export default {
|
|||||||
'User-Agent':
|
'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',
|
'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: '-',
|
||||||
clienttime: '1586163242519'
|
clienttime: '1586163242519',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
|
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)
|
// console.log(info, songInfo, list)
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
@@ -514,8 +515,8 @@ export default {
|
|||||||
img: info.imgurl && info.imgurl.replace('{size}', 240),
|
img: info.imgurl && info.imgurl.replace('{size}', 240),
|
||||||
desc: info.intro,
|
desc: info.intro,
|
||||||
author: info.nickname,
|
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}`, {
|
const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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
|
}).promise
|
||||||
let result = body.match(/var\sphpParam\s=\s({.+?});/)
|
let result = body.match(/var\sphpParam\s=\s({.+?});/)
|
||||||
if (result) result = JSON.parse(result[1])
|
if (result) result = JSON.parse(result[1])
|
||||||
@@ -534,13 +535,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getUserListDetailByPcChain(chain) {
|
async getUserListDetailByPcChain(chain) {
|
||||||
const key = `${chain}_pc_list`
|
let key = `${chain}_pc_list`
|
||||||
if (this.cache.has(key)) return this.cache.get(key)
|
if (this.cache.has(key)) return this.cache.get(key)
|
||||||
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
|
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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
|
}).promise
|
||||||
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
|
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
|
||||||
if (result) result = JSON.parse(result[1])
|
if (result) result = JSON.parse(result[1])
|
||||||
@@ -554,7 +555,7 @@ export default {
|
|||||||
const limit = 100
|
const limit = 100
|
||||||
const [listInfo, list] = await Promise.all([
|
const [listInfo, list] = await Promise.all([
|
||||||
this.getListInfoByChain(chain),
|
this.getListInfoByChain(chain),
|
||||||
this.getUserListDetailById(songInfo.id, page, limit)
|
this.getUserListDetailById(songInfo.id, page, limit),
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
list: list || [],
|
list: list || [],
|
||||||
@@ -566,16 +567,16 @@ export default {
|
|||||||
name: listInfo.specialname,
|
name: listInfo.specialname,
|
||||||
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
||||||
// desc: body.result.info.list_desc,
|
// desc: body.result.info.list_desc,
|
||||||
author: listInfo.nickname
|
author: listInfo.nickname,
|
||||||
// play_count: formatPlayCount(info.count),
|
// play_count: formatPlayCount(info.count),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserListDetail5(chain) {
|
async getUserListDetail5(chain) {
|
||||||
const [listInfo, list] = await Promise.all([
|
const [listInfo, list] = await Promise.all([
|
||||||
this.getListInfoByChain(chain),
|
this.getListInfoByChain(chain),
|
||||||
this.getUserListDetailByPcChain(chain)
|
this.getUserListDetailByPcChain(chain),
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
list: list || [],
|
list: list || [],
|
||||||
@@ -587,28 +588,28 @@ export default {
|
|||||||
name: listInfo.specialname,
|
name: listInfo.specialname,
|
||||||
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
|
||||||
// desc: body.result.info.list_desc,
|
// desc: body.result.info.list_desc,
|
||||||
author: listInfo.nickname
|
author: listInfo.nickname,
|
||||||
// play_count: formatPlayCount(info.count),
|
// play_count: formatPlayCount(info.count),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserListDetailById(id, page, limit) {
|
async getUserListDetailById(id, page, limit) {
|
||||||
const signature = await handleSignature(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}`,
|
`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: {
|
headers: {
|
||||||
Referer: 'https://m3ws.kugou.com/share/index.php',
|
Referer: 'https://m3ws.kugou.com/share/index.php',
|
||||||
'User-Agent':
|
'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',
|
'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)
|
// console.log(info)
|
||||||
const result = await this.getMusicInfos(info.info)
|
let result = await this.getMusicInfos(info.info)
|
||||||
// console.log(info, songInfo)
|
// console.log(info, songInfo)
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
@@ -616,19 +617,15 @@ export default {
|
|||||||
async getUserListDetail(link, page, retryNum = 0) {
|
async getUserListDetail(link, page, retryNum = 0) {
|
||||||
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
|
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
|
||||||
if (link.includes('#')) link = link.replace(/#.*$/, '')
|
if (link.includes('#')) link = link.replace(/#.*$/, '')
|
||||||
if (link.includes('global_collection_id'))
|
if (link.includes('global_collection_id')) return this.getUserListDetail2(link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
|
||||||
return this.getUserListDetail2(
|
|
||||||
link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
|
|
||||||
)
|
|
||||||
if (link.includes('gcid_')) {
|
if (link.includes('gcid_')) {
|
||||||
const gcid = link.match(/gcid_\w+/)?.[0]
|
let gcid = link.match(/gcid_\w+/)?.[0]
|
||||||
if (gcid) {
|
if (gcid) {
|
||||||
const global_collection_id = await this.decodeGcid(gcid)
|
const global_collection_id = await this.decodeGcid(gcid)
|
||||||
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (link.includes('chain='))
|
if (link.includes('chain=')) return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
||||||
return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
|
||||||
if (link.includes('.html')) {
|
if (link.includes('.html')) {
|
||||||
if (link.includes('zlist.html')) {
|
if (link.includes('zlist.html')) {
|
||||||
link = link.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
link = link.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
||||||
@@ -650,34 +647,27 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
Referer: link
|
Referer: link,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
headers: { location },
|
headers: { location },
|
||||||
statusCode,
|
statusCode,
|
||||||
body
|
body,
|
||||||
} = await requestObj_listDetailLink.promise
|
} = await requestObj_listDetailLink.promise
|
||||||
// console.log(body, location)
|
// console.log(body, location)
|
||||||
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
|
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
|
||||||
if (location) {
|
if (location) {
|
||||||
// console.log(location)
|
// console.log(location)
|
||||||
if (location.includes('global_collection_id'))
|
if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
|
||||||
return this.getUserListDetail2(
|
|
||||||
location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
|
|
||||||
)
|
|
||||||
if (location.includes('gcid_')) {
|
if (location.includes('gcid_')) {
|
||||||
const gcid = link.match(/gcid_\w+/)?.[0]
|
let gcid = link.match(/gcid_\w+/)?.[0]
|
||||||
if (gcid) {
|
if (gcid) {
|
||||||
const global_collection_id = await this.decodeGcid(gcid)
|
const global_collection_id = await this.decodeGcid(gcid)
|
||||||
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (location.includes('chain='))
|
if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
|
||||||
return this.getUserListDetail3(
|
|
||||||
location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'),
|
|
||||||
page
|
|
||||||
)
|
|
||||||
if (location.includes('.html')) {
|
if (location.includes('.html')) {
|
||||||
if (location.includes('zlist.html')) {
|
if (location.includes('zlist.html')) {
|
||||||
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
|
||||||
@@ -698,7 +688,7 @@ export default {
|
|||||||
// console.log('location', location)
|
// console.log('location', location)
|
||||||
return this.getUserListDetail(location, page, ++retryNum)
|
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]
|
let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1]
|
||||||
if (!global_collection_id) {
|
if (!global_collection_id) {
|
||||||
let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1]
|
let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1]
|
||||||
@@ -729,184 +719,9 @@ export default {
|
|||||||
|
|
||||||
return this.getListDetailBySpecialId(id, page)
|
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
|
// hash list filter
|
||||||
filterData2(rawList) {
|
async filterData(rawList) {
|
||||||
// console.log(rawList)
|
return await filterData(rawList, { removeDuplicates: true, fix: true })
|
||||||
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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取列表信息
|
// 获取列表信息
|
||||||
@@ -920,14 +735,14 @@ export default {
|
|||||||
limit: body.data.params.pagesize,
|
limit: body.data.params.pagesize,
|
||||||
page: body.data.params.p,
|
page: body.data.params.p,
|
||||||
total: body.data.params.total,
|
total: body.data.params.total,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
getList(sortId, tagId, page) {
|
getList(sortId, tagId, page) {
|
||||||
const tasks = [this.getSongList(sortId, tagId, page)]
|
let tasks = [this.getSongList(sortId, tagId, page)]
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.currentTagInfo.id === tagId
|
this.currentTagInfo.id === tagId
|
||||||
? Promise.resolve(this.currentTagInfo.info)
|
? Promise.resolve(this.currentTagInfo.info)
|
||||||
@@ -943,7 +758,7 @@ export default {
|
|||||||
if (recommendList) list.unshift(...recommendList)
|
if (recommendList) list.unshift(...recommendList)
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
...info
|
...info,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -958,13 +773,13 @@ export default {
|
|||||||
return {
|
return {
|
||||||
hotTag: this.filterInfoHotTag(body.data.hotTag),
|
hotTag: this.filterInfoHotTag(body.data.hotTag),
|
||||||
tags: this.filterTagInfo(body.data.tagids),
|
tags: this.filterTagInfo(body.data.tagids),
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getDetailPageUrl(id) {
|
getDetailPageUrl(id) {
|
||||||
if (typeof id === 'string') {
|
if (typeof id == 'string') {
|
||||||
if (/^https?:\/\//.test(id)) return id
|
if (/^https?:\/\//.test(id)) return id
|
||||||
id = id.replace('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
|
// 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://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(
|
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 }) => {
|
).promise.then(({ body }) => {
|
||||||
if (body.errcode != 0) throw new Error('filed')
|
if (body.errcode != 0) throw new Error('filed')
|
||||||
// console.log(body.data.info)
|
// console.log(body.data.info)
|
||||||
@@ -991,15 +808,15 @@ export default {
|
|||||||
grade: item.grade,
|
grade: item.grade,
|
||||||
desc: item.intro,
|
desc: item.intro,
|
||||||
total: item.songcount,
|
total: item.songcount,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
limit,
|
limit,
|
||||||
total: body.data.total,
|
total: body.data.total,
|
||||||
source: 'kg'
|
source: 'kg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getList
|
// getList
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// import '../../polyfill/array.find'
|
// import '../../polyfill/array.find'
|
||||||
|
|
||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { formatPlayTime, decodeName } from '../index'
|
import { formatPlayTime, decodeName } from '../../index'
|
||||||
// import { debug } from '../../utils/env'
|
// import { debug } from '../../utils/env'
|
||||||
import { formatSinger } from './util'
|
import { formatSinger } from './util'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
regExps: {
|
regExps: {
|
||||||
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/
|
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/,
|
||||||
},
|
},
|
||||||
limit: 30,
|
limit: 30,
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
// console.log(rawData)
|
// console.log(rawData)
|
||||||
for (let i = 0; i < rawData.length; i++) {
|
for (let i = 0; i < rawData.length; i++) {
|
||||||
const info = rawData[i]
|
const info = rawData[i]
|
||||||
const songId = info.MUSICRID.replace('MUSIC_', '')
|
let songId = info.MUSICRID.replace('MUSIC_', '')
|
||||||
// const format = (info.FORMATS || info.formats).split('|')
|
// const format = (info.FORMATS || info.formats).split('|')
|
||||||
|
|
||||||
if (!info.N_MINFO) {
|
if (!info.N_MINFO) {
|
||||||
@@ -43,33 +43,39 @@ export default {
|
|||||||
const types = []
|
const types = []
|
||||||
const _types = {}
|
const _types = {}
|
||||||
|
|
||||||
const infoArr = info.N_MINFO.split(';')
|
let infoArr = info.N_MINFO.split(';')
|
||||||
for (let info of infoArr) {
|
for (let info of infoArr) {
|
||||||
info = info.match(this.regExps.mInfo)
|
info = info.match(this.regExps.mInfo)
|
||||||
if (info) {
|
if (info) {
|
||||||
switch (info[2]) {
|
switch (info[2]) {
|
||||||
|
case '20900':
|
||||||
|
types.push({ type: 'master', size: info[4] })
|
||||||
|
_types.master = {
|
||||||
|
size: info[4].toLocaleUpperCase(),
|
||||||
|
}
|
||||||
|
break
|
||||||
case '4000':
|
case '4000':
|
||||||
types.push({ type: 'flac24bit', size: info[4] })
|
types.push({ type: 'hires', size: info[4] })
|
||||||
_types.flac24bit = {
|
_types.hires = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '2000':
|
case '2000':
|
||||||
types.push({ type: 'flac', size: info[4] })
|
types.push({ type: 'flac', size: info[4] })
|
||||||
_types.flac = {
|
_types.flac = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '320':
|
case '320':
|
||||||
types.push({ type: '320k', size: info[4] })
|
types.push({ type: '320k', size: info[4] })
|
||||||
_types['320k'] = {
|
_types['320k'] = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '128':
|
case '128':
|
||||||
types.push({ type: '128k', size: info[4] })
|
types.push({ type: '128k', size: info[4] })
|
||||||
_types['128k'] = {
|
_types['128k'] = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -77,7 +83,7 @@ export default {
|
|||||||
}
|
}
|
||||||
types.reverse()
|
types.reverse()
|
||||||
|
|
||||||
const interval = parseInt(info.DURATION)
|
let interval = parseInt(info.DURATION)
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
name: decodeName(info.SONGNAME),
|
name: decodeName(info.SONGNAME),
|
||||||
@@ -95,7 +101,7 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// console.log(result)
|
// console.log(result)
|
||||||
@@ -109,7 +115,7 @@ export default {
|
|||||||
// console.log(result)
|
// console.log(result)
|
||||||
if (!result || (result.TOTAL !== '0' && result.SHOW === '0'))
|
if (!result || (result.TOTAL !== '0' && result.SHOW === '0'))
|
||||||
return this.search(str, page, limit, ++retryNum)
|
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)
|
if (list == null) return this.search(str, page, limit, ++retryNum)
|
||||||
|
|
||||||
@@ -122,8 +128,8 @@ export default {
|
|||||||
allPage: this.allPage,
|
allPage: this.allPage,
|
||||||
total: this.total,
|
total: this.total,
|
||||||
limit,
|
limit,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { formatPlayTime, decodeName } from '../index'
|
import { formatPlayTime, decodeName } from '../../index'
|
||||||
import { formatSinger, objStr2JSON } from './util'
|
import { formatSinger, objStr2JSON } from './util'
|
||||||
import album from './album'
|
import album from './album'
|
||||||
|
|
||||||
@@ -13,18 +13,18 @@ export default {
|
|||||||
sortList: [
|
sortList: [
|
||||||
{
|
{
|
||||||
name: '最新',
|
name: '最新',
|
||||||
id: 'new'
|
id: 'new',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '最热',
|
name: '最热',
|
||||||
id: 'hot'
|
id: 'hot',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
regExps: {
|
regExps: {
|
||||||
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/,
|
mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/,
|
||||||
// http://www.kuwo.cn/playlist_detail/2886046289
|
// http://www.kuwo.cn/playlist_detail/2886046289
|
||||||
// https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend
|
// https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend
|
||||||
listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/
|
listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/,
|
||||||
},
|
},
|
||||||
tagsUrl:
|
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',
|
'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) {
|
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
|
// 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
|
// 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) => ({
|
return rawList.map((item) => ({
|
||||||
id: `${item.id}-${item.digest}`,
|
id: `${item.id}-${item.digest}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
filterTagInfo(rawList) {
|
filterTagInfo(rawList) {
|
||||||
@@ -83,8 +85,8 @@ export default {
|
|||||||
parent_name: type.name,
|
parent_name: type.name,
|
||||||
id: `${item.id}-${item.digest}`,
|
id: `${item.id}-${item.digest}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}))
|
})),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ export default {
|
|||||||
let id
|
let id
|
||||||
let type
|
let type
|
||||||
if (tagId) {
|
if (tagId) {
|
||||||
const arr = tagId.split('-')
|
let arr = tagId.split('-')
|
||||||
id = arr[0]
|
id = arr[0]
|
||||||
type = arr[1]
|
type = arr[1]
|
||||||
} else {
|
} else {
|
||||||
@@ -110,7 +112,7 @@ export default {
|
|||||||
total: body.data.total,
|
total: body.data.total,
|
||||||
page: body.data.pn,
|
page: body.data.pn,
|
||||||
limit: body.data.rn,
|
limit: body.data.rn,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}
|
}
|
||||||
} else if (!body.length) {
|
} else if (!body.length) {
|
||||||
return this.getList(sortId, tagId, page, ++tryNum)
|
return this.getList(sortId, tagId, page, ++tryNum)
|
||||||
@@ -120,7 +122,7 @@ export default {
|
|||||||
total: 1000,
|
total: 1000,
|
||||||
page,
|
page,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -145,7 +147,7 @@ export default {
|
|||||||
img: item.img,
|
img: item.img,
|
||||||
grade: item.favorcnt / 10,
|
grade: item.favorcnt / 10,
|
||||||
desc: item.desc,
|
desc: item.desc,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
filterList2(rawData) {
|
filterList2(rawData) {
|
||||||
@@ -164,7 +166,7 @@ export default {
|
|||||||
img: item.img,
|
img: item.img,
|
||||||
grade: item.favorcnt && item.favorcnt / 10,
|
grade: item.favorcnt && item.favorcnt / 10,
|
||||||
desc: item.desc,
|
desc: item.desc,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -188,8 +190,8 @@ export default {
|
|||||||
img: body.pic,
|
img: body.pic,
|
||||||
desc: body.info,
|
desc: body.info,
|
||||||
author: body.uname,
|
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) {
|
getListDetailDigest5Music(id, page, tryNum = 0) {
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
const requestObj = httpFetch(
|
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 }) => {
|
return requestObj.promise.then(({ body }) => {
|
||||||
// console.log(body)
|
// console.log(body)
|
||||||
@@ -223,8 +227,8 @@ export default {
|
|||||||
img: body.pic,
|
img: body.pic,
|
||||||
desc: body.info,
|
desc: body.info,
|
||||||
author: body.uname,
|
author: body.uname,
|
||||||
play_count: this.formatPlayCount(body.playnum)
|
play_count: this.formatPlayCount(body.playnum),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -235,33 +239,33 @@ export default {
|
|||||||
|
|
||||||
filterBDListDetail(rawList) {
|
filterBDListDetail(rawList) {
|
||||||
return rawList.map((item) => {
|
return rawList.map((item) => {
|
||||||
const types = []
|
let types = []
|
||||||
const _types = {}
|
let _types = {}
|
||||||
for (const info of item.audios) {
|
for (let info of item.audios) {
|
||||||
info.size = info.size?.toLocaleUpperCase()
|
info.size = info.size?.toLocaleUpperCase()
|
||||||
switch (info.bitrate) {
|
switch (info.bitrate) {
|
||||||
case '4000':
|
case '4000':
|
||||||
types.push({ type: 'flac24bit', size: info.size })
|
types.push({ type: 'flac24bit', size: info.size })
|
||||||
_types.flac24bit = {
|
_types.flac24bit = {
|
||||||
size: info.size
|
size: info.size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '2000':
|
case '2000':
|
||||||
types.push({ type: 'flac', size: info.size })
|
types.push({ type: 'flac', size: info.size })
|
||||||
_types.flac = {
|
_types.flac = {
|
||||||
size: info.size
|
size: info.size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '320':
|
case '320':
|
||||||
types.push({ type: '320k', size: info.size })
|
types.push({ type: '320k', size: info.size })
|
||||||
_types['320k'] = {
|
_types['320k'] = {
|
||||||
size: info.size
|
size: info.size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '128':
|
case '128':
|
||||||
types.push({ type: '128k', size: info.size })
|
types.push({ type: '128k', size: info.size })
|
||||||
_types['128k'] = {
|
_types['128k'] = {
|
||||||
size: info.size
|
size: info.size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -282,7 +286,7 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -299,8 +303,8 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
plat: 'h5'
|
plat: 'h5',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).promise.catch(() => ({ code: 0 }))
|
).promise.catch(() => ({ code: 0 }))
|
||||||
|
|
||||||
@@ -311,7 +315,7 @@ export default {
|
|||||||
img: infoData.data.pic,
|
img: infoData.data.pic,
|
||||||
desc: infoData.data.description,
|
desc: infoData.data.description,
|
||||||
author: infoData.data.creatorName,
|
author: infoData.data.creatorName,
|
||||||
play_count: infoData.data.playNum
|
play_count: infoData.data.playNum,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getListDetailMusicListByBDUserPub(id) {
|
async getListDetailMusicListByBDUserPub(id) {
|
||||||
@@ -321,8 +325,8 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
plat: 'h5'
|
plat: 'h5',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).promise.catch(() => ({ code: 0 }))
|
).promise.catch(() => ({ code: 0 }))
|
||||||
|
|
||||||
@@ -334,18 +338,20 @@ export default {
|
|||||||
img: infoData.data.userInfo.headImg,
|
img: infoData.data.userInfo.headImg,
|
||||||
desc: '',
|
desc: '',
|
||||||
author: infoData.data.userInfo.nickname,
|
author: infoData.data.userInfo.nickname,
|
||||||
play_count: ''
|
play_count: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getListDetailMusicListByBDList(id, source, page, tryNum = 0) {
|
async getListDetailMusicListByBDList(id, source, page, tryNum = 0) {
|
||||||
const { body: listData } = await httpFetch(
|
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: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
plat: 'h5'
|
plat: 'h5',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).promise.catch(() => {
|
).promise.catch(() => {
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
@@ -359,7 +365,7 @@ export default {
|
|||||||
page,
|
page,
|
||||||
limit: listData.data.pageSize,
|
limit: listData.data.pageSize,
|
||||||
total: listData.data.total,
|
total: listData.data.total,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getListDetailMusicListByBD(id, page) {
|
async getListDetailMusicListByBD(id, page) {
|
||||||
@@ -383,7 +389,7 @@ export default {
|
|||||||
img: '',
|
img: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
author: '',
|
author: '',
|
||||||
play_count: ''
|
play_count: '',
|
||||||
}
|
}
|
||||||
// console.log(listData)
|
// console.log(listData)
|
||||||
return listData
|
return listData
|
||||||
@@ -415,35 +421,53 @@ export default {
|
|||||||
filterListDetail(rawData) {
|
filterListDetail(rawData) {
|
||||||
// console.log(rawData)
|
// console.log(rawData)
|
||||||
return rawData.map((item) => {
|
return rawData.map((item) => {
|
||||||
const infoArr = item.N_MINFO.split(';')
|
let infoArr = item.N_MINFO.split(';')
|
||||||
const types = []
|
let types = []
|
||||||
const _types = {}
|
let _types = {}
|
||||||
for (let info of infoArr) {
|
for (let info of infoArr) {
|
||||||
info = info.match(this.regExps.mInfo)
|
info = info.match(this.regExps.mInfo)
|
||||||
if (info) {
|
if (info) {
|
||||||
switch (info[2]) {
|
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':
|
case '4000':
|
||||||
types.push({ type: 'flac24bit', size: info[4] })
|
types.push({ type: 'hires', size: info[4] })
|
||||||
_types.flac24bit = {
|
_types.flac24bit = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '2000':
|
case '2000':
|
||||||
types.push({ type: 'flac', size: info[4] })
|
types.push({ type: 'flac', size: info[4] })
|
||||||
_types.flac = {
|
_types.flac = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '320':
|
case '320':
|
||||||
types.push({ type: '320k', size: info[4] })
|
types.push({ type: '320k', size: info[4] })
|
||||||
_types['320k'] = {
|
_types['320k'] = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '128':
|
case '128':
|
||||||
types.push({ type: '128k', size: info[4] })
|
types.push({ type: '128k', size: info[4] })
|
||||||
_types['128k'] = {
|
_types['128k'] = {
|
||||||
size: info[4].toLocaleUpperCase()
|
size: info[4].toLocaleUpperCase(),
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -464,7 +488,7 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -472,13 +496,13 @@ export default {
|
|||||||
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||||
tags,
|
tags,
|
||||||
hotTag,
|
hotTag,
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
getDetailPageUrl(id) {
|
getDetailPageUrl(id) {
|
||||||
if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
||||||
else if (/^digest-/.test(id)) {
|
else if (/^digest-/.test(id)) {
|
||||||
const result = id.split('__')
|
let result = id.split('__')
|
||||||
id = result[1]
|
id = result[1]
|
||||||
}
|
}
|
||||||
return `http://www.kuwo.cn/playlist_detail/${id}`
|
return `http://www.kuwo.cn/playlist_detail/${id}`
|
||||||
@@ -486,7 +510,9 @@ export default {
|
|||||||
|
|
||||||
search(text, page, limit = 20) {
|
search(text, page, limit = 20) {
|
||||||
return httpFetch(
|
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 }) => {
|
).promise.then(({ body }) => {
|
||||||
body = objStr2JSON(body)
|
body = objStr2JSON(body)
|
||||||
// console.log(body)
|
// console.log(body)
|
||||||
@@ -501,15 +527,15 @@ export default {
|
|||||||
// time: item.publish_time,
|
// time: item.publish_time,
|
||||||
img: item.pic,
|
img: item.pic,
|
||||||
desc: decodeName(item.intro),
|
desc: decodeName(item.intro),
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
limit,
|
limit,
|
||||||
total: parseInt(body.TOTAL),
|
total: parseInt(body.TOTAL),
|
||||||
source: 'kw'
|
source: 'kw',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getList
|
// getList
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { sizeFormate, formatPlayTime } from '../index'
|
import { sizeFormate, formatPlayTime } from '../../index'
|
||||||
import { toMD5, formatSingerName } from '../utils'
|
import { toMD5, formatSingerName } from '../utils'
|
||||||
|
|
||||||
export const createSignature = (time, str) => {
|
export const createSignature = (time, str) => {
|
||||||
@@ -17,100 +17,6 @@ export default {
|
|||||||
page: 0,
|
page: 0,
|
||||||
allPage: 1,
|
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) {
|
musicSearch(str, page, limit) {
|
||||||
const time = Date.now().toString()
|
const time = Date.now().toString()
|
||||||
const signData = createSignature(time, str)
|
const signData = createSignature(time, str)
|
||||||
@@ -124,8 +30,8 @@ export default {
|
|||||||
sign: signData.sign,
|
sign: signData.sign,
|
||||||
channel: '0146921',
|
channel: '0146921',
|
||||||
'User-Agent':
|
'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)
|
return searchRequest.promise.then(({ body }) => body)
|
||||||
@@ -150,28 +56,28 @@ export default {
|
|||||||
size = sizeFormate(type.asize ?? type.isize)
|
size = sizeFormate(type.asize ?? type.isize)
|
||||||
types.push({ type: '128k', size })
|
types.push({ type: '128k', size })
|
||||||
_types['128k'] = {
|
_types['128k'] = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'HQ':
|
case 'HQ':
|
||||||
size = sizeFormate(type.asize ?? type.isize)
|
size = sizeFormate(type.asize ?? type.isize)
|
||||||
types.push({ type: '320k', size })
|
types.push({ type: '320k', size })
|
||||||
_types['320k'] = {
|
_types['320k'] = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'SQ':
|
case 'SQ':
|
||||||
size = sizeFormate(type.asize ?? type.isize)
|
size = sizeFormate(type.asize ?? type.isize)
|
||||||
types.push({ type: 'flac', size })
|
types.push({ type: 'flac', size })
|
||||||
_types.flac = {
|
_types.flac = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'ZQ24':
|
case 'ZQ24':
|
||||||
size = sizeFormate(type.asize ?? type.isize)
|
size = sizeFormate(type.asize ?? type.isize)
|
||||||
types.push({ type: 'flac24bit', size })
|
types.push({ type: 'hires', size })
|
||||||
_types.flac24bit = {
|
_types.hires = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -196,7 +102,7 @@ export default {
|
|||||||
trcUrl: data.trcUrl,
|
trcUrl: data.trcUrl,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -212,7 +118,7 @@ export default {
|
|||||||
return Promise.reject(new Error(result ? result.info : '搜索失败'))
|
return Promise.reject(new Error(result ? result.info : '搜索失败'))
|
||||||
const songResultData = result.songResultData || { resultList: [], totalCount: 0 }
|
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)
|
if (list == null) return this.search(str, page, limit, retryNum)
|
||||||
|
|
||||||
this.total = parseInt(songResultData.totalCount)
|
this.total = parseInt(songResultData.totalCount)
|
||||||
@@ -224,8 +130,8 @@ export default {
|
|||||||
allPage: this.allPage,
|
allPage: this.allPage,
|
||||||
limit,
|
limit,
|
||||||
total: this.total,
|
total: this.total,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { dateFormat, formatPlayCount } from '../index'
|
import { dateFormat, formatPlayCount } from '../../index'
|
||||||
import { filterMusicInfoList } from './musicInfo'
|
import { filterMusicInfoList } from './musicInfo'
|
||||||
import { createSignature } from './musicSearch'
|
import { createSignature } from './musicSearch'
|
||||||
import { createHttpFetch } from './utils/index'
|
import { createHttpFetch } from './utils/index'
|
||||||
@@ -17,14 +17,14 @@ export default {
|
|||||||
sortList: [
|
sortList: [
|
||||||
{
|
{
|
||||||
name: '推荐',
|
name: '推荐',
|
||||||
id: '15127315'
|
id: '15127315',
|
||||||
// id: '1',
|
// id: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '最新',
|
name: '最新',
|
||||||
id: '15127272'
|
id: '15127272',
|
||||||
// id: '2',
|
// id: '2',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
regExps: {
|
regExps: {
|
||||||
list: /<li><div class="thumb">.+?<\/li>/g,
|
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>/,
|
/.+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
|
// 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/MIGUM3.0/v1.0/template/musiclistplaza-taglist/release',
|
||||||
// tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0',
|
// tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0',
|
||||||
@@ -58,7 +58,7 @@ export default {
|
|||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
'User-Agent':
|
'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',
|
||||||
Referer: 'https://m.music.migu.cn/'
|
Referer: 'https://m.music.migu.cn/',
|
||||||
// language: 'Chinese',
|
// language: 'Chinese',
|
||||||
// ua: 'Android_migu',
|
// ua: 'Android_migu',
|
||||||
// mode: 'android',
|
// mode: 'android',
|
||||||
@@ -74,7 +74,7 @@ export default {
|
|||||||
} else if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
} else if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
|
||||||
|
|
||||||
const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), {
|
const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), {
|
||||||
headers: this.defaultHeaders
|
headers: this.defaultHeaders,
|
||||||
})
|
})
|
||||||
return requestObj_listDetail.promise.then(({ body }) => {
|
return requestObj_listDetail.promise.then(({ body }) => {
|
||||||
if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
|
if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
|
||||||
@@ -85,7 +85,7 @@ export default {
|
|||||||
page,
|
page,
|
||||||
limit: this.limit_song,
|
limit: this.limit_song,
|
||||||
total: body.totalCount,
|
total: body.totalCount,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
const requestObj_listDetailInfo = httpFetch(
|
const requestObj_listDetailInfo = httpFetch(
|
||||||
`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`,
|
`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 }) => {
|
return requestObj_listDetailInfo.promise.then(({ body }) => {
|
||||||
@@ -109,7 +109,7 @@ export default {
|
|||||||
img: body.data.imgItem.img,
|
img: body.data.imgItem.img,
|
||||||
desc: body.data.summary,
|
desc: body.data.summary,
|
||||||
author: body.data.ownerName,
|
author: body.data.ownerName,
|
||||||
play_count: formatPlayCount(body.data.opNumItem.playNum)
|
play_count: formatPlayCount(body.data.opNumItem.playNum),
|
||||||
})
|
})
|
||||||
return cachedDetailInfo
|
return cachedDetailInfo
|
||||||
})
|
})
|
||||||
@@ -122,12 +122,12 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'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',
|
||||||
Referer: link
|
Referer: link,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
headers: { location },
|
headers: { location },
|
||||||
statusCode
|
statusCode,
|
||||||
} = await requestObj_listDetailLink.promise
|
} = await requestObj_listDetailLink.promise
|
||||||
// console.log(body, location)
|
// console.log(body, location)
|
||||||
if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)
|
if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)
|
||||||
@@ -153,7 +153,7 @@ export default {
|
|||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.getListDetailList(id, page, retryNum),
|
this.getListDetailList(id, page, retryNum),
|
||||||
this.getListDetailInfo(id, retryNum)
|
this.getListDetailInfo(id, retryNum),
|
||||||
]).then(([listData, info]) => {
|
]).then(([listData, info]) => {
|
||||||
listData.info = info
|
listData.info = info
|
||||||
return listData
|
return listData
|
||||||
@@ -165,7 +165,7 @@ export default {
|
|||||||
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {
|
this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {
|
||||||
headers: this.defaultHeaders
|
headers: this.defaultHeaders,
|
||||||
// headers: {
|
// headers: {
|
||||||
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
|
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
|
||||||
// timestamp: 1578225871982,
|
// timestamp: 1578225871982,
|
||||||
@@ -205,7 +205,7 @@ export default {
|
|||||||
total: parseInt(body.retMsg.countSize),
|
total: parseInt(body.retMsg.countSize),
|
||||||
page,
|
page,
|
||||||
limit: this.limit_list,
|
limit: this.limit_list,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// return this._requestObj_list.promise.then(({ body }) => {
|
// return this._requestObj_list.promise.then(({ body }) => {
|
||||||
@@ -233,7 +233,7 @@ export default {
|
|||||||
grade: item.grade,
|
grade: item.grade,
|
||||||
total: item.contentCount,
|
total: item.contentCount,
|
||||||
desc: item.summary,
|
desc: item.summary,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export default {
|
|||||||
hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({
|
hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
})),
|
})),
|
||||||
tags: rawList.slice(1).map(({ header, content }) => ({
|
tags: rawList.slice(1).map(({ header, content }) => ({
|
||||||
name: header.title,
|
name: header.title,
|
||||||
@@ -263,10 +263,10 @@ export default {
|
|||||||
// parent_name: objectInfo.columnTitle,
|
// parent_name: objectInfo.columnTitle,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}))
|
|
||||||
})),
|
})),
|
||||||
source: 'mg'
|
})),
|
||||||
|
source: 'mg',
|
||||||
}
|
}
|
||||||
// return {
|
// return {
|
||||||
// hotTag: rawList[0].objectInfo.contents.map(item => ({
|
// hotTag: rawList[0].objectInfo.contents.map(item => ({
|
||||||
@@ -313,7 +313,7 @@ export default {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
img: item.musicListPicUrl,
|
img: item.musicListPicUrl,
|
||||||
total: item.musicNum,
|
total: item.musicNum,
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
@@ -331,8 +331,8 @@ export default {
|
|||||||
sign: signResult.sign,
|
sign: signResult.sign,
|
||||||
channel: '0146921',
|
channel: '0146921',
|
||||||
'User-Agent':
|
'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) => {
|
).then((body) => {
|
||||||
if (!body.songListResultData) throw new Error('get song list faild.')
|
if (!body.songListResultData) throw new Error('get song list faild.')
|
||||||
@@ -342,10 +342,10 @@ export default {
|
|||||||
list,
|
list,
|
||||||
limit,
|
limit,
|
||||||
total: parseInt(body.songListResultData.totalCount),
|
total: parseInt(body.songListResultData.totalCount),
|
||||||
source: 'mg'
|
source: 'mg',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getList
|
// getList
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
export const bHh = '624868746c'
|
export const bHh = '624868746c'
|
||||||
|
|
||||||
export const headers = {
|
export const headers = {
|
||||||
'User-Agent': 'lx-music request',
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||||
[bHh]: [bHh]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const timeout = 15000
|
export const timeout = 15000
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { formatPlayTime, sizeFormate } from '../index'
|
import { formatPlayTime, sizeFormate } from '../../index'
|
||||||
import { formatSingerName } from '../utils'
|
import { formatSingerName } from '../utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
const searchRequest = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
const searchRequest = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
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: {
|
body: {
|
||||||
comm: {
|
comm: {
|
||||||
@@ -26,7 +26,7 @@ export default {
|
|||||||
phonetype: '0',
|
phonetype: '0',
|
||||||
devicelevel: '31',
|
devicelevel: '31',
|
||||||
tmeAppID: 'qqmusiclight',
|
tmeAppID: 'qqmusiclight',
|
||||||
nettype: 'NETWORK_WIFI'
|
nettype: 'NETWORK_WIFI',
|
||||||
},
|
},
|
||||||
req: {
|
req: {
|
||||||
module: 'music.search.SearchCgiService',
|
module: 'music.search.SearchCgiService',
|
||||||
@@ -37,10 +37,10 @@ export default {
|
|||||||
num_per_page: limit,
|
num_per_page: limit,
|
||||||
page_num: page,
|
page_num: page,
|
||||||
nqc_flag: 0,
|
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`)
|
// 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 }) => {
|
return searchRequest.promise.then(({ body }) => {
|
||||||
@@ -56,35 +56,56 @@ export default {
|
|||||||
rawList.forEach((item) => {
|
rawList.forEach((item) => {
|
||||||
if (!item.file?.media_mid) return
|
if (!item.file?.media_mid) return
|
||||||
|
|
||||||
const types = []
|
let types = []
|
||||||
const _types = {}
|
let _types = {}
|
||||||
const file = item.file
|
const file = item.file
|
||||||
if (file.size_128mp3 != 0) {
|
if (file.size_128mp3 != 0) {
|
||||||
const size = sizeFormate(file.size_128mp3)
|
let size = sizeFormate(file.size_128mp3)
|
||||||
types.push({ type: '128k', size })
|
types.push({ type: '128k', size })
|
||||||
_types['128k'] = {
|
_types['128k'] = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file.size_320mp3 !== 0) {
|
if (file.size_320mp3 !== 0) {
|
||||||
const size = sizeFormate(file.size_320mp3)
|
let size = sizeFormate(file.size_320mp3)
|
||||||
types.push({ type: '320k', size })
|
types.push({ type: '320k', size })
|
||||||
_types['320k'] = {
|
_types['320k'] = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file.size_flac !== 0) {
|
if (file.size_flac !== 0) {
|
||||||
const size = sizeFormate(file.size_flac)
|
let size = sizeFormate(file.size_flac)
|
||||||
types.push({ type: 'flac', size })
|
types.push({ type: 'flac', size })
|
||||||
_types.flac = {
|
_types.flac = {
|
||||||
size
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file.size_hires !== 0) {
|
if (file.size_hires !== 0) {
|
||||||
const size = sizeFormate(file.size_hires)
|
let size = sizeFormate(file.size_hires)
|
||||||
types.push({ type: 'flac24bit', size })
|
types.push({ type: 'hires', size })
|
||||||
_types.flac24bit = {
|
_types.hires = {
|
||||||
size
|
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()
|
// types.reverse()
|
||||||
@@ -113,7 +134,7 @@ export default {
|
|||||||
: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`,
|
: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// console.log(list)
|
// console.log(list)
|
||||||
@@ -123,7 +144,7 @@ export default {
|
|||||||
if (limit == null) limit = this.limit
|
if (limit == null) limit = this.limit
|
||||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||||
return this.musicSearch(str, page, limit).then(({ body, meta }) => {
|
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.total = meta.estimate_sum
|
||||||
this.page = page
|
this.page = page
|
||||||
@@ -134,8 +155,8 @@ export default {
|
|||||||
allPage: this.allPage,
|
allPage: this.allPage,
|
||||||
limit,
|
limit,
|
||||||
total: this.total,
|
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 { httpFetch } from '../../request'
|
||||||
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
import { decodeName, formatPlayTime, dateFormat, formatPlayCount } from '../../index'
|
||||||
import { formatSingerName } from '../utils'
|
import { formatSingerName } from '../utils'
|
||||||
|
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
_requestObj_tags: null,
|
_requestObj_tags: null,
|
||||||
@@ -12,12 +13,12 @@ export default {
|
|||||||
sortList: [
|
sortList: [
|
||||||
{
|
{
|
||||||
name: '最热',
|
name: '最热',
|
||||||
id: 5
|
id: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '最新',
|
name: '最新',
|
||||||
id: 2
|
id: 2,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
regExps: {
|
regExps: {
|
||||||
hotTagHtml: /class="c_bg_link js_tag_item" data-id="\w+">.+?<\/a>/g,
|
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://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
|
// 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+)/,
|
listDetailLink: /\/playlist\/(\d+)/,
|
||||||
listDetailLink2: /id=(\d+)/
|
listDetailLink2: /id=(\d+)/,
|
||||||
},
|
},
|
||||||
tagsUrl:
|
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',
|
'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,
|
category_id: id,
|
||||||
size: this.limit_list,
|
size: this.limit_list,
|
||||||
page: page - 1,
|
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),
|
sin: this.limit_list * (page - 1),
|
||||||
size: this.limit_list,
|
size: this.limit_list,
|
||||||
order: sortId,
|
order: sortId,
|
||||||
cur_page: page
|
cur_page: page,
|
||||||
|
},
|
||||||
|
module: 'playlist.PlayListPlazaServer',
|
||||||
},
|
},
|
||||||
module: 'playlist.PlayListPlazaServer'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
@@ -95,17 +96,17 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
filterInfoHotTag(html) {
|
filterInfoHotTag(html) {
|
||||||
const hotTag = html.match(this.regExps.hotTagHtml)
|
let hotTag = html.match(this.regExps.hotTagHtml)
|
||||||
const hotTags = []
|
const hotTags = []
|
||||||
if (!hotTag) return hotTags
|
if (!hotTag) return hotTags
|
||||||
|
|
||||||
hotTag.forEach((tagHtml) => {
|
hotTag.forEach((tagHtml) => {
|
||||||
const result = tagHtml.match(this.regExps.hotTag)
|
let result = tagHtml.match(this.regExps.hotTag)
|
||||||
if (!result) return
|
if (!result) return
|
||||||
hotTags.push({
|
hotTags.push({
|
||||||
id: parseInt(result[1]),
|
id: parseInt(result[1]),
|
||||||
name: result[2],
|
name: result[2],
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return hotTags
|
return hotTags
|
||||||
@@ -118,8 +119,8 @@ export default {
|
|||||||
parent_name: type.group_name,
|
parent_name: type.group_name,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}))
|
})),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -130,7 +131,9 @@ export default {
|
|||||||
this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page))
|
this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page))
|
||||||
// console.log(this.getListUrl(sortId, tagId, page))
|
// console.log(this.getListUrl(sortId, tagId, page))
|
||||||
return this._requestObj_list.promise.then(({ body }) => {
|
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
|
return tagId
|
||||||
? this.filterList2(body.playlist.data, page)
|
? this.filterList2(body.playlist.data, page)
|
||||||
: this.filterList(body.playlist.data, page)
|
: this.filterList(body.playlist.data, page)
|
||||||
@@ -149,12 +152,12 @@ export default {
|
|||||||
// grade: item.favorcnt / 10,
|
// grade: item.favorcnt / 10,
|
||||||
total: item.song_ids?.length,
|
total: item.song_ids?.length,
|
||||||
desc: decodeName(item.desc).replace(/<br>/g, '\n'),
|
desc: decodeName(item.desc).replace(/<br>/g, '\n'),
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
})),
|
})),
|
||||||
total: data.total,
|
total: data.total,
|
||||||
page,
|
page,
|
||||||
limit: this.limit_list,
|
limit: this.limit_list,
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterList2({ content }, page) {
|
filterList2({ content }, page) {
|
||||||
@@ -169,12 +172,12 @@ export default {
|
|||||||
img: basic.cover.medium_url || basic.cover.default_url,
|
img: basic.cover.medium_url || basic.cover.default_url,
|
||||||
// grade: basic.favorcnt / 10,
|
// grade: basic.favorcnt / 10,
|
||||||
desc: decodeName(basic.desc).replace(/<br>/g, '\n'),
|
desc: decodeName(basic.desc).replace(/<br>/g, '\n'),
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
})),
|
})),
|
||||||
total: content.total_cnt,
|
total: content.total_cnt,
|
||||||
page,
|
page,
|
||||||
limit: this.limit_list,
|
limit: this.limit_list,
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@ export default {
|
|||||||
const requestObj_listDetailLink = httpFetch(link)
|
const requestObj_listDetailLink = httpFetch(link)
|
||||||
const {
|
const {
|
||||||
headers: { location },
|
headers: { location },
|
||||||
statusCode
|
statusCode,
|
||||||
} = await requestObj_listDetailLink.promise
|
} = await requestObj_listDetailLink.promise
|
||||||
// console.log(headers)
|
// console.log(headers)
|
||||||
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
||||||
@@ -202,7 +205,6 @@ export default {
|
|||||||
if (!result) throw new Error('failed')
|
if (!result) throw new Error('failed')
|
||||||
}
|
}
|
||||||
id = result[1]
|
id = result[1]
|
||||||
// console.log(id)
|
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
},
|
},
|
||||||
@@ -215,15 +217,16 @@ export default {
|
|||||||
const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), {
|
const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), {
|
||||||
headers: {
|
headers: {
|
||||||
Origin: 'https://y.qq.com',
|
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
|
const { body } = await requestObj_listDetail.promise
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)
|
if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)
|
||||||
const cdlist = body.cdlist[0]
|
const cdlist = body.cdlist[0]
|
||||||
return {
|
return {
|
||||||
list: this.filterListDetail(cdlist.songlist),
|
list: await this.filterListDetail(cdlist.songlist),
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: cdlist.songlist.length + 1,
|
limit: cdlist.songlist.length + 1,
|
||||||
total: cdlist.songlist.length,
|
total: cdlist.songlist.length,
|
||||||
@@ -233,44 +236,23 @@ export default {
|
|||||||
img: cdlist.logo,
|
img: cdlist.logo,
|
||||||
desc: decodeName(cdlist.desc).replace(/<br>/g, '\n'),
|
desc: decodeName(cdlist.desc).replace(/<br>/g, '\n'),
|
||||||
author: cdlist.nickname,
|
author: cdlist.nickname,
|
||||||
play_count: formatPlayCount(cdlist.visitnum)
|
play_count: formatPlayCount(cdlist.visitnum),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterListDetail(rawList) {
|
async filterListDetail(rawList) {
|
||||||
// console.log(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) => {
|
return rawList.map((item) => {
|
||||||
const types = []
|
const { types = [], _types = {} } = qualityInfoMap[item.id] || {}
|
||||||
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()
|
|
||||||
return {
|
return {
|
||||||
singer: formatSingerName(item.singer, 'name'),
|
singer: formatSingerName(item.singer, 'name'),
|
||||||
name: item.title,
|
name: item.title,
|
||||||
@@ -292,7 +274,7 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -300,7 +282,7 @@ export default {
|
|||||||
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||||
tags,
|
tags,
|
||||||
hotTag,
|
hotTag,
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -313,12 +295,16 @@ export default {
|
|||||||
search(text, page, limit = 20, retryNum = 0) {
|
search(text, page, limit = 20, retryNum = 0) {
|
||||||
if (retryNum > 5) throw new Error('max retry')
|
if (retryNum > 5) throw new Error('max retry')
|
||||||
return httpFetch(
|
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: {
|
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)',
|
||||||
Referer: 'http://y.qq.com/portal/search.html'
|
Referer: 'http://y.qq.com/portal/search.html',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
).promise.then(({ body }) => {
|
).promise.then(({ body }) => {
|
||||||
if (body.code != 0) return this.search(text, page, limit, ++retryNum)
|
if (body.code != 0) return this.search(text, page, limit, ++retryNum)
|
||||||
@@ -335,15 +321,15 @@ export default {
|
|||||||
// grade: item.favorcnt / 10,
|
// grade: item.favorcnt / 10,
|
||||||
total: item.song_count,
|
total: item.song_count,
|
||||||
desc: decodeName(decodeName(item.introduction)).replace(/<br>/g, '\n'),
|
desc: decodeName(decodeName(item.introduction)).replace(/<br>/g, '\n'),
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
limit,
|
limit,
|
||||||
total: body.data.sum,
|
total: body.data.sum,
|
||||||
source: 'tx'
|
source: 'tx',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getList
|
// getList
|
||||||
|
|||||||
@@ -10,16 +10,13 @@ export const getHostIp = (hostname) => {
|
|||||||
if (typeof result === 'object') return result
|
if (typeof result === 'object') return result
|
||||||
if (result === true) return
|
if (result === true) return
|
||||||
ipMap.set(hostname, true)
|
ipMap.set(hostname, true)
|
||||||
// console.log(hostname)
|
|
||||||
dns.lookup(
|
dns.lookup(
|
||||||
hostname,
|
hostname,
|
||||||
{
|
{
|
||||||
// family: 4,
|
all: false,
|
||||||
all: false
|
|
||||||
},
|
},
|
||||||
(err, address, family) => {
|
(err, address, family) => {
|
||||||
if (err) return console.log(err)
|
if (err) return console.log(err)
|
||||||
// console.log(address, family)
|
|
||||||
ipMap.set(hostname, { address, family })
|
ipMap.set(hostname, { address, family })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -42,7 +39,7 @@ export const formatSingerName = (singers, nameKey = 'name', join = '、') => {
|
|||||||
if (Array.isArray(singers)) {
|
if (Array.isArray(singers)) {
|
||||||
const singer = []
|
const singer = []
|
||||||
singers.forEach((item) => {
|
singers.forEach((item) => {
|
||||||
const name = item[nameKey]
|
let name = item[nameKey]
|
||||||
if (!name) return
|
if (!name) return
|
||||||
singer.push(name)
|
singer.push(name)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,57 +1,23 @@
|
|||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { weapi } from './utils/crypto'
|
import { weapi } from './utils/crypto'
|
||||||
import { formatPlayTime, sizeFormate } from '../index'
|
import { formatPlayTime } from '../../index'
|
||||||
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
|
import { getBatchMusicQualityInfo } from './quality_detail'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getSinger(singers) {
|
getSinger(singers) {
|
||||||
const arr = []
|
let arr = []
|
||||||
singers?.forEach((singer) => {
|
singers?.forEach((singer) => {
|
||||||
arr.push(singer.name)
|
arr.push(singer.name)
|
||||||
})
|
})
|
||||||
return arr.join('、')
|
return arr.join('、')
|
||||||
},
|
},
|
||||||
filterList({ songs, privileges }) {
|
async filterList({ songs, privileges }) {
|
||||||
// console.log(songs, privileges)
|
|
||||||
const list = []
|
const list = []
|
||||||
|
const idList = songs.map((item) => item.id)
|
||||||
|
const qualityInfoMap = await getBatchMusicQualityInfo(idList)
|
||||||
|
|
||||||
songs.forEach((item, index) => {
|
songs.forEach((item, index) => {
|
||||||
const types = []
|
const { types, _types } = qualityInfoMap[item.id] || { types: [], _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()
|
|
||||||
|
|
||||||
if (item.pc) {
|
if (item.pc) {
|
||||||
list.push({
|
list.push({
|
||||||
@@ -67,7 +33,7 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
list.push({
|
list.push({
|
||||||
@@ -83,11 +49,10 @@ export default {
|
|||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// console.log(list)
|
|
||||||
return list
|
return list
|
||||||
},
|
},
|
||||||
async getList(ids = [], retryNum = 0) {
|
async getList(ids = [], retryNum = 0) {
|
||||||
@@ -98,16 +63,15 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
'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({
|
form: weapi({
|
||||||
c: '[' + ids.map((id) => '{"id":' + id + '}').join(',') + ']',
|
c: '[' + ids.map((id) => '{"id":' + id + '}').join(',') + ']',
|
||||||
ids: '[' + ids.join(',') + ']'
|
ids: '[' + ids.join(',') + ']',
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
const { body, statusCode } = await requestObj.promise
|
const { body, statusCode } = await requestObj.promise
|
||||||
if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')
|
if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')
|
||||||
// console.log(body)
|
return { source: 'wy', list: await this.filterList(body) }
|
||||||
return { source: 'wy', list: this.filterList(body) }
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
// import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
// import { weapi } from './utils/crypto'
|
import { sizeFormate, formatPlayTime } from '../../index'
|
||||||
import { sizeFormate, formatPlayTime } from '../index'
|
|
||||||
// import musicDetailApi from './musicDetail'
|
|
||||||
import { eapiRequest } from './utils/index'
|
import { eapiRequest } from './utils/index'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -9,58 +7,83 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
allPage: 1,
|
allPage: 1,
|
||||||
|
|
||||||
musicSearch(str, page, limit) {
|
musicSearch(str, page, limit) {
|
||||||
const searchRequest = eapiRequest('/api/cloudsearch/pc', {
|
const searchRequest = eapiRequest('/api/cloudsearch/pc', {
|
||||||
s: str,
|
s: str,
|
||||||
type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||||
limit,
|
limit,
|
||||||
total: page == 1,
|
total: page == 1,
|
||||||
offset: limit * (page - 1)
|
offset: limit * (page - 1),
|
||||||
})
|
})
|
||||||
return searchRequest.promise.then(({ body }) => body)
|
return searchRequest.promise.then(({ body }) => body)
|
||||||
},
|
},
|
||||||
|
|
||||||
getSinger(singers) {
|
getSinger(singers) {
|
||||||
const arr = []
|
return singers.map((singer) => singer.name).join('、')
|
||||||
singers.forEach((singer) => {
|
|
||||||
arr.push(singer.name)
|
|
||||||
})
|
|
||||||
return arr.join('、')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleResult(rawList) {
|
handleResult(rawList) {
|
||||||
// console.log(rawList)
|
|
||||||
if (!rawList) return []
|
if (!rawList) return []
|
||||||
return rawList.map((item) => {
|
|
||||||
|
return Promise.all(
|
||||||
|
rawList.map(async (item) => {
|
||||||
const types = []
|
const types = []
|
||||||
const _types = {}
|
const _types = {}
|
||||||
let size
|
let size
|
||||||
|
|
||||||
if (item.privilege.maxBrLevel == 'hires') {
|
try {
|
||||||
size = item.hr ? sizeFormate(item.hr.size) : null
|
const requestObj = httpFetch(
|
||||||
types.push({ type: 'flac24bit', size })
|
`https://music.163.com/api/song/music/detail/get?songId=${item.id}`,
|
||||||
_types.flac24bit = {
|
{
|
||||||
size
|
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')
|
||||||
}
|
}
|
||||||
switch (item.privilege.maxbr) {
|
|
||||||
case 999000:
|
if (body.data.jm && body.data.jm.size) {
|
||||||
size = item.sq ? sizeFormate(item.sq.size) : null
|
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.push({ type: 'flac', size })
|
||||||
_types.flac = {
|
_types.flac = { size }
|
||||||
size
|
|
||||||
}
|
}
|
||||||
case 320000:
|
if (body.data.h && body.data.h.size) {
|
||||||
size = item.h ? sizeFormate(item.h.size) : null
|
size = sizeFormate(body.data.h.size)
|
||||||
types.push({ type: '320k', size })
|
types.push({ type: '320k', size })
|
||||||
_types['320k'] = {
|
_types['320k'] = { size }
|
||||||
size
|
|
||||||
}
|
}
|
||||||
case 192000:
|
if (body.data.m && body.data.m.size) {
|
||||||
case 128000:
|
size = sizeFormate(body.data.m.size)
|
||||||
size = item.l ? sizeFormate(item.l.size) : null
|
|
||||||
types.push({ type: '128k', size })
|
types.push({ type: '128k', size })
|
||||||
_types['128k'] = {
|
_types['128k'] = { size }
|
||||||
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()
|
types.reverse()
|
||||||
@@ -77,20 +100,23 @@ export default {
|
|||||||
lrc: null,
|
lrc: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
typeUrl: {}
|
typeUrl: {},
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
search(str, page = 1, limit, retryNum = 0) {
|
search(str, page = 1, limit, retryNum = 0) {
|
||||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||||
if (limit == null) limit = this.limit
|
if (limit == null) limit = this.limit
|
||||||
return this.musicSearch(str, page, limit).then((result) => {
|
return this.musicSearch(str, page, limit).then((result) => {
|
||||||
// console.log(result)
|
|
||||||
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
|
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
|
||||||
const list = this.handleResult(result.result.songs || [])
|
return this.handleResult(result.result.songs || []).then((list) => {
|
||||||
// console.log(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.total = result.result.songCount || 0
|
||||||
this.page = page
|
this.page = page
|
||||||
@@ -101,9 +127,9 @@ export default {
|
|||||||
allPage: this.allPage,
|
allPage: this.allPage,
|
||||||
limit: this.limit,
|
limit: this.limit,
|
||||||
total: this.total,
|
total: this.total,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
}
|
}
|
||||||
// return result.data
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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 { weapi, linuxapi } from './utils/crypto'
|
||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index'
|
import { /* formatPlayTime, */ dateFormat, formatPlayCount } from '../../index'
|
||||||
import musicDetailApi from './musicDetail'
|
import musicDetailApi from './musicDetail'
|
||||||
import { eapiRequest } from './utils/index'
|
import { eapiRequest } from './utils/index'
|
||||||
import { formatSingerName } from '../utils'
|
// import { formatSingerName } from '../utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
_requestObj_tags: null,
|
_requestObj_tags: null,
|
||||||
@@ -16,16 +16,12 @@ export default {
|
|||||||
sortList: [
|
sortList: [
|
||||||
{
|
{
|
||||||
name: '最热',
|
name: '最热',
|
||||||
id: 'hot'
|
id: 'hot',
|
||||||
}
|
},
|
||||||
// {
|
|
||||||
// name: '最新',
|
|
||||||
// id: 'new',
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
regExps: {
|
regExps: {
|
||||||
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
|
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
|
||||||
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/
|
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/,
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleParseId(link, retryNum = 0) {
|
async handleParseId(link, retryNum = 0) {
|
||||||
@@ -34,9 +30,8 @@ export default {
|
|||||||
const requestObj_listDetailLink = httpFetch(link)
|
const requestObj_listDetailLink = httpFetch(link)
|
||||||
const {
|
const {
|
||||||
headers: { location },
|
headers: { location },
|
||||||
statusCode
|
statusCode,
|
||||||
} = await requestObj_listDetailLink.promise
|
} = await requestObj_listDetailLink.promise
|
||||||
// console.log(statusCode)
|
|
||||||
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
|
||||||
const url = location == null ? link : location
|
const url = location == null ? link : location
|
||||||
return this.regExps.listDetailLink.test(url)
|
return this.regExps.listDetailLink.test(url)
|
||||||
@@ -59,13 +54,11 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
id = await this.handleParseId(id)
|
id = await this.handleParseId(id)
|
||||||
}
|
}
|
||||||
// console.log(id)
|
|
||||||
}
|
}
|
||||||
return { id, cookie }
|
return { id, cookie }
|
||||||
},
|
},
|
||||||
async getListDetail(rawId, page, tryNum = 0) {
|
async getListDetail(rawId, page, tryNum = 0) {
|
||||||
// 获取歌曲列表内的音乐
|
if (tryNum > 1000) return Promise.reject(new Error('try max num'))
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
|
||||||
|
|
||||||
const { id, cookie } = await this.getListId(rawId)
|
const { id, cookie } = await this.getListId(rawId)
|
||||||
if (cookie) this.cookie = cookie
|
if (cookie) this.cookie = cookie
|
||||||
@@ -75,7 +68,7 @@ export default {
|
|||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
'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({
|
form: linuxapi({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -83,20 +76,16 @@ export default {
|
|||||||
params: {
|
params: {
|
||||||
id,
|
id,
|
||||||
n: this.limit_song,
|
n: this.limit_song,
|
||||||
s: 8
|
s: 8,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
const { statusCode, body } = await requestObj_listDetail.promise
|
const { statusCode, body } = await requestObj_listDetail.promise
|
||||||
if (statusCode !== 200 || body.code !== this.successCode)
|
if (statusCode !== 200 || body.code !== this.successCode)
|
||||||
return this.getListDetail(id, page, ++tryNum)
|
return this.getListDetail(id, page, ++tryNum)
|
||||||
const limit = 1000
|
let limit = 50
|
||||||
const rangeStart = (page - 1) * limit
|
let rangeStart = (page - 1) * limit
|
||||||
// console.log(body)
|
|
||||||
let list
|
let list
|
||||||
if (body.playlist.trackIds.length == body.privileges.length) {
|
|
||||||
list = this.filterListDetail(body)
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
list = (
|
list = (
|
||||||
await musicDetailApi.getList(
|
await musicDetailApi.getList(
|
||||||
@@ -111,8 +100,6 @@ export default {
|
|||||||
return this.getListDetail(id, page, ++tryNum)
|
return this.getListDetail(id, page, ++tryNum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// console.log(list)
|
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
page,
|
page,
|
||||||
@@ -124,119 +111,79 @@ export default {
|
|||||||
name: body.playlist.name,
|
name: body.playlist.name,
|
||||||
img: body.playlist.coverImgUrl,
|
img: body.playlist.coverImgUrl,
|
||||||
desc: body.playlist.description,
|
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') {
|
// filterListDetail({ playlist: { tracks } }) {
|
||||||
size = item.hr ? sizeFormate(item.hr.size) : null
|
// const list = []
|
||||||
types.push({ type: 'flac24bit', size })
|
// tracks.forEach((item) => {
|
||||||
_types.flac24bit = {
|
// const types = []
|
||||||
size
|
// const _types = {}
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (privilege.maxbr) {
|
|
||||||
case 999000:
|
|
||||||
size = null
|
|
||||||
types.push({ type: 'flac', size })
|
|
||||||
_types.flac = {
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
case 320000:
|
// if (item.pc) {
|
||||||
size = item.h ? sizeFormate(item.h.size) : null
|
// list.push({
|
||||||
types.push({ type: '320k', size })
|
// singer: item.pc.ar ?? '',
|
||||||
_types['320k'] = {
|
// name: item.pc.sn ?? '',
|
||||||
size
|
// 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) {
|
getList(sortId, tagId, page, tryNum = 0) {
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
if (this._requestObj_list) this._requestObj_list.cancelHttp()
|
||||||
this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {
|
this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
form: weapi({
|
form: weapi({
|
||||||
cat: tagId || '全部', // 全部,华语,欧美,日语,韩语,粤语,小语种,流行,摇滚,民谣,电子,舞曲,说唱,轻音乐,爵士,乡村,R&B/Soul,古典,民族,英伦,金属,朋克,蓝调,雷鬼,世界音乐,拉丁,另类/独立,New Age,古风,后摇,Bossa Nova,清晨,夜晚,学习,工作,午休,下午茶,地铁,驾车,运动,旅行,散步,酒吧,怀旧,清新,浪漫,性感,伤感,治愈,放松,孤独,感动,兴奋,快乐,安静,思念,影视原声,ACG,儿童,校园,游戏,70后,80后,90后,网络歌曲,KTV,经典,翻唱,吉他,钢琴,器乐,榜单,00后
|
cat: tagId || '全部',
|
||||||
order: sortId, // hot,new
|
order: sortId,
|
||||||
limit: this.limit_list,
|
limit: this.limit_list,
|
||||||
offset: this.limit_list * (page - 1),
|
offset: this.limit_list * (page - 1),
|
||||||
total: true
|
total: true,
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
return this._requestObj_list.promise.then(({ body }) => {
|
return this._requestObj_list.promise.then(({ body }) => {
|
||||||
// console.log(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 {
|
return {
|
||||||
list: this.filterList(body.playlists),
|
list: this.filterList(body.playlists),
|
||||||
total: parseInt(body.total),
|
total: parseInt(body.total),
|
||||||
page,
|
page,
|
||||||
limit: this.limit_list,
|
limit: this.limit_list,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
filterList(rawData) {
|
filterList(rawData) {
|
||||||
// console.log(rawData)
|
|
||||||
return rawData.map((item) => ({
|
return rawData.map((item) => ({
|
||||||
play_count: formatPlayCount(item.playCount),
|
play_count: formatPlayCount(item.playCount),
|
||||||
id: String(item.id),
|
id: String(item.id),
|
||||||
@@ -247,20 +194,18 @@ export default {
|
|||||||
grade: item.grade,
|
grade: item.grade,
|
||||||
total: item.trackCount,
|
total: item.trackCount,
|
||||||
desc: item.description,
|
desc: item.description,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取标签
|
|
||||||
getTag(tryNum = 0) {
|
getTag(tryNum = 0) {
|
||||||
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
|
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
this._requestObj_tags = httpFetch('https://music.163.com/weapi/playlist/catalogue', {
|
this._requestObj_tags = httpFetch('https://music.163.com/weapi/playlist/catalogue', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
form: weapi({})
|
form: weapi({}),
|
||||||
})
|
})
|
||||||
return this._requestObj_tags.promise.then(({ body }) => {
|
return this._requestObj_tags.promise.then(({ body }) => {
|
||||||
// console.log(JSON.stringify(body))
|
|
||||||
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
||||||
return this.filterTagInfo(body)
|
return this.filterTagInfo(body)
|
||||||
})
|
})
|
||||||
@@ -274,7 +219,7 @@ export default {
|
|||||||
parent_name: categories[item.category],
|
parent_name: categories[item.category],
|
||||||
id: item.name,
|
id: item.name,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,22 +228,20 @@ export default {
|
|||||||
list.push({
|
list.push({
|
||||||
name: categories[key],
|
name: categories[key],
|
||||||
list: subList[key],
|
list: subList[key],
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取热门标签
|
|
||||||
getHotTag(tryNum = 0) {
|
getHotTag(tryNum = 0) {
|
||||||
if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()
|
if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()
|
||||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||||
this._requestObj_hotTags = httpFetch('https://music.163.com/weapi/playlist/hottags', {
|
this._requestObj_hotTags = httpFetch('https://music.163.com/weapi/playlist/hottags', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
form: weapi({})
|
form: weapi({}),
|
||||||
})
|
})
|
||||||
return this._requestObj_hotTags.promise.then(({ body }) => {
|
return this._requestObj_hotTags.promise.then(({ body }) => {
|
||||||
// console.log(JSON.stringify(body))
|
|
||||||
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
if (body.code !== this.successCode) return this.getTag(++tryNum)
|
||||||
return this.filterHotTagInfo(body.tags)
|
return this.filterHotTagInfo(body.tags)
|
||||||
})
|
})
|
||||||
@@ -307,7 +250,7 @@ export default {
|
|||||||
return rawList.map((item) => ({
|
return rawList.map((item) => ({
|
||||||
id: item.playlistTag.name,
|
id: item.playlistTag.name,
|
||||||
name: 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]) => ({
|
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({
|
||||||
tags,
|
tags,
|
||||||
hotTag,
|
hotTag,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -327,23 +270,18 @@ export default {
|
|||||||
search(text, page, limit = 20) {
|
search(text, page, limit = 20) {
|
||||||
return eapiRequest('/api/cloudsearch/pc', {
|
return eapiRequest('/api/cloudsearch/pc', {
|
||||||
s: text,
|
s: text,
|
||||||
type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
type: 1000,
|
||||||
limit,
|
limit,
|
||||||
total: page == 1,
|
total: page == 1,
|
||||||
offset: limit * (page - 1)
|
offset: limit * (page - 1),
|
||||||
}).promise.then(({ body }) => {
|
}).promise.then(({ body }) => {
|
||||||
if (body.code != this.successCode) throw new Error('filed')
|
if (body.code != this.successCode) throw new Error('filed')
|
||||||
// console.log(body)
|
|
||||||
return {
|
return {
|
||||||
list: this.filterList(body.result.playlists),
|
list: this.filterList(body.result.playlists),
|
||||||
limit,
|
limit,
|
||||||
total: body.result.playlistCount,
|
total: body.result.playlistCount,
|
||||||
source: 'wy'
|
source: 'wy',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getList
|
|
||||||
// getTags
|
|
||||||
// getListDetail
|
|
||||||
|
|||||||
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>
|
validateIntegrity: (hashId: string) => Promise<any>
|
||||||
repairData: (hashId: string) => Promise<any>
|
repairData: (hashId: string) => Promise<any>
|
||||||
forceSave: (hashId: string) => Promise<any>
|
forceSave: (hashId: string) => Promise<any>
|
||||||
|
getFavoritesId: () => Promise<any>
|
||||||
|
setFavoritesId: (favoritesId: string) => Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
ai: {
|
ai: {
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ const api = {
|
|||||||
validateIntegrity: (hashId: string) =>
|
validateIntegrity: (hashId: string) =>
|
||||||
ipcRenderer.invoke('songlist:validate-integrity', hashId),
|
ipcRenderer.invoke('songlist:validate-integrity', hashId),
|
||||||
repairData: (hashId: string) => ipcRenderer.invoke('songlist:repair-data', 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'),
|
getUserConfig: () => ipcRenderer.invoke('get-user-config'),
|
||||||
|
|||||||
144
src/renderer/auto-imports.d.ts
vendored
144
src/renderer/auto-imports.d.ts
vendored
@@ -7,90 +7,72 @@
|
|||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
||||||
const EffectScope: (typeof import('vue'))['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
const computed: (typeof import('vue'))['computed']
|
const computed: typeof import('vue')['computed']
|
||||||
const createApp: (typeof import('vue'))['createApp']
|
const createApp: typeof import('vue')['createApp']
|
||||||
const customRef: (typeof import('vue'))['customRef']
|
const customRef: typeof import('vue')['customRef']
|
||||||
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: (typeof import('vue'))['defineComponent']
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
const effectScope: (typeof import('vue'))['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
const getCurrentWatcher: (typeof import('vue'))['getCurrentWatcher']
|
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||||
const h: (typeof import('vue'))['h']
|
const h: typeof import('vue')['h']
|
||||||
const inject: (typeof import('vue'))['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
const isProxy: (typeof import('vue'))['isProxy']
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
const isReactive: (typeof import('vue'))['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: (typeof import('vue'))['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: (typeof import('vue'))['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
const isShallow: (typeof import('vue'))['isShallow']
|
const isShallow: typeof import('vue')['isShallow']
|
||||||
const markRaw: (typeof import('vue'))['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: (typeof import('vue'))['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
const onActivated: (typeof import('vue'))['onActivated']
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
const onMounted: (typeof import('vue'))['onMounted']
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: (typeof import('vue'))['onUpdated']
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
const provide: (typeof import('vue'))['provide']
|
const provide: typeof import('vue')['provide']
|
||||||
const reactive: (typeof import('vue'))['reactive']
|
const reactive: typeof import('vue')['reactive']
|
||||||
const readonly: (typeof import('vue'))['readonly']
|
const readonly: typeof import('vue')['readonly']
|
||||||
const ref: (typeof import('vue'))['ref']
|
const ref: typeof import('vue')['ref']
|
||||||
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: (typeof import('vue'))['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const toRaw: (typeof import('vue'))['toRaw']
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toRef: (typeof import('vue'))['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: (typeof import('vue'))['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
const toValue: (typeof import('vue'))['toValue']
|
const toValue: typeof import('vue')['toValue']
|
||||||
const triggerRef: (typeof import('vue'))['triggerRef']
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
const unref: (typeof import('vue'))['unref']
|
const unref: typeof import('vue')['unref']
|
||||||
const useAttrs: (typeof import('vue'))['useAttrs']
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
const useCssModule: (typeof import('vue'))['useCssModule']
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
const useCssVars: (typeof import('vue'))['useCssVars']
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
const useDialog: (typeof import('naive-ui'))['useDialog']
|
const useDialog: typeof import('naive-ui')['useDialog']
|
||||||
const useId: (typeof import('vue'))['useId']
|
const useId: typeof import('vue')['useId']
|
||||||
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar']
|
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||||
const useMessage: (typeof import('naive-ui'))['useMessage']
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
const useModel: (typeof import('vue'))['useModel']
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useNotification: (typeof import('naive-ui'))['useNotification']
|
const useNotification: typeof import('naive-ui')['useNotification']
|
||||||
const useSlots: (typeof import('vue'))['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
const watch: (typeof import('vue'))['watch']
|
const watch: typeof import('vue')['watch']
|
||||||
const watchEffect: (typeof import('vue'))['watchEffect']
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type {
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
Component,
|
|
||||||
Slot,
|
|
||||||
Slots,
|
|
||||||
ComponentPublicInstance,
|
|
||||||
ComputedRef,
|
|
||||||
DirectiveBinding,
|
|
||||||
ExtractDefaultPropTypes,
|
|
||||||
ExtractPropTypes,
|
|
||||||
ExtractPublicPropTypes,
|
|
||||||
InjectionKey,
|
|
||||||
PropType,
|
|
||||||
Ref,
|
|
||||||
ShallowRef,
|
|
||||||
MaybeRef,
|
|
||||||
MaybeRefOrGetter,
|
|
||||||
VNode,
|
|
||||||
WritableComputedRef
|
|
||||||
} from 'vue'
|
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="virtual-scroll-content" :style="{ transform: `translateY(${offsetY}px)` }">
|
<div class="virtual-scroll-content" :style="{ transform: `translateY(${offsetY}px)` }">
|
||||||
<div
|
<div
|
||||||
v-for="(song, index) in visibleItems"
|
v-for="(song, index) in visibleItems"
|
||||||
:key="song.id || song.songmid"
|
:key="`${song.source || ''}-${song.songmid}-${song.albumId || ''}-${index}`"
|
||||||
class="song-item"
|
class="song-item"
|
||||||
@mouseenter="hoveredSong = song.id || song.songmid"
|
@mouseenter="hoveredSong = song.id || song.songmid"
|
||||||
@mouseleave="hoveredSong = null"
|
@mouseleave="hoveredSong = null"
|
||||||
@@ -58,8 +58,17 @@
|
|||||||
|
|
||||||
<!-- 喜欢按钮 -->
|
<!-- 喜欢按钮 -->
|
||||||
<div class="col-like">
|
<div class="col-like">
|
||||||
<button class="action-btn like-btn" @click.stop>
|
<button
|
||||||
<i class="icon-heart"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,7 +119,8 @@ import {
|
|||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
AddIcon,
|
AddIcon,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
DeleteIcon
|
DeleteIcon,
|
||||||
|
HeartIcon
|
||||||
} from 'tdesign-icons-vue-next'
|
} from 'tdesign-icons-vue-next'
|
||||||
import ContextMenu from '../ContextMenu/ContextMenu.vue'
|
import ContextMenu from '../ContextMenu/ContextMenu.vue'
|
||||||
import { createMenuItem, createSeparator, calculateMenuPosition } from '../ContextMenu/utils'
|
import { createMenuItem, createSeparator, calculateMenuPosition } from '../ContextMenu/utils'
|
||||||
@@ -290,8 +300,9 @@ const getQualityDisplayName = (quality: any) => {
|
|||||||
|
|
||||||
// 处理滚动事件
|
// 处理滚动事件
|
||||||
const onScroll = (event: Event) => {
|
const onScroll = (event: Event) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement | null
|
||||||
scrollTop.value = target.scrollTop
|
// 兼容程序触发的假事件,target 可能为 null
|
||||||
|
scrollTop.value = target?.scrollTop ?? scrollContainer.value?.scrollTop ?? 0
|
||||||
emit('scroll', event)
|
emit('scroll', event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +416,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) => {
|
const handleAddToSongList = async (song: Song, playlist: SongList) => {
|
||||||
try {
|
try {
|
||||||
@@ -420,7 +514,7 @@ const handleAddToSongList = async (song: Song, playlist: SongList) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 组件挂载后触发一次重新计算
|
// 组件挂载后触发一次重新计算
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (scrollContainer.value) {
|
if (scrollContainer.value) {
|
||||||
@@ -431,10 +525,15 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载歌单列表
|
// 加载歌单列表
|
||||||
loadPlaylists()
|
await loadPlaylists()
|
||||||
|
// 预加载“我的喜欢”集合(确保方法存在于当前文件作用域)
|
||||||
|
await loadFavorites()
|
||||||
|
|
||||||
// 监听歌单变化事件
|
// 监听歌单变化事件
|
||||||
window.addEventListener('playlist-updated', loadPlaylists)
|
window.addEventListener('playlist-updated', async () => {
|
||||||
|
await loadPlaylists()
|
||||||
|
await loadFavorites()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -92,94 +92,135 @@ const state = reactive({
|
|||||||
// 监听歌曲ID变化,获取歌词
|
// 监听歌曲ID变化,获取歌词
|
||||||
watch(
|
watch(
|
||||||
() => props.songId,
|
() => props.songId,
|
||||||
async (newId) => {
|
async (newId, _oldId, onCleanup) => {
|
||||||
if (!newId || !props.songInfo) return
|
if (!newId || !props.songInfo) return
|
||||||
let lyricText = ''
|
// 竞态与取消控制,防止内存泄漏与过期结果覆盖
|
||||||
|
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 {
|
||||||
|
const source =
|
||||||
|
props.songInfo && 'source' in props.songInfo ? (props.songInfo as any).source : 'kg'
|
||||||
let parsedLyrics: LyricLine[] = []
|
let parsedLyrics: LyricLine[] = []
|
||||||
// 创建一个符合 MusicItem 接口的对象,只包含必要的基本属性
|
|
||||||
|
|
||||||
|
if (source === 'wy') {
|
||||||
|
// 网易云:优先尝试 TTML
|
||||||
try {
|
try {
|
||||||
// 检查是否为网易云音乐,只有网易云才使用ttml接口
|
const res = await (
|
||||||
const isNetease =
|
await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`, {
|
||||||
props.songInfo && 'source' in props.songInfo && props.songInfo.source === 'wy'
|
signal: abort.signal
|
||||||
const songinfo: any = _.cloneDeep(toRaw(props.songInfo))
|
})
|
||||||
|
).text()
|
||||||
if (isNetease) {
|
if (!active) return
|
||||||
// 网易云音乐优先尝试ttml接口
|
|
||||||
try {
|
|
||||||
const res = (await (
|
|
||||||
await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`)
|
|
||||||
).text()) as any
|
|
||||||
if (!res || res.length < 100) throw new Error('ttml 无歌词')
|
if (!res || res.length < 100) throw new Error('ttml 无歌词')
|
||||||
parsedLyrics = parseTTML(res).lines
|
parsedLyrics = parseTTML(res).lines
|
||||||
} catch {
|
} catch {
|
||||||
// ttml失败后使用新的歌词API
|
// 回退到统一歌词 API
|
||||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||||
source: 'wy',
|
source: 'wy',
|
||||||
songInfo: songinfo
|
songInfo: _.cloneDeep(toRaw(props.songInfo)) as any
|
||||||
})
|
})
|
||||||
|
if (!active) return
|
||||||
|
|
||||||
if (lyricData.crlyric) {
|
if (lyricData?.crlyric) {
|
||||||
// 使用逐字歌词
|
parsedLyrics = parseYrc(lyricData.crlyric)
|
||||||
lyricText = lyricData.crlyric
|
} else if (lyricData?.lyric) {
|
||||||
|
parsedLyrics = parseLrc(lyricData.lyric)
|
||||||
parsedLyrics = parseYrc(lyricText)
|
|
||||||
} else if (lyricData.lyric) {
|
|
||||||
lyricText = lyricData.lyric
|
|
||||||
parsedLyrics = parseLrc(lyricText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricData.tlyric) {
|
parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
|
||||||
const translatedline = parseLrc(lyricData.tlyric)
|
|
||||||
|
|
||||||
for (let i = 0; i < parsedLyrics.length; i++) {
|
|
||||||
if (translatedline[i] && translatedline[i].words[0]) {
|
|
||||||
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 其他音乐平台直接使用新的歌词API
|
// 其他来源:直接统一歌词 API
|
||||||
const source = props.songInfo && 'source' in props.songInfo ? props.songInfo.source : 'kg'
|
|
||||||
// 创建一个纯净的对象,避免Vue响应式对象序列化问题
|
|
||||||
const cleanSongInfo = JSON.parse(JSON.stringify(toRaw(props.songInfo)))
|
|
||||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||||
source: source,
|
source,
|
||||||
songInfo: cleanSongInfo
|
songInfo: getCleanSongInfo()
|
||||||
})
|
})
|
||||||
|
if (!active) return
|
||||||
|
|
||||||
if (lyricData.crlyric) {
|
if (lyricData?.crlyric) {
|
||||||
// 使用逐字歌词
|
parsedLyrics = parseCrLyricBySource(source, lyricData.crlyric)
|
||||||
lyricText = lyricData.crlyric
|
} else if (lyricData?.lyric) {
|
||||||
if (source === 'tx') {
|
parsedLyrics = parseLrc(lyricData.lyric)
|
||||||
parsedLyrics = parseQrc(lyricText)
|
|
||||||
} else {
|
|
||||||
parsedLyrics = parseYrc(lyricText)
|
|
||||||
}
|
|
||||||
} else if (lyricData.lyric) {
|
|
||||||
lyricText = lyricData.lyric
|
|
||||||
parsedLyrics = parseLrc(lyricText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricData.tlyric) {
|
parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
|
||||||
const translatedline = parseLrc(lyricData.tlyric)
|
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
|
||||||
state.lyricLines = []
|
|
||||||
}
|
}
|
||||||
|
if (!active) return
|
||||||
|
state.lyricLines = parsedLyrics.length > 0 ? parsedLyrics : []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取歌词失败:', error)
|
console.error('获取歌词失败:', error)
|
||||||
|
// 若已无效或已清理,避免写入与持有引用
|
||||||
|
if (!active) return
|
||||||
state.lyricLines = []
|
state.lyricLines = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -211,7 +252,9 @@ async function updateTextColor() {
|
|||||||
useBlackText.value = false // 默认使用白色文本
|
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 })
|
watch(() => actualCoverImage.value, updateTextColor, { immediate: true })
|
||||||
|
|
||||||
@@ -256,6 +299,8 @@ onBeforeUnmount(async () => {
|
|||||||
if (unsubscribePlay.value) {
|
if (unsubscribePlay.value) {
|
||||||
unsubscribePlay.value()
|
unsubscribePlay.value()
|
||||||
}
|
}
|
||||||
|
bgRef.value?.bgRender?.dispose()
|
||||||
|
lyricPlayerRef.value?.lyricPlayer?.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听音频URL变化
|
// 监听音频URL变化
|
||||||
@@ -316,7 +361,7 @@ const lyricTranslateY = computed(() => {
|
|||||||
:album-is-video="false"
|
:album-is-video="false"
|
||||||
:fps="30"
|
:fps="30"
|
||||||
:flow-speed="4"
|
: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"
|
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1"
|
||||||
/>
|
/>
|
||||||
<!-- 全屏按钮 -->
|
<!-- 全屏按钮 -->
|
||||||
@@ -382,11 +427,7 @@ const lyricTranslateY = computed(() => {
|
|||||||
class="lyric-player"
|
class="lyric-player"
|
||||||
:enable-spring="playSetting.getisJumpLyric"
|
:enable-spring="playSetting.getisJumpLyric"
|
||||||
:enable-scale="playSetting.getisJumpLyric"
|
:enable-scale="playSetting.getisJumpLyric"
|
||||||
@line-click="
|
@line-click="jumpTime"
|
||||||
(e) => {
|
|
||||||
if (Audio.audio) Audio.audio.currentTime = e.line.getLine().startTime / 1000
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
</LyricPlayer>
|
</LyricPlayer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,12 +29,45 @@ import {
|
|||||||
} from '@renderer/utils/playlist/playlistManager'
|
} from '@renderer/utils/playlist/playlistManager'
|
||||||
import mediaSessionController from '@renderer/utils/audio/useSmtc'
|
import mediaSessionController from '@renderer/utils/audio/useSmtc'
|
||||||
import defaultCoverImg from '/default-cover.png'
|
import defaultCoverImg from '/default-cover.png'
|
||||||
|
import { downloadSingleSong } from '@renderer/utils/audio/download'
|
||||||
|
import { HeartIcon, DownloadIcon } from 'tdesign-icons-vue-next'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { songListAPI } from '@renderer/api/songList'
|
||||||
|
|
||||||
const controlAudio = ControlAudioStore()
|
const controlAudio = ControlAudioStore()
|
||||||
const localUserStore = LocalUserDetailStore()
|
const localUserStore = LocalUserDetailStore()
|
||||||
const { Audio } = storeToRefs(controlAudio)
|
const { Audio } = storeToRefs(controlAudio)
|
||||||
const { list, userInfo } = storeToRefs(localUserStore)
|
const { list, userInfo } = storeToRefs(localUserStore)
|
||||||
const { setCurrentTime, start, stop, setVolume, setUrl } = controlAudio
|
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 showFullPlay = ref(false)
|
||||||
document.addEventListener('keydown', KeyEvent)
|
document.addEventListener('keydown', KeyEvent)
|
||||||
// 处理最小化右键的事件
|
// 处理最小化右键的事件
|
||||||
@@ -362,6 +395,19 @@ const handleVolumeDragEnd = () => {
|
|||||||
window.removeEventListener('mouseup', 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 showPlaylist = ref(false)
|
||||||
const playlistDrawerRef = ref<InstanceType<typeof PlaylistDrawer> | null>(null)
|
const playlistDrawerRef = ref<InstanceType<typeof PlaylistDrawer> | null>(null)
|
||||||
@@ -620,6 +666,86 @@ const toggleFullPlay = () => {
|
|||||||
showFullPlay.value = !showFullPlay.value
|
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 progressRef = ref<HTMLDivElement | null>(null)
|
||||||
const isDraggingProgress = ref(false)
|
const isDraggingProgress = ref(false)
|
||||||
@@ -864,6 +990,36 @@ watch(showFullPlay, (val) => {
|
|||||||
<div class="song-name">{{ songInfo.name }}</div>
|
<div class="song-name">{{ songInfo.name }}</div>
|
||||||
<div class="artist-name">{{ songInfo.singer }}</div>
|
<div class="artist-name">{{ songInfo.singer }}</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 中间:播放控制 -->
|
<!-- 中间:播放控制 -->
|
||||||
@@ -909,6 +1065,7 @@ watch(showFullPlay, (val) => {
|
|||||||
class="volume-control"
|
class="volume-control"
|
||||||
@mouseenter="showVolumeSlider = true"
|
@mouseenter="showVolumeSlider = true"
|
||||||
@mouseleave="showVolumeSlider = false"
|
@mouseleave="showVolumeSlider = false"
|
||||||
|
@wheel.prevent="handleVolumeWheel"
|
||||||
>
|
>
|
||||||
<button class="control-btn">
|
<button class="control-btn">
|
||||||
<shengyin style="width: 1.5em; height: 1.5em" />
|
<shengyin style="width: 1.5em; height: 1.5em" />
|
||||||
@@ -1176,6 +1333,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 {
|
.center-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -606,8 +606,8 @@ const handleSuggestionSelect = (suggestion: any, _type: any) => {
|
|||||||
|
|
||||||
.mainContent {
|
.mainContent {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
// overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 0;
|
height: 0;
|
||||||
/* 确保flex子元素能够正确计算高度 */
|
/* 确保flex子元素能够正确计算高度 */
|
||||||
|
|||||||
@@ -67,18 +67,11 @@ function setAnimate(routerObj: RouteRecordRaw[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAnimate(routes)
|
setAnimate(routes)
|
||||||
|
|
||||||
const option: RouterOptions = {
|
const option: RouterOptions = {
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
routes
|
||||||
scrollBehavior(_to_, _from_, savedPosition) {
|
|
||||||
if (savedPosition) {
|
|
||||||
return savedPosition
|
|
||||||
} else {
|
|
||||||
return { top: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createRouter(option)
|
const router = createRouter(option)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import { NotifyPlugin, MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
|||||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||||
import { useSettingsStore } from '@renderer/store/Settings'
|
import { useSettingsStore } from '@renderer/store/Settings'
|
||||||
import { toRaw, h } from 'vue'
|
import { toRaw, h } from 'vue'
|
||||||
|
import {
|
||||||
|
QUALITY_ORDER,
|
||||||
|
getQualityDisplayName,
|
||||||
|
buildQualityFormats,
|
||||||
|
getHighestQualityType,
|
||||||
|
compareQuality,
|
||||||
|
type KnownQuality
|
||||||
|
} from '@common/utils/quality'
|
||||||
|
|
||||||
interface MusicItem {
|
interface MusicItem {
|
||||||
singer: string
|
singer: string
|
||||||
@@ -18,44 +26,17 @@ interface MusicItem {
|
|||||||
typeUrl: Record<string, any>
|
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> {
|
function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const LocalUserDetail = LocalUserDetailStore()
|
|
||||||
|
|
||||||
// 获取歌曲支持的音质列表
|
// 获取歌曲支持的音质列表
|
||||||
const availableQualities = songInfo.types || []
|
const availableQualities = buildQualityFormats(songInfo.types || [])
|
||||||
|
// 展示全部音质,但对超出用户最高音质的项做禁用呈现
|
||||||
// 检查用户设置的音质是否为特殊音质
|
const userMaxIndex = QUALITY_ORDER.indexOf(userQuality as KnownQuality)
|
||||||
const isSpecialQuality = ['hires', 'atmos', 'master'].includes(userQuality)
|
|
||||||
|
|
||||||
// 如果是特殊音质且用户支持,添加到选项中(不管歌曲是否有这个音质)
|
|
||||||
const qualityOptions = [...availableQualities]
|
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) => {
|
qualityOptions.sort((a, b) => compareQuality(a.type, b.type))
|
||||||
const aIndex = qualityKey.indexOf(a.type)
|
|
||||||
const bIndex = qualityKey.indexOf(b.type)
|
|
||||||
return bIndex - aIndex // 降序排列,高音质在前
|
|
||||||
})
|
|
||||||
|
|
||||||
const dialog = DialogPlugin.confirm({
|
const dialog = DialogPlugin.confirm({
|
||||||
header: '选择下载音质(可滚动)',
|
header: '选择下载音质(可滚动)',
|
||||||
@@ -80,35 +61,48 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
|||||||
msOverflowStyle: 'none'
|
msOverflowStyle: 'none'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
qualityOptions.map((quality) =>
|
qualityOptions.map((quality) => {
|
||||||
h(
|
const idx = QUALITY_ORDER.indexOf(quality.type as KnownQuality)
|
||||||
|
const disabled = userMaxIndex !== -1 && idx !== -1 && idx < userMaxIndex
|
||||||
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
key: quality.type,
|
key: quality.type,
|
||||||
class: 'quality-item',
|
class: 'quality-item',
|
||||||
|
title: disabled ? '超出你的最高音质设置,已禁用' : undefined,
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
margin: '8px 0',
|
margin: '8px 0',
|
||||||
border: '1px solid #e7e7e7',
|
border: '1px solid ' + (disabled ? '#f0f0f0' : '#e7e7e7'),
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
cursor: 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
transition: 'all 0.2s ease',
|
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: () => {
|
onClick: () => {
|
||||||
|
if (disabled) return
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
resolve(quality.type)
|
resolve(quality.type)
|
||||||
},
|
},
|
||||||
onMouseenter: (e: MouseEvent) => {
|
onMouseenter: (e: MouseEvent) => {
|
||||||
|
if (disabled) return
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
target.style.backgroundColor = '#f0f9ff'
|
target.style.backgroundColor = '#f0f9ff'
|
||||||
target.style.borderColor = '#1890ff'
|
target.style.borderColor = '#1890ff'
|
||||||
},
|
},
|
||||||
onMouseleave: (e: MouseEvent) => {
|
onMouseleave: (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
|
if (disabled) {
|
||||||
|
target.style.backgroundColor =
|
||||||
|
quality.type === userQuality ? '#f5faff' : '#fff'
|
||||||
|
target.style.borderColor = '#f0f0f0'
|
||||||
|
return
|
||||||
|
}
|
||||||
target.style.backgroundColor =
|
target.style.backgroundColor =
|
||||||
quality.type === userQuality ? '#e6f7ff' : '#fff'
|
quality.type === userQuality ? '#e6f7ff' : '#fff'
|
||||||
target.style.borderColor = '#e7e7e7'
|
target.style.borderColor = '#e7e7e7'
|
||||||
@@ -122,17 +116,22 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
|||||||
style: {
|
style: {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: '14px',
|
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(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style: {
|
style: {
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#999',
|
color: disabled ? '#bbb' : '#999',
|
||||||
marginTop: '2px'
|
marginTop: '2px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +144,7 @@ function createQualityDialog(songInfo: MusicItem, userQuality: string): Promise<
|
|||||||
class: 'quality-size',
|
class: 'quality-size',
|
||||||
style: {
|
style: {
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#666',
|
color: disabled ? '#999' : '#666',
|
||||||
fontWeight: '500'
|
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> {
|
async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
console.log('开始下载', toRaw(songInfo))
|
||||||
const LocalUserDetail = LocalUserDetailStore()
|
const LocalUserDetail = LocalUserDetailStore()
|
||||||
const userQuality = LocalUserDetail.userSource.quality as string
|
const userQuality = LocalUserDetail.userSource.quality as string
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
@@ -186,68 +186,20 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let quality = selectedQuality
|
let quality = selectedQuality as string
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查选择的音质是否超出歌曲支持的最高音质
|
// 检查选择的音质是否超出歌曲支持的最高音质
|
||||||
const songMaxQuality = songInfo.types[songInfo.types.length - 1]?.type
|
const songMaxQuality = getHighestQualityType(songInfo.types)
|
||||||
if (songMaxQuality && qualityKey.indexOf(quality) > qualityKey.indexOf(songMaxQuality)) {
|
if (
|
||||||
|
songMaxQuality &&
|
||||||
|
QUALITY_ORDER.indexOf(quality as KnownQuality) <
|
||||||
|
QUALITY_ORDER.indexOf(songMaxQuality as KnownQuality)
|
||||||
|
) {
|
||||||
quality = songMaxQuality
|
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 tip = MessagePlugin.success('开始下载歌曲:' + songInfo.name)
|
||||||
|
|
||||||
const result = await window.api.music.requestSdk('downloadSingleSong', {
|
const result = await window.api.music.requestSdk('downloadSingleSong', {
|
||||||
@@ -255,7 +207,8 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
|||||||
source: songInfo.source,
|
source: songInfo.source,
|
||||||
quality,
|
quality,
|
||||||
songInfo: toRaw(songInfo) as any,
|
songInfo: toRaw(songInfo) as any,
|
||||||
tagWriteOptions: toRaw(settingsStore.settings.tagWriteOptions)
|
tagWriteOptions: toRaw(settingsStore.settings.tagWriteOptions),
|
||||||
|
isCache: true
|
||||||
})
|
})
|
||||||
|
|
||||||
;(await tip).close()
|
;(await tip).close()
|
||||||
|
|||||||
@@ -42,38 +42,6 @@ export async function getSongRealUrl(song: SongList): Promise<string> {
|
|||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const isCache = settingsStore.settings.autoCacheMusic ?? true
|
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 (
|
if (
|
||||||
qualityKey.indexOf(quality) >
|
qualityKey.indexOf(quality) >
|
||||||
@@ -115,16 +83,34 @@ export async function addToPlaylistAndPlay(
|
|||||||
playSongCallback: (song: SongList) => Promise<void>
|
playSongCallback: (song: SongList) => Promise<void>
|
||||||
) {
|
) {
|
||||||
try {
|
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)
|
const playResult = playSongCallback(song)
|
||||||
if (playResult && typeof playResult.then === 'function') {
|
if (playResult && typeof playResult.then === 'function') {
|
||||||
await playResult
|
await playResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// await MessagePlugin.success('已添加到播放列表并开始播放')
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('播放失败:', error)
|
console.error('播放失败:', error)
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
|
|||||||
@@ -23,10 +23,3 @@
|
|||||||
<PlayMusic />
|
<PlayMusic />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.animate__animated {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ onUnmounted(() => {
|
|||||||
.find-container {
|
.find-container {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router'
|
||||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||||
@@ -27,6 +27,10 @@ const LocalUserDetail = LocalUserDetailStore()
|
|||||||
// 响应式状态
|
// 响应式状态
|
||||||
const songs = ref<MusicItem[]>([])
|
const songs = ref<MusicItem[]>([])
|
||||||
const loading = ref(true)
|
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 currentSong = ref<MusicItem | null>(null)
|
||||||
const isPlaying = ref(false)
|
const isPlaying = ref(false)
|
||||||
const playlistInfo = ref({
|
const playlistInfo = ref({
|
||||||
@@ -60,8 +64,8 @@ const fetchPlaylistSongs = async () => {
|
|||||||
// 处理本地歌单
|
// 处理本地歌单
|
||||||
await fetchLocalPlaylistSongs()
|
await fetchLocalPlaylistSongs()
|
||||||
} else {
|
} else {
|
||||||
// 处理网络歌单
|
// 处理网络歌单(重置并加载第一页)
|
||||||
await fetchNetworkPlaylistSongs()
|
await fetchNetworkPlaylistSongs(true)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取歌单歌曲失败:', error)
|
console.error('获取歌单歌曲失败:', error)
|
||||||
@@ -116,22 +120,43 @@ const fetchLocalPlaylistSongs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取网络歌单歌曲
|
/**
|
||||||
const fetchNetworkPlaylistSongs = async () => {
|
* 获取网络歌单歌曲,支持重置与分页追加
|
||||||
|
* @param reset 是否重置为第一页
|
||||||
|
*/
|
||||||
|
const fetchNetworkPlaylistSongs = async (reset = false) => {
|
||||||
try {
|
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', {
|
const result = (await window.api.music.requestSdk('getPlaylistDetail', {
|
||||||
source: playlistInfo.value.source,
|
source: playlistInfo.value.source,
|
||||||
id: playlistInfo.value.id,
|
id: playlistInfo.value.id,
|
||||||
page: 1
|
page: currentPage.value
|
||||||
})) as any
|
})) as any
|
||||||
|
const limit = Number(result?.limit ?? pageSize)
|
||||||
|
|
||||||
console.log(result)
|
if (result && Array.isArray(result.list)) {
|
||||||
if (result && result.list) {
|
const newList = result.list
|
||||||
songs.value = result.list
|
|
||||||
|
|
||||||
// 获取歌曲封面
|
if (reset) {
|
||||||
setPic(0, playlistInfo.value.source)
|
songs.value = newList
|
||||||
|
} else {
|
||||||
|
songs.value = [...songs.value, ...newList]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取新增歌曲封面
|
||||||
|
setPic((currentPage.value - 1) * limit, playlistInfo.value.source)
|
||||||
|
|
||||||
// 如果API返回了歌单详细信息,更新歌单信息
|
// 如果API返回了歌单详细信息,更新歌单信息
|
||||||
if (result.info) {
|
if (result.info) {
|
||||||
@@ -143,10 +168,28 @@ const fetchNetworkPlaylistSongs = async () => {
|
|||||||
total: result.info.total || playlistInfo.value.total
|
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) {
|
} catch (error) {
|
||||||
console.error('获取网络歌单失败:', 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) => {
|
const handleScroll = (event?: Event) => {
|
||||||
let scrollTop = 0
|
let scrollTop = 0
|
||||||
|
let scrollHeight = 0
|
||||||
|
let clientHeight = 0
|
||||||
|
|
||||||
if (event && event.target) {
|
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) {
|
} else if (scrollContainer.value) {
|
||||||
scrollTop = scrollContainer.value.scrollTop
|
scrollTop = scrollContainer.value.scrollTop
|
||||||
|
scrollHeight = scrollContainer.value.scrollHeight
|
||||||
|
clientHeight = scrollContainer.value.clientHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollY.value = scrollTop
|
scrollY.value = scrollTop
|
||||||
// 当滚动超过100px时,启用紧凑模式
|
// 当滚动超过100px时,启用紧凑模式
|
||||||
isHeaderCompact.value = scrollY.value > 100
|
isHeaderCompact.value = scrollY.value > 100
|
||||||
|
|
||||||
|
// 触底加载(参考 search.vue)
|
||||||
|
if (
|
||||||
|
scrollHeight > 0 &&
|
||||||
|
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||||
|
!loadingMore.value &&
|
||||||
|
hasMore.value &&
|
||||||
|
!isLocalPlaylist.value
|
||||||
|
) {
|
||||||
|
fetchNetworkPlaylistSongs(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 组件挂载时获取数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPlaylistSongs()
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ const localSongs = ref<LocalSong[]>([
|
|||||||
// 歌单列表
|
// 歌单列表
|
||||||
const playlists = ref<SongList[]>([])
|
const playlists = ref<SongList[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
// 喜欢歌单ID(用于排序与标记)
|
||||||
|
const favoritesId = ref<string | null>(null)
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
const showCreatePlaylistDialog = ref(false)
|
const showCreatePlaylistDialog = ref(false)
|
||||||
@@ -192,6 +194,18 @@ const loadPlaylists = async () => {
|
|||||||
const result = await songListAPI.getAll()
|
const result = await songListAPI.getAll()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
playlists.value = result.data || []
|
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 {
|
} else {
|
||||||
MessagePlugin.error(result.error || '加载歌单失败')
|
MessagePlugin.error(result.error || '加载歌单失败')
|
||||||
}
|
}
|
||||||
@@ -517,7 +531,7 @@ const setPicForPlaylist = async (songs: any[], source: string) => {
|
|||||||
// 处理网络歌单导入
|
// 处理网络歌单导入
|
||||||
const handleNetworkPlaylistImport = async (input: string) => {
|
const handleNetworkPlaylistImport = async (input: string) => {
|
||||||
try {
|
try {
|
||||||
const load1 = MessagePlugin.loading('正在解析歌单链接...')
|
const load1 = MessagePlugin.loading('正在解析歌单链接...', 0)
|
||||||
|
|
||||||
let playlistId: string = ''
|
let playlistId: string = ''
|
||||||
let platformName: string = ''
|
let platformName: string = ''
|
||||||
@@ -541,14 +555,26 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
|||||||
}
|
}
|
||||||
platformName = '网易云音乐'
|
platformName = '网易云音乐'
|
||||||
} else if (importPlatformType.value === 'tx') {
|
} else if (importPlatformType.value === 'tx') {
|
||||||
// QQ音乐歌单ID解析 - 支持多种链接格式
|
// 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) {}
|
||||||
|
|
||||||
|
if (parsedId) {
|
||||||
|
playlistId = parsedId
|
||||||
|
} else {
|
||||||
const qqPlaylistRegexes = [
|
const qqPlaylistRegexes = [
|
||||||
// 标准歌单链接
|
// 标准歌单链接(强烈推荐)
|
||||||
/(?:y\.qq\.com\/n\/ryqq\/playlist\/|music\.qq\.com\/.*[?&]id=|playlist[?&]id=)(\d+)/i,
|
/(?: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,
|
/(?: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,
|
||||||
/(?:c\.y\.qq\.com\/base\/fcgi-bin\/u\?.*__=)(\d+)/i,
|
|
||||||
// 手机版链接
|
// 手机版链接
|
||||||
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i,
|
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i,
|
||||||
// 通用ID提取 - 匹配 id= 或 &id= 参数
|
// 通用ID提取 - 匹配 id= 或 &id= 参数
|
||||||
@@ -575,6 +601,7 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
platformName = 'QQ音乐'
|
platformName = 'QQ音乐'
|
||||||
} else if (importPlatformType.value === 'kw') {
|
} else if (importPlatformType.value === 'kw') {
|
||||||
// 酷我音乐歌单ID解析
|
// 酷我音乐歌单ID解析
|
||||||
@@ -680,18 +707,11 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证歌单ID是否有效
|
|
||||||
if (!playlistId || playlistId.length < 6) {
|
|
||||||
MessagePlugin.error('歌单ID格式不正确')
|
|
||||||
load1.then((res) => res.close())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭加载提示
|
// 关闭加载提示
|
||||||
load1.then((res) => res.close())
|
load1.then((res) => res.close())
|
||||||
|
|
||||||
// 获取歌单详情
|
// 获取歌单详情
|
||||||
const load2 = MessagePlugin.loading('正在获取歌单信息...')
|
const load2 = MessagePlugin.loading('正在获取歌单信息,请不要离开页面...', 0)
|
||||||
|
|
||||||
const getListDetail = async (page: number) => {
|
const getListDetail = async (page: number) => {
|
||||||
let detailResult: any
|
let detailResult: any
|
||||||
@@ -701,6 +721,7 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
|||||||
id: playlistId,
|
id: playlistId,
|
||||||
page: page
|
page: page
|
||||||
})) as any
|
})) as any
|
||||||
|
console.log('list', detailResult)
|
||||||
} catch {
|
} catch {
|
||||||
MessagePlugin.error(`获取${platformName}歌单详情失败:歌曲信息可能有误`)
|
MessagePlugin.error(`获取${platformName}歌单详情失败:歌曲信息可能有误`)
|
||||||
load2.then((res) => res.close())
|
load2.then((res) => res.close())
|
||||||
@@ -728,6 +749,7 @@ const handleNetworkPlaylistImport = async (input: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (detailResult.total < songs.length) break
|
||||||
page++
|
page++
|
||||||
const { list: songsList } = await getListDetail(page)
|
const { list: songsList } = await getListDetail(page)
|
||||||
if (!(songsList && songsList.length)) {
|
if (!(songsList && songsList.length)) {
|
||||||
@@ -1022,6 +1044,14 @@ onMounted(() => {
|
|||||||
<div class="playlist-info">
|
<div class="playlist-info">
|
||||||
<div class="playlist-name" :title="playlist.name" @click="viewPlaylist(playlist)">
|
<div class="playlist-name" :title="playlist.name" @click="viewPlaylist(playlist)">
|
||||||
{{ playlist.name }}
|
{{ playlist.name }}
|
||||||
|
<t-tag
|
||||||
|
v-if="playlist.id === favoritesId"
|
||||||
|
theme="danger"
|
||||||
|
variant="light-outline"
|
||||||
|
size="small"
|
||||||
|
style="margin-left: 6px"
|
||||||
|
>我的喜欢</t-tag
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="playlist.description"
|
v-if="playlist.description"
|
||||||
@@ -1441,7 +1471,8 @@ onMounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.local-container {
|
.local-container {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -1774,7 +1805,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.playlist-cover {
|
.playlist-cover {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
background: linear-gradient(135deg, var(--td-brand-color-4) 0%, var(--td-brand-color-6) 100%);
|
background: #e4e4e4;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -210,7 +210,9 @@ const formatPlayTime = (timeStr: string): string => {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
min-height: 100vh;
|
// min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ const handleScroll = (event: Event) => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-header {
|
.search-header {
|
||||||
|
|||||||
Reference in New Issue
Block a user