Compare commits

...

8 Commits

Author SHA1 Message Date
sqj
b1c471f15c fix:修复歌单导入失败的问题,歌词翻译匹配逻辑 2025-10-14 21:44:39 +08:00
sqj
f7ecfa1fa9 fix:修复歌单导入失败的问题,歌词翻译匹配逻辑 2025-10-14 21:44:08 +08:00
sqj
d44be6022a fix:优化滚动位置问题,某平台 歌单上限导入失败问题,优化包体积,修复歌曲下载失败 2025-10-11 22:54:10 +08:00
sqj
0c512bccff fix: 修复歌曲无法下载,支持更多音质选择 2025-10-09 20:25:27 +08:00
sqj
b07cc2359a fix: 修复歌曲播放缓存内存泄露问题 feat:歌曲播放出错自动切歌不是暂停 2025-10-09 01:47:07 +08:00
时迁酱
46756a8b09 Merge pull request #12 from GladerJ/main
修复播放新歌曲失败时,原歌曲 UI 状态被错误覆盖的问题。
2025-10-08 16:51:53 +08:00
Glader
deb73fa789 修复播放新歌曲失败时,原歌曲 UI 状态被错误覆盖的问题。
修复播放新歌曲失败时,原歌曲 UI 状态被错误覆盖的问题。
2025-10-08 15:15:30 +08:00
sqj
910ab1ff10 docs: add 赞助名单 2025-10-07 22:24:40 +08:00
40 changed files with 2049 additions and 2017 deletions

View File

@@ -12,5 +12,7 @@
| RiseSun | 9.9 | | RiseSun | 9.9 |
| **b站小友**:光牙阿普斯木兰 | 5 | | **b站小友**:光牙阿普斯木兰 | 5 |
| 青禾 | 8.8 | | 青禾 | 8.8 |
| li peng | 200 |
| **群友**XIZ | 3 |
据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn 据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn

View File

@@ -1,6 +1,6 @@
{ {
"name": "ceru-music", "name": "ceru-music",
"version": "1.4.0", "version": "1.4.4",
"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",

View 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
View 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()
})
}

View File

@@ -0,0 +1,82 @@
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)
}

View File

@@ -1,12 +1,16 @@
import { app, shell, BrowserWindow, ipcMain, Tray, Menu, screen } from 'electron' import { app, shell, BrowserWindow, ipcMain, screen } 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()
@@ -24,71 +28,9 @@ if (!gotTheLock) {
} }
}) })
} }
// import wy from './utils/musicSdk/wy/index'
// import kg from './utils/musicSdk/kg/index'
// wy.hotSearch.getList().then((res) => {
// console.log(res)
// })
// kg.hotSearch.getList().then((res) => {
// console.log(res)
// })
let tray: Tray | null = null
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | 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()
}
}
})
}
function createWindow(): void { function createWindow(): void {
// return
// 获取保存的窗口位置和大小 // 获取保存的窗口位置和大小
const savedBounds = configManager.getWindowBounds() const savedBounds = configManager.getWindowBounds()
@@ -173,29 +115,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']) {
@@ -205,80 +129,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
@@ -295,16 +145,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
@@ -312,63 +152,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)
}
})
createWindow() createWindow()
createTray()
// 注册自动更新事件 // 注册自动更新事件
registerAutoUpdateEvents() registerAutoUpdateEvents()
@@ -398,67 +182,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 主动通知。
if (ping) {
clearInterval(ping)
}
ping = setInterval(() => { ping = setInterval(() => {
if (mainWindow) { // 保留占位,避免调用方报错;不再做任何轮询。
mainWindow.webContents // 可在此处监听自定义 IPC 事件以扩展行为。
.executeJavaScript( clearInterval(ping)
` }, 1000)
(function() {
const audio = document.getElementById("globaAudio");
if(!audio) return { playing:false, ended: false };
if(audio.ended) return { playing:false, ended: true };
return { playing: !audio.paused, ended: false, currentTime: audio.currentTime, duration: audio.duration };
})()
`
)
.then((res) => {
console.log(res)
if (res.duration - res.currentTime <= 20) {
clearInterval(ping)
interval = 500
ping = setInterval(() => {
if (mainWindow) {
mainWindow.webContents
.executeJavaScript(
`
(function() {
const audio = document.getElementById("globaAudio");
if(!audio) return { playing:false, ended: false };
if(audio.ended) return { playing:false, ended: true };
return { playing: !audio.paused, ended: false, currentTime: audio.currentTime, duration: audio.duration };
})()
`
)
.then((res) => {
console.log(res)
if (res && res.ended) {
mainWindow?.webContents.send('song-ended')
console.log('next song')
clearInterval(ping)
}
})
.catch((err) => console.warn(err))
}
}, interval)
}
})
.catch((err) => console.warn(err))
}
}, interval)
} }

View File

