Compare commits

...

47 Commits

Author SHA1 Message Date
lyswhut
3fe34545b9 添加自定义源二进制数据传输支持 2023-05-13 11:53:56 +08:00
lyswhut
da4be33a04 添加上限设置 2023-05-12 20:13:35 +08:00
lyswhut
47e2a5a460 更新依赖 2023-05-08 18:07:26 +08:00
lyswhut
04337ef4b8 添加音效用户预设 2023-05-08 18:04:04 +08:00
lyswhut
df58c8eb80 添加强行构建环境变量参数 2023-05-06 16:36:33 +08:00
lyswhut
b0bf1bf4be 添加繁忙重试重试 2023-05-05 19:01:57 +08:00
lyswhut
1a3d84d4c4 更新版本号 2023-05-04 19:15:52 +08:00
lyswhut
735f629ca1 添加更多环境混响音效 2023-05-04 19:14:00 +08:00
lyswhut
9dc927b50b 修复名称显示 2023-05-04 19:13:48 +08:00
lyswhut
47a336eb64 修复 2023-05-03 18:16:53 +08:00
lyswhut
33b66aaea0 改名 2023-05-03 17:56:32 +08:00
lyswhut
7b9eefdf2d 新增音效设置 2023-05-03 17:45:43 +08:00
lyswhut
ea5e8b56dd 发布v2.2.2 2023-05-01 17:44:11 +08:00
lyswhut
e52fff4d0e 修复添加歌曲弹窗默认列表名字显示问题 2023-05-01 16:15:39 +08:00
lyswhut
8d108509c9 修改默认设置 2023-05-01 16:04:41 +08:00
lyswhut
d3cc630501 更新版本号 2023-05-01 15:32:59 +08:00
lyswhut
e74f297f9e 修复在低版本Linux amd64系统上无法启动的问题 2023-05-01 14:55:35 +08:00
lyswhut
8be9c75734 修改安装依赖的命令 2023-05-01 12:27:46 +08:00
lyswhut
851851a7e6 更新 2023-05-01 11:47:34 +08:00
lyswhut
1cbe46dc3b 发布v2.2.1 2023-05-01 11:13:24 +08:00
lyswhut
675a943d59 更新依赖 2023-04-29 15:19:57 +08:00
Folltoshe
908815def3 更新音源API (#1352)
* 更新API

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-29 15:09:17 +08:00
lyswhut
200ca1eeda 移除多余翻译 2023-04-29 14:16:38 +08:00
Folltoshe
b81cc139b3 优化UI (#1353)
* 优化UI

* 更新

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-29 13:10:56 +08:00
lyswhut
f8ae906326 支持wy热门评论翻页 2023-04-28 19:49:53 +08:00
lyswhut
e6f55804f4 更新依赖 2023-04-27 14:04:01 +08:00
lyswhut
1432a19d7d 更新版本号 2023-04-24 13:13:10 +08:00
lyswhut
74fbfb130c 修复歌单加载 2023-04-24 13:05:11 +08:00
lyswhut
3cf45f602f 修复对存在错误时间标签的歌词的解析 2023-04-22 18:32:49 +08:00
lyswhut
7a9dcc71fd 更新导航栏样式 2023-04-20 23:35:57 +08:00
lyswhut
fb5249a979 修复 2023-04-16 15:02:30 +08:00
lyswhut
3f694b75f8 修复列表加载问题 2023-04-16 14:52:22 +08:00
彭狸花喵
0e1ee66dd6 更新kg获取歌单gid接口 & 修复歌单介绍显示 (#1328) 2023-04-16 13:28:55 +08:00
lyswhut
920e002247 优化搜索框背景配色,使其适应高透明主题 2023-04-15 18:22:54 +08:00
Folltoshe
ab0b352d36 修复tx热门评论回复图片显示异常 (#1326) 2023-04-15 17:59:52 +08:00
lyswhut
aa4b08a156 更新依赖 2023-04-14 19:46:05 +08:00
lyswhut
66514908d2 更新版本号 2023-04-13 15:07:47 +08:00
lyswhut
60f0801a7c 修复评论图片显示问题 2023-04-13 15:07:33 +08:00
lyswhut
7149d0d43f 优化更新弹窗弹出时机 2023-04-13 14:34:18 +08:00
Folltoshe
d970687ebe 音源API更新 & 其他改动 (#1295)
## API

### kg源

- 增加歌单歌曲flac24bit显示
- 修复酷狗码只能导入500首的问题
- 更新排行榜

### mg源

- 修复搜索不显示时长的问题
- 修复评论加载失败的问题
- 更新排行榜

### tx源

- 增加热门评论图片显示
- 更新排行榜

### wy源

- 更新排行榜

## 其他

- 添加自定义源zlib模块的支持

---------

Co-authored-by: 彭狸花喵 <94218819+helloplhm-qwq@users.noreply.github.com>
Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-13 14:32:59 +08:00
lyswhut
d0425fc0d6 更新版本号 2023-04-09 15:36:52 +08:00
lyswhut
bdd5b46708 更新依赖 2023-04-09 15:35:00 +08:00
lyswhut
6d949d7e4d 更新依赖 2023-03-31 20:12:14 +08:00
lyswhut
5d72d092fc 优化&修复同步机制 2023-03-30 17:46:37 +08:00
lyswhut
667fc8c8a6 修复缺失的翻译 2023-03-30 17:45:58 +08:00
lyswhut
f7a2d9fd06 启用桌面歌词时,取消对歌词窗口的聚焦(#1273) 2023-03-29 12:49:30 +08:00
lyswhut
85946efd0b 修复启用全局快捷键时与Media Session注册冲突的问题 2023-03-27 19:57:31 +08:00
151 changed files with 5976 additions and 3389 deletions

View File

@@ -32,7 +32,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src
@@ -124,7 +124,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src
@@ -184,7 +184,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src

View File

@@ -32,7 +32,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src
@@ -81,7 +81,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src
@@ -130,7 +130,7 @@ jobs:
- name: Install Dependencies
run: |
npm install
npm ci
- name: Build src code
run: npm run build:src

View File

@@ -12,5 +12,6 @@
],
"i18n-ally.sortKeys": true,
"javascript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"vue.codeActions.enabled": false
}

View File

@@ -6,6 +6,38 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [2.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.1...v2.2.2) - 2023-05-01
### 修复
- 修复在低版本Linux amd64系统上无法启动的问题glibc版本要求过高导致的采用内置预编译二进制文件的方式解决
- 修复添加歌曲弹窗默认列表名字显示问题
## [2.2.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.0...v2.2.1) - 2023-05-01
### 优化
- 优化对系统Media Session的支持现在切歌不会再会导致信息丢失的问题了
- 启用桌面歌词时,取消对歌词窗口的聚焦
- 增加kg歌单歌曲flac24bit显示@helloplhm-qwq
- 增加tx源热门评论图片显示@Folltoshe
- 优化更新弹窗弹出时机
- 优化搜索框背景配色,使其适应高透明主题
- 支持wy热门评论翻页
### 修复
- 修复启用全局快捷键时与Media Session注册冲突的问题启用全局快捷键时不再注册媒体控制快捷键
- 修复mg搜索不显示时长的问题@Folltoshe
- 修复mg评论加载失败的问题@Folltoshe
- 修复对存在错误时间标签的歌词的解析
### 其他
- 自定义源API utils对象新增`zlib.inflate``zlib.deflate`方法API版本更新到 v1.3.0
- 更新kg、tx、wy等平台排行榜列表
- 更新 electron 到 v22.3.7
## [2.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.2...v2.2.0) - 2023-03-26
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明

3
FAQ.md
View File

@@ -2,6 +2,7 @@
本文档已迁移到:<https://lyswhut.github.io/lx-music-doc/desktop/faq>
<!--
在阅读本常见问题后仍然无法解决你的问题请提交issue或者加企鹅群`830125506`反馈(无事勿加,入群先看群公告),反馈时请**注明**已阅读常见问题!
## ~~软件为什么没有桌面歌词与自定义列表功能~~
@@ -562,4 +563,4 @@ const cancelHttp = window.lx.request(url, options, callback)
- `window.lx.utils.crypto.randomBytes`:生成随机字符串 `randomBytes(size)`
- `window.lx.utils.crypto.rsaEncrypt`RSA加密 `rsaEncrypt(buffer, key)`
目前仅提供以上工具方法如果需要其他方法可以开issue讨论。
目前仅提供以上工具方法如果需要其他方法可以开issue讨论。 -->

View File

@@ -4,6 +4,7 @@ const path = require('path')
const { Arch } = require('electron-builder')
const better_sqlite3_fileNameMap = {
[Arch.x64]: 'electron-v110-linux-x64',
[Arch.arm64]: 'electron-v110-linux-arm64',
[Arch.armv7l]: 'electron-v110-linux-arm',
}
@@ -51,10 +52,11 @@ const replaceQrcDecodeLib = async(platform, arch) => {
module.exports = async(context) => {
const { electronPlatformName, arch } = context
await replaceQrcDecodeLib(electronPlatformName, arch)
if (electronPlatformName !== 'linux') return
if (electronPlatformName !== 'linux' || process.env.FORCE) return
const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
switch (arch) {
case Arch.x64:
case Arch.arm64:
case Arch.armv7l:
if (fs.existsSync(bindingFilePath)) {

View File

@@ -104,7 +104,7 @@ module.exports = {
],
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
type: 'asset',
parser: {
dataUrlCondition: {

4300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "2.2.0",
"version": "2.3.0-beta.3",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"productName": "lx-music-desktop",
@@ -205,77 +205,77 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@babel/core": "^7.21.8",
"@babel/eslint-parser": "^7.21.8",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@types/better-sqlite3": "^7.6.3",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@types/better-sqlite3": "^7.6.4",
"@types/needle": "^3.2.0",
"@types/tunnel": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@volar/vue-language-plugin-pug": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@volar/vue-language-plugin-pug": "^1.6.4",
"babel-loader": "^9.1.2",
"browserslist": "^4.21.5",
"chalk": "^4.1.2",
"changelog-parser": "^3.0.1",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.29.1",
"core-js": "^3.30.2",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"css-minimizer-webpack-plugin": "^5.0.0",
"del": "^6.1.1",
"electron": "^22.3.4",
"electron-builder": "^24.1.1",
"electron": "^22.3.8",
"electron-builder": "^24.3.0",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.340",
"electron-updater": "^6.0.0",
"eslint": "^8.36.0",
"electron-to-chromium": "^1.4.385",
"electron-updater": "^6.1.0",
"eslint": "^8.40.0",
"eslint-config-standard": "^17.0.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.10.0",
"eslint-webpack-plugin": "^4.0.0",
"html-webpack-plugin": "^5.5.0",
"eslint-plugin-vue": "^9.11.1",
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.5",
"node-loader": "^2.0.0",
"postcss": "^8.4.21",
"postcss-loader": "^7.1.0",
"postcss": "^8.4.23",
"postcss-loader": "^7.3.0",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"rimraf": "^4.4.1",
"rimraf": "^5.0.0",
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^4.0.0",
"terser": "^5.16.8",
"terser-webpack-plugin": "^5.3.7",
"terser": "^5.17.1",
"terser-webpack-plugin": "^5.3.8",
"ts-loader": "^9.4.2",
"typescript": "^5.0.2",
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^17.0.1",
"typescript": "^5.0.4",
"vue-eslint-parser": "^9.2.1",
"vue-loader": "^17.1.0",
"vue-template-compiler": "^2.7.14",
"webpack": "^5.76.3",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.1",
"webpack": "^5.82.0",
"webpack-cli": "^5.1.0",
"webpack-dev-server": "^4.15.0",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@simonwep/pickr": "^1.8.2",
"better-sqlite3": "^8.2.0",
"better-sqlite3": "^8.3.0",
"bufferutil": "^4.0.7",
"comlink": "~4.3.1",
"crypto-js": "^4.1.1",
@@ -285,9 +285,8 @@
"iconv-lite": "^0.6.3",
"image-size": "^1.0.2",
"jschardet": "^3.0.0",
"koa": "^2.14.1",
"long": "^5.2.1",
"music-metadata": "^8.1.3",
"long": "^5.2.3",
"music-metadata": "^8.1.4",
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
"node-id3": "^0.2.6",
"sortablejs": "^1.15.0",

View File

@@ -1,32 +1,7 @@
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明
### 不兼容性变更说明
- 同步功能从这个版本起数据同步功能至少需要移动端v1.0.0的版本才能连接,连接的地址格式也略有改变,详情看[文档说明](https://lyswhut.github.io/lx-music-doc/desktop/faq/sync)
### 新增
- 重构数据同步功能,新增客户端模式
- 新增全屏时自动关闭歌词设置,默认开启,可以去设置-桌面歌词设置更改
- 新增设置-桌面歌词设置-重置窗口设置功能,点击时会重置桌面歌词窗口大小及位置
- 新增设置-其他-列表数据清理功能,点击时会清空已创建的所有列表及所有收藏的歌曲
### 优化
- 支持wy源flac hires歌曲类型的显示
- 快捷键调整音量时每次加减2%音量改为4%#1220
- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容,保留点击切换显示/隐藏
- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示#1231, #1238 By @helloplhm-qwq, @Folltoshe
- 播放速率的粒度调整为0.01范围0.6-2.0x
### 修复
- 修复同步连接的处理问题
- 修复记住播放进度的情况下使用Scheme URL打开应用播放的歌曲进度没有被重置的问题
- 修复使用酷狗码无法打开某些类型的歌单的问题
- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题
- 修复连续选择时的初始选择歌曲位置被意外改变的问题
- 新增音效设置实验性功能支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
### 其他
- 更新 Electron 到v22.3.4
- 更新 electron 到 v22.3.8

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@ export const STORE_NAMES = {
LRC_RAW: 'lyrics',
LRC_EDITED: 'lyrics_edited',
THEME: 'theme',
SOUND_EFFECT: 'sound_effect',
} as const
export const APP_EVENT_NAMES = {

View File

@@ -29,21 +29,21 @@ const local: LX.HotKeyConfig = {
const global: LX.HotKeyConfig = {
enable: false,
keys: {
MediaPlayPause: {
type: HOTKEY_PLAYER.toggle_play.type,
name: '',
action: HOTKEY_PLAYER.toggle_play.action,
},
MediaPreviousTrack: {
type: HOTKEY_PLAYER.prev.type,
name: '',
action: HOTKEY_PLAYER.prev.action,
},
MediaNextTrack: {
type: HOTKEY_PLAYER.next.type,
name: '',
action: HOTKEY_PLAYER.next.action,
},
// MediaPlayPause: {
// type: HOTKEY_PLAYER.toggle_play.type,
// name: '',
// action: HOTKEY_PLAYER.toggle_play.action,
// },
// MediaPreviousTrack: {
// type: HOTKEY_PLAYER.prev.type,
// name: '',
// action: HOTKEY_PLAYER.prev.action,
// },
// MediaNextTrack: {
// type: HOTKEY_PLAYER.next.type,
// name: '',
// action: HOTKEY_PLAYER.next.action,
// },
'mod+alt+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,

View File

@@ -2,6 +2,7 @@ import { join } from 'path'
import { homedir } from 'os'
const isMac = process.platform == 'darwin'
const isWin = process.platform == 'win32'
const defaultSetting: LX.AppSetting = {
version: '2.1.0',
@@ -33,12 +34,28 @@ const defaultSetting: LX.AppSetting = {
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isS2t': false,
'player.isPlayLxlrc': !isMac,
'player.isPlayLxlrc': isWin,
'player.isSavePlayTime': false,
'player.audioVisualization': false,
'player.waitPlayEndStop': true,
'player.waitPlayEndStopTime': '',
'player.autoSkipOnError': true,
'player.soundEffect.convolution.fileName': '',
'player.soundEffect.convolution.mainGain': 10,
'player.soundEffect.convolution.sendGain': 0,
'player.soundEffect.biquadFilter.hz31': 0,
'player.soundEffect.biquadFilter.hz62': 0,
'player.soundEffect.biquadFilter.hz125': 0,
'player.soundEffect.biquadFilter.hz250': 0,
'player.soundEffect.biquadFilter.hz500': 0,
'player.soundEffect.biquadFilter.hz1000': 0,
'player.soundEffect.biquadFilter.hz2000': 0,
'player.soundEffect.biquadFilter.hz4000': 0,
'player.soundEffect.biquadFilter.hz8000': 0,
'player.soundEffect.biquadFilter.hz16000': 0,
'player.soundEffect.panner.enable': false,
'player.soundEffect.panner.soundR': 5,
'player.soundEffect.panner.speed': 25,
'playDetail.isZoomActiveLrc': false,
'playDetail.isShowLyricProgressSetting': false,
@@ -71,10 +88,10 @@ const defaultSetting: LX.AppSetting = {
// 'desktopLyric.style.fontWeight': false,
'desktopLyric.style.opacity': 95,
'desktopLyric.style.ellipsis': false,
'desktopLyric.style.isZoomActiveLrc': true,
'desktopLyric.style.isZoomActiveLrc': false,
'desktopLyric.style.isFontWeightFont': true,
'desktopLyric.style.isFontWeightLine': false,
'desktopLyric.style.isFontWeightExtended': false,
'desktopLyric.style.isFontWeightLine': true,
'desktopLyric.style.isFontWeightExtended': true,
'list.isClickPlayList': false,
'list.isShowSource': true,

View File

@@ -90,6 +90,10 @@ const modules = {
get_other_source_count: 'get_other_source_count',
get_data: 'get_data',
save_data: 'save_data',
get_sound_effect_eq_preset: 'get_sound_effect_eq_preset',
save_sound_effect_eq_preset: 'save_sound_effect_eq_preset',
get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset',
save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',

View File

@@ -163,6 +163,86 @@ declare global {
*/
'player.waitPlayEndStopTime': string
/**
* 环境音效文件名
*/
'player.soundEffect.convolution.fileName': string | null
/**
* 环境音效原始输出增益
*/
'player.soundEffect.convolution.mainGain': number
/**
* 环境音效输出增益
*/
'player.soundEffect.convolution.sendGain': number
/**
* 均衡器 31hz 值
*/
'player.soundEffect.biquadFilter.hz31': number
/**
* 均衡器 62hz 值
*/
'player.soundEffect.biquadFilter.hz62': number
/**
* 均衡器 125hz 值
*/
'player.soundEffect.biquadFilter.hz125': number
/**
* 均衡器 250hz 值
*/
'player.soundEffect.biquadFilter.hz250': number
/**
* 均衡器 500hz 值
*/
'player.soundEffect.biquadFilter.hz500': number
/**
* 均衡器 1000hz 值
*/
'player.soundEffect.biquadFilter.hz1000': number
/**
* 均衡器 2000hz 值
*/
'player.soundEffect.biquadFilter.hz2000': number
/**
* 均衡器 4000hz 值
*/
'player.soundEffect.biquadFilter.hz4000': number
/**
* 均衡器 8000hz 值
*/
'player.soundEffect.biquadFilter.hz8000': number
/**
* 均衡器 16000hz 值
*/
'player.soundEffect.biquadFilter.hz16000': number
/**
* 3D立体环绕是否启用
*/
'player.soundEffect.panner.enable': boolean
/**
* 3D立体环绕声音距离
*/
'player.soundEffect.panner.soundR': number
/**
* 3D立体环绕速度
*/
'player.soundEffect.panner.speed': number
/**
* 是否启用音频加载失败时自动切歌
*/

View File

@@ -12,13 +12,15 @@ declare namespace LX {
interface MyDefaultListInfo {
id: 'default'
name: '试听列表'
name: 'list__name_default'
// name: '试听列表'
// list: LX.Music.MusicInfo[]
}
interface MyLoveListInfo {
id: 'love'
name: '我的收藏'
name: 'list__name_love'
// name: '我的收藏'
// list: LX.Music.MusicInfo[]
}

25
src/common/types/sound_effect.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
declare namespace LX {
namespace SoundEffect {
interface EQPreset {
id: string
name: string
hz31: number
hz62: number
hz125: number
hz250: number
hz500: number
hz1000: number
hz2000: number
hz4000: number
hz8000: number
hz16000: number
}
interface ConvolutionPreset {
id: string
name: string
source: string
mainGain: number
sendGain: number
}
}
}

View File

@@ -1,9 +1,7 @@
const { getNow, TimeoutTools } = require('./utils')
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
const timeExp = /[\d:.]+/g
const timeLabelRxp = /^(\[[\d:]+\.)0+(\d+\])/
const timeLabelFixRxp = /(?:\.0+|0+)$/
const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
@@ -14,6 +12,15 @@ const tagRegMap = {
const timeoutTools = new TimeoutTools()
const t_rxp_1 = /^0+(\d+)/
const t_rxp_2 = /:0+(\d+)/g
const t_rxp_3 = /\.0+(\d+)/
const formatTimeLabel = (label) => {
return label.replace(t_rxp_1, '$1')
.replace(t_rxp_2, ':$1')
.replace(t_rxp_3, '.$1')
}
const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
for (let i = 0; i < extendedLines.length; i++) {
@@ -26,9 +33,7 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
const times = timeField.match(timeExp)
if (times == null) continue
for (let time of times) {
if (time.includes('.')) time = time.replace(timeLabelRxp, '$1$2')
else time += '.0'
const timeStr = time.replace(timeLabelFixRxp, '')
const timeStr = formatTimeLabel(time)
const targetLine = lrcLinesMap[timeStr]
if (targetLine) targetLine.extendedLyrics.push(text)
}
@@ -88,19 +93,16 @@ module.exports = class LinePlayer {
const times = timeField.match(timeExp)
if (times == null) continue
for (let time of times) {
if (time.includes('.')) time = time.replace(timeLabelRxp, '$1$2')
else time += '.0'
const timeStr = time.replace(timeLabelFixRxp, '')
const timeStr = formatTimeLabel(time)
if (linesMap[timeStr]) {
linesMap[timeStr].extendedLyrics.push(text)
continue
}
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
} else if (!timeArr[2]) timeArr[2] = '0'
if (timeArr.length > 3) continue
else if (timeArr.length < 3) for (let i = 3 - timeArr.length; i--;) timeArr.unshift('0')
if (timeArr[2].indexOf('.') > -1) timeArr.splice(2, 1, ...timeArr[2].split('.'))
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,

View File

@@ -2,7 +2,7 @@
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
const meta: Record<string, any> = {
songId: oldMusicInfo.songmid, // 歌曲IDmg源为copyrightIdlocal为文件路径
songId: oldMusicInfo.songmid, // 歌曲IDlocal为文件路径
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
picUrl: oldMusicInfo.img, // 歌曲图片链接
}

View File

@@ -12,6 +12,7 @@
"comment__hot_load_error": "Hot comments failed to load, click to try to reload",
"comment__hot_loading": "Hot comments are loading",
"comment__hot_title": "Hot Comment",
"comment__location": "From{location}",
"comment__new_load_error": "The latest comment failed to load, click to try to reload",
"comment__new_loading": "Latest comments are loading",
"comment__new_title": "Latest comment",
@@ -92,6 +93,8 @@
"list__move_to": "Move to ...",
"list__movedown": "Movedown",
"list__moveup": "Move up",
"list__name_default": "Default",
"list__name_love": "Love",
"list__new_list_btn": "New List",
"list__new_list_input": "New list...",
"list__pause": "Pause Task",
@@ -205,6 +208,7 @@
"player__end": "Stopped",
"player__error": "Error loading music. Switch to next song after 5 seconds",
"player__geting_url": "Getting music link...",
"player__geting_url_delay_retry": "The service is busy, try again in {time} seconds...",
"player__hide_detail_tip": "Hide detail page (Right-click in the view to quickly hide the details page)",
"player__loading": "Music loading...",
"player__music_album": "Album: ",
@@ -224,6 +228,41 @@
"player__playing": "Now playing...",
"player__prev": "Prev",
"player__refresh_url": "Music URL expired, refreshing...",
"player__sound_effect": "Sound settings (experimental)",
"player__sound_effect_biquad_filter": "Equalizer",
"player__sound_effect_biquad_filter_preset_classical": "Classical",
"player__sound_effect_biquad_filter_preset_dance": "Dance",
"player__sound_effect_biquad_filter_preset_electronic": "Electronic",
"player__sound_effect_biquad_filter_preset_pop": "Pop",
"player__sound_effect_biquad_filter_preset_rock": "Rock",
"player__sound_effect_biquad_filter_preset_slow": "Slow",
"player__sound_effect_biquad_filter_preset_soft": "Soft",
"player__sound_effect_biquad_filter_preset_subwoofer": "Subwoofer",
"player__sound_effect_biquad_filter_preset_vocal": "Vocal",
"player__sound_effect_biquad_filter_reset_btn": "Reset",
"player__sound_effect_biquad_filter_save_btn": "Save preset as",
"player__sound_effect_biquad_filter_save_input": "New presets...",
"player__sound_effect_convolution": "Ambient reverb sound effect",
"player__sound_effect_convolution_file_bright_hall": "Hall",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "Rock",
"player__sound_effect_convolution_file_cinema_diningroom": "Cinema",
"player__sound_effect_convolution_file_dining_living_true_stereo": "Dining Room",
"player__sound_effect_convolution_file_feedback_spring": "Feedback Spring",
"player__sound_effect_convolution_file_living_bedroom_leveled": "Bathroom",
"player__sound_effect_convolution_file_matrix_1": "Matrix",
"player__sound_effect_convolution_file_matrix_2": "Matrix 2",
"player__sound_effect_convolution_file_s2_r4_bd": "Church",
"player__sound_effect_convolution_file_s3_r1_bd": "Stereo",
"player__sound_effect_convolution_file_spreader25_125ms": "Indoor 2",
"player__sound_effect_convolution_file_spreader50_65ms": "Indoor",
"player__sound_effect_convolution_file_telephone": "Telephone",
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "Rock 2",
"player__sound_effect_convolution_main_gain": "Original Audio Gain",
"player__sound_effect_convolution_send_gain": "Ambient Sound Effect Gain",
"player__sound_effect_panner": "3D stereo surround (need to use headphones)",
"player__sound_effect_panner_enabled": "enable",
"player__sound_effect_panner_sound_r": "Sound distance",
"player__sound_effect_panner_sound_speed": "Surround speed",
"player__stop": "Paused",
"player__volume": "Volume: ",
"player__volume_mute_label": "Mute",
@@ -310,10 +349,12 @@
"setting__desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"setting__desktop_lyric_always_on_top_loop": "Automatically refresh the top of the lyrics (try to enable this setting when the lyrics are still blocked by some programs)",
"setting__desktop_lyric_audio_visualization": "Audio Visualization (Experimental)",
"setting__desktop_lyric_color": "Lyric font color",
"setting__desktop_lyric_color_reset": "Reset color",
"setting__desktop_lyric_delay_scroll": "Delayed lyrics scroll",
"setting__desktop_lyric_direction_horizontal": "horizontal direction",
"setting__desktop_lyric_direction_vertical": "vertical direction",
"setting__desktop_lyric_direction": "Lyrics Display Direction",
"setting__desktop_lyric_direction_horizontal": "Horizontal direction",
"setting__desktop_lyric_direction_vertical": "Vertical direction",
"setting__desktop_lyric_ellipsis": "Lyrics are not allowed to wrap",
"setting__desktop_lyric_enable": "Display lyrics",
"setting__desktop_lyric_font": "Lyric font",
@@ -329,7 +370,7 @@
"setting__desktop_lyric_played_color": "color played",
"setting__desktop_lyric_reset": "Reset",
"setting__desktop_lyric_reset_window": "Reset window settings",
"setting__desktop_lyric_scroll_align": "now playing lyrics scroll position",
"setting__desktop_lyric_scroll_align": "Now playing lyrics scroll position",
"setting__desktop_lyric_scroll_align_center": "Center",
"setting__desktop_lyric_scroll_align_top": "Top",
"setting__desktop_lyric_shadow_color": "Shadow color",
@@ -339,6 +380,7 @@
"setting__download_data_embed": "Whether to embed the following content in the audio file",
"setting__download_embed_lyric": "Embedding lyric",
"setting__download_embed_pic": "Embedding cover",
"setting__download_embed_rlyric": "Also embed Roman accent lyrics (if available)",
"setting__download_embed_tlyric": "Also embed translated lyrics (if available)",
"setting__download_enable": "Whether to enable download function",
"setting__download_lyric": "Lyrics download",
@@ -444,7 +486,7 @@
"setting__play_mediaDevice": "Audio output",
"setting__play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"setting__play_mediaDevice_title": "Select a media device for audio output",
"setting__play_media_device_error_tip": "This function conflicts with the audio visualization function. You have enabled audio visualization when you started the software this time. This setting is temporarily unavailable. Please restart the software and then modify this setting.",
"setting__play_media_device_error_tip": "This function conflicts with advanced audio functions (audio visualization, sound effect settings). These functions have been enabled when you start the software this time. This setting is not available for now. Please close these functions and restart the software before modifying this setting.",
"setting__play_media_device_tip": "This feature conflicts with Audio Visualization, both cannot be enabled at the same time, would you like to turn Audio Visualization off and apply the selected audio output settings?",
"setting__play_quality": "Priority playback of 320K quality songs (if available)",
"setting__play_save_play_time": "Remember playback progress",

View File

@@ -72,7 +72,7 @@ const createI18n = (): I18n => {
return message
},
getMessage(key: keyof Message, val?: TranslateValues): string {
let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? ''
let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? key
return val ? this.fillMessage(targetMessage, val) : targetMessage
},
t(key: keyof Message, val?: TranslateValues): string {

View File

@@ -12,6 +12,7 @@
"comment__hot_load_error": "热门评论加载失败,点击尝试重新加载",
"comment__hot_loading": "热门评论加载中",
"comment__hot_title": "热门评论",
"comment__location": "来自{location}",
"comment__new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment__new_loading": "最新评论加载中",
"comment__new_title": "最新评论",
@@ -92,6 +93,8 @@
"list__move_to": "移动到...",
"list__movedown": "下移",
"list__moveup": "上移",
"list__name_default": "试听列表",
"list__name_love": "我的收藏",
"list__new_list_btn": "新建列表",
"list__new_list_input": "新列表...",
"list__pause": "暂停任务",
@@ -205,6 +208,7 @@
"player__end": "播放完毕",
"player__error": "音频加载出错5 秒后切换下一首",
"player__geting_url": "歌曲链接获取中...",
"player__geting_url_delay_retry": "服务繁忙,{time}秒后重试...",
"player__hide_detail_tip": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"player__loading": "音乐加载中...",
"player__music_album": "专辑名:",
@@ -224,6 +228,40 @@
"player__playing": "播放中...",
"player__prev": "上一首",
"player__refresh_url": "URL过期正在刷新URL...",
"player__sound_effect": "音效设置(实验性)",
"player__sound_effect_biquad_filter": "均衡器",
"player__sound_effect_biquad_filter_preset_classical": "古典",
"player__sound_effect_biquad_filter_preset_dance": "舞曲",
"player__sound_effect_biquad_filter_preset_electronic": "电子乐",
"player__sound_effect_biquad_filter_preset_pop": "流行",
"player__sound_effect_biquad_filter_preset_rock": "摇滚",
"player__sound_effect_biquad_filter_preset_slow": "慢歌",
"player__sound_effect_biquad_filter_preset_soft": "柔和",
"player__sound_effect_biquad_filter_preset_subwoofer": "重低音",
"player__sound_effect_biquad_filter_preset_vocal": "人声",
"player__sound_effect_biquad_filter_reset_btn": "重置",
"player__sound_effect_biquad_filter_save_btn": "另存预设",
"player__sound_effect_biquad_filter_save_input": "新预设...",
"player__sound_effect_convolution": "环境混响音效",
"player__sound_effect_convolution_file_bright_hall": "大厅",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "心形扩散",
"player__sound_effect_convolution_file_cinema_diningroom": "电影院",
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐厅",
"player__sound_effect_convolution_file_feedback_spring": "反馈弹簧",
"player__sound_effect_convolution_file_living_bedroom_leveled": "卫生间",
"player__sound_effect_convolution_file_matrix_1": "矩阵混响",
"player__sound_effect_convolution_file_matrix_2": "矩阵混响2",
"player__sound_effect_convolution_file_s2_r4_bd": "教堂",
"player__sound_effect_convolution_file_s3_r1_bd": "立体声",
"player__sound_effect_convolution_file_spreader50_65ms": "室内",
"player__sound_effect_convolution_file_telephone": "电话",
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "磁性立体声",
"player__sound_effect_convolution_main_gain": "原始音频增益",
"player__sound_effect_convolution_send_gain": "环境音效增益",
"player__sound_effect_panner": "3D立体环绕需使用耳机",
"player__sound_effect_panner_enabled": "启用",
"player__sound_effect_panner_sound_r": "声音距离",
"player__sound_effect_panner_sound_speed": "环绕速度",
"player__stop": "暂停播放",
"player__volume": "当前音量:",
"player__volume_mute_label": "静音",
@@ -447,7 +485,7 @@
"setting__play_mediaDevice": "音频输出",
"setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"setting__play_mediaDevice_title": "选择声音输出的媒体设备",
"setting__play_media_device_error_tip": "此功能与音频可视化功能冲突,你本次启动软件时已启用过音频可视化,此设置暂不可用,请 重启 软件后,再来修改此设置。",
"setting__play_media_device_error_tip": "此功能与高级音频功能(音频可视化、音效设置)冲突,你本次启动软件时已启用这些功能,此设置暂不可用,请 关闭这些功能 并 重启 软件后,再来修改此设置。",
"setting__play_media_device_tip": "此功能与音频可视化功能冲突,两者无法同时启用,是否将音频可视化关闭 并 应用所选音频输出设置?",
"setting__play_quality": "优先播放320K品质的歌曲如果可用",
"setting__play_save_play_time": "记住播放进度",

View File

@@ -12,6 +12,7 @@
"comment__hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
"comment__hot_loading": "熱門評論加載中",
"comment__hot_title": "熱門評論",
"comment__location": "來自{location}",
"comment__new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment__new_loading": "最新評論加載中",
"comment__new_title": "最新評論",
@@ -92,6 +93,8 @@
"list__move_to": "移動到...",
"list__movedown": "下移",
"list__moveup": "上移",
"list__name_default": "試聽清單",
"list__name_love": "我的收藏",
"list__new_list_btn": "新建列表",
"list__new_list_input": "新列表...",
"list__pause": "暫停任務",
@@ -206,6 +209,7 @@
"player__end": "播放完畢",
"player__error": "音頻加載出錯5 秒後切換下一首",
"player__geting_url": "歌曲鏈接獲取中...",
"player__geting_url_delay_retry": "服務繁忙,{time}秒後重試...",
"player__hide_detail_tip": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"player__loading": "音樂加載中...",
"player__music_name": "歌曲名:",
@@ -224,6 +228,41 @@
"player__playing": "播放中...",
"player__prev": "上一首",
"player__refresh_url": "URL過期正在刷新URL...",
"player__sound_effect": "音效設置(實驗性)",
"player__sound_effect_biquad_filter": "均衡器",
"player__sound_effect_biquad_filter_preset_classical": "古典",
"player__sound_effect_biquad_filter_preset_dance": "舞曲",
"player__sound_effect_biquad_filter_preset_electronic": "電子樂",
"player__sound_effect_biquad_filter_preset_pop": "流行",
"player__sound_effect_biquad_filter_preset_rock": "搖滾",
"player__sound_effect_biquad_filter_preset_slow": "慢歌",
"player__sound_effect_biquad_filter_preset_soft": "柔和",
"player__sound_effect_biquad_filter_preset_subwoofer": "重低音",
"player__sound_effect_biquad_filter_preset_vocal": "人聲",
"player__sound_effect_biquad_filter_reset_btn": "重置",
"player__sound_effect_biquad_filter_save_btn": "另存預設",
"player__sound_effect_biquad_filter_save_input": "新預設...",
"player__sound_effect_convolution": "環境混響音效",
"player__sound_effect_convolution_file_bright_hall": "大廳",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "搖滾",
"player__sound_effect_convolution_file_cinema_diningroom": "電影院",
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐廳",
"player__sound_effect_convolution_file_feedback_spring": "反饋彈簧",
"player__sound_effect_convolution_file_living_bedroom_leveled": "衛生間",
"player__sound_effect_convolution_file_matrix_1": "矩陣",
"player__sound_effect_convolution_file_matrix_2": "矩陣2",
"player__sound_effect_convolution_file_s2_r4_bd": "教堂",
"player__sound_effect_convolution_file_s3_r1_bd": "立體聲",
"player__sound_effect_convolution_file_spreader25_125ms": "室內2",
"player__sound_effect_convolution_file_spreader50_65ms": "室內",
"player__sound_effect_convolution_file_telephone": "電話",
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "搖滾2",
"player__sound_effect_convolution_main_gain": "原始音頻增益",
"player__sound_effect_convolution_send_gain": "環境音效增益",
"player__sound_effect_panner": "3D立體環繞需使用耳機",
"player__sound_effect_panner_enabled": "啟用",
"player__sound_effect_panner_sound_r": "聲音距離",
"player__sound_effect_panner_sound_speed": "環繞速度",
"player__stop": "暫停播放",
"player__volume": "當前音量:",
"player__volume_mute_label": "靜音",
@@ -310,6 +349,7 @@
"setting__desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"setting__desktop_lyric_always_on_top_loop": "自動刷新歌詞置頂(當歌詞置頂後仍被某些程序遮擋時可嘗試啟用此設置)",
"setting__desktop_lyric_audio_visualization": "音頻可視化(實驗性)",
"setting__desktop_lyric_color": "歌詞字體顏色",
"setting__desktop_lyric_color_reset": "重置顏色",
"setting__desktop_lyric_delay_scroll": "延遲歌詞滾動",
"setting__desktop_lyric_direction": "歌詞顯示方向",
@@ -340,6 +380,7 @@
"setting__download_data_embed": "是否將以下內容嵌入到音頻文件中",
"setting__download_embed_lyric": "歌詞嵌入",
"setting__download_embed_pic": "封面嵌入",
"setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)",
"setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)",
"setting__download_enable": "是否啟用下載功能",
"setting__download_lyric": "歌詞下載",
@@ -445,7 +486,7 @@
"setting__play_mediaDevice": "音頻輸出",
"setting__play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"setting__play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"setting__play_media_device_error_tip": "此功能與音頻可視化功能衝突,你本次啟動軟件時已啟用過音頻可視化,此設置暫不可用,請 重啟 軟件後,再來修改此設置。",
"setting__play_media_device_error_tip": "此功能與高級音頻功能(音頻可視化、音效設置)衝突,你本次啟動軟件時已啟用這些功能,此設置暫不可用,請 關閉這些功能 並 重啟 軟件後,再來修改此設置。",
"setting__play_media_device_tip": "此功能與音頻可視化功能衝突,兩者無法同時啟用,是否將音頻可視化關閉 並 應用所選音頻輸出設置?",
"setting__play_quality": "優先播放320K品質的歌曲如果可用",
"setting__play_save_play_time": "記住播放進度",

View File

@@ -7,94 +7,114 @@ import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils'
const requestIps = new Map<string, number>()
const getAvailableIP = (req: http.IncomingMessage) => {
let ip = getIP(req)
return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null
}
const verifyByKey = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId)
if (!keyInfo) return null
let text
try {
text = aesDecrypt(encryptMsg, keyInfo.key)
} catch (err) {
return null
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
saveClientKeyInfo(keyInfo)
}
return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
}
return null
}
const verifyByCode = (encryptMsg: string, password: string) => {
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(req.headers.m, authCode, key)
let text
try {
text = aesDecrypt(encryptMsg, key)
} catch (err) {
return null
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
const data = text.split('\n')
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
const deviceName = data[2] || 'Unknown'
const isMobile = data[3] == 'lx_music_mobile'
const keyInfo = createClientKeyInfo(deviceName, isMobile)
return rsaEncrypt(Buffer.from(JSON.stringify({
clientId: keyInfo.clientId,
key: keyInfo.key,
serverName: getComputerName(),
})), publicKey)
}
return null
}
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {
let code = 401
let msg: string = SYNC_CODE.msgAuthFailed
let ip = getIP(req)
// console.log(req.headers)
if (typeof req.headers.m == 'string') {
if (ip && (requestIps.get(ip) ?? 0) < 10) {
if (req.headers.m) {
label:
if (req.headers.i) { // key验证
if (typeof req.headers.i != 'string') break label
const keyInfo = getClientKeyInfo(req.headers.i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(req.headers.m, keyInfo.key)
} catch (err) {
break label
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
code = 200
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
saveClientKeyInfo(keyInfo)
}
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
}
} else { // 连接码验证
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(req.headers.m, authCode, key)
let text
try {
text = aesDecrypt(req.headers.m, key)
} catch (err) {
break label
}
// console.log(text)
if (text.startsWith(SYNC_CODE.authMsg)) {
code = 200
const data = text.split('\n')
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
const deviceName = data[2] || 'Unknown'
const isMobile = data[3] == 'lx_music_mobile'
const keyInfo = createClientKeyInfo(deviceName, isMobile)
msg = rsaEncrypt(Buffer.from(JSON.stringify({
clientId: keyInfo.clientId,
key: keyInfo.key,
serverName: getComputerName(),
})), publicKey)
}
}
let ip = getAvailableIP(req)
if (ip) {
if (typeof req.headers.m == 'string' && req.headers.m) {
const userId = req.headers.i
const _msg = typeof userId == 'string' && userId
? verifyByKey(req.headers.m, userId)
: verifyByCode(req.headers.m, password)
if (_msg != null) {
msg = _msg
code = 200
}
} else {
code = 403
msg = SYNC_CODE.msgBlockedIp
}
if (code != 200) {
const num = requestIps.get(ip) ?? 0
// if (num > 20) return
requestIps.set(ip, num + 1)
}
} else {
code = 403
msg = SYNC_CODE.msgBlockedIp
}
res.writeHead(code)
res.end(msg)
if (ip && code != 200) {
const num = requestIps.get(ip) ?? 0
if (num > 20) return
requestIps.set(ip, num + 1)
}
}
const verifyConnection = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId)
if (!keyInfo) return false
let text
try {
text = aesDecrypt(encryptMsg, keyInfo.key)
} catch (err) {
return false
}
// console.log(text)
return text == SYNC_CODE.msgConnect
}
export const authConnect = async(req: http.IncomingMessage) => {
const query = querystring.parse((req.url as string).split('?')[1])
const i = query.i
const t = query.t
label:
if (typeof i == 'string' && typeof t == 'string') {
const keyInfo = getClientKeyInfo(i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(t, keyInfo.key)
} catch (err) {
break label
}
// console.log(text)
if (text == SYNC_CODE.msgConnect) return
let ip = getAvailableIP(req)
if (ip) {
const query = querystring.parse((req.url as string).split('?')[1])
const i = query.i
const t = query.t
if (typeof i == 'string' && typeof t == 'string' && verifyConnection(t, i)) return
const num = requestIps.get(ip) ?? 0
requestIps.set(ip, num + 1)
}
throw new Error('failed')
}

View File

@@ -40,16 +40,18 @@ const codeTools: {
},
}
const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => {
for (const client of [...wss!.clients]) {
if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue
console.log('duplicate client', client.keyInfo.deviceName)
client.isReady = false
client.close(SYNC_CLOSE_CODE.normal)
}
}
const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {
const queryData = url.parse(request.url as string, true).query as Record<string, string>
socket.onClose(() => {
// console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
sendServerStatus(status)
})
// // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
const keyInfo = getClientKeyInfo(queryData.i)
if (!keyInfo) {
@@ -60,6 +62,8 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
saveClientKeyInfo(keyInfo)
// // socket.lx_keyInfo = keyInfo
socket.keyInfo = keyInfo
checkDuplicateClient(socket)
try {
await syncList(wss as LX.Sync.Server.SocketServer, socket)
} catch (err) {
@@ -68,6 +72,11 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
return
}
status.devices.push(keyInfo)
socket.onClose(() => {
// console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
sendServerStatus(status)
})
// handleConnection(io, socket)
sendServerStatus(status)
@@ -216,15 +225,15 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis
})
const interval = setInterval(() => {
wss?.clients.forEach(ws => {
if (ws.isAlive == false) {
ws.terminate()
wss?.clients.forEach(socket => {
if (socket.isAlive == false) {
socket.terminate()
return
}
ws.isAlive = false
ws.ping(noop)
if (ws.keyInfo.isMobile) ws.send('ping', noop)
socket.isAlive = false
socket.ping(noop)
if (socket.keyInfo.isMobile) socket.send('ping', noop)
})
}, 30000)

View File

@@ -1,5 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron')
const needle = require('needle')
const zlib = require('zlib')
const { createCipheriv, publicEncrypt, constants, randomBytes, createHash } = require('crypto')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
@@ -139,11 +140,15 @@ const handleShowUpdateAlert = (data, resolve, reject) => {
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
request(url, { method = 'get', timeout, headers, body, form, formData, bodyBinaryBase64 }, callback) {
let options = { headers }
let data
if (body) {
data = body
} else if (bodyBinaryBase64) {
try {
data = Buffer.from(bodyBinaryBase64, 'base64')
} catch {}
} else if (form) {
data = form
// data.content_type = 'application/x-www-form-urlencoded'
@@ -233,8 +238,26 @@ contextBridge.exposeInMainWorld('lx', {
return Buffer.from(buf, 'binary').toString(format)
},
},
zlib: {
inflate(buf) {
return new Promise((resolve, reject) => {
zlib.inflate(buf, (err, data) => {
if (err) reject(new Error(err.message))
else resolve(data)
})
})
},
deflate(data) {
return new Promise((resolve, reject) => {
zlib.deflate(data, (err, buf) => {
if (err) reject(new Error(err.message))
else resolve(buf)
})
})
},
},
},
version: '1.2.0',
version: '1.4.0',
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers

View File

@@ -81,6 +81,7 @@ const winEvent = () => {
// browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')
// }
if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop()
browserWindow!.blur()
})
}

View File

@@ -9,6 +9,7 @@ import sync from './sync'
import data from './data'
import music from './music'
import download from './download'
import soundEffect from './soundEffect'
import { sendEvent } from '../main'
export * from './app'
@@ -33,6 +34,7 @@ export default () => {
data()
music()
download()
soundEffect()
global.lx.event_app.on('updated_config', (keys, setting) => {
sendConfigChange(setting)

View File

@@ -0,0 +1,20 @@
import { STORE_NAMES } from '@common/constants'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
import { mainOn, mainHandle } from '@common/mainIpc'
import getStore from '@main/utils/store'
export default () => {
mainHandle<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset, async() => {
return getStore(STORE_NAMES.SOUND_EFFECT).get('eqPreset') as LX.SoundEffect.EQPreset[] | null ?? []
})
mainOn<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, ({ params }) => {
getStore(STORE_NAMES.SOUND_EFFECT).set('eqPreset', params)
})
mainHandle<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset, async() => {
return getStore(STORE_NAMES.SOUND_EFFECT).get('convolutionPreset') as LX.SoundEffect.ConvolutionPreset[] | null ?? []
})
mainOn<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => {
getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params)
})
}

View File

@@ -10,3 +10,4 @@ import '@common/types/player'
import '@common/types/desktop_lyric'
import '@common/types/theme'
import '@common/types/ipc_main'
import '@common/types/sound_effect'

View File

@@ -154,7 +154,15 @@ export const initHotKey = async() => {
let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null
let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null
if (!localConfig) {
if (globalConfig) {
// 移除v2.2.0及之前设置的全局媒体快捷键注册
if (globalConfig.keys.MediaPlayPause) {
delete globalConfig.keys.MediaPlayPause
delete globalConfig.keys.MediaNextTrack
delete globalConfig.keys.MediaPreviousTrack
electronStore_hotKey.set('global', globalConfig)
}
} else {
// migrate hotKey
const config = await migrateHotKey()
if (config) {

View File

@@ -29,7 +29,7 @@ export default (db: Database.Database) => {
// PRAGMA user_version = x
// console.log(db.prepare('PRAGMA user_version').get().user_version)
// https://github.com/WiseLibs/better-sqlite3/issues/668#issuecomment-1145285728
const version = db.prepare('SELECT "field_value" FROM "main"."db_info" WHERE "field_name" = ?').get('version').field_value
const version = (db.prepare<[string]>('SELECT "field_value" FROM "main"."db_info" WHERE "field_name" = ?').get('version') as { field_value: string }).field_value
switch (version) {
case '1':
migrateV1(db)

View File

@@ -11,9 +11,9 @@ import {
/**
* 查询下载歌曲列表
*/
export const queryDownloadList = (): LX.DBService.DownloadMusicInfo[] => {
export const queryDownloadList = () => {
const queryStatement = createQueryStatement()
return queryStatement.all()
return queryStatement.all() as LX.DBService.DownloadMusicInfo[]
}
/**

View File

@@ -6,7 +6,7 @@ import { getDB } from '../../db'
*/
export const createQueryStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
SELECT "id", "isComplate", "status", "statusText", "progress_downloaded", "progress_total", "url", "quality", "ext", "fileName", "filePath", "musicInfo", "position"
FROM download_list
ORDER BY "position" ASC
@@ -30,7 +30,7 @@ export const createInsertStatement = () => {
*/
export const createClearStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
DELETE FROM "main"."download_list"
`)
}

View File

@@ -24,8 +24,8 @@ import {
* 获取用户列表
* @returns
*/
export const queryAllUserList = (): LX.DBService.UserListInfo[] => {
return createListQueryStatement().all()
export const queryAllUserList = () => {
return createListQueryStatement().all() as LX.DBService.UserListInfo[]
}
/**
@@ -154,9 +154,9 @@ export const updateMusicInfos = (list: LX.DBService.MusicInfo[]) => {
* @param listId 列表Id
* @returns 列表歌曲
*/
export const queryMusicInfoByListId = (listId: string): LX.DBService.MusicInfo[] => {
export const queryMusicInfoByListId = (listId: string) => {
const musicInfoQueryStatement = createMusicInfoQueryStatement()
return musicInfoQueryStatement.all({ listId })
return musicInfoQueryStatement.all({ listId }) as LX.DBService.MusicInfo[]
}
/**
@@ -268,9 +268,9 @@ export const removeMusicInfoByListId = (ids: string[]) => {
* @param musicInfoId 音乐id
* @returns
*/
export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string): LX.DBService.MusicInfo | null => {
export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string) => {
const musicInfoByListAndMusicInfoIdQueryStatement = createMusicInfoByListAndMusicInfoIdQueryStatement()
return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId })
return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId }) as LX.DBService.MusicInfo | null
}
/**
@@ -278,9 +278,9 @@ export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId
* @param id 音乐id
* @returns
*/
export const queryMusicInfoByMusicInfoId = (id: string): LX.DBService.MusicInfo[] => {
export const queryMusicInfoByMusicInfoId = (id: string) => {
const musicInfoByMusicInfoIdQueryStatement = createMusicInfoByMusicInfoIdQueryStatement()
return musicInfoByMusicInfoIdQueryStatement.all(id)
return musicInfoByMusicInfoIdQueryStatement.all(id) as LX.DBService.MusicInfo[]
}
/**

View File

@@ -7,7 +7,7 @@ import { getDB } from '../../db'
*/
export const createListQueryStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
SELECT "id", "name", "source", "sourceListId", "position", "locationUpdateTime"
FROM "main"."my_list"
`)
@@ -30,7 +30,7 @@ export const createListInsertStatement = () => {
*/
export const createListClearStatement = () => {
const db = getDB()
return db.prepare('DELETE FROM "main"."my_list"')
return db.prepare<[]>('DELETE FROM "main"."my_list"')
}
/**
@@ -100,7 +100,7 @@ export const createMusicInfoUpdateStatement = () => {
*/
export const createMusicInfoClearStatement = () => {
const db = getDB()
return db.prepare('DELETE FROM "main"."my_list_music_info"')
return db.prepare<[]>('DELETE FROM "main"."my_list_music_info"')
}
/**
@@ -162,7 +162,7 @@ export const createMusicInfoOrderInsertStatement = () => {
*/
export const createMusicInfoOrderClearStatement = () => {
const db = getDB()
return db.prepare('DELETE FROM "main"."my_list_music_info_order"')
return db.prepare<[]>('DELETE FROM "main"."my_list_music_info_order"')
}
/**

View File

@@ -20,9 +20,9 @@ import {
* @param id 歌曲id
* @returns 歌词信息
*/
export const queryLyric = (id: string): LX.DBService.Lyricnfo[] => {
export const queryLyric = (id: string) => {
const lyricQueryStatement = createLyricQueryStatement()
return lyricQueryStatement.all(id)
return lyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
}
/**
@@ -30,9 +30,9 @@ export const queryLyric = (id: string): LX.DBService.Lyricnfo[] => {
* @param id 歌曲id
* @returns 歌词信息
*/
export const queryRawLyric = (id: string): LX.DBService.Lyricnfo[] => {
export const queryRawLyric = (id: string) => {
const rawLyricQueryStatement = createRawLyricQueryStatement()
return rawLyricQueryStatement.all(id)
return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
}
/**
@@ -84,7 +84,7 @@ export const clearRawLyric = () => {
*/
export const countRawLyric = () => {
const countStatement = createRawLyricCountStatement()
return countStatement.get().count
return (countStatement.get() as { count: number }).count
}
@@ -93,9 +93,9 @@ export const countRawLyric = () => {
* @param id 歌曲id
* @returns 歌词信息
*/
export const queryEditedLyric = (id: string): LX.DBService.Lyricnfo[] => {
export const queryEditedLyric = (id: string) => {
const rawLyricQueryStatement = createEditedLyricQueryStatement()
return rawLyricQueryStatement.all(id)
return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
}
/**
@@ -148,5 +148,5 @@ export const clearEditedLyric = () => {
*/
export const countEditedLyric = () => {
const countStatement = createEditedLyricCountStatement()
return countStatement.get().count
return (countStatement.get() as { count: number }).count
}

View File

@@ -46,7 +46,7 @@ export const createRawLyricInsertStatement = () => {
*/
export const createRawLyricClearStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
DELETE FROM "main"."lyric"
WHERE "source"='${RAW_LYRIC}'
`)
@@ -83,7 +83,7 @@ export const createRawLyricUpdateStatement = () => {
*/
export const createRawLyricCountStatement = () => {
const db = getDB()
return db.prepare(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${RAW_LYRIC}'`)
return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${RAW_LYRIC}'`)
}
@@ -117,7 +117,7 @@ export const createEditedLyricInsertStatement = () => {
*/
export const createEditedLyricClearStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
DELETE FROM "main"."lyric"
WHERE "source"='${EDITED_LYRIC}'
`)
@@ -153,5 +153,5 @@ export const createEditedLyricUpdateStatement = () => {
*/
export const createEditedLyricCountStatement = () => {
const db = getDB()
return db.prepare(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${EDITED_LYRIC}'`)
return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${EDITED_LYRIC}'`)
}

View File

@@ -13,9 +13,9 @@ import {
* @param id 歌曲id
* @returns 歌曲信息
*/
export const queryMusicInfo = (id: string): LX.DBService.MusicInfoOtherSource[] => {
export const queryMusicInfo = (id: string) => {
const musicInfoQueryStatement = createMusicInfoQueryStatement()
return musicInfoQueryStatement.all(id)
return musicInfoQueryStatement.all(id) as LX.DBService.MusicInfoOtherSource[]
}
/**
@@ -55,5 +55,5 @@ export const clearMusicInfo = () => {
*/
export const countMusicInfo = () => {
const countStatement = createCountStatement()
return countStatement.get().count
return (countStatement.get() as { count: number }).count
}

View File

@@ -33,7 +33,7 @@ export const createMusicInfoInsertStatement = () => {
*/
export const createMusicInfoClearStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
DELETE FROM "main"."music_info_other_source"
`)
}
@@ -56,5 +56,5 @@ export const createMusicInfoDeleteStatement = () => {
*/
export const createCountStatement = () => {
const db = getDB()
return db.prepare('SELECT COUNT(*) as count FROM "main"."music_info_other_source"')
return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_info_other_source"')
}

View File

@@ -13,9 +13,9 @@ import {
* @param id 歌曲id
* @returns url
*/
export const queryMusicUrl = (id: string): string | null => {
export const queryMusicUrl = (id: string) => {
const queryStatement = createQueryStatement()
return queryStatement.get(id)?.url
return (queryStatement.get(id) as { url: string } | null)?.url ?? null
}
/**
@@ -71,5 +71,5 @@ export const clearMusicUrl = () => {
*/
export const countMusicUrl = () => {
const countStatement = createCountStatement()
return countStatement.get().count
return (countStatement.get() as { count: number }).count
}

View File

@@ -30,7 +30,7 @@ export const createInsertStatement = () => {
*/
export const createClearStatement = () => {
const db = getDB()
return db.prepare(`
return db.prepare<[]>(`
DELETE FROM "main"."music_url"
`)
}
@@ -65,5 +65,5 @@ export const createUpdateStatement = () => {
*/
export const createCountStatement = () => {
const db = getDB()
return db.prepare('SELECT COUNT(*) as count FROM "main"."music_url"')
return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_url"')
}

View File

@@ -3,7 +3,7 @@ import tables from './tables'
const rxp = /\n|\s|;|--.+/g
export default (db: Database.Database) => {
const result = db.prepare('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all()
const result = db.prepare<[]>('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all() as Array<{ type: string, name: string, tbl_name: string, sql: string }>
const dbTableMap = new Map<string, string>()
for (const info of result) dbTableMap.set(info.name, info.sql.replace(rxp, ''))
return Array.from(tables.entries()).every(([name, sql]) => {

View File

@@ -1,11 +1,11 @@
<html lang="cn">
<!DOCTYPE html>
<html lang="en" style="background-color: transparent;">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>桌面歌词-洛雪音乐助手</title>
</head>
<body>
<body id="body" style="background-color: transparent;">
<div id="root"></div>
<script>
window.dom_style_theme = document.createElement('style')

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M8 13C6.14 13 4.59 14.28 4.14 16H2V18H4.14C4.59 19.72 6.14 21 8 21S11.41 19.72 11.86 18H22V16H11.86C11.41 14.28 9.86 13 8 13M8 19C6.9 19 6 18.1 6 17C6 15.9 6.9 15 8 15S10 15.9 10 17C10 18.1 9.1 19 8 19M19.86 6C19.41 4.28 17.86 3 16 3S12.59 4.28 12.14 6H2V8H12.14C12.59 9.72 14.14 11 16 11S19.41 9.72 19.86 8H22V6H19.86M16 9C14.9 9 14 8.1 14 7C14 5.9 14.9 5 16 5S18 5.9 18 7C18 8.1 17.1 9 16 9Z" />
</svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@@ -56,6 +56,7 @@ const popupStyle = reactive({
const arrowHeight = 9
const arrowWidth = 8
const sidePadding = 50
watch(() => props.visible, (visible) => {
if (!visible || !dom_content.value || !props.btnEl) return
@@ -63,24 +64,24 @@ watch(() => props.visible, (visible) => {
const maxHeight = document.body.clientHeight
const elTop = rect.top - window.lx.rootOffset
const bottomTopVal = elTop + rect.height
const contentHeight = dom_content.value.scrollHeight + arrowHeight + 10
const contentHeight = dom_content.value.scrollHeight + arrowHeight + sidePadding
if (bottomTopVal + contentHeight < maxHeight || (contentHeight > elTop && elTop <= maxHeight - bottomTopVal)) {
isShowTop.value = false
popupStyle.top = bottomTopVal + arrowHeight + 'px'
popupStyle.maxHeight = maxHeight - bottomTopVal - arrowHeight - 10 + 'px'
popupStyle.maxHeight = maxHeight - bottomTopVal - arrowHeight - sidePadding + 'px'
} else {
isShowTop.value = true
let maxContentHeight = elTop - arrowHeight - 10
popupStyle.top = (elTop - (elTop < contentHeight ? elTop : contentHeight) + 10) + 'px'
let maxContentHeight = elTop - arrowHeight - sidePadding
popupStyle.top = (elTop - (elTop < contentHeight ? elTop : contentHeight) + sidePadding) + 'px'
popupStyle.maxHeight = maxContentHeight + 'px'
}
const maxWidth = document.body.clientWidth - 20
let center = dom_content.value.clientWidth / 2
let left = rect.left + rect.width / 2 - window.lx.rootOffset - center
if (left < 10) {
center -= 10 - left
left = 10
if (left < sidePadding) {
center -= sidePadding - left
left = sidePadding
} else if (left + dom_content.value.clientWidth > maxWidth) {
let newLeft = maxWidth - dom_content.value.clientWidth
center = center + left - newLeft

View File

@@ -22,6 +22,7 @@ import { watch, ref, onBeforeUnmount } from '@common/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
import { addListMusics, moveListMusics, createUserList, getMusicExistListIds } from '@renderer/store/list/action'
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
import { useI18n } from '@/lang'
export default {
props: {
@@ -63,6 +64,7 @@ export default {
emits: ['update:show'],
setup(props) {
const keyModDown = useKeyDown('mod')
const t = useI18n()
const lists = ref([])
const currentMusicInfo = ref({})
@@ -81,8 +83,8 @@ export default {
const getList = () => {
lists.value = [
defaultList,
loveList,
{ ...defaultList, name: t(defaultList.name) },
{ ...loveList, name: t(loveList.name) },
...userLists,
].filter(l => !props.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: false }))
checkMusicExist(currentMusicInfo.value)

View File

@@ -21,6 +21,7 @@ import { computed } from '@common/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
import { addListMusics, moveListMusics, createUserList } from '@renderer/store/list/action'
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
import { useI18n } from '@/lang'
export default {
props: {
@@ -64,11 +65,12 @@ export default {
emits: ['update:show', 'confirm'],
setup(props) {
const keyModDown = useKeyDown('mod')
const t = useI18n()
const lists = computed(() => {
return [
defaultList,
loveList,
{ ...defaultList, name: t(defaultList.name) },
{ ...loveList, name: t(loveList.name) },
...userLists,
].filter(l => !props.excludeListId.includes(l.id))
})

View File

@@ -0,0 +1,92 @@
<template>
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
<svg-icon name="plus" />
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
</base-btn>
</template>
<script setup>
import { ref, nextTick } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
import { saveUserConvolutionPreset } from '@renderer/store/soundEffect'
const isEditing = ref(false)
const input = ref(false)
const newPresetName = ref('')
const handleEditing = () => {
if (isEditing.value) return
// if (!this.newPresetName) this.newPresetName = this.listName
isEditing.value = true
nextTick(() => {
input.value.$el.focus()
})
}
const handleSave = (event) => {
let name = event.target.value.trim()
newPresetName.value = event.target.value = ''
isEditing.value = false
if (!name) return
if (name.length > 20) name = name.substring(0, 20)
saveUserConvolutionPreset({
id: Date.now().toString(),
name,
source: appSetting['player.soundEffect.convolution.fileName'],
mainGain: appSetting['player.soundEffect.convolution.mainGain'],
sendGain: appSetting['player.soundEffect.convolution.sendGain'],
})
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.newPreset {
position: relative;
border: 1px dashed var(--color-primary-font-hover);
// background-color: var(--color-main-background);
color: var(--color-primary-font-hover);
opacity: .7;
height: 22px;
&.editing {
opacity: 1;
width: 90px;
svg {
display: none;
}
.newPresetInput {
display: block;
}
}
:global {
.svg-icon {
vertical-align: 0;
}
}
}
.newPresetInput {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
// line-height: 16px;
background: none !important;
font-size: 12px;
text-align: center;
font-family: inherit;
box-sizing: border-box;
padding: 0 3px;
border-radius: 0;
display: none;
&::placeholder {
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
<svg-icon name="plus" />
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
</base-btn>
</template>
<script setup>
import { ref, nextTick } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
import { saveUserEQPreset } from '@renderer/store/soundEffect'
const isEditing = ref(false)
const input = ref(false)
const newPresetName = ref('')
const handleEditing = () => {
if (isEditing.value) return
// if (!this.newPresetName) this.newPresetName = this.listName
isEditing.value = true
nextTick(() => {
input.value.$el.focus()
})
}
const handleSave = (event) => {
let name = event.target.value.trim()
newPresetName.value = event.target.value = ''
isEditing.value = false
if (!name) return
if (name.length > 20) name = name.substring(0, 20)
saveUserEQPreset({
id: Date.now().toString(),
name,
hz31: appSetting['player.soundEffect.biquadFilter.hz31'],
hz62: appSetting['player.soundEffect.biquadFilter.hz62'],
hz125: appSetting['player.soundEffect.biquadFilter.hz125'],
hz250: appSetting['player.soundEffect.biquadFilter.hz250'],
hz500: appSetting['player.soundEffect.biquadFilter.hz500'],
hz1000: appSetting['player.soundEffect.biquadFilter.hz1000'],
hz2000: appSetting['player.soundEffect.biquadFilter.hz2000'],
hz4000: appSetting['player.soundEffect.biquadFilter.hz4000'],
hz8000: appSetting['player.soundEffect.biquadFilter.hz8000'],
hz16000: appSetting['player.soundEffect.biquadFilter.hz16000'],
})
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.newPreset {
position: relative;
border: 1px dashed var(--color-primary-font-hover);
// background-color: var(--color-main-background);
color: var(--color-primary-font-hover);
opacity: .7;
height: 22px;
&.editing {
opacity: 1;
width: 90px;
svg {
display: none;
}
.newPresetInput {
display: block;
}
}
:global {
.svg-icon {
vertical-align: 0;
}
}
}
.newPresetInput {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
// line-height: 16px;
background: none !important;
font-size: 12px;
text-align: center;
font-family: inherit;
box-sizing: border-box;
padding: 0 3px;
border-radius: 0;
display: none;
&::placeholder {
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div :class="$style.contnet">
<h3 class="player__sound_effect_title">{{ $t('player__sound_effect_convolution') }}</h3>
<div :class="$style.convolution">
<div :class="$style.convolutionList">
<base-checkbox
v-for="item in convolutions"
:id="`player__convolution_${item.name}`"
:key="item.name"
:class="$style.checkbox"
:model-value="appSetting['player.soundEffect.convolution.fileName']"
:label="$t(`player__sound_effect_convolution_file_${item.name}`)"
:value="item.source"
@update:model-value="updateConvolution($event)"
/>
</div>
<div :class="$style.sliderList">
<div :class="$style.sliderItem">
<span :class="$style.label">{{ $t('player__sound_effect_convolution_main_gain') }}</span>
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.convolution.mainGain']" :min="0" :max="50" @change="handleUpdateMainGain" />
<span :class="[$style.value]">{{ appSetting['player.soundEffect.convolution.mainGain'] * 10 }}%</span>
</div>
<div :class="$style.sliderItem">
<span :class="$style.label">{{ $t('player__sound_effect_convolution_send_gain') }}</span>
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.convolution.sendGain']" :min="0" :max="50" @change="handleUpdateSendGain" />
<span :class="[$style.value]">{{ appSetting['player.soundEffect.convolution.sendGain'] * 10 }}%</span>
</div>
</div>
</div>
<div :class="['scroll', $style.saveList]">
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
<AddConvolutionPresetBtn v-if="userPresetList.length < 31" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { convolutions } from '@renderer/plugins/player'
import AddConvolutionPresetBtn from './AddConvolutionPresetBtn'
import { getUserConvolutionPresetList, removeUserConvolutionPreset } from '@renderer/store/soundEffect'
const updateConvolution = val => {
const target = convolutions.find(c => c.source == val)
const setting = {
'player.soundEffect.convolution.fileName': val,
}
if (target) {
setting['player.soundEffect.convolution.mainGain'] = target.mainGain * 10
setting['player.soundEffect.convolution.sendGain'] = target.sendGain * 10
}
updateSetting(setting)
}
const handleUpdateMainGain = (value) => {
updateSetting({ 'player.soundEffect.convolution.mainGain': Math.round(value) })
}
const handleUpdateSendGain = (value) => {
updateSetting({ 'player.soundEffect.convolution.sendGain': Math.round(value) })
}
const handleSetPreset = (item) => {
updateSetting({
'player.soundEffect.convolution.fileName': item.source,
'player.soundEffect.convolution.mainGain': item.mainGain,
'player.soundEffect.convolution.sendGain': item.sendGain,
})
}
const userPresetList = ref([])
const handleRemovePreset = id => {
removeUserConvolutionPreset(id)
}
onMounted(() => {
getUserConvolutionPresetList().then(list => {
userPresetList.value = list
})
})
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.contnet {
display: flex;
flex-flow: column nowrap;
gap: 3px;
min-height: 0;
}
.convolution {
display: flex;
flex-flow: column wrap;
gap: 15px;
width: 100%;
}
.convolutionList {
display: flex;
flex-flow: row wrap;
gap: 8px;
width: 100%;
}
.checkbox {
margin-right: 10px;
font-size: 12px;
}
.sliderList {
display: flex;
flex-flow: column nowrap;
gap: 15px;
width: 100%;
}
.sliderItem {
display: flex;
flex-flow: row nowrap;
gap: 8px;
}
.slider {
flex: auto;
}
.label {
flex: none;
// width: 50px;
font-size: 12px;
}
.value {
flex: none;
width: 40px;
font-size: 12px;
text-align: center;
&.active {
color: var(--color-primary-font);
}
}
.saveList {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div :class="$style.contnet">
<div class="player__sound_effect_title" :class="$style.header">
<h3>{{ $t('player__sound_effect_panner') }}</h3>
<base-checkbox
id="player__sound_effect_panner_enabled"
:class="$style.checkbox"
:label="$t('player__sound_effect_panner_enabled')"
:model-value="appSetting['player.soundEffect.panner.enable']"
@update:model-value="updateEnabled"
/>
</div>
<div :class="$style.eqList">
<div :class="$style.eqItem">
<span :class="$style.label">{{ $t('player__sound_effect_panner_sound_speed') }}</span>
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.panner.speed']" :min="1" :max="50" @change="handleUpdateSpeed" />
<span :class="[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.speed'] != 25 }]">{{ appSetting['player.soundEffect.panner.speed'] }}</span>
</div>
<div :class="$style.eqItem">
<span :class="$style.label">{{ $t('player__sound_effect_panner_sound_r') }}</span>
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.panner.soundR']" :min="1" :max="30" @change="handleUpdateSoundR" />
<span :class="[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.soundR'] != 5 }]">{{ appSetting['player.soundEffect.panner.soundR'] }}</span>
</div>
</div>
</div>
</template>
<script setup>
// import { reactive } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
// const setting = reactive({
// enabled: false,
// soundR: 5,
// speed: 25,
// })
const updateEnabled = (enabled) => {
// console.log(enabled)
updateSetting({ 'player.soundEffect.panner.enable': enabled })
}
const handleUpdateSoundR = (value) => {
updateSetting({ 'player.soundEffect.panner.soundR': Math.round(value) })
}
const handleUpdateSpeed = (value) => {
updateSetting({ 'player.soundEffect.panner.speed': Math.round(value) })
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.contnet {
padding-top: 15px;
position: relative;
display: flex;
flex-flow: column nowrap;
gap: 8px;
&:before {
.mixin-after;
position: absolute;
top: 0;
height: 1px;
width: 100%;
border-top: 1px dashed var(--color-primary-light-100-alpha-700);
}
}
.header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
padding-bottom: 5px;
// padding-top: 5px;
}
.eqList {
display: flex;
flex-flow: column nowrap;
gap: 15px;
width: 100%;
}
.eqItem {
display: flex;
flex-flow: row nowrap;
gap: 8px;
}
.label {
flex: none;
// width: 50px;
font-size: 12px;
}
.value {
flex: none;
width: 40px;
font-size: 12px;
text-align: center;
&.active {
color: var(--color-primary-font);
}
}
.footer {
display: flex;
flex-flow: row nowrap;
// justify-content: space-between;
justify-content: center;
align-items: center;
// font-size: 13px;
span {
line-height: 1;
}
}
.slider {
flex: auto;
}
.checkbox {
margin-right: 10px;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div :class="$style.contnet">
<div class="player__sound_effect_title" :class="$style.header">
<h3>{{ $t('player__sound_effect_biquad_filter') }}</h3>
<base-btn min @click="handleReset">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>
</div>
<div :class="$style.eqList">
<div v-for="(v, i) in freqs" :key="v" :class="$style.eqItem">
<span :class="$style.label">{{ labels[i] }}</span>
<base-slider-bar :class="$style.slider" :value="appSetting[`player.soundEffect.biquadFilter.hz${v}`]" :min="-15" :max="15" @change="handleUpdate(v, $event)" />
<span :class="$style.value">{{ appSetting[`player.soundEffect.biquadFilter.hz${v}`] }}db</span>
</div>
</div>
<div :class="['scroll', $style.saveList]">
<!-- <base-btn min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_slow`) }}</base-btn> -->
<base-btn v-for="item in freqsPreset" :key="item.name" min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_${item.name}`) }}</base-btn>
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
<AddEQPresetBtn v-if="userPresetList.length < 31" />
</div>
<!-- <div :class="$style.footer">
<base-btn min @click="handleReset">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>
</div> -->
</div>
</template>
<script setup>
import { onMounted, ref } from '@common/utils/vueTools'
import { freqs, freqsPreset } from '@renderer/plugins/player'
import { appSetting, updateSetting } from '@renderer/store/setting'
import AddEQPresetBtn from './AddEQPresetBtn'
import { getUserEQPresetList, removeUserEQPreset } from '@renderer/store/soundEffect'
const labels = freqs.map(num => num < 1000 ? num : `${num / 1000}k`)
const handleUpdate = (key, value) => {
value = Math.round(value)
// values[index] = value
updateSetting({ [`player.soundEffect.biquadFilter.hz${key}`]: value })
// console.log(index, event.target.value, bfs)
}
const handleReset = () => {
const setting = {}
for (const key of freqs) {
setting[`player.soundEffect.biquadFilter.hz${key}`] = 0
}
updateSetting(setting)
}
const handleSetPreset = (item) => {
updateSetting({
'player.soundEffect.biquadFilter.hz31': item.hz31,
'player.soundEffect.biquadFilter.hz62': item.hz62,
'player.soundEffect.biquadFilter.hz125': item.hz125,
'player.soundEffect.biquadFilter.hz250': item.hz250,
'player.soundEffect.biquadFilter.hz500': item.hz500,
'player.soundEffect.biquadFilter.hz1000': item.hz1000,
'player.soundEffect.biquadFilter.hz2000': item.hz2000,
'player.soundEffect.biquadFilter.hz4000': item.hz4000,
'player.soundEffect.biquadFilter.hz8000': item.hz8000,
'player.soundEffect.biquadFilter.hz16000': item.hz16000,
})
}
const userPresetList = ref([])
const handleRemovePreset = id => {
removeUserEQPreset(id)
}
onMounted(() => {
getUserEQPresetList().then(list => {
userPresetList.value = list
})
})
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.contnet {
display: flex;
flex-flow: column nowrap;
gap: 8px;
min-height: 0;
}
.header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
padding-bottom: 5px;
// padding-top: 5px;
}
.eqList {
display: flex;
flex-flow: row wrap;
// gap: 15px;
width: 100%;
justify-content: space-between;
position: relative;
&:before {
.mixin-after;
position: absolute;
left: 50%;
height: 100%;
border-left: 1px dashed var(--color-primary-light-100-alpha-700);
}
}
.eqItem {
display: flex;
flex-flow: row nowrap;
width: 50%;
gap: 8px;
margin-bottom: 15px;
box-sizing: border-box;
&:nth-child(odd) {
padding-right: 10px;
}
&:nth-child(even) {
padding-left: 10px;
}
&:nth-last-child(1), &:nth-last-child(2) {
margin-bottom: 0;
}
}
.label {
flex: none;
width: 40px;
font-size: 12px;
text-align: center;
}
.value {
flex: none;
width: 40px;
font-size: 12px;
text-align: center;
}
.footer {
display: flex;
flex-flow: row nowrap;
// justify-content: space-between;
justify-content: center;
align-items: center;
// font-size: 13px;
span {
line-height: 1;
}
}
.slider {
flex: auto;
}
.saveList {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<button :class="$style.btn" :aria-label="$t('player__sound_effect')" @click="visible = true">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="90%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-tune-variant" />
</svg>
</button>
<material-modal :show="visible" bg-close="bg-close" :teleport="teleport" @close="visible = false">
<!-- <main :class="$style.main"> -->
<!-- <h2 :class="$style.title">{{ $t('theme_edit_modal__title') }}</h2> -->
<div :class="$style.content">
<div :class="$style.row">
<AudioConvolution />
<AudioPanner />
</div>
<div :class="$style.row">
<BiquadFilter />
</div>
</div>
<!-- </main> -->
</material-modal>
</template>
<script setup>
import { ref } from '@common/utils/vueTools'
// import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
// import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
// import { musicInfo, playMusicInfo } from '@renderer/store/player/state'
// import { saveVolumeIsMute } from '@renderer/store/setting'
// import { volume, isMute } from '@renderer/store/player/volume'
// import fs from 'node:fs'
import BiquadFilter from './BiquadFilter'
import AudioPanner from './AudioPanner'
import AudioConvolution from './AudioConvolution'
defineProps({
teleport: {
type: String,
default: '#root',
},
})
const visible = ref(false)
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.btn {
position: relative;
// color: var(--color-button-font);
justify-content: center;
align-items: center;
transition: color @transition-normal;
cursor: pointer;
background-color: transparent;
border: none;
width: 24px;
display: flex;
flex-flow: column nowrap;
padding: 0;
svg {
transition: opacity @transition-fast;
opacity: .6;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));
}
&:hover {
svg {
opacity: .9;
}
}
&:active {
svg {
opacity: 1;
}
}
}
.main {
min-width: 300px;
// max-height: 100%;
// overflow: hidden;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
}
// .title {
// flex: none;
// font-size: 16px;
// color: var(--color-font);
// line-height: 1.3;
// text-align: center;
// padding: 10px;
// }
.content {
display: flex;
flex-flow: row nowrap;
padding: 0 15px;
margin: 15px 0;
gap: 30px;
position: relative;
min-height: 0;
&:before {
.mixin-after;
position: absolute;
left: 50%;
height: 100%;
border-left: 1px dashed var(--color-primary-light-100-alpha-700);
}
// width: 400px;
:global {
// .player__sound_effect_contnet {
// display: flex;
// }
.player__sound_effect_title {
// margin-bottom: 10px;
font-size: 14px;
padding-bottom: 8px;
}
}
}
.row {
width: 50%;
display: flex;
gap: 15px;
flex-flow: column nowrap;
}
</style>

View File

@@ -137,8 +137,8 @@ export default {
// padding: 18px 3px;
// margin: 5px 0;
// border-left: 5px solid transparent;
transition: @transition-normal;
transition-property: color;
transition: @transition-fast;
transition-property: background-color, opacity;
color: var(--color-nav-font);
cursor: pointer;
font-size: 11.5px;
@@ -148,17 +148,30 @@ export default {
align-items: center;
justify-content: center;
transition: 0.3s ease;
transition-property: background-color, opacity;
// border-radius: @radius-border;
.mixin-ellipsis-1;
&:before {
.mixin-after;
left: 0;
top: 0;
width: 3px;
height: 100%;
background-color: var(--color-primary-dark-200-alpha-700);
border-radius: 4px;
transform: translateX(-100%);
transition: transform @transition-fast;
}
&.active {
// border-left-color: @color-theme-active;
background-color: var(--color-primary-light-400-alpha-600);
background-color: var(--color-primary-light-300-alpha-700);
&:before {
transform: translateX(0);
}
&:hover {
background-color: var(--color-primary-light-300-alpha-600);
background-color: var(--color-primary-light-300-alpha-800);
}
}
@@ -168,12 +181,12 @@ export default {
&:not(.active) {
opacity: .8;
background-color: var(--color-primary-light-500-alpha-600);
background-color: var(--color-primary-light-400-alpha-700);
}
}
&:active:not(.active) {
opacity: .6;
background-color: var(--color-primary-light-200-alpha-600);
background-color: var(--color-primary-light-300-alpha-600);
}
}

View File

@@ -18,14 +18,14 @@
</div>
<div v-if="!isAgreePact" :class="$style.btns">
<base-btn :class="$style.btn" @click="handleClose(true)">{{ $t('not_agree') }}</base-btn>
<base-btn :class="$style.btn" :disabled="!btnEnable" @click="handleClick()">{{ $t('agree') }} {{ timeStr }}</base-btn>
<base-btn :class="$style.btn" :disabled="!btnEnable" @click="handleClick">{{ $t('agree') }} {{ timeStr }}</base-btn>
</div>
</main>
</material-modal>
</template>
<script>
import { quitApp } from '@renderer/utils/ipc'
import { checkUpdate, quitApp } from '@renderer/utils/ipc'
import { openUrl } from '@common/utils/electron'
import { isShowPact } from '@renderer/store'
import { appSetting, saveAgreePact } from '@renderer/store/setting'
@@ -75,6 +75,8 @@ export default {
this.$dialog({
message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),
confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
}).then(() => {
checkUpdate()
})
}, 2e3)
},

View File

@@ -14,6 +14,7 @@ div(:class="$style.footerLeftControlBtns")
button(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" @click="toggleVisibleComment" :aria-label="$t('comment__show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
common-sound-effect-btn
common-playback-rate-btn
common-volume-btn
common-toggle-play-mode-btn

View File

@@ -7,15 +7,16 @@ div(:class="$style.container")
img( :class="$style.avatar" :src="item.avatar || commentDefImg" @error="handleUserImg")
div(:class="$style.right")
div(:class="$style.info")
div.select(:class="$style.name") {{item.userName}}
time(:class="$style.label" v-if="item.timeStr") {{timeFormat(item.timeStr)}}
div(:class="$style.label" v-if="item.location") {{item.location}}
div(:class="$style.baseInfo")
div.select(:class="$style.name") {{item.userName}}
div(:class="$style.metaInfo")
time(:class="$style.label" v-if="item.timeStr") {{timeFormat(item.timeStr)}}
div(:class="$style.label" v-if="item.location") {{$t('comment__location', { location: item.location })}}
div(:class="$style.likes" v-if="item.likedCount != null")
svg(:class="$style.likesIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-thumbs-up')
| {{item.likedCount}}
div.select(:class="$style.comment_text")
p(v-for="text in item.text") {{text}}
p.select(:class="$style.comment_text") {{item.text}}
div(v-if="item.images?.length" :class="$style.comment_images")
img(v-for="url in item.images" :src="url" loading="lazy" decoding="async")
comment-floor(v-if="item.reply && item.reply.length" :class="$style.reply_floor" :comments="item.reply")
@@ -87,12 +88,27 @@ export default {
.info {
display: flex;
flex-flow: row nowrap;
align-items: flex-end;
min-width: 0;
gap: 15px;
width: 100%;
height: 40px;
line-height: 1.3;
gap: 6px;
color: var(--color-450);
}
.baseInfo {
height: 100%;
flex: auto;
display: flex;
min-width: 0;
flex-flow: column nowrap;
justify-content: space-evenly;
}
.metaInfo {
display: flex;
flex-flow: row nowrap;
min-width: 0;
gap: 10px;
overflow: hidden;
}
.name {
flex: 0 1 auto;
min-width: 0;
@@ -105,11 +121,11 @@ export default {
// margin-left: 5px;
}
.likes {
flex: 1 0 auto;
margin-left: 10px;
flex: none;
font-size: 11px;
align-self: flex-end;
text-align: right;
padding-top: 3px;
align-self: flex-start;
}
.likesIcon {
width: 12px;
@@ -120,12 +136,10 @@ export default {
.comment_text {
text-align: justify;
font-size: 14px;
padding-top: 5px;
p {
line-height: 1.5;
word-break: break-all;
overflow-wrap: break-word;
}
line-height: 1.5;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.comment_images {
display: flex;

View File

@@ -48,12 +48,12 @@ export default {
const tipSearch = debounce(async() => {
if (searchText.value === '' && prevTempSearchSource) {
tipList.value = []
music[prevTempSearchSource].tempSearch.cancelTempSearch()
music[prevTempSearchSource].tipSearch.cancelTipSearch()
return
}
const { temp_source } = await getSearchSetting()
prevTempSearchSource = temp_source
music[prevTempSearchSource].tempSearch.search(searchText.value).then(list => {
music[prevTempSearchSource].tipSearch.search(searchText.value).then(list => {
tipList.value = list
}).catch(() => {})
}, 50)

View File

@@ -203,9 +203,10 @@ export default {
transition: box-shadow .4s ease, background-color @transition-normal;
display: flex;
flex-flow: column nowrap;
background-color: var(--color-primary-light-600-alpha-100);
background-color: var(--color-primary-light-300-alpha-700);
&.active {
background-color: var(--color-primary-light-600-alpha-100);
box-shadow: 0 1px 5px 0 rgba(0,0,0,.2);
.form {
input {

View File

@@ -9,6 +9,7 @@ import {
} from '@renderer/utils/ipc'
import { appSetting } from '@renderer/store/setting'
import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils'
import { requestMsg } from '@renderer/utils/message'
const getOtherSourcePromises = new Map()
@@ -187,6 +188,7 @@ export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggl
return { musicInfo, url, quality: type, isFromCache: false }
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).catch((err: any) => {
if (err.message == requestMsg.tooManyRequests) throw err
console.log(err)
return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource })
})
@@ -220,7 +222,7 @@ export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSourc
return { musicInfo, url, quality: type, isFromCache: false }
}).catch(async(err: any) => {
console.log(err)
if (!allowToggleSource) throw err
if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err
onToggleSource()
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOtherSource(musicInfo).then(otherSource => {
@@ -266,7 +268,7 @@ export const getOnlineOtherSourcePicUrl = async({ musicInfos, onToggleSource, is
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)).promise
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))
} catch (err: any) {
reqPromise = Promise.reject(err)
}
@@ -296,7 +298,7 @@ export const handleGetOnlinePicUrl = async({ musicInfo, isRefresh, onToggleSourc
// console.log(musicInfo.source)
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)).promise
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))
} catch (err) {
reqPromise = Promise.reject(err)
}

View File

@@ -52,12 +52,36 @@ const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTim
* 检查音乐信息是否已更改
*/
const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {
return curMusicInfo !== playMusicInfo.musicInfo || isPlay.value
// return curMusicInfo !== playMusicInfo.musicInfo || isPlay.value
return gettingUrlId != curMusicInfo.id || curMusicInfo.id != playMusicInfo.musicInfo?.id || isPlay.value
}
let cancelDelayRetry: (() => void) | null = null
const delayRetry = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<string | null> => {
// if (cancelDelayRetry) cancelDelayRetry()
return new Promise<string | null>((resolve, reject) => {
const time = getRandom(2, 6)
setAllStatus(window.i18n.t('player__geting_url_delay_retry', { time }))
const tiemout = setTimeout(() => {
getMusicPlayUrl(musicInfo, isRefresh, true).then((result) => {
cancelDelayRetry = null
resolve(result)
}).catch(async(err: any) => {
cancelDelayRetry = null
reject(err)
})
}, time * 1000)
cancelDelayRetry = () => {
clearTimeout(tiemout)
cancelDelayRetry = null
resolve(null)
}
})
}
const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false, isRetryed = false): Promise<string | null> => {
// this.musicInfo.url = await getMusicPlayUrl(targetSong, type)
setAllStatus(window.i18n.t('player__geting_url'))
if (appSetting['player.autoSkipOnError']) addLoadTimeout()
// const type = getPlayType(appSetting['player.highQuality'], musicInfo)
@@ -79,6 +103,8 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
diffCurrentMusicInfo(musicInfo) ||
err.message == requestMsg.cancelRequest) return null
if (err.message == requestMsg.tooManyRequests) return delayRetry(musicInfo, isRefresh)
if (!isRetryed) return getMusicPlayUrl(musicInfo, isRefresh, true)
throw err
@@ -86,7 +112,9 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
}
export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh?: boolean) => {
if (appSetting['player.autoSkipOnError']) addLoadTimeout()
// if (appSetting['player.autoSkipOnError']) addLoadTimeout()
if (!diffCurrentMusicInfo(musicInfo)) return
if (cancelDelayRetry) cancelDelayRetry()
gettingUrlId = musicInfo.id
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
if (!url) return
@@ -152,8 +180,7 @@ const handlePlay = () => {
}
const musicInfo = playMusicInfo.musicInfo
if (!musicInfo || gettingUrlId == musicInfo.id) return
gettingUrlId &&= ''
if (!musicInfo) return
setStop()
window.app_event.pause()

View File

@@ -67,7 +67,7 @@ export default () => {
sendInited()
handleListAutoUpdate()
if (window.lx.isProd) checkUpdate()
if (window.lx.isProd && appSetting['common.isAgreePact']) checkUpdate()
})
})
}

View File

@@ -1,19 +1,44 @@
import { onBeforeUnmount } from '@common/utils/vueTools'
import { getDuration, getPlaybackRate, getCurrentTime } from '@renderer/plugins/player'
import { musicInfo } from '@renderer/store/player/state'
import { isPlay, musicInfo, playMusicInfo } from '@renderer/store/player/state'
import { playProgress } from '@renderer/store/player/playProgress'
import { playNext, playPrev, stop } from '@renderer/core/player'
// import { } from ''
import { pause, play, playNext, playPrev, stop } from '@renderer/core/player'
export default () => {
// 创建一个空白音频以保持对 Media Session 的注册
const emptyAudio = new Audio()
emptyAudio.autoplay = false
emptyAudio.src = require('@renderer/assets/medias/Silence02s.mp3')
emptyAudio.controls = false
emptyAudio.preload = 'auto'
emptyAudio.onplaying = () => {
emptyAudio.pause()
}
void emptyAudio.play()
let prevPicUrl = ''
const updateMediaSessionInfo = () => {
if (musicInfo.id == null) {
navigator.mediaSession.metadata = null
return
}
const mediaMetadata: MediaMetadata = {
title: musicInfo.name,
artist: musicInfo.singer,
album: musicInfo.album,
artwork: [],
}
if (musicInfo.pic) mediaMetadata.artwork = [{ src: musicInfo.pic }]
if (musicInfo.pic) {
const pic = new Image()
pic.src = prevPicUrl = musicInfo.pic
pic.onload = () => {
if (prevPicUrl == pic.src) {
mediaMetadata.artwork = [{ src: pic.src }]
// @ts-expect-error
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
}
}
} else prevPicUrl = ''
// @ts-expect-error
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
@@ -48,25 +73,27 @@ export default () => {
navigator.mediaSession.playbackState = 'none'
}
const handleSetPlayInfo = () => {
updateMediaSessionInfo()
updatePositionState({
position: playProgress.nowPlayTime,
duration: playProgress.maxPlayTime,
emptyAudio.play().finally(() => {
updateMediaSessionInfo()
updatePositionState({
position: playProgress.nowPlayTime,
duration: playProgress.maxPlayTime,
})
handlePause()
})
handlePause()
}
// const registerMediaSessionHandler = () => {
// navigator.mediaSession.setActionHandler('play', () => {
// if (this.isPlay || !this.playMusicInfo) return
// console.log('play')
// this.startPlay()
// })
// navigator.mediaSession.setActionHandler('pause', () => {
// if (!this.isPlay || !this.playMusicInfo) return
// console.log('pause')
// this.stopPlay()
// })
navigator.mediaSession.setActionHandler('play', () => {
if (isPlay.value || !playMusicInfo) return
console.log('play')
play()
})
navigator.mediaSession.setActionHandler('pause', () => {
if (!isPlay.value || !playMusicInfo) return
console.log('pause')
pause()
})
navigator.mediaSession.setActionHandler('stop', () => {
console.log('stop')
setStop()
@@ -107,6 +134,8 @@ export default () => {
window.app_event.on('pause', handlePause)
window.app_event.on('stop', handleStop)
window.app_event.on('error', handlePause)
window.app_event.on('playerEmptied', handleSetPlayInfo)
// window.app_event.on('playerLoadstart', handleSetPlayInfo)
window.app_event.on('musicToggled', handleSetPlayInfo)
window.app_event.on('picUpdated', updateMediaSessionInfo)
@@ -117,6 +146,8 @@ export default () => {
window.app_event.off('pause', handlePause)
window.app_event.off('stop', handleStop)
window.app_event.off('error', handlePause)
window.app_event.off('playerEmptied', handleSetPlayInfo)
// window.app_event.off('playerLoadstart', handleSetPlayInfo)
window.app_event.off('musicToggled', handleSetPlayInfo)
window.app_event.off('picUpdated', updateMediaSessionInfo)
})

View File

@@ -31,6 +31,7 @@ import useWatchList from './useWatchList'
import { HOTKEY_PLAYER } from '@common/hotKey'
import { playNext, pause, playPrev, togglePlay } from '@renderer/core/player'
import usePlaybackRate from './usePlaybackRate'
import useSoundEffect from './useSoundEffect'
export default () => {
@@ -41,6 +42,7 @@ export default () => {
usePlayEvent()
useLyric()
useVolume()
useSoundEffect()
usePlaybackRate()
useWatchList()

View File

@@ -0,0 +1,152 @@
import { watch } from '@common/utils/vueTools'
import {
freqs,
getAudioContext,
getBiquadFilter,
setConvolver,
setPannerSoundR,
setPannerSpeed,
startPanner,
stopPanner,
setConvolverMainGain,
setConvolverSendGain,
} from '@renderer/plugins/player'
import { appSetting } from '@renderer/store/setting'
const cache = new Map<string, AudioBuffer>()
const loadBuffer = async(name: string) => new Promise<AudioBuffer>((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('@static/medias/filters/' + name) as string
if (cache.has(path)) {
resolve(cache.get(path) as AudioBuffer)
return
}
// Load buffer asynchronously
let request = new XMLHttpRequest()
request.open('GET', path, true)
request.responseType = 'arraybuffer'
request.onload = function() {
// Asynchronously decode the audio file data in request.response
void getAudioContext().decodeAudioData(request.response, (buffer) => {
if (!buffer) {
reject(new Error('error decoding file data: ' + path))
return
}
cache.set(path, buffer)
resolve(buffer)
},
function(error) {
reject(error)
console.error('decodeAudioData error', error)
})
}
request.onerror = function() {
reject(new Error('XHR error'))
}
request.send()
})
export default () => {
console.log(appSetting['player.soundEffect.panner.enable'])
if (appSetting['player.soundEffect.panner.enable']) startPanner()
setPannerSoundR(appSetting['player.soundEffect.panner.soundR'] / 10)
setPannerSpeed(2 * (appSetting['player.soundEffect.panner.speed'] / 10))
if (freqs.some(v => appSetting[`player.soundEffect.biquadFilter.hz${v}`] != 0)) {
const bfs = getBiquadFilter()
for (const item of freqs) {
bfs.get(`hz${item}`)!.gain.value = appSetting[`player.soundEffect.biquadFilter.hz${item}`]
}
}
if (appSetting['player.soundEffect.convolution.fileName']) {
void loadBuffer(appSetting['player.soundEffect.convolution.fileName']).then((buffer) => {
setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)
})
}
watch(() => appSetting['player.soundEffect.panner.enable'], (enable) => {
if (enable) {
startPanner()
} else {
stopPanner()
}
})
watch(() => appSetting['player.soundEffect.panner.soundR'], (soundR) => {
setPannerSoundR(soundR / 10)
})
watch(() => appSetting['player.soundEffect.panner.speed'], (speed) => {
setPannerSpeed(2 * (speed / 10))
})
watch(() => appSetting['player.soundEffect.convolution.fileName'], (fileName) => {
setTimeout(() => {
if (fileName) {
void loadBuffer(fileName).then((buffer) => {
setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)
})
} else {
setConvolver(null, 0, 0)
}
})
})
watch(() => appSetting['player.soundEffect.convolution.mainGain'], (mainGain) => {
setConvolverMainGain(mainGain / 10)
})
watch(() => appSetting['player.soundEffect.convolution.sendGain'], (sendGain) => {
setConvolverSendGain(sendGain / 10)
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz31'], (hz31) => {
const bfs = getBiquadFilter()
bfs.get('hz31')!.gain.value = hz31
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz62'], (hz62) => {
const bfs = getBiquadFilter()
bfs.get('hz62')!.gain.value = hz62
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz125'], (hz125) => {
const bfs = getBiquadFilter()
bfs.get('hz125')!.gain.value = hz125
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz250'], (hz250) => {
const bfs = getBiquadFilter()
bfs.get('hz250')!.gain.value = hz250
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz500'], (hz500) => {
const bfs = getBiquadFilter()
bfs.get('hz500')!.gain.value = hz500
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz1000'], (hz1000) => {
const bfs = getBiquadFilter()
bfs.get('hz1000')!.gain.value = hz1000
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz2000'], (hz2000) => {
const bfs = getBiquadFilter()
bfs.get('hz2000')!.gain.value = hz2000
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz4000'], (hz4000) => {
const bfs = getBiquadFilter()
bfs.get('hz4000')!.gain.value = hz4000
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz8000'], (hz8000) => {
const bfs = getBiquadFilter()
bfs.get('hz8000')!.gain.value = hz8000
})
watch(() => appSetting['player.soundEffect.biquadFilter.hz16000'], (hz16000) => {
const bfs = getBiquadFilter()
bfs.get('hz16000')!.gain.value = hz16000
})
// window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
// window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)
// window.app_event.on('setPlaybackRate', handleSetPlaybackRate)
// onBeforeUnmount(() => {
// // window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
// // window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)
// window.app_event.off('setPlaybackRate', handleSetPlaybackRate)
// })
}

View File

@@ -2,6 +2,50 @@ let audio: HTMLAudioElement | null = null
let audioContext: AudioContext
let mediaSource: MediaElementAudioSourceNode
let analyser: AnalyserNode
// https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext
// https://benzleung.gitbooks.io/web-audio-api-mini-guide/content/chapter5-1.html
export const freqs = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] as const
type Freqs = (typeof freqs)[number]
let biquads: Map<`hz${Freqs}`, BiquadFilterNode>
export const freqsPreset = [
{ name: 'pop', hz31: 6, hz62: 5, hz125: -3, hz250: -2, hz500: 5, hz1000: 4, hz2000: -4, hz4000: -3, hz8000: 6, hz16000: 4 },
{ name: 'dance', hz31: 4, hz62: 3, hz125: -4, hz250: -6, hz500: 0, hz1000: 0, hz2000: 3, hz4000: 4, hz8000: 4, hz16000: 5 },
{ name: 'rock', hz31: 7, hz62: 6, hz125: 2, hz250: 1, hz500: -3, hz1000: -4, hz2000: 2, hz4000: 1, hz8000: 4, hz16000: 5 },
{ name: 'classical', hz31: 6, hz62: 7, hz125: 1, hz250: 2, hz500: -1, hz1000: 1, hz2000: -4, hz4000: -6, hz8000: -7, hz16000: -8 },
{ name: 'vocal', hz31: -5, hz62: -6, hz125: -4, hz250: -3, hz500: 3, hz1000: 4, hz2000: 5, hz4000: 4, hz8000: -3, hz16000: -3 },
{ name: 'slow', hz31: 5, hz62: 4, hz125: 2, hz250: 0, hz500: -2, hz1000: 0, hz2000: 3, hz4000: 6, hz8000: 7, hz16000: 8 },
{ name: 'electronic', hz31: 6, hz62: 5, hz125: 0, hz250: -5, hz500: -4, hz1000: 0, hz2000: 6, hz4000: 8, hz8000: 8, hz16000: 7 },
{ name: 'subwoofer', hz31: 8, hz62: 7, hz125: 5, hz250: 4, hz500: 0, hz1000: 0, hz2000: 0, hz4000: 0, hz8000: 0, hz16000: 0 },
{ name: 'soft', hz31: -5, hz62: -5, hz125: -4, hz250: -4, hz500: 3, hz1000: 2, hz2000: 4, hz4000: 4, hz8000: 0, hz16000: 0 },
] as const
export const convolutions = [
{ name: 'telephone', mainGain: 0.0, sendGain: 3.0, source: 'filter-telephone.wav' }, // 电话
{ name: 's2_r4_bd', mainGain: 1.8, sendGain: 0.9, source: 's2_r4_bd.wav' }, // 教堂
{ name: 'bright_hall', mainGain: 0.8, sendGain: 2.4, source: 'bright-hall.wav' },
{ name: 'cinema_diningroom', mainGain: 0.6, sendGain: 2.3, source: 'cinema-diningroom.wav' },
{ name: 'dining_living_true_stereo', mainGain: 0.6, sendGain: 1.8, source: 'dining-living-true-stereo.wav' },
{ name: 'living_bedroom_leveled', mainGain: 0.6, sendGain: 2.1, source: 'living-bedroom-leveled.wav' },
{ name: 'spreader50_65ms', mainGain: 1, sendGain: 2.5, source: 'spreader50-65ms.wav' },
// { name: 'spreader25_125ms', mainGain: 1, sendGain: 2.5, source: 'spreader25-125ms.wav' },
// { name: 'backslap', mainGain: 1.8, sendGain: 0.8, source: 'backslap1.wav' },
{ name: 's3_r1_bd', mainGain: 1.8, sendGain: 0.8, source: 's3_r1_bd.wav' },
{ name: 'matrix_1', mainGain: 1.5, sendGain: 0.9, source: 'matrix-reverb1.wav' },
{ name: 'matrix_2', mainGain: 1.3, sendGain: 1, source: 'matrix-reverb2.wav' },
{ name: 'cardiod_35_10_spread', mainGain: 1.8, sendGain: 0.6, source: 'cardiod-35-10-spread.wav' },
{ name: 'tim_omni_35_10_magnetic', mainGain: 1, sendGain: 0.2, source: 'tim-omni-35-10-magnetic.wav' },
// { name: 'spatialized', mainGain: 1.8, sendGain: 0.8, source: 'spatialized8.wav' },
// { name: 'zing_long_stereo', mainGain: 0.8, sendGain: 1.8, source: 'zing-long-stereo.wav' },
{ name: 'feedback_spring', mainGain: 1.8, sendGain: 0.8, source: 'feedback-spring.wav' },
// { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' },
] as const
let convolver: ConvolverNode
let convolverSourceGainNode: GainNode
let convolverOutputGainNode: GainNode
let convolverDynamicsCompressor: DynamicsCompressorNode
let gainNode: GainNode
let panner: PannerNode
export const soundR = 0.5
export const createAudio = () => {
if (audio) return
@@ -11,21 +55,168 @@ export const createAudio = () => {
audio.preload = 'auto'
}
export const getAnalyser = (): AnalyserNode | null => {
if (!audio) throw new Error('audio not defined')
const initAnalyser = () => {
analyser = audioContext.createAnalyser()
analyser.fftSize = 256
}
if (audioContext == null) {
audioContext = new window.AudioContext()
mediaSource = audioContext.createMediaElementSource(audio)
analyser = audioContext.createAnalyser()
analyser.fftSize = 256
mediaSource.connect(analyser)
analyser.connect(audioContext.destination)
const initBiquadFilter = () => {
biquads = new Map()
let i
for (const item of freqs) {
const filter = audioContext.createBiquadFilter()
biquads.set(`hz${item}`, filter)
filter.type = 'peaking'
filter.frequency.value = item
filter.Q.value = 1.4
filter.gain.value = 0
}
for (i = 1; i < freqs.length; i++) {
(biquads.get(`hz${freqs[i - 1]}`) as BiquadFilterNode).connect(biquads.get(`hz${freqs[i]}`) as BiquadFilterNode)
}
}
const initConvolver = () => {
convolverSourceGainNode = audioContext.createGain()
convolverOutputGainNode = audioContext.createGain()
convolverDynamicsCompressor = audioContext.createDynamicsCompressor()
convolver = audioContext.createConvolver()
convolver.connect(convolverOutputGainNode)
convolverSourceGainNode.connect(convolverDynamicsCompressor)
convolverOutputGainNode.connect(convolverDynamicsCompressor)
}
const initPanner = () => {
panner = audioContext.createPanner()
}
const initGain = () => {
gainNode = audioContext.createGain()
}
const initAdvancedAudioFeatures = () => {
if (audioContext) return
if (!audio) throw new Error('audio not defined')
audioContext = new window.AudioContext()
mediaSource = audioContext.createMediaElementSource(audio)
initAnalyser()
mediaSource.connect(analyser)
// analyser.connect(audioContext.destination)
initBiquadFilter()
analyser.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
initConvolver()
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
lastBiquadFilter.connect(convolverSourceGainNode)
lastBiquadFilter.connect(convolver)
initPanner()
convolverDynamicsCompressor.connect(panner)
initGain()
panner.connect(gainNode)
gainNode.connect(audioContext.destination)
}
export const getAudioContext = () => {
initAdvancedAudioFeatures()
return audioContext
}
export const getAnalyser = (): AnalyserNode | null => {
initAdvancedAudioFeatures()
return analyser
}
export const hasInitedAnalyser = (): boolean => audioContext != null
export const getBiquadFilter = () => {
initAdvancedAudioFeatures()
return biquads
}
// let isConvolverConnected = false
export const setConvolver = (buffer: AudioBuffer | null, mainGain: number, sendGain: number) => {
initAdvancedAudioFeatures()
convolver.buffer = buffer
// console.log(mainGain, sendGain)
if (buffer) {
convolverSourceGainNode.gain.value = mainGain
convolverOutputGainNode.gain.value = sendGain
} else {
convolverSourceGainNode.gain.value = 1
convolverOutputGainNode.gain.value = 0
}
}
export const setConvolverMainGain = (gain: number) => {
if (convolverSourceGainNode.gain.value == gain) return
// console.log(gain)
convolverSourceGainNode.gain.value = gain
}
export const setConvolverSendGain = (gain: number) => {
if (convolverOutputGainNode.gain.value == gain) return
// console.log(gain)
convolverOutputGainNode.gain.value = gain
}
let pannerInfo = {
x: 0,
y: 0,
z: 0,
soundR: 0.5,
rad: 0,
speed: 1,
intv: null as NodeJS.Timeout | null,
}
const setPannerXYZ = (nx: number, ny: number, nz: number) => {
pannerInfo.x = nx
pannerInfo.y = ny
pannerInfo.z = nz
// console.log(pannerInfo)
panner.positionX.value = nx * pannerInfo.soundR
panner.positionY.value = ny * pannerInfo.soundR
panner.positionZ.value = nz * pannerInfo.soundR
}
export const setPannerSoundR = (r: number) => {
pannerInfo.soundR = r
}
export const setPannerSpeed = (speed: number) => {
pannerInfo.speed = speed
if (pannerInfo.intv) startPanner()
}
export const stopPanner = () => {
if (pannerInfo.intv) {
clearInterval(pannerInfo.intv)
pannerInfo.intv = null
pannerInfo.rad = 0
}
panner.positionX.value = 0
panner.positionY.value = 0
panner.positionZ.value = 0
}
export const startPanner = () => {
initAdvancedAudioFeatures()
if (pannerInfo.intv) {
clearInterval(pannerInfo.intv)
pannerInfo.intv = null
pannerInfo.rad = 0
}
pannerInfo.intv = setInterval(() => {
pannerInfo.rad += 1
if (pannerInfo.rad > 360) pannerInfo.rad -= 360
setPannerXYZ(Math.sin(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180))
}, pannerInfo.speed * 10)
}
export const hasInitedAdvancedAudioFeatures = (): boolean => audioContext != null
export const setResource = (src: string) => {
if (audio) audio.src = src

View File

@@ -5,12 +5,14 @@ export const allMusicList: Map<string, LX.Music.MusicInfo[]> = markRaw(new Map()
export const defaultList = markRaw<LX.List.MyDefaultListInfo>({
id: LIST_IDS.DEFAULT,
name: '试听列表',
name: 'list__name_default',
// name: '试听列表',
})
export const loveList = markRaw<LX.List.MyLoveListInfo>({
id: LIST_IDS.LOVE,
name: '我的收藏',
name: 'list__name_love',
// name: '我的收藏',
})
export const tempList = markRaw<LX.List.MyTempListInfo>({
id: LIST_IDS.TEMP,

View File

@@ -37,6 +37,7 @@ type Tags = Partial<Record<LX.OnlineSource, TagInfo>>
export const tags = shallowReactive<Tags>({})
export declare interface ListInfoItem {
play_count: string
id: string

View File

@@ -0,0 +1,56 @@
import { reactive, toRaw } from '@common/utils/vueTools'
import { getUserSoundEffectConvolutionPresetList, getUserSoundEffectEQPresetList, saveUserSoundEffectConvolutionPresetList, saveUserSoundEffectEQPresetList } from '@renderer/utils/ipc'
let userEqPresetList: LX.SoundEffect.EQPreset[] | null = null
export const getUserEQPresetList = async() => {
if (userEqPresetList == null) {
userEqPresetList = reactive(await getUserSoundEffectEQPresetList())
}
return userEqPresetList
}
export const saveUserEQPreset = async(preset: LX.SoundEffect.EQPreset) => {
if (userEqPresetList == null) {
userEqPresetList = reactive(await getUserSoundEffectEQPresetList())
}
const target = userEqPresetList.find(p => p.id == preset.id)
if (target) Object.assign(target, preset)
else userEqPresetList.push(preset)
saveUserSoundEffectEQPresetList(toRaw(userEqPresetList))
}
export const removeUserEQPreset = async(id: string) => {
if (userEqPresetList == null) {
userEqPresetList = reactive(await getUserSoundEffectEQPresetList())
}
const index = userEqPresetList.findIndex(p => p.id == id)
if (index < 0) return
userEqPresetList.splice(index, 1)
saveUserSoundEffectEQPresetList(toRaw(userEqPresetList))
}
let userConvolutionPresetList: LX.SoundEffect.ConvolutionPreset[] | null = null
export const getUserConvolutionPresetList = async() => {
if (userEqPresetList == null) {
userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())
}
return userConvolutionPresetList
}
export const saveUserConvolutionPreset = async(preset: LX.SoundEffect.ConvolutionPreset) => {
if (userConvolutionPresetList == null) {
userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())
}
const target = userConvolutionPresetList.find(p => p.id == preset.id)
if (target) Object.assign(target, preset)
else userConvolutionPresetList.push(preset)
saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))
}
export const removeUserConvolutionPreset = async(id: string) => {
if (userConvolutionPresetList == null) {
userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())
}
const index = userConvolutionPresetList.findIndex(p => p.id == id)
if (index < 0) return
userConvolutionPresetList.splice(index, 1)
saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))
}

View File

@@ -13,3 +13,4 @@ import '@common/types/desktop_lyric'
import '@common/types/ipc_renderer'
import '@common/types/config_files'
import '@common/types/music_metadata'
import '@common/types/sound_effect'

View File

@@ -5,6 +5,17 @@ export * from '@common/utils/nodejs'
export * from '@common/utils/common'
export * from '@common/utils/tools'
/**
* 格式化播放数量
* @param {*} num 数字
*/
export const formatPlayCount = (num: number): string => {
if (num > 100000000) return `${Math.trunc(num / 10000000) / 10}亿`
if (num > 10000) return `${Math.trunc(num / 1000) / 10}`
return String(num)
}
/**
* 时间格式化
*/

View File

@@ -291,6 +291,22 @@ export const getSystemFonts = async() => {
})
}
export const getUserSoundEffectEQPresetList = async() => {
return await rendererInvoke<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset)
}
export const saveUserSoundEffectEQPresetList = (list: LX.SoundEffect.EQPreset[]) => {
rendererSend<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, list)
}
export const getUserSoundEffectConvolutionPresetList = async() => {
return await rendererInvoke<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset)
}
export const saveUserSoundEffectConvolutionPresetList = (list: LX.SoundEffect.ConvolutionPreset[]) => {
rendererSend<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, list)
}
export const allHotKeys = markRaw({
local: [

View File

@@ -5,4 +5,5 @@ export const requestMsg = {
// unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
notConnectNetwork: '无法连接到服务器',
cancelRequest: '取消http请求',
tooManyRequests: '服务器繁忙',
} as const

View File

@@ -13,7 +13,11 @@ const api_test = {
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},

View File

@@ -16,8 +16,7 @@ const bd = {
},
getPic(songInfo) {
const requestObj = this.getMusicInfo(songInfo)
requestObj.promise = requestObj.promise.then(info => info.pic_premium)
return requestObj
return requestObj.promise.then(info => info.pic_premium)
},
getLyric(songInfo) {
const requestObj = this.getMusicInfo(songInfo)

View File

@@ -0,0 +1,63 @@
import { getMusicInfosByList } from './musicInfo'
import { createHttpFetch } from './util'
export default {
/**
* 通过AlbumId获取专辑信息
* @param {*} id
*/
async getAlbumInfo(id) {
const albumInfoRequest = await createHttpFetch('http://kmrserviceretry.kugou.com/container/v1/album?dfid=1tT5He3kxrNC4D29ad1MMb6F&mid=22945702112173152889429073101964063697&userid=0&appid=1005&clientver=11589', {
method: 'POST',
body: {
appid: 1005,
clienttime: 1681833686,
clientver: 11589,
data: [{ album_id: id }],
fields: 'language,grade_count,intro,mix_intro,heat,category,sizable_cover,cover,album_name,type,quality,publish_company,grade,special_tag,author_name,publish_date,language_id,album_id,exclusive,is_publish,trans_param,authors,album_tag',
isBuy: 0,
key: 'e6f3306ff7e2afb494e89fbbda0becbf',
mid: '22945702112173152889429073101964063697',
show_album_tag: 0,
},
})
if (!albumInfoRequest) return Promise.reject(new Error('get album info failed.'))
const albumInfo = albumInfoRequest[0]
return {
name: albumInfo.album_name,
image: albumInfo.sizable_cover.replace('{size}', 240),
desc: albumInfo.intro,
authorName: albumInfo.author_name,
// play_count: this.formatPlayCount(info.count),
}
},
/**
* 通过AlbumId获取专辑
* @param {*} id
* @param {*} page
*/
async getAlbumDetail(id, page = 1, limit = 200) {
const albumList = await createHttpFetch(`http://mobiles.kugou.com/api/v3/album/song?version=9108&albumid=${id}&plat=0&pagesize=${limit}&area_code=0&page=${page}&with_res_tag=0`)
if (!albumList.info) return Promise.reject(new Error('Get album list failed.'))
let result = await getMusicInfosByList(albumList.info)
const info = await this.getAlbumInfo(id)
return {
list: result || [],
page,
limit,
total: albumList.total,
source: 'kg',
info: {
name: info.name,
img: info.image,
desc: info.desc,
author: info.authorName,
// play_count: this.formatPlayCount(info.count),
},
}
},
}

View File

@@ -13,7 +13,11 @@ const api_test = {
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},

View File

@@ -1,12 +1,6 @@
import { httpFetch } from '../../request'
import { decodeName, dateFormat2 } from '../../index'
import { toMD5 } from '../utils'
const signatureParams = (params) => {
let OIlwieks = '28dk2k092lksi2UIkp'
let sign_params = `OIlwieks${OIlwieks}${params.replace(/&/g, '')}OIlwieks${OIlwieks}`
return toMD5(sign_params)
}
import { signatureParams } from './util'
export default {
_requestObj: null,
@@ -16,8 +10,7 @@ export default {
let timestamp = Date.now()
const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=0&p=${page}&pagesize=${limit}&uuid=0&ver=10`
let signature = signatureParams(params)
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/v1/cmtlist?${params}&signature=${signature}`, {
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/v1/cmtlist?${params}&signature=${signatureParams(params)}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24',
},
@@ -32,8 +25,7 @@ export default {
if (this._requestObj2) this._requestObj2.cancelHttp()
let timestamp = Date.now()
const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=0&p=${page}&pagesize=${limit}&uuid=0&ver=10`
let signature = signatureParams(params)
const _requestObj2 = httpFetch(`http://m.comment.service.kugou.com/v1/weightlist?${params}&signature=${signature}`, {
const _requestObj2 = httpFetch(`http://m.comment.service.kugou.com/v1/weightlist?${params}&signature=${signatureParams(params)}`, {
headers: {
'User-Agent': 'Android712-AndroidPhone-8983-18-0-COMMENT-wifi',
},
@@ -65,7 +57,7 @@ export default {
return rawList.map(item => {
let data = {
id: item.id,
text: decodeName(item.content || '').split('\n'),
text: decodeName(item.content || ''),
images: item.images ? item.images.map(i => i.url) : [],
location: item.location,
time: item.addtime,
@@ -81,7 +73,7 @@ export default {
return item.pcontent
? {
id: item.id,
text: decodeName(item.pcontent).split('\n'),
text: decodeName(item.pcontent),
time: null,
userName: item.puser,
avatar: null,

View File

@@ -6,8 +6,10 @@ import pic from './pic'
import lyric from './lyric'
import hotSearch from './hotSearch'
import comment from './comment'
// import tipSearch from './tipSearch'
const kg = {
// tipSearch,
leaderboard,
songList,
musicSearch,

View File

@@ -1,14 +1,15 @@
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { formatSingerName } from '../utils'
let boardList = [{ id: 'kg__8888', name: '酷狗TOP500', bangid: '8888' }, { id: 'kg__6666', name: '酷狗飙升榜', bangid: '6666' }, { id: 'kg__37361', name: '酷狗雷达榜', bangid: '37361' }, { id: 'kg__23784', name: '网络红歌榜', bangid: '23784' }, { id: 'kg__24971', name: 'DJ热歌榜', bangid: '24971' }, { id: 'kg__35811', name: '会员专享热歌榜', bangid: '35811' }, { id: 'kg__31308', name: '华语新歌榜', bangid: '31308' }, { id: 'kg__31310', name: '欧美新歌榜', bangid: '31310' }, { id: 'kg__31311', name: '韩国新歌榜', bangid: '31311' }, { id: 'kg__31312', name: '日本新歌榜', bangid: '31312' }, { id: 'kg__31313', name: '粤语新歌榜', bangid: '31313' }, { id: 'kg__33162', name: 'ACG新歌榜', bangid: '33162' }, { id: 'kg__21101', name: '酷狗分享榜', bangid: '21101' }, { id: 'kg__30972', name: '腾讯音乐人原创榜', bangid: '30972' }, { id: 'kg__22603', name: '5sing音乐榜', bangid: '22603' }, { id: 'kg__33160', name: '电音热歌榜', bangid: '33160' }, { id: 'kg__21335', name: '繁星音乐榜', bangid: '21335' }, { id: 'kg__33161', name: '古风新歌榜', bangid: '33161' }, { id: 'kg__33163', name: '影视金曲榜', bangid: '33163' }, { id: 'kg__33166', name: '欧美金曲榜', bangid: '33166' }, { id: 'kg__33165', name: '粤语金曲榜', bangid: '33165' }, { id: 'kg__36107', name: '小语种热歌榜', bangid: '36107' }, { id: 'kg__4681', name: '美国BillBoard榜', bangid: '4681' }, { id: 'kg__4680', name: '英国单曲榜', bangid: '4680' }, { id: 'kg__4673', name: '日本公信榜', bangid: '4673' }, { id: 'kg__38623', name: '韩国Melon音乐榜', bangid: '38623' }, { id: 'kg__42807', name: 'joox本地热歌榜', bangid: '42807' }, { id: 'kg__42808', name: '台湾KKBOX风云榜', bangid: '42808' }]
let boardList = [{ id: 'kg__8888', name: 'TOP500', bangid: '8888' }, { id: 'kg__6666', name: '飙升榜', bangid: '6666' }, { id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' }, { id: 'kg__52144', name: '抖音热歌榜', bangid: '52144' }, { id: 'kg__52767', name: '快手热歌榜', bangid: '52767' }, { id: 'kg__24971', name: 'DJ热歌榜', bangid: '24971' }, { id: 'kg__23784', name: '网络红歌榜', bangid: '23784' }, { id: 'kg__44412', name: '说唱先锋榜', bangid: '44412' }, { id: 'kg__31308', name: '内地榜', bangid: '31308' }, { id: 'kg__33160', name: '电音榜', bangid: '33160' }, { id: 'kg__31313', name: '香港地区榜', bangid: '31313' }, { id: 'kg__51341', name: '民谣榜', bangid: '51341' }, { id: 'kg__54848', name: '台湾地区榜', bangid: '54848' }, { id: 'kg__31310', name: '欧美榜', bangid: '31310' }, { id: 'kg__33162', name: 'ACG新歌榜', bangid: '33162' }, { id: 'kg__31311', name: '韩国榜', bangid: '31311' }, { id: 'kg__31312', name: '日本榜', bangid: '31312' }, { id: 'kg__49225', name: '80后热歌榜', bangid: '49225' }, { id: 'kg__49223', name: '90后热歌榜', bangid: '49223' }, { id: 'kg__49224', name: '00后热歌榜', bangid: '49224' }, { id: 'kg__33165', name: '粤语金曲榜', bangid: '33165' }, { id: 'kg__33166', name: '欧美金曲榜', bangid: '33166' }, { id: 'kg__33163', name: '影视金曲榜', bangid: '33163' }, { id: 'kg__51340', name: '伤感榜', bangid: '51340' }, { id: 'kg__35811', name: '会员专享榜', bangid: '35811' }, { id: 'kg__37361', name: '雷达榜', bangid: '37361' }, { id: 'kg__21101', name: '分享榜', bangid: '21101' }, { id: 'kg__46910', name: '综艺新歌榜', bangid: '46910' }, { id: 'kg__30972', name: '酷狗音乐人原创榜', bangid: '30972' }, { id: 'kg__60170', name: '闽南语榜', bangid: '60170' }, { id: 'kg__65234', name: '儿歌榜', bangid: '65234' }, { id: 'kg__4681', name: '美国BillBoard榜', bangid: '4681' }, { id: 'kg__25028', name: 'Beatport电子舞曲榜', bangid: '25028' }, { id: 'kg__4680', name: '英国单曲榜', bangid: '4680' }, { id: 'kg__38623', name: '韩国Melon音乐榜', bangid: '38623' }, { id: 'kg__42807', name: 'joox本地热歌榜', bangid: '42807' }, { id: 'kg__36107', name: '小语种热歌榜', bangid: '36107' }, { id: 'kg__4673', name: '日本公信榜', bangid: '4673' }, { id: 'kg__46868', name: '日本SPACE SHOWER榜', bangid: '46868' }, { id: 'kg__42808', name: 'KKBOX风云榜', bangid: '42808' }, { id: 'kg__60171', name: '越南语榜', bangid: '60171' }, { id: 'kg__60172', name: '泰语榜', bangid: '60172' }, { id: 'kg__59895', name: 'R&B榜', bangid: '59895' }, { id: 'kg__59896', name: '摇滚榜', bangid: '59896' }, { id: 'kg__59897', name: '爵士榜', bangid: '59897' }, { id: 'kg__59898', name: '乡村音乐榜', bangid: '59898' }, { id: 'kg__59900', name: '纯音乐榜', bangid: '59900' }, { id: 'kg__59899', name: '古典榜', bangid: '59899' }, { id: 'kg__22603', name: '5sing音乐榜', bangid: '22603' }, { id: 'kg__21335', name: '繁星音乐榜', bangid: '21335' }, { id: 'kg__33161', name: '古风新歌榜', bangid: '33161' }]
export default {
listDetailLimit: 100,
list: [
{
id: 'kgtop500',
name: '酷狗TOP500',
name: 'TOP500',
bangid: '8888',
},
{
@@ -74,7 +75,7 @@ export default {
_requestBoardsObj: null,
getBoardsData() {
if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()
this._requestBoardsObj = httpFetch('http://mobilecdnbj.kugou.com/api/v3/rank/list?version=9108&plat=0&showtype=2&parentid=0&apiver=6&area_code=1&withsong=1')
this._requestBoardsObj = httpFetch('http://mobilecdnbj.kugou.com/api/v5/rank/list?version=9108&plat=0&showtype=2&parentid=0&apiver=6&area_code=1&withsong=1')
return this._requestBoardsObj.promise
},
getData(url) {
@@ -126,7 +127,7 @@ export default {
}
}
return {
singer: decodeName(this.getSinger(item.authors)),
singer: formatSingerName(item.authors, 'author_name'),
name: decodeName(item.songname),
albumName: decodeName(item.remark),
albumId: item.album_id,
@@ -168,7 +169,8 @@ export default {
// // console.log(response.body)
// if (response.statusCode !== 200 || response.body.errcode !== 0) return this.getBoards(retryNum)
// const list = this.filterBoardsData(response.body.data.info)
// // console.log(list)
// console.log(list)
// // console.log(JSON.stringify(list))
// this.list = list
// return {
// list,

View File

@@ -0,0 +1,104 @@
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { createHttpFetch } from './util'
const createGetMusicInfosTask = (hashs) => {
let data = {
appid: 1001,
clienttime: 639437935,
clientver: 9020,
fields: 'album_info,author_name,audio_info,ori_audio_name',
is_publish: '1',
key: '0475af1457cd3363c7b45b871e94428a',
mid: '21511157a05844bd085308bc76ef3342',
show_privilege: 1,
}
let list = hashs
let tasks = []
while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'http://kmr.service.kugou.com/v2/album_audio/audio'
return tasks.map(task => createHttpFetch(url, {
method: 'POST',
body: task,
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
},
}).then(data => data.map(s => s[0])))
}
export const filterMusicInfoList = (rawList) => {
// console.log(rawList)
let ids = new Set()
let 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') {
let 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') {
let 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') {
let 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') {
let 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.ori_audio_name),
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
}
export const getMusicInfos = async(hashs) => {
return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(hashs)).then(data => data.flat()))
}
export const getMusicInfo = async(hash) => {
return getMusicInfos([hash]).then(data => data[0])
}
export const getMusicInfosByList = (list) => {
return getMusicInfos(list.map(item => ({ hash: item.hash })))
}

View File

@@ -1,9 +1,7 @@
// import '../../polyfill/array.find'
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
// import { debug } from '../../utils/env'
// import { formatSinger } from './util'
import { formatSingerName } from '../utils'
export default {
limit: 30,
@@ -50,7 +48,7 @@ export default {
}
}
return {
singer: decodeName(rawData.SingerName),
singer: decodeName(formatSingerName(rawData.Singers, 'name')),
name: decodeName(rawData.SongName),
albumName: decodeName(rawData.AlbumName),
albumId: rawData.AlbumID,

View File

@@ -37,13 +37,12 @@ export default {
},
},
)
requestObj.promise = requestObj.promise.then(({ body }) => {
return requestObj.promise.then(({ body }) => {
if (body.error_code !== 0) return Promise.reject('图片获取失败')
let info = body.data[0].info
const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
if (!img) return Promise.reject('Pic get failed')
return img
})
return requestObj
},
}

View File

@@ -1,7 +1,7 @@
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate, dateFormat } from '../../index'
import { toMD5 } from '../utils'
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index'
import infSign from './vendors/infSign.min'
import { signatureParams } from './util'
const handleSignature = (id, page, limit) => new Promise((resolve, reject) => {
infSign({ appid: 1058, type: 0, module: 'playlist', page, pagesize: limit, specialid: id }, null, {
@@ -52,6 +52,113 @@ export default {
// https://www.kugou.com/yy/special/single/1067062.html
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/,
},
// async getGlobalSpecialId(specialId) {
// return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, {
// headers: {
// 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70',
// },
// }).promise.then(({ body }) => {
// // console.log(body)
// if (!body.data.global_specialid) Promise.reject(new Error('Failed to get global collection id.'))
// return body.data.global_specialid
// })
// },
// async getListInfoBySpecialId(special_id, retry = 0) {
// if (++retry > 2) throw new Error('failed')
// return httpFetch(`https://m.kugou.com/plist/list/${special_id}/?json=true`, {
// headers: {
// 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70',
// },
// follow_max: 2,
// }).promise.then(({ body }) => {
// // console.log(body)
// if (!body.info.list) return this.getListInfoBySpecialId(special_id, retry)
// let listinfo = body.info.list
// return {
// listInfo: {
// name: listinfo.specialname,
// image: listinfo.imgurl.replace('{size}', '150'),
// intro: listinfo.intro,
// author: listinfo.nickname,
// playcount: listinfo.playcount,
// total: listinfo.songcount,
// },
// globalSpecialId: listinfo.global_specialid,
// }
// })
// },
// async getSongListDetailByGlobalSpecialId(id, page, limit = 100, retry = 0) {
// if (++retry > 2) throw new Error('failed')
// console.log(id)
// const params = `specialid=0&need_sort=1&module=CloudMusic&clientver=11409&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=1&area_code=1&appid=1005`
// return httpFetch(`http://pubsongscdn.tx.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params)}`).promise.then(({ body }) => {
// // console.log(body)
// if (body.data?.info == null) return this.getSongListDetailByGlobalSpecialId(id, page, limit, retry)
// return body.data.info
// })
// },
parseHtmlDesc(html) {
const prefix = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
let index = html.indexOf(prefix)
if (index < 0) return null
const afterStr = html.substring(index + prefix.length)
index = afterStr.indexOf('</div>')
if (index < 0) return null
return decodeName(afterStr.substring(0, index))
},
async getListDetailBySpecialId(id, page, tryNum = 0) {
if (tryNum > 2) throw new Error('try max num')
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
let listData = body.match(this.regExps.listData)
let listInfo = body.match(this.regExps.listInfo)
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
let list = await this.getMusicInfos(JSON.parse(listData[1]))
// listData = this.filterData(JSON.parse(listData[1]))
let name
let pic
if (listInfo) {
name = listInfo[1]
pic = listInfo[2]
}
let desc = this.parseHtmlDesc(body)
return {
list,
page: 1,
limit: 10000,
total: list.length,
source: 'kg',
info: {
name,
img: pic,
desc,
// author: body.result.info.userinfo.username,
// play_count: formatPlayCount(body.result.listen_num),
},
}
// const globalSpecialId = await this.getGlobalSpecialId(id)
// const limit = 100
// const listData = await this.getSongListDetailByGlobalSpecialId(globalSpecialId, page, limit)
// if (!Array.isArray(listData))
// return this.getUserListDetail2(globalSpecialId)
// return {
// list: this.filterDatav9(listData),
// page,
// limit,
// total: listInfo.total,
// source: 'kg',
// info: {
// name: listInfo.name,
// img: listInfo.image,
// desc: listInfo.intro,
// author: listInfo.author,
// play_count: formatPlayCount(listInfo.playcount),
// },
// }
},
getInfoUrl(tagId) {
return tagId
? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}`
@@ -65,15 +172,6 @@ export default {
return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html`
},
/**
* 格式化播放数量
* @param {*} num
*/
formatPlayCount(num) {
if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿'
if (num > 10000) return parseInt(num / 1000) / 10 + '万'
return num
},
filterInfoHotTag(rawData) {
const result = []
if (rawData.status !== 1) return result
@@ -145,7 +243,7 @@ export default {
},
filterList(rawData) {
return rawData.map(item => ({
play_count: item.total_play_count || this.formatPlayCount(item.play_count),
play_count: item.total_play_count || formatPlayCount(item.play_count),
id: 'id_' + item.specialid,
author: item.nickname,
name: item.specialname,
@@ -210,6 +308,15 @@ export default {
},
}).then(data => data.map(s => s[0])))
},
async getMusicInfos(list) {
return this.filterData2(
await Promise.all(
this.createTask(
this.deDuplication(list)
.map(item => ({ hash: item.hash })),
))
.then(([...datas]) => datas.flat()))
},
async getUserListDetailByCode(id) {
const songInfo = await this.createHttp('http://t.kugou.com/command/', {
@@ -222,8 +329,17 @@ export default {
body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id },
})
// console.log(songInfo)
// type 1单曲2歌单3电台4酷狗码5别人的播放队列
let songList
let info = songInfo.info
switch (info.type) {
case 2:
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
break
default:
break
}
if (info.global_collection_id) return this.getUserListDetail2(info.global_collection_id)
if (info.userid != null) {
songList = await this.createHttp('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', {
@@ -237,19 +353,19 @@ export default {
})
// console.log(songList)
}
let result = await Promise.all(this.createTask((songList || songInfo.list).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
let list = await this.getMusicInfos(songList || songInfo.list)
return {
list: this.filterData2(result) || [],
list,
page: 1,
limit: info.count,
total: info.count,
total: list.length,
source: 'kg',
info: {
name: info.name,
img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img,
// desc: body.result.info.list_desc,
author: info.username,
// play_count: this.formatPlayCount(info.count),
// play_count: formatPlayCount(info.count),
},
}
},
@@ -264,20 +380,20 @@ export default {
if (songInfo.global_collection_id) return this.getUserListDetail2(songInfo.global_collection_id)
else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain))
}
let result = await Promise.all(this.createTask(songInfo.list.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
let list = await this.getMusicInfos(songInfo.list)
// console.log(info, songInfo)
return {
list: this.filterData2(result) || [],
list,
page: 1,
limit: this.listDetailLimit,
total: songInfo.count,
total: list.length,
source: 'kg',
info: {
name: songInfo.info.name,
img: songInfo.info.img,
// desc: body.result.info.list_desc,
author: songInfo.info.username,
// play_count: this.formatPlayCount(info.count),
// play_count: formatPlayCount(info.count),
},
}
},
@@ -308,20 +424,20 @@ export default {
}).then(data => data.list.info))
}
let result = await Promise.all(tasks).then(([...datas]) => datas.flat())
result = await Promise.all(this.createTask(this.deDuplication(result).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
result = await this.getMusicInfos(result)
// console.log(result)
return {
list: this.filterData2(result) || [],
list: result,
page,
limit: this.listDetailLimit,
total: listInfo.count,
total: result.length,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: this.formatPlayCount(listInfo.count),
// play_count: formatPlayCount(listInfo.count),
},
}
},
@@ -332,7 +448,8 @@ export default {
const limit = total > 300 ? 300 : total
total -= limit
page += 1
tasks.push(this.createHttp('https://mobiles.kugou.com/api/v5/special/song_v2?appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163263991clientver=20000dfid=-global_specialid=' + id + 'mid=1586163263991page=' + page + 'pagesize=' + limit + 'plat=0specialid=0srcappid=2919uuid=1586163263991version=8000NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-'
tasks.push(this.createHttp(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 5)}`, {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
@@ -347,7 +464,8 @@ export default {
async getUserListDetail2(global_collection_id) {
let id = global_collection_id
if (id.length > 1000) throw new Error('get list error')
let info = await this.createHttp('https://mobiles.kugou.com/api/v5/special/info_v2?appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163242519clientver=20000dfid=-format=jsonpglobal_specialid=' + id + 'mid=1586163242519specialid=0srcappid=2919uuid=1586163242519NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
let info = await this.createHttp(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 5)}`, {
headers: {
mid: '1586163242519',
Referer: 'https://m3ws.kugou.com/share/index.php',
@@ -357,20 +475,20 @@ export default {
},
})
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let result = await Promise.all(this.createTask(this.deDuplication(songInfo).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
let list = await this.getMusicInfos(songInfo)
// console.log(info, songInfo, list)
return {
list: this.filterData2(result) || [],
list,
page: 1,
limit: this.listDetailLimit,
total: info.songcount,
total: list.length,
source: 'kg',
info: {
name: info.specialname,
img: info.imgurl && info.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
desc: info.intro,
author: info.nickname,
// play_count: this.formatPlayCount(info.count),
play_count: formatPlayCount(info.playcount),
},
}
},
@@ -399,9 +517,9 @@ export default {
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
if (result) result = JSON.parse(result[1])
this.cache.set(chain, result)
result = await Promise.all(this.createTask(result.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
result = await this.getMusicInfos(result)
// console.log(info, songInfo)
return this.filterData2(result)
return result
},
async getUserListDetail4(songInfo, chain, page) {
@@ -414,14 +532,14 @@ export default {
list: list || [],
page,
limit,
total: listInfo.songcount,
total: list.length ?? 0,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: this.formatPlayCount(info.count),
// play_count: formatPlayCount(info.count),
},
}
},
@@ -435,14 +553,14 @@ export default {
list: list || [],
page: 1,
limit: this.listDetailLimit,
total: listInfo.songcount,
total: list.length ?? 0,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: this.formatPlayCount(info.count),
// play_count: formatPlayCount(info.count),
},
}
},
@@ -458,9 +576,9 @@ export default {
})
// console.log(info)
let result = await Promise.all(this.createTask(info.info.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
let result = await this.getMusicInfos(info.info)
// console.log(info, songInfo)
return this.filterData2(result)
return result
},
async getUserListDetail(link, page, retryNum = 0) {
@@ -511,9 +629,7 @@ export default {
return this.getUserListDetailByLink(body, link)
},
getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (tryNum > 2) return Promise.reject(new Error('try max num'))
async getListDetail(id, page) { // 获取歌曲列表内的音乐
id = id.toString()
if (id.includes('special/single/')) {
id = id.replace(this.regExps.listDetailLink, '$1')
@@ -525,36 +641,9 @@ export default {
} else if (id.startsWith('id_')) {
id = id.replace('id_', '')
}
// if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id))
return requestObj_listDetail.promise.then(({ body }) => {
let listData = body.match(this.regExps.listData)
let listInfo = body.match(this.regExps.listInfo)
if (!listData) return this.getListDetail(id, page, ++tryNum)
listData = this.filterData(JSON.parse(listData[1]))
let name
let pic
if (listInfo) {
name = listInfo[1]
pic = listInfo[2]
}
return {
list: listData,
page: 1,
limit: 10000,
total: listData.length,
source: 'kg',
info: {
name,
img: pic,
// desc: body.result.info.list_desc,
// author: body.result.info.userinfo.username,
// play_count: this.formatPlayCount(body.result.listen_num),
},
}
})
return this.getListDetailBySpecialId(id, page)
},
filterData(rawList) {
// console.log(rawList)
@@ -610,6 +699,68 @@ export default {
}
})
},
// getSinger(singers) {
// let arr = []
// singers?.forEach(singer => {
// arr.push(singer.name)
// })
// return arr.join('、')
// },
// v9 API
// filterDatav9(rawList) {
// console.log(rawList)
// return rawList.map(item => {
// const types = []
// const _types = {}
// item.relate_goods.forEach(qualityObj => {
// if (qualityObj.level === 2) {
// let size = sizeFormate(qualityObj.size)
// types.push({ type: '128k', size, hash: qualityObj.hash })
// _types['128k'] = {
// size,
// hash: qualityObj.hash,
// }
// } else if (qualityObj.level === 4) {
// let size = sizeFormate(qualityObj.size)
// types.push({ type: '320k', size, hash: qualityObj.hash })
// _types['320k'] = {
// size,
// hash: qualityObj.hash,
// }
// } else if (qualityObj.level === 5) {
// let size = sizeFormate(qualityObj.size)
// types.push({ type: 'flac', size, hash: qualityObj.hash })
// _types.flac = {
// size,
// hash: qualityObj.hash,
// }
// } else if (qualityObj.level === 6) {
// let size = sizeFormate(qualityObj.size)
// types.push({ type: 'flac24bit', size, hash: qualityObj.hash })
// _types.flac24bit = {
// size,
// hash: qualityObj.hash,
// }
// }
// })
// const nameInfo = item.name.split(' - ')
// return {
// singer: this.getSinger(item.singerinfo),
// name: decodeName((nameInfo[1] ?? nameInfo[0]).trim()),
// albumName: decodeName(item.albuminfo.name),
// albumId: item.albuminfo.id,
// songmid: item.audio_id,
// source: 'kg',
// interval: formatPlayTime(item.timelen / 1000),
// img: null,
// lrc: null,
// hash: item.hash,
// types,
// _types,
// typeUrl: {},
// }
// })
// },
// hash list filter
filterData2(rawList) {
@@ -646,6 +797,14 @@ export default {
hash: item.audio_info.hash_flac,
}
}
if (item.audio_info.filesize_high !== '0') {
let 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.ori_audio_name),
@@ -737,7 +896,7 @@ export default {
return {
list: body.data.info.map(item => {
return {
play_count: this.formatPlayCount(item.playcount),
play_count: formatPlayCount(item.playcount),
id: 'id_' + item.specialid,
author: item.nickname,
name: item.specialname,

View File

@@ -0,0 +1,112 @@
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { signatureParams, createHttpFetch } from './util'
import { formatSingerName } from '../../utils'
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page, limit) {
const sign = signatureParams(`userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&keyword=${str}&dfid=-&clientver=11409&platform=AndroidFilter&tag=`, 3)
return createHttpFetch(`https://gateway.kugou.com/complexsearch/v3/search/song?userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&dfid=-&clientver=11409&platform=AndroidFilter&tag=&keyword=${encodeURIComponent(str)}&signature=${sign}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
referer: 'https://kugou.com',
},
}).then(body => body)
},
filterList(raw) {
let ids = new Set()
const list = []
raw.forEach(item => {
if (ids.has(item.Audioid)) return
ids.add(item.Audioid)
const types = []
const _types = {}
if (item.FileSize !== 0) {
let size = sizeFormate(item.FileSize)
types.push({ type: '128k', size, hash: item.FileHash })
_types['128k'] = {
size,
hash: item.FileHash,
}
}
if (item.HQ != undefined) {
let size = sizeFormate(item.HQ.FileSize)
types.push({ type: '320k', size, hash: item.HQ.Hash })
_types['320k'] = {
size,
hash: item.HQ.Hash,
}
}
if (item.SQ != undefined) {
let size = sizeFormate(item.SQ.FileSize)
types.push({ type: 'flac', size, hash: item.SQ.Hash })
_types.flac = {
size,
hash: item.SQ.Hash,
}
}
if (item.Res != undefined) {
let size = sizeFormate(item.Res.FileSize)
types.push({ type: 'flac24bit', size, hash: item.Res.Hash })
_types.flac24bit = {
size,
hash: item.Res.Hash,
}
}
list.push({
singer: decodeName(formatSingerName(item.Singers)),
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
},
handleResult(rawData) {
const rawList = []
rawData.forEach(item => {
rawList.push(item)
item.Grp.forEach(e => rawList.push(e))
})
return this.filterList(rawList)
},
search(str, page = 1, limit, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit == null) limit = this.limit
return this.musicSearch(str, page, limit).then(data => {
let list = this.handleResult(data.lists)
if (!list) return this.search(str, page, limit, retryNum)
this.total = data.total
this.page = page
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
limit,
total: this.total,
source: 'kg',
})
})
},
}

View File

@@ -0,0 +1,794 @@
import { httpFetch } from '../../../request'
import { formatSingerName } from '../../utils'
import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../../index'
import { signatureParams, createHttpFetch } from './../util'
import { getMusicInfosByList } from '../musicInfo'
import album from '../album'
export default {
_requestObj_tags: null,
_requestObj_listInfo: null,
_requestObj_list: null,
_requestObj_listRecommend: null,
listDetailLimit: 10000,
currentTagInfo: {
id: undefined,
info: undefined,
},
sortList: [
{
name: '推荐',
id: '5',
},
{
name: '最热',
id: '6',
},
{
name: '最新',
id: '7',
},
{
name: '热藏',
id: '3',
},
{
name: '飙升',
id: '8',
},
],
cache: new Map(),
collectionIdListInfoCache: new Map(),
regExps: {
listData: /global\.data = (\[.+\]);/,
listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/,
// https://www.kugou.com/yy/special/single/1067062.html
listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/,
},
/**
* 获取歌曲列表内的音乐
* @param {*} id
* @param {*} page
*/
async getListDetail(id, page) {
id = id.toString()
if (id.includes('special/single/')) id = id.replace(this.regExps.listDetailLink, '$1')
// fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1
if (/https?:/.test(id)) {
if (id.includes('#')) id = id.replace(/#.*$/, '')
if (id.includes('global_collection_id')) return this.getUserListDetailByCollectionId(id.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
if (id.includes('chain=')) return this.getUserListDetail3(id.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
if (id.includes('.html')) {
if (id.includes('zlist.html')) {
id = id.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
if (id.includes('pagesize')) {
id = id.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)
} else {
id += `&pagesize=${this.listDetailLimit}&page=${page}`
}
} else if (!id.includes('song.html')) return this.getUserListDetail3(id.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page)
}
return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page)
}
if (/^\d+$/.test(id)) return this.getUserListDetailByCode(id, page)
if (id.startsWith('gid_')) return this.getUserListDetailByCollectionId(id.replace('gid_', ''), page)
if (id.startsWith('id_')) return this.getUserListDetailBySpecialId(id.replace('id_', ''), page)
return new Error('Failed.')
},
/**
* 获取SpecialId歌单
* @param {*} id
*/
async getUserListDetailBySpecialId(id, page, tryNum = 0) {
if (tryNum > 2) throw new Error('try max num')
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
let listData = body.match(this.regExps.listData)
let listInfo = body.match(this.regExps.listInfo)
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
let list = await getMusicInfosByList(JSON.parse(listData[1]))
let name
let pic
if (listInfo) {
name = listInfo[1]
pic = listInfo[2]
}
let desc = this.parseHtmlDesc(body)
return {
list,
page: 1,
limit: 10000,
total: list.length,
source: 'kg',
info: {
name,
img: pic,
desc,
// author: body.result.info.userinfo.username,
// play_count: formatPlayCount(body.result.listen_num),
},
}
},
parseHtmlDesc(html) {
const prefix = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
let index = html.indexOf(prefix)
if (index < 0) return null
const afterStr = html.substring(index + prefix.length)
index = afterStr.indexOf('</div>')
if (index < 0) return null
return decodeName(afterStr.substring(0, index))
},
/**
* 使用SpecialId获取CollectionId
* @param {*} specialId
*/
async getCollectionIdBySpecialId(specialId) {
return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70',
},
}).promise.then(({ body }) => {
// console.log('getCollectionIdBySpecialId', body)
if (!body.data.global_specialid) return Promise.reject(new Error('Failed to get global collection id.'))
return body.data.global_specialid
})
},
/**
* 获取歌单URL
* @param {*} sortId
* @param {*} tagId
* @param {*} page
*/
getSongListUrl(sortId, tagId, page) {
if (tagId == null) tagId = ''
return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}`
},
getInfoUrl(tagId) {
return tagId
? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}`
: 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&'
},
getSongListDetailUrl(id) {
return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html`
},
filterInfoHotTag(rawData) {
const result = []
if (rawData.status !== 1) return result
for (const key of Object.keys(rawData.data)) {
let tag = rawData.data[key]
result.push({
id: tag.special_id,
name: tag.special_name,
source: 'kg',
})
}
return result
},
filterTagInfo(rawData) {
const result = []
for (const name of Object.keys(rawData)) {
result.push({
name,
list: rawData[name].data.map(tag => ({
parent_id: tag.parent_id,
parent_name: tag.pname,
id: tag.id,
name: tag.name,
source: 'kg',
})),
})
}
return result
},
filterSongList(rawData) {
return rawData.map(item => ({
play_count: item.total_play_count || formatPlayCount(item.play_count),
id: 'id_' + item.specialid,
author: item.nickname,
name: item.specialname,
time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'),
img: item.img || item.imgurl,
total: item.songcount,
grade: item.grade,
desc: item.intro,
source: 'kg',
}))
},
getSongList(sortId, tagId, page, tryNum = 0) {
if (this._requestObj_list) this._requestObj_list.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_list = httpFetch(
this.getSongListUrl(sortId, tagId, page),
)
return this._requestObj_list.promise.then(({ body }) => {
if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum)
return this.filterSongList(body.special_db)
})
},
getSongListRecommend(tryNum = 0) {
if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_listRecommend = httpFetch(
'http://everydayrec.service.kugou.com/guess_special_recommend',
{
method: 'post',
headers: {
'User-Agent': 'KuGou2012-8275-web_browser_event_handler',
},
body: {
appid: 1001,
clienttime: 1566798337219,
clientver: 8275,
key: 'f1f93580115bb106680d2375f8032d96',
mid: '21511157a05844bd085308bc76ef3343',
platform: 'pc',
userid: '262643156',
return_min: 6,
return_max: 15,
},
},
)
return this._requestObj_listRecommend.promise.then(({ body }) => {
if (body.status !== 1) return this.getSongListRecommend(++tryNum)
return this.filterSongList(body.data.special_list)
})
},
/**
* 通过CollectionId获取歌单详情
* @param {*} id
*/
async getUserListInfoByCollectionId(id) {
if (!id || id.length > 1000) return Promise.reject(new Error('get list error'))
if (this.collectionIdListInfoCache.has(id)) return this.collectionIdListInfoCache.get(id)
const params = `appid=1058&specialid=0&global_specialid=${id}&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-`
return createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 5)}`, {
headers: {
mid: '1586163242519',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163242519',
},
}).then(body => {
let info = {
type: body.type,
userName: body.nickname,
userAvatar: body.user_avatar,
imageUrl: body.imgurl,
desc: body.intro,
name: body.specialname,
globalSpecialid: body.global_specialid,
total: body.songcount,
playCount: body.playcount,
}
this.collectionIdListInfoCache.set(id, info)
return info
})
},
/**
* 通过SpecialId获取歌单
* @param {*} id
*/
// async getUserListDetailBySpecialId(id, page = 1, limit = 300) {
// if (!id || id.length > 1000) return Promise.reject(new Error('get list error.'))
// const listInfo = await this.getListInfoBySpecialId(id)
// const params = `specialid=${id}&need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&userid=0&page=${page}&type=0&area_code=1&appid=1005`
// return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 2)}`, {
// headers: {
// 'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi',
// },
// }).then(body => {
// if (!body.info) return Promise.reject(new Error('Get list failed.'))
// const songList = this.filterListByCollectionId(body.info)
// return {
// list: songList || [],
// page,
// limit,
// total: body.count,
// source: 'kg',
// info: {
// name: listInfo.name,
// img: listInfo.image,
// desc: listInfo.desc,
// // author: listInfo.userName,
// // play_count: formatPlayCount(listInfo.playCount),
// },
// }
// })
// },
/**
* 通过CollectionId获取歌单
* @param {*} id
*/
async getUserListDetailByCollectionId(id, page = 1, limit = 300) {
if (!id || id.length > 1000) return Promise.reject(new Error('ID error.'))
const listInfo = await this.getUserListInfoByCollectionId(id)
const params = `need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=0&area_code=1&appid=1005`
return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 2)}`, {
headers: {
'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi',
},
}).then(body => {
if (!body.info) return Promise.reject(new Error('Get list failed.'))
const songList = this.filterListByCollectionId(body.info)
return {
list: songList || [],
page,
limit,
total: listInfo.total,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.imageUrl && listInfo.imageUrl.replace('{size}', 240),
desc: listInfo.desc,
author: listInfo.userName,
play_count: formatPlayCount(listInfo.playCount),
},
}
})
},
/**
* 过滤GlobalSpecialId歌单数据
* @param {*} rawData
*/
filterListByCollectionId(rawData) {
let ids = new Set()
let list = []
rawData.forEach(item => {
if (!item) return
if (ids.has(item.hash)) return
ids.add(item.hash)
const types = []
const _types = {}
item.relate_goods.forEach(data => {
let size = sizeFormate(data.size)
switch (data.level) {
case 2:
types.push({ type: '128k', size, hash: data.hash })
_types['128k'] = {
size,
hash: data.hash,
}
break
case 4:
types.push({ type: '320k', size, hash: data.hash })
_types['320k'] = {
size,
hash: data.hash,
}
break
case 5:
types.push({ type: 'flac', size, hash: data.hash })
_types.flac = {
size,
hash: data.hash,
}
break
case 6:
types.push({ type: 'flac24bit', size, hash: data.hash })
_types.flac24bit = {
size,
hash: data.hash,
}
break
}
})
list.push({
singer: formatSingerName(item.singerinfo, 'name') || decodeName(item.name).split(' - ')[0].replace(/&/g, '、'),
name: decodeName(item.name).split(' - ')[1],
albumName: decodeName(item.albuminfo.name),
albumId: item.albuminfo.id,
songmid: item.audio_id,
source: 'kg',
interval: formatPlayTime(parseInt(item.timelen) / 1000),
img: null,
lrc: null,
hash: item.hash,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
/**
* 通过酷狗码获取歌单
* @param {*} id
* @param {*} page
*/
async getUserListDetailByCode(id, page = 1) {
// type 1单曲2歌单3电台4酷狗码5别人的播放队列
const codeData = await createHttpFetch('http://t.kugou.com/command/', {
method: 'POST',
headers: {
'KG-RC': 1,
'KG-THash': 'network_super_call.cpp:3676261689:379',
'User-Agent': '',
},
body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id },
})
if (!codeData) return Promise.reject(new Error('Get list failed.'))
const codeInfo = codeData.info
switch (codeInfo.type) {
case 2:
if (!codeInfo.global_collection_id) return this.getUserListDetailBySpecialId(codeInfo.id, page)
break
case 3:
return album.getAlbumDetail(codeInfo.id, page)
}
if (codeInfo.global_collection_id) return this.getUserListDetailByCollectionId(codeInfo.global_collection_id, page)
if (codeInfo.userid != null) {
const songList = await createHttpFetch('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', {
method: 'POST',
headers: {
'KG-RC': 1,
'KG-THash': 'network_super_call.cpp:3676261689:379',
'User-Agent': '',
},
body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: { id: codeInfo.id, type: 3, userid: codeInfo.userid, collect_type: 0, page: 1, pagesize: codeInfo.count } },
})
// console.log(songList)
let list = await getMusicInfosByList(songList || codeInfo.list)
return {
list,
page: 1,
limit: codeInfo.count,
total: list.length,
source: 'kg',
info: {
name: codeInfo.name,
img: (codeInfo.img_size && codeInfo.img_size.replace('{size}', 240)) || codeInfo.img,
// desc: body.result.info.list_desc,
author: codeInfo.username,
// play_count: formatPlayCount(info.count),
},
}
}
},
async getUserListDetail3(chain, page) {
const songInfo = await createHttpFetch(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
},
})
if (!songInfo.list) {
if (songInfo.global_collection_id) return this.getUserListDetailByCollectionId(songInfo.global_collection_id, page)
else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain))
}
let list = await getMusicInfosByList(songInfo.list)
// console.log(info, songInfo)
return {
list,
page: 1,
limit: this.listDetailLimit,
total: list.length,
source: 'kg',
info: {
name: songInfo.info.name,
img: songInfo.info.img,
// desc: body.result.info.list_desc,
author: songInfo.info.username,
// play_count: formatPlayCount(info.count),
},
}
},
async getUserListDetailByLink({ info }, link) {
let listInfo = info['0']
let total = listInfo.count
let tasks = []
let page = 0
while (total) {
const limit = total > 90 ? 90 : total
total -= limit
page += 1
tasks.push(createHttpFetch(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
Referer: link,
},
}).then(data => data.list.info))
}
let result = await Promise.all(tasks).then(([...datas]) => datas.flat())
result = await getMusicInfosByList(result)
// console.log(result)
return {
list: result,
page,
limit: this.listDetailLimit,
total: result.length,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: formatPlayCount(listInfo.count),
},
}
},
createGetListDetail2Task(id, total) {
let tasks = []
let page = 0
while (total) {
const limit = total > 300 ? 300 : total
total -= limit
page += 1
const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-'
tasks.push(createHttpFetch(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 5)}`, {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163263991',
},
}).then(data => data.info))
}
return Promise.all(tasks).then(([...datas]) => datas.flat())
},
async getUserListDetail2(global_collection_id) {
let id = global_collection_id
if (id.length > 1000) throw new Error('get list error')
const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
let info = await createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 5)}`, {
headers: {
mid: '1586163242519',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163242519',
},
})
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let list = await getMusicInfosByList(songInfo)
// console.log(info, songInfo, list)
return {
list,
page: 1,
limit: this.listDetailLimit,
total: list.length,
source: 'kg',
info: {
name: info.specialname,
img: info.imgurl && info.imgurl.replace('{size}', 240),
desc: info.intro,
author: info.nickname,
play_count: formatPlayCount(info.playcount),
},
}
},
async getListInfoByChain(chain) {
if (this.cache.has(chain)) return this.cache.get(chain)
const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
},
}).promise
// console.log(body)
let result = body.match(/var\sphpParam\s=\s({.+?});/)
if (result) result = JSON.parse(result[1])
this.cache.set(chain, result)
return result
},
async getUserListDetailByPcChain(chain) {
let key = `${chain}_pc_list`
if (this.cache.has(key)) return this.cache.get(key)
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
},
}).promise
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
if (result) result = JSON.parse(result[1])
this.cache.set(chain, result)
result = await getMusicInfosByList(result)
// console.log(info, songInfo)
return result
},
async getUserListDetail4(songInfo, chain, page) {
const limit = 100
const [listInfo, list] = await Promise.all([
this.getListInfoByChain(chain),
this.getUserListDetailBySpecialId(songInfo.id, page, limit),
])
return {
list: list || [],
page,
limit,
total: list.length ?? 0,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: formatPlayCount(info.count),
},
}
},
async getUserListDetail5(chain) {
const [listInfo, list] = await Promise.all([
this.getListInfoByChain(chain),
this.getUserListDetailByPcChain(chain),
])
return {
list: list || [],
page: 1,
limit: this.listDetailLimit,
total: list.length ?? 0,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: formatPlayCount(info.count),
},
}
},
async getUserListDetail(link, page, retryNum = 0) {
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
const requestLink = httpFetch(link, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
Referer: link,
},
follow_max: 2,
})
const { headers: { location }, statusCode, body } = await requestLink.promise
// console.log(body, location, statusCode)
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
if (typeof body == 'string') {
if (body.includes('"global_collection_id":')) return this.getUserListDetailByCollectionId(body.replace(/^[\s\S]+?"global_collection_id":"(\w+)"[\s\S]+?$/, '$1'), page)
if (body.includes('"albumid":')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"albumid":(\w+)[\s\S]+?$/, '$1'), page)
if (body.includes('"album_id":') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"album_id":(\w+)[\s\S]+?$/, '$1'), page)
if (body.includes('list_id = "') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?list_id = "(\w+)"[\s\S]+?$/, '$1'), page)
}
if (location) {
// 概念版分享链接 https://t1.kugou.com/xxx
if (location.includes('global_specialid')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_specialid=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
if (location.includes('global_collection_id')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
if (location.includes('.html')) {
if (location.includes('zlist.html')) {
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
if (link.includes('pagesize')) {
link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)
} else {
link += `&pagesize=${this.listDetailLimit}&page=${page}`
}
return this.getUserListDetail(link, page, ++retryNum)
} else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page)
}
return this.getUserListDetail(location, page, ++retryNum)
}
if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum)
return this.getUserListDetailByLink(body, link)
},
// 获取列表信息
getListInfo(tagId, tryNum = 0) {
if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId))
return this._requestObj_listInfo.promise.then(({ body }) => {
if (body.status !== 1) return this.getListInfo(tagId, ++tryNum)
return {
limit: body.data.params.pagesize,
page: body.data.params.p,
total: body.data.params.total,
source: 'kg',
}
})
},
// 获取列表数据
getList(sortId, tagId, page) {
let tasks = [this.getSongList(sortId, tagId, page)]
tasks.push(
this.currentTagInfo.id === tagId
? Promise.resolve(this.currentTagInfo.info)
: this.getListInfo(tagId).then(info => {
this.currentTagInfo.id = tagId
this.currentTagInfo.info = Object.assign({}, info)
return info
}),
)
if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别,则顺便获取推荐列表
return Promise.all(tasks).then(([list, info, recommendList]) => {
if (recommendList) list.unshift(...recommendList)
return {
list,
...info,
}
})
},
// 获取标签
getTags(tryNum = 0) {
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_tags = httpFetch(this.getInfoUrl())
return this._requestObj_tags.promise.then(({ body }) => {
if (body.status !== 1) return this.getTags(++tryNum)
return {
hotTag: this.filterInfoHotTag(body.data.hotTag),
tags: this.filterTagInfo(body.data.tagids),
source: 'kg',
}
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') {
if (/^https?:\/\//.test(id)) return id
id = id.replace('id_', '')
}
return `https://www.kugou.com/yy/special/single/${id}.html`
},
search(text, page, limit = 20) {
const params = `userid=1384394652&req_custom=1&appid=1005&req_multi=1&version=11589&page=${page}&filter=0&pagesize=${limit}&order=0&clienttime=1681779443&iscorrection=1&searchsong=0&keyword=${text}&mid=288799920684148686226285199951543865551&dfid=3eSBsO1u97EY1zeIZd40hH4p&clientver=11589&platform=AndroidFilter`
const url = encodeURI(`http://complexsearchretry.kugou.com/v1/search/special?${params}&signature=${signatureParams(params, 1)}`)
return createHttpFetch(url).then(body => {
// console.log(body)
return {
list: body.lists.map(item => {
return {
play_count: formatPlayCount(item.total_play_count),
id: item.gid ? `gid_${item.gid}` : `id_${item.specialid}`,
author: item.nickname,
name: item.specialname,
time: dateFormat(item.publish_time, 'Y-M-D'),
img: item.img,
grade: item.grade,
desc: item.intro,
total: item.song_count,
source: 'kg',
}
}),
limit,
total: body.total,
source: 'kg',
}
})
// 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://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5
// http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2
},
}
// getList
// getTags
// getListDetail

View File

@@ -0,0 +1,25 @@
import { createHttpFetch } from './util'
export default {
requestObj: null,
cancelTipSearch() {
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
},
tipSearchBySong(str) {
this.cancelTipSearch()
this.requestObj = createHttpFetch(`https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword=${encodeURIComponent(str)}`, {
headers: {
referer: 'https://www.kugou.com/',
},
})
return this.requestObj.then(body => {
return body[0].RecordDatas
})
},
handleResult(rawData) {
return rawData.map(info => info.HintInfo)
},
async search(str) {
return this.tipSearchBySong(str).then(result => this.handleResult(result))
},
}

View File

@@ -1,4 +1,6 @@
import { inflate } from 'zlib'
import { toMD5 } from '../utils'
import { httpFetch } from '../../request'
// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784
const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary')
@@ -17,3 +19,44 @@ export const decodeLyric = str => new Promise((resolve, reject) => {
// s.content[0].lyricContent.forEach(([str]) => {
// console.log(str)
// })
/**
* 签名
* @param {*} params
* @param {*} apiver
*/
export const signatureParams = (params, apiver = 9) => {
let keyparam = 'OIlwieks28dk2k092lksi2UIkp'
if (apiver === 5) keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'
let param_list = params.split('&')
param_list.sort()
let sign_params = `${keyparam}${param_list.join('')}${keyparam}`
return toMD5(sign_params)
}
/**
* 创建一个适用于KG的Http请求
* @param {*} url
* @param {*} options
* @param {*} retryNum
*/
export const createHttpFetch = async(url, options, retryNum = 0) => {
if (retryNum > 2) throw new Error('try max num')
let result
try {
result = await httpFetch(url, options).promise
} catch (err) {
console.log(err)
return createHttpFetch(url, options, ++retryNum)
}
// console.log(result.statusCode, result.body)
if (result.statusCode !== 200 ||
(
result.body.error_code ??
result.body.errcode ??
result.body.err_code) != 0
) return createHttpFetch(url, options, ++retryNum)
if (result.body.data) return result.body.data
if (Array.isArray(result.body.info)) return result.body
return result.body.info
}

View File

@@ -1,4 +1,5 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
@@ -12,7 +13,11 @@ const api_temp = {
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(body.msg))
}
})
return requestObj
},

View File

@@ -24,7 +24,11 @@ const api_test = {
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},

View File

@@ -35,7 +35,7 @@ export default {
return rawList.map(item => {
let data = {
id: item.id,
text: item.msg.split('\n'),
text: item.msg,
time: item.time,
timeStr: dateFormat2(new Date(item.time).getTime()),
userName: decodeURIComponent(item.u_name),
@@ -48,7 +48,7 @@ export default {
? {
id: item.id,
rootId: item.reply.id,
text: item.reply.msg.split('\n'),
text: item.reply.msg,
time: item.reply.time,
timeStr: dateFormat2(new Date(item.reply.time).getTime()),
userName: decodeURIComponent(item.reply.u_name),

Some files were not shown because too many files have changed in this diff Show More