Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21619e72dc | ||
|
|
33742bca6b | ||
|
|
e0575fc179 | ||
|
|
97fe5fa496 | ||
|
|
97533cd4aa | ||
|
|
a7672168c6 | ||
|
|
5c46ca648d | ||
|
|
38c7bc0d0d | ||
|
|
cb2798cba9 | ||
|
|
f0342b852b | ||
|
|
97346617a5 | ||
|
|
ecdc018fa1 | ||
|
|
cbcbe3e8c8 | ||
|
|
b12c5ed12b | ||
|
|
96921b255f | ||
|
|
2df1077ee4 | ||
|
|
4937ddb09c | ||
|
|
4b00a06a88 | ||
|
|
be48e78a6f | ||
|
|
b7a3663078 | ||
|
|
9c364b482e | ||
|
|
355c5e584b | ||
|
|
f956fd93d5 | ||
|
|
69e285d3ca | ||
|
|
728fed2ed3 | ||
|
|
bc0d2264a3 | ||
|
|
ba8a6bf76c | ||
|
|
2cf84a796d | ||
|
|
ba62ae27af | ||
|
|
e35895d752 | ||
|
|
cc192cab44 | ||
|
|
e36792b76a | ||
|
|
61231ab99e | ||
|
|
ae2ee0e61e | ||
|
|
a339f0fd0c | ||
|
|
b9c3c3506e | ||
|
|
44d96264fb | ||
|
|
19eab0418a | ||
|
|
0695bbaa70 | ||
|
|
8d6b207833 | ||
|
|
3371a42822 | ||
|
|
fe712318b8 | ||
|
|
593d8327b2 | ||
|
|
d5c54b770f | ||
|
|
32a9a21be1 | ||
|
|
e0735f8928 | ||
|
|
c6dc962134 | ||
|
|
e068103e4d | ||
|
|
6eb5ade9ea | ||
|
|
c40168cacd |
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"corejs": "3",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
// [
|
||||
// "minify",
|
||||
// {
|
||||
// "builtIns": false,
|
||||
// "evaluate": false,
|
||||
// "mangle": false
|
||||
// }
|
||||
// ]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-modules-umd",
|
||||
"@babel/plugin-transform-runtime",
|
||||
"@babel/plugin-transform-class-properties"
|
||||
]
|
||||
}
|
||||
@@ -55,7 +55,6 @@ const vueRule = {
|
||||
exports.base = {
|
||||
extends: ['standard'],
|
||||
rules: baseRule,
|
||||
parser: '@babel/eslint-parser',
|
||||
}
|
||||
|
||||
exports.html = {
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -16,7 +16,7 @@ runs:
|
||||
# https://docs.npmjs.com/cli/v10/configuring-npm/folders#cache
|
||||
- name: Cache node modules
|
||||
id: cache-npm
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.NPM_CACHE }}
|
||||
key: ${{ runner.os }}-npm-cache-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Check out git repository
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
|
||||
# - name: Install Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get npm cache directory
|
||||
shell: pwsh
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Build Package Setup x64
|
||||
run: npm run pack:win:setup:x64
|
||||
- name: Upload Artifact Setup x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-x64-Setup
|
||||
path: build/*-x64-Setup.exe
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Build Package 7z x64
|
||||
run: npm run pack:win:7z:x64
|
||||
- name: Upload Artifact 7z x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-win_x64-green
|
||||
path: build/*win_x64-green.7z
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Build Package Setup arm64
|
||||
run: npm run pack:win:setup:arm64
|
||||
- name: Upload Artifact Setup arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-arm64-Setup
|
||||
path: build/*-arm64-Setup.exe
|
||||
@@ -89,22 +89,56 @@ jobs:
|
||||
- name: Build Package 7z arm64
|
||||
run: npm run pack:win:7z:arm64
|
||||
- name: Upload Artifact 7z arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-win_arm64-green
|
||||
path: build/*win_arm64-green.7z
|
||||
|
||||
- name: Generate file MD5
|
||||
run: |
|
||||
cd build
|
||||
Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List
|
||||
|
||||
- name: Install old electron
|
||||
run: npm install electron@22
|
||||
Windows_7:
|
||||
name: Windows_7
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
BUILD_WIN7: true
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install python setuptools
|
||||
run: pip.exe install setuptools
|
||||
- name: Get npm cache directory
|
||||
shell: pwsh
|
||||
run: echo "NPM_CACHE=$(npm config get cache)" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup Node Env
|
||||
env:
|
||||
NPM_CACHE: ${{ env.NPM_CACHE }}
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Prepare win7 undici env
|
||||
run: |
|
||||
npm install undici@5
|
||||
Set-Content -Path .\src\common\utils\request.ts -Value "export * from './request_node16'"
|
||||
|
||||
- name: Build src code
|
||||
env:
|
||||
BUILD_WIN7: true
|
||||
run: |
|
||||
git status --porcelain
|
||||
npm run build
|
||||
|
||||
- name: Prepare win7 electron env
|
||||
run: |
|
||||
npm install electron@22
|
||||
pip.exe install setuptools
|
||||
|
||||
- name: Build Package win7 Setup x64
|
||||
run: npm run pack:win7:setup:x64
|
||||
- name: Upload Artifact win7 Setup x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-win7_x64-Setup
|
||||
path: build/*win7_x64-Setup.exe
|
||||
@@ -112,7 +146,7 @@ jobs:
|
||||
- name: Build Package win7 7z x64
|
||||
run: npm run pack:win7:7z:x64
|
||||
- name: Upload Artifact win7 7z x64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-win7_x64-green
|
||||
path: build/*win7_x64-green.7z
|
||||
@@ -120,7 +154,7 @@ jobs:
|
||||
- name: Build Package win7 7z x86
|
||||
run: npm run pack:win7:7z:x86
|
||||
- name: Upload Artifact win7 7z x86
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-win7_x86-green
|
||||
path: build/*win7_x86-green.7z
|
||||
@@ -136,7 +170,7 @@ jobs:
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install python setuptools
|
||||
run: brew install python-setuptools
|
||||
@@ -161,14 +195,14 @@ jobs:
|
||||
npm run pack:mac:dmg:arm64
|
||||
|
||||
- name: Upload Artifact dmg
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-mac-dmg
|
||||
path: |
|
||||
build/*.dmg
|
||||
!build/*-arm64.dmg
|
||||
- name: Upload Artifact dmg
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-mac-dmg-arm64
|
||||
path: build/*-arm64.dmg
|
||||
@@ -187,7 +221,7 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
|
||||
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get npm cache directory
|
||||
shell: bash
|
||||
@@ -206,7 +240,7 @@ jobs:
|
||||
- name: Build Package deb amd64
|
||||
run: npm run pack:linux:deb:amd64
|
||||
- name: Upload Artifact deb amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-deb-amd64
|
||||
path: build/*_amd64.deb
|
||||
@@ -214,7 +248,7 @@ jobs:
|
||||
- name: Build Package deb arm64
|
||||
run: npm run pack:linux:deb:arm64
|
||||
- name: Upload Artifact deb arm64
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-deb-arm64
|
||||
path: build/*_arm64.deb
|
||||
@@ -222,7 +256,7 @@ jobs:
|
||||
- name: Build Package deb armv7l
|
||||
run: npm run pack:linux:deb:armv7l
|
||||
- name: Upload Artifact deb armv7l
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-deb-armv7l
|
||||
path: build/*_armv7l.deb
|
||||
@@ -230,7 +264,7 @@ jobs:
|
||||
- name: Build Package x64 appImage
|
||||
run: npm run pack:linux:appImage
|
||||
- name: Upload Artifact x64 appImage
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-x64-appImage
|
||||
path: build/*_x64.AppImage
|
||||
@@ -238,7 +272,7 @@ jobs:
|
||||
- name: Build Package x64 rpm
|
||||
run: npm run pack:linux:rpm
|
||||
- name: Upload Artifact x64 rpm
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-x64-rpm
|
||||
path: build/*.x64.rpm
|
||||
@@ -246,7 +280,7 @@ jobs:
|
||||
- name: Build Package x64 pacman
|
||||
run: npm run pack:linux:pacman
|
||||
- name: Upload Artifact x64 pacman
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: lx-music-desktop-x64-pacman
|
||||
path: build/*_x64.pacman
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: lyswhut/lx-music-desktop-version-info
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Check out git repository
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
|
||||
# - name: Install Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get npm cache directory
|
||||
shell: pwsh
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get npm cache directory
|
||||
shell: pwsh
|
||||
@@ -98,7 +98,14 @@ jobs:
|
||||
NPM_CACHE: ${{ env.NPM_CACHE }}
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Prepare win7 undici env
|
||||
run: |
|
||||
npm install undici@5
|
||||
Set-Content -Path .\src\common\utils\request.ts -Value "export * from './request_node16'"
|
||||
|
||||
- name: Build src code
|
||||
env:
|
||||
BUILD_WIN7: true
|
||||
run: |
|
||||
git status --porcelain
|
||||
npm run build
|
||||
@@ -128,7 +135,7 @@ jobs:
|
||||
# needs: CheckCode
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install python3 setuptools
|
||||
run: brew install python-setuptools
|
||||
@@ -171,7 +178,7 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
|
||||
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get npm cache directory
|
||||
shell: bash
|
||||
|
||||
@@ -14,6 +14,8 @@ module.exports = {
|
||||
'@types/node',
|
||||
'electron-debug',
|
||||
'eslint-webpack-plugin',
|
||||
'typescript',
|
||||
'undici',
|
||||
|
||||
'eslint-plugin-vue',
|
||||
'vue-eslint-parser',
|
||||
@@ -35,11 +37,13 @@ module.exports = {
|
||||
|
||||
// target: 'minor',
|
||||
// filter: [
|
||||
// // 'electron',
|
||||
// 'electron',
|
||||
// 'eslint',
|
||||
// 'eslint-webpack-plugin',
|
||||
// 'electron-debug',
|
||||
// 'typescript',
|
||||
// '@types/node',
|
||||
// 'undici',
|
||||
|
||||
// 'eslint-plugin-vue',
|
||||
// 'vue-eslint-parser',
|
||||
|
||||
@@ -6,6 +6,40 @@ 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.12.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.1...v2.12.2) - 2026-05-01
|
||||
|
||||
我们很高兴地宣布新项目 Any Listen 的桌面版已发布,目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能,更多功能仍在积极开发中,桌面版与Web版将同步更新。
|
||||
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化歌单内歌曲搜索结果排序 (#2734)
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复桌面歌词的 鼠标移入歌词区域时提高透明度 设置不稳定的问题(#2679, @Little100)
|
||||
- 修复某些情况下可能播放没有声音的问题(#2693)
|
||||
- 修复 tx 搜索结果显示异常的问题(#2753)
|
||||
- 修复音乐名称和歌手信息格式化问题(#2733)
|
||||
|
||||
### 其他
|
||||
|
||||
- 更新 Electron 到 40.8.3
|
||||
|
||||
## [2.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.0...v2.12.1) - 2026-02-16
|
||||
|
||||
我们很高兴地宣布新项目 Any Listen 的桌面版已发布,目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能,更多功能仍在积极开发中,桌面版与Web版将同步更新。
|
||||
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化托盘图标行为:在非 Windows 系统中,点击托盘图标时不再显示主窗口
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复音量条在调整音量时实际音量与显示的数值不一致的问题(#2606)
|
||||
- 修复某些情况下搜索框的搜索按钮布局错位的问题(#2622)
|
||||
|
||||
## [2.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.11.0...v2.12.0) - 2025-11-29
|
||||
|
||||
我们很高兴地宣布新项目 Any Listen 的桌面版已发布,目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能,更多功能仍在积极开发中,桌面版与Web版将同步更新。
|
||||
|
||||
@@ -10,23 +10,6 @@ const better_sqlite3_fileNameMap = {
|
||||
[Arch.armv7l]: 'linux-arm',
|
||||
}
|
||||
|
||||
const qrc_decode_fileNameMap = {
|
||||
win32: {
|
||||
[Arch.x64]: 'win32-x64',
|
||||
[Arch.ia32]: 'win32-ia32',
|
||||
[Arch.arm64]: 'win32-arm64',
|
||||
},
|
||||
linux: {
|
||||
[Arch.x64]: 'linux-x64',
|
||||
[Arch.arm64]: 'linux-arm64',
|
||||
[Arch.armv7l]: 'linux-arm',
|
||||
},
|
||||
darwin: {
|
||||
[Arch.x64]: 'darwin-x64',
|
||||
[Arch.arm64]: 'darwin-arm64',
|
||||
},
|
||||
}
|
||||
|
||||
const replaceSqliteLib = async(electronNodeAbi, arch) => {
|
||||
// console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
|
||||
// if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
|
||||
@@ -40,22 +23,11 @@ const replaceSqliteLib = async(electronNodeAbi, arch) => {
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
const replaceQrcDecodeLib = async(electronNodeAbi, platform, arch) => {
|
||||
console.log('replace qrc_decode lib...', platform, electronNodeAbi, qrc_decode_fileNameMap[platform][arch])
|
||||
const filePath = path.join(__dirname, `./lib/qrc_decode_electron-v${electronNodeAbi}-${qrc_decode_fileNameMap[platform][arch]}.node`)
|
||||
const targetPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
const targetDir = path.dirname(targetPath)
|
||||
if (fs.existsSync(targetDir)) await fsPromises.unlink(targetPath).catch(_ => _)
|
||||
else await fsPromises.mkdir(targetDir, { recursive: true })
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
|
||||
module.exports = async(context) => {
|
||||
const { electronPlatformName, arch } = context
|
||||
const electronVersion = context.packager?.info?._framework?.version ?? require('../package.json').devDependencies.electron.replace(/^[^\d]*?(\d+)/, '$1')
|
||||
const electronNodeAbi = nodeAbi.getAbi(electronVersion, 'electron')
|
||||
await replaceQrcDecodeLib(electronNodeAbi, electronPlatformName, arch)
|
||||
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')
|
||||
|
||||
@@ -35,7 +35,6 @@ const options = {
|
||||
'node_modules/node-gyp-build',
|
||||
'node_modules/bufferutil',
|
||||
'node_modules/utf-8-validate',
|
||||
'build/Release/qrc_decode.node',
|
||||
'dist/**/*',
|
||||
],
|
||||
asar: {
|
||||
|
||||
@@ -25,7 +25,6 @@ const unzip = async(filePath) => {
|
||||
}
|
||||
|
||||
const files = [
|
||||
'qrc_decode',
|
||||
'better_sqlite3',
|
||||
]
|
||||
const moveFile = async(filePath) => {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,6 @@
|
||||
const path = require('path')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
target: 'electron-main',
|
||||
output: {
|
||||
@@ -18,7 +16,6 @@ module.exports = {
|
||||
'electron-font-manager': 'electron-font-manager',
|
||||
bufferutil: 'bufferutil',
|
||||
'utf-8-validate': 'utf-8-validate',
|
||||
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -19,15 +19,6 @@ module.exports = merge(baseConfig, {
|
||||
externals: [
|
||||
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
|
||||
@@ -18,15 +18,6 @@ module.exports = merge(baseConfig, {
|
||||
externals: [
|
||||
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
|
||||
@@ -19,7 +19,13 @@ const gitInfo = {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!execSync('git status --porcelain').toString().trim()) {
|
||||
let isClean = !execSync('git status --porcelain').toString().trim()
|
||||
if (process.env.BUILD_WIN7) {
|
||||
console.warn('BUILD_WIN7 is set, skipping git status check.')
|
||||
console.log('Workspace status:', execSync('git status --porcelain').toString().trim())
|
||||
isClean = true
|
||||
}
|
||||
if (isClean) {
|
||||
gitInfo.commit_id = execSync('git log -1 --pretty=format:"%H"').toString().trim()
|
||||
gitInfo.commit_date = execSync('git log -1 --pretty=format:"%ad" --date=iso-strict').toString().trim()
|
||||
} else if (process.env.IS_CI) {
|
||||
@@ -33,15 +39,6 @@ module.exports = merge(baseConfig, {
|
||||
externals: [
|
||||
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./",
|
||||
"paths": {
|
||||
"@main/*": ["src/main/*"],
|
||||
"@renderer/*": ["src/renderer/*"],
|
||||
"@lyric/*": ["src/renderer-lyric/*"],
|
||||
"@static/*": ["src/static/*"],
|
||||
"@common/*": ["src/common/*"],
|
||||
"@main/*": ["./src/main/*"],
|
||||
"@renderer/*": ["./src/renderer/*"],
|
||||
"@lyric/*": ["./src/renderer-lyric/*"],
|
||||
"@static/*": ["./src/static/*"],
|
||||
"@common/*": ["./src/common/*"],
|
||||
}
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
|
||||
Generated
+2959
-5491
File diff suppressed because it is too large
Load Diff
+47
-51
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lx-music-desktop",
|
||||
"version": "2.12.0",
|
||||
"version": "2.12.3-beta.5",
|
||||
"description": "一个免费的音乐查找助手",
|
||||
"main": "./dist/main.js",
|
||||
"scripts": {
|
||||
@@ -107,96 +107,87 @@
|
||||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
"@babel/eslint-parser": "^7.28.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.28.5",
|
||||
"@babel/preset-env": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@tsconfig/recommended": "^1.0.13",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/needle": "^3.3.0",
|
||||
"@types/node": "^20.19.25",
|
||||
"@types/node": "^20.19.41",
|
||||
"@types/tunnel": "^0.0.7",
|
||||
"@types/ws": "8.5.4",
|
||||
"@vue/language-plugin-pug": "^3.1.5",
|
||||
"babel-loader": "^10.0.0",
|
||||
"browserslist": "^4.28.0",
|
||||
"@vue/language-plugin-pug": "^3.3.5",
|
||||
"browserslist": "^4.28.2",
|
||||
"chalk": "^4.1.2",
|
||||
"changelog-parser": "^3.0.1",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"core-js": "^3.47.0",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"core-js": "^3.49.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"del": "^6.1.1",
|
||||
"electron": "37.6.1",
|
||||
"electron-builder": "^26.3.2",
|
||||
"electron": "42.4.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-devtools-installer": "github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03",
|
||||
"electron-to-chromium": "^1.5.262",
|
||||
"electron-updater": "6.7.2",
|
||||
"electron-to-chromium": "^1.5.372",
|
||||
"electron-updater": "6.8.9",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
|
||||
"eslint-plugin-html": "^8.1.3",
|
||||
"eslint-plugin-html": "^8.1.4",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"eslint-plugin-vue-pug": "^0.6.2",
|
||||
"eslint-webpack-plugin": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.6.5",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^12.3.0",
|
||||
"mini-css-extract-plugin": "^2.9.4",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
"less": "^4.6.6",
|
||||
"less-loader": "^13.0.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"node-loader": "^2.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-loader": "^8.2.0",
|
||||
"postcss": "^8.5.15",
|
||||
"postcss-loader": "^8.2.1",
|
||||
"postcss-pxtorem": "^6.1.0",
|
||||
"pug": "^3.0.3",
|
||||
"pug": "^3.0.4",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"rimraf": "^6.1.2",
|
||||
"rimraf": "^6.1.3",
|
||||
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"svg-transform-loader": "^2.0.13",
|
||||
"svgo-loader": "^4.0.0",
|
||||
"terser": "^5.44.1",
|
||||
"terser-webpack-plugin": "^5.3.14",
|
||||
"svgo-loader": "^5.0.0",
|
||||
"terser": "^5.48.0",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-loader": "^9.5.4",
|
||||
"ts-loader": "^9.6.0",
|
||||
"typescript": "5.9.3",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"vue-loader": "^17.4.2",
|
||||
"webpack": "^5.103.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "5.2.2",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "5.2.5",
|
||||
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
|
||||
"webpack-merge": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "^1.9.1",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"bufferutil": "^4.0.9",
|
||||
"better-sqlite3": "^12.10.1",
|
||||
"bufferutil": "^4.1.0",
|
||||
"comlink": "~4.3.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron-log": "^5.4.3",
|
||||
"font-list": "^2.0.1",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"electron-log": "^5.4.4",
|
||||
"font-list": "^2.1.0",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"image-size": "^1.1.0",
|
||||
"jschardet": "^3.1.4",
|
||||
"long": "^5.3.2",
|
||||
"message2call": "^0.1.3",
|
||||
"music-metadata": "^11.10.3",
|
||||
"music-metadata": "^11.13.0",
|
||||
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
|
||||
"node-id3": "^0.2.9",
|
||||
"sortablejs": "^1.15.6",
|
||||
"sortablejs": "^1.15.7",
|
||||
"tunnel": "^0.0.6",
|
||||
"utf-8-validate": "^6.0.5",
|
||||
"undici": "^7.27.1",
|
||||
"utf-8-validate": "^6.0.6",
|
||||
"vue": "~3.3.13",
|
||||
"vue-router": "^4.5.1",
|
||||
"ws": "^8.18.3"
|
||||
"vue-router": "~4.5.1",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "^11",
|
||||
@@ -204,14 +195,19 @@
|
||||
"node-abi": "latest",
|
||||
"minimatch": "latest",
|
||||
"semver": "latest",
|
||||
"posthtml": "latest",
|
||||
"node-gyp": "latest",
|
||||
"sockjs": {
|
||||
"uuid": "^12"
|
||||
},
|
||||
"svg-transform-loader": {
|
||||
"postcss": "latest"
|
||||
"postcss": "^8"
|
||||
},
|
||||
"svg-sprite-loader": {
|
||||
"postcss": "latest"
|
||||
"postcss": "^8"
|
||||
},
|
||||
"svg-baker": {
|
||||
"postcss": "latest"
|
||||
"postcss": "^8"
|
||||
},
|
||||
"braces": "latest",
|
||||
"node-gyp-build": "latest",
|
||||
|
||||
+10
-3
@@ -1,6 +1,13 @@
|
||||
我们很高兴地宣布新项目 Any Listen 的桌面版已发布,目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能,更多功能仍在积极开发中,桌面版与Web版将同步更新。
|
||||
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
|
||||
### 优化
|
||||
|
||||
- 优化kw歌单列表数据显示
|
||||
- 同步服务在连接时允许URL重定向
|
||||
- Linux 点击托盘时将显示主界面(#2840)
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复音量条在调整音量时实际音量与显示的数值不一致的问题(#2606)
|
||||
- 修复 kg 搜索结果显示问题 (#2782)
|
||||
- 修复某些情况下开放 API 获取到的音量为 0 的问题 (#2790)
|
||||
- 修复 mg 图片、歌词获取
|
||||
- 修复某些 tx 歌单打开失败的问题 (#1060, @ght-000)
|
||||
- 修复在某些情况下添加歌曲时可能导致保存的歌曲顺序不对的问题(#2842)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -78,10 +78,6 @@ const modules = {
|
||||
|
||||
restart_window: 'restart_window',
|
||||
|
||||
// lang_s2t: 'lang_s2t',
|
||||
|
||||
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
|
||||
handle_tx_decode_lyric: 'handle_tx_decode_lyric',
|
||||
get_lyric_info: 'get_lyric_info',
|
||||
set_lyric_info: 'set_lyric_info',
|
||||
set_config: 'set_config',
|
||||
@@ -166,6 +162,7 @@ const modules = {
|
||||
key_down: 'key_down',
|
||||
request_main_window_channel: 'request_main_window_channel',
|
||||
provide_main_window_channel: 'provide_main_window_channel',
|
||||
mouse_enter_leave: 'mouse_enter_leave',
|
||||
},
|
||||
hotKey: {
|
||||
enable: 'enable',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"./types"
|
||||
],
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@common/*": ["./*"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 非业务工具方法
|
||||
|
||||
import { pathToFileURL } from 'url'
|
||||
/**
|
||||
* 获取两个数之间的随机整数,大于等于min,小于max
|
||||
* @param {*} min
|
||||
@@ -188,7 +189,7 @@ export const sortInsert = <T>(arr: Array<{ num: number, data: T }>, data: { num:
|
||||
}
|
||||
|
||||
export const encodePath = (path: string) => {
|
||||
return encodeURI(path.replaceAll('\\', '/'))
|
||||
return pathToFileURL(path).href
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import qs from 'node:querystring'
|
||||
import {
|
||||
FormData,
|
||||
getGlobalDispatcher,
|
||||
interceptors,
|
||||
request as nodeRrequest,
|
||||
ProxyAgent,
|
||||
setGlobalDispatcher,
|
||||
type Dispatcher,
|
||||
} from 'undici'
|
||||
|
||||
const defaultOptions: Options = {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
},
|
||||
maxRedirect: 5,
|
||||
} as const
|
||||
const redirectDispatcher = interceptors.redirect({ maxRedirections: defaultOptions.maxRedirect })
|
||||
const dispatchers = [
|
||||
interceptors.retry({
|
||||
maxRetries: 3,
|
||||
minTimeout: 1000,
|
||||
maxTimeout: 10000,
|
||||
timeoutFactor: 2,
|
||||
retryAfter: true,
|
||||
}),
|
||||
// interceptors.responseError(),
|
||||
] as const
|
||||
let proxyAgent: ProxyAgent | null = null
|
||||
let globalDispatcher = getGlobalDispatcher()
|
||||
const buildDispatcher = (redirectDispatcher: Dispatcher.DispatcherComposeInterceptor | null, retryNum = 3) => {
|
||||
const otherInterceptors =
|
||||
retryNum == 3
|
||||
? dispatchers
|
||||
: [
|
||||
interceptors.retry({
|
||||
maxRetries: retryNum,
|
||||
minTimeout: 1000,
|
||||
maxTimeout: 6000,
|
||||
timeoutFactor: 2,
|
||||
retryAfter: true,
|
||||
}),
|
||||
]
|
||||
if (redirectDispatcher) {
|
||||
return (proxyAgent ?? globalDispatcher).compose(redirectDispatcher, ...otherInterceptors)
|
||||
}
|
||||
return (proxyAgent ?? globalDispatcher).compose(...otherInterceptors)
|
||||
}
|
||||
|
||||
setGlobalDispatcher(buildDispatcher(redirectDispatcher))
|
||||
|
||||
export const setProxy = (url?: string) => {
|
||||
proxyAgent = url ? new ProxyAgent(url) : null
|
||||
setGlobalDispatcher(buildDispatcher(redirectDispatcher))
|
||||
}
|
||||
export const setProxyByHost = (host?: string, port?: string) => {
|
||||
setProxy(host ? `http://${host}:${port}` : undefined)
|
||||
}
|
||||
const CONTENT_TYPE = {
|
||||
json: 'application/json',
|
||||
form: 'application/x-www-form-urlencoded',
|
||||
text: 'text/plain',
|
||||
formdata: 'multipart/form-data',
|
||||
xml: 'application/xml',
|
||||
binary: 'application/octet-stream',
|
||||
}
|
||||
type ParamsData = Record<string, string | number | null | undefined | boolean>
|
||||
export interface Options {
|
||||
method?:
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE'
|
||||
| 'OPTIONS'
|
||||
| 'PATCH'
|
||||
| 'PROPFIND'
|
||||
| 'COPY'
|
||||
| 'MOVE'
|
||||
| 'MKCOL'
|
||||
| 'PROPPATCH'
|
||||
| 'QUOTA'
|
||||
query?: ParamsData
|
||||
headers?: Record<string, string | string[]>
|
||||
timeout?: number
|
||||
maxRedirect?: number
|
||||
signal?: AbortController['signal']
|
||||
json?: Record<string, unknown>
|
||||
form?: ParamsData
|
||||
binary?: Buffer | Uint8Array
|
||||
text?: string
|
||||
formdata?: Record<string, string | Buffer | Uint8Array>
|
||||
xml?: string
|
||||
needBody?: boolean
|
||||
needRaw?: boolean
|
||||
retryNum?: number
|
||||
}
|
||||
export interface Response<Res> {
|
||||
headers: {
|
||||
accept?: string | undefined
|
||||
'accept-language'?: string | undefined
|
||||
'accept-patch'?: string | undefined
|
||||
'accept-ranges'?: string | undefined
|
||||
'access-control-allow-credentials'?: string | undefined
|
||||
'access-control-allow-headers'?: string | undefined
|
||||
'access-control-allow-methods'?: string | undefined
|
||||
'access-control-allow-origin'?: string | undefined
|
||||
'access-control-expose-headers'?: string | undefined
|
||||
'access-control-max-age'?: string | undefined
|
||||
'access-control-request-headers'?: string | undefined
|
||||
'access-control-request-method'?: string | undefined
|
||||
age?: string | undefined
|
||||
allow?: string | undefined
|
||||
'alt-svc'?: string | undefined
|
||||
authorization?: string | undefined
|
||||
'cache-control'?: string | undefined
|
||||
connection?: string | undefined
|
||||
'content-disposition'?: string | undefined
|
||||
'content-encoding'?: string | undefined
|
||||
'content-language'?: string | undefined
|
||||
'content-length'?: string | undefined
|
||||
'content-location'?: string | undefined
|
||||
'content-range'?: string | undefined
|
||||
'content-type'?: string | undefined
|
||||
cookie?: string | undefined
|
||||
date?: string | undefined
|
||||
etag?: string | undefined
|
||||
expect?: string | undefined
|
||||
expires?: string | undefined
|
||||
forwarded?: string | undefined
|
||||
from?: string | undefined
|
||||
host?: string | undefined
|
||||
'if-match'?: string | undefined
|
||||
'if-modified-since'?: string | undefined
|
||||
'if-none-match'?: string | undefined
|
||||
'if-unmodified-since'?: string | undefined
|
||||
'last-modified'?: string | undefined
|
||||
location?: string | undefined
|
||||
origin?: string | undefined
|
||||
pragma?: string | undefined
|
||||
'proxy-authenticate'?: string | undefined
|
||||
'proxy-authorization'?: string | undefined
|
||||
'public-key-pins'?: string | undefined
|
||||
range?: string | undefined
|
||||
referer?: string | undefined
|
||||
'retry-after'?: string | undefined
|
||||
'sec-websocket-accept'?: string | undefined
|
||||
'sec-websocket-extensions'?: string | undefined
|
||||
'sec-websocket-key'?: string | undefined
|
||||
'sec-websocket-protocol'?: string | undefined
|
||||
'sec-websocket-version'?: string | undefined
|
||||
'set-cookie'?: string[] | undefined
|
||||
'strict-transport-security'?: string | undefined
|
||||
tk?: string | undefined
|
||||
trailer?: string | undefined
|
||||
'transfer-encoding'?: string | undefined
|
||||
upgrade?: string | undefined
|
||||
'user-agent'?: string | undefined
|
||||
vary?: string | undefined
|
||||
via?: string | undefined
|
||||
warning?: string | undefined
|
||||
'www-authenticate'?: string | undefined
|
||||
}
|
||||
body: Res
|
||||
raw: Uint8Array
|
||||
statusCode?: number
|
||||
statusMessage?: string
|
||||
}
|
||||
|
||||
const buildRequestBody = (options: Options) => {
|
||||
let contentType: string | undefined
|
||||
let body: string | Buffer | Uint8Array | FormData | undefined
|
||||
if (options.json != null) {
|
||||
contentType = CONTENT_TYPE.json
|
||||
body = JSON.stringify(options.json)
|
||||
} else if (options.form != null) {
|
||||
contentType = CONTENT_TYPE.form
|
||||
body = qs.stringify(options.form)
|
||||
} else if (options.binary != null) {
|
||||
contentType = CONTENT_TYPE.binary
|
||||
body = options.binary
|
||||
} else if (options.formdata != null) {
|
||||
const formdata = new FormData()
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in options.formdata) {
|
||||
formdata.append(key, options.formdata[key])
|
||||
}
|
||||
contentType = CONTENT_TYPE.formdata
|
||||
body = formdata
|
||||
} else if (options.xml != null) {
|
||||
contentType = CONTENT_TYPE.xml
|
||||
body = options.xml
|
||||
} else if (options.text != null) {
|
||||
contentType = CONTENT_TYPE.text
|
||||
body = options.text
|
||||
}
|
||||
const headers = options.headers ?? {}
|
||||
if (headers['Content-Type']) {
|
||||
contentType = headers['Content-Type'] as string
|
||||
}
|
||||
const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }
|
||||
if (contentType) finalHeaders['Content-Type'] = contentType
|
||||
return [finalHeaders, body] as const
|
||||
}
|
||||
|
||||
const buildRequestDispatcher = (options: Options) => {
|
||||
let dispatcher: Dispatcher.ComposedDispatcher | undefined
|
||||
|
||||
if (options.maxRedirect != null) {
|
||||
if (options.maxRedirect != defaultOptions.maxRedirect) {
|
||||
if (options.maxRedirect) {
|
||||
dispatcher = buildDispatcher(interceptors.redirect({ maxRedirections: options.maxRedirect }), options.retryNum)
|
||||
} else {
|
||||
dispatcher = buildDispatcher(null, options.retryNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dispatcher
|
||||
}
|
||||
|
||||
export const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {
|
||||
const method = options.method?.toUpperCase() ?? 'GET'
|
||||
const timeout = options.timeout ?? defaultOptions.timeout
|
||||
const [headers, body] = buildRequestBody(options)
|
||||
// console.log(url, {
|
||||
// method,
|
||||
// bodyTimeout: timeout,
|
||||
// headersTimeout: timeout,
|
||||
// headers,
|
||||
// query: options.query,
|
||||
// body,
|
||||
// signal: options.signal,
|
||||
// dispatcher: buildRequestDispatcher(options),
|
||||
// })
|
||||
return nodeRrequest(url, {
|
||||
method,
|
||||
bodyTimeout: timeout,
|
||||
headersTimeout: timeout,
|
||||
headers,
|
||||
query: options.query,
|
||||
body,
|
||||
signal: options.signal,
|
||||
dispatcher: buildRequestDispatcher(options),
|
||||
}).then(async(response) => {
|
||||
if (options.needBody) {
|
||||
return {
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
body: response.body as unknown as T,
|
||||
} satisfies Omit<Response<T>, 'raw'> as Response<T>
|
||||
}
|
||||
if (options.needRaw) {
|
||||
return {
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
raw: await response.body.bytes(),
|
||||
} satisfies Omit<Response<T>, 'body'> as Response<T>
|
||||
}
|
||||
// console.log(response)
|
||||
let body = (await response.body.text()) as T
|
||||
if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {
|
||||
try {
|
||||
body = JSON.parse(body as string) as T
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
body,
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
} satisfies Omit<Response<T>, 'raw'> as Response<T>
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import qs from 'node:querystring'
|
||||
import {
|
||||
FormData,
|
||||
getGlobalDispatcher,
|
||||
request as nodeRrequest,
|
||||
ProxyAgent,
|
||||
setGlobalDispatcher,
|
||||
type Dispatcher,
|
||||
} from 'undici'
|
||||
|
||||
const defaultOptions: Options = {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
},
|
||||
maxRedirect: 5,
|
||||
} as const
|
||||
let proxyAgent: ProxyAgent | null = null
|
||||
let globalDispatcher = getGlobalDispatcher()
|
||||
const buildDispatcher = () => {
|
||||
return proxyAgent ?? globalDispatcher
|
||||
}
|
||||
|
||||
setGlobalDispatcher(buildDispatcher())
|
||||
|
||||
export const setProxy = (url?: string) => {
|
||||
proxyAgent = url ? new ProxyAgent(url) : null
|
||||
setGlobalDispatcher(buildDispatcher())
|
||||
}
|
||||
export const setProxyByHost = (host?: string, port?: string) => {
|
||||
console.log(host)
|
||||
setProxy(host ? `http://${host}:${port}` : undefined)
|
||||
}
|
||||
const CONTENT_TYPE = {
|
||||
json: 'application/json',
|
||||
form: 'application/x-www-form-urlencoded',
|
||||
text: 'text/plain',
|
||||
formdata: 'multipart/form-data',
|
||||
xml: 'application/xml',
|
||||
binary: 'application/octet-stream',
|
||||
}
|
||||
type ParamsData = Record<string, string | number | null | undefined | boolean>
|
||||
export interface Options {
|
||||
method?:
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE'
|
||||
| 'OPTIONS'
|
||||
| 'PATCH'
|
||||
| 'PROPFIND'
|
||||
| 'COPY'
|
||||
| 'MOVE'
|
||||
| 'MKCOL'
|
||||
| 'PROPPATCH'
|
||||
| 'QUOTA'
|
||||
query?: ParamsData
|
||||
headers?: Record<string, string | string[]>
|
||||
timeout?: number
|
||||
maxRedirect?: number
|
||||
signal?: AbortController['signal']
|
||||
json?: Record<string, unknown>
|
||||
form?: ParamsData
|
||||
binary?: Buffer | Uint8Array
|
||||
text?: string
|
||||
formdata?: Record<string, string | Buffer | Uint8Array>
|
||||
xml?: string
|
||||
needBody?: boolean
|
||||
needRaw?: boolean
|
||||
retryNum?: number
|
||||
}
|
||||
export interface Response<Res> {
|
||||
headers: {
|
||||
accept?: string | undefined
|
||||
'accept-language'?: string | undefined
|
||||
'accept-patch'?: string | undefined
|
||||
'accept-ranges'?: string | undefined
|
||||
'access-control-allow-credentials'?: string | undefined
|
||||
'access-control-allow-headers'?: string | undefined
|
||||
'access-control-allow-methods'?: string | undefined
|
||||
'access-control-allow-origin'?: string | undefined
|
||||
'access-control-expose-headers'?: string | undefined
|
||||
'access-control-max-age'?: string | undefined
|
||||
'access-control-request-headers'?: string | undefined
|
||||
'access-control-request-method'?: string | undefined
|
||||
age?: string | undefined
|
||||
allow?: string | undefined
|
||||
'alt-svc'?: string | undefined
|
||||
authorization?: string | undefined
|
||||
'cache-control'?: string | undefined
|
||||
connection?: string | undefined
|
||||
'content-disposition'?: string | undefined
|
||||
'content-encoding'?: string | undefined
|
||||
'content-language'?: string | undefined
|
||||
'content-length'?: string | undefined
|
||||
'content-location'?: string | undefined
|
||||
'content-range'?: string | undefined
|
||||
'content-type'?: string | undefined
|
||||
cookie?: string | undefined
|
||||
date?: string | undefined
|
||||
etag?: string | undefined
|
||||
expect?: string | undefined
|
||||
expires?: string | undefined
|
||||
forwarded?: string | undefined
|
||||
from?: string | undefined
|
||||
host?: string | undefined
|
||||
'if-match'?: string | undefined
|
||||
'if-modified-since'?: string | undefined
|
||||
'if-none-match'?: string | undefined
|
||||
'if-unmodified-since'?: string | undefined
|
||||
'last-modified'?: string | undefined
|
||||
location?: string | undefined
|
||||
origin?: string | undefined
|
||||
pragma?: string | undefined
|
||||
'proxy-authenticate'?: string | undefined
|
||||
'proxy-authorization'?: string | undefined
|
||||
'public-key-pins'?: string | undefined
|
||||
range?: string | undefined
|
||||
referer?: string | undefined
|
||||
'retry-after'?: string | undefined
|
||||
'sec-websocket-accept'?: string | undefined
|
||||
'sec-websocket-extensions'?: string | undefined
|
||||
'sec-websocket-key'?: string | undefined
|
||||
'sec-websocket-protocol'?: string | undefined
|
||||
'sec-websocket-version'?: string | undefined
|
||||
'set-cookie'?: string[] | undefined
|
||||
'strict-transport-security'?: string | undefined
|
||||
tk?: string | undefined
|
||||
trailer?: string | undefined
|
||||
'transfer-encoding'?: string | undefined
|
||||
upgrade?: string | undefined
|
||||
'user-agent'?: string | undefined
|
||||
vary?: string | undefined
|
||||
via?: string | undefined
|
||||
warning?: string | undefined
|
||||
'www-authenticate'?: string | undefined
|
||||
}
|
||||
body: Res
|
||||
raw: Uint8Array
|
||||
statusCode?: number
|
||||
statusMessage?: string
|
||||
}
|
||||
|
||||
const buildRequestBody = (options: Options) => {
|
||||
let contentType: string | undefined
|
||||
let body: string | Buffer | Uint8Array | FormData | undefined
|
||||
if (options.json != null) {
|
||||
contentType = CONTENT_TYPE.json
|
||||
body = JSON.stringify(options.json)
|
||||
} else if (options.form != null) {
|
||||
contentType = CONTENT_TYPE.form
|
||||
body = qs.stringify(options.form)
|
||||
} else if (options.binary != null) {
|
||||
contentType = CONTENT_TYPE.binary
|
||||
body = options.binary
|
||||
} else if (options.formdata != null) {
|
||||
const formdata = new FormData()
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in options.formdata) {
|
||||
formdata.append(key, options.formdata[key])
|
||||
}
|
||||
contentType = CONTENT_TYPE.formdata
|
||||
body = formdata
|
||||
} else if (options.xml != null) {
|
||||
contentType = CONTENT_TYPE.xml
|
||||
body = options.xml
|
||||
} else if (options.text != null) {
|
||||
contentType = CONTENT_TYPE.text
|
||||
body = options.text
|
||||
}
|
||||
const headers = options.headers ?? {}
|
||||
if (headers['Content-Type']) {
|
||||
contentType = headers['Content-Type'] as string
|
||||
}
|
||||
const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }
|
||||
if (contentType) finalHeaders['Content-Type'] = contentType
|
||||
return [finalHeaders, body] as const
|
||||
}
|
||||
|
||||
const buildRequestDispatcher = (options: Options) => {
|
||||
return buildDispatcher()
|
||||
}
|
||||
|
||||
export const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {
|
||||
const method = (options.method?.toUpperCase() ?? 'GET') as Dispatcher.RequestOptions['method']
|
||||
const timeout = options.timeout ?? defaultOptions.timeout
|
||||
const [headers, body] = buildRequestBody(options)
|
||||
// console.log(url, {
|
||||
// method,
|
||||
// bodyTimeout: timeout,
|
||||
// headersTimeout: timeout,
|
||||
// headers,
|
||||
// query: options.query,
|
||||
// body,
|
||||
// signal: options.signal,
|
||||
// dispatcher: buildRequestDispatcher(options),
|
||||
// })
|
||||
return nodeRrequest(url, {
|
||||
method,
|
||||
bodyTimeout: timeout,
|
||||
headersTimeout: timeout,
|
||||
headers,
|
||||
query: options.query,
|
||||
body,
|
||||
signal: options.signal,
|
||||
dispatcher: buildRequestDispatcher(options),
|
||||
maxRedirections: options.maxRedirect ?? defaultOptions.maxRedirect,
|
||||
}).then(async(response) => {
|
||||
if (options.needBody) {
|
||||
return {
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
body: response.body as unknown as T,
|
||||
} satisfies Omit<Response<T>, 'raw'> as Response<T>
|
||||
}
|
||||
if (options.needRaw) {
|
||||
return {
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
raw: new Uint8Array(await response.body.arrayBuffer()),
|
||||
} satisfies Omit<Response<T>, 'body'> as unknown as Response<T>
|
||||
}
|
||||
// console.log(response)
|
||||
let body = (await response.body.text()) as T
|
||||
if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {
|
||||
try {
|
||||
body = JSON.parse(body as string) as T
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
body,
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
} satisfies Omit<Response<T>, 'raw'> as Response<T>
|
||||
})
|
||||
}
|
||||
@@ -148,3 +148,7 @@ export const clipFileNameLength = (name: string) => {
|
||||
return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name
|
||||
}
|
||||
|
||||
export const formatMusicName = (format: string, name: string, singer: string) => {
|
||||
// return format.replace(/歌名|歌手/g, match => match === '歌名' ? name : singer)
|
||||
return format.replace('歌手', singer).replace('歌名', name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -613,7 +613,7 @@
|
||||
"songlist__import_input_tip_1": "Cross-service playlists are not supported. Please confirm whether the playlist to be opened corresponds to the current chosen service",
|
||||
"songlist__import_input_tip_2": "If you encounter a playlist link that cannot be opened. Please send us your feedback",
|
||||
"songlist__import_input_tip_3": "Unable to open Kugou playlist with playlist ID or link from lite edition, but support to open it with Kugou code or link from main edition",
|
||||
"songlist__import_input_tip_4": "NetEase Cloud Music's \"I Like\" playlist requires a token to open. For details, see ",
|
||||
"songlist__import_input_tip_4": "NetEase’s “Private Radar” playlists require a token to access. Tencent’s personal playlists require your profile be public to access. For details, see ",
|
||||
"songlist__import_input_title": "Open shared playlist",
|
||||
"songlist__open_list": "Open \"{name}\" playlist",
|
||||
"songlist__tag_info_hot_tag": "Top Tags",
|
||||
|
||||
+1
-1
@@ -613,7 +613,7 @@
|
||||
"songlist__import_input_tip_1": "不支持跨源打开歌单,请确认要打开的歌单与当前选择的歌单来源是否对应",
|
||||
"songlist__import_input_tip_2": "若遇到无法打开的歌单链接,欢迎反馈",
|
||||
"songlist__import_input_tip_3": "酷狗源歌单不支持用歌单 ID 或概念版链接打开,但支持用普通版链接或酷狗码打开",
|
||||
"songlist__import_input_tip_4": "网易源的「我喜欢」歌单需要 Token 才能打开,详情看 ",
|
||||
"songlist__import_input_tip_4": "网易源「私人雷达」歌单需要 Token 才能打开,企鹅源个人歌单需要将个人主页公开才能打开,请参阅 ",
|
||||
"songlist__import_input_title": "打开分享的歌单",
|
||||
"songlist__open_list": "打开「{name}」歌单",
|
||||
"songlist__tag_info_hot_tag": "热门标签",
|
||||
|
||||
+1
-1
@@ -613,7 +613,7 @@
|
||||
"songlist__import_input_tip_1": "不支援跨平台開啟歌單,請確認要開啟的歌單與目前歌單的來源平台是否對應",
|
||||
"songlist__import_input_tip_2": "若遇到無法開啟的歌單連結,歡迎回報",
|
||||
"songlist__import_input_tip_3": "酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟,但支援用普通版連結與酷狗碼開啟",
|
||||
"songlist__import_input_tip_4": "網易雲音樂的「我喜歡」歌單需要 Token 才能開啟,詳情看 ",
|
||||
"songlist__import_input_tip_4": "網易雲音樂「私人雷達」歌單需要 Token 才能開啟,企鵝音樂個人歌單需要將個人資料頁公開才能開啟,詳情看 ",
|
||||
"songlist__import_input_title": "開啟分享的歌單",
|
||||
"songlist__open_list": "開啟「{name}」歌單",
|
||||
"songlist__tag_info_hot_tag": "熱門標籤",
|
||||
|
||||
+20
-4
@@ -2,7 +2,7 @@ import path from 'node:path'
|
||||
import { existsSync, mkdirSync, renameSync } from 'fs'
|
||||
import { app, shell, screen, nativeTheme, dialog } from 'electron'
|
||||
import { URL_SCHEME_RXP } from '@common/constants'
|
||||
import { getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
|
||||
import { getProxy, getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
|
||||
import { navigationUrlWhiteList } from '@common/config'
|
||||
import defaultSetting from '@common/defaultSetting'
|
||||
import { isExistWindow as isExistMainWindow, showWindow as showMainWindow } from './modules/winMain'
|
||||
@@ -11,6 +11,7 @@ import { isMac, log } from '@common/utils'
|
||||
import createWorkers from './worker'
|
||||
import { migrateDBData } from './utils/migrate'
|
||||
import { openDirInExplorer } from '@common/utils/electron'
|
||||
import { setProxyByHost } from '@common/utils/request'
|
||||
|
||||
export const initGlobalData = () => {
|
||||
const envParams = parseEnvParams()
|
||||
@@ -236,14 +237,29 @@ export const listenerAppEvent = (startApp: () => void) => {
|
||||
global.lx?.event_app.system_theme_change(shouldUseDarkColors)
|
||||
})
|
||||
|
||||
global.lx.event_app.on('updated_config', (config, setting) => {
|
||||
if (config.includes('player.volume')) {
|
||||
const setProxy = () => {
|
||||
const proxy = getProxy()
|
||||
if (proxy) {
|
||||
setProxyByHost(proxy.host, proxy.port ? String(proxy.port) : undefined)
|
||||
} else setProxyByHost()
|
||||
}
|
||||
global.lx.event_app.on('updated_config', (keys, setting) => {
|
||||
if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) {
|
||||
setProxy()
|
||||
}
|
||||
|
||||
if (keys.includes('player.volume')) {
|
||||
global.lx.event_app.player_status({ volume: Math.trunc(setting['player.volume']! * 100) })
|
||||
}
|
||||
if (config.includes('player.isMute')) {
|
||||
if (keys.includes('player.isMute')) {
|
||||
global.lx.event_app.player_status({ mute: setting['player.isMute'] })
|
||||
}
|
||||
})
|
||||
global.lx.event_app.on('app_inited', () => {
|
||||
setProxy()
|
||||
global.lx.event_app.player_status({ volume: Math.trunc(global.lx.appSetting['player.volume'] * 100) })
|
||||
global.lx.event_app.player_status({ mute: global.lx.appSetting['player.isMute'] })
|
||||
})
|
||||
}
|
||||
|
||||
const initTheme = () => {
|
||||
|
||||
@@ -151,6 +151,7 @@ let client: LX.Sync.Client.Socket | null
|
||||
// let listSyncPromise: Promise<void>
|
||||
export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
|
||||
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {
|
||||
followRedirects: true,
|
||||
}) as LX.Sync.Client.Socket
|
||||
client.data = {
|
||||
keyInfo,
|
||||
|
||||
@@ -3,10 +3,9 @@ import { httpFetch, type RequestOptions } from '@main/utils/request'
|
||||
import { decodeData, encodeData } from '../utils'
|
||||
|
||||
export const request = async(url: string, options: RequestOptions = { }) => {
|
||||
return httpFetch(url, {
|
||||
return httpFetch<string>(url, {
|
||||
...options,
|
||||
timeout: options.timeout ?? 10000,
|
||||
follow_max: 5,
|
||||
}).then(response => {
|
||||
return {
|
||||
text: response.body,
|
||||
|
||||
@@ -150,9 +150,11 @@ export const createTray = () => {
|
||||
// tray.setToolTip('LX Music')
|
||||
// createMenu()
|
||||
tray.setIgnoreDoubleClickEvents(true)
|
||||
tray.on('click', () => {
|
||||
showMainWindow()
|
||||
})
|
||||
if (!isMac) {
|
||||
tray.on('click', () => {
|
||||
showMainWindow()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const destroyTray = () => {
|
||||
|
||||
@@ -109,14 +109,20 @@ const inflateScript = async(script: string) => new Promise<string>((resolve, rej
|
||||
})
|
||||
export const importApi = async(scriptRaw: string): Promise<LX.UserApi.UserApiInfo> => {
|
||||
let scriptInfo = parseScriptInfo(scriptRaw)
|
||||
const script = await deflateScript(scriptRaw)
|
||||
userApis ??= []
|
||||
for (const api of userApis) {
|
||||
const existingScript = scripts.get(api.id)
|
||||
if (existingScript === script) {
|
||||
throw new Error(`导入失败,脚本内容与已有的源「${api.name}」相同`)
|
||||
}
|
||||
}
|
||||
const apiInfo = {
|
||||
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
|
||||
...scriptInfo,
|
||||
allowShowUpdateAlert: true,
|
||||
}
|
||||
userApis ??= []
|
||||
userApis.push(apiInfo)
|
||||
const script = await deflateScript(scriptRaw)
|
||||
scripts.set(apiInfo.id, script)
|
||||
saveData()
|
||||
return apiInfo
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isLinux } from '@common/utils'
|
||||
import { closeWindow, createWindow, getBounds, isExistWindow, alwaysOnTopTools, setBounds, setIgnoreMouseEvents, setSkipTaskbar } from './main'
|
||||
import { sendConfigChange } from './rendererEvent'
|
||||
import { sendConfigChange, sendMouseLeave } from './rendererEvent'
|
||||
import { buildLyricConfig, getLyricWindowBounds, initWindowSize, watchConfigKeys } from './utils'
|
||||
import { mouseCheckTools } from './mouseCheckTools'
|
||||
|
||||
let isLock: boolean
|
||||
let isEnable: boolean
|
||||
@@ -21,14 +22,21 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
|
||||
isLock = global.lx.appSetting['desktopLyric.isLock']
|
||||
if (global.lx.appSetting['desktopLyric.isLock']) {
|
||||
setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })
|
||||
mouseCheckTools.runCheck(sendMouseLeave)
|
||||
} else {
|
||||
setIgnoreMouseEvents(false, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })
|
||||
mouseCheckTools.cacnelCheck()
|
||||
}
|
||||
}
|
||||
if (keys.includes('desktopLyric.isHoverHide') && isHoverHide != global.lx.appSetting['desktopLyric.isHoverHide']) {
|
||||
isHoverHide = global.lx.appSetting['desktopLyric.isHoverHide']
|
||||
if (!isLinux) {
|
||||
setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: global.lx.appSetting['desktopLyric.isHoverHide'] })
|
||||
setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: isHoverHide })
|
||||
if (isHoverHide) {
|
||||
mouseCheckTools.runCheck(sendMouseLeave)
|
||||
} else {
|
||||
mouseCheckTools.cacnelCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keys.includes('desktopLyric.isAlwaysOnTop') && isAlwaysOnTop != global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {
|
||||
@@ -54,7 +62,7 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
|
||||
if (keys.includes('desktopLyric.isLockScreen') && isLockScreen != global.lx.appSetting['desktopLyric.isLockScreen']) {
|
||||
isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']
|
||||
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
|
||||
setBounds(getLyricWindowBounds(getBounds(), {
|
||||
setBounds(getLyricWindowBounds(getBounds()!, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: global.lx.appSetting['desktopLyric.width'],
|
||||
@@ -77,6 +85,7 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
|
||||
createWindow()
|
||||
} else {
|
||||
alwaysOnTopTools.clearLoop()
|
||||
mouseCheckTools.cacnelCheck()
|
||||
closeWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ export const sendEvent = <T = any>(name: string, params?: T) => {
|
||||
mainSend(browserWindow, name, params)
|
||||
}
|
||||
|
||||
export const getBounds = (): Electron.Rectangle => {
|
||||
if (!browserWindow) throw new Error('window is not available')
|
||||
export const getBounds = (): Electron.Rectangle | null => {
|
||||
if (!browserWindow) return null
|
||||
return browserWindow.getBounds()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { screen } from 'electron'
|
||||
import { getBounds } from './main'
|
||||
import { isLinux } from '@common/utils'
|
||||
|
||||
|
||||
export const mouseCheckTools = {
|
||||
timer: null as NodeJS.Timeout | null,
|
||||
isMouseInWindow: false,
|
||||
sendMouseLeave: null as (() => void) | null,
|
||||
|
||||
onInBoundsChange(isInBounds: boolean) {
|
||||
if (isInBounds) return
|
||||
this.stop()
|
||||
this.sendMouseLeave?.()
|
||||
},
|
||||
|
||||
checkMousePosition() {
|
||||
const bounds = getBounds()
|
||||
if (!bounds) return
|
||||
const point = screen.getCursorScreenPoint()
|
||||
const isInBounds = point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
|
||||
point.y >= bounds.y && point.y <= bounds.y + bounds.height
|
||||
if (isInBounds !== this.isMouseInWindow) {
|
||||
this.isMouseInWindow = isInBounds
|
||||
this.onInBoundsChange(isInBounds)
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = null
|
||||
this.checkMousePosition()
|
||||
this.start()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (!this.timer) return
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
// console.log('stop check')
|
||||
},
|
||||
|
||||
setMouseInWindow(isInWindow: boolean) {
|
||||
this.isMouseInWindow = isInWindow
|
||||
},
|
||||
runCheck(sendMouseLeave: () => void) {
|
||||
if (isLinux || !global.lx.appSetting['desktopLyric.isLock'] || !global.lx.appSetting['desktopLyric.isHoverHide'] || !this.isMouseInWindow) return
|
||||
// console.log('run check')
|
||||
this.sendMouseLeave = sendMouseLeave
|
||||
this.stop()
|
||||
this.start()
|
||||
},
|
||||
cacnelCheck() {
|
||||
this.stop()
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { buildLyricConfig, getLyricWindowBounds } from './utils'
|
||||
import { sendNewDesktopLyricClient } from '@main/modules/winMain'
|
||||
import { getBounds, getMainFrame, sendEvent, setBounds, setResizeable } from './main'
|
||||
import { MessageChannelMain } from 'electron'
|
||||
import { mouseCheckTools } from './mouseCheckTools'
|
||||
|
||||
|
||||
export default () => {
|
||||
@@ -26,7 +27,7 @@ export default () => {
|
||||
})
|
||||
|
||||
mainOn<LX.DesktopLyric.NewBounds>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, ({ params: options }) => {
|
||||
setBounds(getLyricWindowBounds(getBounds(), options))
|
||||
setBounds(getLyricWindowBounds(getBounds()!, options))
|
||||
})
|
||||
|
||||
mainOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, ({ params: resizable }) => {
|
||||
@@ -45,6 +46,16 @@ export default () => {
|
||||
// without going through the main process!
|
||||
console.log('request_main_window_channel')
|
||||
})
|
||||
|
||||
mainOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, ({ params: isEnter }) => {
|
||||
if (isEnter) {
|
||||
mouseCheckTools.setMouseInWindow(true)
|
||||
mouseCheckTools.runCheck(sendMouseLeave)
|
||||
} else {
|
||||
mouseCheckTools.setMouseInWindow(false)
|
||||
mouseCheckTools.cacnelCheck()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const sendConfigChange = (setting: Partial<LX.DesktopLyric.Config>) => {
|
||||
@@ -55,3 +66,6 @@ export const sendMainWindowInitedEvent = () => {
|
||||
sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited)
|
||||
}
|
||||
|
||||
export const sendMouseLeave = () => {
|
||||
sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, false)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { registerRendererEvents as list } from '@main/modules/commonRenderers/li
|
||||
import { registerRendererEvents as dislike } from '@main/modules/commonRenderers/dislike'
|
||||
import app, { sendConfigChange } from './app'
|
||||
import hotKey from './hotKey'
|
||||
import kw_decodeLyric from './kw_decodeLyric'
|
||||
import tx_decodeLyric from './tx_decodeLyric'
|
||||
import userApi from './userApi'
|
||||
import sync from './sync'
|
||||
import data from './data'
|
||||
@@ -30,8 +28,6 @@ export default () => {
|
||||
dislike(sendEvent)
|
||||
app()
|
||||
hotKey()
|
||||
kw_decodeLyric()
|
||||
tx_decodeLyric()
|
||||
userApi()
|
||||
sync()
|
||||
data()
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { inflate } from 'zlib'
|
||||
import iconv from 'iconv-lite'
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
const handleInflate = async(data: Buffer) => {
|
||||
return new Promise((resolve: (result: Buffer) => void, reject) => {
|
||||
inflate(data, (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
|
||||
const decodeLyric = async(buf: Buffer, isGetLyricx: boolean) => {
|
||||
// const info = buf.slice(0, index).toString()
|
||||
// if (!info.startsWith('tp=content')) return null
|
||||
// const isLyric = info.includes('\r\nlrcx=0\r\n')
|
||||
if (buf.toString('utf8', 0, 10) != 'tp=content') return ''
|
||||
// const index = buf.indexOf('\r\n\r\n') + 4
|
||||
const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4))
|
||||
|
||||
if (!isGetLyricx) return iconv.decode(lrcData, 'gb18030')
|
||||
|
||||
const buf_str = Buffer.from(lrcData.toString(), 'base64')
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint8Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_str[i] ^ buf_key[j]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return iconv.decode(Buffer.from(output), 'gb18030')
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
mainHandle<{ lrcBase64: string, isGetLyricx: boolean }, string>(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, async({ params: { lrcBase64, isGetLyricx } }) => {
|
||||
const lrc = await decodeLyric(Buffer.from(lrcBase64, 'base64'), isGetLyricx)
|
||||
return Buffer.from(lrc).toString('base64')
|
||||
})
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { createInflate, constants as zlibConstants } from 'node:zlib'
|
||||
// import path from 'path'
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/quotes
|
||||
// const require = module[`require`].bind(module)
|
||||
|
||||
let qrc_decode: (buf: Buffer, len: number) => Buffer
|
||||
|
||||
const inflate = async(lrcBuf: Buffer) => new Promise<string>((resolve, reject) => {
|
||||
const buffer_builder: Buffer[] = []
|
||||
const decompress_stream = createInflate()
|
||||
.on('data', (chunk) => {
|
||||
buffer_builder.push(chunk)
|
||||
})
|
||||
.on('close', () => {
|
||||
resolve(Buffer.concat(buffer_builder).toString())
|
||||
})
|
||||
.on('error', (err: any) => {
|
||||
// console.log(err)
|
||||
if (err.errno !== zlibConstants.Z_BUF_ERROR) { // EOF: expected
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// decompress_stream.write(lrcBuf)
|
||||
decompress_stream.end(lrcBuf)
|
||||
})
|
||||
|
||||
const decode = async(str: string): Promise<string> => {
|
||||
if (!str) return ''
|
||||
const buf = Buffer.from(str, 'hex')
|
||||
return inflate(qrc_decode(buf, buf.length))
|
||||
}
|
||||
|
||||
|
||||
// 感谢某位不愿透露姓名的大佬提供的C++算法源码,但由于作者不希望公开,所以将会以预构建二进制文件的形式加入代码仓库中
|
||||
const handleDecode = async(lrc: string, tlrc: string, rlrc: string) => {
|
||||
if (!qrc_decode) {
|
||||
// const nativeBindingPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// const nativeBindingPath = process.env.NODE_ENV !== 'production' ? path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const addon = require('qrc_decode.node')
|
||||
// console.log(addon)
|
||||
qrc_decode = addon.qrc_decode
|
||||
}
|
||||
|
||||
const [lyric, tlyric, rlyric] = await Promise.all([decode(lrc), decode(tlrc), decode(rlrc)])
|
||||
return {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
mainHandle<{ lrc: string, tlrc: string, rlrc: string }, { lyric: string, tlyric: string, rlyric: string }>(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, async({ params: { lrc, tlrc, rlrc } }) => {
|
||||
return handleDecode(lrc, tlrc, rlrc)
|
||||
})
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
"./types"
|
||||
],
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@main/*": ["main/*"],
|
||||
"@static/*": ["static/*"],
|
||||
"@/*": ["./*"],
|
||||
"@common/*": ["../common/*"],
|
||||
"@main/*": ["../main/*"],
|
||||
"@static/*": ["../static/*"],
|
||||
"@/*": ["../*"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { encodePath, isUrl, throttle } from '@common/utils'
|
||||
import { encodePath, isUrl, throttle, isMac } from '@common/utils'
|
||||
import migrateSetting from '@common/utils/migrateSetting'
|
||||
import getStore from '@main/utils/store'
|
||||
import { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants'
|
||||
@@ -293,7 +293,7 @@ export const setPowerSaveBlocker = (enabled: boolean) => {
|
||||
let isEnabled = powerSaveBlockerId != null && powerSaveBlocker.isStarted(powerSaveBlockerId)
|
||||
if (enabled) {
|
||||
if (isEnabled) return
|
||||
powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension')
|
||||
powerSaveBlockerId = powerSaveBlocker.start(isMac ? 'prevent-display-sleep' : 'prevent-app-suspension')
|
||||
} else {
|
||||
if (!isEnabled) return
|
||||
powerSaveBlocker.stop(powerSaveBlockerId!)
|
||||
|
||||
+4
-131
@@ -1,8 +1,5 @@
|
||||
import needle, { type NeedleHttpVerbs, type NeedleOptions, type BodyData, type NeedleCallback, type NeedleResponse } from 'needle'
|
||||
// import progress from 'request-progress'
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
import { type ClientRequest } from 'node:http'
|
||||
import { getProxy } from './index'
|
||||
import { request, type Options } from '@common/utils/request'
|
||||
// import fs from 'fs'
|
||||
|
||||
export const requestMsg = {
|
||||
@@ -14,53 +11,6 @@ export const requestMsg = {
|
||||
cancelRequest: '取消http请求',
|
||||
} as const
|
||||
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
const getRequestAgent = (url: string) => {
|
||||
const proxy = getProxy()
|
||||
return proxy ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({ proxy }) : undefined
|
||||
}
|
||||
|
||||
export interface RequestOptions extends NeedleOptions {
|
||||
method?: NeedleHttpVerbs
|
||||
body?: BodyData
|
||||
form?: BodyData
|
||||
formData?: BodyData
|
||||
}
|
||||
export type RequestCallback = NeedleCallback
|
||||
type RequestResponse = NeedleResponse
|
||||
const request = (url: string, options: RequestOptions, callback: RequestCallback): ClientRequest => {
|
||||
let data: BodyData = null
|
||||
if (options.body) {
|
||||
data = options.body
|
||||
} else if (options.form) {
|
||||
data = options.form
|
||||
// data.content_type = 'application/x-www-form-urlencoded'
|
||||
options.json = false
|
||||
} else if (options.formData) {
|
||||
data = options.formData
|
||||
// data.content_type = 'multipart/form-data'
|
||||
options.json = false
|
||||
}
|
||||
options.response_timeout = options.timeout
|
||||
|
||||
return needle.request(options.method ?? 'get', url, data, options, (err, resp, body) => {
|
||||
if (!err) {
|
||||
body = resp.body = resp.raw.toString()
|
||||
try {
|
||||
resp.body = JSON.parse(resp.body)
|
||||
} catch (_) {}
|
||||
body = resp.body
|
||||
}
|
||||
callback(err, resp, body)
|
||||
// @ts-expect-error
|
||||
}).request
|
||||
}
|
||||
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
}
|
||||
// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port;
|
||||
// var proxiedRequest = request.defaults({'proxy': proxyUrl});
|
||||
|
||||
@@ -68,70 +18,13 @@ const defaultHeaders = {
|
||||
// abort: () => void
|
||||
// }
|
||||
|
||||
/**
|
||||
* promise 形式的请求方法
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
const buildHttpPromose = async(url: string, options: RequestOptions): Promise<RequestResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
void fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(resp)
|
||||
})
|
||||
// .then(request => {
|
||||
// // obj.requestObj = ro
|
||||
// // if (obj.isCancelled) obj.cancelHttp()
|
||||
// promise.abort = () => {
|
||||
// request.destroy(new Error('cancelled'))
|
||||
// }
|
||||
// })
|
||||
})
|
||||
// let obj = {
|
||||
// isCancelled: false,
|
||||
// cancelHttp: () => {
|
||||
// if (!obj.requestObj) return obj.isCancelled = true
|
||||
// cancelHttp(obj.requestObj)
|
||||
// obj.requestObj = null
|
||||
// obj.promise = obj.cancelHttp = null
|
||||
// obj.cancelFn(new Error(requestMsg.cancelRequest))
|
||||
// obj.cancelFn = null
|
||||
// },
|
||||
// }
|
||||
// obj.promise = new Promise((resolve, reject) => {
|
||||
// obj.cancelFn = reject
|
||||
// debugRequest && console.log(`\n---send request------${url}------------`)
|
||||
// fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// // options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
// if (err) { reject(err); return }
|
||||
// resolve(resp)
|
||||
// }).then(ro => {
|
||||
// obj.requestObj = ro
|
||||
// if (obj.isCancelled) obj.cancelHttp()
|
||||
// })
|
||||
// })
|
||||
// return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求超时自动重试
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
export const httpFetch = async(url: string, options: RequestOptions = { method: 'get' }) => {
|
||||
return buildHttpPromose(url, options).catch(async(err: any) => {
|
||||
export const httpFetch = async<T = unknown> (url: string, options: Options) => {
|
||||
return request<T>(url, options).catch(async(err: any) => {
|
||||
// console.log('出错', err)
|
||||
if (err.message === 'socket hang up') {
|
||||
// window.globalObj.apiSource = 'temp'
|
||||
@@ -166,24 +59,4 @@ export const httpFetch = async(url: string, options: RequestOptions = { method:
|
||||
// return requestPromise
|
||||
}
|
||||
|
||||
const fetchData = async(url: string, method: RequestOptions['method'], {
|
||||
headers = {},
|
||||
format = 'json',
|
||||
timeout = 15000,
|
||||
...options
|
||||
}, callback: RequestCallback) => {
|
||||
// console.log(url, options)
|
||||
console.log('---start---', url)
|
||||
headers = Object.assign({}, headers)
|
||||
|
||||
return request(url, {
|
||||
...options,
|
||||
method,
|
||||
headers: Object.assign({}, defaultHeaders, headers),
|
||||
timeout,
|
||||
agent: getRequestAgent(url),
|
||||
json: format === 'json',
|
||||
}, (err, resp, body) => {
|
||||
callback(err, resp, body)
|
||||
})
|
||||
}
|
||||
export type RequestOptions = Options
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
createMusicInfoOrderClearStatement,
|
||||
createMusicInfoByListAndMusicInfoIdQueryStatement,
|
||||
createMusicInfoByMusicInfoIdQueryStatement,
|
||||
createMusicInfoOrderStatement,
|
||||
} from './statements'
|
||||
|
||||
const idFixRxp = /\.0$/
|
||||
@@ -368,3 +369,14 @@ export const overwriteListData = (lists: LX.DBService.UserListInfo[], musicInfos
|
||||
})(lists, musicInfos)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取列表内音乐的排序
|
||||
* @param listId 列表id
|
||||
* @param musicInfoId 音乐id
|
||||
* @returns 音乐排序信息
|
||||
*/
|
||||
export const getMusicInfoOrder = (listId: string, musicInfoId: string) => {
|
||||
const musicInfoOrderStatement = createMusicInfoOrderStatement()
|
||||
return musicInfoOrderStatement.get({ listId, musicInfoId })
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
updateMusicInfoOrder,
|
||||
updateMusicInfos,
|
||||
updateUserLists as updateUserListsFromDB,
|
||||
getMusicInfoOrder,
|
||||
} from './dbHelper'
|
||||
|
||||
let userLists: LX.DBService.UserListInfo[]
|
||||
let musicLists = new Map<string, LX.Music.MusicInfo[]>()
|
||||
let rawPoss = new Map<string, number>()
|
||||
|
||||
const toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset: number = 0): LX.DBService.MusicInfo[] => {
|
||||
return musicInfos.map((info, index) => {
|
||||
@@ -41,8 +43,10 @@ const toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset:
|
||||
export const getAllUserList = (): LX.List.UserListInfo[] => {
|
||||
userLists ??= queryAllUserList()
|
||||
|
||||
rawPoss.clear()
|
||||
return userLists.map(list => {
|
||||
const { position, ...newList } = list
|
||||
rawPoss.set(list.id, position)
|
||||
return newList
|
||||
})
|
||||
}
|
||||
@@ -55,10 +59,15 @@ export const getAllUserList = (): LX.List.UserListInfo[] => {
|
||||
export const createUserLists = (position: number, lists: LX.List.UserListInfo[]) => {
|
||||
userLists ??= queryAllUserList()
|
||||
if (position < 0 || position >= userLists.length) {
|
||||
// 如果是最末尾,那么取最后一个列表的原始位置 + 1 作为新列表的原始位置,否则新列表的原始位置为 position
|
||||
// 因为原始 pos 可能比 userLists.length 大,所以不能直接用 userLists.length 作为新列表的原始位置
|
||||
const order = userLists.length ? (rawPoss.get(userLists.at(-1)!.id) ?? userLists.length) + 1 : 0
|
||||
const newLists: LX.DBService.UserListInfo[] = lists.map((list, index) => {
|
||||
const pos = order + index
|
||||
rawPoss.set(list.id, pos)
|
||||
return {
|
||||
...list,
|
||||
position: position + index,
|
||||
position: pos,
|
||||
}
|
||||
})
|
||||
insertUserLists(newLists)
|
||||
@@ -72,6 +81,8 @@ export const createUserLists = (position: number, lists: LX.List.UserListInfo[])
|
||||
})
|
||||
insertUserLists(newUserLists, true)
|
||||
userLists = newUserLists
|
||||
rawPoss.clear()
|
||||
for (const list of userLists) rawPoss.set(list.id, list.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +108,7 @@ export const createUserLists = (position: number, lists: LX.List.UserListInfo[])
|
||||
export const removeUserLists = (ids: string[]) => {
|
||||
deleteUserLists(ids)
|
||||
userLists &&= queryAllUserList()
|
||||
for (const id of ids) rawPoss.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +159,8 @@ export const updateUserListsPosition = (position: number, ids: string[]) => {
|
||||
})
|
||||
insertUserLists(newUserLists, true)
|
||||
userLists = newUserLists
|
||||
rawPoss.clear()
|
||||
for (const list of userLists) rawPoss.set(list.id, list.position)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,10 +224,14 @@ export const musicsAdd = (listId: string, musicInfos: LX.Music.MusicInfo[], addM
|
||||
arrUnshift(targetList, musicInfos)
|
||||
break
|
||||
case 'bottom':
|
||||
default:
|
||||
insertMusicInfoList(toDBMusicInfo(musicInfos, listId, targetList.length))
|
||||
default: {
|
||||
// 如果是添加到最后,那么取最后一个歌曲的原始位置 + 1 作为新歌曲的原始位置,否则新歌曲的原始位置为 position
|
||||
// 因为原始 pos 可能比 targetList.length 大,所以不能直接用 targetList.length 作为新歌曲的原始位置
|
||||
const order = targetList.length ? (getMusicInfoOrder(listId, targetList.at(-1)!.id)?.order ?? targetList.length) + 1 : 0
|
||||
insertMusicInfoList(toDBMusicInfo(musicInfos, listId, order))
|
||||
arrPush(targetList, musicInfos)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +381,8 @@ export const listDataOverwrite = (myListData: MakeOptional<LX.List.ListDataFull,
|
||||
if (userLists) userLists.splice(0, userLists.length, ...dbLists)
|
||||
else userLists = dbLists
|
||||
|
||||
rawPoss.clear()
|
||||
for (const list of userLists) rawPoss.set(list.id, list.position)
|
||||
musicLists.clear()
|
||||
musicLists.set(LIST_IDS.DEFAULT, listData.defaultList)
|
||||
musicLists.set(LIST_IDS.LOVE, listData.loveList)
|
||||
|
||||
@@ -145,6 +145,17 @@ export const createMusicInfoByMusicInfoIdQueryStatement = () => {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建根据列表Id与音乐id查询音乐排序语句
|
||||
* @returns 查询语句
|
||||
*/
|
||||
export const createMusicInfoOrderStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare<[LX.DBService.ListMusicInfoQuery], LX.DBService.MusicInfoOrder>(`SELECT "listId", "musicInfoId", "order"
|
||||
FROM "main"."my_list_music_info_order"
|
||||
WHERE "listId"=@listId AND "musicInfoId"=@musicInfoId`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建音乐信息排序插入语句
|
||||
* @returns 插入语句
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="container" :class="[{ lock: setting['desktopLyric.isLock'] }, { hide: isHide || (isHoverHide && isMouseEnter) }]">
|
||||
<div id="main" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @mousemove="handleMouseMoveMain">
|
||||
<div id="container" :class="[{ lock: setting['desktopLyric.isLock'] }, { hide: isHide || isHoverHide }]">
|
||||
<div id="main">
|
||||
<transition enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut">
|
||||
<div v-show="!setting['desktopLyric.isLock']" class="control-bar">
|
||||
<layout-control-bar />
|
||||
@@ -41,7 +41,7 @@ import usePauseHide from '@lyric/useApp/usePauseHide'
|
||||
const isShowResize = window.os != 'windows'
|
||||
useCommon()
|
||||
const { handleMouseDown, handleTouchDown } = useWindowSize()
|
||||
const { handleMouseMoveMain, isHoverHide, isMouseEnter } = useHoverHide()
|
||||
const isHoverHide = useHoverHide()
|
||||
useLyric()
|
||||
useTheme()
|
||||
const isHide = usePauseHide()
|
||||
@@ -87,7 +87,8 @@ body {
|
||||
}
|
||||
&.hide {
|
||||
opacity: .05;
|
||||
&:hover {
|
||||
|
||||
&:not(.lock):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@common/*": ["../common/*"],
|
||||
// "@renderer/*": ["renderer/*"],
|
||||
"@lyric/*": ["renderer-lyric/*"],
|
||||
"@static/*": ["static/*"],
|
||||
"@root/*": ["./*"],
|
||||
"@lyric/*": ["../renderer-lyric/*"],
|
||||
"@static/*": ["../static/*"],
|
||||
"@root/*": ["../*"],
|
||||
},
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"./types"
|
||||
|
||||
@@ -1,98 +1,41 @@
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { computed, ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { setting } from '@lyric/store/state'
|
||||
|
||||
let mouseCheckTools: {
|
||||
x: number
|
||||
y: number
|
||||
preX: number
|
||||
preY: number
|
||||
timeout: NodeJS.Timeout | null
|
||||
handleCheck: (setShow: () => void) => void
|
||||
handleMove: (x: number, y: number, setShow: () => void) => void
|
||||
startTimeout: (setShow: () => void) => void
|
||||
stopTimeout: () => void
|
||||
} = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
preX: 0,
|
||||
preY: 0,
|
||||
timeout: null,
|
||||
handleCheck(setShow: () => void) {
|
||||
let xDiff = Math.abs(this.x - this.preX)
|
||||
let yDiff = Math.abs(this.y - this.preY)
|
||||
if (xDiff > 8) {
|
||||
if (this.x > this.preX) {
|
||||
if (this.x + xDiff * 1.25 > window.innerWidth - 16) {
|
||||
setShow()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (this.x - xDiff * 1.25 < 8) {
|
||||
setShow()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (yDiff > 8) {
|
||||
if (this.y > this.preY) {
|
||||
if (this.y + yDiff * 1.25 > window.innerHeight - 16) {
|
||||
setShow()
|
||||
}
|
||||
} else {
|
||||
if (this.y - yDiff * 1.25 < 8) {
|
||||
setShow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setShow(false)
|
||||
},
|
||||
handleMove(x: number, y: number, setShow: () => void) {
|
||||
// console.log(x, y, this.x, this.y)
|
||||
this.preX = this.x
|
||||
this.preY = this.y
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.startTimeout(setShow)
|
||||
},
|
||||
startTimeout(setShow: () => void) {
|
||||
this.stopTimeout()
|
||||
this.timeout = setTimeout(this.handleCheck.bind(this), 200, setShow)
|
||||
},
|
||||
stopTimeout() {
|
||||
if (!this.timeout) return
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = null
|
||||
},
|
||||
}
|
||||
import { sendMouseEnterLeave, onMouseEnterLeave } from '@lyric/utils/ipc'
|
||||
|
||||
export default () => {
|
||||
const isMouseEnter = ref(false)
|
||||
|
||||
const isHoverHide = computed(() => {
|
||||
return setting['desktopLyric.isLock'] && setting['desktopLyric.isHoverHide']
|
||||
return setting['desktopLyric.isHoverHide'] && isMouseEnter.value
|
||||
})
|
||||
|
||||
const handleMouseMoveMain = (event: MouseEvent) => {
|
||||
if (!isHoverHide.value) return
|
||||
const handleMouseMove = () => {
|
||||
handleMouseEnter()
|
||||
mouseCheckTools.handleMove(event.clientX, event.clientY, () => {
|
||||
handleMouseLeave()
|
||||
})
|
||||
}
|
||||
const handleMouseEnter = () => {
|
||||
if (!isHoverHide.value || isMouseEnter.value) return
|
||||
if (isMouseEnter.value) return
|
||||
isMouseEnter.value = true
|
||||
sendMouseEnterLeave(true)
|
||||
}
|
||||
const handleMouseLeave = () => {
|
||||
if (!isHoverHide.value) return
|
||||
if (!isMouseEnter.value) return
|
||||
isMouseEnter.value = false
|
||||
mouseCheckTools.stopTimeout()
|
||||
sendMouseEnterLeave(false)
|
||||
}
|
||||
|
||||
return {
|
||||
isMouseEnter,
|
||||
isHoverHide,
|
||||
handleMouseMoveMain,
|
||||
}
|
||||
const removeMouseEnterLeaveListener = onMouseEnterLeave(({ params: isEnter }) => {
|
||||
isMouseEnter.value = isEnter
|
||||
})
|
||||
document.body.addEventListener('mousemove', handleMouseMove)
|
||||
document.body.addEventListener('mouseenter', handleMouseEnter)
|
||||
document.body.addEventListener('mouseleave', handleMouseLeave)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeMouseEnterLeaveListener()
|
||||
document.body.removeEventListener('mousemove', handleMouseMove)
|
||||
document.body.removeEventListener('mouseenter', handleMouseEnter)
|
||||
document.body.removeEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
|
||||
return isHoverHide
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@ export const onMainWindowInited = (listener: LX.IpcRendererEventListener): Remov
|
||||
}
|
||||
}
|
||||
|
||||
export const sendMouseEnterLeave = (isEnter: boolean) => {
|
||||
rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, isEnter)
|
||||
}
|
||||
export const onMouseEnterLeave = (listener: LX.IpcRendererEventListenerParams<boolean>): RemoveListener => {
|
||||
rendererOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Theme Change
|
||||
* @param listener LX.IpcRendererEventListenerParams<shouldUseDarkColors: boolean>
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { togglePlay, playNext, playPrev } from '@renderer/core/player'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { formatMusicName } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
name: 'CorePlayBar',
|
||||
@@ -112,7 +113,7 @@ export default {
|
||||
|
||||
const title = computed(() => {
|
||||
return musicInfo.name
|
||||
? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)
|
||||
? formatMusicName(appSetting['download.fileName'], musicInfo.name, musicInfo.singer)
|
||||
: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { togglePlay, playNext, playPrev } from '@renderer/core/player'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { formatMusicName } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
name: 'CorePlayBar',
|
||||
@@ -112,7 +113,7 @@ export default {
|
||||
|
||||
const title = computed(() => {
|
||||
return musicInfo.name
|
||||
? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)
|
||||
? formatMusicName(appSetting['download.fileName'], musicInfo.name, musicInfo.singer)
|
||||
: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { togglePlay, playNext, playPrev } from '@renderer/core/player'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { formatMusicName } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
name: 'CorePlayBar',
|
||||
@@ -114,7 +115,7 @@ export default {
|
||||
|
||||
const title = computed(() => {
|
||||
return musicInfo.name
|
||||
? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)
|
||||
? formatMusicName(appSetting['download.fileName'], musicInfo.name, musicInfo.singer)
|
||||
: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ export default {
|
||||
// border-bottom: 2px solid var(--color-primary);
|
||||
// border-color: var(--color-primary);
|
||||
border: none;
|
||||
min-width: 0;
|
||||
|
||||
outline: none;
|
||||
// height: @height-toolbar * .7;
|
||||
|
||||
@@ -51,7 +51,7 @@ let convolverDynamicsCompressor: DynamicsCompressorNode
|
||||
let gainNode: GainNode
|
||||
let panner: PannerNode
|
||||
let pitchShifterNode: AudioWorkletNode
|
||||
let pitchShifterNodePitchFactor: AudioParam | null
|
||||
let pitchShifterNodePitchFactor: AudioParam
|
||||
let pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none'
|
||||
let pitchShifterNodeTempValue = 1
|
||||
let defaultChannelCount = 2
|
||||
@@ -65,6 +65,16 @@ export const createAudio = () => {
|
||||
audio.autoplay = true
|
||||
audio.preload = 'auto'
|
||||
audio.crossOrigin = 'anonymous'
|
||||
|
||||
// https://developer.chrome.com/blog/autoplay
|
||||
audio.addEventListener('playing', () => {
|
||||
if (audioContext?.state == 'suspended') {
|
||||
void audioContext.resume().catch((err) => {
|
||||
console.error('Resume audio context failed:', err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const initAnalyser = () => {
|
||||
@@ -322,7 +332,7 @@ const connectPitchShifterNode = () => {
|
||||
// convolverDynamicsCompressor.connect(pitchShifterNode)
|
||||
// pitchShifterNode.connect(panner)
|
||||
pitchShifterNodeLoadStatus = 'connected'
|
||||
pitchShifterNodePitchFactor!.value = pitchShifterNodeTempValue
|
||||
pitchShifterNodePitchFactor.value = pitchShifterNodeTempValue
|
||||
}
|
||||
const disconnectPitchShifterNode = () => {
|
||||
console.log('disconnect Pitch Shifter Node')
|
||||
@@ -331,7 +341,6 @@ const disconnectPitchShifterNode = () => {
|
||||
lastBiquadFilter.connect(convolver)
|
||||
lastBiquadFilter.connect(convolverSourceGainNode)
|
||||
pitchShifterNodeLoadStatus = 'unconnect'
|
||||
pitchShifterNodePitchFactor = null
|
||||
|
||||
audio!.removeEventListener('playing', connectNode)
|
||||
audio!.removeEventListener('pause', disconnectNode)
|
||||
@@ -373,7 +382,7 @@ export const setPitchShifter = (val: number) => {
|
||||
case 'connected':
|
||||
// a: 1 = 半音
|
||||
// value = 2 ** (a / 12)
|
||||
pitchShifterNodePitchFactor!.value = val
|
||||
pitchShifterNodePitchFactor.value = val
|
||||
break
|
||||
case 'unconnect':
|
||||
connectPitchShifterNode()
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@renderer/*": ["renderer/*"],
|
||||
// "@lyric/*": ["renderer-lyric/*"],
|
||||
"@static/*": ["static/*"],
|
||||
"@root/*": ["./*"],
|
||||
"@common/*": ["../common/*"],
|
||||
"@renderer/*": ["../renderer/*"],
|
||||
// "@lyric/*": ["../renderer-lyric/*"],
|
||||
"@static/*": ["../static/*"],
|
||||
"@root/*": ["../*"],
|
||||
},
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"./types"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'
|
||||
import { formatPlayTime } from '@common/utils/common'
|
||||
import type { IComment } from 'music-metadata/lib/type'
|
||||
import { decodeKrc } from '@common/utils/lyricUtils/kg'
|
||||
import { type IAudioMetadata } from 'music-metadata'
|
||||
|
||||
export const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<boolean> => {
|
||||
return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) &&
|
||||
@@ -164,6 +164,7 @@ export const getLocalMusicFilePic = async(path: string) => {
|
||||
// return lyricInfo
|
||||
// }
|
||||
|
||||
type IComment = NonNullable<IAudioMetadata['common']['comment']> extends Array<infer U> ? U : never
|
||||
|
||||
/**
|
||||
* 获取歌曲文件歌词
|
||||
@@ -181,7 +182,7 @@ export const getLocalMusicFileLyric = async(path: string): Promise<LX.Music.Lyri
|
||||
const { confidence, encoding } = detect(lrcBuf)
|
||||
console.log('lrc file encoding', confidence, encoding)
|
||||
if (confidence > 0.8) {
|
||||
const iconv = await import('iconv-lite')
|
||||
const iconv = (await import('iconv-lite')).default
|
||||
if (iconv.encodingExists(encoding)) {
|
||||
const lrc = iconv.decode(lrcBuf, encoding)
|
||||
if (lrc) {
|
||||
|
||||
@@ -49,7 +49,7 @@ export default {
|
||||
}
|
||||
return {
|
||||
singer: decodeName(formatSingerName(rawData.Singers, 'name')),
|
||||
name: decodeName(rawData.SongName),
|
||||
name: decodeName(`${rawData.OriSongName}${rawData.Suffix ? ` ${rawData.Suffix}` : ''}`),
|
||||
albumName: decodeName(rawData.AlbumName),
|
||||
albumId: rawData.AlbumID,
|
||||
songmid: rawData.Audioid,
|
||||
|
||||
@@ -67,31 +67,31 @@ export default {
|
||||
}
|
||||
*/
|
||||
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
const buildParams = (id, isGetLyricx) => {
|
||||
let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
|
||||
if (isGetLyricx) params += '&lrcx=1'
|
||||
const buf_str = Buffer.from(params)
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint16Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_key[j] ^ buf_str[i]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
return Buffer.from(output).toString('base64')
|
||||
}
|
||||
// const buf_key = Buffer.from('yeelion')
|
||||
// const buf_key_len = buf_key.length
|
||||
// const buildParams = (id, isGetLyricx) => {
|
||||
// let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
|
||||
// if (isGetLyricx) params += '&lrcx=1'
|
||||
// const buf_str = Buffer.from(params)
|
||||
// const buf_str_len = buf_str.length
|
||||
// const output = new Uint16Array(buf_str_len)
|
||||
// let i = 0
|
||||
// while (i < buf_str_len) {
|
||||
// let j = 0
|
||||
// while (j < buf_key_len && i < buf_str_len) {
|
||||
// output[i] = buf_key[j] ^ buf_str[i]
|
||||
// i++
|
||||
// j++
|
||||
// }
|
||||
// }
|
||||
// return Buffer.from(output).toString('base64')
|
||||
// }
|
||||
|
||||
// console.log(buildParams('207527604', false))
|
||||
// console.log(buildParams('207527604', true))
|
||||
|
||||
const timeExp = /^\[([\d:.]*)\]{1}/g
|
||||
const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/
|
||||
const existTimeExp = /\[\d{1,2}:.*\d{1,4}]/
|
||||
const lyricxTag = /^<-?\d+,-?\d+>/
|
||||
export default {
|
||||
/* sortLrcArr(arr) {
|
||||
@@ -168,14 +168,6 @@ export default {
|
||||
|
||||
if (!isLyricx && lrcT.length > lrc.length * 0.3 && lrc.length - lrcT.length > 6) {
|
||||
throw new Error('failed')
|
||||
// if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译
|
||||
// const tItem = lrc.pop()
|
||||
// tItem.time = lrc[lrc.length - 1].time
|
||||
// lrcT.push(tItem)
|
||||
// } else {
|
||||
// lrc = arr
|
||||
// lrcT = []
|
||||
// }
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -235,35 +227,35 @@ export default {
|
||||
// },
|
||||
getLyric(musicInfo, isGetLyricx = true) {
|
||||
// this.getLyric2(musicInfo)
|
||||
const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {
|
||||
const requestObj = httpFetch(`http://mlyric.kuwo.cn/mobi.s?f=web&type=lyric&lrcx=${isGetLyricx ? 1 : 0}&rid=${musicInfo.songmid}&encode=utf8`)
|
||||
requestObj.promise = requestObj.promise.then(async({ statusCode, body, raw }) => {
|
||||
if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
|
||||
return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
|
||||
// let lrcInfo
|
||||
// try {
|
||||
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
// } catch {
|
||||
// return Promise.reject(new Error('Get lyric failed'))
|
||||
// }
|
||||
let lrcInfo
|
||||
// console.log(Buffer.from(base64Data, 'base64').toString())
|
||||
try {
|
||||
lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('Get lyric failed'))
|
||||
}
|
||||
// console.log(lrcInfo)
|
||||
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
try {
|
||||
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
} catch {
|
||||
lrcInfo.lxlyric = ''
|
||||
}
|
||||
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))
|
||||
// console.log(lrcInfo)
|
||||
return lrcInfo
|
||||
})
|
||||
const lrcText = await decodeLyric(raw, isGetLyricx)
|
||||
// console.log(lrcText)
|
||||
// let lrcInfo
|
||||
// try {
|
||||
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
// } catch {
|
||||
// return Promise.reject(new Error('Get lyric failed'))
|
||||
// }
|
||||
let lrcInfo
|
||||
// console.log(Buffer.from(base64Data, 'base64').toString())
|
||||
try {
|
||||
lrcInfo = this.parseLrc(lrcText)
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('Get lyric failed'))
|
||||
}
|
||||
// console.log(lrcInfo)
|
||||
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
try {
|
||||
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
} catch {
|
||||
lrcInfo.lxlyric = ''
|
||||
}
|
||||
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))
|
||||
// console.log(lrcInfo)
|
||||
return lrcInfo
|
||||
})
|
||||
return requestObj
|
||||
},
|
||||
|
||||
@@ -145,22 +145,25 @@ export default {
|
||||
}))
|
||||
},
|
||||
filterList2(rawData) {
|
||||
// console.log(rawData)
|
||||
const list = []
|
||||
const allowedTypes = ['songlist', 'list', 'album']
|
||||
// digest: 5 = list, 8 = songlist, 13 = album, 4 = artist
|
||||
rawData.forEach(item => {
|
||||
if (!item.label) return
|
||||
list.push(...item.list.map(item => ({
|
||||
play_count: item.play_count && this.formatPlayCount(item.listencnt),
|
||||
id: `digest-${item.digest}__${item.id}`,
|
||||
author: item.uname,
|
||||
name: item.name,
|
||||
total: item.total,
|
||||
// time: item.publish_time,
|
||||
img: item.img,
|
||||
grade: item.favorcnt && item.favorcnt / 10,
|
||||
desc: item.desc,
|
||||
source: 'kw',
|
||||
})))
|
||||
item.list.forEach(item => {
|
||||
if (!allowedTypes.includes(item.type)) return
|
||||
list.push({
|
||||
play_count: item.play_count && this.formatPlayCount(item.listencnt),
|
||||
id: `digest-${item.digest}__${item.id}`,
|
||||
author: item.uname,
|
||||
name: item.name,
|
||||
total: item.total,
|
||||
// time: item.publish_time,
|
||||
img: item.img,
|
||||
grade: item.favorcnt && item.favorcnt / 10,
|
||||
desc: item.desc,
|
||||
source: 'kw',
|
||||
})
|
||||
})
|
||||
})
|
||||
return list
|
||||
},
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
// import { httpGet, httpFetch } from '../../request'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { rendererInvoke } from '@common/rendererIpc'
|
||||
import { createCipheriv, createDecipheriv } from 'crypto'
|
||||
import { toMD5 } from '../utils'
|
||||
import { inflate } from 'zlib'
|
||||
|
||||
// const kw_token = {
|
||||
// token: null,
|
||||
// isGetingToken: false,
|
||||
// }
|
||||
|
||||
// const translationMap = {
|
||||
// "{'": '{"',
|
||||
// "'}\n": '"}',
|
||||
// "'}": '"}',
|
||||
// "':'": '":"',
|
||||
// "','": '","',
|
||||
// "':{'": '":{"',
|
||||
// "':['": '":["',
|
||||
// "'}],'": '"}],"',
|
||||
// "':[{'": '":[{"',
|
||||
// "'},'": '"},"',
|
||||
// "'},{'": '"},{"',
|
||||
// "':[],'": '":[],"',
|
||||
// "':{},'": '":{},"',
|
||||
// "'}]}": '"}]}',
|
||||
// }
|
||||
const handleInflate = async(data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
inflate(data, (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// export const objStr2JSON = str => {
|
||||
// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s]))
|
||||
// }
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
|
||||
export const decodeLyric = async(rawData, isGetLyricx) => {
|
||||
const buf = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData)
|
||||
if (buf.toString('utf8', 0, 10).toLowerCase() !== 'tp=content') return ''
|
||||
const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4))
|
||||
if (!isGetLyricx) return lrcData.toString('utf8')
|
||||
const buf_str = Buffer.from(lrcData.toString(), 'base64')
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint8Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_str[i] ^ buf_key[j]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(output).toString('utf8')
|
||||
}
|
||||
|
||||
export const objStr2JSON = str => {
|
||||
return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"'))
|
||||
@@ -37,13 +46,13 @@ export const objStr2JSON = str => {
|
||||
|
||||
export const formatSinger = rawData => rawData.replace(/&/g, '、')
|
||||
|
||||
export const matchToken = headers => {
|
||||
try {
|
||||
return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1]
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// export const matchToken = headers => {
|
||||
// try {
|
||||
// return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1]
|
||||
// } catch (err) {
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
|
||||
// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
|
||||
|
||||
@@ -63,8 +72,6 @@ export const matchToken = headers => {
|
||||
// })
|
||||
// })
|
||||
|
||||
export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data)
|
||||
|
||||
// export const tokenRequest = async(url, options = {}) => {
|
||||
// let token = kw_token.token
|
||||
// if (!token) token = await getToken()
|
||||
@@ -88,8 +95,8 @@ export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_
|
||||
|
||||
export const lrcTools = {
|
||||
rxps: {
|
||||
wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/,
|
||||
tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/,
|
||||
wordLine: /^(\[\d{1,2}:.*\d{1,4}])\s*(\S+(?:\s+\S+)*)?\s*/,
|
||||
tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*]/,
|
||||
wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g,
|
||||
wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/,
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@ const mrcTools = {
|
||||
},
|
||||
async getMusicInfo(songInfo) {
|
||||
return songInfo.mrcUrl == null
|
||||
? getMusicInfo(songInfo.copyrightId)
|
||||
? getMusicInfo(songInfo.songmid)
|
||||
: songInfo
|
||||
},
|
||||
getLyric(songInfo) {
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import getSongId from './songId'
|
||||
import { getMusicInfo } from './musicInfo'
|
||||
|
||||
export default {
|
||||
async getPicUrl(songId, tryNum = 0) {
|
||||
let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`, {
|
||||
headers: {
|
||||
Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu',
|
||||
},
|
||||
})
|
||||
requestObj.promise.then(({ body }) => {
|
||||
if (body.returnCode !== '000000') {
|
||||
if (tryNum > 5) return Promise.reject(new Error('图片获取失败'))
|
||||
let tryRequestObj = this.getPic(songId, ++tryNum)
|
||||
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
|
||||
return tryRequestObj.promise
|
||||
}
|
||||
let url = body.largePic || body.mediumPic || body.smallPic
|
||||
if (!/https?:/.test(url)) url = 'http:' + url
|
||||
return url
|
||||
})
|
||||
return requestObj
|
||||
},
|
||||
async getPic(songInfo) {
|
||||
const songId = await getSongId(songInfo)
|
||||
return this.getPicUrl(songId)
|
||||
const info = await getMusicInfo(songInfo.songmid)
|
||||
return info.img
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getMusicInfo } from './musicInfo'
|
||||
|
||||
const getSongId = async(mInfo) => {
|
||||
if (mInfo.songmid != mInfo.copyrightId) return mInfo.songmid
|
||||
const musicInfo = await getMusicInfo(mInfo.copyrightId)
|
||||
const musicInfo = await getMusicInfo(mInfo.songmid)
|
||||
return musicInfo.songmid
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import getMusicInfo from './musicInfo'
|
||||
import { rendererInvoke } from '@common/rendererIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { decodeQrc } from './qrcDecode'
|
||||
|
||||
const songIdMap = new Map()
|
||||
const promises = new Map()
|
||||
export const decodeLyric = (lrc, tlrc, rlrc) => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc })
|
||||
export const decodeLyric = async(lrc, tlrc, rlrc) => ({
|
||||
lyric: await decodeQrc(lrc),
|
||||
tlyric: await decodeQrc(tlrc),
|
||||
rlyric: await decodeQrc(rlrc),
|
||||
})
|
||||
|
||||
|
||||
const parseTools = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, sizeFormate } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
import { signRequest } from './utils'
|
||||
|
||||
export default {
|
||||
limit: 50,
|
||||
@@ -10,71 +10,58 @@ export default {
|
||||
successCode: 0,
|
||||
musicSearch(str, page, limit, retryNum = 0) {
|
||||
if (retryNum > 5) return Promise.reject(new Error('搜索失败'))
|
||||
// searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=sizer.yqq.song_next&searchid=49252838123499591&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`)
|
||||
// const searchRequest = httpFetch(`https://shc.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&remoteplace=txt.yqq.top&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&cv=4747474&ct=24&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&uin=0&hostUin=0&loginUin=0`)
|
||||
const searchRequest = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': 'QQMusic 14090508(android 12)',
|
||||
const searchRequest = signRequest({
|
||||
comm: {
|
||||
_channelid: '0',
|
||||
_os_version: '6.2.9200-2',
|
||||
ct: '19',
|
||||
cv: '2151',
|
||||
guid: '1F70E520B2EAA7D25E11760783C53CA9',
|
||||
patch: '118',
|
||||
psrf_access_token_expiresAt: 0,
|
||||
psrf_qqaccess_token: '',
|
||||
psrf_qqopenid: '',
|
||||
psrf_qqunionid: '',
|
||||
tmeAppID: 'qqmusic',
|
||||
tmeLoginType: 0,
|
||||
uin: '0',
|
||||
wid: '7223299733393904640',
|
||||
},
|
||||
body: {
|
||||
comm: {
|
||||
ct: '11',
|
||||
cv: '14090508',
|
||||
v: '14090508',
|
||||
tmeAppID: 'qqmusic',
|
||||
phonetype: 'EBG-AN10',
|
||||
deviceScore: '553.47',
|
||||
devicelevel: '50',
|
||||
newdevicelevel: '20',
|
||||
rom: 'HuaWei/EMOTION/EmotionUI_14.2.0',
|
||||
os_ver: '12',
|
||||
OpenUDID: '0',
|
||||
OpenUDID2: '0',
|
||||
QIMEI36: '0',
|
||||
udid: '0',
|
||||
chid: '0',
|
||||
aid: '0',
|
||||
oaid: '0',
|
||||
taid: '0',
|
||||
tid: '0',
|
||||
wid: '0',
|
||||
uid: '0',
|
||||
sid: '0',
|
||||
modeSwitch: '6',
|
||||
teenMode: '0',
|
||||
ui_mode: '2',
|
||||
nettype: '1020',
|
||||
v4ip: '',
|
||||
},
|
||||
req: {
|
||||
module: 'music.search.SearchCgiService',
|
||||
method: 'DoSearchForQQMusicMobile',
|
||||
param: {
|
||||
search_type: 0,
|
||||
query: str,
|
||||
page_num: page,
|
||||
num_per_page: limit,
|
||||
highlight: 0,
|
||||
nqc_flag: 0,
|
||||
multi_zhida: 0,
|
||||
cat: 2,
|
||||
grp: 1,
|
||||
sin: 0,
|
||||
sem: 0,
|
||||
},
|
||||
'music.search.SearchCgiService': {
|
||||
module: 'music.search.SearchCgiService',
|
||||
method: 'DoSearchForQQMusicDesktop',
|
||||
param: {
|
||||
grp: 1,
|
||||
num_per_page: limit,
|
||||
page_num: page,
|
||||
query: str,
|
||||
remoteplace: 'txt.newclient.top',
|
||||
search_type: 0,
|
||||
searchid: this.getSearchId(),
|
||||
},
|
||||
},
|
||||
})
|
||||
// searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)
|
||||
return searchRequest.promise.then(({ body }) => {
|
||||
return searchRequest.then(({ body }) => {
|
||||
// console.log(body)
|
||||
if (body.code != this.successCode || body.req.code != this.successCode) return this.musicSearch(str, page, limit, ++retryNum)
|
||||
return body.req.data
|
||||
const req = body?.['music.search.SearchCgiService'] ?? body?.req
|
||||
if (!req || body.code != this.successCode || req.code != this.successCode) {
|
||||
return this.musicSearch(str, page, limit, ++retryNum)
|
||||
}
|
||||
return req.data
|
||||
})
|
||||
},
|
||||
/**
|
||||
* PC 客户端版 searchid:32 位大写十六进制 GUID + 5 位补零随机数 = 37 字符。
|
||||
* 对应 QQ 音乐 PC 端 searchid 形状(服务端只需要唯一的会话 ID,形状一致即可)。
|
||||
*/
|
||||
getSearchId() {
|
||||
let guid = ''
|
||||
for (let i = 0; i < 32; i++) guid += Math.floor(Math.random() * 16).toString(16)
|
||||
return guid.toUpperCase() + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
|
||||
},
|
||||
handleResult(rawList) {
|
||||
// console.log(rawList)
|
||||
if (!rawList || !Array.isArray(rawList)) return []
|
||||
const list = []
|
||||
rawList.forEach(item => {
|
||||
if (!item.file?.media_mid) return
|
||||
@@ -119,11 +106,12 @@ export default {
|
||||
}
|
||||
list.push({
|
||||
singer: formatSingerName(item.singer, 'name'),
|
||||
name: item.name + (item.title_extra ?? ''),
|
||||
// name: item.name + (item.title_extra ?? ''),
|
||||
name: item.title,
|
||||
albumName,
|
||||
albumId,
|
||||
source: 'tx',
|
||||
interval: formatPlayTime(item.interval),
|
||||
interval: item.interval ? formatPlayTime(item.interval) : null,
|
||||
songId: item.id,
|
||||
albumMid: item.album?.mid ?? '',
|
||||
strMediaMid: item.file.media_mid,
|
||||
@@ -141,11 +129,10 @@ export default {
|
||||
},
|
||||
search(str, page = 1, limit) {
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then(({ body, meta }) => {
|
||||
let list = this.handleResult(body.item_song)
|
||||
let list = this.handleResult(body.song.list)
|
||||
|
||||
this.total = meta.estimate_sum
|
||||
this.total = meta.sum
|
||||
this.page = page
|
||||
this.allPage = Math.ceil(this.total / limit)
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { inflate, constants } from 'zlib'
|
||||
|
||||
/**
|
||||
* QRC 歌词解密(3DES-ECB + zlib inflate)
|
||||
*
|
||||
* 由原 C++ native 插件(qrc_decode.node)1:1 移植为纯 JS 实现。
|
||||
* 加密算法为标准 3DES(DES-EDE3,ECB 模式、无填充),密钥固定。
|
||||
* 数据流程:hex 字符串 -> 字节 -> 3DES 解密 -> zlib inflate -> UTF-8 文本。
|
||||
*/
|
||||
|
||||
const DES_ENCRYPT = 1
|
||||
const DES_DECRYPT = 0
|
||||
|
||||
// DES S-box。注意:QRC 使用的是非标准 DES 变体,
|
||||
// 其中 S2[23]=15、S4[53]=10 两处与标准 DES 不同(标准为 14、1)。
|
||||
// 这两个值与原 native 插件(qrc_decode.node)二进制完全一致,不可改为标准值,
|
||||
// 否则解密结果错误。下表逐字节对应原 C++ 实现。
|
||||
const sbox = [
|
||||
[
|
||||
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
||||
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
|
||||
],
|
||||
[
|
||||
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5,
|
||||
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
|
||||
],
|
||||
[
|
||||
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
||||
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
|
||||
],
|
||||
[
|
||||
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
||||
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
|
||||
],
|
||||
[
|
||||
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
||||
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
|
||||
],
|
||||
[
|
||||
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
||||
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
||||
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
|
||||
],
|
||||
[
|
||||
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
||||
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11,
|
||||
],
|
||||
]
|
||||
|
||||
const key_rnd_shift = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||
const key_perm_c = [
|
||||
56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
||||
]
|
||||
const key_perm_d = [
|
||||
62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3,
|
||||
]
|
||||
const key_compression = [
|
||||
13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46,
|
||||
54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31,
|
||||
]
|
||||
|
||||
// 全部以无符号 32 位处理,>>> 0 保证溢出行为与 C++ uint32_t 一致
|
||||
const bitnum = (a, b, c) => (((a[((b / 32) | 0) * 4 + 3 - (((b % 32) / 8) | 0)] >>> (7 - (b % 8))) & 1) << c) >>> 0
|
||||
|
||||
const bitnum_intr = (a, b, c) => (((a >>> (31 - b)) & 1) << c) >>> 0
|
||||
|
||||
const bitnum_intl = (a, b, c) => (((((a << b) >>> 0) & 0x80000000) >>> 0) >>> c) >>> 0
|
||||
|
||||
const sbox_bit = a => (a & 32) | ((a & 31) >>> 1) | ((a & 1) << 4)
|
||||
|
||||
// 初始置换:返回 [s0, s1]
|
||||
const initial_permutation = (input) => {
|
||||
const s0 = (
|
||||
bitnum(input, 57, 31) | bitnum(input, 49, 30) | bitnum(input, 41, 29) | bitnum(input, 33, 28) |
|
||||
bitnum(input, 25, 27) | bitnum(input, 17, 26) | bitnum(input, 9, 25) | bitnum(input, 1, 24) |
|
||||
bitnum(input, 59, 23) | bitnum(input, 51, 22) | bitnum(input, 43, 21) | bitnum(input, 35, 20) |
|
||||
bitnum(input, 27, 19) | bitnum(input, 19, 18) | bitnum(input, 11, 17) | bitnum(input, 3, 16) |
|
||||
bitnum(input, 61, 15) | bitnum(input, 53, 14) | bitnum(input, 45, 13) | bitnum(input, 37, 12) |
|
||||
bitnum(input, 29, 11) | bitnum(input, 21, 10) | bitnum(input, 13, 9) | bitnum(input, 5, 8) |
|
||||
bitnum(input, 63, 7) | bitnum(input, 55, 6) | bitnum(input, 47, 5) | bitnum(input, 39, 4) |
|
||||
bitnum(input, 31, 3) | bitnum(input, 23, 2) | bitnum(input, 15, 1) | bitnum(input, 7, 0)
|
||||
) >>> 0
|
||||
const s1 = (
|
||||
bitnum(input, 56, 31) | bitnum(input, 48, 30) | bitnum(input, 40, 29) | bitnum(input, 32, 28) |
|
||||
bitnum(input, 24, 27) | bitnum(input, 16, 26) | bitnum(input, 8, 25) | bitnum(input, 0, 24) |
|
||||
bitnum(input, 58, 23) | bitnum(input, 50, 22) | bitnum(input, 42, 21) | bitnum(input, 34, 20) |
|
||||
bitnum(input, 26, 19) | bitnum(input, 18, 18) | bitnum(input, 10, 17) | bitnum(input, 2, 16) |
|
||||
bitnum(input, 60, 15) | bitnum(input, 52, 14) | bitnum(input, 44, 13) | bitnum(input, 36, 12) |
|
||||
bitnum(input, 28, 11) | bitnum(input, 20, 10) | bitnum(input, 12, 9) | bitnum(input, 4, 8) |
|
||||
bitnum(input, 62, 7) | bitnum(input, 54, 6) | bitnum(input, 46, 5) | bitnum(input, 38, 4) |
|
||||
bitnum(input, 30, 3) | bitnum(input, 22, 2) | bitnum(input, 14, 1) | bitnum(input, 6, 0)
|
||||
) >>> 0
|
||||
return [s0, s1]
|
||||
}
|
||||
|
||||
// 逆置换:将 s0/s1 写回 8 字节 out
|
||||
const inverse_permutation = (s0, s1, out) => {
|
||||
out[3] = (bitnum_intr(s1, 7, 7) | bitnum_intr(s0, 7, 6) | bitnum_intr(s1, 15, 5) | bitnum_intr(s0, 15, 4) | bitnum_intr(s1, 23, 3) | bitnum_intr(s0, 23, 2) | bitnum_intr(s1, 31, 1) | bitnum_intr(s0, 31, 0)) & 0xff
|
||||
out[2] = (bitnum_intr(s1, 6, 7) | bitnum_intr(s0, 6, 6) | bitnum_intr(s1, 14, 5) | bitnum_intr(s0, 14, 4) | bitnum_intr(s1, 22, 3) | bitnum_intr(s0, 22, 2) | bitnum_intr(s1, 30, 1) | bitnum_intr(s0, 30, 0)) & 0xff
|
||||
out[1] = (bitnum_intr(s1, 5, 7) | bitnum_intr(s0, 5, 6) | bitnum_intr(s1, 13, 5) | bitnum_intr(s0, 13, 4) | bitnum_intr(s1, 21, 3) | bitnum_intr(s0, 21, 2) | bitnum_intr(s1, 29, 1) | bitnum_intr(s0, 29, 0)) & 0xff
|
||||
out[0] = (bitnum_intr(s1, 4, 7) | bitnum_intr(s0, 4, 6) | bitnum_intr(s1, 12, 5) | bitnum_intr(s0, 12, 4) | bitnum_intr(s1, 20, 3) | bitnum_intr(s0, 20, 2) | bitnum_intr(s1, 28, 1) | bitnum_intr(s0, 28, 0)) & 0xff
|
||||
out[7] = (bitnum_intr(s1, 3, 7) | bitnum_intr(s0, 3, 6) | bitnum_intr(s1, 11, 5) | bitnum_intr(s0, 11, 4) | bitnum_intr(s1, 19, 3) | bitnum_intr(s0, 19, 2) | bitnum_intr(s1, 27, 1) | bitnum_intr(s0, 27, 0)) & 0xff
|
||||
out[6] = (bitnum_intr(s1, 2, 7) | bitnum_intr(s0, 2, 6) | bitnum_intr(s1, 10, 5) | bitnum_intr(s0, 10, 4) | bitnum_intr(s1, 18, 3) | bitnum_intr(s0, 18, 2) | bitnum_intr(s1, 26, 1) | bitnum_intr(s0, 26, 0)) & 0xff
|
||||
out[5] = (bitnum_intr(s1, 1, 7) | bitnum_intr(s0, 1, 6) | bitnum_intr(s1, 9, 5) | bitnum_intr(s0, 9, 4) | bitnum_intr(s1, 17, 3) | bitnum_intr(s0, 17, 2) | bitnum_intr(s1, 25, 1) | bitnum_intr(s0, 25, 0)) & 0xff
|
||||
out[4] = (bitnum_intr(s1, 0, 7) | bitnum_intr(s0, 0, 6) | bitnum_intr(s1, 8, 5) | bitnum_intr(s0, 8, 4) | bitnum_intr(s1, 16, 3) | bitnum_intr(s0, 16, 2) | bitnum_intr(s1, 24, 1) | bitnum_intr(s0, 24, 0)) & 0xff
|
||||
}
|
||||
|
||||
// Feistel 轮函数 f(state, key)
|
||||
const des_f = (state, key) => {
|
||||
const t1 = (
|
||||
bitnum_intl(state, 31, 0) | (((state & 0xF0000000) >>> 0) >>> 1) | bitnum_intl(state, 4, 5) |
|
||||
bitnum_intl(state, 3, 6) | (((state & 0x0F000000) >>> 0) >>> 3) | bitnum_intl(state, 8, 11) |
|
||||
bitnum_intl(state, 7, 12) | (((state & 0x00F00000) >>> 0) >>> 5) | bitnum_intl(state, 12, 17) |
|
||||
bitnum_intl(state, 11, 18) | (((state & 0x000F0000) >>> 0) >>> 7) | bitnum_intl(state, 16, 23)
|
||||
) >>> 0
|
||||
const t2 = (
|
||||
bitnum_intl(state, 15, 0) | (((state & 0x0000F000) << 15) >>> 0) | bitnum_intl(state, 20, 5) |
|
||||
bitnum_intl(state, 19, 6) | (((state & 0x00000F00) << 13) >>> 0) | bitnum_intl(state, 24, 11) |
|
||||
bitnum_intl(state, 23, 12) | (((state & 0x000000F0) << 11) >>> 0) | bitnum_intl(state, 28, 17) |
|
||||
bitnum_intl(state, 27, 18) | (((state & 0x0000000F) << 9) >>> 0) | bitnum_intl(state, 0, 23)
|
||||
) >>> 0
|
||||
|
||||
const lrgstate = [
|
||||
(t1 >>> 24) & 0xFF, (t1 >>> 16) & 0xFF, (t1 >>> 8) & 0xFF,
|
||||
(t2 >>> 24) & 0xFF, (t2 >>> 16) & 0xFF, (t2 >>> 8) & 0xFF,
|
||||
]
|
||||
for (let i = 0; i < 6; i++) lrgstate[i] ^= key[i]
|
||||
|
||||
let s = (
|
||||
(sbox[0][sbox_bit(lrgstate[0] >>> 2)] << 28) |
|
||||
(sbox[1][sbox_bit(((lrgstate[0] & 0x03) << 4) | (lrgstate[1] >>> 4))] << 24) |
|
||||
(sbox[2][sbox_bit(((lrgstate[1] & 0x0F) << 2) | (lrgstate[2] >>> 6))] << 20) |
|
||||
(sbox[3][sbox_bit(lrgstate[2] & 0x3F)] << 16) |
|
||||
(sbox[4][sbox_bit(lrgstate[3] >>> 2)] << 12) |
|
||||
(sbox[5][sbox_bit(((lrgstate[3] & 0x03) << 4) | (lrgstate[4] >>> 4))] << 8) |
|
||||
(sbox[6][sbox_bit(((lrgstate[4] & 0x0F) << 2) | (lrgstate[5] >>> 6))] << 4) |
|
||||
sbox[7][sbox_bit(lrgstate[5] & 0x3F)]
|
||||
) >>> 0
|
||||
|
||||
return (
|
||||
bitnum_intl(s, 15, 0) | bitnum_intl(s, 6, 1) | bitnum_intl(s, 19, 2) | bitnum_intl(s, 20, 3) |
|
||||
bitnum_intl(s, 28, 4) | bitnum_intl(s, 11, 5) | bitnum_intl(s, 27, 6) | bitnum_intl(s, 16, 7) |
|
||||
bitnum_intl(s, 0, 8) | bitnum_intl(s, 14, 9) | bitnum_intl(s, 22, 10) | bitnum_intl(s, 25, 11) |
|
||||
bitnum_intl(s, 4, 12) | bitnum_intl(s, 17, 13) | bitnum_intl(s, 30, 14) | bitnum_intl(s, 9, 15) |
|
||||
bitnum_intl(s, 1, 16) | bitnum_intl(s, 7, 17) | bitnum_intl(s, 23, 18) | bitnum_intl(s, 13, 19) |
|
||||
bitnum_intl(s, 31, 20) | bitnum_intl(s, 26, 21) | bitnum_intl(s, 2, 22) | bitnum_intl(s, 8, 23) |
|
||||
bitnum_intl(s, 18, 24) | bitnum_intl(s, 12, 25) | bitnum_intl(s, 29, 26) | bitnum_intl(s, 5, 27) |
|
||||
bitnum_intl(s, 21, 28) | bitnum_intl(s, 10, 29) | bitnum_intl(s, 3, 30) | bitnum_intl(s, 24, 31)
|
||||
) >>> 0
|
||||
}
|
||||
|
||||
// 单次 DES 加/解密一个 8 字节分组(轮密钥 schedule 为 16x6)
|
||||
const des_crypt = (input, schedule, output) => {
|
||||
let [s0, s1] = initial_permutation(input)
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const prev = s1
|
||||
s1 = (des_f(s1, schedule[i]) ^ s0) >>> 0
|
||||
s0 = prev
|
||||
}
|
||||
s0 = (des_f(s1, schedule[15]) ^ s0) >>> 0
|
||||
inverse_permutation(s0, s1, output)
|
||||
}
|
||||
|
||||
// 生成 16 轮子密钥
|
||||
const key_schedule = (key, mode) => {
|
||||
const schedule = Array.from({ length: 16 }, () => new Uint8Array(6))
|
||||
let c = 0
|
||||
let d = 0
|
||||
for (let i = 0; i < 28; i++) {
|
||||
c = (c | bitnum(key, key_perm_c[i], 31 - i)) >>> 0
|
||||
d = (d | bitnum(key, key_perm_d[i], 31 - i)) >>> 0
|
||||
}
|
||||
for (let i = 0; i < 16; i++) {
|
||||
c = ((((c << key_rnd_shift[i]) >>> 0) | (c >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0
|
||||
d = ((((d << key_rnd_shift[i]) >>> 0) | (d >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0
|
||||
const togen = mode === DES_DECRYPT ? 15 - i : i
|
||||
for (let j = 0; j < 24; j++) {
|
||||
schedule[togen][(j / 8) | 0] |= bitnum_intr(c, key_compression[j], 7 - (j % 8))
|
||||
}
|
||||
for (let j = 24; j < 48; j++) {
|
||||
schedule[togen][(j / 8) | 0] |= bitnum_intr(d, key_compression[j] - 27, 7 - (j % 8))
|
||||
}
|
||||
}
|
||||
return schedule
|
||||
}
|
||||
|
||||
// 3DES(EDE3)密钥编排,返回 3 组 schedule
|
||||
const tripledes_key_setup = (key, mode) => {
|
||||
if (mode === DES_ENCRYPT) {
|
||||
return [
|
||||
key_schedule(key.subarray(0, 8), DES_ENCRYPT),
|
||||
key_schedule(key.subarray(8, 16), DES_DECRYPT),
|
||||
key_schedule(key.subarray(16, 24), DES_ENCRYPT),
|
||||
]
|
||||
}
|
||||
return [
|
||||
key_schedule(key.subarray(16, 24), DES_DECRYPT),
|
||||
key_schedule(key.subarray(8, 16), DES_ENCRYPT),
|
||||
key_schedule(key.subarray(0, 8), DES_DECRYPT),
|
||||
]
|
||||
}
|
||||
|
||||
// 3DES 加/解密一个 8 字节分组
|
||||
const tripledes_crypt = (input, schedule, output) => {
|
||||
const buf = new Uint8Array(8)
|
||||
des_crypt(input, schedule[0], buf)
|
||||
des_crypt(buf, schedule[1], output)
|
||||
des_crypt(output, schedule[2], buf)
|
||||
output.set(buf)
|
||||
}
|
||||
|
||||
// QRC 固定密钥:!@#)(*$%123ZXC!@!@#)(NHL
|
||||
const QRC_KEY = Buffer.from([
|
||||
0x21, 0x40, 0x23, 0x29, 0x28, 0x2a, 0x24, 0x25, 0x31, 0x32, 0x33, 0x5a,
|
||||
0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c,
|
||||
])
|
||||
|
||||
const handleInflate = (data) => new Promise((resolve, reject) => {
|
||||
// Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为)
|
||||
inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => {
|
||||
if (err) reject(err)
|
||||
else resolve(result)
|
||||
})
|
||||
})
|
||||
/**
|
||||
* 解密腾讯 QRC 歌词。
|
||||
* @param {string} hexData 服务端返回的十六进制字符串
|
||||
* @returns {Promise<string>} 解密并解压后的歌词文本(解析失败时返回空字符串)
|
||||
*/
|
||||
export const decodeQrc = async(hexData) => {
|
||||
if (!hexData || hexData.length % 2 !== 0) return ''
|
||||
const encrypted = Buffer.from(hexData, 'hex')
|
||||
if (encrypted.length === 0) return ''
|
||||
|
||||
const schedule = tripledes_key_setup(QRC_KEY, DES_DECRYPT)
|
||||
|
||||
const block = new Uint8Array(8)
|
||||
for (let i = 0; i + 8 <= encrypted.length; i += 8) {
|
||||
tripledes_crypt(encrypted.subarray(i, i + 8), schedule, block)
|
||||
encrypted.set(block, i)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handleInflate(encrypted)
|
||||
return result.toString('utf8')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,65 @@ export default {
|
||||
return id
|
||||
},
|
||||
// 获取歌曲列表内的音乐
|
||||
async getListDetail2(id, tryNum = 0) {
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
|
||||
const requestObj_listDetail = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
Origin: 'https://y.qq.com',
|
||||
Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`,
|
||||
},
|
||||
body: {
|
||||
comm: {
|
||||
cv: 4747474,
|
||||
ct: 24,
|
||||
format: 'json',
|
||||
inCharset: 'utf-8',
|
||||
outCharset: 'utf-8',
|
||||
platform: 'yqq.json',
|
||||
needNewCode: 1,
|
||||
uin: 0,
|
||||
},
|
||||
req_1: {
|
||||
module: 'music.srfDissInfo.aiDissInfo',
|
||||
method: 'uniform_get_Dissinfo',
|
||||
param: {
|
||||
disstid: parseInt(id),
|
||||
userinfo: 1,
|
||||
tag: 1,
|
||||
orderlist: 1,
|
||||
song_begin: 0,
|
||||
song_num: this.limit_song,
|
||||
onlysonglist: 0,
|
||||
enc_host_uin: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const { body } = await requestObj_listDetail.promise
|
||||
// console.log(body)
|
||||
if (body.code !== this.successCode) return this.getListDetail2(id, ++tryNum)
|
||||
if (body.req_1.code !== this.successCode) throw new Error('failed')
|
||||
|
||||
const result = body.req_1.data
|
||||
const dirinfo = result.dirinfo
|
||||
return {
|
||||
list: this.filterListDetail(result.songlist),
|
||||
page: 1,
|
||||
limit: this.limit_song,
|
||||
total: result.songnum,
|
||||
source: 'tx',
|
||||
info: {
|
||||
name: dirinfo.title,
|
||||
img: dirinfo.picurl,
|
||||
desc: decodeName(dirinfo.desc ?? '').replace(/<br>/g, '\n'),
|
||||
author: dirinfo.host_nick,
|
||||
play_count: formatPlayCount(dirinfo.listennum),
|
||||
},
|
||||
}
|
||||
},
|
||||
// 获取歌曲列表内的音乐
|
||||
async getListDetail(id, tryNum = 0) {
|
||||
if (tryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
|
||||
@@ -206,7 +265,9 @@ export default {
|
||||
})
|
||||
const { body } = await requestObj_listDetail.promise
|
||||
|
||||
// console.log(body)
|
||||
if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)
|
||||
if (body.subcode !== this.successCode || !body.cdlist) return this.getListDetail2(id)
|
||||
const cdlist = body.cdlist[0]
|
||||
return {
|
||||
list: this.filterListDetail(cdlist.songlist),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
|
||||
const PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
|
||||
const SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179]
|
||||
|
||||
async function hashSHA1(data) {
|
||||
return crypto.createHash('sha1').update(data).digest('hex')
|
||||
}
|
||||
|
||||
function pickHashByIdx(hash, indexes) {
|
||||
return indexes.map((idx) => hash[idx]).join('')
|
||||
}
|
||||
|
||||
function base64Encode(data) {
|
||||
return Buffer.from(data)
|
||||
.toString('base64')
|
||||
.replace(/[\\/+=]/g, '')
|
||||
}
|
||||
|
||||
export async function zzcSign(text) {
|
||||
const hash = await hashSHA1(text)
|
||||
const part1 = pickHashByIdx(hash, PART_1_INDEXES)
|
||||
const part2 = pickHashByIdx(hash, PART_2_INDEXES)
|
||||
const part3 = SCRAMBLE_VALUES.map((value, i) => value ^ parseInt(hash.slice(i * 2, i * 2 + 2), 16))
|
||||
const b64Part = base64Encode(part3).replace(/[\\/+=]/g, '')
|
||||
return `zzc${part1}${b64Part}${part2}`.toLowerCase()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
import { httpFetch } from '../../../request'
|
||||
import { zzcSign } from './crypto'
|
||||
|
||||
export const signRequest = async(data) => {
|
||||
// console.log(data)
|
||||
const sign = await zzcSign(JSON.stringify(data))
|
||||
// console.log('sign', sign)
|
||||
return httpFetch(`https://u.y.qq.com/cgi-bin/musics.fcg?sign=${sign}`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': 'QQMusic 14090508(android 12)',
|
||||
},
|
||||
body: data,
|
||||
}).promise
|
||||
}
|
||||
@@ -10,12 +10,21 @@ export default {
|
||||
page: 0,
|
||||
allPage: 1,
|
||||
musicSearch(str, page, limit) {
|
||||
const searchRequest = eapiRequest('/api/cloudsearch/pc', {
|
||||
s: str,
|
||||
type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||
limit,
|
||||
total: page == 1,
|
||||
// const searchRequest = eapiRequest('/api/cloudsearch/pc', {
|
||||
// s: str,
|
||||
// type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||
// limit,
|
||||
// total: page == 1,
|
||||
// offset: limit * (page - 1),
|
||||
// })
|
||||
const searchRequest = eapiRequest('/api/search/song/list/page', {
|
||||
keyword: str,
|
||||
needCorrect: '1',
|
||||
channel: 'typing',
|
||||
offset: limit * (page - 1),
|
||||
scene: 'normal',
|
||||
total: page == 1,
|
||||
limit,
|
||||
})
|
||||
return searchRequest.promise.then(({ body }) => body)
|
||||
},
|
||||
@@ -30,6 +39,7 @@ export default {
|
||||
// console.log(rawList)
|
||||
if (!rawList) return []
|
||||
return rawList.map(item => {
|
||||
item = item.baseInfo.simpleSongData
|
||||
const types = []
|
||||
const _types = {}
|
||||
let size
|
||||
@@ -87,12 +97,12 @@ export default {
|
||||
return this.musicSearch(str, page, limit).then(result => {
|
||||
// console.log(result)
|
||||
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
|
||||
let list = this.handleResult(result.result.songs || [])
|
||||
let list = this.handleResult(result.data.resources || [])
|
||||
// console.log(list)
|
||||
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = result.result.songCount || 0
|
||||
this.total = result.data.totalCount || 0
|
||||
this.page = page
|
||||
this.allPage = Math.ceil(this.total / this.limit)
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ import useTaskActions from './useTaskActions'
|
||||
import useMusicAdd from './useMusicAdd'
|
||||
import { downloadStatus } from '@renderer/store/download/state'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { formatMusicName } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
name: 'Download',
|
||||
@@ -203,7 +204,7 @@ export default {
|
||||
}
|
||||
|
||||
const getName = (downloadInfo) => {
|
||||
return appSetting['download.fileName'].replace('歌名', downloadInfo.metadata.musicInfo.name).replace('歌手', downloadInfo.metadata.musicInfo.singer)
|
||||
return formatMusicName(appSetting['download.fileName'], downloadInfo.metadata.musicInfo.name, downloadInfo.metadata.musicInfo.singer)
|
||||
}
|
||||
const getTypeName = (quality) => {
|
||||
return quality == 'flac24bit' ? 'FLAC Hires' : quality?.toUpperCase()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { removeListMusics } from '@renderer/store/list/action'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { toOldMusicInfo } from '@renderer/utils/index'
|
||||
import { formatMusicName, toOldMusicInfo } from '@renderer/utils/index'
|
||||
import { addDislikeInfo, hasDislike } from '@renderer/core/dislikeList'
|
||||
import { playNext } from '@renderer/core/player'
|
||||
import { playMusicInfo } from '@renderer/store/player/state'
|
||||
@@ -34,7 +34,7 @@ export default ({ props, list, selectedList, removeAllSelect }) => {
|
||||
|
||||
const handleCopyName = index => {
|
||||
const minfo = list.value[index]
|
||||
clipboardWriteText(appSetting['download.fileName'].replace('歌名', minfo.name).replace('歌手', minfo.singer))
|
||||
clipboardWriteText(formatMusicName(appSetting['download.fileName'], minfo.name, minfo.singer))
|
||||
}
|
||||
|
||||
const handleDislikeMusic = async(index) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DOWNLOAD_STATUS, QUALITYS } from '@common/constants'
|
||||
import { filterFileName } from '@common/utils/common'
|
||||
import { buildLyrics } from './lrcTool'
|
||||
import fs from 'fs'
|
||||
import { clipFileNameLength, clipNameLength } from '@common/utils/tools'
|
||||
import { clipFileNameLength, clipNameLength, formatMusicName } from '@common/utils/tools'
|
||||
|
||||
/**
|
||||
* 保存歌词文件
|
||||
@@ -14,7 +14,7 @@ export const saveLrc = async(lrcData: LX.Music.LyricInfo, info: {
|
||||
downloadTlrc: boolean
|
||||
downloadRlrc: boolean
|
||||
}) => {
|
||||
const iconv = await import('iconv-lite')
|
||||
const iconv = (await import('iconv-lite')).default
|
||||
const lrc = buildLyrics(lrcData, info.downloadLxlrc, info.downloadTlrc, info.downloadRlrc)
|
||||
switch (info.format) {
|
||||
case 'gbk':
|
||||
@@ -91,9 +91,7 @@ export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX
|
||||
ext,
|
||||
filePath: '',
|
||||
listId,
|
||||
fileName: filterFileName(`${clipFileNameLength(fileName
|
||||
.replace('歌名', musicInfo.name)
|
||||
.replace('歌手', clipNameLength(musicInfo.singer)))}.${ext}`),
|
||||
fileName: filterFileName(`${clipFileNameLength(formatMusicName(fileName, musicInfo.name, clipNameLength(musicInfo.singer)))}.${ext}`),
|
||||
},
|
||||
}
|
||||
// downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName)
|
||||
|
||||
@@ -243,9 +243,24 @@ export const filterDuplicateMusic = async(list: LX.Music.MusicInfo[], isFilterVa
|
||||
}
|
||||
|
||||
export const searchListMusic = (list: LX.Music.MusicInfo[], text: string) => {
|
||||
const fullMathNameResults = new Set<LX.Music.MusicInfo>()
|
||||
const fullMathSingerResults = new Set<LX.Music.MusicInfo>()
|
||||
const fullMathAlbumResults = new Set<LX.Music.MusicInfo>()
|
||||
const textLower = text.toLowerCase()
|
||||
for (const mInfo of list) {
|
||||
if (mInfo.name?.toLowerCase().includes(textLower)) {
|
||||
fullMathNameResults.add(mInfo)
|
||||
} else if (mInfo.singer?.toLowerCase().includes(textLower)) {
|
||||
fullMathSingerResults.add(mInfo)
|
||||
} else if (mInfo.meta.albumName?.toLowerCase().includes(textLower)) {
|
||||
fullMathAlbumResults.add(mInfo)
|
||||
}
|
||||
}
|
||||
let result: LX.Music.MusicInfo[] = []
|
||||
let rxp = new RegExp(text.split('').map(s => s.replace(/[.*+?^${}()|[\]\\]/, '\\$&')).join('.*') + '.*', 'i')
|
||||
for (const mInfo of list) {
|
||||
if (fullMathNameResults.has(mInfo) || fullMathSingerResults.has(mInfo) || fullMathAlbumResults.has(mInfo)) continue
|
||||
|
||||
const str = `${mInfo.name}${mInfo.singer}${mInfo.meta.albumName ? mInfo.meta.albumName : ''}`
|
||||
if (rxp.test(str)) result.push(mInfo)
|
||||
}
|
||||
@@ -258,7 +273,12 @@ export const searchListMusic = (list: LX.Music.MusicInfo[], text: string) => {
|
||||
data: mInfo,
|
||||
})
|
||||
}
|
||||
return sortedList.map(item => item.data).reverse()
|
||||
return [
|
||||
...fullMathNameResults.values(),
|
||||
...fullMathSingerResults.values(),
|
||||
...fullMathAlbumResults.values(),
|
||||
...sortedList.map(item => item.data).reverse(),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +324,7 @@ export const createLocalMusicInfos = async(filePaths: string[]): Promise<LX.Musi
|
||||
* @param isMerge 是否合并
|
||||
*/
|
||||
export const exportPlayListToText = async(savePath: string, lists: Array<LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull>, isMerge: boolean) => {
|
||||
const iconv = await import('iconv-lite')
|
||||
const iconv = (await import('iconv-lite')).default
|
||||
|
||||
if (isMerge) {
|
||||
await saveStrToFile(savePath,
|
||||
@@ -328,7 +348,7 @@ export const exportPlayListToCSV = async(savePath: string,
|
||||
lists: Array<LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull>,
|
||||
isMerge: boolean,
|
||||
header: string) => {
|
||||
const iconv = await import('iconv-lite')
|
||||
const iconv = (await import('iconv-lite')).default
|
||||
|
||||
const filterStr = (str: string) => {
|
||||
if (!str) return ''
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "@common/*": ["common/*"],
|
||||
// "@renderer/*": ["renderer/*"],
|
||||
|
||||
Reference in New Issue
Block a user