@@ -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 {

View File

@@ -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)
types.push({ type: '128k', size, hash: rawData.FileHash })
_types['128k'] = {
size,
hash: rawData.FileHash
}
}
if (rawData.HQFileSize !== 0) {
const size = sizeFormate(rawData.HQFileSize)
types.push({ type: '320k', size, hash: rawData.HQFileHash })
_types['320k'] = {
size,
hash: rawData.HQFileHash
}
}
if (rawData.SQFileSize !== 0) {
const size = sizeFormate(rawData.SQFileSize)
types.push({ type: 'flac', size, hash: rawData.SQFileHash })
_types.flac = {
size,
hash: rawData.SQFileHash
}
}
if (rawData.ResFileSize !== 0) {
const size = sizeFormate(rawData.ResFileSize)
types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash })
_types.flac24bit = {
size,
hash: rawData.ResFileHash
}
}
return {
singer: decodeName(formatSingerName(rawData.Singers, 'name')),
name: decodeName(rawData.SongName),
albumName: decodeName(rawData.AlbumName),
albumId: rawData.AlbumID,
songmid: rawData.Audioid,
source: 'kg',
interval: formatPlayTime(rawData.Duration),
_interval: rawData.Duration,
img: null,
lrc: null,
otherSource: null,
hash: rawData.FileHash,
types,
_types,
typeUrl: {}
}
},
handleResult(rawData) {
const ids = new Set()
const list = []
rawData.forEach((item) => { rawData.forEach((item) => {
const key = item.Audioid + item.FileHash const key = item.Audioid + item.FileHash
if (ids.has(key)) return if (!ids.has(key)) {
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) ids.add(key)
list.push(this.filterData(childItem)) items.push(item)
}
for (const childItem of item.Grp || []) {
const childKey = childItem.Audioid + childItem.FileHash
if (!ids.has(childKey)) {
ids.add(childKey)
items.push(childItem)
}
}
})
const hashList = items.map((item) => item.FileHash)
let qualityInfoMap = {}
try {
const qualityInfoRequest = getBatchMusicQualityInfo(hashList)
qualityInfoMap = await qualityInfoRequest.promise
} catch (error) {
console.error('Failed to fetch quality info:', error)
}
return items.map((item) => {
const { types = [], _types = {} } = qualityInfoMap[item.FileHash] || {}
return {
singer: decodeName(formatSingerName(item.Singers, 'name')),
name: decodeName(item.SongName),
albumName: decodeName(item.AlbumName),
albumId: item.AlbumID,
songmid: item.Audioid,
source: 'kg',
interval: formatPlayTime(item.Duration),
_interval: item.Duration,
img: null,
lrc: null,
otherSource: null,
hash: item.FileHash,
types,
_types,
typeUrl: {},
} }
}) })
return list
}, },
search(str, page = 1, limit, retryNum = 0) { 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',
}) })
}) })
} },
} }

View 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: {},
}
})
}

View File

@@ -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))}`,
{
method: 'POST',
headers: {
'User-Agent':
'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36',
Referer: 'https://m.kugou.com/'
}, },
body ],
} }
) const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, {
method: 'POST',
headers: {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36',
Referer: 'https://m.kugou.com/',
},
body,
})
return result.list[0].global_collection_id 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,17 +808,17 @@ 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
// getTags // getTags
// getListDetail // getListDetail

View File

@@ -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',
}) })
}) })
} },
} }

View File

@@ -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,17 +527,17 @@ 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
// getTags // getTags
// getListDetail // getListDetail

View File

@@ -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',
} }
}) })
} },
} }

View File

@@ -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,12 +342,12 @@ export default {
list, list,
limit, limit,
total: parseInt(body.songListResultData.totalCount), total: parseInt(body.songListResultData.totalCount),
source: 'mg' source: 'mg',
} }
}) })
} },
} }
// getList // getList
// getTags // getTags
// getListDetail // getListDetail

View File

@@ -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

View File

@@ -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',
}) })
}) })
} },
} }

View 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
}

View File

@@ -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&notice=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&notice=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,17 +321,17 @@ 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
// getTags // getTags
// getListDetail // getListDetail

View File

@@ -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,11 +39,11 @@ 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)
}) })
return decodeName(singer.join(join)) return decodeName(singer.join(join))
} }
return decodeName(String(singers ?? '')) return decodeName(String(singers ?? ''))
} }

View File

@@ -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) } },
} }
}

View File

@@ -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,101 +7,129 @@ 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) => {
const types = []
const _types = {}
let size
if (item.privilege.maxBrLevel == 'hires') { return Promise.all(
size = item.hr ? sizeFormate(item.hr.size) : null rawList.map(async (item) => {
types.push({ type: 'flac24bit', size }) const types = []
_types.flac24bit = { const _types = {}
size let size
try {
const requestObj = httpFetch(
`https://music.163.com/api/song/music/detail/get?songId=${item.id}`,
{
method: 'get',
headers: {
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
},
}
)
const { body, statusCode } = await requestObj.promise
if (statusCode !== 200 || !body || body.code !== 200) {
throw new Error('Failed to get song quality information')
}
if (body.data.jm && body.data.jm.size) {
size = sizeFormate(body.data.jm.size)
types.push({ type: 'master', size })
_types.master = { size }
}
if (body.data.db && body.data.db.size) {
size = sizeFormate(body.data.db.size)
types.push({ type: 'dolby', size })
_types.dolby = { size }
}
if (body.data.hr && body.data.hr.size) {
size = sizeFormate(body.data.hr.size)
types.push({ type: 'hires', size })
_types.hires = { size }
}
if (body.data.sq && body.data.sq.size) {
size = sizeFormate(body.data.sq.size)
types.push({ type: 'flac', size })
_types.flac = { size }
}
if (body.data.h && body.data.h.size) {
size = sizeFormate(body.data.h.size)
types.push({ type: '320k', size })
_types['320k'] = { size }
}
if (body.data.m && body.data.m.size) {
size = sizeFormate(body.data.m.size)
types.push({ type: '128k', size })
_types['128k'] = { size }
} else if (body.data.l && body.data.l.size) {
size = sizeFormate(body.data.l.size)
types.push({ type: '128k', size })
_types['128k'] = { size }
}
types.reverse()
return {
singer: this.getSinger(item.ar),
name: item.name,
albumName: item.al.name,
albumId: item.al.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al.picUrl,
lrc: null,
types,
_types,
typeUrl: {},
}
} catch (error) {
console.error(error.message)
return null
} }
} })
switch (item.privilege.maxbr) { )
case 999000:
size = item.sq ? sizeFormate(item.sq.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
types.reverse()
return {
singer: this.getSinger(item.ar),
name: item.name,
albumName: item.al.name,
albumId: item.al.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al.picUrl,
lrc: null,
types,
_types,
typeUrl: {}
}
})
}, },
search(str, page = 1, limit, retryNum = 0) { 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.page = page
this.allPage = Math.ceil(this.total / this.limit)
this.total = result.result.songCount || 0 return {
this.page = page list,
this.allPage = Math.ceil(this.total / this.limit) allPage: this.allPage,
limit: this.limit,
return { total: this.total,
list, source: 'wy',
allPage: this.allPage, }
limit: this.limit, })
total: this.total,
source: 'wy'
}
// return result.data
}) })
} },
} }

