diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index b934f52f..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,21 +0,0 @@ -platform: - - x64 - -cache: - - node_modules - - '%APPDATA%\npm-cache' - - '%LOCALAPPDATA%\electron\Cache' - - '%LOCALAPPDATA%\electron-builder\Cache' - -install: - - ps: Install-Product node 12 x64 - - npm install - -build_script: - - npm run publish:gh - -test: off - -branches: - only: - - master diff --git a/.babelrc b/.babelrc index b4f2472d..c3affdfe 100644 --- a/.babelrc +++ b/.babelrc @@ -19,6 +19,7 @@ "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-modules-umd", - "@babel/plugin-transform-runtime" + "@babel/plugin-transform-runtime", + "@babel/plugin-proposal-class-properties" ] } diff --git a/.github/workflows/beta-pack.yml b/.github/workflows/beta-pack.yml new file mode 100644 index 00000000..0f6631c7 --- /dev/null +++ b/.github/workflows/beta-pack.yml @@ -0,0 +1,145 @@ +name: Build Beta + +on: + push: + branches: + - beta + +jobs: + Windows: + name: Windows + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + %APPDATA%\npm-cache + %LOCALAPPDATA%\electron\Cache + %LOCALAPPDATA%\electron-builder\Cache + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Build Package + run: | + npm run pack:win:setup:x86_64 + npm run pack:win:7z:x64 + npm run pack:win:7z:x86 + npm run pack:win:7z:arm64 + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + path: | + build/*.exe + build/*.7z + + Mac: + name: Mac + runs-on: macos-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + $HOME/.cache/electron + $HOME/.cache/electron-builder + $HOME/.npm/_prebuilds + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Build Package + run: npm run pack:mac:dmg + env: + ELECTRON_CACHE: $HOME/.cache/electron + ELECTRON_BUILDERCACHE: $HOME/.cache/electron-builder + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + path: | + build/*.dmg + + Linux: + name: Linux + runs-on: ubuntu-latest + steps: + - name: Install package + run: sudo apt-get install -y rpm libarchive-tools + + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + $HOME/.cache/electron + $HOME/.cache/electron-builder + $HOME/.npm/_prebuilds + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Build Package + run: | + npm run pack:linux:deb:x64 + npm run pack:linux:deb:x86 + npm run pack:linux:deb:arm64 + npm run pack:linux:deb:armv7l + npm run pack:linux:appImage + npm run pack:linux:rpm + npm run pack:linux:pacman + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + path: | + build/*.deb + build/*.appImage + build/*.rpm + build/*.pacman diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b97efa66 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +name: Build + +on: + push: + branches: + - master + +jobs: + Windows: + name: Windows + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + %APPDATA%\npm-cache + %LOCALAPPDATA%\electron\Cache + %LOCALAPPDATA%\electron-builder\Cache + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Release package + run: | + npm run publish:win:setup:always + npm run publish:win:7z:x64 + npm run publish:win:7z:x86 + npm run publish:win:7z:arm64 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BT_TOKEN: ${{ secrets.BT_TOKEN }} + + Mac: + name: Mac + runs-on: macos-latest + steps: + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + $HOME/.cache/electron + $HOME/.cache/electron-builder + $HOME/.npm/_prebuilds + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Release package + run: npm run publish:mac:dmg:always + env: + ELECTRON_CACHE: $HOME/.cache/electron + ELECTRON_BUILDERCACHE: $HOME/.cache/electron-builder + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BT_TOKEN: ${{ secrets.BT_TOKEN }} + + Linux: + name: Linux + runs-on: ubuntu-latest + steps: + - name: Install package + run: sudo apt-get install -y rpm libarchive-tools + + - name: Check out git repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '12.x' + + - name: Cache file + uses: actions/cache@v2 + with: + path: | + node_modules + $HOME/.cache/electron + $HOME/.cache/electron-builder + $HOME/.npm/_prebuilds + key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install Dependencies + run: npm install + + - name: Build src code + run: npm run build:src + + - name: Release package + run: | + npm run publish:linux:deb:x64:always + npm run publish:linux:deb:x86 + npm run publish:linux:deb:arm64 + npm run publish:linux:deb:armv7l + npm run publish:linux:appImage + npm run publish:linux:rpm + npm run publish:linux:pacman + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BT_TOKEN: ${{ secrets.BT_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7284625e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -language: node_js -node_js: 12 - -matrix: - include: - - os: osx - osx_image: xcode10.2 - env: - - ELECTRON_CACHE=$HOME/.cache/electron - - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - - - os: linux - dist: trusty - sudo: required - addons: - apt: - packages: - - rpm - - bsdtar - -cache: - directories: - - node_modules - - $HOME/.cache/electron - - $HOME/.cache/electron-builder - - $HOME/.npm/_prebuilds - -notifications: - email: false - -script: - - node --version - - npm --version - - | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then - npm install && npm run publish:gh:linux - else - npm run publish:gh:mac - fi - -before_cache: - - rm -rf $HOME/.cache/electron-builder/wine - -# only run this script on pull requests and merges into -# the 'master' and 'prod' branches -branches: - only: - - master - - dev diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4ff61fa1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "path-intellisense.mappings": { + "@main/*": "${workspaceFolder}/src/main/*", + "@renderer/*": "${workspaceFolder}/src/renderer/*", + "@lyric/*": "${workspaceFolder}/src/renderer-lyric/*", + "@static/*": "${workspaceFolder}/src/static/*", + "@common/*": "${workspaceFolder}/src/common/*", + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b4a58e6..5a34c2b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,121 @@ 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/). +## [1.8.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.8.1...v1.8.2) - 2021-03-09 + +### 修复 + +- 修复歌曲ID存储变更导致酷狗图片获取失败的问题 +- 修复收藏的在线列表id迁移保存出错的问题 + +## [1.8.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.8.0...v1.8.1) - 2021-03-07 + +### 修复 + +- 修复歌词翻译的主题颜色适配问题 + +## [1.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.1...v1.8.0) - 2021-03-07 + +### 新增 + +- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用 +- 新增启动参数`-play`,可以在启动软件时播放指定歌单,使用方法看Readme.md的"启动参数"部分 +- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效 +- 新增自定义源功能,源编写规则可以去常见问题查看 + +### 优化 + +- 允许播放除了搜索列表以外的所有歌曲,即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注:该功能的原理是尝试自动切换到其他源播放,所以不一定会播放成功,特别是对于那些独家的资源 +- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示;现在在收藏单首歌曲时,若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说,在添加单首歌曲时若列表名是灰色,则证明当前歌曲已在那个列表中 +- 将歌词翻译放到原文的下方,同时新增当前播放翻译的高亮功能 + +### 移除 + +- 移除虾米源。注:虽然已移除该源,但仍可尝试去播放之前添加的歌曲,虽然不一定会成功 + +### 修复 + +- 修复音乐搜索列表的稍后播放功能无效的问题 +- 修复搜索列表双击不支持播放的源时会导致切歌的问题 +- 修复歌单列表加载失败时无法进入歌单打开界面的问题 +- 修复mg源歌单列表无法加载的问题 +- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题 +- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题 +- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题 + +### 变更 + +- 歌词翻译显示功能修改为默认关闭,注:此变更仅影响首次安装软件的用户 + +### 其他 + +- 更新electron到v9.4.4 + +## [1.7.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.0...v1.7.1) - 2021-01-30 + +### 修复 + +- 修复非透明模式下右侧滚动条无法拖动的问题 +- 修复MAC下xm音乐滑块验证问题 + +## [1.7.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.1...v1.7.0) - 2021-01-30 + +### 新增 + +- 搜索界面新增搜索状态的提示 +- 新增“稍后播放”功能,可在歌曲列表右键菜单使用 +- 新增“记住播放进度”功能的控制,该功能默认不再开启,可到播放设置-记住播放进度开启 + +### 优化 + +- 优化播放歌曲换源匹配 +- 优化设置界面设置项的展示 + +### 修复 + +- 修复快速切换歌曲时, 会出现播放的歌曲和界面展示的歌曲不一致的问题 +- 修复了一个由版本更新日志显示导致的潜在远程代码执行攻击漏洞,该漏洞影响v1.6.1及之前的所有版本,请务必更新到最新版本 +- 修复xm搜索源验证问题 + +### 其他 + +- 更新electron到9.4.2 + +## [1.6.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.0...v1.6.1) - 2021-01-13 + +### 优化 + +- 改进自动换源时的歌曲匹配 + +### 修复 + +- 修复某些情况下自动换源的时间过长时会终止换源自动切歌的问题 +- 修复自动换源导致的搜索列表每页变成10条数据的问题 +- 降级electron到9.3.3修复部分系统没有声音的问题 + +## [1.6.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.5.0...v1.6.0) - 2021-01-10 + +### 新增 + +- 我的列表右键菜单新增列表排序功能,可调整单曲、多选后的歌曲的顺序。注意:多选排序还将会按照选中歌曲时的顺序排序 +- 添加鼠标提示的自动关闭功能,鼠标长时间(目前是10秒)不动时鼠标提示将会自动关闭 +- 添加鼠标指向歌曲封面的提示(对于进度条左边的歌曲封面,你可能不知道的操作->右击在“我的列表”定位当前播放的歌曲) +- 隐藏播放详情页按钮添加快速隐藏详情页提示(你可能不知道的操作->在播放详情页内的任意非窗口可拖动区域右键双击可以快速隐藏详情页) +- 添加桌面歌词字体、透明度调整按钮微调提示(你可能不知道的操作->对于字体、透明度可右击微调) +- 我的列表右键菜单添加搜索当前歌曲功能 +- 新增`-dha`参数,添加此启动参数将禁用硬件加速启动(Disable Hardware Acceleration),窗口显示有问题时可以尝试添加此参数启动,Linux系统的界面显示有问题时可尝试添加此参数启动,若不行可尝试添加`-dt`参数启动 +- 新增播放自动换源功能~ + +### 变更 + +- `-nt`参数更名为`-dt`(Disable Transparent),目前原来的`-nt`参数仍然可用,但将在后续的版本中移除 + +### 修复 + +- 修复恢复上次播放的歌曲时在随机播放模式下不把恢复播放的歌曲放入已播放队列的问题(该问题会导致随机模式下会导致未播放完整个列表前就会再次随机到该歌曲,以及无法通过上一曲切回该歌曲) +- 修复音乐嵌入的封面在 Mac 系统无法显示的问题 +- 修复`-dt`(原来的`-nt`)启动参数不真正生效的问题 + ## [1.5.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.4.1...v1.5.0) - 2020-12-13 ### 新增 diff --git a/FAQ.md b/FAQ.md index a4cf5271..bbe04a79 100644 --- a/FAQ.md +++ b/FAQ.md @@ -13,7 +13,7 @@ 1. 尝试更新到最新版本 2. 尝试切换其他歌曲(或直接搜索该歌曲),若全部歌曲都无法试听与下载则进行下一步 3. 尝试到 设置-音乐来源 切换到其他接口 -4. 尝试切换网络,比如用手机开热点(目前存在某些网络无法访问接口服务器的情况) +4. 尝试切换网络,比如用手机开热点(所有歌曲都提示请求异常时可通过此方法解决,或等一两天后再试) 5. 若还不行请到这个链接查看详情: 6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明,则应该是你网络无法访问接口服务器的问题,如果接口有问题我会在那里说明。 @@ -62,6 +62,8 @@ 根据Electron里issue的[解决方案](https://github.com/electron/electron/issues/2170#issuecomment-736223269),
若你遇到透明问题可尝试添加启动参数 `-dha` 来禁用硬件加速,例如:`.\lx-music-desktop.exe -dha`。 +注:v1.6.0及之后的版本才支持`-dha`参数 + ## 软件启动后,界面无法显示 对于软件启动后,可以在任务栏看到软件,但软件界面在桌面上无任何显示,或者整个界面偶尔闪烁的情况。
@@ -109,18 +111,24 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的 `v1.2.1`以前的版本在 Ubuntu 18.10 下第一次开启桌面歌词时歌词窗口会变白,需要关闭后再开启, `v1.2.1`及之后的版本已修复该问题。 -其他 Linux 系统未测试,如有异常也是意料之中,目前不打算去处理 Linux 平台的桌面歌词问题。 +其他 Linux 系统未测试,如有异常也是意料之中,目前不打算去处理 Linux 平台的桌面歌词问题,但你可以尝试按照`Linux 下界面异常`的解决方案去解决。 ## 歌曲下载失败 ### 提示 `ENOENT: no such file or directory, mkdir` -更换下载歌曲目录即可解决(一般是设置的歌曲下载目录没有读写入权限导致的)。 +更换下载歌曲目录即可解决(一般是设置的歌曲下载目录没有读写权限导致的)。 ### 提示 `请求异常` 或 `Fail` 尝试更换网络,如切换到移动网络。 +## 使用软件时导致耳机意外关机 + +据反馈,漫步者部分型号的耳机与本软件一起使用时将会导致耳机意外关机, +详情看:, +若出现该问题可尝试添加`-dhmkh`启动参数解决,启动参数添加方法请自行百度“windows给应用程序加启动参数的方法”。 + ### 其他错误 按照前面的 "歌曲无法试听与下载" 方案解决。 @@ -162,10 +170,172 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的 ## 杀毒软件提示有病毒或恶意行为 -本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为,并且软件代码已开源,请自行查阅,软件安装包也是由CI拉取源代码构建,构建日志:[windows包](https://ci.appveyor.com/project/lyswhut/lx-music-desktop)、[Mac/Linux包](https://travis-ci.com/github/lyswhut/lx-music-desktop)
+本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为,并且软件代码已开源,请自行查阅,软件安装包也是由CI拉取源代码构建,构建日志:[GitHub Actions](https://github.com/lyswhut/lx-music-desktop/actions)
尽管如此,但这不意味着软件是100%安全的,由于软件使用了第三方依赖,当这些依赖存在恶意行为时([供应链攻击](https://docs.microsoft.com/zh-cn/windows/security/threat-protection/intelligence/supply-chain-malware)),软件也将会受到牵连,所以我只能尽量选择使用较多人用、信任度较高的依赖。
当然,以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的,或者有相关能力者还可以下载源代码自己构建安装包。 从`v0.17.0`起,由于加入了音频输出设备切换功能,该功能调用了 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/enumerateDevices),可能导致安全软件提示洛雪要访问摄像头(目前发现卡巴斯基会提示),但实际上没有用到摄像头,并且摄像头的提示灯也不会亮,你可以选择阻止访问。 最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件! + +## 自定义源脚本编写说明 + +文件请使用UTF-8编码格式编写,脚本所用编程语言为JavaScript,可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子: + +```js +/** + * @name 测试音乐源 + * @description 我只是一个测试音乐源哦 + * @version 1.0.0 + * @author xxx + * @homepage http://xxx + */ + + +const { EVENT_NAMES, request, on, send } = window.lx + +const qualitys = { + kw: { + '128k': '128', + '320k': '320', + flac: 'flac', + }, +} +const httpRequest = (url, options) => new Promise((resolve, reject) => { + request(url, options, (err, resp) => { + if (err) return reject(err) + resolve(resp.body) + }) +}) + +const apis = { + kw: { + musicUrl({ songmid }, quality) { + return httpRequest('http://xxx').then(data => { + return data.url + }) + }, + }, +} + +// 注册应用API请求事件 +// source 音乐源,可能的值取决于初始化时传入的sources对象的源key值 +// info 请求附加信息,内容根据action变化 +// action 请求操作类型,目前只有musicUrl,即获取音乐URL链接, +// 当action为musicUrl时info的结构:{type, musicInfo}, +// info.type:音乐质量,可能的值有128k / 320k / flac(取决于初始化时对应源传入的qualitys值中的一个), +// info.musicInfo:音乐信息对象,里面有音乐ID、名字等信息 +on(EVENT_NAMES.request, ({ source, action, info }) => { + // 回调必须返回 Promise 对象 + switch (action) { + // action 为 musicUrl 时需要在 Promise 返回歌曲 url + case 'musicUrl': + return apis[source].musicUrl(info.musicInfo, qualitys[source][info.type]).catch(err => { + console.log(err) + return Promise.reject(err) + }) + } +}) + +// 脚本初始化完成后需要发送inited事件告知应用 +send(EVENT_NAMES.inited, { + status: true, // 初始化成功 or 失败 + openDevTools: false, // 是否打开开发者工具,方便用于调试脚本 + sources: { // 当前脚本支持的源 + kw: { // 支持的源对象,可用key值:kw/kg/tx/wy/mg + name: '酷我音乐', + type: 'music', // 目前固定为 music + actions: ['musicUrl'], // 目前固定为 ['musicUrl'] + qualitys: ['128k', '320k', 'flac'], // 当前脚本的该源所支持获取的Url音质,有效的值有:['128k', '320k', 'flac'] + }, + }, +}) + +``` + +### 自定义源信息 + +文件的开头必须包含以下注释: + +```js +/** + * @name 测试脚本 + * @description 我只是一个测试脚本 + * @version 1.0.0 + * @author xxx + * @homepage http://xxx + */ + +``` + +- `@name `:源的名字,建议不要过长,10个字符以内 +- `@description `:源的描述,建议不要过长,20个字符以内,可不填,不填时必须保留 @description +- `@version`:源的版本号,可不填,不填时可以删除 @version +- `@author `:脚本作者名字,可不填,不填时可以删除 @author +- `@homepage `:脚本主页,可不填,不填时可以删除 @homepage + +### `window.lx` + +应用为脚本暴露的API对象。 + +#### `window.lx.EVENT_NAMES` + +常量事件名称对象,发送、注册事件时传入事件名时使用,可用值: + +| 事件名 | 描述 +| --- | --- +| `inited` | 脚本初始化完成后发送给应用的事件名,发送该事件时需要传入以下信息:`{status, sources, openDevTools}`
`status`:初始化结果(`true`成功,`false`失败)
`openDevTools`:是否打开DevTools,此选项可用于开发脚本时的调试
`sources`:支持的源信息对象,
`sources[kw/kg/tx/wy/mg].name`:源的名字(目前非必须)
`sources[kw/kg/tx/wy/mg].type`:源类型,目前固定值需为`music`
`sources[kw/kg/tx/wy/mg].actions`:支持的actions,由于目前只支持`musicUrl`,所以固定传`['musicUrl']`即可
`sources[kw/kg/tx/wy/mg].qualitys`:该源支持的音质列表,有效的值为`['128k', '320k', 'flac']`,该字段用于控制应用可用的音质类型 +| `request` | 应用API请求事件名,回调入参:`handler({ source, action, info})`,回调必须返回`Promise`对象
`source`:音乐源,可能的值取决于初始化时传入的`sources`对象的源key值
`info`:请求附加信息,内容根据`action`变化
`action`:请求操作类型,目前只有`musicUrl`,即获取音乐URL链接,需要在 Promise 返回歌曲 url,`info`的结构:`{type, musicInfo}`,`info.type`:音乐质量,可能的值有`128k` / `320k` / `flac`(取决于初始化时对应源传入的`qualitys`值中的一个),`info.musicInfo`:音乐信息对象,里面有音乐ID、名字等信息 + + +#### `window.lx.on` + +事件注册方法,应用主动与脚本通信时使用: + +```js +/** + * @param event_name 事件名 + * @param handler 事件处理回调 -- 注意:注册的回调必须返回 Promise 对象 + */ +window.lx.on(event_name, handler) +``` + +**注意:** 注册的回调必须返回 `Promise` 对象。 + +#### `window.lx.send` + +事件发送方法,脚本主动与应用通信时使用: + +```js +/** + * @param event_name 事件名 + * @param datas 要传给应用的数据 + */ +window.lx.send(event_name, datas) +``` + +#### `window.lx.request` + +HTTP请求方法,用于发送HTTP请求,此HTTP请求方法不受跨域规则限制: + +```js +/** + * @param url 请求的URL + * @param options 请求选项,可用选项有 method / headers / body / form / formData / timeout + * @param callback 请求结果的回调 入参:err, resp, body + * @return 返回一个方法,调用此方法可以终止HTTP请求 + */ +const cancelHttp = window.lx.request(url, options, callback) +``` + +#### `window.lx.utils` + +应用提供给脚本的工具方法: + +- `window.lx.utils.buffer.from`:对应Node.js的 `Buffer.from` +- `window.lx.utils.crypto.aesEncrypt`:AES加密 `aesEncrypt(buffer, mode, key, iv)` +- `window.lx.utils.crypto.md5`:MD5加密 `md5(str)` +- `window.lx.utils.crypto.randomBytes`:生成随机字符串 `randomBytes(size)` +- `window.lx.utils.crypto.rsaEncrypt`:RSA加密 `rsaEncrypt(buffer, key)` + +目前仅提供以上工具方法,如果需要其他方法可以开issue讨论。 diff --git a/README.md b/README.md index 305491e3..f3ec7795 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Release version - Build status - Build status + Build status + Build status Electron version Dev branch version @@ -47,7 +47,7 @@ 软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)
-或者到网盘下载(网盘内有MAC、windows版):`https://www.lanzoux.com/b0bf2cfa/` 密码:`glqw`
+或者到网盘下载(网盘内有MAC、windows版):`https://www.lanzous.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)
使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md) ### 源码使用方法 @@ -62,7 +62,7 @@ npm run dev npm run pack:dir # 构建安装包(Windows版) -npm run pack +npm run pack:win # 构建安装包(Mac版) npm run pack:mac @@ -81,8 +81,16 @@ npm run pack:linux 目前软件已支持的启动参数如下: - `-search` 启动软件时自动在搜索框搜索指定的内容,例如:`-search="突然的自我 - 伍佰"` -- `-dha` 禁用硬件加速启动(Disable Hardware Acceleration),窗口显示有问题时可以尝试添加此参数启动 -- `-dt` 以非透明模式启动(Disable Transparent),对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示,原来的`-nt`参数已重命名为`-dt` +- `-dha` 禁用硬件加速启动(Disable Hardware Acceleration),窗口显示有问题时可以尝试添加此参数启动(v1.6.0起新增) +- `-dt` 以非透明模式启动(Disable Transparent),对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示(注:该参数对桌面歌词无效),原来的`-nt`参数已重命名为`-dt`(v1.6.0起重命名) +- `-dhmkh` 禁用硬件媒体密钥处理(Disable Hardware Media Key Handling),此选项将禁用Chromium的Hardware Media Key Handling特性(v1.8.1起新增) +- `-play` 启动时播放指定列表的音乐,参数说明: + - `type`:播放类型,目前固定为`songList` + - `source`:播放源,可用值为`kw/kg/tx/wy/mg/myList`,其中`kw/kg/tx/wy/mg`对应各源的在线列表,`myList`为本地列表 + - `link`:要播放的在线列表歌单链接、或ID,source为`kw/kg/tx/wy/mg`之一(在线列表)时必传,举例:`./lx-music-desktop -play="type=songList&source=kw&link=歌单URL or ID"`,注意:如果传入URL时必须对URL进行编码后再传入 + - `name`:要播放的本地列表歌单名字,source为`myList`时必传,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表"` + - `index`:从列表的哪个位置开始播放,选传,若不传默认播放第一首歌曲,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表&index=2"` + ### 常见问题 diff --git a/build-config/main/webpack.config.dev.js b/build-config/main/webpack.config.dev.js index e867f7f5..f0624f43 100644 --- a/build-config/main/webpack.config.dev.js +++ b/build-config/main/webpack.config.dev.js @@ -17,8 +17,8 @@ module.exports = merge(baseConfig, { NODE_ENV: '"development"', }, __static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`, + __userApi: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`, }), - new webpack.NoEmitOnErrorsPlugin(), new FriendlyErrorsPlugin({ onErrors(severity, errors) { // Silent warning from electron-debug if (severity != 'warning') return diff --git a/build-config/main/webpack.config.prod.js b/build-config/main/webpack.config.prod.js index db519a62..552fe054 100644 --- a/build-config/main/webpack.config.prod.js +++ b/build-config/main/webpack.config.prod.js @@ -1,6 +1,7 @@ const path = require('path') const { merge } = require('webpack-merge') const webpack = require('webpack') +const CopyWebpackPlugin = require('copy-webpack-plugin') const baseConfig = require('./webpack.config.base') @@ -20,6 +21,18 @@ module.exports = merge(baseConfig, { __filename: false, }, plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(__dirname, '../../src/main/modules/userApi/renderer'), + to: path.join(__dirname, '../../dist/electron/userApi/renderer'), + }, + { + from: path.join(__dirname, '../../src/main/modules/userApi/rendererEvent/name.js'), + to: path.join(__dirname, '../../dist/electron/userApi/rendererEvent/name.js'), + }, + ], + }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"', diff --git a/build-config/renderer-lyric/webpack.config.base.js b/build-config/renderer-lyric/webpack.config.base.js index d0e72ed2..3bcfc5b2 100644 --- a/build-config/renderer-lyric/webpack.config.base.js +++ b/build-config/renderer-lyric/webpack.config.base.js @@ -37,6 +37,7 @@ module.exports = { loader: 'eslint-loader', options: { formatter: require('eslint-formatter-friendly'), + emitWarning: isDev, }, }, exclude: /node_modules/, diff --git a/build-config/renderer-lyric/webpack.config.dev.js b/build-config/renderer-lyric/webpack.config.dev.js index 871afd3e..60398d95 100644 --- a/build-config/renderer-lyric/webpack.config.dev.js +++ b/build-config/renderer-lyric/webpack.config.dev.js @@ -11,7 +11,6 @@ module.exports = merge(baseConfig, { devtool: 'eval-source-map', plugins: [ new webpack.HotModuleReplacementPlugin(), - new webpack.NoEmitOnErrorsPlugin(), new FriendlyErrorsPlugin(), new webpack.DefinePlugin({ 'process.env': { diff --git a/build-config/renderer-lyric/webpack.config.prod.js b/build-config/renderer-lyric/webpack.config.prod.js index 8cc3d8dd..e9e9f7be 100644 --- a/build-config/renderer-lyric/webpack.config.prod.js +++ b/build-config/renderer-lyric/webpack.config.prod.js @@ -34,7 +34,6 @@ module.exports = merge(baseConfig, { }), ], optimization: { - chunkIds: 'named', minimizer: [ new TerserPlugin(), new OptimizeCSSAssetsPlugin({}), diff --git a/build-config/renderer/webpack.config.base.js b/build-config/renderer/webpack.config.base.js index 6721ac9a..050e768e 100644 --- a/build-config/renderer/webpack.config.base.js +++ b/build-config/renderer/webpack.config.base.js @@ -37,6 +37,7 @@ module.exports = { loader: 'eslint-loader', options: { formatter: require('eslint-formatter-friendly'), + emitWarning: isDev, }, }, exclude: /node_modules/, diff --git a/build-config/renderer/webpack.config.dev.js b/build-config/renderer/webpack.config.dev.js index 871afd3e..60398d95 100644 --- a/build-config/renderer/webpack.config.dev.js +++ b/build-config/renderer/webpack.config.dev.js @@ -11,7 +11,6 @@ module.exports = merge(baseConfig, { devtool: 'eval-source-map', plugins: [ new webpack.HotModuleReplacementPlugin(), - new webpack.NoEmitOnErrorsPlugin(), new FriendlyErrorsPlugin(), new webpack.DefinePlugin({ 'process.env': { diff --git a/build-config/renderer/webpack.config.prod.js b/build-config/renderer/webpack.config.prod.js index 8cc3d8dd..e9e9f7be 100644 --- a/build-config/renderer/webpack.config.prod.js +++ b/build-config/renderer/webpack.config.prod.js @@ -34,7 +34,6 @@ module.exports = merge(baseConfig, { }), ], optimization: { - chunkIds: 'named', minimizer: [ new TerserPlugin(), new OptimizeCSSAssetsPlugin({}), diff --git a/build-config/runner-dev.js b/build-config/runner-dev.js index 19235c14..6ec092db 100644 --- a/build-config/runner-dev.js +++ b/build-config/runner-dev.js @@ -179,6 +179,9 @@ function startElectron() { function electronLog(data, color) { let log = data.toString() if (/[0-9A-z]+/.test(log)) { + // 抑制 user api 窗口使用 data url 加载页面时 vue扩展 的报错日志刷屏的问题 + if (color == 'red' && typeof log === 'string' && log.includes('"Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled/extensions/extensions.js')) return + console.log(chalk[color](log)) } } diff --git a/build-config/utils.js b/build-config/utils.js index e3a9b5e0..136b2e50 100644 --- a/build-config/utils.js +++ b/build-config/utils.js @@ -2,8 +2,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const cssLoaderConfig = require('./css-loader.config') const chalk = require('chalk') -const isDev = process.env.NODE_ENV === 'development' - // merge css-loader exports.mergeCSSLoader = beforeLoader => { const loader = [ @@ -11,12 +9,7 @@ exports.mergeCSSLoader = beforeLoader => { { resourceQuery: /module/, use: [ - { - loader: MiniCssExtractPlugin.loader, - options: { - hmr: isDev, - }, - }, + MiniCssExtractPlugin.loader, { loader: 'css-loader', options: cssLoaderConfig, @@ -27,12 +20,7 @@ exports.mergeCSSLoader = beforeLoader => { // 这里匹配普通的 ` diff --git a/src/renderer/components/material/Btn.vue b/src/renderer/components/material/Btn.vue index fafc6017..b11b6eb0 100644 --- a/src/renderer/components/material/Btn.vue +++ b/src/renderer/components/material/Btn.vue @@ -1,5 +1,5 @@ @@ -9,6 +9,10 @@ export default { min: { type: Boolean, }, + outline: { + type: Boolean, + default: false, + }, disabled: { type: Boolean, default: false, @@ -36,6 +40,10 @@ export default { opacity: .4; } + &.outline { + background-color: transparent; + } + &:hover { background-color: @color-btn-hover; } @@ -54,6 +62,9 @@ each(@themes, { .btn { color: ~'@{color-@{value}-btn}'; background-color: ~'@{color-@{value}-btn-background}'; + &.outline { + background-color: transparent; + } &:hover { background-color: ~'@{color-@{value}-btn-hover}'; } diff --git a/src/renderer/components/material/ListAddModal.vue b/src/renderer/components/material/ListAddModal.vue index c42c8de3..dca5e789 100644 --- a/src/renderer/components/material/ListAddModal.vue +++ b/src/renderer/components/material/ListAddModal.vue @@ -6,7 +6,7 @@ material-modal(:show="show" :bg-close="bgClose" @close="handleClose") span(:class="$style.name") {{this.musicInfo && `${musicInfo.name}`}} |  {{$t('material.list_add_modal.title_last')}} div.scroll(:class="$style.btnContent") - material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}} + material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}} material-btn(:class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('view.list.lists_new_list_btn')") svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 42 42' space='preserve') use(xlink:href='#icon-addTo') @@ -57,11 +57,12 @@ export default { computed: { ...mapGetters('list', ['defaultList', 'loveList', 'userList']), lists() { + if (!this.musicInfo) return [] return [ this.defaultList, this.loveList, ...this.userList, - ].filter(l => l.id != this.excludeListId.includes(l.id)) + ].filter(l => !this.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: l.list.some(s => s.songmid == this.musicInfo.songmid) })) }, spaceNum() { return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1) diff --git a/src/renderer/components/material/ListAddMultipleModal.vue b/src/renderer/components/material/ListAddMultipleModal.vue index 40a9e734..cee9a6c0 100644 --- a/src/renderer/components/material/ListAddMultipleModal.vue +++ b/src/renderer/components/material/ListAddMultipleModal.vue @@ -62,7 +62,7 @@ export default { this.defaultList, this.loveList, ...this.userList, - ].filter(l => l.id != this.excludeListId.includes(l.id)) + ].filter(l => !this.excludeListId.includes(l.id)) }, spaceNum() { return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1) diff --git a/src/renderer/components/material/Menu.vue b/src/renderer/components/material/Menu.vue index a48afd6e..ad4df157 100644 --- a/src/renderer/components/material/Menu.vue +++ b/src/renderer/components/material/Menu.vue @@ -138,7 +138,7 @@ export default { // will-change: transform; li { cursor: pointer; - min-width: 90px; + min-width: 96px; line-height: 34px; // color: @color-btn; padding: 0 10px; diff --git a/src/renderer/components/material/Modal.vue b/src/renderer/components/material/Modal.vue index 83a4f4fa..d9981428 100644 --- a/src/renderer/components/material/Modal.vue +++ b/src/renderer/components/material/Modal.vue @@ -74,9 +74,9 @@ export default { 'slideOutLeft', 'slideOutRight', 'slideOutUp', - 'hinge', + // 'hinge', ], - inClass: 'animated flipInX', + inClass: 'animated jackInTheBox', outClass: 'animated flipOutX', unwatchFn: null, } diff --git a/src/renderer/components/material/MusicComment.vue b/src/renderer/components/material/MusicComment.vue index 7b5c198e..f13c74bd 100644 --- a/src/renderer/components/material/MusicComment.vue +++ b/src/renderer/components/material/MusicComment.vue @@ -50,7 +50,7 @@ export default { singer: '', }, page: 1, - total: 10, + total: 0, maxPage: 1, limit: 20, isHotLoading: true, diff --git a/src/renderer/components/material/PactModal.vue b/src/renderer/components/material/PactModal.vue index 9b42af54..ff6cee06 100644 --- a/src/renderer/components/material/PactModal.vue +++ b/src/renderer/components/material/PactModal.vue @@ -80,7 +80,7 @@ export default { watch: { 'setting.isAgreePact'(n) { if (n) return - this.time = 10 + this.time = 5 this.startTimeout() }, }, diff --git a/src/renderer/components/material/SongList.vue b/src/renderer/components/material/SongList.vue index 3b83508e..afb4b7fc 100644 --- a/src/renderer/components/material/SongList.vue +++ b/src/renderer/components/material/SongList.vue @@ -31,8 +31,6 @@ div(:class="$style.songList") td(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;") material-list-buttons(:index="index" :class="$style.btns" :remove-btn="false" @btn-click="handleListBtnClick" - :listAdd-btn="assertApiSupport(item.source)" - :play-btn="assertApiSupport(item.source)" :download-btn="assertApiSupport(item.source)") //- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)') 下载 //- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)') 试听 @@ -114,6 +112,11 @@ export default { action: 'download', disabled: !this.listMenu.itemMenuControl.download, }, + { + name: this.$t('material.song_list.list_play_later'), + action: 'playLater', + disabled: !this.listMenu.itemMenuControl.playLater, + }, { name: this.$t('material.song_list.list_search'), action: 'search', @@ -173,6 +176,7 @@ export default { itemMenuControl: { play: true, addTo: true, + playLater: true, download: true, search: true, sourceDetail: true, @@ -237,7 +241,7 @@ export default { this.clickIndex = index return } - this.emitEvent(this.assertApiSupport(this.source) ? 'testPlay' : 'search', index) + this.emitEvent('testPlay', index) this.clickTime = 0 this.clickIndex = -1 }, @@ -334,8 +338,9 @@ export default { }, handleListItemRigthClick(event, index) { this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl - this.listMenu.itemMenuControl.play = - this.listMenu.itemMenuControl.download = + // this.listMenu.itemMenuControl.play = + // this.listMenu.itemMenuControl.playLater = + this.listMenu.itemMenuControl.download = this.assertApiSupport(this.list[index].source) let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected') if (dom_selected) dom_selected.classList.remove('selected') diff --git a/src/renderer/components/material/UserApiModal.vue b/src/renderer/components/material/UserApiModal.vue new file mode 100644 index 00000000..afe94b68 --- /dev/null +++ b/src/renderer/components/material/UserApiModal.vue @@ -0,0 +1,240 @@ + + + + + + diff --git a/src/renderer/components/material/VersionModal.vue b/src/renderer/components/material/VersionModal.vue index 9439d57e..e3095e38 100644 --- a/src/renderer/components/material/VersionModal.vue +++ b/src/renderer/components/material/VersionModal.vue @@ -3,17 +3,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV main(:class="$style.main" v-if="version.isDownloaded") h2 🚀程序更新🚀 - div.scroll(:class="$style.info") + div.scroll.select(:class="$style.info") div(:class="$style.current") h3 最新版本:{{version.newVersion.version}} h3 当前版本:{{version.version}} h3 版本变化: - p(:class="$style.desc" v-html="version.newVersion.desc") + pre(:class="$style.desc" v-text="version.newVersion.desc") div(:class="[$style.history, $style.desc]" v-if="history.length") h3 历史版本: div(:class="$style.item" v-for="ver in history") h4 v{{ver.version}} - p(v-html="ver.desc") + pre(v-text="ver.desc") div(:class="$style.footer") div(:class="$style.desc") p 新版本已下载完毕, @@ -27,17 +27,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV main(:class="$style.main" v-else-if="version.isError && !version.isUnknow && version.newVersion.version != version.version") h2 ❌ 版本更新出错 ❌ - div.scroll(:class="$style.info") + div.scroll.select(:class="$style.info") div(:class="$style.current") h3 最新版本:{{version.newVersion.version}} h3 当前版本:{{version.version}} h3 版本变化: - p(:class="$style.desc" v-html="version.newVersion.desc") + pre(:class="$style.desc" v-text="version.newVersion.desc") div(:class="[$style.history, $style.desc]" v-if="history.length") h3 历史版本: div(:class="$style.item" v-for="ver in history") h4 v{{ver.version}} - p(v-html="ver.desc") + pre(v-text="ver.desc") div(:class="$style.footer") div(:class="$style.desc") @@ -46,7 +46,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV | 你可以去  strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页 |  或  - strong.hover.underline(@click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')" tips="点击打开") 网盘 + strong.hover.underline(@click="handleOpenUrl('https://www.lanzous.com/b0bf2cfa/')" tips="点击打开") 网盘 | (密码: strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw | ) 下载新版本, @@ -58,12 +58,12 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV main(:class="$style.main" v-else-if="version.isDownloading && version.isTimeOut && !version.isUnknow") h2 ❗️ 新版本下载超时 ❗️ div(:class="$style.desc") - p 你当前所在网络访问GitHub较慢,导致新版本下载超时(已经下了半个钟了😳),建议手动更新版本! + p 你当前所在网络访问GitHub较慢,导致新版本下载超时(已经下了半个钟了😳),你仍可选择继续等,但墙裂建议手动更新版本! p | 你可以去 material-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页 | 或 - material-btn(min @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')" tips="点击打开") 网盘 + material-btn(min @click="handleOpenUrl('https://www.lanzous.com/b0bf2cfa/')" tips="点击打开") 网盘 | (密码: strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw | )下载新版本, @@ -75,7 +75,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV main(:class="$style.main" v-else-if="version.isUnknow") h2 ❓ 获取最新版本信息失败 ❓ - div.scroll(:class="$style.info") + div.scroll.select(:class="$style.info") div(:class="$style.current") h3 当前版本:{{version.version}} div(:class="$style.desc") @@ -84,7 +84,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV | 检查方法:打开 material-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页 | 或 - material-btn(min @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')" tips="点击打开") 网盘 + material-btn(min @click="handleOpenUrl('https://www.lanzous.com/b0bf2cfa/')" tips="点击打开") 网盘 | (密码: strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw | )查看它们的 @@ -94,17 +94,17 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV main(:class="$style.main" v-else) h2 🌟发现新版本🌟 - div.scroll(:class="$style.info") + div.scroll.select(:class="$style.info") div(:class="$style.current") h3 最新版本:{{version.newVersion.version}} h3 当前版本:{{version.version}} h3 版本变化: - p(:class="$style.desc" v-html="version.newVersion.desc") + pre(:class="$style.desc" v-text="version.newVersion.desc") div(:class="[$style.history, $style.desc]" v-if="history.length") h3 历史版本: div(:class="$style.item" v-for="ver in history") h4 v{{ver.version}} - p(v-html="ver.desc") + pre(v-text="ver.desc") div(:class="$style.footer") div(:class="$style.desc") @@ -117,7 +117,7 @@ material-modal(:show="version.showModal" @close="handleClose" v-if="version.newV | 手动更新可以去  strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页 |  或  - strong.hover.underline(@click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')" tips="点击打开") 网盘 + strong.hover.underline(@click="handleOpenUrl('https://www.lanzous.com/b0bf2cfa/')" tips="点击打开") 网盘 | (密码: strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw | ) 下载, @@ -146,7 +146,7 @@ export default { progress() { return this.version.downloadProgress ? `${this.version.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.version.downloadProgress.transferred)}/${sizeFormate(this.version.downloadProgress.total)} - ${sizeFormate(this.version.downloadProgress.bytesPerSecond)}/s` - : '初始化中...' + : '处理更新中...' }, isIgnored() { return this.setting.ignoreVersion == this.version.newVersion.version @@ -207,6 +207,11 @@ export default { font-size: 14px; line-height: 1.3; } + pre { + white-space: pre-wrap; + text-align: justify; + margin-top: 10px; + } } .info { diff --git a/src/renderer/components/material/XmVerifyModal.vue b/src/renderer/components/material/XmVerifyModal.vue deleted file mode 100644 index 6a910c4c..00000000 --- a/src/renderer/components/material/XmVerifyModal.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - diff --git a/src/renderer/lang/en-us/material/song_list.json b/src/renderer/lang/en-us/material/song_list.json index 8782cd51..d0b3ba23 100644 --- a/src/renderer/lang/en-us/material/song_list.json +++ b/src/renderer/lang/en-us/material/song_list.json @@ -1,5 +1,6 @@ { "list_play": "Play", + "list_play_later": "Play later", "list_add_to": "Add to ...", "list_download": "Download", "list_search": "Search", diff --git a/src/renderer/lang/en-us/material/user_api_modal.json b/src/renderer/lang/en-us/material/user_api_modal.json new file mode 100644 index 00000000..79c983ec --- /dev/null +++ b/src/renderer/lang/en-us/material/user_api_modal.json @@ -0,0 +1,10 @@ +{ + "title": "Custom Source Management", + "readme": "Source writing instructions: ", + "note": "Tip: Although we have isolated the script's running environment as much as possible, importing scripts containing malicious behaviors may still affect your system. Please import them carefully.", + "btn_remove": "Remove", + "btn_import": "Import", + "btn_export": "Export", + "import_file": "Select music API script file", + "noitem": "There is nothing here...😲" +} diff --git a/src/renderer/lang/en-us/view/download.json b/src/renderer/lang/en-us/view/download.json index 57f45632..1fd22e42 100644 --- a/src/renderer/lang/en-us/view/download.json +++ b/src/renderer/lang/en-us/view/download.json @@ -1,5 +1,6 @@ { "menu_play": "Play", + "menu_play_later": "Play later", "menu_start": "Start task", "menu_pause": "Pause Task", "menu_file": "Locate File", diff --git a/src/renderer/lang/en-us/view/list.json b/src/renderer/lang/en-us/view/list.json index 0bb795de..ba66d08e 100644 --- a/src/renderer/lang/en-us/view/list.json +++ b/src/renderer/lang/en-us/view/list.json @@ -7,6 +7,7 @@ "lists_sync": "Sync", "lists_remove": "Remove", "list_play": "Play", + "list_play_later": "Play later", "list_copy_name": "Copy name", "list_add_to": "Add to ...", "list_move_to": "Move to ...", diff --git a/src/renderer/lang/en-us/view/search.json b/src/renderer/lang/en-us/view/search.json index a265f2cf..655d1d4c 100644 --- a/src/renderer/lang/en-us/view/search.json +++ b/src/renderer/lang/en-us/view/search.json @@ -1,5 +1,6 @@ { "list_play": "Play", + "list_play_later": "Play later", "list_add_to": "Add to ...", "list_download": "Download", "list_source_detail": "Song Page", @@ -10,6 +11,7 @@ "time": "Length", "lossless": "SQ", "high_quality": "HQ", + "loding_list": "Loading...", "no_item": "Search what I want to 😉", "hot_search": "Top Searches", "history_search": "History Searches", diff --git a/src/renderer/lang/en-us/view/setting.json b/src/renderer/lang/en-us/view/setting.json index 8c94a881..66cfb238 100644 --- a/src/renderer/lang/en-us/view/setting.json +++ b/src/renderer/lang/en-us/view/setting.json @@ -2,7 +2,6 @@ "basic": "General", "basic_theme": "Theme", "basic_show_animation": "Show switching animation", - "basic_animation_title": "Animation effect of the pop-up layer", "basic_animation": "Random pop-up animation", "basic_source_title": "Choose a music source", "basic_source_test": "Test API (Available for most software features)", @@ -12,6 +11,10 @@ "basic_sourcename_real": "Original", "basic_sourcename_alias": "Aliases", "basic_sourcename": "Source name", + "basic_source_status_success": "Initialization successful", + "basic_source_status_initing": "Initializing", + "basic_source_status_failed": "Initialization failed", + "basic_source_user_api_btn": "Custom Source Management", "basic_window_size_title": "Set the window size", "basic_window_size": "Window size", "basic_window_size_smaller": "Smaller", @@ -21,8 +24,7 @@ "basic_window_size_larger": "Larger", "basic_window_size_oversized": "Oversized", "basic_window_size_huge": "Huge", - "basic_to_tray_title": "Minimize it to the system tray without closing the software when closing", - "basic_to_tray": "Minimize to system tray when closing", + "basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray", "basic_lang_title": "The language displayed in the software", "basic_lang": "Language", "basic_control_btn_position": "Control Button Position", @@ -30,21 +32,14 @@ "basic_control_btn_position_right": "Right", "play": "Play", - "play_toggle_title": "If none selected, it stopped when the music playing is done.", - "play_toggle": "Playback mode", - "play_toggle_list_loop": "Playlist repeat", - "play_toggle_random": "Playlist shuffle", - "play_toggle_list": "Play in order", - "play_toggle_single_loop": "Single repeat", + "play_save_play_time": "Remember playback progress", "play_lyric_transition": "Show lyrics translation", - "play_quality_title": "The 320k quality is preferred for playing", - "play_quality": "Prefer High Quality 320k", - "play_task_bar_title": "Show playing progress on the taskbar", - "play_task_bar": "Taskbar play progress bar", + "play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)", + "play_quality": "Play 320K quality songs first (if supported)", + "play_task_bar": "Show playing progress on the taskbar", "play_mediaDevice_title": "Select a media device for audio output", "play_mediaDevice": "Audio output", - "play_mediaDevice_remove_stop_play": "Whether to pause playback when the audio output device is changed", - "play_mediaDevice_remove_stop_play_title": "Whether to pause the song when the current sound output device is changed", + "play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed", "desktop_lyric": "Desktop Lyric Settings", "desktop_lyric_enable": "Display lyrics", @@ -53,18 +48,13 @@ "desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen", "search": "Search", - "search_hot_title": "Select whether to show popular searches", "search_hot": "Top Searches", - "search_history_title": "Select whether to show search history", "search_history": "Search history", - "search_focus_search_box_title": "Whether the search box is automatically focused on startup", - "search_focus_search_box": "Whether the search box is focused on startup", + "search_focus_search_box": "Automatically focus the search box on startup", "list": "List", - "list_source_title": "Select whether to show music source", - "list_source": "Select whether to show music source (for Your Library only)", - "list_scroll_title": "Select whether to remember the playlist scrollbar position", - "list_scroll": "Remember playlist scrolling position (for Your library only)", + "list_source": "Show song source (only valid for my music category)", + "list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)", "download": "Download", "download_enable": "Whether to enable download function", @@ -136,10 +126,11 @@ "other_tray_theme": "Tray Icon Style", "other_tray_theme_native": "Solid Color", "other_tray_theme_origin": "Primary Color", - "other_cache": "Cache size (Not recommended since resources such as pictures after the cache is cleaned need re-downloading. The software will dynamically manage the cache size based on disk space)", - "other_cache_label": "Cache size used: ", - "other_cache_label_title": "Currently used cache size", - "other_cache_clear_btn": "Clear cache", + "other_resource_cache": "Resource cache management (pictures, audios and other caches, pictures and other resources will need to be downloaded again after cleaning up, it is not recommended to clean up, the software will dynamically manage the cache size according to the disk space)", + "other_resource_cache_label": "The software has used cache size: ", + "other_resource_cache_clear_btn": "Clear resource cache", + "other_play_list_cache": "List cache management (links to songs that have been cached in my list, alternative sources for playback, after cleaning up, you need to re-acquire them when you play and download songs, and do not clean up if there are no issues related to song playback)", + "other_play_list_cache_clear_btn": "Clear list cache information", "update": "Update", "update_latest_label": "Latest version: ", @@ -150,7 +141,7 @@ "update_latest": "The software is up-to-date, enjoy yourself!🥂", "update_open_version_modal_btn": "Open the update window🚀", "update_checking": "Checking for updates...", - "update_init": "Initializing update...", + "update_init": "Processing update...", "about": "About lx-music-desktop", diff --git a/src/renderer/lang/en-us/view/song_list.json b/src/renderer/lang/en-us/view/song_list.json index 8c600d5b..55512767 100644 --- a/src/renderer/lang/en-us/view/song_list.json +++ b/src/renderer/lang/en-us/view/song_list.json @@ -8,5 +8,6 @@ "tip_2": "If you encounter a link to a playlist that cannot be opened, welcome feedback", "tip_3": "Kugou source does not support opening with playlist ID, but supports Kugou code opening", "play_all": "Play", + "play_later": "Play later", "add_all": "Collect" } diff --git a/src/renderer/lang/zh-cn/material/song_list.json b/src/renderer/lang/zh-cn/material/song_list.json index a0eb76f1..8735c6d9 100644 --- a/src/renderer/lang/zh-cn/material/song_list.json +++ b/src/renderer/lang/zh-cn/material/song_list.json @@ -1,5 +1,6 @@ { "list_play": "播放", + "list_play_later": "稍后播放", "list_add_to": "添加到...", "list_download": "下载", "list_source_detail": "歌曲详情页", diff --git a/src/renderer/lang/zh-cn/material/user_api_modal.json b/src/renderer/lang/zh-cn/material/user_api_modal.json new file mode 100644 index 00000000..894fb5b6 --- /dev/null +++ b/src/renderer/lang/zh-cn/material/user_api_modal.json @@ -0,0 +1,10 @@ +{ + "title": "自定义源管理", + "readme": "源编写说明:", + "note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。", + "btn_remove": "移除", + "btn_import": "导入", + "btn_export": "导出", + "import_file": "选择音乐API脚本文件", + "noitem": "这里竟然是空的 😲" +} diff --git a/src/renderer/lang/zh-cn/store/state.json b/src/renderer/lang/zh-cn/store/state.json index 3bba8d17..c57527e4 100644 --- a/src/renderer/lang/zh-cn/store/state.json +++ b/src/renderer/lang/zh-cn/store/state.json @@ -33,6 +33,6 @@ "source_alias_all": "聚合大会", - "load_list_file_error_title": "播放列表数据加载错误", + "load_list_file_error_title": "播放列表数据加载错误(建议到GitHub或加群反馈)", "load_list_file_error_detail": "我们已经帮你把旧的列表文件备份到{path}\n它以 JSON 格式存储,你可以尝试手动修复并恢复它\n\n错误详情:{detail}" } diff --git a/src/renderer/lang/zh-cn/view/download.json b/src/renderer/lang/zh-cn/view/download.json index 89af68b3..846f3de1 100644 --- a/src/renderer/lang/zh-cn/view/download.json +++ b/src/renderer/lang/zh-cn/view/download.json @@ -1,5 +1,6 @@ { "menu_play": "播放", + "menu_play_later": "稍后播放", "menu_start": "开始任务", "menu_pause": "暂停任务", "menu_file": "定位文件", diff --git a/src/renderer/lang/zh-cn/view/list.json b/src/renderer/lang/zh-cn/view/list.json index ae8507eb..bd619951 100644 --- a/src/renderer/lang/zh-cn/view/list.json +++ b/src/renderer/lang/zh-cn/view/list.json @@ -7,6 +7,7 @@ "lists_sync": "同步", "lists_remove": "删除", "list_play": "播放", + "list_play_later": "稍后播放", "list_copy_name": "复制歌曲名", "list_source_detail": "歌曲详情页", "list_add_to": "添加到...", diff --git a/src/renderer/lang/zh-cn/view/search.json b/src/renderer/lang/zh-cn/view/search.json index b981c68f..bc418cb6 100644 --- a/src/renderer/lang/zh-cn/view/search.json +++ b/src/renderer/lang/zh-cn/view/search.json @@ -1,5 +1,6 @@ { "list_play": "播放", + "list_play_later": "稍后播放", "list_add_to": "添加到...", "list_download": "下载", "list_source_detail": "歌曲详情页", @@ -10,6 +11,7 @@ "time": "时长", "lossless": "无损", "high_quality": "高品质", + "loding_list": "加载中...", "no_item": "搜我所想~~😉", "hot_search": "热门搜索", "history_search": "历史搜索", diff --git a/src/renderer/lang/zh-cn/view/setting.json b/src/renderer/lang/zh-cn/view/setting.json index 86472ccd..9b33455c 100644 --- a/src/renderer/lang/zh-cn/view/setting.json +++ b/src/renderer/lang/zh-cn/view/setting.json @@ -1,7 +1,6 @@ { "basic": "基本设置", "basic_theme": "主题颜色", - "basic_animation_title": "弹出层的动画效果", "basic_animation": "弹出层随机动画", "basic_show_animation": "显示切换动画", "basic_source_title": "选择音乐来源", @@ -12,6 +11,10 @@ "basic_sourcename_real": "原名", "basic_sourcename_alias": "别名", "basic_sourcename": "音源名字", + "basic_source_status_success": "初始化成功", + "basic_source_status_initing": "初始化中", + "basic_source_status_failed": "初始化失败", + "basic_source_user_api_btn": "自定义源管理", "basic_window_size_title": "设置软件窗口尺寸", "basic_window_size": "窗口尺寸", "basic_window_size_smaller": "较小", @@ -21,8 +24,7 @@ "basic_window_size_larger": "较大", "basic_window_size_oversized": "超大", "basic_window_size_huge": "巨大", - "basic_to_tray_title": "关闭时不退出软件将其最小化到系统托盘", - "basic_to_tray": "关闭时最小化到系统托盘", + "basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘", "basic_lang_title": "软件显示的语言", "basic_lang": "语言", "basic_control_btn_position": "控制按钮位置", @@ -30,21 +32,14 @@ "basic_control_btn_position_right": "右边", "play": "播放设置", - "play_toggle_title": "都不选时播放完当前歌曲就停止播放", - "play_toggle": "歌曲切换方式", - "play_toggle_list_loop": "列表循环", - "play_toggle_random": "列表随机", - "play_toggle_list": "顺序播放", - "play_toggle_single_loop": "单曲循环", + "play_save_play_time": "记住播放进度", "play_lyric_transition": "显示歌词翻译", - "play_quality_title": "启用时将优先播放320K品质的歌曲", - "play_quality": "优先播放高品质音乐", - "play_task_bar_title": "在任务栏上显示当前歌曲播放进度", - "play_task_bar": "任务栏播放进度条", + "play_lyric_lxlrc": "使用卡拉OK式歌词播放(如果支持)", + "play_quality": "优先播放320K品质的歌曲(如果支持)", + "play_task_bar": "在任务栏上显示当前歌曲播放进度", "play_mediaDevice_title": "选择声音输出的媒体设备", "play_mediaDevice": "音频输出", - "play_mediaDevice_remove_stop_play": "音频输出设备被改变时是否暂停播放", - "play_mediaDevice_remove_stop_play_title": "当前的声音输出设备被改变时是否暂停播放歌曲", + "play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲", "desktop_lyric": "桌面歌词设置", "desktop_lyric_enable": "显示歌词", @@ -53,18 +48,13 @@ "desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外", "search": "搜索设置", - "search_hot_title": "是否显示热门搜索", - "search_hot": "热门搜索", - "search_history_title": "是否显示历史搜索记录", - "search_history": "搜索历史", - "search_focus_search_box_title": "启动时是否自动聚焦搜索框", - "search_focus_search_box": "启动时是否聚焦搜索框", + "search_hot": "显示热门搜索", + "search_history": "显示历史搜索记录", + "search_focus_search_box": "启动时自动聚焦搜索框", "list": "列表设置", - "list_source_title": "是否显示歌曲源", - "list_source": "是否显示歌曲源(仅对我的音乐分类有效)", - "list_scroll_title": "是否记住播放列表滚动条位置", - "list_scroll": "记住列表滚动位置(仅对我的音乐分类有效)", + "list_source": "显示歌曲源(仅对我的音乐分类有效)", + "list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)", "download": "下载设置", "download_enable": "是否启用下载功能", @@ -136,10 +126,11 @@ "other_tray_theme": "托盘图标样式", "other_tray_theme_native": "纯色", "other_tray_theme_origin": "原色", - "other_cache": "缓存大小(清理缓存后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)", - "other_cache_label": "软件已使用缓存大小:", - "other_cache_label_title": "当前已用缓存", - "other_cache_clear_btn": "清理缓存", + "other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)", + "other_resource_cache_label": "软件已使用缓存大小:", + "other_resource_cache_clear_btn": "清理资源缓存", + "other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)", + "other_play_list_cache_clear_btn": "清理列表缓存信息", "update": "软件更新", "update_latest_label": "最新版本:", @@ -150,7 +141,7 @@ "update_latest": "软件已是最新,尽情地体验吧~🥂", "update_open_version_modal_btn": "打开更新窗口 🚀", "update_checking": "检查更新中...", - "update_init": "更新初始化中...", + "update_init": "处理更新中...", "about": "关于洛雪音乐", diff --git a/src/renderer/lang/zh-cn/view/song_list.json b/src/renderer/lang/zh-cn/view/song_list.json index bbbadf6b..f0b6f1fb 100644 --- a/src/renderer/lang/zh-cn/view/song_list.json +++ b/src/renderer/lang/zh-cn/view/song_list.json @@ -8,5 +8,6 @@ "tip_2": "若遇到无法打开的歌单链接,欢迎反馈", "tip_3": "酷狗源不支持用歌单ID打开,但支持酷狗码打开", "play_all": "播放", + "play_later": "稍后播放", "add_all": "收藏" } diff --git a/src/renderer/lang/zh-tw/material/song_list.json b/src/renderer/lang/zh-tw/material/song_list.json index ed5a4460..c91c8ca4 100644 --- a/src/renderer/lang/zh-tw/material/song_list.json +++ b/src/renderer/lang/zh-tw/material/song_list.json @@ -1,5 +1,6 @@ { "list_play": "播放", + "list_play_later": "稍後播放", "list_add_to": "添加到...", "list_download": "下載", "list_search": "搜索", diff --git a/src/renderer/lang/zh-tw/material/user_api_modal.json b/src/renderer/lang/zh-tw/material/user_api_modal.json new file mode 100644 index 00000000..0d1948ee --- /dev/null +++ b/src/renderer/lang/zh-tw/material/user_api_modal.json @@ -0,0 +1,10 @@ +{ + "title": "自定義源管理", + "readme": "源編寫說明:", + "note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。", + "btn_remove": "移除", + "btn_import": "導入", + "btn_export": "導出", + "import_file": "選擇音樂API腳本文件", + "noitem": "這裡竟然是空的 😲" +} diff --git a/src/renderer/lang/zh-tw/view/download.json b/src/renderer/lang/zh-tw/view/download.json index 025f5b98..6264abae 100644 --- a/src/renderer/lang/zh-tw/view/download.json +++ b/src/renderer/lang/zh-tw/view/download.json @@ -1,5 +1,6 @@ { "menu_play": "播放", + "menu_play_later": "稍後播放", "menu_start": "開始任務", "menu_pause": "暫停任務", "menu_file": "定位文件", diff --git a/src/renderer/lang/zh-tw/view/list.json b/src/renderer/lang/zh-tw/view/list.json index fbdf37f9..6f2d1731 100644 --- a/src/renderer/lang/zh-tw/view/list.json +++ b/src/renderer/lang/zh-tw/view/list.json @@ -7,6 +7,7 @@ "lists_sync": "同步", "lists_remove": "刪除", "list_play": "播放", + "list_play_later": "稍後播放", "list_copy_name": "複製歌曲名", "list_add_to": "添加到...", "list_move_to": "移動到...", diff --git a/src/renderer/lang/zh-tw/view/search.json b/src/renderer/lang/zh-tw/view/search.json index ac9d7a36..9a1c2168 100644 --- a/src/renderer/lang/zh-tw/view/search.json +++ b/src/renderer/lang/zh-tw/view/search.json @@ -1,5 +1,6 @@ { "list_play": "播放", + "list_play_later": "稍後播放", "list_add_to": "添加到...", "list_download": "下載", "list_source_detail": "歌曲詳情頁", @@ -10,6 +11,7 @@ "time": "時長", "lossless": "無損", "high_quality": "高品質", + "loding_list": "加載中...", "no_item": "搜我所想~~😉", "hot_search": "熱門搜索", "history_search": "歷史搜索", diff --git a/src/renderer/lang/zh-tw/view/setting.json b/src/renderer/lang/zh-tw/view/setting.json index 0ced2518..78ecffba 100644 --- a/src/renderer/lang/zh-tw/view/setting.json +++ b/src/renderer/lang/zh-tw/view/setting.json @@ -1,7 +1,6 @@ { "basic": "基本設置", "basic_theme": "主題顏色", - "basic_animation_title": "彈出層的動畫效果", "basic_animation": "彈出層隨機動畫", "basic_show_animation": "顯示切換動畫", "basic_source_title": "選擇音樂來源", @@ -12,6 +11,10 @@ "basic_sourcename_real": "原名", "basic_sourcename_alias": "別名", "basic_sourcename": "音源名字", + "basic_source_status_success": "初始化成功", + "basic_source_status_initing": "初始化中", + "basic_source_status_failed": "初始化失敗", + "basic_source_user_api_btn": "自定義源管理", "basic_window_size_title": "設置軟件窗口尺寸", "basic_window_size": "窗口尺寸", "basic_window_size_smaller": "較小", @@ -21,46 +24,38 @@ "basic_window_size_larger": "較大", "basic_window_size_oversized": "超大", "basic_window_size_huge": "巨大", - "basic_to_tray_title": "關閉時不退出軟件將其最小化到系統托盤", - "basic_to_tray": "關閉時最小化到系統托盤", + "basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤", "basic_lang_title": "軟件顯示的語言", "basic_lang": "語言", "basic_control_btn_position": "控制按鈕位置", "basic_control_btn_position_left": "左邊", "basic_control_btn_position_right": "右邊", + "play": "播放設置", - "play_toggle_title": "都不選時播放完當前歌曲就停止播放", - "play_toggle": "歌曲切換方式", - "play_toggle_list_loop": "列表循環", - "play_toggle_random": "列表隨機", - "play_toggle_list": "順序播放", - "play_toggle_single_loop": "單曲循環", + "play_save_play_time": "記住播放進度", "play_lyric_transition": "顯示歌詞翻譯", - "play_quality_title": "啟用時將優先播放320K品質的歌曲", - "play_quality": "優先播放高品質音樂", - "play_task_bar_title": "在任務欄上顯示當前歌曲播放進度", - "play_task_bar": "任務欄播放進度條", + "play_lyric_lxlrc": "使用卡拉OK式歌詞播放(如果支持)", + "play_quality": "優先播放320K品質的歌曲(如果支持)", + "play_task_bar": "在任務欄上顯示當前歌曲播放進度", "play_mediaDevice_title": "選擇聲音輸出的媒體設備", "play_mediaDevice": "音頻輸出", - "play_mediaDevice_remove_stop_play": "音頻輸出設備被改變時是否暫停播放", - "play_mediaDevice_remove_stop_play_title": "當前的聲音輸出設備被改變時是否暫停播放歌曲", + "play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲", + "desktop_lyric": "桌面歌詞設置", "desktop_lyric_enable": "顯示歌詞", "desktop_lyric_lock": "鎖定歌詞", "desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上", "desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外", + "search": "搜索設置", - "search_hot_title": "是否顯示熱門搜索", - "search_hot": "熱門搜索", - "search_history_title": "是否顯示歷史搜索記錄", - "search_history": "搜索歷史", - "search_focus_search_box_title": "啟動時是否自動聚焦搜索框", - "search_focus_search_box": "啟動時是否聚焦搜索框", + "search_hot": "顯示熱門搜索", + "search_history": "顯示歷史搜索記錄", + "search_focus_search_box": "啟動時自動聚焦搜索框", + "list": "列表設置", - "list_source_title": "是否顯示歌曲源", - "list_source": "是否顯示歌曲源(僅對我的音樂分類有效)", - "list_scroll_title": "是否記住播放列表滾動條位置", - "list_scroll": "記住列表滾動位置(僅對我的音樂分類有效)", + "list_source": "顯示歌曲源(僅對我的音樂分類有效)", + "list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)", + "download": "下載設置", "download_enable": "是否啟用下載功能", "download_path_title": "下載歌曲保存的路徑", @@ -79,6 +74,7 @@ "download_name2": "歌手 - 歌名", "download_name3": "歌名", "download_select_save_path": "選擇歌曲保存路徑", + "hot_key": "快捷鍵設置", "hot_key_local_title": "軟件內快捷鍵", "hot_key_global_title": "全局快捷鍵", @@ -98,15 +94,18 @@ "hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞", "hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換", "hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換", + "network": "網絡設置", "network_proxy_title": "HTTP代理設置(亂設置軟件將無法聯網)", "network_proxy_host": "主機", "network_proxy_port": "端口", "network_proxy_username": "用戶名", "network_proxy_password": "密碼", + "odc": "強迫症設置", "odc_clear_search_input": "離開搜索界面時清空搜索框", "odc_clear_search_list": "離開搜索界面時清空搜索列表", + "backup": "備份與恢復", "backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)", "backup_part_import_list": "導入列表", @@ -121,15 +120,18 @@ "backup_part_import_setting_desc": "選擇配置文件", "backup_part_export_setting_desc": "選擇設置保存位置", "backup_part_import_list_desc": "選擇列表文件", - "backup_part_export_list_desc": "選擇列表保存位置", + "backup_part_export_list_desc": "選擇歌單保存位置", + "other": "其他", "other_tray_theme": "托盤圖標樣式", "other_tray_theme_native": "純色", "other_tray_theme_origin": "原色", - "other_cache": "緩存大小(清理緩存後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)", - "other_cache_label": "軟件已使用緩存大小:", - "other_cache_label_title": "當前已用緩存", - "other_cache_clear_btn": "清理緩存", + "other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)", + "other_resource_cache_label": "軟件已使用緩存大小:", + "other_resource_cache_clear_btn": "清理資源緩存", + "other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)", + "other_play_list_cache_clear_btn": "清理列表緩存信息", + "update": "軟件更新", "update_latest_label": "最新版本:", "update_unknown": "未知", @@ -139,8 +141,11 @@ "update_latest": "軟件已是最新,盡情地體驗吧~🥂", "update_open_version_modal_btn": "打開更新窗口 🚀", "update_checking": "檢查更新中...", - "update_init": "更新初始化中...", + "update_init": "處理更新中...", + "about": "關於洛雪音樂", + + "is_enable": "是否啟用", "is_show": "是否顯示", "click_open": "點擊打開", diff --git a/src/renderer/lang/zh-tw/view/song_list.json b/src/renderer/lang/zh-tw/view/song_list.json index a52e8aa9..b4f484b5 100644 --- a/src/renderer/lang/zh-tw/view/song_list.json +++ b/src/renderer/lang/zh-tw/view/song_list.json @@ -8,5 +8,6 @@ "tip_2": "若遇到無法打開的歌單鏈接,歡迎反饋", "tip_3": "酷狗源不支持用歌單ID打開,但支持酷狗碼打開", "play_all": "播放", + "play_later": "稍後播放", "add_all": "收藏" } diff --git a/src/renderer/store/actions.js b/src/renderer/store/actions.js index ed8b8ec1..266dd7d5 100644 --- a/src/renderer/store/actions.js +++ b/src/renderer/store/actions.js @@ -19,16 +19,31 @@ export default { }, getVersionInfo2(state, retryNum = 0) { return new Promise((resolve, reject) => { - httpGet('https://cdn.stsky.cn/lx-music/desktop/version.json', { + httpGet('https://gitee.com/lyswhut/lx-music-desktop-versions/raw/master/version.json', { timeout: 20000, }, (err, resp, body) => { + if (!err && !body.version) err = new Error(JSON.stringify(body)) if (err) { return ++retryNum > 3 - ? reject(err) + ? this.dispatch('getVersionInfo3').then(resolve).catch(reject) : this.dispatch('getVersionInfo2', retryNum).then(resolve).catch(reject) } resolve(body) }) }) }, + getVersionInfo3(state, retryNum = 0) { + return new Promise((resolve, reject) => { + httpGet('https://cdn.stsky.cn/lx-music/desktop/version.json', { + timeout: 20000, + }, (err, resp, body) => { + if (err) { + return ++retryNum > 3 + ? reject(err) + : this.dispatch('getVersionInfo3', retryNum).then(resolve).catch(reject) + } + resolve(body) + }) + }) + }, } diff --git a/src/renderer/store/modules/download.js b/src/renderer/store/modules/download.js index 5e438108..d4e3c2f8 100644 --- a/src/renderer/store/modules/download.js +++ b/src/renderer/store/modules/download.js @@ -3,7 +3,7 @@ import fs from 'fs' import path from 'path' import music from '../../utils/music' import { getMusicType } from '../../utils/music/utils' -import { setMeta, saveLrc } from '../../utils' +import { setMeta, saveLrc, getLyric, setLyric, getMusicUrl, setMusicUrl } from '../../utils' // state const state = { @@ -147,15 +147,20 @@ const pauseTasks = async(store, list, runs = []) => { await pauseTasks(store, list, runs) } -const getUrl = (downloadInfo, isRefresh) => { - const url = downloadInfo.musicInfo.typeUrl[downloadInfo.type] +const getUrl = async(downloadInfo, isRefresh) => { + const cachedUrl = await getMusicUrl(downloadInfo.musicInfo, downloadInfo.type) if (!downloadInfo.musicInfo._types[downloadInfo.type]) { // 兼容旧版酷我源搜索列表过滤128k音质的bug - if (!(downloadInfo.musicInfo.source == 'kw' && downloadInfo.type == '128k')) return Promise.reject(new Error('该歌曲没有可下载的音频')) + if (!(downloadInfo.musicInfo.source == 'kw' && downloadInfo.type == '128k')) throw new Error('该歌曲没有可下载的音频') // return Promise.reject(new Error('该歌曲没有可下载的音频')) } - return url && !isRefresh ? Promise.resolve({ url }) : music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise + return cachedUrl && !isRefresh + ? cachedUrl + : music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise.then(({ url }) => { + setMusicUrl(downloadInfo.musicInfo, downloadInfo.type, url) + return url + }) } // 修复 1.1.x版本 酷狗源歌词格式 @@ -179,12 +184,17 @@ const saveMeta = (downloadInfo, filePath, isEmbedPic, isEmbedLyric) => { }) : Promise.resolve(), isEmbedLyric - ? downloadInfo.musicInfo.lrc - ? Promise.resolve({ lyric: downloadInfo.musicInfo.lrc, tlyric: downloadInfo.musicInfo.tlrc || '' }) - : music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.catch(err => { - console.log(err) - return null - }) + ? getLyric(downloadInfo.musicInfo).then(lrcInfo => { + return lrcInfo.lyric + ? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' }) + : music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.then(({ lyric, tlyric, lxlyric }) => { + setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric }) + return { lyric, tlyric, lxlyric } + }).catch(err => { + console.log(err) + return null + }) + }) : Promise.resolve(), ] Promise.all(tasks).then(([imgUrl, lyrics = {}]) => { @@ -205,9 +215,14 @@ const saveMeta = (downloadInfo, filePath, isEmbedPic, isEmbedLyric) => { * @param {*} filePath */ const downloadLyric = (downloadInfo, filePath) => { - const promise = downloadInfo.musicInfo.lrc - ? Promise.resolve({ lyric: downloadInfo.musicInfo.lrc, tlyric: downloadInfo.musicInfo.tlrc || '' }) - : music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise + const promise = getLyric(downloadInfo.musicInfo).then(lrcInfo => { + return lrcInfo.lyric + ? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' }) + : music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.then(({ lyric, tlyric, lxlyric }) => { + setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric }) + return { lyric, tlyric, lxlyric } + }) + }) promise.then(lrcs => { if (lrcs.lyric) { lrcs.lyric = fixKgLyric(lrcs.lyric) @@ -218,12 +233,12 @@ const downloadLyric = (downloadInfo, filePath) => { const refreshUrl = function(commit, downloadInfo) { commit('setStatusText', { downloadInfo, text: '链接失效,正在刷新链接' }) - getUrl(downloadInfo, true).then(result => { - commit('updateUrl', { downloadInfo, url: result.url }) + getUrl(downloadInfo, true).then(url => { + commit('updateUrl', { downloadInfo, url }) commit('setStatusText', { downloadInfo, text: '链接刷新成功' }) const dl = dls[downloadInfo.key] if (!dl) return - dl.refreshUrl(result.url) + dl.refreshUrl(url) dl.start().catch(err => { commit('onError', { downloadInfo, errorMsg: err.message }) commit('setStatusText', { downloadInfo, text: err.message }) @@ -379,10 +394,10 @@ const actions = { commit('setStatusText', { downloadInfo, text: '获取URL中...' }) let p = options.url ? Promise.resolve() - : getUrl(downloadInfo).then(result => { - commit('updateUrl', { downloadInfo, url: result.url }) - if (!result.url) return Promise.reject(new Error('获取URL失败')) - options.url = result.url + : getUrl(downloadInfo).then(url => { + commit('updateUrl', { downloadInfo, url }) + if (!url) return Promise.reject(new Error('获取URL失败')) + options.url = url }) p.then(() => { tryNum[downloadInfo.key] = 0 diff --git a/src/renderer/store/modules/list.js b/src/renderer/store/modules/list.js index ce6ce500..4228b2c2 100644 --- a/src/renderer/store/modules/list.js +++ b/src/renderer/store/modules/list.js @@ -1,3 +1,6 @@ +import musicSdk from '../../utils/music' +import { clearLyric, clearMusicUrl } from '../../utils' + let allList = {} window.allList = allList @@ -48,7 +51,12 @@ const getters = { // actions const actions = { - + getOtherSource({ state, commit }, musicInfo) { + return (musicInfo.otherSource && musicInfo.otherSource.length ? Promise.resolve(musicInfo.otherSource) : musicSdk.findMusic(musicInfo)).then(otherSource => { + commit('setOtherSource', { musicInfo, otherSource }) + return otherSource + }) + }, } // mitations @@ -142,9 +150,9 @@ const mutations = { if (!targetList) return targetList.list.splice(0, targetList.list.length) }, - updateMusicInfo(state, { id, index, data }) { + updateMusicInfo(state, { id, index, data, musicInfo = {} }) { let targetList = allList[id] - if (!targetList) return + if (!targetList) return Object.assign(musicInfo, data) Object.assign(targetList.list[index], data) }, createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) { @@ -191,6 +199,28 @@ const mutations = { targetList.list.splice(sortNum - 1, 0, ...musicInfos) }, + clearCache() { + const lists = Object.values(allList) + for (const { list } of lists) { + for (const item of list) { + if (item.otherSource) item.otherSource = null + if (item.typeUrl['128k']) delete item.typeUrl['128k'] + if (item.typeUrl['320k']) delete item.typeUrl['320k'] + if (item.typeUrl.flac) delete item.typeUrl.flac + if (item.typeUrl.wav) delete item.typeUrl.wav + + // v1.8.2以前的Lyric + if (item.lxlrc) delete item.lxlrc + if (item.lrc) delete item.lrc + if (item.tlrc) delete item.tlrc + } + } + clearMusicUrl() + clearLyric() + }, + setOtherSource(state, { musicInfo, otherSource }) { + musicInfo.otherSource = otherSource + }, } export default { diff --git a/src/renderer/store/modules/player.js b/src/renderer/store/modules/player.js index 62c12804..d926b204 100644 --- a/src/renderer/store/modules/player.js +++ b/src/renderer/store/modules/player.js @@ -1,4 +1,13 @@ +import path from 'path' import music from '../../utils/music' +import { + getRandom, + checkPath, + getLyric as getStoreLyric, + setLyric, + setMusicUrl, + getMusicUrl, +} from '../../utils' // state const state = { @@ -10,107 +19,360 @@ const state = { changePlay: false, isShowPlayerDetail: false, playedList: [], + + playMusicInfo: null, + tempPlayList: [], } let urlRequest -let picRequest -let lrcRequest +// let picRequest +// let lrcRequest + +const filterList = async({ playedList, listInfo, savePath, commit }) => { + // if (this.list.listName === null) return + let list + let canPlayList = [] + const filteredPlayedList = playedList.filter(({ listId, isTempPlay }) => listInfo.id === listId && !isTempPlay).map(({ musicInfo }) => musicInfo) + if (listInfo.id == 'download') { + list = [] + for (const item of listInfo.list) { + const filePath = path.join(savePath, item.fileName) + if (!await checkPath(filePath) || !item.isComplate || /\.ape$/.test(filePath)) continue + + canPlayList.push(item) + + // 排除已播放音乐 + let index = filteredPlayedList.indexOf(item) + if (index > -1) { + filteredPlayedList.splice(index, 1) + continue + } + list.push(item) + } + } else { + list = listInfo.list.filter(s => { + // if (!assertApiSupport(s.source)) return false + canPlayList.push(s) + + let index = filteredPlayedList.indexOf(s) + if (index > -1) { + filteredPlayedList.splice(index, 1) + return false + } + return true + }) + } + if (!list.length && playedList.length) { + commit('clearPlayedList') + return canPlayList + } + return list +} + +const getPic = function(musicInfo, retryedSource = [], originMusic) { + // console.log(musicInfo.source) + return music[musicInfo.source].getPic(musicInfo).promise.catch(err => { + if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source) + return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => { + if (!originMusic) originMusic = musicInfo + console.log('find otherSource', otherSource) + if (otherSource.length) { + for (const item of otherSource) { + if (retryedSource.includes(item.source)) continue + console.log('try toggle to: ', item.source, item.name, item.singer, item.interval) + return getPic.call(this, item, retryedSource, originMusic) + } + } + return Promise.reject(err) + }) + }) +} +const getLyric = function(musicInfo, retryedSource = [], originMusic) { + return music[musicInfo.source].getLyric(musicInfo).promise.catch(err => { + if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source) + return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => { + if (!originMusic) originMusic = musicInfo + console.log('find otherSource', otherSource) + if (otherSource.length) { + for (const item of otherSource) { + if (retryedSource.includes(item.source)) continue + console.log('try toggle to: ', item.source, item.name, item.singer, item.interval) + return getLyric.call(this, item, retryedSource, originMusic) + } + } + return Promise.reject(err) + }) + }) +} // getters const getters = { list: state => state.listInfo.list, - listId: state => state.listInfo.id, changePlay: satte => satte.changePlay, - playIndex: state => state.playIndex, + playInfo(state) { + if (state.playMusicInfo == null) return { listId: null, playIndex: -1, playListId: null, listPlayIndex: -1, isPlayList: false, musicInfo: null } + const playListId = state.listInfo.id + let listId = state.playMusicInfo.listId + const isTempPlay = !!state.playMusicInfo.isTempPlay + const isPlayList = listId === playListId + let playIndex = -1 + let listPlayIndex = state.playIndex + + if (listId != '__temp__') { + if (isPlayList) { + playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo) + if (!isTempPlay) listPlayIndex = playIndex + } else { + let list = window.allList[listId] + if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo) + } + } + // console.log({ + // listId, + // playIndex, + // playListId, + // listPlayIndex, + // isPlayList, + // isTempPlay, + // musicInfo: state.playMusicInfo.musicInfo, + // }) + return { + listId, + playIndex, + playListId, + listPlayIndex, + isPlayList, + isTempPlay, + musicInfo: state.playMusicInfo.musicInfo, + } + }, isShowPlayerDetail: state => state.isShowPlayerDetail, + playMusicInfo: state => state.playMusicInfo, playedList: state => state.playedList, + tempPlayList: state => state.tempPlayList, } // actions const actions = { - getUrl({ commit, state }, { musicInfo, originMusic, type, isRefresh }) { + async getUrl({ commit, state }, { musicInfo, originMusic, type, isRefresh }) { if (!musicInfo._types[type]) { // 兼容旧版酷我源搜索列表过滤128k音质的bug - if (!(musicInfo.source == 'kw' && type == '128k')) return Promise.reject(new Error('该歌曲没有可播放的音频')) + if (!(musicInfo.source == 'kw' && type == '128k')) throw new Error('该歌曲没有可播放的音频') // return Promise.reject(new Error('该歌曲没有可播放的音频')) } if (urlRequest && urlRequest.cancelHttp) urlRequest.cancelHttp() - if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve() + const cachedUrl = await getMusicUrl(musicInfo, type) + if (cachedUrl && !isRefresh) return cachedUrl + urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type) - return urlRequest.promise.then(result => { - if (originMusic) commit('setUrl', { musicInfo: originMusic, url: result.url, type }) - commit('setUrl', { musicInfo, url: result.url, type }) + + return urlRequest.promise.then(({ url }) => { + if (originMusic) commit('setUrl', { musicInfo: originMusic, url, type }) + commit('setUrl', { musicInfo, url, type }) urlRequest = null + return url }).catch(err => { urlRequest = null return Promise.reject(err) }) }, getPic({ commit, state }, musicInfo) { - if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp() - picRequest = music[musicInfo.source].getPic(musicInfo) - return picRequest.promise.then(url => { - picRequest = null + // if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp() + // picRequest = music[musicInfo.source].getPic(musicInfo) + return getPic.call(this, musicInfo).then(url => { + // picRequest = null commit('getPic', { musicInfo, url }) }).catch(err => { - picRequest = null + // picRequest = null return Promise.reject(err) }) }, - getLrc({ commit, state }, musicInfo) { - if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp() - if (musicInfo.lrc && musicInfo.tlrc != null) { - if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) { - let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '') - commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc }) - } - return Promise.resolve() + async getLrc({ commit, state }, musicInfo) { + const lrcInfo = await getStoreLyric(musicInfo) + // if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp() + if (lrcInfo.lyric && lrcInfo.tlyric != null) { + // if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) { + // let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '') + // commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc }) + // } else if (musicInfo.lrc.startsWith('[id:$00000000]')) { + // let str = musicInfo.lrc.replace('[id:$00000000]\n', '') + // commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc }) + // } + + if ((lrcInfo.lxlyric == null && musicInfo.source != 'kg') || lrcInfo.lxlyric != null) return lrcInfo } - - lrcRequest = music[musicInfo.source].getLyric(musicInfo) - return lrcRequest.promise.then(({ lyric, tlyric }) => { - lrcRequest = null - commit('setLrc', { musicInfo, lyric, tlyric }) + // lrcRequest = music[musicInfo.source].getLyric(musicInfo) + return getLyric.call(this, musicInfo).then(({ lyric, tlyric, lxlyric }) => { + // lrcRequest = null + commit('setLrc', { musicInfo, lyric, tlyric, lxlyric }) + return { lyric, tlyric, lxlyric } }).catch(err => { - lrcRequest = null + // lrcRequest = null return Promise.reject(err) }) }, + + async playPrev({ state, rootState, commit, getters }) { + const currentListId = state.listInfo.id + const currentList = state.listInfo.list + if (state.playedList.length) { + // 从已播放列表移除播放列表已删除的歌曲 + let index + for (index = state.playedList.indexOf(state.playMusicInfo) - 1; index > -1; index--) { + const playMusicInfo = state.playedList[index] + if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) { + commit('removePlayedList', index) + continue + } + break + } + + if (index > -1) { + commit('setPlayMusicInfo', state.playedList[index]) + return + } + } + + let filteredList = await filterList({ + listInfo: state.listInfo, + playedList: state.playedList, + savePath: rootState.setting.download.savePath, + commit, + }) + if (!filteredList.length) return commit('setPlayMusicInfo', null) + const playInfo = getters.playInfo + let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex]) + if (currentIndex == -1) currentIndex = 0 + let nextIndex = currentIndex + if (!playInfo.isTempPlay) { + switch (rootState.setting.player.togglePlayMethod) { + case 'random': + nextIndex = getRandom(0, filteredList.length) + break + case 'listLoop': + case 'list': + nextIndex = currentIndex === 0 ? filteredList.length - 1 : currentIndex - 1 + break + case 'singleLoop': + break + default: + nextIndex = -1 + return + } + if (nextIndex < 0) return + } + + commit('setPlayMusicInfo', { + musicInfo: filteredList[nextIndex], + listId: currentListId, + }) + }, + async playNext({ state, rootState, commit, getters }) { + if (state.tempPlayList.length) { + const playMusicInfo = state.tempPlayList[0] + commit('removeTempPlayList', 0) + commit('setPlayMusicInfo', playMusicInfo) + return + } + const currentListId = state.listInfo.id + const currentList = state.listInfo.list + if (state.playedList.length) { + // 从已播放列表移除播放列表已删除的歌曲 + let index + for (index = state.playedList.indexOf(state.playMusicInfo) + 1; index < state.playedList.length; index++) { + const playMusicInfo = state.playedList[index] + if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) { + commit('removePlayedList', index) + continue + } + break + } + + if (index < state.playedList.length) { + commit('setPlayMusicInfo', state.playedList[index]) + return + } + } + let filteredList = await filterList({ + listInfo: state.listInfo, + playedList: state.playedList, + savePath: rootState.setting.download.savePath, + commit, + }) + + if (!filteredList.length) return commit('setPlayMusicInfo', null) + const playInfo = getters.playInfo + const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex]) + let nextIndex = currentIndex + switch (rootState.setting.player.togglePlayMethod) { + case 'listLoop': + nextIndex = currentIndex === filteredList.length - 1 ? 0 : currentIndex + 1 + break + case 'random': + nextIndex = getRandom(0, filteredList.length) + break + case 'list': + nextIndex = currentIndex === filteredList.length - 1 ? -1 : currentIndex + 1 + break + case 'singleLoop': + break + default: + nextIndex = -1 + return + } + if (nextIndex < 0) return + + commit('setPlayMusicInfo', { + musicInfo: filteredList[nextIndex], + listId: currentListId, + }) + }, } // mitations const mutations = { - setUrl(state, datas) { - datas.musicInfo.typeUrl = Object.assign({}, datas.musicInfo.typeUrl, { [datas.type]: datas.url }) + setUrl(state, { musicInfo, type, url }) { + setMusicUrl(musicInfo, type, url) }, getPic(state, datas) { datas.musicInfo.img = datas.url }, setLrc(state, datas) { - datas.musicInfo.lrc = datas.lyric - datas.musicInfo.tlrc = datas.tlyric + // datas.musicInfo.lrc = datas.lyric + // datas.musicInfo.tlrc = datas.tlyric + // datas.musicInfo.lxlrc = datas.lxlyric + setLyric(datas.musicInfo, { + lyric: datas.lyric, + tlyric: datas.tlyric, + lxlyric: datas.lxlyric, + }) }, setList(state, { list, index }) { + state.playMusicInfo = { + musicInfo: list.list[index], + listId: list.id, + } state.listInfo = list state.playIndex = index state.changePlay = true + // console.log(state.playMusicInfo) if (state.playedList.length) this.commit('player/clearPlayedList') + if (state.tempPlayList.length) this.commit('player/clearTempPlayeList') }, setPlayIndex(state, index) { state.playIndex = index - state.changePlay = true - // console.log(state.changePlay) }, - fixPlayIndex(state, index) { - state.playIndex = index + setChangePlay(state) { + state.changePlay = true }, resetChangePlay(state) { state.changePlay = false }, setPlayedList(state, item) { + // console.log(item) if (state.playedList.includes(item)) return state.playedList.push(item) }, @@ -118,11 +380,35 @@ const mutations = { state.playedList.splice(index, 1) }, clearPlayedList(state) { - state.playedList = [] + state.playedList.splice(0, state.playedList.length) }, visiblePlayerDetail(state, visible) { state.isShowPlayerDetail = visible }, + setTempPlayList(state, list) { + state.tempPlayList.push(...list.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true }))) + if (!state.playMusicInfo) this.commit('player/playNext') + }, + removeTempPlayList(state, index) { + state.tempPlayList.splice(index, 1) + }, + clearTempPlayeList(state) { + state.tempPlayList.splice(0, state.tempPlayList.length) + }, + + setPlayMusicInfo(state, playMusicInfo) { + let playIndex = state.playIndex + if (playMusicInfo == null) { + playIndex = -1 + } else { + let listId = playMusicInfo.listId + if (listId != '__temp__' && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(playMusicInfo.musicInfo) + } + + state.playMusicInfo = playMusicInfo + state.playIndex = playIndex + state.changePlay = true + }, } export default { diff --git a/src/renderer/store/modules/search.js b/src/renderer/store/modules/search.js index ff3a3e2b..9e4d26ef 100644 --- a/src/renderer/store/modules/search.js +++ b/src/renderer/store/modules/search.js @@ -127,7 +127,7 @@ const actions = { } })) } - Promise.all(task).then(results => commit('setLists', { results, page })) + return Promise.all(task).then(results => commit('setLists', { results, page })) } else { return music[rootState.setting.search.searchSource].musicSearch.search(text, page, limit).catch(error => { console.log(error) @@ -167,7 +167,7 @@ const mutations = { list.push(...source.list) pages.push(source.allPage) total += source.total - limit += source.limit + // limit = Math.max(source.limit, limit) } state.allPage = Math.max(...pages) state.total = total diff --git a/src/renderer/utils/index.js b/src/renderer/utils/index.js index 51fd5b3a..1228b68b 100644 --- a/src/renderer/utils/index.js +++ b/src/renderer/utils/index.js @@ -232,6 +232,7 @@ export const objectDeepMerge = (target, source, mergedObj) => { * @param {*} url */ export const openUrl = url => { + if (!/^https?:\/\//.test(url)) return shell.openExternal(url) } @@ -370,7 +371,7 @@ export const clearCache = () => rendererInvoke(NAMES.mainWindow.clear_cache) export const setWindowSize = (width, height) => rendererSend(NAMES.mainWindow.set_window_size, { width, height }) -export const getProxyInfo = () => window.globalObj.proxy.enable +export const getProxyInfo = () => window.globalObj.proxy.enable && window.globalObj.proxy.host ? `http://${window.globalObj.proxy.username}:${window.globalObj.proxy.password}@${window.globalObj.proxy.host}:${window.globalObj.proxy.port};` : undefined @@ -378,7 +379,7 @@ export const getProxyInfo = () => window.globalObj.proxy.enable export const assertApiSupport = source => window.globalObj.qualityList[source] != undefined export const getSetting = () => rendererInvoke(NAMES.mainWindow.get_setting) -export const saveSetting = () => rendererInvoke(NAMES.mainWindow.set_app_setting) +export const saveSetting = setting => rendererInvoke(NAMES.mainWindow.set_app_setting, setting) export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).catch(error => { rendererInvoke(NAMES.mainWindow.get_data_path).then(dataPath => { @@ -395,3 +396,28 @@ export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).c return rendererInvoke(NAMES.mainWindow.get_playlist, true) }) +// 解析URL参数为对象 +export const parseUrlParams = str => { + const params = {} + if (typeof str !== 'string') return params + const paramsArr = str.split('&') + for (const param of paramsArr) { + let [key, value] = param.split('=') + params[key] = value + } + return params +} + +export const getLyric = musicInfo => rendererInvoke(NAMES.mainWindow.get_lyric, `${musicInfo.source}_${musicInfo.songmid}`) +export const setLyric = (musicInfo, { lyric, tlyric, lxlyric }) => rendererSend(NAMES.mainWindow.save_lyric, { + id: `${musicInfo.source}_${musicInfo.songmid}`, + lyrics: { lyric, tlyric, lxlyric }, +}) +export const clearLyric = () => rendererSend(NAMES.mainWindow.clear_lyric) + +export const getMusicUrl = (musicInfo, type) => rendererInvoke(NAMES.mainWindow.get_music_url, `${musicInfo.source}_${musicInfo.songmid}_${type}`) +export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWindow.save_music_url, { + id: `${musicInfo.source}_${musicInfo.songmid}_${type}`, + url, +}) +export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url) diff --git a/src/renderer/utils/lyric-font-player/font-player.js b/src/renderer/utils/lyric-font-player/font-player.js new file mode 100644 index 00000000..fbdfbab8 --- /dev/null +++ b/src/renderer/utils/lyric-font-player/font-player.js @@ -0,0 +1,291 @@ +const { getNow, TimeoutTools } = require('./utils') + +// const fontFormateRxp = /(?=<\d+,\d+>).*?/g +const fontSplitRxp = /(?=<\d+,\d+>).*?/g +const timeRxp = /<(\d+),(\d+)>/ + + +// Create animation +const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [ + { backgroundSize: '0 100%' }, + { backgroundSize: '100% 100%' }, +], { + duration, + easing: 'linear', +}, +), document.timeline) + + +// https://jsfiddle.net/ceqpnbky/ +// https://jsfiddle.net/ceqpnbky/1/ + +module.exports = class FontPlayer { + constructor({ lyric = '', translationLyric = '', lineClassName = '', fontClassName = '', translationClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) { + this.lyric = lyric + this.translationLyric = translationLyric + + this.lineClassName = lineClassName + this.fontClassName = fontClassName + this.translationClassName = translationClassName + this.lineModeClassName = lineModeClassName + this.shadowContent = shadowContent + this.shadowClassName = shadowClassName + + this.isPlay = false + this.curFontNum = 0 + this.maxFontNum = 0 + this._performanceTime = 0 + this._performanceOffsetTime = 0 + + this.fontContent = null + + this.timeoutTools = new TimeoutTools() + this.waitPlayTimeout = new TimeoutTools() + + this._init() + } + + _init() { + if (this.lyric == null) this.lyric = '' + + this.isLineMode = false + + this.lineContent = document.createElement('div') + if (this.lineClassName) this.lineContent.classList.add(this.lineClassName) + this.fontContent = document.createElement('div') + this.fontContent.style = 'position:relative;display:inline-block;' + if (this.fontClassName) this.fontContent.classList.add(this.fontClassName) + if (this.shadowContent) { + this.fontShadowContent = document.createElement('div') + this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;' + this.fontShadowContent.className = this.shadowClassName + this.fontContent.appendChild(this.fontShadowContent) + } + this.lineContent.appendChild(this.fontContent) + if (this.translationLyric) { + this.translationContent = document.createElement('div') + this.translationContent.style = 'position:relative;display:inline-block;' + this.translationContent.className = this.translationClassName + this.translationContent.textContent = this.translationLyric + this.lineContent.appendChild(document.createElement('br')) + this.lineContent.appendChild(this.translationContent) + + if (this.shadowContent) { + this.translationShadowContent = document.createElement('div') + this.translationShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;' + this.translationShadowContent.className = this.shadowClassName + this.translationShadowContent.textContent = this.translationLyric + this.translationContent.appendChild(this.translationShadowContent) + } + } + this._parseLyric() + } + + _parseLyric() { + const fonts = this.lyric.split(fontSplitRxp) + // console.log(fonts) + + this.maxFontNum = fonts.length - 1 + this.fonts = [] + let text + for (const font of fonts) { + text = font.replace(timeRxp, '') + if (RegExp.$2 == '') return this._handleLineParse() + const time = parseInt(RegExp.$2) + + const dom = document.createElement('span') + let shadowDom + dom.textContent = text + const animation = createAnimation(dom, time) + this.fontContent.appendChild(dom) + + if (this.shadowContent) { + shadowDom = document.createElement('span') + shadowDom.textContent = text + this.fontShadowContent.appendChild(shadowDom) + } + // dom.style = shadowDom.style = this.fontStyle + // dom.className = shadowDom.className = this.fontClassName + + this.fonts.push({ + text, + startTime: parseInt(RegExp.$1), + time, + dom, + shadowDom, + animation, + }) + } + // console.log(this.fonts) + } + + _handleLineParse() { + this.isLineMode = true + const dom = document.createElement('span') + let shadowDom + dom.classList.add(this.lineModeClassName) + dom.textContent = this.lyric + if (this.shadowContent) { + shadowDom = document.createElement('span') + shadowDom.textContent = this.lyric + this.fontShadowContent.appendChild(shadowDom) + } + this.fontContent.appendChild(dom) + this.fonts.push({ + text: this.lyric, + dom, + shadowDom, + }) + } + + _currentTime() { + return getNow() - this._performanceTime + this._performanceOffsetTime + } + + _findcurFontNum(curTime) { + const length = this.fonts.length + for (let index = 0; index < length; index++) if (curTime <= this.fonts[index].startTime) return index === 0 ? 0 : index - 1 + return length - 1 + } + + _handlePlayMaxFontNum() { + let curFont = this.fonts[this.curFontNum] + // console.log(curFont.text) + const currentTime = this._currentTime() + const driftTime = currentTime - curFont.startTime + if (currentTime > curFont.startTime + curFont.time) { + this._handlePlayFont(curFont, driftTime, true) + this.pause() + } else { + this._handlePlayFont(curFont, driftTime) + this.isPlay = false + } + } + + _handlePlayFont(font, currentTime, toFinishe) { + switch (font.animation.playState) { + case 'finished': + break + case 'idle': + font.dom.style.backgroundSize = '100% 100%' + if (!toFinishe) font.animation.play() + break + default: + if (toFinishe) { + font.animation.cancel() + } else { + font.animation.currentTime = currentTime + font.animation.play() + } + break + } + } + + _handlePlayLine(isPlayed) { + this.isPlay = false + this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0' + } + + _handlePauseFont(font) { + if (font.animation.playState == 'running') font.animation.pause() + } + + _refresh() { + this.curFontNum++ + // console.log('curFontNum time', this.fonts[this.curFontNum].time) + if (this.curFontNum === this.maxFontNum) return this._handlePlayMaxFontNum() + let curFont = this.fonts[this.curFontNum] + let nextFont = this.fonts[this.curFontNum + 1] + // console.log(curFont, nextFont, this.curFontNum, this.maxFontNum) + const currentTime = this._currentTime() + // console.log(curFont.text) + const driftTime = currentTime - curFont.startTime + + // console.log(currentTime, driftTime) + + if (driftTime >= 0 || this.curFontNum == 0) { + this.delay = nextFont.startTime - curFont.startTime - driftTime + if (this.delay > 0) { + this._handlePlayFont(curFont, driftTime) + this.timeoutTools.start(() => { + this._refresh() + }, this.delay) + return + } + } else if (this.curFontNum == 0) { + this.curFontNum-- + this.waitPlayTimeout.start(() => { + this._refresh() + }, -driftTime) + return + } + + this.curFontNum = this._findcurFontNum(currentTime) + for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true) + this.curFontNum-- + this._refresh() + } + + play(curTime = 0) { + // console.log('play', curTime) + if (!this.fonts.length) return + this.pause() + + if (this.isLineMode) return this._handlePlayLine(true) + this.isPlay = true + this._performanceTime = getNow() - curTime + this._performanceOffsetTime = 0 + if (this._performanceTime < 0) { + this._performanceOffsetTime = -this._performanceTime + this._performanceTime = 0 + } + + this.curFontNum = this._findcurFontNum(curTime) + for (let i = this.curFontNum; i > -1; i--) { + this._handlePlayFont(this.fonts[i], 0, true) + } + for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) { + let font = this.fonts[i] + font.animation.cancel() + font.dom.style.backgroundSize = '0 100%' + } + + this.curFontNum-- + + this._refresh() + } + + pause() { + if (!this.isPlay) return + this.isPlay = false + this.timeoutTools.clear() + this.waitPlayTimeout.clear() + this._handlePauseFont(this.fonts[this.curFontNum]) + if (this.curFontNum === this.maxLine) return + const curFontNum = this._findcurFontNum(this._currentTime()) + if (this.curFontNum === curFontNum) return + for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true) + } + + finish() { + this.pause() + if (this.isLineMode) return this._handlePlayLine(true) + + for (const font of this.fonts) { + font.animation.cancel() + font.dom.style.backgroundSize = '100% 100%' + } + this.curFontNum = this.maxFontNum + } + + reset() { + this.pause() + if (this.isLineMode) return this._handlePlayLine(false) + for (const font of this.fonts) { + font.animation.cancel() + font.dom.style.backgroundSize = '0 100%' + } + this.curFontNum = 0 + } +} + diff --git a/src/renderer/utils/lyric-font-player/index.js b/src/renderer/utils/lyric-font-player/index.js new file mode 100644 index 00000000..d542b640 --- /dev/null +++ b/src/renderer/utils/lyric-font-player/index.js @@ -0,0 +1,167 @@ +const LinePlayer = require('./line-player') +const FontPlayer = require('./font-player') + +const fontTimeExp = /<(\d+),(\d+)>/g + +module.exports = class Lyric { + constructor({ + lyric = '', + translationLyric = '', + offset = 150, + lineClassName = '', + fontClassName = 'font', + translationClassName = 'translation', + activeLineClassName = 'active', + lineModeClassName = 'line', + shadowClassName = '', + shadowContent = false, + onPlay = function() { }, + onSetLyric = function() { }, + }) { + this.lyric = lyric + this.translationLyric = translationLyric + this.offset = offset + this.onPlay = onPlay + this.onSetLyric = onSetLyric + + this.lineClassName = lineClassName + this.fontClassName = fontClassName + this.translationClassName = translationClassName + this.activeLineClassName = activeLineClassName + this.lineModeClassName = lineModeClassName + this.shadowClassName = shadowClassName + this.shadowContent = shadowContent + + this.playingLineNum = -1 + this.isLineMode = false + } + + _init() { + this.playingLineNum = -1 + this.isLineMode = false + + if (this.linePlayer) { + this.linePlayer.setLyric(this.lyric, this.translationLyric) + } else { + this.linePlayer = new LinePlayer({ + lyric: this.lyric, + translationLyric: this.translationLyric, + offset: this.offset, + onPlay: this._handleLinePlayerOnPlay, + onSetLyric: this._handleLinePlayerOnSetLyric, + }) + } + } + + _handleLinePlayerOnPlay = (num, text, curTime) => { + if (this.isLineMode) { + if (num < this.playingLineNum + 1) { + for (let i = this.playingLineNum; i > num - 1; i--) { + const font = this._lineFonts[i] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else if (num > this.playingLineNum + 1) { + for (let i = Math.max(this.playingLineNum, 0); i < num; i++) { + const font = this._lineFonts[i] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else if (this.playingLineNum > -1) { + const font = this._lineFonts[this.playingLineNum] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else { + if (num < this.playingLineNum + 1) { + for (let i = this.playingLineNum; i > num - 1; i--) { + const font = this._lineFonts[i] + font.lineContent.classList.remove(this.activeLineClassName) + font.reset() + } + } else if (num > this.playingLineNum + 1) { + for (let i = Math.max(this.playingLineNum, 0); i < num; i++) { + const font = this._lineFonts[i] + font.lineContent.classList.remove(this.activeLineClassName) + font.finish() + } + } else if (this.playingLineNum > -1) { + const font = this._lineFonts[this.playingLineNum] + font.lineContent.classList.remove(this.activeLineClassName) + } + } + this.playingLineNum = num + const font = this._lineFonts[num] + font.lineContent.classList.add(this.activeLineClassName) + font.play(curTime - this._lines[num].time) + this.onPlay(num, this._lines[num].text) + } + + _handleLinePlayerOnSetLyric = lyricLines => { + // console.log(lyricLines) + // this._lines = lyricsLines + this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text) + + this._lineFonts = [] + if (this.isLineMode) { + this._lines = lyricLines.map(line => { + const fontPlayer = new FontPlayer({ + lyric: line.text, + translationLyric: line.translation, + lineClassName: this.lineClassName, + fontClassName: this.fontClassName, + translationClassName: this.translationClassName, + lineModeClassName: this.lineModeClassName, + shadowClassName: this.shadowClassName, + shadowContent: this.shadowContent, + }) + + this._lineFonts.push(fontPlayer) + return { + text: line.text, + time: line.time, + dom_line: fontPlayer.lineContent, + } + }) + } else { + this._lines = lyricLines.map(line => { + const fontPlayer = new FontPlayer({ + lyric: line.text, + translationLyric: line.translation, + lineClassName: this.lineClassName, + fontClassName: this.fontClassName, + translationClassName: this.translationClassName, + shadowClassName: this.shadowClassName, + shadowContent: this.shadowContent, + }) + + this._lineFonts.push(fontPlayer) + return { + text: line.text.replace(fontTimeExp, ''), + time: line.time, + translation: line.translation, + dom_line: fontPlayer.lineContent, + } + }) + } + + this.onSetLyric(this._lines) + } + + play(curTime) { + if (!this.linePlayer) return + this.linePlayer.play(curTime) + } + + pause() { + if (!this.linePlayer) return + this.linePlayer.pause() + if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause() + } + + setLyric(lyric, translationLyric) { + this.lyric = lyric + this.translationLyric = translationLyric + this._init() + } +} diff --git a/src/renderer/utils/lyric-font-player/line-player.js b/src/renderer/utils/lyric-font-player/line-player.js new file mode 100644 index 00000000..78f4893c --- /dev/null +++ b/src/renderer/utils/lyric-font-player/line-player.js @@ -0,0 +1,177 @@ +const { getNow, TimeoutTools } = require('./utils') + +const timeExp = /^\[([\d:.]*)\]{1}/g +const tagRegMap = { + title: 'ti', + artist: 'ar', + album: 'al', + offset: 'offset', + by: 'by', +} + +const timeoutTools = new TimeoutTools() + +module.exports = class LinePlayer { + constructor({ lyric = '', translationLyric = '', offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) { + this.lyric = lyric + this.translationLyric = translationLyric + this.tags = {} + this.lines = null + this.translationLines = null + this.onPlay = onPlay + this.onSetLyric = onSetLyric + this.isPlay = false + this.curLineNum = 0 + this.maxLine = 0 + this.offset = offset + this.isOffseted = false + this._performanceTime = 0 + this._performanceOffsetTime = 0 + this._init() + } + + _init() { + if (this.lyric == null) this.lyric = '' + if (this.translationLyric == null) this.translationLyric = '' + this._initTag() + this._initLines() + this.onSetLyric(this.lines) + } + + _initTag() { + for (let tag in tagRegMap) { + const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i')) + this.tags[tag] = (matches && matches[1]) || '' + } + } + + _initLines() { + this.lines = [] + this.translationLines = [] + const lines = this.lyric.split('\n') + const linesMap = {} + // const translationLines = this.translationLyric.split('\n') + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + let result = timeExp.exec(line) + if (result) { + const text = line.replace(timeExp, '').trim() + if (text) { + const timeStr = RegExp.$1 + const timeArr = timeStr.split(':') + if (timeArr.length < 3) timeArr.unshift(0) + if (timeArr[2].indexOf('.') > -1) { + timeArr.push(...timeArr[2].split('.')) + timeArr.splice(2, 1) + } + linesMap[timeStr] = { + time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0), + text, + } + } + } + } + + const translationLines = this.translationLyric.split('\n') + for (let i = 0; i < translationLines.length; i++) { + const line = translationLines[i].trim() + let result = timeExp.exec(line) + if (result) { + const text = line.replace(timeExp, '').trim() + if (text) { + const timeStr = RegExp.$1 + const targetLine = linesMap[timeStr] + if (targetLine) targetLine.translation = text + } + } + } + this.lines = Object.values(linesMap) + this.lines.sort((a, b) => { + return a.time - b.time + }) + this.maxLine = this.lines.length - 1 + } + + _currentTime() { + return getNow() - this._performanceTime + this._performanceOffsetTime + } + + _findCurLineNum(curTime) { + const length = this.lines.length + for (let index = 0; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1 + return length - 1 + } + + _handleMaxLine() { + this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime()) + this.pause() + } + + _refresh() { + this.curLineNum++ + // console.log('curLineNum time', this.lines[this.curLineNum].time) + let curLine = this.lines[this.curLineNum] + let nextLine = this.lines[this.curLineNum + 1] + const currentTime = this._currentTime() + const driftTime = currentTime - curLine.time + + if (driftTime >= 0 || this.curLineNum === 0) { + if (this.curLineNum === this.maxLine) return this._handleMaxLine() + this.delay = nextLine.time - curLine.time - driftTime + if (this.delay > 0) { + if (!this.isOffseted && this.delay >= this.offset) { + this._performanceOffsetTime += this.offset + this.delay -= this.offset + this.isOffseted = true + } + timeoutTools.start(() => { + this._refresh() + }, this.delay) + this.onPlay(this.curLineNum, curLine.text, currentTime) + return + } + } + + this.curLineNum = this._findCurLineNum(currentTime) - 1 + this._refresh() + } + + play(curTime = 0) { + if (!this.lines.length) return + this.pause() + this.isPlay = true + + this._performanceOffsetTime = 0 + this._performanceTime = getNow() - curTime + if (this._performanceTime < 0) { + this._performanceOffsetTime = -this._performanceTime + this._performanceTime = 0 + } + + this.curLineNum = this._findCurLineNum(curTime) - 1 + + this._refresh() + } + + pause() { + if (!this.isPlay) return + this.isPlay = false + this.isOffseted = false + timeoutTools.clear() + if (this.curLineNum === this.maxLine) return + const currentTime = this._currentTime() + const curLineNum = this._findCurLineNum(currentTime) + if (this.curLineNum !== curLineNum) { + this.curLineNum = curLineNum + this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime) + } + } + + setLyric(lyric, translationLyric) { + // console.log(translationLyric) + if (this.isPlay) this.pause() + this.lyric = lyric + this.translationLyric = translationLyric + this._init() + } +} diff --git a/src/renderer/utils/lyric-font-player/utils.js b/src/renderer/utils/lyric-font-player/utils.js new file mode 100644 index 00000000..10ebd473 --- /dev/null +++ b/src/renderer/utils/lyric-font-player/utils.js @@ -0,0 +1,50 @@ + +const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date) + +exports.TimeoutTools = class TimeoutTools { + constructor() { + this.invokeTime = 0 + this.animationFrameId = null + this.timeoutId = null + this.callback = null + this.thresholdTime = 200 + } + + run() { + this.animationFrameId = window.requestAnimationFrame(() => { + this.animationFrameId = null + let diff = this.invokeTime - getNow() + // console.log('diff', diff) + if (diff > 0) { + if (diff < this.thresholdTime) return this.run() + return this.timeoutId = setTimeout(() => { + this.timeoutId = null + this.run() + }, diff - this.thresholdTime) + } + + // console.log('diff', diff) + this.callback(diff) + }) + } + + start(callback = () => {}, timeout = 0) { + // console.log(timeout) + this.callback = callback + this.invokeTime = getNow() + timeout + + this.run() + } + + clear() { + if (this.animationFrameId) { + window.cancelAnimationFrame(this.animationFrameId) + this.animationFrameId = null + } + if (this.timeoutId) { + window.clearTimeout(this.timeoutId) + this.timeoutId = null + } + } +} + diff --git a/src/renderer/utils/music/api-source-info.js b/src/renderer/utils/music/api-source-info.js index 74379655..e3ea06f7 100644 --- a/src/renderer/utils/music/api-source-info.js +++ b/src/renderer/utils/music/api-source-info.js @@ -11,7 +11,6 @@ module.exports = [ tx: ['128k'], wy: ['128k'], mg: ['128k'], - xm: ['128k'], // bd: ['128k'], }, }, diff --git a/src/renderer/utils/music/api-source.js b/src/renderer/utils/music/api-source.js index 3a598191..c94555cc 100644 --- a/src/renderer/utils/music/api-source.js +++ b/src/renderer/utils/music/api-source.js @@ -13,6 +13,7 @@ for (const api of apiSourceInfo) { const getAPI = source => apiList[`${window.globalObj.apiSource}_api_${source}`] const apis = source => { + if (/^user_api/.test(window.globalObj.apiSource)) return window.globalObj.userApi.apis[source] let api = getAPI(source) if (api) return api throw new Error('Api is not found') diff --git a/src/renderer/utils/music/bd/musicSearch.js b/src/renderer/utils/music/bd/musicSearch.js index d1b00903..c7574b81 100644 --- a/src/renderer/utils/music/bd/musicSearch.js +++ b/src/renderer/utils/music/bd/musicSearch.js @@ -11,9 +11,9 @@ export default { total: 0, page: 0, allPage: 1, - musicSearch(str, page) { + musicSearch(str, page, limit) { if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() - searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${this.limit}&type=0&data_source=0&use_cluster=1`) + searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${limit}&type=0&data_source=0&use_cluster=1`) return searchRequest.promise.then(({ body }) => body) }, handleResult(rawData) { @@ -66,9 +66,9 @@ export default { }, search(str, page = 1, { limit } = {}, retryNum = 0) { if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit + if (limit == null) limit = this.limit - return this.musicSearch(str, page).then(result => { + return this.musicSearch(str, page, limit).then(result => { if (!result || result.error_code !== 22000) return this.search(str, page, { limit }, retryNum) let list = this.handleResult(result.result.song_info.song_list) @@ -76,12 +76,12 @@ export default { this.total = result.result.song_info.total this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return Promise.resolve({ list, allPage: this.allPage, - limit: this.limit, + limit: limit, total: this.total, source: 'bd', }) diff --git a/src/renderer/utils/music/index.js b/src/renderer/utils/music/index.js index 81240ab8..fb97975f 100644 --- a/src/renderer/utils/music/index.js +++ b/src/renderer/utils/music/index.js @@ -50,66 +50,87 @@ const sources = { export default { ...sources, init() { + const tasks = [] for (let source of sources.sources) { let sm = sources[source.id] - sm && sm.init && sm.init() + sm && sm.init && tasks.push(sm.init()) } + return Promise.all(tasks) }, supportQuality, async findMusic(musicInfo) { const tasks = [] + const sortSingle = singer => singer.includes('、') ? singer.split('、').sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)).join('、') : singer + const sortMusic = (arr, callback) => { + const tempResult = [] + for (let i = arr.length - 1; i > -1; i--) { + const item = arr[i] + if (callback(item)) { + delete item.sortedSinger + tempResult.push(item) + arr.splice(i, 1) + } + } + tempResult.reverse() + return tempResult + } + const trimStr = str => typeof str == 'string' ? str.trim() : str + const sortedSinger = String(sortSingle(musicInfo.singer)).toLowerCase() + const musicName = trimStr(musicInfo.name) + const lowerCaseName = String(musicName).toLowerCase() + const lowerCaseAlbumName = String(musicInfo.albumName).toLowerCase() for (const source of sources.sources) { if (!sources[source.id].musicSearch || source.id === musicInfo.source || source.id === 'xm') continue - tasks.push(sources[source.id].musicSearch.search(`${musicInfo.name} ${musicInfo.singer || ''} ${musicInfo.albumName || ''}`.trim(), 1, { limit: 5 }).then(res => { + tasks.push(sources[source.id].musicSearch.search(`${musicName} ${musicInfo.singer || ''}`.trim(), 1, { limit: 10 }).then(res => { for (const item of res.list) { + item.sortedSinger = String(sortSingle(item.singer)).toLowerCase() + item.name = trimStr(item.name) + item.lowerCaseName = String(item.name).toLowerCase() + item.lowerCaseAlbumName = String(item.albumName).toLowerCase() + // console.log(lowerCaseName, item.lowerCaseName) if ( ( - item.singer === musicInfo.singer && - (item.name === musicInfo.name || item.interval === musicInfo.interval) + item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName ) || ( - item.interval === musicInfo.interval && item.name === musicInfo.name && - (item.singer.includes(musicInfo.singer) || musicInfo.singer.includes(item.singer)) + item.interval === musicInfo.interval && item.lowerCaseName === lowerCaseName && + (item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger)) + ) || + ( + item.lowerCaseName === lowerCaseName && item.lowerCaseAlbumName === lowerCaseAlbumName && + item.interval === musicInfo.interval ) ) { return item } } + for (const item of res.list) { + item.sortedSinger = String(sortSingle(item.singer)).toLowerCase() + item.name = trimStr(item.name) + item.lowerCaseName = String(item.name).toLowerCase() + item.lowerCaseAlbumName = String(item.albumName).toLowerCase() + // console.log(lowerCaseName, item.lowerCaseName) + if ( + item.sortedSinger === sortedSinger && item.interval === musicInfo.interval + ) { + return item + } + } return null }).catch(_ => null)) } const result = (await Promise.all(tasks)).filter(s => s) const newResult = [] if (result.length) { - for (let i = result.length - 1; i > -1; i--) { - const item = result[i] - if (item.singer === musicInfo.singer && item.name === musicInfo.name && item.interval === musicInfo.interval) { - newResult.push(item) - result.splice(i, 1) - } - } - for (let i = result.length - 1; i > -1; i--) { - const item = result[i] - if (item.singer === musicInfo.singer && item.interval === musicInfo.interval) { - newResult.push(item) - result.splice(i, 1) - } - } - for (let i = result.length - 1; i > -1; i--) { - const item = result[i] - if (item.name === musicInfo.name && item.singer === musicInfo.singer && item.albumName === musicInfo.albumName) { - newResult.push(item) - result.splice(i, 1) - } - } - for (let i = result.length - 1; i > -1; i--) { - const item = result[i] - if (item.singer === musicInfo.singer && item.name === musicInfo.name) { - newResult.push(item) - result.splice(i, 1) - } + newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName && item.interval === musicInfo.interval)) + newResult.push(...sortMusic(result, item => item.lowerCaseName === lowerCaseName && item.sortedSinger === sortedSinger && item.lowerCaseAlbumName === lowerCaseAlbumName)) + newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName)) + newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.interval === musicInfo.interval)) + for (const item of result) { + delete item.sortedSinger + delete item.lowerCaseName } newResult.push(...result) } diff --git a/src/renderer/utils/music/kg/comment.js b/src/renderer/utils/music/kg/comment.js index 6cb6fd52..cffd7aa8 100644 --- a/src/renderer/utils/music/kg/comment.js +++ b/src/renderer/utils/music/kg/comment.js @@ -31,9 +31,13 @@ export default { if (statusCode != 200 || body.err_code !== 0) throw new Error('获取热门评论失败') return { source: 'kg', comments: this.filterComment(body.weightList || []) } }, - async getReplyComment({ songmid }, replyId, page = 1, limit = 100) { + async getReplyComment({ songmid, audioId }, replyId, page = 1, limit = 100) { if (this._requestObj2) this._requestObj2.cancelHttp() + songmid = songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? audioId.split('_')[0] + : songmid + const _requestObj2 = httpFetch(`http://comment.service.kugou.com/index.php?r=commentsv2/getReplyWithLike&code=fc4be23b4e972707f36b8a828a93ba8a&p=${page}&pagesize=${limit}&ver=1.01&clientver=8373&kugouid=687373022&appid=1001&childrenid=${songmid}&tid=${replyId}`, { headers: { 'User-Agent': 'Android712-AndroidPhone-8983-18-0-COMMENT-wifi', diff --git a/src/renderer/utils/music/kg/index.js b/src/renderer/utils/music/kg/index.js index c0ccfbb2..4d4ac3da 100644 --- a/src/renderer/utils/music/kg/index.js +++ b/src/renderer/utils/music/kg/index.js @@ -26,7 +26,7 @@ const kg = { return pic.getPic(songInfo) }, getMusicDetailPageUrl(songInfo) { - return `https://www.kugou.com/song/#hash=${songInfo.hash}` + return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}` }, // getPic(songInfo) { // return apis('kg').getPic(songInfo) diff --git a/src/renderer/utils/music/kg/lyric.js b/src/renderer/utils/music/kg/lyric.js index 1e6bc9c3..2467ae3f 100644 --- a/src/renderer/utils/music/kg/lyric.js +++ b/src/renderer/utils/music/kg/lyric.js @@ -2,10 +2,13 @@ import { httpFetch } from '../../request' import { decodeLyric } from './util' import { decodeName } from '../..' +const headExp = /^.*\[id:\$\w+\]\n/ + const parseLyric = str => { - str = str.replace(/(?:<\d+,\d+,\d+>|\r)/g, '') - if (str.startsWith('\ufeff[id:$00000000]')) str = str.replace('\ufeff[id:$00000000]\n', '') + str = str.replace(/\r/g, '') + if (headExp.test(str)) str = str.replace(headExp, '') let trans = str.match(/\[language:([\w=\\/+]+)\]/) + let lyric let tlyric if (trans) { str = str.replace(/\[language:[\w=\\/+]+\]\n/, '') @@ -18,7 +21,7 @@ const parseLyric = str => { } } let i = 0 - let lyric = str.replace(/\[((\d+),\d+)\].*/g, str => { + let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => { let result = str.match(/\[((\d+),\d+)\].*/) let time = parseInt(result[2]) let ms = time % 1000 @@ -31,11 +34,14 @@ const parseLyric = str => { return str.replace(result[1], time) }) tlyric = tlyric ? tlyric.join('\n') : '' - lyric = decodeName(lyric) + lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>') + lxlyric = decodeName(lxlyric) + lyric = lxlyric.replace(/<\d+,\d+>/g, '') tlyric = decodeName(tlyric) return { lyric, tlyric, + lxlyric, } } @@ -119,7 +125,7 @@ export default { let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval)) requestObj.promise = requestObj.promise.then(result => { - if (!result) return { lyric: '', tlyric: '' } + if (!result) return { lyric: null, tlyric: null, lxlyric: null } let requestObj2 = this.getLyricDownload(result.id, result.accessKey) diff --git a/src/renderer/utils/music/kg/musicSearch.js b/src/renderer/utils/music/kg/musicSearch.js index 364229f1..e5909076 100644 --- a/src/renderer/utils/music/kg/musicSearch.js +++ b/src/renderer/utils/music/kg/musicSearch.js @@ -11,69 +11,79 @@ export default { total: 0, page: 0, allPage: 1, - musicSearch(str, page) { + musicSearch(str, page, limit) { if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() - searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`) + searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`) return searchRequest.promise.then(({ body }) => body) }, + filterData(rawData) { + const types = [] + const _types = {} + if (rawData.filesize !== 0) { + let size = sizeFormate(rawData.filesize) + types.push({ type: '128k', size, hash: rawData.hash }) + _types['128k'] = { + size, + hash: rawData.hash, + } + } + if (rawData['320filesize'] !== 0) { + let size = sizeFormate(rawData['320filesize']) + types.push({ type: '320k', size, hash: rawData['320hash'] }) + _types['320k'] = { + size, + hash: rawData['320hash'], + } + } + if (rawData.sqfilesize !== 0) { + let size = sizeFormate(rawData.sqfilesize) + types.push({ type: 'flac', size, hash: rawData.sqhash }) + _types.flac = { + size, + hash: rawData.sqhash, + } + } + return { + singer: decodeName(rawData.singername), + name: decodeName(rawData.songname), + albumName: decodeName(rawData.album_name), + albumId: rawData.album_id, + songmid: rawData.audio_id, + source: 'kg', + interval: formatPlayTime(rawData.duration), + _interval: rawData.duration, + img: null, + lrc: null, + otherSource: null, + hash: rawData.hash, + types, + _types, + typeUrl: {}, + } + }, handleResult(rawData) { // console.log(rawData) let ids = new Set() const list = [] rawData.forEach(item => { - if (ids.has(item.audio_id)) return - ids.add(item.audio_id) - const types = [] - const _types = {} - if (item.filesize !== 0) { - let size = sizeFormate(item.filesize) - types.push({ type: '128k', size, hash: item.hash }) - _types['128k'] = { - size, - hash: item.hash, - } + const key = item.audio_id + item.hash + if (ids.has(key)) return + ids.add(key) + list.push(this.filterData(item)) + for (const childItem of item.group) { + const key = item.audio_id + item.hash + if (ids.has(key)) return + ids.add(key) + list.push(this.filterData(childItem)) } - if (item['320filesize'] !== 0) { - let size = sizeFormate(item['320filesize']) - types.push({ type: '320k', size, hash: item['320hash'] }) - _types['320k'] = { - size, - hash: item['320hash'], - } - } - if (item.sqfilesize !== 0) { - let size = sizeFormate(item.sqfilesize) - types.push({ type: 'flac', size, hash: item.sqhash }) - _types.flac = { - size, - hash: item.sqhash, - } - } - list.push({ - singer: decodeName(item.singername), - name: decodeName(item.songname), - albumName: decodeName(item.album_name), - albumId: item.album_id, - songmid: item.audio_id, - source: 'kg', - interval: formatPlayTime(item.duration), - _interval: item.duration, - img: null, - lrc: null, - otherSource: null, - hash: item.hash, - types, - _types, - typeUrl: {}, - }) }) return list }, search(str, page = 1, { limit } = {}, retryNum = 0) { if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit + if (limit == null) limit = this.limit // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page).then(result => { + return this.musicSearch(str, page, limit).then(result => { if (!result || result.errcode !== 0) return this.search(str, page, { limit }, retryNum) let list = this.handleResult(result.data.info) @@ -81,12 +91,12 @@ export default { this.total = result.data.total this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return Promise.resolve({ list, allPage: this.allPage, - limit: this.limit, + limit, total: this.total, source: 'kg', }) diff --git a/src/renderer/utils/music/kg/pic.js b/src/renderer/utils/music/kg/pic.js index feceef68..5d9c262d 100644 --- a/src/renderer/utils/music/kg/pic.js +++ b/src/renderer/utils/music/kg/pic.js @@ -20,7 +20,10 @@ export default { relate: 1, resource: [ { - album_audio_id: songInfo.songmid, + album_audio_id: + songInfo.songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? songInfo.audioId.split('_')[0] + : songInfo.songmid, album_id: songInfo.albumId, hash: songInfo.hash, id: 0, @@ -37,7 +40,9 @@ export default { requestObj.promise = requestObj.promise.then(({ body }) => { if (body.error_code !== 0) return Promise.reject('图片获取失败') let info = body.data[0].info - return info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image + const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image + if (!img) return Promise.reject('Pic get failed') + return img }) return requestObj }, diff --git a/src/renderer/utils/music/kw/index.js b/src/renderer/utils/music/kw/index.js index 1039305e..d8c4dc21 100644 --- a/src/renderer/utils/music/kw/index.js +++ b/src/renderer/utils/music/kw/index.js @@ -106,7 +106,7 @@ const kw = { }, init() { - getToken() + return getToken() }, } diff --git a/src/renderer/utils/music/kw/musicSearch.js b/src/renderer/utils/music/kw/musicSearch.js index 4f201de9..8931d338 100644 --- a/src/renderer/utils/music/kw/musicSearch.js +++ b/src/renderer/utils/music/kw/musicSearch.js @@ -16,14 +16,14 @@ export default { page: 0, allPage: 1, // cancelFn: null, - musicSearch(str, page) { + musicSearch(str, page, limit) { if (this._musicSearchRequestObj != null) { cancelHttp(this._musicSearchRequestObj) this._musicSearchPromiseCancelFn(new Error('取消http请求')) } return new Promise((resolve, reject) => { this._musicSearchPromiseCancelFn = reject - this._musicSearchRequestObj = httpGet(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${this.limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`, (err, resp, body) => { + this._musicSearchRequestObj = httpGet(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`, (err, resp, body) => { this._musicSearchRequestObj = null this._musicSearchPromiseCancelFn = null if (err) { @@ -125,9 +125,9 @@ export default { }, search(str, page = 1, { limit } = {}, retryNum = 0) { if (retryNum > 2) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit + if (limit == null) limit = this.limit // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page).then(result => { + return this.musicSearch(str, page, limit).then(result => { // console.log(result) if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, { limit }, ++retryNum) let list = this.handleResult(result.abslist) @@ -136,13 +136,13 @@ export default { this.total = parseInt(result.TOTAL) this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return Promise.resolve({ list, allPage: this.allPage, total: this.total, - limit: this.limit, + limit, source: 'kw', }) }) diff --git a/src/renderer/utils/music/mg/comment.js b/src/renderer/utils/music/mg/comment.js index a6c8a134..c3fab1a4 100644 --- a/src/renderer/utils/music/mg/comment.js +++ b/src/renderer/utils/music/mg/comment.js @@ -16,7 +16,7 @@ export default { if (!info) info = list.find(s => s.songId == songmid) return info ? info.songId : null }, - async getComment(musicInfo, page = 1, limit = 20) { + async getComment(musicInfo, page = 1, limit = 10) { if (this._requestObj) this._requestObj.cancelHttp() if (!musicInfo.songId) { let id = await this.getSongId(musicInfo) @@ -35,7 +35,7 @@ export default { if (statusCode != 200 || body.returnCode !== '000000') throw new Error('获取评论失败') return { source: 'mg', comments: this.filterComment(body.data.items), total: body.data.itemTotal, page, limit, maxPage: Math.ceil(body.data.itemTotal / limit) || 1 } }, - async getHotComment(musicInfo, page = 1, limit = 100) { + async getHotComment(musicInfo, page = 1, limit = 5) { if (this._requestObj2) this._requestObj2.cancelHttp() if (!musicInfo.songId) { @@ -55,7 +55,7 @@ export default { if (statusCode != 200 || body.returnCode !== '000000') throw new Error('获取热门评论失败') return { source: 'mg', comments: this.filterComment(body.data.items) } }, - async getReplyComment(musicInfo, replyId, page = 1, limit = 100) { + async getReplyComment(musicInfo, replyId, page = 1, limit = 10) { if (this._requestObj2) this._requestObj2.cancelHttp() const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listCommentsById?commentId=${replyId}&pageSize=${limit}&pageNo=${page}`, { @@ -75,7 +75,7 @@ export default { time: item.createTime, timeStr: dateFormat2(new Date(item.createTime).getTime()), userName: item.author.name, - avatar: item.author.avatar, + avatar: item.author.avatar && item.author.avatar.startsWith('//') ? `http:${item.author.avatar}` : item.author.avatar, userId: item.author.id, likedCount: item.praiseCount, replyNum: item.replyTotal, diff --git a/src/renderer/utils/music/mg/musicSearch.js b/src/renderer/utils/music/mg/musicSearch.js index 8ed3d238..183a3d56 100644 --- a/src/renderer/utils/music/mg/musicSearch.js +++ b/src/renderer/utils/music/mg/musicSearch.js @@ -11,9 +11,9 @@ export default { total: 0, page: 0, allPage: 1, - musicSearch(str, page) { + musicSearch(str, page, limit) { if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() - searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${this.limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { + searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { headers: { sign: 'c3b7ae985e2206e97f1b2de8f88691e2', timestamp: 1578225871982, @@ -25,7 +25,7 @@ export default { 'User-Agent': 'okhttp/3.9.1', }, }) - // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${this.limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) + // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) return searchRequest.promise.then(({ body }) => body) }, getSinger(singers) { @@ -99,9 +99,9 @@ export default { }, search(str, page = 1, { limit } = {}, retryNum = 0) { if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit + if (limit == null) limit = this.limit // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page).then(result => { + return this.musicSearch(str, page, limit).then(result => { // console.log(result) if (!result || result.code !== '000000') return Promise.reject(new Error(result ? result.info : '搜索失败')) const songResultData = result.songResultData || { resultList: [], totalCount: 0 } @@ -111,12 +111,12 @@ export default { this.total = parseInt(songResultData.totalCount) this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return Promise.resolve({ list, allPage: this.allPage, - limit: this.limit, + limit, total: this.total, source: 'mg', }) diff --git a/src/renderer/utils/music/mg/songList.js b/src/renderer/utils/music/mg/songList.js index 5eaf869d..11b3a18c 100644 --- a/src/renderer/utils/music/mg/songList.js +++ b/src/renderer/utils/music/mg/songList.js @@ -5,9 +5,11 @@ export default { _requestObj_tags: null, _requestObj_list: null, _requestObj_listDetail: null, + _requestObj_listDetailInfo: null, limit_list: 10, limit_song: 10000, successCode: '000000', + cachedDetailInfo: {}, sortList: [ { name: '推荐', @@ -48,10 +50,12 @@ export default { return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}` }, defaultHeaders: { - language: 'Chinese', - ua: 'Android_migu', - mode: 'android', - version: '6.8.5', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + Referer: 'https://m.music.migu.cn/', + // language: 'Chinese', + // ua: 'Android_migu', + // mode: 'android', + // version: '6.8.5', }, /** @@ -64,11 +68,14 @@ export default { return num }, - getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐 + getListDetailList(id, page, tryNum = 0) { if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp() if (tryNum > 2) return Promise.reject(new Error('try max num')) + // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 - if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + if (/playlist\/index\.html\?/.test(id)) { + id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') + } else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') this._requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), { headers: this.defaultHeaders }) return this._requestObj_listDetail.promise.then(({ body }) => { @@ -81,16 +88,47 @@ export default { limit: this.limit_song, total: body.totalCount, source: 'mg', - // info: { - // // name: body.result.info.list_title, - // // img: body.result.info.list_pic, - // // desc: body.result.info.list_desc, - // // author: body.result.info.userinfo.username, - // // play_count: this.formatPlayCount(body.result.listen_num), - // }, } }) }, + + getListDetailInfo(id, tryNum = 0) { + if (this._requestObj_listDetailInfo) this._requestObj_listDetailInfo.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + if (this.cachedDetailInfo[id]) return Promise.resolve(this.cachedDetailInfo[id]) + this._requestObj_listDetailInfo = httpFetch(`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`, { + headers: this.defaultHeaders, + }) + return this._requestObj_listDetailInfo.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum) + // console.log(JSON.stringify(body)) + // console.log(body) + const cachedDetailInfo = this.cachedDetailInfo[id] = { + name: body.data.title, + img: body.data.imgItem.img, + desc: body.data.summary, + author: body.data.ownerName, + play_count: this.formatPlayCount(body.data.opNumItem.playNum), + } + return cachedDetailInfo + }) + }, + + getListDetail(id, page) { // 获取歌曲列表内的音乐 + // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 + if (/playlist\/index\.html\?/.test(id)) { + id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') + } else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + return Promise.all([ + this.getListDetailList(id, page), + this.getListDetailInfo(id), + ]).then(([listData, info]) => { + listData.info = info + return listData + }) + }, filterListDetail(rawList) { // console.log(rawList) let ids = new Set() @@ -155,6 +193,7 @@ export default { if (this._requestObj_list) this._requestObj_list.cancelHttp() if (tryNum > 2) return Promise.reject(new Error('try max num')) this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), { + headers: this.defaultHeaders, // headers: { // sign: 'c3b7ae985e2206e97f1b2de8f88691e2', // timestamp: 1578225871982, @@ -186,6 +225,7 @@ export default { // }) // }) return this._requestObj_list.promise.then(({ body }) => { + // console.log(body) if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) return { list: this.filterList(body.retMsg.playlist), diff --git a/src/renderer/utils/music/tx/musicSearch.js b/src/renderer/utils/music/tx/musicSearch.js index c2d21f92..db638542 100644 --- a/src/renderer/utils/music/tx/musicSearch.js +++ b/src/renderer/utils/music/tx/musicSearch.js @@ -12,13 +12,13 @@ export default { page: 0, allPage: 1, successCode: 0, - musicSearch(str, page, retryNum = 0) { + musicSearch(str, page, limit, retryNum = 0) { if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() 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=${this.limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`) + 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`) // 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 }) => { - if (body.code !== this.successCode) return this.musicSearch(str, page, ++retryNum) + if (body.code !== this.successCode) return this.musicSearch(str, page, limit, ++retryNum) return body.data }) }, @@ -86,19 +86,19 @@ export default { }) }, search(str, page = 1, { limit } = {}) { - if (limit != null) this.limit = limit + if (limit == null) limit = this.limit // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page).then(({ song }) => { + return this.musicSearch(str, page, limit).then(({ song }) => { let list = this.handleResult(song.list) this.total = song.totalnum this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return Promise.resolve({ list, allPage: this.allPage, - limit: this.limit, + limit, total: this.total, source: 'tx', }) diff --git a/src/renderer/utils/music/wy/index.js b/src/renderer/utils/music/wy/index.js index 69830372..73122433 100644 --- a/src/renderer/utils/music/wy/index.js +++ b/src/renderer/utils/music/wy/index.js @@ -20,7 +20,9 @@ const wy = { return getLyric(songInfo.songmid) }, getPic(songInfo) { - return getMusicInfo(songInfo.songmid).then(info => info.al.picUrl) + const requestObj = getMusicInfo(songInfo.songmid) + requestObj.promise = requestObj.promise.then(info => info.al.picUrl) + return requestObj }, getMusicDetailPageUrl(songInfo) { return `https://music.163.com/#/song?id=${songInfo.songmid}` diff --git a/src/renderer/utils/music/wy/lyric.js b/src/renderer/utils/music/wy/lyric.js index 6ae95d34..301a2bb4 100644 --- a/src/renderer/utils/music/wy/lyric.js +++ b/src/renderer/utils/music/wy/lyric.js @@ -1,5 +1,38 @@ import { httpFetch } from '../../request' import { linuxapi } from './utils/crypto' +// import { decodeName } from '../..' + +// const parseLyric = (str, lrc) => { +// if (!str) return '' + +// str = str.replace(/\r/g, '') + +// let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => { +// let result = str.match(/\[((\d+),\d+)\].*/) +// let time = parseInt(result[2]) +// let ms = time % 1000 +// time /= 1000 +// let m = parseInt(time / 60).toString().padStart(2, '0') +// time %= 60 +// let s = parseInt(time).toString().padStart(2, '0') +// time = `${m}:${s}.${ms}` +// str = str.replace(result[1], time) + +// let startTime = 0 +// str = str.replace(/\(0,1\) /g, ' ').replace(/\(\d+,\d+\)/g, time => { +// const [start, end] = time.replace(/^\((\d+,\d+)\)$/, '$1').split(',') + +// time = `<${parseInt(startTime + parseInt(start))},${end}>` +// startTime = parseInt(startTime + parseInt(end)) +// return time +// }) + +// return str +// }) + +// lxlyric = decodeName(lxlyric) +// return lxlyric.trim() +// } export default songmid => { const requestObj = httpFetch('https://music.163.com/api/linux/forward', { @@ -21,6 +54,7 @@ export default songmid => { return { lyric: body.lrc.lyric, tlyric: body.tlyric.lyric, + // lxlyric: parseLyric(body.klyric.lyric), } }) return requestObj diff --git a/src/renderer/utils/music/wy/musicSearch.js b/src/renderer/utils/music/wy/musicSearch.js index d7a40fd9..4fdf8558 100644 --- a/src/renderer/utils/music/wy/musicSearch.js +++ b/src/renderer/utils/music/wy/musicSearch.js @@ -9,7 +9,7 @@ export default { total: 0, page: 0, allPage: 1, - musicSearch(str, page) { + musicSearch(str, page, limit) { if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() searchRequest = httpFetch('https://music.163.com/weapi/search/get', { method: 'post', @@ -20,8 +20,8 @@ export default { form: weapi({ s: str, type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频 - limit: this.limit, - offset: this.limit * (page - 1), + limit, + offset: limit * (page - 1), }), }) return searchRequest.promise.then(({ body }) => @@ -29,13 +29,13 @@ export default { ? musicDetailApi.getList(body.result.songs.map(s => s.id)).then(({ list }) => { this.total = body.result.songCount || 0 this.page = page - this.allPage = Math.ceil(this.total / this.limit) + this.allPage = Math.ceil(this.total / limit) return { code: 200, data: { list, allPage: this.allPage, - limit: this.limit, + limit, total: this.total, source: 'wy', }, @@ -103,8 +103,8 @@ export default { }, */ search(str, page = 1, { limit } = {}, retryNum = 0) { if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit - return this.musicSearch(str, page).then(result => { + if (limit == null) limit = this.limit + return this.musicSearch(str, page, limit).then(result => { // console.log(result) if (!result || result.code !== 200) return this.search(str, page, { limit }, retryNum) // let list = this.handleResult(result.result.songs || []) diff --git a/src/renderer/utils/music/xm.js b/src/renderer/utils/music/xm.js new file mode 100644 index 00000000..5142a0db --- /dev/null +++ b/src/renderer/utils/music/xm.js @@ -0,0 +1,57 @@ +// import { apis } from '../api-source' +// import leaderboard from './leaderboard' +// import songList from './songList' +// import musicSearch from './musicSearch' +// import pic from './pic' +// import lyric from './lyric' +// import hotSearch from './hotSearch' +// import comment from './comment' +// import musicInfo from './musicInfo' +// import { closeVerifyModal } from './util' + +const xm = { + // songList, + // musicSearch, + // leaderboard, + // hotSearch, + // closeVerifyModal, + comment: { + getComment() { + return Promise.reject(new Error('fail')) + }, + getHotComment() { + return Promise.reject(new Error('fail')) + }, + }, + getMusicUrl(songInfo, type) { + return { + promise: Promise.reject(new Error('fail')), + } + // return apis('xm').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return { + promise: Promise.reject(new Error('fail')), + } + // return lyric.getLyric(songInfo) + }, + getPic(songInfo) { + return { + promise: Promise.reject(new Error('fail')), + } + // return pic.getPic(songInfo) + }, + // getMusicDetailPageUrl(songInfo) { + // if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}` + + // musicInfo.getMusicInfo(songInfo).then(({ data }) => { + // songInfo.songStringId = data.songStringId + // }) + // return `https://www.xiami.com/song/${songInfo.songmid}` + // }, + // init() { + // getToken() + // }, +} + +export default xm diff --git a/src/renderer/utils/music/xm/api-test.js b/src/renderer/utils/music/xm/api-test.js deleted file mode 100644 index c9881164..00000000 --- a/src/renderer/utils/music/xm/api-test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { httpFetch } from '../../request' -import { requestMsg } from '../../message' -import { headers, timeout } from '../options' - -const api_test = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusic.tk/url/xm/${songInfo.songmid}/${type}`, { - method: 'get', - timeout, - headers, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ body }) => { - return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) - }) - return requestObj - }, -} - -export default api_test diff --git a/src/renderer/utils/music/xm/comment.js b/src/renderer/utils/music/xm/comment.js deleted file mode 100644 index d8432cbe..00000000 --- a/src/renderer/utils/music/xm/comment.js +++ /dev/null @@ -1,47 +0,0 @@ -import { xmRequest } from './util' -import { dateFormat2 } from '../../' - -export default { - _requestObj: null, - _requestObj2: null, - async getComment({ songmid }, page = 1, limit = 20) { - if (this._requestObj) this._requestObj.cancelHttp() - - const _requestObj = xmRequest('/api/comment/getCommentList', { objectId: songmid, objectType: 'song', pagingVO: { page, pageSize: limit } }) - const { body, statusCode } = await _requestObj.promise - // console.log(body) - if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取评论失败') - return { source: 'xm', comments: this.filterComment(body.result.data.commentList), total: body.result.data.pagingVO.count, page, limit, maxPage: Math.ceil(body.result.data.pagingVO.count / limit) || 1 } - }, - async getHotComment({ songmid }, page = 1, limit = 100) { - if (this._requestObj2) this._requestObj2.cancelHttp() - if (!songmid) throw new Error('获取失败') - const _requestObj2 = xmRequest('/api/comment/getHotCommentList', { objectId: songmid, objectType: 'song', pagingVO: { page, pageSize: limit } }) - const { body, statusCode } = await _requestObj2.promise - // console.log(body) - if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取热门评论失败') - return { source: 'xm', comments: this.filterComment(body.result.data.hotList) } - }, - filterComment(rawList) { - return rawList.map(item => ({ - id: item.commentId, - text: item.message.split('\n').filter(t => !!t), - time: item.gmtCreate, - timeStr: dateFormat2(item.gmtCreate), - userName: item.nickName, - avatar: item.avatar, - userId: item.userId, - likedCount: item.likes, - reply: item.replyData ? item.replyData.map(c => ({ - id: c.commentId, - text: c.message.split('\n').filter(t => !!t), - time: c.gmtCreate, - timeStr: dateFormat2(c.gmtCreate), - userName: c.nickName, - avatar: c.avatar, - userId: c.userId, - likedCount: c.likes, - })) : [], - })) - }, -} diff --git a/src/renderer/utils/music/xm/hotSearch.js b/src/renderer/utils/music/xm/hotSearch.js deleted file mode 100644 index 3dbd67c0..00000000 --- a/src/renderer/utils/music/xm/hotSearch.js +++ /dev/null @@ -1,19 +0,0 @@ -import { xmRequest } from './util' - -export default { - _requestObj: null, - async getList(retryNum = 0) { - if (this._requestObj) this._requestObj.cancelHttp() - if (retryNum > 2) return Promise.reject(new Error('try max num')) - - const _requestObj = xmRequest('/api/search/getHotSearchWords') - const { body, statusCode } = await _requestObj.promise - // console.log(body) - if (statusCode != 200 || body.code !== 'SUCCESS') return this.getList(++retryNum) - // // console.log(body, statusCode) - return { source: 'xm', list: this.filterList(body.result.data.hotWords) } - }, - filterList(rawList) { - return rawList.map(item => item.word) - }, -} diff --git a/src/renderer/utils/music/xm/index.js b/src/renderer/utils/music/xm/index.js deleted file mode 100644 index e586bf25..00000000 --- a/src/renderer/utils/music/xm/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import { apis } from '../api-source' -import leaderboard from './leaderboard' -import songList from './songList' -import musicSearch from './musicSearch' -// import pic from './pic' -import lyric from './lyric' -import hotSearch from './hotSearch' -import comment from './comment' -import musicInfo from './musicInfo' -import { closeVerifyModal } from './util' - -const xm = { - songList, - musicSearch, - leaderboard, - hotSearch, - closeVerifyModal, - comment, - getMusicUrl(songInfo, type) { - return apis('xm').getMusicUrl(songInfo, type) - }, - getLyric(songInfo) { - return lyric.getLyric(songInfo) - }, - getPic(songInfo) { - return Promise.reject(new Error('fail')) - // return pic.getPic(songInfo) - }, - getMusicDetailPageUrl(songInfo) { - if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}` - - musicInfo.getMusicInfo(songInfo).then(({ data }) => { - songInfo.songStringId = data.songStringId - }) - return `https://www.xiami.com/song/${songInfo.songmid}` - }, - // init() { - // getToken() - // }, -} - -export default xm diff --git a/src/renderer/utils/music/xm/leaderboard.js b/src/renderer/utils/music/xm/leaderboard.js deleted file mode 100644 index 5831e56b..00000000 --- a/src/renderer/utils/music/xm/leaderboard.js +++ /dev/null @@ -1,211 +0,0 @@ -import { xmRequest } from './util' -import { formatPlayTime, sizeFormate } from '../../index' -// import jshtmlencode from 'js-htmlencode' - -let boardList = [{ id: 'xm__102', name: '新歌榜', bangid: '102' }, { id: 'xm__103', name: '热歌榜', bangid: '103' }, { id: 'xm__104', name: '原创榜', bangid: '104' }, { id: 'xm__306', name: 'K歌榜', bangid: '306' }, { id: 'xm__332', name: '抖音热歌榜', bangid: '332' }, { id: 'xm__305', name: '歌单收录榜', bangid: '305' }, { id: 'xm__327', name: '趴间热歌榜', bangid: '327' }, { id: 'xm__324', name: '影视原声榜', bangid: '324' }, { id: 'xm__204', name: '美国Billboard单曲榜', bangid: '204' }, { id: 'xm__206', name: '韩国MNET音乐排行榜', bangid: '206' }, { id: 'xm__201', name: 'Hito 中文排行榜', bangid: '201' }, { id: 'xm__203', name: '英国UK单曲榜', bangid: '203' }, { id: 'xm__205', name: 'oricon公信单曲榜', bangid: '205' }, { id: 'xm__328', name: '美国iTunes榜', bangid: '328' }, { id: 'xm__329', name: 'Beatport电音榜', bangid: '329' }, { id: 'xm__330', name: '香港商业电台榜', bangid: '330' }] - -export default { - limit: 200, - list: [ - { - id: 'xmrgb', - name: '热歌榜', - bangid: '103', - }, - { - id: 'xmxgb', - name: '新歌榜', - bangid: '102', - }, - { - id: 'xmrcb', - name: '原创榜', - bangid: '104', - }, - { - id: 'xmdyb', - name: '抖音榜', - bangid: '332', - }, - { - id: 'xmkgb', - name: 'K歌榜', - bangid: '306', - }, - { - id: 'xmfxb', - name: '分享榜', - bangid: '307', - }, - { - id: 'xmrdtlb', - name: '讨论榜', - bangid: '331', - }, - { - id: 'xmgdslb', - name: '歌单榜', - bangid: '305', - }, - { - id: 'xmpjrgb', - name: '趴间榜', - bangid: '327', - }, - { - id: 'xmysysb', - name: '影视榜', - bangid: '324', - }, - ], - requestBoardsObj: null, - requestObj: null, - getBoardsData() { - if (this.requestBoardsObj) this.requestBoardsObj.cancelHttp() - this.requestBoardsObj = xmRequest('/api/billboard/getBillboards') - return this.requestBoardsObj.promise - }, - getData(id) { - if (this.requestObj) this.requestObj.cancelHttp() - this.requestObj = xmRequest('/api/billboard/getBillboardDetail', { billboardId: id }) - return this.requestObj.promise - }, - getSinger(singers) { - let arr = [] - singers.forEach(singer => { - arr.push(singer.artistName) - }) - return arr.join('、') - }, - filterData(rawList) { - // console.log(rawList) - let ids = new Set() - const list = [] - rawList.forEach(songData => { - if (!songData) return - if (ids.has(songData.songId)) return - ids.add(songData.songId) - - const types = [] - const _types = {} - let size = null - for (const item of songData.purviewRoleVOs) { - if (!item.filesize) continue - size = sizeFormate(item.filesize) - switch (item.quality) { - case 's': - types.push({ type: 'wav', size }) - _types.wav = { - size, - } - break - case 'h': - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'l': - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - } - } - types.reverse() - - list.push({ - singer: this.getSinger(songData.singerVOs), - name: songData.songName, - albumName: songData.albumName, - albumId: songData.albumId, - source: 'xm', - interval: formatPlayTime(parseInt(songData.length / 1000)), - songmid: songData.songId, - img: songData.albumLogo || songData.albumLogoS, - songStringId: songData.songStringId, - lrc: null, - lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, - otherSource: null, - types, - _types, - typeUrl: {}, - }) - }) - - return list - }, - filterBoardsData(rawList) { - // console.log(rawList) - let list = [] - if (rawList.xiamiBillboards) { - for (const board of rawList.xiamiBillboards) { - if (board.itemType != 1) continue - list.push({ - id: 'xm__' + board.billboardId, - name: board.name, - bangid: String(board.billboardId), - }) - } - } - if (rawList.spBillboards) { - for (const board of rawList.spBillboards) { - if (board.itemType != 1) continue - list.push({ - id: 'xm__' + board.billboardId, - name: board.name, - bangid: String(board.billboardId), - }) - } - } - if (rawList.globalBillboards) { - for (const board of rawList.globalBillboards) { - if (board.itemType != 1) continue - list.push({ - id: 'xm__' + board.billboardId, - name: board.name, - bangid: String(board.billboardId), - }) - } - } - return list - }, - async getBoards(retryNum = 0) { - // if (++retryNum > 3) return Promise.reject(new Error('try max num')) - // let response - // try { - // response = await this.getBoardsData() - // } catch (error) { - // return this.getBoards(retryNum) - // } - // if (response.statusCode !== 200 || response.body.code !== 'SUCCESS') return this.getBoards(retryNum) - // const list = this.filterBoardsData(response.body.result.data) - // this.list = list - // return { - // list, - // source: 'xm', - // } - this.list = boardList - return { - list: boardList, - source: 'xm', - } - }, - getList(bangid, page, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - return this.getData(bangid).then(({ statusCode, body }) => { - if (statusCode !== 200 || body.code !== 'SUCCESS') return this.getList(bangid, page, retryNum) - // console.log(body) - const list = this.filterData(body.result.data.billboard.songs) - - return { - total: parseInt(body.result.data.billboard.attributeMap.item_size), - list, - limit: this.limit, - page, - source: 'xm', - } - }) - }, -} diff --git a/src/renderer/utils/music/xm/lyric.js b/src/renderer/utils/music/xm/lyric.js deleted file mode 100644 index d1e85d79..00000000 --- a/src/renderer/utils/music/xm/lyric.js +++ /dev/null @@ -1,107 +0,0 @@ -import { httpGet, httpFetch } from '../../request' -import { xmRequest } from './util' - -const parseLyric = str => { - str = str.replace(/(?:<\d+>|\r)/g, '') - let tlyric = [] - let lyric = str.replace(/\[[\d:.]+\].*?\n\[x-trans\].*/g, s => { - // console.log(s) - let [lrc, tlrc] = s.split('\n') - tlrc = tlrc.replace('[x-trans]', lrc.replace(/^(\[[\d:.]+\]).*$/, '$1')) - tlyric.push(tlrc) - return lrc - }) - tlyric = tlyric.join('\n') - return { - lyric, - tlyric, - } -} - -export default { - failTime: 0, - expireTime: 60 * 1000 * 1000, - getLyricFile_1(url, retryNum = 0) { - if (retryNum > 5) return Promise.reject('歌词获取失败') - let requestObj = httpFetch(url) - requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { - if (statusCode !== 200) { - let tryRequestObj = this.getLyric(url, ++retryNum) - requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) - return tryRequestObj.promise - } - return url.endsWith('.xtrc') ? parseLyric(body) : { - lyric: body, - tlyric: '', - } - }) - return requestObj - }, - getLyricFile_2(url, retryNum = 0) { - if (retryNum > 5) return Promise.reject('歌词获取失败') - return new Promise((resolve, reject) => { - httpGet(url, { - 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', - referer: 'https://www.xiami.com', - }, - }, function(err, resp, body) { - if (err || resp.statusCode !== 200) return this.getLyricFile(url, ++retryNum).then(resolve).catch(reject) - return resolve(url.endsWith('.xtrc') ? parseLyric(body) : { - lyric: body, - tlyric: '', - }) - }) - }) - }, - getLyricUrl_1(songInfo, retryNum = 0) { - if (retryNum > 2) return Promise.reject('歌词获取失败') - let requestObj = xmRequest('/api/lyric/getSongLyrics', { songId: songInfo.songmid }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode !== 200) { - let tryRequestObj = this.getLyricUrl_1(songInfo, ++retryNum) - requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) - return tryRequestObj.promise - } - if (body.code !== 'SUCCESS') { - this.failTime = Date.now() - let tryRequestObj = this.getLyricUrl_2(songInfo) - requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) - return tryRequestObj.promise - } - if (!body.result.data.lyrics.length) return Promise.reject(new Error('未找到歌词')) - let lrc = body.result.data.lyrics.find(lyric => /\.(trc|lrc)$/.test(lyric.lyricUrl)) - return lrc - ? lrc.lyricUrl.endsWith('.trc') - ? parseLyric(lrc.content) - : { lyric: lrc.content, tlyric: '' } - : Promise.reject(new Error('未找到歌词')) - }) - return requestObj - }, - getLyricUrl_2(songInfo, retryNum = 0) { - if (retryNum > 2) return Promise.reject('歌词获取失败') - // https://github.com/listen1/listen1_chrome_extension/blob/2587e627d23a85e490628acc0b3c9b534bc8323d/js/provider/xiami.js#L149 - let requestObj = httpFetch(`https://emumo.xiami.com/song/playlist/id/${songInfo.songmid}/object_name/default/object_id/0/cat/json`, { - 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', - referer: 'https://www.xiami.com', - }, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode !== 200 || !body.status) { - let tryRequestObj = this.getLyricUrl_2(songInfo, ++retryNum) - requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) - return tryRequestObj.promise - } - let url = body.data.trackList[0].lyric_url - if (!url) return Promise.reject(new Error('未找到歌词')) - return this.getLyricFile_2(/^http:/.test(url) ? url : ('http:' + url)) - }) - return requestObj - }, - getLyric(songInfo) { - if (songInfo.lrcUrl && /\.(xtrc|lrc)$/.test(songInfo.lrcUrl)) return this.getLyricFile_1(songInfo.lrcUrl) - return Date.now() - this.failTime > this.expireTime ? this.getLyricUrl_1(songInfo) : this.getLyricUrl_2(songInfo) - }, -} diff --git a/src/renderer/utils/music/xm/musicInfo.js b/src/renderer/utils/music/xm/musicInfo.js deleted file mode 100644 index 7ca9b1be..00000000 --- a/src/renderer/utils/music/xm/musicInfo.js +++ /dev/null @@ -1,14 +0,0 @@ -import { xmRequest } from './util' - -export default { - _requestObj: null, - async getMusicInfo({ songmid }, page = 1, limit = 20) { - if (this._requestObj) this._requestObj.cancelHttp() - - const _requestObj = xmRequest('/api/song/initialize', { songId: songmid }) - const { body, statusCode } = await _requestObj.promise - // console.log(body) - if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取歌曲信息失败') - return { source: 'xm', data: body.result.data.songDetail } - }, -} diff --git a/src/renderer/utils/music/xm/musicSearch.js b/src/renderer/utils/music/xm/musicSearch.js deleted file mode 100644 index b4a7ff6c..00000000 --- a/src/renderer/utils/music/xm/musicSearch.js +++ /dev/null @@ -1,115 +0,0 @@ -// import '../../polyfill/array.find' -// import jshtmlencode from 'js-htmlencode' -import { xmRequest } from './util' -import { formatPlayTime, sizeFormate } from '../../index' -// import { debug } from '../../utils/env' -// import { formatSinger } from './util' -// "cdcb72dc3eba41cb5bc4267f09183119_xmMain_/api/list/collect_{"pagingVO":{"page":1,"pageSize":60},"dataType":"system"}" -let searchRequest -export default { - limit: 30, - total: 0, - page: 0, - allPage: 1, - musicSearch(str, page) { - if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() - searchRequest = xmRequest('/api/search/searchSongs', { - key: str, - pagingVO: { - page: page, - pageSize: this.limit, - }, - }) - return searchRequest.promise.then(({ body }) => body) - }, - getSinger(singers) { - let arr = [] - singers.forEach(singer => { - arr.push(singer.artistName) - }) - return arr.join('、') - }, - handleResult(rawData) { - // console.log(rawData) - let ids = new Set() - const list = [] - rawData.forEach(songData => { - if (!songData) return - if (ids.has(songData.songId)) return - ids.add(songData.songId) - - const types = [] - const _types = {} - let size = null - for (const item of songData.purviewRoleVOs) { - if (!item.filesize) continue - size = sizeFormate(item.filesize) - switch (item.quality) { - case 's': - types.push({ type: 'wav', size }) - _types.wav = { - size, - } - break - case 'h': - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'l': - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - } - } - types.reverse() - - list.push({ - singer: this.getSinger(songData.singerVOs), - name: songData.songName, - albumName: songData.albumName, - albumId: songData.albumId, - source: 'xm', - interval: formatPlayTime(parseInt(songData.length / 1000)), - songmid: songData.songId, - img: songData.albumLogo || songData.albumLogoS, - songStringId: songData.songStringId, - lrc: null, - lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, - otherSource: null, - types, - _types, - typeUrl: {}, - }) - }) - return list - }, - search(str, page = 1, { limit } = {}, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit != null) this.limit = limit - // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page).then(result => { - if (!result) return this.search(str, page, { limit }, retryNum) - if (result.code !== 'SUCCESS') return this.search(str, page, { limit }, retryNum) - // const songResultData = result.data || { songs: [], total: 0 } - - let list = this.handleResult(result.result.data.songs) - if (list == null) return this.search(str, page, { limit }, retryNum) - - this.total = parseInt(result.result.data.pagingVO.count) - this.page = page - this.allPage = Math.ceil(this.total / this.limit) - - return Promise.resolve({ - list, - allPage: this.allPage, - limit: this.limit, - total: this.total, - source: 'xm', - }) - }).catch(err => err.message.includes('canceled verify') ? Promise.reject(err) : this.search(str, page, { limit }, retryNum)) - }, -} diff --git a/src/renderer/utils/music/xm/songList.js b/src/renderer/utils/music/xm/songList.js deleted file mode 100644 index 5389cff0..00000000 --- a/src/renderer/utils/music/xm/songList.js +++ /dev/null @@ -1,221 +0,0 @@ -import { xmRequest } from './util' -import { sizeFormate, formatPlayTime } from '../../index' - -export default { - _requestObj_tags: null, - _requestObj_list: null, - _requestObj_listDetail: null, - limit_list: 36, - limit_song: 100000, - successCode: 'SUCCESS', - sortList: [ - { - name: '推荐', - id: 'system', - }, - { - name: '精选', - id: 'recommend', - }, - { - name: '最热', - id: 'hot', - }, - { - name: '最新', - id: 'new', - }, - ], - regExps: { - // https://www.xiami.com/collect/1138092824?action=play - listDetailLink: /^.+\/collect\/(\d+)(?:\s\(.*|\?.*|&.*$|#.*$|$)/, - }, - tagsUrl: '/api/collect/getRecommendTags', - songListUrl: '/api/list/collect', - songListDetailUrl: '/api/collect/initialize', - getSongListData(sortId, tagId, page) { - if (tagId == null) { - return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId } - } - switch (sortId) { - case 'system': - case 'recommend': - sortId = 'hot' - } - return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId, key: tagId } - }, - - /** - * 格式化播放数量 - * @param {*} num - */ - formatPlayCount(num) { - if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' - if (num > 10000) return parseInt(num / 1000) / 10 + '万' - return num - }, - getSinger(singers) { - let arr = [] - singers.forEach(singer => { - arr.push(singer.artistName) - }) - return arr.join('、') - }, - - getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐 - if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - - if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') - - this._requestObj_listDetail = xmRequest('/api/collect/getCollectStaticUrl', { listId: id }) - return this._requestObj_listDetail.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum) - this._requestObj_listDetail = xmRequest(body.result.data.data.data.url) - return this._requestObj_listDetail.promise.then(({ body }) => { - if (!body.status) return this.getListDetail(id, page, ++tryNum) - // console.log(JSON.stringify(body)) - return { - list: this.filterListDetail(body.resultObj.songs), - page, - limit: this.limit_song, - total: body.resultObj.songCount, - source: 'xm', - info: { - name: body.resultObj.collectName, - img: body.resultObj.collectLogo, - desc: body.resultObj.description, - author: body.resultObj.userName, - play_count: this.formatPlayCount(body.resultObj.playCount), - }, - } - }) - }) - }, - filterListDetail(rawList) { - // console.log(rawList) - let ids = new Set() - const list = [] - rawList.forEach(songData => { - if (!songData) return - if (ids.has(songData.songId)) return - ids.add(songData.songId) - - const types = [] - const _types = {} - let size = null - for (const item of songData.purviewRoleVOs) { - if (!item.filesize) continue - size = sizeFormate(item.filesize) - switch (item.quality) { - case 's': - types.push({ type: 'wav', size }) - _types.wav = { - size, - } - break - case 'h': - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'l': - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - } - } - types.reverse() - - list.push({ - singer: this.getSinger(songData.singerVOs), - name: songData.songName, - albumName: songData.albumName, - albumId: songData.albumId, - source: 'xm', - interval: formatPlayTime(parseInt(songData.length / 1000)), - songmid: songData.songId, - songStringId: songData.songStringId, - img: songData.albumLogo || songData.albumLogoS, - lrc: null, - lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile, - otherSource: null, - types, - _types, - typeUrl: {}, - }) - }) - return list - }, - - // 获取列表数据 - getList(sortId, tagId, page, tryNum = 0) { - if (this._requestObj_list) this._requestObj_list.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_list = xmRequest(this.songListUrl, this.getSongListData(sortId, tagId, page)) - return this._requestObj_list.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) - return { - list: this.filterList(body.result.data.collects), - total: body.result.data.pagingVO.count, - page, - limit: body.result.data.pagingVO.pageSize, - source: 'xm', - } - }) - }, - filterList(rawData) { - return rawData.map(item => ({ - play_count: this.formatPlayCount(item.playCount), - id: item.listId, - author: item.userName, - name: item.collectName, - time: null, - img: item.collectLogo, - grade: null, - desc: null, - source: 'xm', - })) - }, - - // 获取标签 - getTag(tryNum = 0) { - if (this._requestObj_tags) this._requestObj_tags.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_tags = xmRequest(this.tagsUrl, { recommend: 1 }) - return this._requestObj_tags.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getTag(++tryNum) - return this.filterTagInfo(body.result.data.recommendTags) - }) - }, - filterTagInfo(rawList) { - return { - hotTag: rawList[0].items.map(item => ({ - id: item.name, - name: item.name, - source: 'xm', - })), - tags: rawList.slice(1).map(item => ({ - name: item.title, - list: item.items.map(tag => ({ - parent_id: item.title, - parent_name: item.title, - id: tag.name, - name: tag.name, - source: 'xm', - })), - })), - source: 'xm', - } - }, - getTags() { - return this.getTag() - }, -} - -// getList -// getTags -// getListDetail diff --git a/src/renderer/utils/music/xm/util.js b/src/renderer/utils/music/xm/util.js deleted file mode 100644 index de8fa799..00000000 --- a/src/renderer/utils/music/xm/util.js +++ /dev/null @@ -1,122 +0,0 @@ -import { httpGet, httpFetch } from '../../request' -import { toMD5 } from '../../index' -// import crateIsg from './isg' -import { rendererInvoke, NAMES } from '../../../../common/ipc' - -if (!window.xm_token) { - let data = window.localStorage.getItem('xm_token') - window.xm_token = data ? JSON.parse(data) : { - cookies: {}, - cookie: null, - token: null, - isGetingToken: false, - } - window.xm_token.isGetingToken = false -} - -export const formatSinger = rawData => rawData.replace(/&/g, '、') - -const matchToken = headers => { - let cookies = {} - let token - for (const item of headers['set-cookie']) { - const [key, value] = item.substring(0, item.indexOf(';')).split('=') - cookies[key] = value - if (key == 'xm_sg_tk') token = value.substring(0, value.indexOf('_')) - } - // console.log(cookies) - return { token, cookies } -} - -const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) - -const createToken = (token, path, params) => toMD5(`${token}_xmMain_${path}_${params}`) - -const handleSaveToken = ({ token, cookies }) => { - Object.assign(window.xm_token.cookies, cookies) - // window.xm_token.cookies.isg = crateIsg() - window.xm_token.cookie = Object.keys(window.xm_token.cookies).map(k => `${k}=${window.xm_token.cookies[k]};`).join(' ') - if (token) window.xm_token.token = token - - window.localStorage.setItem('xm_token', JSON.stringify(window.xm_token)) -} - -export const getToken = (path, params) => new Promise((resolve, reject) => { - if (window.xm_token.isGetingToken) return wait(1000).then(() => getToken(path, params).then(data => resolve(data))) - if (window.xm_token.token) return resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie }) - window.xm_token.isGetingToken = true - httpGet('https://www.xiami.com/', (err, resp) => { - window.xm_token.isGetingToken = false - if (err) return reject(err) - if (resp.statusCode != 200) return reject(new Error('获取失败')) - - handleSaveToken(matchToken(resp.headers)) - - resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie }) - }) -}) - -const baseUrl = 'https://www.xiami.com' -export const xmRequest = (path, params = '') => { - let query = params - if (params != '') { - params = JSON.stringify(params) - query = '&_q=' + encodeURIComponent(params) - } - let requestObj = { - isInited: false, - isCancelled: false, - cancelHttp() { - if (!this.isInited) this.isCancelled = true - this.requestObj.cancelHttp() - }, - } - - requestObj.promise = getToken(path, params).then(data => { - // console.log(data) - if (requestObj.isCancelled) return Promise.reject('取消请求') - let url = path - if (!/^http/.test(path)) url = baseUrl + path - let s = `_s=${data.token}${query}` - url += (url.includes('?') ? '&' : '?') + s - requestObj.requestObj = httpFetch(url, { - headers: { - Referer: 'https://www.xiami.com/', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', - cookie: data.cookie, - }, - }) - return requestObj.requestObj.promise.then(resp => { - // console.log(resp.body) - if (resp.statusCode != 200) { - // console.log(resp.headers) - window.xm_token.token = null - return Promise.reject(new Error('获取失败')) - } - if (resp.body.code !== 'SUCCESS' && resp.body.rgv587_flag == 'sm') { - window.globalObj.xm.isShowVerify = true - return wait(300).then(() => { - return rendererInvoke(NAMES.mainWindow.handle_xm_verify_open, 'https:' + resp.body.url).then(x5sec => { - handleSaveToken({ cookies: { x5sec } }) - // console.log(x5sec) - window.globalObj.xm.isShowVerify = false - return Promise.reject(new Error('获取成功')) - }).catch(err => { - window.globalObj.xm.isShowVerify = false - return Promise.reject(err) - }) - }) - } - if (resp.headers['set-cookie']) handleSaveToken(matchToken(resp.headers)) - - return Promise.resolve(resp) - }) - }) - return requestObj -} - -export const closeVerifyModal = async() => { - if (!window.globalObj.xm.isShowVerify) return - await rendererInvoke(NAMES.mainWindow.handle_xm_verify_close) - window.globalObj.xm.isShowVerify = false -} diff --git a/src/renderer/views/Download.vue b/src/renderer/views/Download.vue index 77a327df..4dd190cc 100644 --- a/src/renderer/views/Download.vue +++ b/src/renderer/views/Download.vue @@ -45,7 +45,7 @@ export default { return { clickTime: window.performance.now(), clickIndex: -1, - selectdData: [], + selectedData: [], isShowDownloadMultiple: false, tabId: 'all', keyEvent: { @@ -59,6 +59,7 @@ export default { play: true, start: true, pause: true, + playLater: true, file: true, search: true, remove: true, @@ -74,13 +75,13 @@ export default { computed: { ...mapGetters(['setting']), ...mapGetters('download', ['list', 'downloadStatus']), - ...mapGetters('player', ['listId', 'playIndex']), + ...mapGetters('player', ['playInfo']), isPlayList() { - return this.listId == 'download' + return this.playInfo.listId == 'download' }, playListIndex() { - if (this.listId != 'download' || !this.list.length) return - let info = this.list[this.playIndex] + if (this.playInfo.listId != 'download' || !this.list.length) return + let info = this.list[this.playInfo.playIndex] if (!info) return -1 let key = info.key return this.showList.findIndex(i => i.key == key) @@ -140,6 +141,11 @@ export default { action: 'pause', hide: !this.listMenu.itemMenuControl.pause, }, + { + name: this.$t('view.download.menu_play_later'), + action: 'playLater', + hide: !this.listMenu.itemMenuControl.playLater, + }, { name: this.$t('view.download.menu_file'), action: 'file', @@ -229,7 +235,7 @@ export default { }, handleSelectData(event, clickIndex) { if (this.keyEvent.isShiftDown) { - if (this.selectdData.length) { + if (this.selectedData.length) { let lastSelectIndex = this.lastSelectIndex this.removeAllSelect() if (lastSelectIndex != clickIndex) { @@ -240,8 +246,8 @@ export default { clickIndex = temp isNeedReverse = true } - this.selectdData = this.showList.slice(lastSelectIndex, clickIndex + 1) - if (isNeedReverse) this.selectdData.reverse() + this.selectedData = this.showList.slice(lastSelectIndex, clickIndex + 1) + if (isNeedReverse) this.selectedData.reverse() let nodes = this.$refs.dom_tbody.childNodes do { nodes[lastSelectIndex].classList.add('active') @@ -250,24 +256,24 @@ export default { } } else { event.currentTarget.classList.add('active') - this.selectdData.push(this.showList[clickIndex]) + this.selectedData.push(this.showList[clickIndex]) this.lastSelectIndex = clickIndex } } else if (this.keyEvent.isModDown) { this.lastSelectIndex = clickIndex let item = this.showList[clickIndex] - let index = this.selectdData.indexOf(item) + let index = this.selectedData.indexOf(item) if (index < 0) { - this.selectdData.push(item) + this.selectedData.push(item) event.currentTarget.classList.add('active') } else { - this.selectdData.splice(index, 1) + this.selectedData.splice(index, 1) event.currentTarget.classList.remove('active') } - } else if (this.selectdData.length) this.removeAllSelect() + } else if (this.selectedData.length) this.removeAllSelect() }, removeAllSelect() { - this.selectdData = [] + this.selectedData = [] let dom_tbody = this.$refs.dom_tbody if (!dom_tbody) return let nodes = dom_tbody.querySelectorAll('.active') @@ -306,6 +312,14 @@ export default { case 'remove': this.removeTask(item) break + case 'playLater': + if (this.selectedData.length) { + this.setTempPlayList(this.selectedData.map(s => ({ listId: 'download', musicInfo: s }))) + this.removeAllSelect() + } else { + this.setTempPlayList([{ listId: 'download', musicInfo: item }]) + } + break case 'file': this.handleOpenFolder(item.filePath) break @@ -316,7 +330,7 @@ export default { }, handleSelectAllData() { this.removeAllSelect() - this.selectdData = [...this.showList] + this.selectedData = [...this.showList] let nodes = this.$refs.dom_tbody.childNodes for (const node of nodes) { @@ -324,19 +338,19 @@ export default { } }, // async handleFlowBtnClick(action) { - // let selectdData = [...this.selectdData] + // let selectedData = [...this.selectedData] // this.removeAllSelect() // await this.$nextTick() // switch (action) { // case 'start': - // this.startTasks(selectdData) + // this.startTasks(selectedData) // break // case 'pause': - // this.pauseTasks(selectdData) + // this.pauseTasks(selectedData) // break // case 'remove': - // this.removeTasks(selectdData) + // this.removeTasks(selectedData) // break // } // }, @@ -353,7 +367,7 @@ export default { }) }, handleTabChange() { - this.selectdData = [] + this.selectedData = [] }, handleListItemRigthClick(event, index) { this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.showList[index].musicInfo.source].getMusicDetailPageUrl @@ -368,16 +382,19 @@ export default { let item = this.showList[index] if (item.isComplate) { this.listMenu.itemMenuControl.play = + this.listMenu.itemMenuControl.playLater = this.listMenu.itemMenuControl.file = true this.listMenu.itemMenuControl.start = this.listMenu.itemMenuControl.pause = false } else if (item.status === this.downloadStatus.ERROR || item.status === this.downloadStatus.PAUSE) { this.listMenu.itemMenuControl.play = + this.listMenu.itemMenuControl.playLater = this.listMenu.itemMenuControl.pause = this.listMenu.itemMenuControl.file = false this.listMenu.itemMenuControl.start = true } else { this.listMenu.itemMenuControl.play = + this.listMenu.itemMenuControl.playLater = this.listMenu.itemMenuControl.start = this.listMenu.itemMenuControl.file = false this.listMenu.itemMenuControl.pause = true @@ -407,10 +424,10 @@ export default { if (item) this.handlePlay(item) break case 'start': - if (this.selectdData.length) { - let selectdData = [...this.selectdData] + if (this.selectedData.length) { + let selectedData = [...this.selectedData] this.removeAllSelect() - this.startTasks(selectdData) + this.startTasks(selectedData) } else { item = this.showList[index] if (item) this.startTask(item) @@ -420,10 +437,10 @@ export default { } break case 'pause': - if (this.selectdData.length) { - let selectdData = [...this.selectdData] + if (this.selectedData.length) { + let selectedData = [...this.selectedData] this.removeAllSelect() - this.pauseTasks(selectdData) + this.pauseTasks(selectedData) } else { item = this.showList[index] if (item) this.pauseTask(item) @@ -443,10 +460,10 @@ export default { if (item) this.handleSearch(item.musicInfo) break case 'remove': - if (this.selectdData.length) { - let selectdData = [...this.selectdData] + if (this.selectedData.length) { + let selectedData = [...this.selectedData] this.removeAllSelect() - this.removeTasks(selectdData) + this.removeTasks(selectedData) } else { item = this.showList[index] if (item) this.removeTask(item) diff --git a/src/renderer/views/Leaderboard.vue b/src/renderer/views/Leaderboard.vue index 7d90705f..317b1b3c 100644 --- a/src/renderer/views/Leaderboard.vue +++ b/src/renderer/views/Leaderboard.vue @@ -77,7 +77,7 @@ export default { ...mapActions('leaderboard', ['getBoardsList', 'getList']), ...mapActions('download', ['createDownload', 'createDownloadMultiple']), ...mapMutations('list', ['listAdd', 'listAddMultiple']), - ...mapMutations('player', ['setList']), + ...mapMutations('player', ['setList', 'setTempPlayList']), handleListBtnClick(info) { switch (info.action) { case 'download': @@ -121,6 +121,14 @@ export default { } this.testPlay(info.index) break + case 'playLater': + if (this.selectedData.length) { + this.setTempPlayList(this.selectedData.map(s => ({ listId: '__temp__', musicInfo: s }))) + this.resetSelect() + } else { + this.setTempPlayList([{ listId: '__temp__', musicInfo: this.list[info.index] }]) + } + break case 'search': this.handleSearch(info.index) break diff --git a/src/renderer/views/List.vue b/src/renderer/views/List.vue index b875cbaf..15e8966b 100644 --- a/src/renderer/views/List.vue +++ b/src/renderer/views/List.vue @@ -37,7 +37,7 @@ table tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody") tr(v-for='(item, index) in list' :key='item.songmid' :id="'mid_' + item.songmid" @contextmenu="handleListItemRigthClick($event, index)" - @click="handleDoubleClick($event, index)" :class="[isPlayList && playIndex === index ? $style.active : '', assertApiSupport(item.source) ? null : $style.disabled]") + @click="handleDoubleClick($event, index)" :class="[isPlayList && playInfo.playIndex === index ? $style.active : '', assertApiSupport(item.source) ? null : $style.disabled]") td.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}} td.break span.select {{item.name}} @@ -54,7 +54,7 @@ td(style="width: 9%;") span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}} td(style="width: 15%; padding-left: 0; padding-right: 0;") - material-list-buttons(:index="index" @btn-click="handleListBtnClick") + material-list-buttons(:index="index" @btn-click="handleListBtnClick" :download-btn="assertApiSupport(item.source)") //- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)') 下载 //- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)') 试听 //- button.btn-secondary(type='button' @click.stop='handleRemove(index)') 删除 @@ -122,6 +122,7 @@ export default { isShowItemMenu: false, itemMenuControl: { play: true, + playLater: true, copyName: true, addTo: true, moveTo: true, @@ -145,12 +146,12 @@ export default { computed: { ...mapGetters(['userInfo', 'setting']), ...mapGetters('list', ['isInitedList', 'defaultList', 'loveList', 'userList']), - ...mapGetters('player', { - playerListId: 'listId', - playIndex: 'playIndex', - }), + ...mapGetters('player', ['playInfo']), + playerListId() { + return this.playInfo.listId + }, isPlayList() { - return this.playerListId == this.listId + return this.playInfo.listId == this.listId }, list() { return this.listData.list @@ -223,6 +224,11 @@ export default { action: 'download', disabled: !this.listMenu.itemMenuControl.download, }, + { + name: this.$t('view.list.list_play_later'), + action: 'playLater', + disabled: !this.listMenu.itemMenuControl.playLater, + }, { name: this.$t('view.list.list_add_to'), action: 'addTo', @@ -355,6 +361,7 @@ export default { ...mapActions('download', ['createDownload', 'createDownloadMultiple']), ...mapMutations('player', { setPlayList: 'setList', + setTempPlayList: 'setTempPlayList', }), listenEvent() { window.eventHub.$on('key_shift_down', this.handle_key_shift_down) @@ -562,7 +569,7 @@ export default { } }, testPlay(index) { - if (!this.assertApiSupport(this.list[index].source)) return + // if (!this.assertApiSupport(this.list[index].source)) return this.setPlayList({ list: this.listData, index }) }, handleRemove(index) { @@ -572,7 +579,7 @@ export default { switch (info.action) { case 'download': { const minfo = this.list[info.index] - if (!this.assertApiSupport(minfo.source)) return + // if (!this.assertApiSupport(minfo.source)) return this.musicInfo = minfo this.$nextTick(() => { this.isShowDownload = true @@ -725,8 +732,9 @@ export default { }, handleListItemRigthClick(event, index) { this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl - this.listMenu.itemMenuControl.play = - this.listMenu.itemMenuControl.download = + // this.listMenu.itemMenuControl.play = + // this.listMenu.itemMenuControl.playLater = + this.listMenu.itemMenuControl.download = this.assertApiSupport(this.list[index].source) let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected') if (dom_selected) dom_selected.classList.remove('selected') @@ -789,6 +797,14 @@ export default { case 'play': this.testPlay(index) break + case 'playLater': + if (this.selectdListDetailData.length) { + this.setTempPlayList(this.selectdListDetailData.map(s => ({ listId: this.listId, musicInfo: s }))) + this.removeAllSelectListDetail() + } else { + this.setTempPlayList([{ listId: this.listId, musicInfo: this.list[index] }]) + } + break case 'copyName': minfo = this.list[index] clipboardWriteText(this.setting.download.fileName.replace('歌名', minfo.name).replace('歌手', minfo.singer)) diff --git a/src/renderer/views/Search.vue b/src/renderer/views/Search.vue index f7ed67a4..e8aac721 100644 --- a/src/renderer/views/Search.vue +++ b/src/renderer/views/Search.vue @@ -3,60 +3,65 @@ //- transition div(:class="$style.header") material-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" v-model="searchSourceId") - div(v-if="listInfo.list.length" :class="$style.list") - div(:class="$style.thead") - table - thead - tr - th.nobreak.center(style="width: 5%;") # - th.nobreak {{$t('view.search.name')}} - th.nobreak(style="width: 22%;") {{$t('view.search.singer')}} - th.nobreak(style="width: 22%;") {{$t('view.search.album')}} - th.nobreak(style="width: 8%;") {{$t('view.search.time')}} - th.nobreak(style="width: 13%;") {{$t('view.search.action')}} - div.scroll(:class="$style.tbody" ref="dom_scrollContent") - table - tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody") - tr(v-for='(item, index) in listInfo.list' :key='item.songmid' @contextmenu="handleListItemRigthClick($event, index)" @click="handleDoubleClick($event, index)") - td.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}} - td.break - span.select {{item.name}} - span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('material.song_list.lossless')}} - span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('material.song_list.high_quality')}} - span(:class="[$style.labelSource, $style.noSelect]" v-if="searchSourceId == 'all'") {{item.source}} - td.break(style="width: 22%;") - span.select {{item.singer}} - td.break(style="width: 22%;") - span.select {{item.albumName}} - td(style="width: 8%;") - span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}} - td(style="width: 13%; padding-left: 0; padding-right: 0;") - material-list-buttons(:index="index" :remove-btn="false" :class="$style.listBtn" - :play-btn="assertApiSupport(item.source)" - :download-btn="assertApiSupport(item.source)" - @btn-click="handleListBtnClick") - div(:class="$style.pagination") - material-pagination(:max-page="listInfo.allPage" :limit="listInfo.limit" :page="page" @btn-click="handleTogglePage") - div(v-else :class="$style.noitem") - div.scroll(:class="$style.noitemListContainer" v-if="setting.search.isShowHotSearch || setting.search.isShowHistorySearch") - dl(:class="[$style.noitemList, $style.noitemHotSearchList]" v-if="setting.search.isShowHotSearch") - dt(:class="$style.noitemListTitle") {{$t('view.search.hot_search')}} - dd(:class="$style.noitemListItem" @click="handleNoitemSearch(item)" v-for="item in hotSearchList") {{item}} - dl(:class="$style.noitemList" v-if="setting.search.isShowHistorySearch && historyList.length") - dt(:class="$style.noitemListTitle") - span {{$t('view.search.history_search')}} - span(:class="$style.historyClearBtn" @click="clearHistory" :tips="$t('view.search.history_clear')") - svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 512 512' space='preserve') - use(xlink:href='#icon-eraser') - dd(:class="$style.noitemListItem" v-for="(item, index) in historyList" @contextmenu="removeHistory(index)" :key="index + item" @click="handleNoitemSearch(item)" :tips="$t('view.search.history_remove')") {{item}} - div(v-else :class="$style.noitem_list") - p {{$t('view.search.no_item')}} + div(:class="$style.main") + div(:class="$style.list" v-show="isLoading || listInfo.list.length") + div(:class="$style.thead") + table + thead + tr + th.nobreak.center(style="width: 5%;") # + th.nobreak {{$t('view.search.name')}} + th.nobreak(style="width: 22%;") {{$t('view.search.singer')}} + th.nobreak(style="width: 22%;") {{$t('view.search.album')}} + th.nobreak(style="width: 8%;") {{$t('view.search.time')}} + th.nobreak(style="width: 13%;") {{$t('view.search.action')}} + div.scroll(:class="$style.tbody" ref="dom_scrollContent") + table + tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody") + tr(v-for='(item, index) in listInfo.list' :key='item.songmid' @contextmenu="handleListItemRigthClick($event, index)" @click="handleDoubleClick($event, index)") + td.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}} + td.break + span.select {{item.name}} + span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('material.song_list.lossless')}} + span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('material.song_list.high_quality')}} + span(:class="[$style.labelSource, $style.noSelect]" v-if="searchSourceId == 'all'") {{item.source}} + td.break(style="width: 22%;") + span.select {{item.singer}} + td.break(style="width: 22%;") + span.select {{item.albumName}} + td(style="width: 8%;") + span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}} + td(style="width: 13%; padding-left: 0; padding-right: 0;") + material-list-buttons(:index="index" :remove-btn="false" :class="$style.listBtn" + :play-btn="assertApiSupport(item.source)" + :download-btn="assertApiSupport(item.source)" + @btn-click="handleListBtnClick") + div(:class="$style.pagination") + material-pagination(:max-page="listInfo.allPage" :limit="listInfo.limit" :page="page" @btn-click="handleTogglePage") + transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated fadeOut") + div(v-show="isLoading" :class="$style.loading") + p {{$t('view.search.loding_list')}} + transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut") + div(v-show="!isLoading && !listInfo.list.length" :class="$style.noitem") + div.scroll(:class="$style.noitemListContainer" v-if="setting.search.isShowHotSearch || setting.search.isShowHistorySearch") + dl(:class="[$style.noitemList, $style.noitemHotSearchList]" v-if="setting.search.isShowHotSearch") + dt(:class="$style.noitemListTitle") {{$t('view.search.hot_search')}} + dd(:class="$style.noitemListItem" @click="handleNoitemSearch(item)" v-for="item in hotSearchList") {{item}} + dl(:class="$style.noitemList" v-if="setting.search.isShowHistorySearch && historyList.length") + dt(:class="$style.noitemListTitle") + span {{$t('view.search.history_search')}} + span(:class="$style.historyClearBtn" @click="clearHistory" :tips="$t('view.search.history_clear')") + svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 512 512' space='preserve') + use(xlink:href='#icon-eraser') + dd(:class="$style.noitemListItem" v-for="(item, index) in historyList" @contextmenu="removeHistory(index)" :key="index + item" @click="handleNoitemSearch(item)" :tips="$t('view.search.history_remove')") {{item}} + div(v-else :class="$style.noitem_list") + p {{$t('view.search.no_item')}} + material-menu(:menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :isShow="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick") material-download-modal(:show="isShowDownload" :musicInfo="musicInfo" @select="handleAddDownload" @close="isShowDownload = false") material-download-multiple-modal(:show="isShowDownloadMultiple" :list="selectedData" @select="handleAddDownloadMultiple" @close="isShowDownloadMultiple = false") //- material-flow-btn(:show="isShowEditBtn && (searchSourceId == 'all' || assertApiSupport(searchSourceId))" :remove-btn="false" @btn-click="handleFlowBtnClick") material-list-add-modal(:show="isShowListAdd" :musicInfo="musicInfo" @close="handleListAddModalClose") material-list-add-multiple-modal(:show="isShowListAddMultiple" :musicList="selectedData" @close="handleListAddMultipleModalClose") - material-menu(:menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :isShow="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick") @@ -1090,12 +1140,12 @@ export default {