View 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
}

View File

@@ -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,36 +76,30 @@ 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) { try {
list = this.filterListDetail(body) list = (
} else { await musicDetailApi.getList(
try { body.playlist.trackIds.slice(rangeStart, limit * page).map((trackId) => trackId.id)
list = ( )
await musicDetailApi.getList( ).list
body.playlist.trackIds.slice(rangeStart, limit * page).map((trackId) => trackId.id) } catch (err) {
) console.log(err)
).list if (err.message == 'try max num') {
} catch (err) { throw err
console.log(err) } else {
if (err.message == 'try max num') { return this.getListDetail(id, page, ++tryNum)
throw err
} else {
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

View File

@@ -8,6 +8,11 @@ const api = {
console.log('preload: 发送 window-minimize 事件') console.log('preload: 发送 window-minimize 事件')
ipcRenderer.send('window-minimize') ipcRenderer.send('window-minimize')
}, },
// 阻止系统息屏
powerSaveBlocker: {
start: () => ipcRenderer.invoke('power-save-blocker:start'),
stop: () => ipcRenderer.invoke('power-save-blocker:stop')
},
maximize: () => { maximize: () => {
console.log('preload: 发送 window-maximize 事件') console.log('preload: 发送 window-maximize 事件')
ipcRenderer.send('window-maximize') ipcRenderer.send('window-maximize')

View File

@@ -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')
} }

View File

@@ -28,6 +28,8 @@ const emit = defineEmits<{
const canvasRef = ref<HTMLCanvasElement>() const canvasRef = ref<HTMLCanvasElement>()
const animationId = ref<number>() const animationId = ref<number>()
const analyser = ref<AnalyserNode>() const analyser = ref<AnalyserNode>()
// 节流渲染,目标 ~30fps
const lastFrameTime = ref(0)
const dataArray = ref<Uint8Array>() const dataArray = ref<Uint8Array>()
const resizeObserver = ref<ResizeObserver>() const resizeObserver = ref<ResizeObserver>()
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`) const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
@@ -75,93 +77,87 @@ const initAudioAnalyser = () => {
} }
// 绘制可视化 // 绘制可视化
const draw = () => { const draw = (ts?: number) => {
if (!canvasRef.value || !analyser.value || !dataArray.value) return if (!canvasRef.value || !analyser.value || !dataArray.value) return
// 帧率节流 ~30fps
const now = ts ?? performance.now()
if (now - lastFrameTime.value < 33) {
animationId.value = requestAnimationFrame(draw)
return
}
lastFrameTime.value = now
const canvas = canvasRef.value const canvas = canvasRef.value
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) return if (!ctx) {
animationId.value = requestAnimationFrame(draw)
return
}
// 获取频域数据或生成模拟数据 // 获取频域数据或生成模拟数据
if (analyser.value && dataArray.value) { if (analyser.value && dataArray.value) {
// 有真实音频分析器,获取真实数据
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>) analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
} else { } else {
// 没有音频分析器,生成模拟数据 const time = now * 0.001
const time = Date.now() * 0.001
for (let i = 0; i < dataArray.value.length; i++) { for (let i = 0; i < dataArray.value.length; i++) {
// 生成基于时间的模拟频谱数据
const frequency = i / dataArray.value.length const frequency = i / dataArray.value.length
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5 const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
const bass = Math.sin(time * 4) * 0.3 + 0.7 // 低频变化 const bass = Math.sin(time * 4) * 0.3 + 0.7
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7)) dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
} }
} }
// 计算低频音量 (80hz-120hz 范围) // 计算低频音量(前 3 个 bin
// 假设采样率为 44100HzfftSize 为 256则每个频率 bin 约为 172Hz
// 80-120Hz 大约对应前 1-2 个 bin
const lowFreqStart = 0
const lowFreqEnd = Math.min(3, dataArray.value.length) // 取前几个低频 bin
let lowFreqSum = 0 let lowFreqSum = 0
for (let i = lowFreqStart; i < lowFreqEnd; i++) { const lowBins = Math.min(3, dataArray.value.length)
lowFreqSum += dataArray.value[i] for (let i = 0; i < lowBins; i++) lowFreqSum += dataArray.value[i]
} emit('lowFreqUpdate', lowFreqSum / lowBins / 255)
const lowFreqVolume = lowFreqSum / (lowFreqEnd - lowFreqStart) / 255
// 发送低频音量给父组件 // 清屏
emit('lowFreqUpdate', lowFreqVolume)
// 完全清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
// 如果有背景色,再填充背景 // 背景
if (props.backgroundColor !== 'transparent') { if (props.backgroundColor !== 'transparent') {
ctx.fillStyle = props.backgroundColor ctx.fillStyle = props.backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillRect(0, 0, canvas.width, canvas.height)
} }
// 使用容器的实际尺寸进行计算,因为 ctx 已经缩放过了 // 计算尺寸
const container = canvas.parentElement const container = canvas.parentElement
if (!container) return if (!container) {
animationId.value = requestAnimationFrame(draw)
return
}
const containerRect = container.getBoundingClientRect() const containerRect = container.getBoundingClientRect()
const canvasWidth = containerRect.width const canvasWidth = containerRect.width
const canvasHeight = props.height const canvasHeight = props.height
// 计算对称柱状参数 // 柱状参数
const halfBarCount = Math.floor(props.barCount / 2) const halfBarCount = Math.floor(props.barCount / 2)
const barWidth = canvasWidth / 2 / halfBarCount const barWidth = canvasWidth / 2 / halfBarCount
const maxBarHeight = canvasHeight * 0.9 const maxBarHeight = canvasHeight * 0.9
const centerX = canvasWidth / 2 const centerX = canvasWidth / 2
// 绘制左右对称的频谱柱状图 // 每帧仅创建一次渐变(自底向上),减少对象分配
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0)
gradient.addColorStop(0, props.color)
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
ctx.fillStyle = gradient
// 绘制对称频谱
for (let i = 0; i < halfBarCount; i++) { for (let i = 0; i < halfBarCount; i++) {
// 增强低频响应,让可视化更敏感
let barHeight = (dataArray.value[i] / 255) * maxBarHeight let barHeight = (dataArray.value[i] / 255) * maxBarHeight
// 对数据进行增强处理,让变化更明显
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
const y = canvasHeight - barHeight const y = canvasHeight - barHeight
// 创建渐变色
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, y)
gradient.addColorStop(0, props.color)
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
ctx.fillStyle = gradient
// 绘制左侧柱状图(从中心向左)
const leftX = centerX - (i + 1) * barWidth const leftX = centerX - (i + 1) * barWidth
ctx.fillRect(leftX, y, barWidth, barHeight) ctx.fillRect(leftX, y, barWidth, barHeight)
// 绘制右侧柱状图(从中心向右)
const rightX = centerX + i * barWidth const rightX = centerX + i * barWidth
ctx.fillRect(rightX, y, barWidth, barHeight) ctx.fillRect(rightX, y, barWidth, barHeight)
} }
// 继续动画
if (props.show && Audio.value.isPlay) { if (props.show && Audio.value.isPlay) {
animationId.value = requestAnimationFrame(draw) animationId.value = requestAnimationFrame(draw)
} }
@@ -286,6 +282,10 @@ onBeforeUnmount(() => {
analyser.value.disconnect() analyser.value.disconnect()
analyser.value = undefined analyser.value = undefined
} }
// 通知管理器移除对该分析器的引用,防止 Map 持有导致 GC 不回收
try {
audioManager.removeAnalyser(componentId.value)
} catch {}
} catch (error) { } catch (error) {
console.warn('清理音频资源时出错:', error) console.warn('清理音频资源时出错:', error)
} }

View File

@@ -92,103 +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 parsedLyrics: LyricLine[] = [] let active = true
// 创建一个符合 MusicItem 接口的对象,只包含必要的基本属性 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 { try {
// 检查是否为网易云音乐只有网易云才使用ttml接口 const source =
const isNetease = props.songInfo && 'source' in props.songInfo ? (props.songInfo as any).source : 'kg'
props.songInfo && 'source' in props.songInfo && props.songInfo.source === 'wy' let parsedLyrics: LyricLine[] = []
const songinfo: any = _.cloneDeep(toRaw(props.songInfo))
console.log(songinfo) if (source === 'wy') {
if (isNetease) { // 网易云:优先尝试 TTML
// 网易云音乐优先尝试ttml接口
try { try {
const res = (await ( const res = await (
await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`) await fetch(`https://amll.bikonoo.com/ncm-lyrics/${newId}.ttml`, {
).text()) as any signal: abort.signal
})
).text()
if (!active) return
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
console.log('搜索到ttml歌词', parsedLyrics)
} 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
}) })
console.log('网易云歌词数据:', lyricData) if (!active) return
if (lyricData.crlyric) { if (lyricData?.crlyric) {
// 使用逐字歌词 parsedLyrics = parseYrc(lyricData.crlyric)
lyricText = lyricData.crlyric } else if (lyricData?.lyric) {
console.log('网易云逐字歌词', lyricText) parsedLyrics = parseLrc(lyricData.lyric)
parsedLyrics = parseYrc(lyricText)
console.log('使用网易云逐字歌词', parsedLyrics)
} else if (lyricData.lyric) {
lyricText = lyricData.lyric
parsedLyrics = parseLrc(lyricText)
console.log('使用网易云普通歌词', parsedLyrics)
} }
if (lyricData.tlyric) { parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
const translatedline = parseLrc(lyricData.tlyric)
console.log('网易云翻译歌词:', translatedline)
for (let i = 0; i < parsedLyrics.length; i++) {
if (translatedline[i] && translatedline[i].words[0]) {
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
}
}
}
} }
} 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()
}) })
console.log(`${source}歌词数据:`, lyricData) 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)
}
console.log(`使用${source}逐字歌词`, parsedLyrics)
} else if (lyricData.lyric) {
lyricText = lyricData.lyric
parsedLyrics = parseLrc(lyricText)
console.log(`使用${source}普通歌词`, parsedLyrics)
} }
if (lyricData.tlyric) { parsedLyrics = mergeTranslation(parsedLyrics, lyricData?.tlyric)
const translatedline = parseLrc(lyricData.tlyric)
console.log(`${source}翻译歌词:`, translatedline)
for (let i = 0; i < parsedLyrics.length; i++) {
if (translatedline[i] && translatedline[i].words[0]) {
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
}
}
}
}
if (parsedLyrics.length > 0) {
state.lyricLines = parsedLyrics
console.log('歌词加载成功', parsedLyrics.length)
} else {
state.lyricLines = []
console.log('未找到歌词或解析失败')
} }
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 = []
} }
}, },
@@ -197,6 +229,7 @@ watch(
const bgRef = ref<BackgroundRenderRef | undefined>(undefined) const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined) const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
// 订阅音频事件,保持数据同步 // 订阅音频事件,保持数据同步
const unsubscribeTimeUpdate = ref<(() => void) | undefined>(undefined) const unsubscribeTimeUpdate = ref<(() => void) | undefined>(undefined)
const unsubscribePlay = ref<(() => void) | undefined>(undefined) const unsubscribePlay = ref<(() => void) | undefined>(undefined)
@@ -214,24 +247,51 @@ const useBlackText = ref(false)
async function updateTextColor() { async function updateTextColor() {
try { try {
useBlackText.value = await shouldUseBlackText(actualCoverImage.value) useBlackText.value = await shouldUseBlackText(actualCoverImage.value)
console.log('使用黑色文本:', useBlackText.value)
} catch (error) { } catch (error) {
console.error('获取对比色失败:', error) console.error('获取对比色失败:', error)
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 })
// 在全屏播放显示时阻止系统息屏
const blockerActive = ref(false)
watch(
() => props.show,
async (visible) => {
try {
if (visible && !blockerActive.value) {
await (window as any)?.api?.powerSaveBlocker?.start?.()
blockerActive.value = true
} else if (!visible && blockerActive.value) {
await (window as any)?.api?.powerSaveBlocker?.stop?.()
blockerActive.value = false
}
} catch (e) {
console.error('powerSaveBlocker 切换失败:', e)
}
},
{ immediate: true }
)
// 组件挂载时初始化 // 组件挂载时初始化
onMounted(() => { onMounted(() => {
updateTextColor() updateTextColor()
console.log('组件挂载完成', bgRef.value, lyricPlayerRef.value)
}) })
// 组件卸载前清理订阅 // 组件卸载前清理订阅
onBeforeUnmount(() => { onBeforeUnmount(async () => {
// 组件卸载时确保恢复系统息屏
if (blockerActive.value) {
try {
await (window as any)?.api?.powerSaveBlocker?.stop?.()
} catch {}
blockerActive.value = false
}
// 取消订阅以防止内存泄漏 // 取消订阅以防止内存泄漏
if (unsubscribeTimeUpdate.value) { if (unsubscribeTimeUpdate.value) {
unsubscribeTimeUpdate.value() unsubscribeTimeUpdate.value()
@@ -239,6 +299,8 @@ onBeforeUnmount(() => {
if (unsubscribePlay.value) { if (unsubscribePlay.value) {
unsubscribePlay.value() unsubscribePlay.value()
} }
bgRef.value?.bgRender?.dispose()
lyricPlayerRef.value?.lyricPlayer?.dispose()
}) })
// 监听音频URL变化 // 监听音频URL变化
@@ -299,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"
/> />
<!-- 全屏按钮 --> <!-- 全屏按钮 -->
@@ -365,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>

View File

@@ -1,5 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, provide, ref, onActivated, onDeactivated } from 'vue' import {
onMounted,
onUnmounted,
provide,
ref,
onActivated,
onDeactivated,
watch,
nextTick
} from 'vue'
import { ControlAudioStore } from '@renderer/store/ControlAudio' import { ControlAudioStore } from '@renderer/store/ControlAudio'
const audioStore = ControlAudioStore() const audioStore = ControlAudioStore()
@@ -17,6 +26,26 @@ onMounted(() => {
// window.api.ping(handleEnded) // window.api.ping(handleEnded)
}) })
/**
* 监听 URL 变化,先重置旧音频再加载新音频,避免旧解码/缓冲滞留
*/
watch(
() => audioStore.Audio.url,
async (newUrl) => {
const a = audioMeta.value
if (!a) return
try {
a.pause()
} catch {}
a.removeAttribute('src')
a.load()
await nextTick()
// 模板绑定会把 src 更新为 newUrl这里再触发一次 load
if (newUrl) {
a.load()
}
}
)
// 组件被激活时(从缓存中恢复) // 组件被激活时(从缓存中恢复)
onActivated(() => { onActivated(() => {
console.log('音频组件被激活') console.log('音频组件被激活')
@@ -71,22 +100,29 @@ const handlePlay = (): void => {
audioStore.publish('play') audioStore.publish('play')
} }
let rafId: number | null = null
const startSetupInterval = (): void => { const startSetupInterval = (): void => {
if (rafId !== null) return
const onFrame = () => { const onFrame = () => {
if (audioMeta.value && !audioMeta.value.paused) { if (audioMeta.value && !audioMeta.value.paused) {
audioStore.publish('timeupdate') audioStore.publish('timeupdate')
audioStore.setCurrentTime((audioMeta.value && audioMeta.value.currentTime) || 0) audioStore.setCurrentTime((audioMeta.value && audioMeta.value.currentTime) || 0)
requestAnimationFrame(onFrame)
} }
rafId = requestAnimationFrame(onFrame)
} }
requestAnimationFrame(onFrame) rafId = requestAnimationFrame(onFrame)
} }
const handlePause = (): void => { const handlePause = (): void => {
audioStore.Audio.isPlay = false audioStore.Audio.isPlay = false
audioStore.publish('pause') audioStore.publish('pause')
// 停止单实例 rAF
if (rafId !== null) {
try {
cancelAnimationFrame(rafId)
} catch {}
rafId = null
}
} }
const handleError = (event: Event): void => { const handleError = (event: Event): void => {
@@ -112,8 +148,23 @@ const handleCanPlay = (): void => {
onUnmounted(() => { onUnmounted(() => {
// 组件卸载时清空所有订阅者 // 组件卸载时清空所有订阅者
window.api.pingService.stop() try {
window.api.pingService.stop()
} catch {}
// 停止 rAF
if (rafId !== null) {
try {
cancelAnimationFrame(rafId)
} catch {}
rafId = null
}
if (audioMeta.value) {
try {
audioMeta.value.pause()
} catch {}
audioMeta.value.removeAttribute('src')
audioMeta.value.load()
}
audioStore.clearAllSubscribers() audioStore.clearAllSubscribers()
}) })
</script> </script>

View File

@@ -136,38 +136,45 @@ const playSong = async (song: SongList) => {
// 设置加载状态 // 设置加载状态
isLoadingSong.value = true isLoadingSong.value = true
// 检查是否需要恢复播放位置历史播放 // 检查是否需要恢复播放位置(历史播放)
const isHistoryPlay = const isHistoryPlay =
song.songmid === userInfo.value.lastPlaySongId && song.songmid === userInfo.value.lastPlaySongId &&
userInfo.value.currentTime !== undefined && userInfo.value.currentTime !== undefined &&
userInfo.value.currentTime > 0 userInfo.value.currentTime > 0
if (isHistoryPlay && userInfo.value.currentTime !== undefined) { if (isHistoryPlay && userInfo.value.currentTime !== undefined) {
pendingRestorePosition = userInfo.value.currentTime pendingRestorePosition = userInfo.value.currentTime
pendingRestoreSongId = song.songmid pendingRestoreSongId = song.songmid
console.log(`准备恢复播放位置: ${pendingRestorePosition}`) console.log(`准备恢复播放位置: ${pendingRestorePosition}`)
// 清除历史位置避免重复恢复 // 清除历史位置,避免重复恢复
userInfo.value.currentTime = 0 userInfo.value.currentTime = 0
} else { } else {
pendingRestorePosition = 0 pendingRestorePosition = 0
pendingRestoreSongId = null pendingRestoreSongId = null
} }
// 更新当前播放歌曲ID // 立刻暂停当前播放 - 不等待渐变
if (Audio.value.isPlay && Audio.value.audio) {
Audio.value.isPlay = false
Audio.value.audio.pause()
// 恢复音量避免下次播放音量为0
Audio.value.audio.volume = Audio.value.volume / 100
}
// 立刻更新 UI 到新歌曲
songInfo.value.name = song.name
songInfo.value.singer = song.singer
songInfo.value.albumName = song.albumName
songInfo.value.img = song.img
userInfo.value.lastPlaySongId = song.songmid userInfo.value.lastPlaySongId = song.songmid
// 如果播放列表是打开的滚动到当前播放歌曲 // 如果播放列表是打开的,滚动到当前播放歌曲
if (showPlaylist.value) { if (showPlaylist.value) {
nextTick(() => { nextTick(() => {
playlistDrawerRef.value?.scrollToCurrentSong() playlistDrawerRef.value?.scrollToCurrentSong()
}) })
} }
// 更新歌曲信息并触发主题色更新
songInfo.value.name = song.name
songInfo.value.singer = song.singer
songInfo.value.albumName = song.albumName
songInfo.value.img = song.img
// 更新媒体会话元数据 // 更新媒体会话元数据
mediaSessionController.updateMetadata({ mediaSessionController.updateMetadata({
title: song.name, title: song.name,
@@ -176,68 +183,85 @@ const playSong = async (song: SongList) => {
artworkUrl: song.img || defaultCoverImg artworkUrl: song.img || defaultCoverImg
}) })
// 确保主题色更新 // 尝试获取 URL
let urlToPlay = '' let urlToPlay = ''
// 获取URL
// eslint-disable-next-line no-useless-catch
try { try {
urlToPlay = await getSongRealUrl(toRaw(song)) urlToPlay = await getSongRealUrl(toRaw(song))
} catch (error) { } catch (error: any) {
throw error console.error('获取歌曲 URL 失败,播放下一首原歌曲:', error)
isLoadingSong.value = false
tryAutoNext('获取歌曲 URL 失败')
return
} }
// 先停止当前播放 // 在切换前彻底重置旧音频,释放缓冲与解码器
if (Audio.value.isPlay) { if (Audio.value.audio) {
const stopResult = stop() const a = Audio.value.audio
if (stopResult && typeof stopResult.then === 'function') { try {
await stopResult a.pause()
} } catch {}
a.removeAttribute('src')
a.load()
} }
// 设置 URL(这会触发音频重新加载)
// 设置URL这会触发音频重新加载
setUrl(urlToPlay) setUrl(urlToPlay)
// 等待音频准备就绪 // 等待音频准备就绪
await waitForAudioReady() await waitForAudioReady()
await setColor() await setColor()
songInfo.value = {
...song
}
// // 短暂延迟确保音频状态稳定
// await new Promise((resolve) => setTimeout(resolve, 100))
// 开始播放 // 更新完整歌曲信息
try { songInfo.value = { ...song }
start()
} catch (error) { /**
console.error('启动播放失败:', error) * 提前关闭加载状态
// 如果是 AbortError尝试重新播放 * 这样UI不会卡在“加载中”用户能立刻看到播放键切换
if ((error as { name: string }).name === 'AbortError') { */
console.log('检测到 AbortError尝试重新播放...') isLoadingSong.value = false
await new Promise((resolve) => setTimeout(resolve, 200))
try { /**
const retryResult = start() * 异步开始播放不await以免阻塞UI
if (retryResult && typeof retryResult.then === 'function') { */
await retryResult start()
} .catch(async (error: any) => {
} catch (retryError) { console.error('启动播放失败:', error)
console.error('重试播放失败:', retryError) tryAutoNext('启动播放失败')
throw retryError })
} .then(() => {
} else { autoNextCount.value = 0
throw error })
}
/**
* 注册事件监听确保浏览器播放事件触发时同步关闭loading
* (多一道保险)
*/
if (Audio.value.audio) {
Audio.value.audio.addEventListener(
'playing',
() => {
isLoadingSong.value = false
},
{ once: true }
)
Audio.value.audio.addEventListener(
'error',
() => {
isLoadingSong.value = false
},
{ once: true }
)
} }
} catch (error: any) { } catch (error: any) {
console.error('播放歌曲失败:', error) console.error('播放歌曲失败(外层捕获):', error)
MessagePlugin.error('播放失败,原因:' + error.message) tryAutoNext('播放歌曲失败')
// MessagePlugin.error('播放失败,原因:' + error.message)
isLoadingSong.value = false
} finally { } finally {
// 无论成功还是失败,都清除加载状态 // 最后的保险,确保加载状态一定会被关闭
isLoadingSong.value = false isLoadingSong.value = false
} }
} }
provide('PlaySong', playSong) provide('PlaySong', playSong)
// 歌曲信息 // 歌曲信息
const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE) const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
@@ -246,6 +270,23 @@ const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
// 歌曲加载状态 // 歌曲加载状态
const isLoadingSong = ref(false) const isLoadingSong = ref(false)
// 自动下一首次数限制不超过当前列表的30%
const autoNextCount = ref(0)
const getAutoNextLimit = () => Math.max(1, Math.floor(list.value.length * 0.3))
const tryAutoNext = (reason: string) => {
const limit = getAutoNextLimit()
MessagePlugin.error(`自动跳过当前歌曲:原因:${reason}`)
if (autoNextCount.value >= limit && autoNextCount.value > 2) {
MessagePlugin.error(
`自动下一首失败超过当前列表30%限制(${autoNextCount.value}/${limit})。原因:${reason}`
)
return
}
autoNextCount.value++
playNext()
}
// 更新播放模式 // 更新播放模式
const updatePlayMode = () => { const updatePlayMode = () => {
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE] const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]

View File

@@ -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子元素能够正确计算高度 */

View File

@@ -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

View File

@@ -4,6 +4,8 @@ class AudioManager {
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>() private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>() private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
private analysers = new Map<string, AnalyserNode>() private analysers = new Map<string, AnalyserNode>()
// 为每个 audioElement 复用一个分流器,避免重复断开重连主链路
private splitters = new WeakMap<HTMLAudioElement, GainNode>()
static getInstance(): AudioManager { static getInstance(): AudioManager {
if (!AudioManager.instance) { if (!AudioManager.instance) {
@@ -60,16 +62,19 @@ class AudioManager {
analyser.fftSize = fftSize analyser.fftSize = fftSize
analyser.smoothingTimeConstant = 0.6 analyser.smoothingTimeConstant = 0.6
// 创建增益节点作为中介,避免直接断开主音频链 // 复用每个 audioElement 的分流器source -> splitter -> destination
const gainNode = context.createGain() let splitter = this.splitters.get(audioElement)
gainNode.gain.value = 1.0 if (!splitter) {
splitter = context.createGain()
splitter.gain.value = 1.0
// 仅第一次建立主链路,不要断开已有连接,避免累积
source.connect(splitter)
splitter.connect(context.destination)
this.splitters.set(audioElement, splitter)
}
// 连接source -> gainNode -> analyser // 将分析器挂到分流器上,不影响主链路
// -> destination (保持音频播放) splitter.connect(analyser)
source.disconnect() // 先断开所有连接
source.connect(gainNode)
gainNode.connect(context.destination) // 确保音频继续播放
gainNode.connect(analyser) // 连接到分析器
// 存储分析器引用 // 存储分析器引用
this.analysers.set(id, analyser) this.analysers.set(id, analyser)
@@ -104,6 +109,15 @@ class AudioManager {
context.close() context.close()
} }
// 断开并移除分流器
const splitter = this.splitters.get(audioElement)
if (splitter) {
try {
splitter.disconnect()
} catch {}
this.splitters.delete(audioElement)
}
this.audioSources.delete(audioElement) this.audioSources.delete(audioElement)
this.audioContexts.delete(audioElement) this.audioContexts.delete(audioElement)

View File

@@ -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<
) )
] ]
) )
) })
) )
] ]
), ),
@@ -186,68 +185,16 @@ 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 +202,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()

View File

@@ -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) >

View File

@@ -23,10 +23,3 @@
<PlayMusic /> <PlayMusic />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.animate__animated {
position: absolute;
width: 100%;
}
</style>

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -517,7 +517,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,38 +541,51 @@ const handleNetworkPlaylistImport = async (input: string) => {
} }
platformName = '网易云音乐' platformName = '网易云音乐'
} else if (importPlatformType.value === 'tx') { } else if (importPlatformType.value === 'tx') {
// QQ音乐歌单ID解析 - 支持多种链接格式 // QQ音乐歌单ID解析:优先通过 SDK 解析,失败再回退到正则
const qqPlaylistRegexes = [ let parsedId = ''
// 标准歌单链接 try {
/(?:y\.qq\.com\/n\/ryqq\/playlist\/|music\.qq\.com\/.*[?&]id=|playlist[?&]id=)(\d+)/i, const parsed: any = await window.api.music.requestSdk('parsePlaylistId', {
// 分享链接格式 source: 'tx',
/(?:i\.y\.qq\.com\/n2\/m\/share\/details\/taoge\.html.*[?&]id=)(\d+)/i, url: input
// 其他可能的分享格式 })
/(?:c\.y\.qq\.com\/base\/fcgi-bin\/u\?.*__=)(\d+)/i, console.log('QQ音乐歌单解析结果', parsed)
// 手机版链接 if (parsed) parsedId = parsed
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i, } catch (e) {}
// 通用ID提取 - 匹配 id= 或 &id= 参数
/[?&]id=(\d+)/i
]
let match: RegExpMatchArray | null = null if (parsedId) {
for (const regex of qqPlaylistRegexes) { playlistId = parsedId
match = input.match(regex) } else {
if (match && match[1]) { const qqPlaylistRegexes = [
playlistId = match[1] // 标准歌单链接(强烈推荐)
break /(?:y\.qq\.com\/n\/ryqq\/playlist\/|music\.qq\.com\/.*[?&]id=|playlist[?&]id=)(\d+)/i,
// 分享链接格式
/(?:i\.y\.qq\.com\/n2\/m\/share\/details\/taoge\.html.*[?&]id=)(\d+)/i,
// 其他可能的分享格式 https:\/\/c\d+\.y\.qq\.com\/base\/fcgi-bin\/u\?.*__=([A-Za-z0-9]+)/i,
// 手机版链接
/(?:i\.y\.qq\.com\/v8\/playsquare\/playlist\.html.*[?&]id=)(\d+)/i,
// 通用ID提取 - 匹配 id= 或 &id= 参数
/[?&]id=(\d+)/i
]
let match: RegExpMatchArray | null = null
for (const regex of qqPlaylistRegexes) {
match = input.match(regex)
if (match && match[1]) {
playlistId = match[1]
break
}
} }
}
if (!match || !match[1]) { if (!match || !match[1]) {
// 检查是否直接输入的是纯数字ID // 检查是否直接输入的是纯数字ID
const numericMatch = input.match(/^\d+$/) const numericMatch = input.match(/^\d+$/)
if (numericMatch) { if (numericMatch) {
playlistId = input playlistId = input
} else { } else {
MessagePlugin.error('无法识别的QQ音乐歌单链接或ID格式请检查链接是否正确') MessagePlugin.error('无法识别的QQ音乐歌单链接或ID格式请检查链接是否正确')
load1.then((res) => res.close()) load1.then((res) => res.close())
return return
}
} }
} }
platformName = 'QQ音乐' platformName = 'QQ音乐'
@@ -680,18 +693,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 +707,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 +735,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)) {
@@ -1441,7 +1449,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;

View File

@@ -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 {

View File

@@ -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 {