mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-09-05 21:16:52 +00:00
Compare commits
79 Commits
ZonyLrcToo
...
ZonyLrcToo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
093e2879ad | ||
![]() |
fea5e1124f | ||
![]() |
2ce7b00d14 | ||
![]() |
ad6ea732f8 | ||
![]() |
06bf733ae9 | ||
![]() |
558cca3c0e | ||
![]() |
93cc31b1e2 | ||
![]() |
dfe18d8541 | ||
![]() |
40b73c3dfa | ||
![]() |
243a0e2559 | ||
![]() |
d83bff1516 | ||
![]() |
8d946ebbd5 | ||
![]() |
4c3d554885 | ||
![]() |
14c78f9a83 | ||
![]() |
b9d6da5f72 | ||
![]() |
c8ffbc55af | ||
![]() |
b0835dcf01 | ||
![]() |
4e1c4d1519 | ||
![]() |
3d96b96edf | ||
![]() |
7f81a3edea | ||
![]() |
24624a6d21 | ||
![]() |
d860632327 | ||
![]() |
1186369d16 | ||
![]() |
b240564cf7 | ||
![]() |
6e2848a9de | ||
![]() |
6361bf9265 | ||
![]() |
383e2c5939 | ||
![]() |
1e5c41852f | ||
![]() |
41cba02833 | ||
![]() |
98c935ed93 | ||
![]() |
62d08df735 | ||
![]() |
1f312c749d | ||
![]() |
30e10f4c01 | ||
![]() |
35468cfb71 | ||
![]() |
126741f344 | ||
![]() |
22f5bc69ec | ||
![]() |
788ff38be2 | ||
![]() |
ab5f79bd50 | ||
![]() |
f935f07609 | ||
![]() |
493f48cefe | ||
![]() |
4999e4d694 | ||
![]() |
334528eca6 | ||
![]() |
e76ddb75eb | ||
![]() |
d6d384590c | ||
![]() |
949dac7ad1 | ||
![]() |
2ffd75ed1d | ||
![]() |
82af3009d3 | ||
![]() |
572e555f2a | ||
![]() |
286731b1a6 | ||
![]() |
5850614663 | ||
![]() |
c20a280a07 | ||
![]() |
55de00ec1f | ||
![]() |
bc2b314d6a | ||
![]() |
0aafe91f96 | ||
![]() |
2e71914578 | ||
![]() |
b916323986 | ||
![]() |
afe1a7013c | ||
![]() |
fb1f743365 | ||
![]() |
0e5e48cd00 | ||
![]() |
18d9c2d32c | ||
![]() |
5dbabab5a6 | ||
![]() |
bfd89e9d82 | ||
![]() |
9a029225e1 | ||
![]() |
a7ecfbe44f | ||
![]() |
f1a6eefe45 | ||
![]() |
86e341290e | ||
![]() |
0aa8ca5aef | ||
![]() |
2eef72e165 | ||
![]() |
f4b17097fd | ||
![]() |
e68d6b9cf9 | ||
![]() |
84231d9613 | ||
![]() |
8398d80a8e | ||
![]() |
991db8ab48 | ||
![]() |
3e957fdb25 | ||
![]() |
fc09839775 | ||
![]() |
8d5a45c9ef | ||
![]() |
6b538f3e0a | ||
![]() |
d7e11af3ba | ||
![]() |
f25ba90b9a |
6
.github/workflows/dotnet.yml
vendored
6
.github/workflows/dotnet.yml
vendored
@@ -17,10 +17,8 @@ jobs:
|
|||||||
run: echo "::set-output name=date::$(date +'%Y%m%d')${{github.run_number}}"
|
run: echo "::set-output name=date::$(date +'%Y%m%d')${{github.run_number}}"
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v2
|
uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 7.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
@@ -38,6 +36,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
retention-days: 90
|
||||||
name: release-files
|
name: release-files
|
||||||
path: |
|
path: |
|
||||||
./TempFiles
|
./TempFiles
|
||||||
@@ -45,6 +44,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.date.outputs.date }}
|
version: ${{ steps.date.outputs.date }}
|
||||||
release:
|
release:
|
||||||
|
if: github.event_name == 'push'
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
46
Directory.Packages.props
Normal file
46
Directory.Packages.props
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.0"/>
|
||||||
|
<PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.0"/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="QRCoder" Version="1.4.3"/>
|
||||||
|
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0"/>
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0"/>
|
||||||
|
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0"/>
|
||||||
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="Polly" Version="8.2.0"/>
|
||||||
|
<PackageVersion Include="TagLibSharp" Version="2.3.0"/>
|
||||||
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
|
<PackageVersion Include="NetEscapades.Configuration.Yaml" Version="3.1.0"/>
|
||||||
|
<PackageVersion Include="MusicDecrypto.Library" Version="2.3.0"/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="AutoMapper" Version="12.0.1"/>
|
||||||
|
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.0"/>
|
||||||
|
<PackageVersion Include="SuperSocket.WebSocket" Version="2.0.0-beta.11"/>
|
||||||
|
<PackageVersion Include="SuperSocket.WebSocket.Server" Version="2.0.0-beta.11"/>
|
||||||
|
|
||||||
|
<!-- Testing Projects -->
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||||
|
<PackageVersion Include="Shouldly" Version="4.2.1"/>
|
||||||
|
<PackageVersion Include="xunit" Version="2.6.3"/>
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.5"/>
|
||||||
|
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
|
||||||
|
|
||||||
|
<!-- Avalonia -->
|
||||||
|
<PackageVersion Include="Avalonia" Version="11.0.6"/>
|
||||||
|
<PackageVersion Include="Avalonia.Desktop" Version="11.0.6"/>
|
||||||
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.0.6"/>
|
||||||
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.6"/>
|
||||||
|
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.0.6"/>
|
||||||
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.6"/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
BIN
README.assets/image-20230328223155280.png
Normal file
BIN
README.assets/image-20230328223155280.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
153
README.md
153
README.md
@@ -1,161 +1,30 @@
|
|||||||
简体中文 | [English](./docs/en_US.md)
|
简体中文 | [English](./docs/en_US.md)
|
||||||
|
|
||||||
## 免责声明
|
# 免责声明
|
||||||
|
|
||||||
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||||
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),[本人](https://github.com/real-zony) 不承担任何法律责任。
|
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||||
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||||
|
|
||||||
## 简介
|
# 简介
|
||||||
|
|
||||||
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
|
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具,**QQ 群:337656932**,群文件里面有详细的视频教程。
|
||||||
|
|
||||||
🚧 当前版本正在开发当中。
|
🚧 当前版本正在开发当中。
|
||||||
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
|
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
|
||||||
|
|
||||||
## 下载
|
# 下载
|
||||||
|
|
||||||
工具会执行每日构建动作,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
|
如果你要获取最新版本,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
|
||||||
|
|
||||||
## 用法
|
# 用法
|
||||||
|
|
||||||
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell/命令提示符/Windows 终端,根据下述说明执行命令即可。
|
软件的具体使用方法已经迁移到了全新的文档站点,请跳转到 [https://docs.myzony.com](https://docs.myzony.com),里面包含完整的说明文档。
|
||||||
|
|
||||||
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
|
# 捐赠
|
||||||
|
|
||||||
### 子命令
|
|
||||||
|
|
||||||
#### 歌词下载
|
|
||||||
|
|
||||||
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
|
|
||||||
|
|
||||||
```shell
|
|
||||||
.\ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
|
|
||||||
|
|
||||||
.\ZonyLrcTools.Cli.exe download -h|--help
|
|
||||||
```
|
|
||||||
|
|
||||||
**例子**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# 下载歌词
|
|
||||||
.\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2
|
|
||||||
|
|
||||||
# 下载专辑封面
|
|
||||||
.\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 加密格式转换
|
|
||||||
|
|
||||||
子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**。
|
|
||||||
|
|
||||||
具体支持的格式请参考项目 [MusicDecrypto](https://github.com/davidxuang/MusicDecrypto/blob/master/MusicDecrypto.Library/DecryptoFactory.cs#L23),本工具仅做一个集成,替换掉原本自己的一些实现。现在不需要指定对应的类型参数,程序会自动根据文件后缀选择适合的解密算法。
|
|
||||||
|
|
||||||
命令只需要一个参数 `-s`,指定需要转换的文件夹或者是文件路径。
|
|
||||||
|
|
||||||
```shell
|
|
||||||
.\ZonyLrcTools.Cli.exe util -s D:\CloudMusic
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置文件
|
|
||||||
|
|
||||||
程序的所有的配置信息,都在 `config.yaml` 进行更改,下面标注了各个配置的说明。
|
|
||||||
|
|
||||||
其中是否开启的可选项为 `true` 或者 `false`,等同于中文的是和否。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
globalOption:
|
|
||||||
# 允许扫描的歌曲文件后缀名。
|
|
||||||
supportFileExtensions:
|
|
||||||
- '*.mp3'
|
|
||||||
- '*.flac'
|
|
||||||
- '*.wav'
|
|
||||||
# 网络代理服务设置,仅支持 HTTP 代理。
|
|
||||||
networkOptions:
|
|
||||||
isEnable: false # 是否启用代理。
|
|
||||||
ip: 127.0.0.1 # 代理服务 IP 地址。
|
|
||||||
port: 4780 # 代理服务端口号。
|
|
||||||
updateUrl: https://api.myzony.com/lrc-tools/update # 更新检查地址。
|
|
||||||
|
|
||||||
# 下载器的相关参数配置。
|
|
||||||
provider:
|
|
||||||
# 标签扫描器的相关参数配置。
|
|
||||||
tag:
|
|
||||||
# 支持的标签扫描器。
|
|
||||||
plugin:
|
|
||||||
- name: Taglib # 基于 Taglib 库的标签扫描器。
|
|
||||||
priority: 1 # 优先级,升序排列。
|
|
||||||
- name: FileName # 基于文件名的标签扫描器。
|
|
||||||
priority: 2
|
|
||||||
# 基于文件名扫描器的扩展参数。
|
|
||||||
extensions:
|
|
||||||
# 正则表达式,用于匹配文件名中的作者信息和歌曲信息,可根据
|
|
||||||
# 自己的需求进行调整。
|
|
||||||
regularExpressions: "(?'artist'.+)\\s-\\s(?'name'.+)"
|
|
||||||
# 歌曲标签屏蔽字典替换功能。
|
|
||||||
blockWord:
|
|
||||||
isEnable: false # 是否启用屏蔽字典。
|
|
||||||
filePath: 'BlockWords.json' # 屏蔽字典的路径。
|
|
||||||
# 歌词下载器的相关参数配置。
|
|
||||||
lyric:
|
|
||||||
# 支持的歌词下载器。
|
|
||||||
plugin:
|
|
||||||
- name: NetEase # 基于网易云音乐的歌词下载器。
|
|
||||||
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
|
|
||||||
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
|
||||||
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
|
||||||
priority: 2
|
|
||||||
# depth: 10 # 暂时不支持。
|
|
||||||
- name: KuGou # 基于酷狗音乐的歌词下载器。
|
|
||||||
priority: 3
|
|
||||||
depth: 10
|
|
||||||
- name: KuWo # 基于酷我音乐的歌词下载器。
|
|
||||||
priority: 4
|
|
||||||
depth: 10
|
|
||||||
# 歌词下载的一些共有配置参数。
|
|
||||||
config:
|
|
||||||
isOneLine: true # 双语歌词是否合并为一行展示。
|
|
||||||
lineBreak: "\n" # 换行符的类型,记得使用双引号指定。
|
|
||||||
isEnableTranslation: true # 是否启用翻译歌词。
|
|
||||||
isOnlyOutputTranslation: false # 是否只输出翻译歌词。
|
|
||||||
isSkipExistLyricFiles: false # 如果歌词文件已经存在,是否跳过这些文件。
|
|
||||||
fileEncoding: 'utf-8' # 歌词文件的编码格式。
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 支持的编码格式
|
|
||||||
|
|
||||||
详细信息请参考: [MSDN Encoding 列表](https://learn.microsoft.com/en-us/dotnet/api/System.Text.Encoding.GetEncodings?view=net-6.0#examples),使用 `identifier and name` 作为参数值填入 `config.yaml` 文件当中的 `fileEncoding`。
|
|
||||||
|
|
||||||
#### 支持的歌词源
|
|
||||||
|
|
||||||
| 歌词源 | 默认优先级 |
|
|
||||||
| ---------- | ---------- |
|
|
||||||
| 网易云音乐 | 1 |
|
|
||||||
| QQ 音乐 | 2 |
|
|
||||||
| 酷狗音乐 | 3 |
|
|
||||||
| 酷我音乐 | 4 |
|
|
||||||
|
|
||||||
### 屏蔽字典
|
|
||||||
|
|
||||||
屏蔽字典适用于网易云音乐歌词下载,针对某些单词,网易云音乐使用了 * 号进行屏蔽,这个时候可以使用屏蔽字典,设置歌曲名的关键词替换。例如有一首歌曲叫做 *Fucking ABC* ,这个时候网易云实际的名字是 *Fu****ing* ,用户只需要在屏蔽字典加入替换逻辑即可,例如:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Fuckking": "Fu****ing"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
屏蔽字典默认路径为程序所在目录的 *BlockWords.json* 文件,用户可以在 *appsettings.json* 文件中配置其他路径。
|
|
||||||
|
|
||||||
## 捐赠
|
|
||||||
|
|
||||||
<img src="./docs/img/alipay.jpg" width="200"/><img src="./docs/img/wechat.jpg" width="200"/>
|
<img src="./docs/img/alipay.jpg" width="200"/><img src="./docs/img/wechat.jpg" width="200"/>
|
||||||
|
|
||||||
## Star History
|
# Star History
|
||||||
|
|
||||||
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
||||||
|
|
||||||
## 路线图
|
|
||||||
|
|
||||||
- [x] 支持跨平台的 CLI 工具。
|
|
||||||
- [x] 基于 Web GUI 的操作站点。
|
|
||||||
- [ ] 支持插件系统(Lua 引擎)。
|
|
||||||
|
@@ -15,9 +15,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.LocalServer",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Common", "src\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj", "{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Common", "src\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj", "{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vendor", "vendor", "{AB0E3F21-DDBF-459D-995C-04C177BF8995}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Desktop", "src\ZonyLrcTools.Desktop\ZonyLrcTools.Desktop.csproj", "{DC92902B-4303-4E43-AFB3-3F93FD3986AD}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicDecrypto.Library", "vendor\MusicDecrypto\MusicDecrypto.Library\MusicDecrypto.Library.csproj", "{6BB67593-38CD-42E4-8409-EDAC3C138A6B}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -41,10 +39,10 @@ Global
|
|||||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -57,6 +55,6 @@ Global
|
|||||||
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
|
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
|
||||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
{2875A08A-FFD6-4863-B815-5384DCFE88FC} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||||
{6BB67593-38CD-42E4-8409-EDAC3C138A6B} = {AB0E3F21-DDBF-459D-995C-04C177BF8995}
|
{DC92902B-4303-4E43-AFB3-3F93FD3986AD} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
3
ZonyLrcTools.sln.DotSettings
Normal file
3
ZonyLrcTools.sln.DotSettings
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QQ/@EntryIndexedValue">QQ</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zony/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@@ -1,8 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using McMaster.Extensions.CommandLineUtils;
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
|
||||||
using ZonyLrcTools.Common;
|
using ZonyLrcTools.Common;
|
||||||
using ZonyLrcTools.Common.Album;
|
using ZonyLrcTools.Common.Album;
|
||||||
using ZonyLrcTools.Common.Lyrics;
|
using ZonyLrcTools.Common.Lyrics;
|
||||||
|
using ZonyLrcTools.Common.MusicScanner;
|
||||||
|
|
||||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
// ReSharper disable MemberCanBePrivate.Global
|
||||||
@@ -15,14 +21,17 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
private readonly ILyricsDownloader _lyricsDownloader;
|
private readonly ILyricsDownloader _lyricsDownloader;
|
||||||
private readonly IAlbumDownloader _albumDownloader;
|
private readonly IAlbumDownloader _albumDownloader;
|
||||||
private readonly IMusicInfoLoader _musicInfoLoader;
|
private readonly IMusicInfoLoader _musicInfoLoader;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
public DownloadCommand(ILyricsDownloader lyricsDownloader,
|
public DownloadCommand(ILyricsDownloader lyricsDownloader,
|
||||||
IMusicInfoLoader musicInfoLoader,
|
IMusicInfoLoader musicInfoLoader,
|
||||||
IAlbumDownloader albumDownloader)
|
IAlbumDownloader albumDownloader,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_lyricsDownloader = lyricsDownloader;
|
_lyricsDownloader = lyricsDownloader;
|
||||||
_musicInfoLoader = musicInfoLoader;
|
_musicInfoLoader = musicInfoLoader;
|
||||||
_albumDownloader = albumDownloader;
|
_albumDownloader = albumDownloader;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region > Options <
|
#region > Options <
|
||||||
@@ -37,24 +46,92 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
|
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
|
||||||
public bool DownloadAlbum { get; set; }
|
public bool DownloadAlbum { get; set; }
|
||||||
|
|
||||||
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
|
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 1)")]
|
||||||
public int ParallelNumber { get; set; } = 2;
|
public int ParallelNumber { get; set; } = 1;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region > Scanner Options <
|
||||||
|
|
||||||
|
[Option("-sc|--scanner", CommandOptionType.SingleValue, Description = "指定歌词文件扫描器,目前支持本地文件(local),网易云音乐(netease),csv 文件(csv),默认值为 local。")]
|
||||||
|
public string Scanner { get; set; } = "local";
|
||||||
|
|
||||||
|
[Option("-o|--output", Description = "指定歌词文件的输出路径。")]
|
||||||
|
public string OutputDirectory { get; set; } = "DownloadedLrc";
|
||||||
|
|
||||||
|
[Option("-p|--pattern", Description = "指定歌词文件的输出文件名模式。")]
|
||||||
|
public string OutputFileNamePattern { get; set; } = "{Artist} - {Name}.lrc";
|
||||||
|
|
||||||
|
[Option("-f|--file", Description = "指定 CSV 文件的路径。")]
|
||||||
|
public string CsvFilePath { get; set; }
|
||||||
|
|
||||||
|
[Option("-s|--song-list-id", Description = "指定网易云音乐歌单的 ID,如果有多个歌单,请使用 ';' 分割 ID。")]
|
||||||
|
public string SongListId { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||||
{
|
{
|
||||||
|
if (!DownloadAlbum && !DownloadLyric)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。");
|
||||||
|
}
|
||||||
|
|
||||||
if (DownloadLyric)
|
if (DownloadLyric)
|
||||||
{
|
{
|
||||||
await _lyricsDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
|
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DownloadAlbum)
|
if (DownloadAlbum)
|
||||||
{
|
{
|
||||||
await _albumDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
|
await _albumDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the music infos by the scanner.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<MusicInfo>> GetMusicInfosAsync(string scanner)
|
||||||
|
{
|
||||||
|
ValidateScannerOptions(scanner);
|
||||||
|
|
||||||
|
return scanner switch
|
||||||
|
{
|
||||||
|
MusicScannerConsts.LocalScanner => await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber),
|
||||||
|
MusicScannerConsts.CsvScanner => await _serviceProvider.GetService<CsvFileMusicScanner>()
|
||||||
|
.GetMusicInfoFromCsvFileAsync(CsvFilePath, OutputDirectory, OutputFileNamePattern),
|
||||||
|
MusicScannerConsts.NeteaseScanner => await _serviceProvider.GetService<NetEaseMusicSongListMusicScanner>()
|
||||||
|
.GetMusicInfoFromNetEaseMusicSongListAsync(SongListId, OutputDirectory, OutputFileNamePattern),
|
||||||
|
_ => await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manually validate the options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scanner">Scanner Name.</param>
|
||||||
|
/// <exception cref="ArgumentException">If the options are invalid.</exception>
|
||||||
|
private void ValidateScannerOptions(string scanner)
|
||||||
|
{
|
||||||
|
if (scanner != MusicScannerConsts.LocalScanner && string.IsNullOrEmpty(OutputDirectory))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("当使用非本地文件扫描器时,必须指定歌词文件的输出路径。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanner != MusicScannerConsts.LocalScanner && !Directory.Exists(OutputDirectory))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("指定的歌词文件输出路径不存在。");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scanner)
|
||||||
|
{
|
||||||
|
case MusicScannerConsts.CsvScanner when string.IsNullOrWhiteSpace(CsvFilePath):
|
||||||
|
throw new ArgumentException("当使用 CSV 文件扫描器时,必须指定 CSV 文件的路径。");
|
||||||
|
case MusicScannerConsts.NeteaseScanner when string.IsNullOrWhiteSpace(SongListId):
|
||||||
|
throw new ArgumentException("当使用网易云音乐扫描器时,必须指定歌单的 ID。");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Cli.Commands.SubCommand;
|
||||||
|
|
||||||
|
[Command("manual", Description = "手动指定歌曲信息,然后下载对应的歌词数据。")]
|
||||||
|
public class ManualDownloadCommand : ToolCommandBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<ManualDownloadCommand> _logger;
|
||||||
|
|
||||||
|
public ManualDownloadCommand(ILogger<ManualDownloadCommand> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,23 @@
|
|||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using McMaster.Extensions.CommandLineUtils;
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
namespace ZonyLrcTools.Cli.Commands
|
namespace ZonyLrcTools.Cli.Commands
|
||||||
{
|
{
|
||||||
[HelpOption("--help|-h", Description = "欢迎使用 ZonyLrcToolsX Command Line Interface。")]
|
[HelpOption("--help|-h",
|
||||||
|
Description = "欢迎使用 ZonyLrcToolsX Command Line Interface,有任何问题请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。",
|
||||||
|
ShowInHelpText = true)]
|
||||||
public abstract class ToolCommandBase
|
public abstract class ToolCommandBase
|
||||||
{
|
{
|
||||||
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||||
{
|
{
|
||||||
|
if (!Environment.UserInteractive)
|
||||||
|
{
|
||||||
|
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
|
||||||
|
Console.ReadKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.ShowHelp();
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
// ReSharper disable IdentifierTypo
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
|
||||||
|
|
||||||
|
public class MusicScannerConsts
|
||||||
|
{
|
||||||
|
public const string LocalScanner = "local";
|
||||||
|
public const string NeteaseScanner = "netease";
|
||||||
|
public const string CsvScanner = "csv";
|
||||||
|
}
|
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using McMaster.Extensions.CommandLineUtils;
|
using McMaster.Extensions.CommandLineUtils;
|
||||||
@@ -8,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
using Serilog.Sinks.SystemConsole.Themes;
|
|
||||||
using ZonyLrcTools.Cli.Commands;
|
using ZonyLrcTools.Cli.Commands;
|
||||||
using ZonyLrcTools.Cli.Commands.SubCommand;
|
using ZonyLrcTools.Cli.Commands.SubCommand;
|
||||||
using ZonyLrcTools.Cli.Infrastructure;
|
using ZonyLrcTools.Cli.Infrastructure;
|
||||||
@@ -17,6 +15,8 @@ using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
|||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||||
|
|
||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
|
||||||
namespace ZonyLrcTools.Cli
|
namespace ZonyLrcTools.Cli
|
||||||
{
|
{
|
||||||
[Command("lyric-tool")]
|
[Command("lyric-tool")]
|
||||||
@@ -33,6 +33,7 @@ namespace ZonyLrcTools.Cli
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Log.Logger.Information("Starting...");
|
||||||
return await BuildHostedService(args);
|
return await BuildHostedService(args);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -63,12 +64,12 @@ namespace ZonyLrcTools.Cli
|
|||||||
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
|
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
|
||||||
.WriteTo.Logger(lc =>
|
.WriteTo.Logger(lc =>
|
||||||
{
|
{
|
||||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
|
lc.Filter.ByIncludingOnly(warningLog => warningLog.Level == LogEventLevel.Warning)
|
||||||
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
|
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
|
||||||
})
|
})
|
||||||
.WriteTo.Logger(lc =>
|
.WriteTo.Logger(lc =>
|
||||||
{
|
{
|
||||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error)
|
lc.Filter.ByIncludingOnly(errLog => errLog.Level == LogEventLevel.Error)
|
||||||
.WriteTo.Async(c => c.File("Logs/errors.txt"));
|
.WriteTo.Async(c => c.File("Logs/errors.txt"));
|
||||||
})
|
})
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
@@ -77,11 +78,11 @@ namespace ZonyLrcTools.Cli
|
|||||||
private static Task<int> BuildHostedService(string[] args)
|
private static Task<int> BuildHostedService(string[] args)
|
||||||
{
|
{
|
||||||
return new HostBuilder()
|
return new HostBuilder()
|
||||||
.ConfigureLogging(builder => builder.AddSerilog())
|
.ConfigureLogging(l => l.AddSerilog())
|
||||||
.ConfigureHostConfiguration(builder =>
|
.ConfigureHostConfiguration(builder =>
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||||
.AddYamlFile("config.yaml");
|
.AddYamlFile("config.yaml");
|
||||||
})
|
})
|
||||||
.ConfigureServices((_, services) =>
|
.ConfigureServices((_, services) =>
|
||||||
|
@@ -4,7 +4,8 @@
|
|||||||
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
|
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
|
||||||
"10003": "不能获取文件的后缀信息。",
|
"10003": "不能获取文件的后缀信息。",
|
||||||
"10004": "没有扫描到任何音乐文件。",
|
"10004": "没有扫描到任何音乐文件。",
|
||||||
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。"
|
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。",
|
||||||
|
"10006": "无法从网易云音乐获取歌曲列表。"
|
||||||
},
|
},
|
||||||
"Warning": {
|
"Warning": {
|
||||||
"50001": "扫描文件时出现了错误。",
|
"50001": "扫描文件时出现了错误。",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<AssemblyVersion>0.0.0.1</AssemblyVersion>
|
<AssemblyVersion>0.0.0.1</AssemblyVersion>
|
||||||
<FileVersion>0.0.0.1</FileVersion>
|
<FileVersion>0.0.0.1</FileVersion>
|
||||||
@@ -10,36 +10,34 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.1" />
|
<PackageReference Include="McMaster.Extensions.CommandLineUtils"/>
|
||||||
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.1" />
|
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
<PackageReference Include="Serilog.Extensions.Hosting"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Async"/>
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageReference Include="Serilog.Sinks.File"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages"/>
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="appsettings.json" />
|
<None Remove="appsettings.json"/>
|
||||||
<None Remove="Resources\error_msg.json" />
|
<None Remove="Resources\error_msg.json"/>
|
||||||
<Content Include="Resources\error_msg.json">
|
<Content Include="Resources\error_msg.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<None Remove="BlockWords.json" />
|
<None Remove="BlockWords.json"/>
|
||||||
<Content Include="BlockWords.json">
|
<Content Include="BlockWords.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<None Remove="config.yaml" />
|
<None Remove="config.yaml"/>
|
||||||
<Content Include="config.yaml">
|
<Content Include="config.yaml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj" />
|
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -4,6 +4,9 @@ globalOption:
|
|||||||
- '*.mp3'
|
- '*.mp3'
|
||||||
- '*.flac'
|
- '*.flac'
|
||||||
- '*.wav'
|
- '*.wav'
|
||||||
|
- '*.m4a'
|
||||||
|
- '*.ogg'
|
||||||
|
- '*.opus'
|
||||||
# 网络代理服务设置,仅支持 HTTP 代理。
|
# 网络代理服务设置,仅支持 HTTP 代理。
|
||||||
networkOptions:
|
networkOptions:
|
||||||
isEnable: false # 是否启用代理。
|
isEnable: false # 是否启用代理。
|
||||||
@@ -37,6 +40,8 @@ globalOption:
|
|||||||
- name: NetEase # 基于网易云音乐的歌词下载器。
|
- name: NetEase # 基于网易云音乐的歌词下载器。
|
||||||
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
|
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
|
||||||
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
||||||
|
additional:
|
||||||
|
isEnableRomanOutput: false # 是否启用罗马音输出,本参数仅当对应歌曲有罗马音歌词时有效。
|
||||||
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
||||||
priority: 2
|
priority: 2
|
||||||
# depth: 10 # 暂时不支持。
|
# depth: 10 # 暂时不支持。
|
||||||
|
20
src/ZonyLrcTools.Cli/publish.ps1
Normal file
20
src/ZonyLrcTools.Cli/publish.ps1
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
$Platforms = @('win-x64', 'linux-x64', 'osx-x64', 'win-arm64', 'linux-arm64', 'osx-arm64')
|
||||||
|
|
||||||
|
if (-not (Test-Path ./TempFiles)) {
|
||||||
|
New-Item -ItemType Directory -Path ./TempFiles | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item ./TempFiles/* -Recurse -Force
|
||||||
|
|
||||||
|
foreach ($platform in $Platforms) {
|
||||||
|
dotnet publish -r $platform -c Release -p:PublishSingleFile=true --self-contained true | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Location ./bin/Release/net7.0/$platform/publish/
|
||||||
|
Compress-Archive -Path ./* -DestinationPath ./ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip | Out-Null
|
||||||
|
Set-Location ../../../../../
|
||||||
|
|
||||||
|
Move-Item ./bin/Release/net7.0/$platform/publish/ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip ./TempFiles
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
Platforms=('win-x64' 'linux-x64' 'osx-x64')
|
Platforms=('win-x64' 'linux-x64' 'osx-x64' 'win-arm64' 'linux-arm64' 'osx-arm64')
|
||||||
|
|
||||||
if ! [ -d './TempFiles' ];
|
if ! [ -d './TempFiles' ];
|
||||||
then
|
then
|
||||||
@@ -10,11 +10,11 @@ rm -rf ./TempFiles/*
|
|||||||
|
|
||||||
for platform in "${Platforms[@]}"
|
for platform in "${Platforms[@]}"
|
||||||
do
|
do
|
||||||
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true || exit 1
|
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:DebugType=none --self-contained true || exit 1
|
||||||
|
|
||||||
cd ./bin/Release/net7.0/"$platform"/publish/ || exit 1
|
cd ./bin/Release/net8.0/"$platform"/publish/ || exit 1
|
||||||
zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1
|
zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1
|
||||||
cd ../../../../../ || exit 1
|
cd ../../../../../ || exit 1
|
||||||
|
|
||||||
mv ./bin/Release/net7.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
|
mv ./bin/Release/net8.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
|
||||||
done
|
done
|
@@ -41,7 +41,7 @@ namespace ZonyLrcTools.Common.Album.NetEase
|
|||||||
true,
|
true,
|
||||||
_defaultOption);
|
_defaultOption);
|
||||||
|
|
||||||
if (searchResult is not { StatusCode: 200 } || searchResult.Items?.SongCount <= 0)
|
if (searchResult is not { StatusCode: 200 } || searchResult.Items is not { SongCount: > 0 })
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。
|
/// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string FilePath { get; set; }
|
public string FilePath { get; set; } = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,16 +5,16 @@ namespace ZonyLrcTools.Common.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支持的音乐文件后缀集合。
|
/// 支持的音乐文件后缀集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> SupportFileExtensions { get; set; }
|
public List<string> SupportFileExtensions { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 网络代理相关的配置信息。
|
/// 网络代理相关的配置信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NetworkOptions NetworkOptions { get; set; }
|
public NetworkOptions NetworkOptions { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 定义下载器的相关配置信息。
|
/// 定义下载器的相关配置信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProviderOptions Provider { get; set; }
|
public ProviderOptions Provider { get; set; } = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,12 +2,12 @@ namespace ZonyLrcTools.Common.Configuration;
|
|||||||
|
|
||||||
public class LyricsOptions
|
public class LyricsOptions
|
||||||
{
|
{
|
||||||
public IEnumerable<LyricsProviderOptions> Plugin { get; set; }
|
public IEnumerable<LyricsProviderOptions> Plugin { get; set; } = null!;
|
||||||
|
|
||||||
public GlobalLyricsConfigOptions Config { get; set; }
|
public GlobalLyricsConfigOptions Config { get; set; } = null!;
|
||||||
|
|
||||||
public LyricsProviderOptions GetLyricProviderOption(string name)
|
public LyricsProviderOptions GetLyricProviderOption(string name)
|
||||||
{
|
{
|
||||||
return Plugin.FirstOrDefault(x => x.Name == name);
|
return Plugin.FirstOrDefault(x => x.Name == name)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,7 +5,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词下载器的唯一标识。
|
/// 歌词下载器的唯一标识。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词下载时的优先级,当值为 -1 时是禁用。
|
/// 歌词下载时的优先级,当值为 -1 时是禁用。
|
||||||
|
@@ -13,7 +13,7 @@ namespace ZonyLrcTools.Common.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 代理服务器的 Ip。
|
/// 代理服务器的 Ip。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Ip { get; set; }
|
public string Ip { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 代理服务器的 端口。
|
/// 代理服务器的 端口。
|
||||||
|
@@ -5,10 +5,10 @@ public class ProviderOptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标签加载器相关的配置属性。
|
/// 标签加载器相关的配置属性。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TagInfoOptions Tag { get; set; }
|
public TagInfoOptions Tag { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词下载相关的配置信息。
|
/// 歌词下载相关的配置信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LyricsOptions Lyric { get; set; }
|
public LyricsOptions Lyric { get; set; } = null!;
|
||||||
}
|
}
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
public class TagInfoOptions
|
public class TagInfoOptions
|
||||||
{
|
{
|
||||||
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; }
|
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 屏蔽词功能相关配置。
|
/// 屏蔽词功能相关配置。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BlockWordOptions BlockWord { get; set; }
|
public BlockWordOptions BlockWord { get; set; } = null!;
|
||||||
}
|
}
|
@@ -2,10 +2,10 @@ namespace ZonyLrcTools.Common.Configuration
|
|||||||
{
|
{
|
||||||
public class TagInfoProviderOptions
|
public class TagInfoProviderOptions
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, string> Extensions { get; set; }
|
public Dictionary<string, string> Extensions { get; set; } = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -45,7 +45,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
|||||||
|
|
||||||
public static List<Type> GetDefaultExposedTypes(Type type)
|
public static List<Type> GetDefaultExposedTypes(Type type)
|
||||||
{
|
{
|
||||||
var serviceTypes = new List<Type>();
|
var serviceTypes = new List<Type> { type };
|
||||||
|
|
||||||
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
|
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
|
||||||
{
|
{
|
||||||
@@ -59,7 +59,6 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
|||||||
if (type.Name.EndsWith(interfaceName))
|
if (type.Name.EndsWith(interfaceName))
|
||||||
{
|
{
|
||||||
serviceTypes.Add(interfaceType);
|
serviceTypes.Add(interfaceType);
|
||||||
serviceTypes.Add(type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置工具会用到的服务。
|
/// 配置工具会用到的服务。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
|
public static IServiceCollection? ConfigureToolService(this IServiceCollection? services)
|
||||||
{
|
{
|
||||||
if (services == null)
|
if (services == null)
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
|||||||
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
|
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||||
.AddYamlFile("config.yaml")
|
.AddYamlFile("config.yaml")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
@@ -0,0 +1,109 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供网易云音乐 API 的相关加密方法。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 加密方法参考以下开源项目:
|
||||||
|
/// 1. https://github.com/jitwxs/163MusicLyrics/blob/master/MusicLyricApp/Api/Music/NetEaseMusicNativeApi.cs
|
||||||
|
/// 2. https://github.com/mos9527/pyncm/blob/ad0a84b2ed5f1affa9890d5f54f6170c2cf99bbb/pyncm/utils/crypto.py#L53
|
||||||
|
/// </remarks>
|
||||||
|
public static class NetEaseMusicEncryptionHelper
|
||||||
|
{
|
||||||
|
public const string Modulus =
|
||||||
|
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
|
||||||
|
|
||||||
|
public const string Nonce = "0CoJUm6Qyw8W8jud";
|
||||||
|
public const string PubKey = "010001";
|
||||||
|
public const string Vi = "0102030405060708";
|
||||||
|
public static readonly byte[] ID_XOR_KEY_1 = "3go8&$8*3*3h0k(2)2"u8.ToArray();
|
||||||
|
|
||||||
|
public static string RsaEncode(string text)
|
||||||
|
{
|
||||||
|
var srText = new string(text.Reverse().ToArray());
|
||||||
|
var a = BCHexDec(BitConverter.ToString(Encoding.Default.GetBytes(srText)).Replace("-", string.Empty));
|
||||||
|
var b = BCHexDec(PubKey);
|
||||||
|
var c = BCHexDec(Modulus);
|
||||||
|
var key = BigInteger.ModPow(a, b, c).ToString("x");
|
||||||
|
key = key.PadLeft(256, '0');
|
||||||
|
|
||||||
|
return key.Length > 256 ? key.Substring(key.Length - 256, 256) : key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigInteger BCHexDec(string hex)
|
||||||
|
{
|
||||||
|
var dec = new BigInteger(0);
|
||||||
|
var len = hex.Length;
|
||||||
|
|
||||||
|
for (var i = 0; i < len; i++)
|
||||||
|
{
|
||||||
|
dec += BigInteger.Multiply(new BigInteger(Convert.ToInt32(hex[i].ToString(), 16)),
|
||||||
|
BigInteger.Pow(new BigInteger(16), len - i - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AesEncode(string secretData, string secret = "TA3YiYCfY2dDJQgg")
|
||||||
|
{
|
||||||
|
byte[] encrypted;
|
||||||
|
var iv = Encoding.UTF8.GetBytes(Vi);
|
||||||
|
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = Encoding.UTF8.GetBytes(secret);
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
using (var encryptor = aes.CreateEncryptor())
|
||||||
|
{
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var cryptoStream = new CryptoStream(stream, encryptor, CryptoStreamMode.Write))
|
||||||
|
{
|
||||||
|
using (var sw = new StreamWriter(cryptoStream))
|
||||||
|
{
|
||||||
|
sw.Write(secretData);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted = stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.ToBase64String(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CreateSecretKey(int length)
|
||||||
|
{
|
||||||
|
const string str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
var sb = new StringBuilder(length);
|
||||||
|
var rnd = new Random();
|
||||||
|
|
||||||
|
for (var i = 0; i < length; ++i)
|
||||||
|
{
|
||||||
|
sb.Append(str[rnd.Next(0, str.Length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CloudMusicDllEncode(string deviceId)
|
||||||
|
{
|
||||||
|
var xored = new byte[deviceId.Length];
|
||||||
|
for (var i = 0; i < deviceId.Length; i++)
|
||||||
|
{
|
||||||
|
xored[i] = (byte)(deviceId[i] ^ ID_XOR_KEY_1[i % ID_XOR_KEY_1.Length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var md5 = MD5.Create())
|
||||||
|
{
|
||||||
|
var digest = md5.ComputeHash(xored);
|
||||||
|
return Convert.ToBase64String(digest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
|||||||
{
|
{
|
||||||
public int ErrorCode { get; }
|
public int ErrorCode { get; }
|
||||||
|
|
||||||
public object AttachObject { get; }
|
public object? AttachObject { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
|
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
|
||||||
@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
|||||||
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
||||||
/// <param name="message">错误信息。</param>
|
/// <param name="message">错误信息。</param>
|
||||||
/// <param name="attachObj">附加的对象数据。</param>
|
/// <param name="attachObj">附加的对象数据。</param>
|
||||||
public ErrorCodeException(int errorCode, string? message = null, object attachObj = null) : base(message)
|
public ErrorCodeException(int errorCode, string? message = null, object? attachObj = null) : base(message)
|
||||||
{
|
{
|
||||||
ErrorCode = errorCode;
|
ErrorCode = errorCode;
|
||||||
AttachObject = attachObj;
|
AttachObject = attachObj;
|
||||||
|
@@ -26,14 +26,14 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "error_msg.json");
|
var jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "error_msg.json");
|
||||||
using var jsonReader = new JsonTextReader(File.OpenText(jsonPath));
|
using var jsonReader = new JsonTextReader(File.OpenText(jsonPath));
|
||||||
var jsonObj = JObject.Load(jsonReader);
|
var jsonObj = JObject.Load(jsonReader);
|
||||||
|
|
||||||
var errors = jsonObj.SelectTokens("$.Error.*");
|
var errors = jsonObj.SelectTokens("$.Error.*");
|
||||||
var warnings = jsonObj.SelectTokens("$.Warning.*");
|
var warnings = jsonObj.SelectTokens("$.Warning.*");
|
||||||
errors.Union(warnings).Select(m => m.Parent).OfType<JProperty>().ToList()
|
errors.Union(warnings).Select(m => m.Parent).OfType<JProperty>().ToList()
|
||||||
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>()));
|
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>() ?? string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];
|
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];
|
||||||
|
@@ -31,6 +31,11 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
|||||||
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
|
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int NotSupportedFileEncoding = 10005;
|
public const int NotSupportedFileEncoding = 10005;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文本: 无法从网易云音乐获取歌曲列表。
|
||||||
|
/// </summary>
|
||||||
|
public const int UnableGetSongListFromNetEaseCloudMusic = 10006;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
|||||||
/// <param name="logger">日志记录器实例。</param>
|
/// <param name="logger">日志记录器实例。</param>
|
||||||
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
|
||||||
/// <param name="e">异常实例,可为空。</param>
|
/// <param name="e">异常实例,可为空。</param>
|
||||||
public static void LogWarningWithErrorCode(this IWarpLogger logger, int errorCode, Exception e = null)
|
public static void LogWarningWithErrorCode(this IWarpLogger logger, int errorCode, Exception? e = null)
|
||||||
{
|
{
|
||||||
logger.WarnAsync($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}").GetAwaiter().GetResult();
|
logger.WarnAsync($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
@@ -19,9 +19,29 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<string> PostAsync(string url,
|
public async ValueTask<string> PostAsync(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
bool isQueryStringParam = false,
|
bool isQueryStringParam = false,
|
||||||
Action<HttpRequestMessage> requestOption = null)
|
Action<HttpRequestMessage>? requestOption = null)
|
||||||
|
{
|
||||||
|
using var responseMessage = await PostReturnHttpResponseAsync(url, parameters, isQueryStringParam, requestOption);
|
||||||
|
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
|
||||||
|
object? parameters = null,
|
||||||
|
bool isQueryStringParam = false,
|
||||||
|
Action<HttpRequestMessage>? requestOption = null)
|
||||||
|
{
|
||||||
|
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
|
||||||
|
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<HttpResponseMessage> PostReturnHttpResponseAsync(string url,
|
||||||
|
object? parameters = null,
|
||||||
|
bool isQueryStringParam = false,
|
||||||
|
Action<HttpRequestMessage>? requestOption = null)
|
||||||
{
|
{
|
||||||
var parametersStr = isQueryStringParam ? BuildQueryString(parameters) : BuildJsonBodyString(parameters);
|
var parametersStr = isQueryStringParam ? BuildQueryString(parameters) : BuildJsonBodyString(parameters);
|
||||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
|
||||||
@@ -29,24 +49,12 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
|
|
||||||
requestOption?.Invoke(requestMessage);
|
requestOption?.Invoke(requestMessage);
|
||||||
|
|
||||||
using var responseMessage = await BuildHttpClient().SendAsync(requestMessage);
|
return await BuildHttpClient().SendAsync(requestMessage);
|
||||||
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
|
|
||||||
object parameters = null,
|
|
||||||
bool isQueryStringParam = false,
|
|
||||||
Action<HttpRequestMessage> requestOption = null)
|
|
||||||
{
|
|
||||||
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
|
|
||||||
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<string> GetAsync(string url,
|
public async ValueTask<string> GetAsync(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
Action<HttpRequestMessage> requestOption = null)
|
Action<HttpRequestMessage>? requestOption = null)
|
||||||
{
|
{
|
||||||
var requestParamsStr = BuildQueryString(parameters);
|
var requestParamsStr = BuildQueryString(parameters);
|
||||||
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
|
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
|
||||||
@@ -59,8 +67,8 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
|
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
Action<HttpRequestMessage> requestOption = null)
|
Action<HttpRequestMessage>? requestOption = null)
|
||||||
{
|
{
|
||||||
var responseString = await GetAsync(url, parameters, requestOption);
|
var responseString = await GetAsync(url, parameters, requestOption);
|
||||||
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
|
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
|
||||||
@@ -71,7 +79,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
return _httpClientFactory.CreateClient(HttpClientNameConstant);
|
return _httpClientFactory.CreateClient(HttpClientNameConstant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildQueryString(object parameters)
|
private string BuildQueryString(object? parameters)
|
||||||
{
|
{
|
||||||
if (parameters == null)
|
if (parameters == null)
|
||||||
{
|
{
|
||||||
@@ -81,7 +89,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
var type = parameters.GetType();
|
var type = parameters.GetType();
|
||||||
if (type == typeof(string))
|
if (type == typeof(string))
|
||||||
{
|
{
|
||||||
return parameters as string;
|
return parameters as string ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var properties = type.GetProperties();
|
var properties = type.GetProperties();
|
||||||
@@ -98,7 +106,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
return paramBuilder.ToString().TrimEnd('&');
|
return paramBuilder.ToString().TrimEnd('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildJsonBodyString(object parameters)
|
private string BuildJsonBodyString(object? parameters)
|
||||||
{
|
{
|
||||||
if (parameters == null) return string.Empty;
|
if (parameters == null) return string.Empty;
|
||||||
if (parameters is string result) return result;
|
if (parameters is string result) return result;
|
||||||
@@ -114,7 +122,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
|
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
|
||||||
/// <returns>如果响应正常,则返回具体的响应内容。</returns>
|
/// <returns>如果响应正常,则返回具体的响应内容。</returns>
|
||||||
/// <exception cref="ErrorCodeException">如果 Http 响应不正常,则可能抛出本异常。</exception>
|
/// <exception cref="ErrorCodeException">如果 Http 响应不正常,则可能抛出本异常。</exception>
|
||||||
private string ValidateHttpResponse(HttpResponseMessage responseMessage, object requestParameters, string responseString)
|
private string ValidateHttpResponse(HttpResponseMessage responseMessage, object? requestParameters, string responseString)
|
||||||
{
|
{
|
||||||
return responseMessage.StatusCode switch
|
return responseMessage.StatusCode switch
|
||||||
{
|
{
|
||||||
@@ -131,7 +139,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
|
|||||||
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
|
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
|
||||||
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
||||||
/// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception>
|
/// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception>
|
||||||
private TResponse ConvertHttpResponseToObject<TResponse>(object requestParameters, string responseString)
|
private TResponse ConvertHttpResponseToObject<TResponse>(object? requestParameters, string responseString)
|
||||||
{
|
{
|
||||||
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new { requestParameters, responseString });
|
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new { requestParameters, responseString });
|
||||||
|
|
||||||
|
@@ -14,9 +14,9 @@
|
|||||||
/// <param name="requestOption">请求时的配置动作。</param>
|
/// <param name="requestOption">请求时的配置动作。</param>
|
||||||
/// <returns>服务端的响应结果。</returns>
|
/// <returns>服务端的响应结果。</returns>
|
||||||
ValueTask<string> PostAsync(string url,
|
ValueTask<string> PostAsync(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
bool isQueryStringParam = false,
|
bool isQueryStringParam = false,
|
||||||
Action<HttpRequestMessage> requestOption = null);
|
Action<HttpRequestMessage>? requestOption = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
||||||
@@ -28,9 +28,14 @@
|
|||||||
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
||||||
/// <returns>服务端的响应结果。</returns>
|
/// <returns>服务端的响应结果。</returns>
|
||||||
ValueTask<TResponse> PostAsync<TResponse>(string url,
|
ValueTask<TResponse> PostAsync<TResponse>(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
bool isQueryStringParam = false,
|
bool isQueryStringParam = false,
|
||||||
Action<HttpRequestMessage> requestOption = null);
|
Action<HttpRequestMessage>? requestOption = null);
|
||||||
|
|
||||||
|
ValueTask<HttpResponseMessage> PostReturnHttpResponseAsync(string url,
|
||||||
|
object? parameters = null,
|
||||||
|
bool isQueryStringParam = false,
|
||||||
|
Action<HttpRequestMessage>? requestOption = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
|
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
|
||||||
@@ -40,8 +45,8 @@
|
|||||||
/// <param name="requestOption">请求时的配置动作。</param>
|
/// <param name="requestOption">请求时的配置动作。</param>
|
||||||
/// <returns>服务端的响应结果。</returns>
|
/// <returns>服务端的响应结果。</returns>
|
||||||
ValueTask<string> GetAsync(string url,
|
ValueTask<string> GetAsync(string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
Action<HttpRequestMessage> requestOption = null);
|
Action<HttpRequestMessage>? requestOption = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
||||||
@@ -53,7 +58,7 @@
|
|||||||
/// <returns>服务端的响应结果。</returns>
|
/// <returns>服务端的响应结果。</returns>
|
||||||
ValueTask<TResponse> GetAsync<TResponse>(
|
ValueTask<TResponse> GetAsync<TResponse>(
|
||||||
string url,
|
string url,
|
||||||
object parameters = null,
|
object? parameters = null,
|
||||||
Action<HttpRequestMessage> requestOption = null);
|
Action<HttpRequestMessage>? requestOption = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,10 +1,22 @@
|
|||||||
namespace ZonyLrcTools.Common.Lyrics;
|
namespace ZonyLrcTools.Common.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 歌词下载核心逻辑的接口定义。
|
||||||
|
/// </summary>
|
||||||
public interface ILyricsDownloader
|
public interface ILyricsDownloader
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 使用给定的歌词信息下载歌词,并输出文件到指定的路径。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="needDownloadMusicInfos">需要下载的歌词信息。</param>
|
||||||
|
/// <param name="parallelCount">下载线程/并发量。</param>
|
||||||
|
/// <param name="cancellationToken">任务取消标记。</param>
|
||||||
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
||||||
int parallelCount = 2,
|
int parallelCount = 2,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目前可用的歌词下载器。
|
||||||
|
/// </summary>
|
||||||
IEnumerable<ILyricsProvider> AvailableProviders { get; }
|
IEnumerable<ILyricsProvider> AvailableProviders { get; }
|
||||||
}
|
}
|
@@ -18,6 +18,6 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||||
/// <param name="translationLyric">翻译歌词数据。</param>
|
/// <param name="translationLyric">翻译歌词数据。</param>
|
||||||
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
|
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
|
||||||
LyricsItemCollection Build(string sourceLyric, string translationLyric);
|
LyricsItemCollection Build(string? sourceLyric, string? translationLyric);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -12,7 +12,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <param name="artist">歌曲的作者。</param>
|
/// <param name="artist">歌曲的作者。</param>
|
||||||
/// <param name="duration">歌曲的时长。</param>
|
/// <param name="duration">歌曲的时长。</param>
|
||||||
/// <returns>歌曲的歌词数据对象。</returns>
|
/// <returns>歌曲的歌词数据对象。</returns>
|
||||||
ValueTask<LyricsItemCollection> DownloadAsync(string? songName, string? artist, long? duration = null);
|
ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 下载器的名称。
|
/// 下载器的名称。
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Collections.Immutable;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ZonyLrcTools.Common.Configuration;
|
using ZonyLrcTools.Common.Configuration;
|
||||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
@@ -36,7 +37,7 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
public async Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
||||||
int parallelCount = 2,
|
int parallelCount = 1,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _logger.InfoAsync("开始下载歌词文件数据...");
|
await _logger.InfoAsync("开始下载歌词文件数据...");
|
||||||
@@ -66,7 +67,11 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
|
|||||||
|
|
||||||
await Task.WhenAll(downloadTasks);
|
await Task.WhenAll(downloadTasks);
|
||||||
|
|
||||||
await _logger.InfoAsync($"歌词数据下载完成,成功: {needDownloadMusicInfos.Count(m => m.IsSuccessful)} 失败{needDownloadMusicInfos.Count(m => m.IsSuccessful == false)}。");
|
var successfulCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: false });
|
||||||
|
var skippedCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: true });
|
||||||
|
var failedCount = needDownloadMusicInfos.Count(m => m.IsSuccessful == false);
|
||||||
|
await _logger.InfoAsync($"歌词数据下载完成,成功: {successfulCount} 跳过(纯音乐): {skippedCount} 失败{failedCount}。");
|
||||||
|
await LogFailedSongFilesInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"歌词下载失败列表_{DateTime.Now:yyyyMMddHHmmss}.txt"), needDownloadMusicInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DownloadAndWriteLyricsAsync(ILyricsProvider provider, MusicInfo info)
|
private async Task DownloadAndWriteLyricsAsync(ILyricsProvider provider, MusicInfo info)
|
||||||
@@ -78,6 +83,7 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
|
|||||||
if (lyrics.IsPruneMusic)
|
if (lyrics.IsPruneMusic)
|
||||||
{
|
{
|
||||||
info.IsSuccessful = true;
|
info.IsSuccessful = true;
|
||||||
|
info.IsPruneMusic = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +123,13 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
|
|||||||
private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics)
|
private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics)
|
||||||
{
|
{
|
||||||
var supportEncodings = Encoding.GetEncodings();
|
var supportEncodings = Encoding.GetEncodings();
|
||||||
|
|
||||||
|
// If the encoding is UTF-8 BOM, add the BOM to the front of the lyrics.
|
||||||
|
if (_options.Provider.Lyric.Config.FileEncoding == "utf-8-bom")
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetPreamble().Concat(lyrics.GetUtf8Bytes()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
|
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
||||||
@@ -124,4 +137,23 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
|
|||||||
|
|
||||||
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyrics.GetUtf8Bytes());
|
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyrics.GetUtf8Bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LogFailedSongFilesInfo(string outFilePath, IEnumerable<MusicInfo> musicInfos)
|
||||||
|
{
|
||||||
|
var failedSongList = musicInfos.Where(m => m.IsSuccessful == false).ToImmutableList();
|
||||||
|
if (!failedSongList.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var failedSongFiles = new StringBuilder();
|
||||||
|
failedSongFiles.AppendLine("歌曲名,歌手,路径");
|
||||||
|
|
||||||
|
foreach (var failedSongFile in failedSongList)
|
||||||
|
{
|
||||||
|
failedSongFiles.AppendLine($"{failedSongFile.Name},{failedSongFile.Artist},{failedSongFile.FilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(outFilePath, failedSongFiles.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词文本数据。
|
/// 歌词文本数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LyricText { get; }
|
public string? LyricText { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌词所在的时间(分)。
|
/// 歌词所在的时间(分)。
|
||||||
@@ -55,21 +55,21 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// <param name="minute">歌词所在的时间(分)。</param>
|
/// <param name="minute">歌词所在的时间(分)。</param>
|
||||||
/// <param name="second">歌词所在的时间(秒)。</param>
|
/// <param name="second">歌词所在的时间(秒)。</param>
|
||||||
/// <param name="lyricText">歌词文本数据。</param>
|
/// <param name="lyricText">歌词文本数据。</param>
|
||||||
public LyricsItem(int minute, double second, string lyricText)
|
public LyricsItem(int minute, double second, string? lyricText)
|
||||||
{
|
{
|
||||||
Minute = minute;
|
Minute = minute;
|
||||||
Second = second;
|
Second = second;
|
||||||
LyricText = lyricText;
|
LyricText = lyricText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CompareTo(LyricsItem other)
|
public int CompareTo(LyricsItem? other)
|
||||||
{
|
{
|
||||||
if (SortScore > other.SortScore)
|
if (SortScore > other?.SortScore)
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SortScore < other.SortScore)
|
if (SortScore < other?.SortScore)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -87,12 +87,12 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
return left.SortScore < right.SortScore;
|
return left.SortScore < right.SortScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator ==(LyricsItem left, LyricsItem right)
|
public static bool operator ==(LyricsItem? left, LyricsItem? right)
|
||||||
{
|
{
|
||||||
return (int?)left?.SortScore == (int?)right?.SortScore;
|
return (int?)left?.SortScore == (int?)right?.SortScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator !=(LyricsItem item1, LyricsItem item2)
|
public static bool operator !=(LyricsItem? item1, LyricsItem? item2)
|
||||||
{
|
{
|
||||||
return !(item1 == item2);
|
return !(item1 == item2);
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second);
|
return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(null, obj)) return false;
|
if (ReferenceEquals(null, obj)) return false;
|
||||||
if (ReferenceEquals(this, obj)) return true;
|
if (ReferenceEquals(this, obj)) return true;
|
||||||
|
@@ -14,7 +14,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPruneMusic => Count == 0;
|
public bool IsPruneMusic => Count == 0;
|
||||||
|
|
||||||
public GlobalLyricsConfigOptions? Options { get; private set; }
|
public GlobalLyricsConfigOptions? Options { get; }
|
||||||
|
|
||||||
public LyricsItemCollection(GlobalLyricsConfigOptions? options)
|
public LyricsItemCollection(GlobalLyricsConfigOptions? options)
|
||||||
{
|
{
|
||||||
@@ -29,6 +29,11 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
}
|
}
|
||||||
|
|
||||||
var option = left.Options;
|
var option = left.Options;
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
throw new NullReferenceException("LyricsItemCollection.Options");
|
||||||
|
}
|
||||||
|
|
||||||
var newCollection = new LyricsItemCollection(option);
|
var newCollection = new LyricsItemCollection(option);
|
||||||
var indexDiff = left.Count - right.Count;
|
var indexDiff = left.Count - right.Count;
|
||||||
if (!option.IsOneLine)
|
if (!option.IsOneLine)
|
||||||
@@ -100,6 +105,11 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
if (Options == null)
|
||||||
|
{
|
||||||
|
throw new NullReferenceException("LyricsItemCollection.Options");
|
||||||
|
}
|
||||||
|
|
||||||
var lyricBuilder = new StringBuilder();
|
var lyricBuilder = new StringBuilder();
|
||||||
ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.LineBreak));
|
ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.LineBreak));
|
||||||
return lyricBuilder.ToString().TrimEnd(Options.LineBreak);
|
return lyricBuilder.ToString().TrimEnd(Options.LineBreak);
|
||||||
|
@@ -30,7 +30,7 @@ namespace ZonyLrcTools.Common.Lyrics
|
|||||||
return lyric;
|
return lyric;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LyricsItemCollection Build(string sourceLyric, string translationLyric)
|
public LyricsItemCollection Build(string? sourceLyric, string? translationLyric)
|
||||||
{
|
{
|
||||||
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||||
if (string.IsNullOrEmpty(sourceLyric))
|
if (string.IsNullOrEmpty(sourceLyric))
|
||||||
|
@@ -10,9 +10,9 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
|||||||
|
|
||||||
[JsonProperty("client")] public string UnknownParameters3 { get; }
|
[JsonProperty("client")] public string UnknownParameters3 { get; }
|
||||||
|
|
||||||
[JsonProperty("hash")] public string FileHash { get; }
|
[JsonProperty("hash")] public string? FileHash { get; }
|
||||||
|
|
||||||
public GetLyricAccessKeyRequest(string fileHash)
|
public GetLyricAccessKeyRequest(string? fileHash)
|
||||||
{
|
{
|
||||||
UnknownParameters1 = 1;
|
UnknownParameters1 = 1;
|
||||||
UnknownParameters2 = "yes";
|
UnknownParameters2 = "yes";
|
||||||
|
@@ -8,13 +8,13 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
|||||||
|
|
||||||
[JsonProperty("errcode")] public int ErrorCode { get; set; }
|
[JsonProperty("errcode")] public int ErrorCode { get; set; }
|
||||||
|
|
||||||
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject> AccessKeyDataObjects { get; set; }
|
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject>? AccessKeyDataObjects { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetLyricAccessKeyDataObject
|
public class GetLyricAccessKeyDataObject
|
||||||
{
|
{
|
||||||
[JsonProperty("accesskey")] public string AccessKey { get; set; }
|
[JsonProperty("accesskey")] public string? AccessKey { get; set; }
|
||||||
|
|
||||||
[JsonProperty("id")] public string Id { get; set; }
|
[JsonProperty("id")] public string? Id { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -12,11 +12,11 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
|||||||
|
|
||||||
[JsonProperty("charset")] public string UnknownParameters4 { get; }
|
[JsonProperty("charset")] public string UnknownParameters4 { get; }
|
||||||
|
|
||||||
[JsonProperty("id")] public string Id { get; }
|
[JsonProperty("id")] public string? Id { get; }
|
||||||
|
|
||||||
[JsonProperty("accesskey")] public string AccessKey { get; }
|
[JsonProperty("accesskey")] public string? AccessKey { get; }
|
||||||
|
|
||||||
public GetLyricRequest(string id, string accessKey)
|
public GetLyricRequest(string? id, string? accessKey)
|
||||||
{
|
{
|
||||||
UnknownParameters1 = 1;
|
UnknownParameters1 = 1;
|
||||||
UnknownParameters2 = "iphone";
|
UnknownParameters2 = "iphone";
|
||||||
|
@@ -6,20 +6,20 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
|||||||
{
|
{
|
||||||
[JsonProperty("status")] public int Status { get; set; }
|
[JsonProperty("status")] public int Status { get; set; }
|
||||||
|
|
||||||
[JsonProperty("data")] public SongSearchResponseInnerData Data { get; set; }
|
[JsonProperty("data")] public SongSearchResponseInnerData? Data { get; set; }
|
||||||
|
|
||||||
[JsonProperty("error_code")] public int ErrorCode { get; set; }
|
[JsonProperty("error_code")] public int ErrorCode { get; set; }
|
||||||
|
|
||||||
[JsonProperty("error_msg")] public string ErrorMessage { get; set; }
|
[JsonProperty("error_msg")] public string? ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SongSearchResponseInnerData
|
public class SongSearchResponseInnerData
|
||||||
{
|
{
|
||||||
[JsonProperty("lists")] public List<SongSearchResponseSongDetail> List { get; set; }
|
[JsonProperty("lists")] public List<SongSearchResponseSongDetail>? List { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SongSearchResponseSongDetail
|
public class SongSearchResponseSongDetail
|
||||||
{
|
{
|
||||||
public string FileHash { get; set; }
|
public string? FileHash { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -38,7 +38,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
|
|||||||
|
|
||||||
// 获得特殊的 AccessToken 与 Id,真正请求歌词数据。
|
// 获得特殊的 AccessToken 与 Id,真正请求歌词数据。
|
||||||
var accessKeyResponse = await _warpHttpClient.GetAsync<GetLyricAccessKeyResponse>(KuGouGetLyricAccessKeyUrl,
|
var accessKeyResponse = await _warpHttpClient.GetAsync<GetLyricAccessKeyResponse>(KuGouGetLyricAccessKeyUrl,
|
||||||
new GetLyricAccessKeyRequest(searchResult.Data.List[0].FileHash));
|
new GetLyricAccessKeyRequest(searchResult.Data?.List?[0].FileHash));
|
||||||
|
|
||||||
|
if (accessKeyResponse.AccessKeyDataObjects == null || accessKeyResponse.AccessKeyDataObjects.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||||
|
}
|
||||||
|
|
||||||
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
|
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
|
||||||
return await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
|
return await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
|
||||||
@@ -49,18 +54,18 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
|
|||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
var lyricJsonObj = JObject.Parse((data as string)!);
|
var lyricJsonObj = JObject.Parse((data as string)!);
|
||||||
if (lyricJsonObj.SelectToken("$.status").Value<int>() != 200)
|
if (lyricJsonObj.SelectToken("$.status")?.Value<int>() != 200)
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content").Value<string>()));
|
var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content")?.Value<string>() ?? string.Empty));
|
||||||
return _lyricsItemCollectionFactory.Build(lyricText);
|
return _lyricsItemCollectionFactory.Build(lyricText);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
if (response.ErrorCode != 0 && response.Status != 1 || response.Data.List == null)
|
if ((response.ErrorCode != 0 && response.Status != 1) || response.Data?.List?.Count == 0)
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ public class GetLyricsResponse
|
|||||||
{
|
{
|
||||||
[JsonProperty("status")] public int Status { get; set; }
|
[JsonProperty("status")] public int Status { get; set; }
|
||||||
|
|
||||||
[JsonProperty("data")] public GetLyricsResponseInnerData Data { get; set; }
|
[JsonProperty("data")] public GetLyricsResponseInnerData? Data { get; set; }
|
||||||
|
|
||||||
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
|
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
@@ -15,12 +15,12 @@ public class GetLyricsResponse
|
|||||||
|
|
||||||
public class GetLyricsResponseInnerData
|
public class GetLyricsResponseInnerData
|
||||||
{
|
{
|
||||||
[JsonProperty("lrclist")] public ICollection<GetLyricsItem> Lyrics { get; set; }
|
[JsonProperty("lrclist")] public ICollection<GetLyricsItem>? Lyrics { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetLyricsItem
|
public class GetLyricsItem
|
||||||
{
|
{
|
||||||
[JsonProperty("lineLyric")] public string Text { get; set; }
|
[JsonProperty("lineLyric")] public string? Text { get; set; }
|
||||||
|
|
||||||
[JsonProperty("time")] public string Position { get; set; }
|
[JsonProperty("time")] public string Position { get; set; } = null!;
|
||||||
}
|
}
|
@@ -4,13 +4,49 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
|||||||
|
|
||||||
public class SongSearchRequest
|
public class SongSearchRequest
|
||||||
{
|
{
|
||||||
[JsonProperty("key")] public string Keyword { get; set; }
|
[JsonProperty("all")] public string Keyword { get; set; }
|
||||||
|
|
||||||
[JsonProperty("pn")] public int PageNumber { get; }
|
[JsonProperty("pn")] public int PageNumber { get; }
|
||||||
|
|
||||||
[JsonProperty("rn")] public int PageSize { get; }
|
[JsonProperty("rn")] public int PageSize { get; }
|
||||||
|
|
||||||
public SongSearchRequest(string name, string artist, int pageNumber = 1, int pageSize = 20)
|
[JsonProperty("ft")] public string Unknown1 { get; } = "music";
|
||||||
|
|
||||||
|
[JsonProperty("newsearch")] public string Unknown2 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("alflac")] public string Unknown3 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("itemset")] public string Unknown4 { get; } = "web_2013";
|
||||||
|
|
||||||
|
[JsonProperty("client")] public string Unknown5 { get; } = "kt";
|
||||||
|
|
||||||
|
[JsonProperty("cluster")] public string Unknown6 { get; } = "0";
|
||||||
|
|
||||||
|
[JsonProperty("vermerge")] public string Unknown7 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("rformat")] public string Unknown8 { get; } = "json";
|
||||||
|
|
||||||
|
[JsonProperty("encoding")] public string Unknown9 { get; } = "utf8";
|
||||||
|
|
||||||
|
[JsonProperty("show_copyright_off")] public string Unknown10 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("pcmp4")] public string Unknown11 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("ver")] public string Unknown12 { get; } = "mbox";
|
||||||
|
|
||||||
|
[JsonProperty("plat")] public string Unknown13 { get; } = "pc";
|
||||||
|
|
||||||
|
[JsonProperty("vipver")] public string Unknown14 { get; } = "MUSIC_9.2.0.0_W6";
|
||||||
|
|
||||||
|
[JsonProperty("devid")] public string Unknown15 { get; } = "11404450";
|
||||||
|
|
||||||
|
[JsonProperty("newver")] public string Unknown16 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("issubtitle")] public string Unknown17 { get; } = "1";
|
||||||
|
|
||||||
|
[JsonProperty("pcjson")] public string Unknown18 { get; } = "1";
|
||||||
|
|
||||||
|
public SongSearchRequest(string name, string artist, int pageNumber = 0, int pageSize = 20)
|
||||||
{
|
{
|
||||||
Keyword = $"{name} {artist}";
|
Keyword = $"{name} {artist}";
|
||||||
PageNumber = pageNumber;
|
PageNumber = pageNumber;
|
||||||
|
@@ -4,15 +4,14 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
|||||||
|
|
||||||
public class SongSearchResponse
|
public class SongSearchResponse
|
||||||
{
|
{
|
||||||
[JsonProperty("code")] public int Code { get; set; }
|
[JsonProperty("TOTAL")] public int TotalCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty("data")] public SongSearchResponseInnerData InnerData { get; set; }
|
[JsonProperty("abslist")]
|
||||||
|
public IList<SongSearchResponseSongDetail> SongList { get; set; }
|
||||||
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
|
|
||||||
|
|
||||||
public long GetMatchedMusicId(string musicName, string artistName, long? duration)
|
public long GetMatchedMusicId(string musicName, string artistName, long? duration)
|
||||||
{
|
{
|
||||||
var prefectMatch = InnerData.SongItems.FirstOrDefault(x => x.Name == musicName && x.Artist == artistName);
|
var prefectMatch = SongList.FirstOrDefault(x => x.Name == musicName && x.Artist == artistName);
|
||||||
if (prefectMatch != null)
|
if (prefectMatch != null)
|
||||||
{
|
{
|
||||||
return prefectMatch.MusicId;
|
return prefectMatch.MusicId;
|
||||||
@@ -20,27 +19,42 @@ public class SongSearchResponse
|
|||||||
|
|
||||||
if (duration is null or 0)
|
if (duration is null or 0)
|
||||||
{
|
{
|
||||||
return InnerData.SongItems.First().MusicId;
|
return SongList.First().MusicId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return InnerData.SongItems.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().MusicId;
|
return SongList.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().MusicId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SongSearchResponseInnerData
|
public class SongSearchResponseSongDetail
|
||||||
{
|
{
|
||||||
[JsonProperty("total")] public string Total { get; set; }
|
/// <summary>
|
||||||
|
/// 专辑名称。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("ALBUM")]
|
||||||
|
public string Album { get; set; }
|
||||||
|
|
||||||
[JsonProperty("list")] public ICollection<SongSearchResponseDetail> SongItems { get; set; }
|
/// <summary>
|
||||||
}
|
/// 歌手名称。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("ARTIST")]
|
||||||
|
public string Artist { get; set; }
|
||||||
|
|
||||||
public class SongSearchResponseDetail
|
/// <summary>
|
||||||
{
|
/// 歌曲名称。
|
||||||
[JsonProperty("artist")] public string? Artist { get; set; }
|
/// </summary>
|
||||||
|
[JsonProperty("SONGNAME")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
[JsonProperty("name")] public string? Name { get; set; }
|
/// <summary>
|
||||||
|
/// 歌曲的 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("DC_TARGETID")]
|
||||||
|
public long MusicId { get; set; }
|
||||||
|
|
||||||
[JsonProperty("rid")] public long MusicId { get; set; }
|
/// <summary>
|
||||||
|
/// 歌曲的时间长度。
|
||||||
[JsonProperty("duration")] public long Duration { get; set; }
|
/// </summary>
|
||||||
|
[JsonProperty("DURATION")]
|
||||||
|
public long Duration { get; set; }
|
||||||
}
|
}
|
@@ -13,29 +13,27 @@ public class KuWoLyricsProvider : LyricsProvider
|
|||||||
{
|
{
|
||||||
public override string DownloaderName => InternalLyricsProviderNames.KuWo;
|
public override string DownloaderName => InternalLyricsProviderNames.KuWo;
|
||||||
|
|
||||||
private const string KuWoSearchMusicUrl = @"https://www.kuwo.cn/api/www/search/searchMusicBykeyWord";
|
private const string KuWoSearchMusicUrl = @"https://search.kuwo.cn/r.s";
|
||||||
private const string KuWoSearchLyricsUrl = @"https://m.kuwo.cn/newh5/singles/songinfoandlrc";
|
private const string KuWoSearchLyricsUrl = @"https://m.kuwo.cn/newh5/singles/songinfoandlrc";
|
||||||
private const string KuWoDefaultToken = "ABCDE12345";
|
private const string KuWoDefaultToken = "ABCDE12345";
|
||||||
|
|
||||||
private readonly IWarpHttpClient _warpHttpClient;
|
private readonly IWarpHttpClient _warpHttpClient;
|
||||||
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
|
|
||||||
private readonly GlobalOptions _options;
|
private readonly GlobalOptions _options;
|
||||||
|
|
||||||
private static readonly ProductInfoHeaderValue UserAgent = new("Chrome", "81.0.4044.138");
|
private static readonly ProductInfoHeaderValue UserAgent = new("Chrome", "81.0.4044.138");
|
||||||
|
|
||||||
public KuWoLyricsProvider(IWarpHttpClient warpHttpClient,
|
public KuWoLyricsProvider(IWarpHttpClient warpHttpClient,
|
||||||
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
|
|
||||||
IOptions<GlobalOptions> options)
|
IOptions<GlobalOptions> options)
|
||||||
{
|
{
|
||||||
_warpHttpClient = warpHttpClient;
|
_warpHttpClient = warpHttpClient;
|
||||||
_lyricsItemCollectionFactory = lyricsItemCollectionFactory;
|
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
var songSearchResponse = await _warpHttpClient.GetAsync<SongSearchResponse>(KuWoSearchMusicUrl,
|
var songSearchResponse = await _warpHttpClient.GetAsync<SongSearchResponse>(KuWoSearchMusicUrl,
|
||||||
new SongSearchRequest(args.SongName, args.Artist, pageSize: _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
new SongSearchRequest(args.SongName, args.Artist,
|
||||||
|
pageSize: _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
||||||
op =>
|
op =>
|
||||||
{
|
{
|
||||||
op.Headers.UserAgent.Add(UserAgent);
|
op.Headers.UserAgent.Add(UserAgent);
|
||||||
@@ -55,16 +53,23 @@ public class KuWoLyricsProvider : LyricsProvider
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args)
|
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject,
|
||||||
|
LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|
||||||
var lyricsResponse = (GetLyricsResponse)lyricsObject;
|
var lyricsResponse = (GetLyricsResponse)lyricsObject;
|
||||||
|
if (lyricsResponse.Data?.Lyrics == null)
|
||||||
|
{
|
||||||
|
return new LyricsItemCollection(null);
|
||||||
|
}
|
||||||
|
|
||||||
var items = lyricsResponse.Data.Lyrics.Select(l =>
|
var items = lyricsResponse.Data.Lyrics.Select(l =>
|
||||||
{
|
{
|
||||||
var position = double.Parse(l.Position);
|
var position = double.Parse(l.Position);
|
||||||
var positionSpan = TimeSpan.FromSeconds(position);
|
var positionSpan = TimeSpan.FromSeconds(position);
|
||||||
return new LyricsItem(positionSpan.Minutes, double.Parse($"{positionSpan.Seconds}.{positionSpan.Milliseconds}"), l.Text);
|
return new LyricsItem(positionSpan.Minutes,
|
||||||
|
double.Parse($"{positionSpan.Seconds}.{positionSpan.Milliseconds}"), l.Text);
|
||||||
});
|
});
|
||||||
|
|
||||||
var lyricsItemCollection = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
var lyricsItemCollection = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||||
@@ -74,12 +79,7 @@ public class KuWoLyricsProvider : LyricsProvider
|
|||||||
|
|
||||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
if (response.Code != 200)
|
if (response.TotalCount == 0)
|
||||||
{
|
|
||||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, response.ErrorMessage, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.InnerData.SongItems.Count == 0)
|
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
{
|
{
|
||||||
public GetLyricRequest(long songId)
|
public GetLyricRequest(long songId)
|
||||||
{
|
{
|
||||||
OS = "ios";
|
OS = "pc";
|
||||||
Id = songId;
|
Id = songId;
|
||||||
Lv = Kv = Tv = Rv = -1;
|
Lv = Kv = Tv = Rv = -1;
|
||||||
}
|
}
|
||||||
|
@@ -8,31 +8,31 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// 原始的歌词。
|
/// 原始的歌词。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("lrc")]
|
[JsonProperty("lrc")]
|
||||||
public InnerLyric OriginalLyric { get; set; }
|
public InnerLyric? OriginalLyric { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 卡拉 OK 歌词。
|
/// 卡拉 OK 歌词。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("klyric")]
|
[JsonProperty("klyric")]
|
||||||
public InnerLyric KaraokeLyric { get; set; }
|
public InnerLyric? KaraokeLyric { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 如果存在翻译歌词,则本项内容为翻译歌词。
|
/// 如果存在翻译歌词,则本项内容为翻译歌词。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("tlyric")]
|
[JsonProperty("tlyric")]
|
||||||
public InnerLyric TranslationLyric { get; set; }
|
public InnerLyric? TranslationLyric { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
|
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("romalrc")]
|
[JsonProperty("romalrc")]
|
||||||
public InnerLyric RomaLyric { get; set; }
|
public InnerLyric? RomaLyric { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 状态码。
|
/// 状态码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("code")]
|
[JsonProperty("code")]
|
||||||
public string StatusCode { get; set; }
|
public string? StatusCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,12 +40,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class InnerLyric
|
public class InnerLyric
|
||||||
{
|
{
|
||||||
[JsonProperty("version")] public string Version { get; set; }
|
[JsonProperty("version")] public string? Version { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 具体的歌词数据。
|
/// 具体的歌词数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("lyric")]
|
[JsonProperty("lyric")]
|
||||||
public string Text { get; set; }
|
public string? Text { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -45,22 +45,19 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
|
|
||||||
[JsonProperty("crypto")] public string Crypto { get; set; } = "weapi";
|
[JsonProperty("crypto")] public string Crypto { get; set; } = "weapi";
|
||||||
|
|
||||||
public SongSearchRequest()
|
public SongSearchRequest(string musicName, string artistName, int limit = 10)
|
||||||
{
|
{
|
||||||
CsrfToken = string.Empty;
|
CsrfToken = string.Empty;
|
||||||
Type = 1;
|
Type = 1;
|
||||||
Offset = 0;
|
Offset = 0;
|
||||||
IsTotal = true;
|
IsTotal = true;
|
||||||
Limit = 10;
|
Limit = 10;
|
||||||
}
|
|
||||||
|
|
||||||
public SongSearchRequest(string musicName, string artistName, int limit = 10) : this()
|
|
||||||
{
|
|
||||||
// Remove all the brackets and the content inside them.
|
// Remove all the brackets and the content inside them.
|
||||||
var regex = new Regex(@"\([^)]*\)");
|
var regex = new Regex(@"\([^)]*\)");
|
||||||
musicName = regex.Replace(musicName, string.Empty);
|
musicName = regex.Replace(musicName, string.Empty);
|
||||||
|
|
||||||
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
|
SearchKey = $"{musicName}+{artistName}";
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
{
|
{
|
||||||
public class SongSearchResponse
|
public class SongSearchResponse
|
||||||
{
|
{
|
||||||
[JsonProperty("result")] public InnerListItemModel Items { get; set; }
|
[JsonProperty("result")] public InnerListItemModel Items { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("code")] public int StatusCode { get; set; }
|
[JsonProperty("code")] public int StatusCode { get; set; }
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
|
|
||||||
public class InnerListItemModel
|
public class InnerListItemModel
|
||||||
{
|
{
|
||||||
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; }
|
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("songCount")] public int SongCount { get; set; }
|
[JsonProperty("songCount")] public int SongCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// 歌曲的名称。
|
/// 歌曲的名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("name")]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌曲的 Sid (Song Id)。
|
/// 歌曲的 Sid (Song Id)。
|
||||||
@@ -50,13 +50,13 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// 歌曲的演唱者。
|
/// 歌曲的演唱者。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("artists")]
|
[JsonProperty("artists")]
|
||||||
public IList<SongArtistModel> Artists { get; set; }
|
public IList<SongArtistModel>? Artists { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌曲的专辑信息。
|
/// 歌曲的专辑信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("album")]
|
[JsonProperty("album")]
|
||||||
public SongAlbumModel Album { get; set; }
|
public SongAlbumModel? Album { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌曲的实际长度。
|
/// 歌曲的实际长度。
|
||||||
@@ -71,7 +71,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// 歌手/艺术家的名称。
|
/// 歌手/艺术家的名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("name")]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SongAlbumModel
|
public class SongAlbumModel
|
||||||
@@ -80,12 +80,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
|||||||
/// 专辑的名称。
|
/// 专辑的名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("name")]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 专辑图像的 Url 地址。
|
/// 专辑图像的 Url 地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("img1v1Url")]
|
[JsonProperty("img1v1Url")]
|
||||||
public string PictureUrl { get; set; }
|
public string? PictureUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,6 +2,7 @@ using System.Net.Http.Headers;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ZonyLrcTools.Common.Configuration;
|
using ZonyLrcTools.Common.Configuration;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||||
using ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel;
|
using ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel;
|
||||||
@@ -16,11 +17,10 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
|
|||||||
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
|
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
|
||||||
private readonly GlobalOptions _options;
|
private readonly GlobalOptions _options;
|
||||||
|
|
||||||
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
|
private const string NetEaseSearchMusicUrl = @"https://music.163.com/weapi/search/get";
|
||||||
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
|
private const string NetEaseGetLyricUrl = @"https://music.163.com/weapi/song/lyric?csrf_token=";
|
||||||
|
|
||||||
private const string NetEaseRequestReferer = @"https://music.163.com";
|
private const string NetEaseRequestReferer = @"https://music.163.com";
|
||||||
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
|
|
||||||
|
|
||||||
public NetEaseLyricsProvider(IWarpHttpClient warpHttpClient,
|
public NetEaseLyricsProvider(IWarpHttpClient warpHttpClient,
|
||||||
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
|
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
|
||||||
@@ -33,25 +33,30 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
|
|||||||
|
|
||||||
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
|
var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16);
|
||||||
NetEaseSearchMusicUrl,
|
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
|
||||||
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
|
||||||
true,
|
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(NetEaseSearchMusicUrl,
|
||||||
msg =>
|
requestOption: request =>
|
||||||
{
|
{
|
||||||
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
|
request.Headers.Referrer = new Uri(NetEaseRequestReferer);
|
||||||
if (msg.Content != null)
|
request.Content = new FormUrlEncodedContent(HandleRequest(
|
||||||
{
|
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
||||||
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
|
secretKey,
|
||||||
}
|
encSecKey));
|
||||||
});
|
});
|
||||||
|
|
||||||
ValidateSongSearchResponse(searchResult, args);
|
ValidateSongSearchResponse(searchResult, args);
|
||||||
|
|
||||||
return await _warpHttpClient.GetAsync(
|
return await _warpHttpClient.PostAsync(NetEaseGetLyricUrl,
|
||||||
NetEaseGetLyricUrl,
|
requestOption: request =>
|
||||||
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName, args.Duration)),
|
{
|
||||||
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
|
request.Headers.Referrer = new Uri(NetEaseRequestReferer);
|
||||||
|
request.Content = new FormUrlEncodedContent(HandleRequest(
|
||||||
|
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName, args.Duration)),
|
||||||
|
secretKey,
|
||||||
|
encSecKey));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args)
|
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args)
|
||||||
@@ -71,7 +76,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
|
|||||||
|
|
||||||
return _lyricsItemCollectionFactory.Build(
|
return _lyricsItemCollectionFactory.Build(
|
||||||
json.OriginalLyric.Text,
|
json.OriginalLyric.Text,
|
||||||
json.TranslationLyric.Text);
|
json.TranslationLyric?.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||||
@@ -81,10 +86,23 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
|
|||||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.Items?.SongCount <= 0)
|
if (response.Items is not { SongCount: > 0 })
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> HandleRequest(object srcParams, string secretKey, string encSecKey)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"params", NetEaseMusicEncryptionHelper.AesEncode(
|
||||||
|
NetEaseMusicEncryptionHelper.AesEncode(
|
||||||
|
JsonConvert.SerializeObject(srcParams), NetEaseMusicEncryptionHelper.Nonce), secretKey)
|
||||||
|
},
|
||||||
|
{ "encSecKey", encSecKey }
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -16,10 +16,6 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
|
|||||||
|
|
||||||
[JsonProperty("g_tk")] public int Gtk { get; set; }
|
[JsonProperty("g_tk")] public int Gtk { get; set; }
|
||||||
|
|
||||||
protected GetLyricRequest()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetLyricRequest(string? songId)
|
public GetLyricRequest(string? songId)
|
||||||
{
|
{
|
||||||
IsNoBase64Encoding = 1;
|
IsNoBase64Encoding = 1;
|
||||||
|
@@ -19,8 +19,8 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
|
|||||||
public string Platform { get; protected set; }
|
public string Platform { get; protected set; }
|
||||||
|
|
||||||
[JsonProperty("key")]
|
[JsonProperty("key")]
|
||||||
public string Keyword { get; protected set; }
|
public string Keyword { get; protected set; } = null!;
|
||||||
|
|
||||||
protected SongSearchRequest()
|
protected SongSearchRequest()
|
||||||
{
|
{
|
||||||
Format = "json";
|
Format = "json";
|
||||||
|
@@ -6,21 +6,21 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
|
|||||||
{
|
{
|
||||||
[JsonProperty("code")] public int StatusCode { get; set; }
|
[JsonProperty("code")] public int StatusCode { get; set; }
|
||||||
|
|
||||||
[JsonProperty("data")] public QQMusicInnerDataModel Data { get; set; }
|
[JsonProperty("data")] public QQMusicInnerDataModel? Data { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class QQMusicInnerDataModel
|
public class QQMusicInnerDataModel
|
||||||
{
|
{
|
||||||
[JsonProperty("song")] public QQMusicInnerSongModel Song { get; set; }
|
[JsonProperty("song")] public QQMusicInnerSongModel? Song { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class QQMusicInnerSongModel
|
public class QQMusicInnerSongModel
|
||||||
{
|
{
|
||||||
[JsonProperty("itemlist")] public List<QQMusicInnerSongItem> SongItems { get; set; }
|
[JsonProperty("itemlist")] public List<QQMusicInnerSongItem>? SongItems { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class QQMusicInnerSongItem
|
public class QQMusicInnerSongItem
|
||||||
{
|
{
|
||||||
[JsonProperty("mid")] public string SongId { get; set; }
|
[JsonProperty("mid")] public string? SongId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -35,7 +35,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
|
|||||||
ValidateSongSearchResponse(searchResult, args);
|
ValidateSongSearchResponse(searchResult, args);
|
||||||
|
|
||||||
return await _warpHttpClient.GetAsync(QQGetLyricUrl,
|
return await _warpHttpClient.GetAsync(QQGetLyricUrl,
|
||||||
new GetLyricRequest(searchResult.Data.Song.SongItems.FirstOrDefault()?.SongId),
|
new GetLyricRequest(searchResult.Data?.Song?.SongItems?.FirstOrDefault()?.SongId),
|
||||||
op => op.Headers.Referrer = new Uri(QQMusicRequestReferer));
|
op => op.Headers.Referrer = new Uri(QQMusicRequestReferer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,15 +57,15 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lyricJsonObj = JObject.Parse(lyricJsonString);
|
var lyricJsonObj = JObject.Parse(lyricJsonString);
|
||||||
var sourceLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.lyric").Value<string>()));
|
var sourceLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.lyric")!.Value<string>()));
|
||||||
var translateLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.trans").Value<string>()));
|
var translateLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.trans")!.Value<string>()));
|
||||||
|
|
||||||
return _lyricsItemCollectionFactory.Build(sourceLyric, translateLyric);
|
return _lyricsItemCollectionFactory.Build(sourceLyric, translateLyric);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||||
{
|
{
|
||||||
if (response is not { StatusCode: 0 } || response.Data.Song.SongItems == null)
|
if (response is not { StatusCode: 0 } || response.Data?.Song?.SongItems == null)
|
||||||
{
|
{
|
||||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
|
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ namespace ZonyLrcTools.Common.MusicDecryption
|
|||||||
await file.CopyToAsync(buffer);
|
await file.CopyToAsync(buffer);
|
||||||
|
|
||||||
using var decrypto = DecryptoFactory.Create(buffer, Path.GetFileName(filePath), message => { });
|
using var decrypto = DecryptoFactory.Create(buffer, Path.GetFileName(filePath), message => { });
|
||||||
var outFileName = decrypto.Decrypt().NewName;
|
var outFileName = (await decrypto.DecryptAsync()).NewName;
|
||||||
var outFilePath = Path.Combine(Path.GetDirectoryName(filePath)!, outFileName);
|
var outFilePath = Path.Combine(Path.GetDirectoryName(filePath)!, outFileName);
|
||||||
|
|
||||||
if (!File.Exists(outFilePath))
|
if (!File.Exists(outFilePath))
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace ZonyLrcTools.Common
|
namespace ZonyLrcTools.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌曲信息的承载类,携带歌曲的相关数据。
|
/// 歌曲信息的承载类,携带歌曲的相关数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MusicInfo
|
public partial class MusicInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 歌曲对应的物理文件路径。
|
/// 歌曲对应的物理文件路径。
|
||||||
@@ -30,6 +32,11 @@ namespace ZonyLrcTools.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSuccessful { get; set; } = true;
|
public bool IsSuccessful { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否时纯音乐?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPruneMusic { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构建一个新的 <see cref="MusicInfo"/> 对象。
|
/// 构建一个新的 <see cref="MusicInfo"/> 对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -38,9 +45,17 @@ namespace ZonyLrcTools.Common
|
|||||||
/// <param name="artist">歌曲的作者。</param>
|
/// <param name="artist">歌曲的作者。</param>
|
||||||
public MusicInfo(string filePath, string name, string artist)
|
public MusicInfo(string filePath, string name, string artist)
|
||||||
{
|
{
|
||||||
FilePath = filePath;
|
FilePath = Path.Combine(Path.GetDirectoryName(filePath)!, HandleInvalidFilePath(Path.GetFileName(filePath)));
|
||||||
Name = name;
|
Name = name;
|
||||||
Artist = artist;
|
Artist = artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string HandleInvalidFilePath(string srcText)
|
||||||
|
{
|
||||||
|
return InvalidFilePathRegex().Replace(srcText, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"[<>:""/\\|?*]")]
|
||||||
|
private static partial Regex InvalidFilePathRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
32
src/ZonyLrcTools.Common/MusicScanner/CsvFileMusicScanner.cs
Normal file
32
src/ZonyLrcTools.Common/MusicScanner/CsvFileMusicScanner.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Common.MusicScanner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 CSV 文件的音乐信息扫描器。
|
||||||
|
/// </summary>
|
||||||
|
public class CsvFileMusicScanner : ITransientDependency
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Csv 文件中获取需要下载的歌曲信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="csvFilePath">CSV 文件的路径。</param>
|
||||||
|
/// <param name="outputDirectory">歌词文件的输出目录。</param>
|
||||||
|
/// <param name="pattern">输出的歌词文件格式,默认是 "{Artist} - {Title}.lrc" 的形式。</param>
|
||||||
|
public async Task<List<MusicInfo>> GetMusicInfoFromCsvFileAsync(string csvFilePath, string outputDirectory, string pattern)
|
||||||
|
{
|
||||||
|
var csvFileContent = await File.ReadAllTextAsync(csvFilePath);
|
||||||
|
var csvFileLines = csvFileContent.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
return csvFileLines.Skip(1).Select(line => GetMusicInfoFromCsvFileLine(line, outputDirectory, pattern)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MusicInfo GetMusicInfoFromCsvFileLine(string csvFileLine, string outputDirectory, string pattern)
|
||||||
|
{
|
||||||
|
var csvFileLineItems = csvFileLine.Split(',');
|
||||||
|
var name = csvFileLineItems[0];
|
||||||
|
var artist = csvFileLineItems[1];
|
||||||
|
var fakeFilePath = Path.Combine(outputDirectory, pattern.Replace("{Name}", name).Replace("{Artist}", artist));
|
||||||
|
var musicInfo = new MusicInfo(fakeFilePath, name, artist);
|
||||||
|
return musicInfo;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,77 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Common.MusicScanner.JsonModel;
|
||||||
|
|
||||||
|
public sealed class GetMusicInfoFromNetEaseMusicSongListResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 请求结果代码,为 200 时请求成功。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("code")]
|
||||||
|
public int Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 歌单信息。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("playlist")]
|
||||||
|
public PlayListModel? PlayList { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayListModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 歌单的歌曲列表。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("tracks")]
|
||||||
|
public ICollection<PlayListSongModel>? SongList { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayListSongModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 歌曲的名称。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 歌曲的艺术家信息,可能会有多位艺术家/歌手。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("ar")]
|
||||||
|
[JsonConverter(typeof(PlayListSongArtistModelJsonConverter))]
|
||||||
|
public ICollection<PlayListSongArtistModel>? Artists { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayListSongArtistModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 艺术家的名称。
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayListSongArtistModelJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
return token.Type switch
|
||||||
|
{
|
||||||
|
JTokenType.Array => token.ToObject(objectType),
|
||||||
|
JTokenType.Object => new List<PlayListSongArtistModel> { token.ToObject<PlayListSongArtistModel>()! },
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return objectType == typeof(ICollection<PlayListSongArtistModel>);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,222 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using QRCoder;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
|
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||||
|
using ZonyLrcTools.Common.MusicScanner.JsonModel;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Common.MusicScanner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网易云歌单音乐扫描器,用于从网易云歌单获取需要下载的歌词列表。
|
||||||
|
/// </summary>
|
||||||
|
public class NetEaseMusicSongListMusicScanner : ISingletonDependency
|
||||||
|
{
|
||||||
|
private readonly IWarpHttpClient _warpHttpClient;
|
||||||
|
private readonly ILogger<NetEaseMusicSongListMusicScanner> _logger;
|
||||||
|
private const string Host = "https://music.163.com";
|
||||||
|
|
||||||
|
private string Cookie { get; set; } = string.Empty;
|
||||||
|
private string CsrfToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建一个新的 <see cref="NetEaseMusicSongListMusicScanner"/> 对象。
|
||||||
|
/// </summary>
|
||||||
|
public NetEaseMusicSongListMusicScanner(IWarpHttpClient warpHttpClient,
|
||||||
|
ILogger<NetEaseMusicSongListMusicScanner> logger)
|
||||||
|
{
|
||||||
|
_warpHttpClient = warpHttpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从网易云歌单获取需要下载的歌曲列表,调用这个 API 需要用户登录,否则获取的歌单数据不全。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="songListIds">网易云音乐歌单的 ID。</param>
|
||||||
|
/// <param name="outputDirectory">歌词文件的输出路径。</param>
|
||||||
|
/// <param name="pattern">输出的歌词文件格式,默认是 "{Artist} - {Title}.lrc" 的形式。</param>
|
||||||
|
/// <returns>返回获取到的歌曲列表。</returns>
|
||||||
|
public async Task<List<MusicInfo>> GetMusicInfoFromNetEaseMusicSongListAsync(string songListIds, string outputDirectory, string pattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Cookie))
|
||||||
|
{
|
||||||
|
var loginResponse = await LoginViqQrCodeAsync();
|
||||||
|
Cookie = loginResponse.cookieContainer?.GetCookieHeader(new Uri(Host)) ?? string.Empty;
|
||||||
|
CsrfToken = loginResponse.csrfToken ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<List<MusicInfo>> GetMusicInfoBySongIdAsync(string songId)
|
||||||
|
{
|
||||||
|
var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16);
|
||||||
|
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
|
||||||
|
var response = await _warpHttpClient.PostAsync<GetMusicInfoFromNetEaseMusicSongListResponse>(
|
||||||
|
$"{Host}/weapi/v6/playlist/detail?csrf_token={CsrfToken}", requestOption:
|
||||||
|
request =>
|
||||||
|
{
|
||||||
|
request.Headers.Add("Cookie", Cookie);
|
||||||
|
request.Content = new FormUrlEncodedContent(HandleRequest(new
|
||||||
|
{
|
||||||
|
csrf_token = CsrfToken,
|
||||||
|
id = songId,
|
||||||
|
n = 1000,
|
||||||
|
offset = 0,
|
||||||
|
total = true,
|
||||||
|
limit = 1000,
|
||||||
|
}, secretKey, encSecKey));
|
||||||
|
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.Code != 200 || response.PlayList?.SongList == null)
|
||||||
|
{
|
||||||
|
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.PlayList.SongList
|
||||||
|
.Where(song => !string.IsNullOrEmpty(song.Name))
|
||||||
|
.Select(song =>
|
||||||
|
{
|
||||||
|
var artistName = song.Artists?.FirstOrDefault()?.Name ?? string.Empty;
|
||||||
|
var fakeFilePath = Path.Combine(outputDirectory, pattern.Replace("{Name}", song.Name).Replace("{Artist}", artistName));
|
||||||
|
|
||||||
|
return new MusicInfo(fakeFilePath, song.Name!, artistName);
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var musicInfoList = new List<MusicInfo>();
|
||||||
|
foreach (var songListId in songListIds.Split(';'))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("正在获取歌单 {SongListId} 的歌曲列表。", songListId);
|
||||||
|
var musicInfos = await GetMusicInfoBySongIdAsync(songListId);
|
||||||
|
musicInfoList.AddRange(musicInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return musicInfoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于加密请求参数,具体加密算法请参考网易云音乐的 JS 代码。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="srcParams"></param>
|
||||||
|
/// <param name="secretKey"></param>
|
||||||
|
/// <param name="encSecKey"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private Dictionary<string, string> HandleRequest(object srcParams, string secretKey, string encSecKey)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"params", NetEaseMusicEncryptionHelper.AesEncode(
|
||||||
|
NetEaseMusicEncryptionHelper.AesEncode(
|
||||||
|
JsonConvert.SerializeObject(srcParams), NetEaseMusicEncryptionHelper.Nonce), secretKey)
|
||||||
|
},
|
||||||
|
{ "encSecKey", encSecKey }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过二维码登录网易云音乐,登录成功后返回 Cookie 和 CSRF Token。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(string? csrfToken, CookieContainer? cookieContainer)> LoginViqQrCodeAsync()
|
||||||
|
{
|
||||||
|
// Get unikey.
|
||||||
|
var qrCodeKeyJson = await (await PostAsync($"{Host}/weapi/login/qrcode/unikey", new
|
||||||
|
{
|
||||||
|
type = 1
|
||||||
|
})).Content.ReadAsStringAsync();
|
||||||
|
var uniKey = JObject.Parse(qrCodeKeyJson).SelectToken("$.unikey")!.Value<string>();
|
||||||
|
if (string.IsNullOrEmpty(uniKey)) return (null, null);
|
||||||
|
|
||||||
|
// Generate QR code.
|
||||||
|
var qrGenerator = new QRCodeGenerator();
|
||||||
|
var qrCodeData = qrGenerator.CreateQrCode($"{Host}/login?codekey={uniKey}",
|
||||||
|
QRCodeGenerator.ECCLevel.L);
|
||||||
|
var qrCode = new AsciiQRCode(qrCodeData);
|
||||||
|
var asciiQrCodeString = qrCode.GetGraphic(1, drawQuietZones: false);
|
||||||
|
|
||||||
|
_logger.LogInformation("请使用网易云 APP 扫码登录:");
|
||||||
|
_logger.LogInformation("\n{AsciiQrCodeString}", asciiQrCodeString);
|
||||||
|
|
||||||
|
// Wait for login success.
|
||||||
|
var isLogin = false;
|
||||||
|
while (!isLogin)
|
||||||
|
{
|
||||||
|
var (isSuccess, cookieContainer) = await CheckIsLoginAsync(uniKey);
|
||||||
|
isLogin = isSuccess;
|
||||||
|
|
||||||
|
if (!isLogin)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (cookieContainer?.GetCookies(new Uri(Host))["__csrf"]?.Value, cookieContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 <paramref name="uniKey"/> 检测是否登录成功。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uniKey">由网易云 API 生成的唯一 Key,用于登录。</param>
|
||||||
|
/// <returns>
|
||||||
|
/// 当登录成功的时候,元组 <c>isSuccess</c> 会为 true,<c>cookieContainer</c> 会包含登录成功后的 Cookie。<br/>
|
||||||
|
/// 如果登录失败,<c>isSuccess</c> 会为 false,<c>cookieContainer</c> 会为 null。
|
||||||
|
/// </returns>
|
||||||
|
private async Task<(bool isSuccess, CookieContainer? cookieContainer)> CheckIsLoginAsync(string uniKey)
|
||||||
|
{
|
||||||
|
var responseMessage = await PostAsync($"{Host}/weapi/login/qrcode/client/login", new
|
||||||
|
{
|
||||||
|
key = uniKey,
|
||||||
|
type = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
var responseString = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
var responseCode = JObject.Parse(responseString)["code"]?.Value<int>();
|
||||||
|
|
||||||
|
if (responseCode != 803)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseMessage.Headers.TryGetValues("Set-Cookie", out var cookies))
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookieContainer = new CookieContainer();
|
||||||
|
foreach (var cookie in cookies)
|
||||||
|
{
|
||||||
|
cookieContainer.SetCookies(new Uri(Host), cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, cookieContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 封装了网易云音乐的加密请求方式。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">需要请求的网易云音乐 API 地址。</param>
|
||||||
|
/// <param name="params">API 请求参数。</param>
|
||||||
|
/// <returns>
|
||||||
|
/// 正常情况下会返回一个 <see cref="HttpResponseMessage"/> 对象。
|
||||||
|
/// </returns>
|
||||||
|
private async Task<HttpResponseMessage> PostAsync(string url, object @params)
|
||||||
|
{
|
||||||
|
var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16);
|
||||||
|
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
|
||||||
|
|
||||||
|
return await _warpHttpClient.PostReturnHttpResponseAsync(url, requestOption:
|
||||||
|
request =>
|
||||||
|
{
|
||||||
|
request.Content = new FormUrlEncodedContent(HandleRequest(@params, secretKey, encSecKey));
|
||||||
|
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
// namespace ZonyLrcTools.Common.MusicScanner;
|
||||||
|
//
|
||||||
|
// public class QQMusicSongListMusicScanner : IMusicScanner
|
||||||
|
// {
|
||||||
|
// }
|
@@ -22,11 +22,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
|||||||
_wordsDictionary = new Lazy<Dictionary<string, string>>(() =>
|
_wordsDictionary = new Lazy<Dictionary<string, string>>(() =>
|
||||||
{
|
{
|
||||||
var jsonData = File.ReadAllText(_options.Provider.Tag.BlockWord.FilePath);
|
var jsonData = File.ReadAllText(_options.Provider.Tag.BlockWord.FilePath);
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonData);
|
return JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonData) ?? throw new InvalidOperationException("屏蔽词字典文件格式错误。");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetValue(string key)
|
public string? GetValue(string key)
|
||||||
{
|
{
|
||||||
if (_wordsDictionary.Value.TryGetValue(key, out var value))
|
if (_wordsDictionary.Value.TryGetValue(key, out var value))
|
||||||
{
|
{
|
||||||
|
@@ -25,7 +25,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
|||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
public async ValueTask<MusicInfo?> LoadAsync(string filePath)
|
||||||
{
|
{
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="key">原始单词。</param>
|
/// <param name="key">原始单词。</param>
|
||||||
/// <returns>原始单词对应的屏蔽词。</returns>
|
/// <returns>原始单词对应的屏蔽词。</returns>
|
||||||
string GetValue(string key);
|
string? GetValue(string key);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -18,6 +18,6 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filePath">歌曲文件的路径。</param>
|
/// <param name="filePath">歌曲文件的路径。</param>
|
||||||
/// <returns>加载完成的歌曲信息。</returns>
|
/// <returns>加载完成的歌曲信息。</returns>
|
||||||
ValueTask<MusicInfo> LoadAsync(string filePath);
|
ValueTask<MusicInfo?> LoadAsync(string filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -14,7 +14,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
|||||||
public string Name => ConstantName;
|
public string Name => ConstantName;
|
||||||
public const string ConstantName = "Taglib";
|
public const string ConstantName = "Taglib";
|
||||||
|
|
||||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
public async ValueTask<MusicInfo?> LoadAsync(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -30,12 +30,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
|||||||
|
|
||||||
await ValueTask.CompletedTask;
|
await ValueTask.CompletedTask;
|
||||||
|
|
||||||
if (songName == null && songArtist == null)
|
return songName == null ? null : new MusicInfo(filePath, songName, songArtist);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MusicInfo(filePath, songName, songArtist);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@@ -23,6 +23,11 @@ public class DefaultUpdater : IUpdater, ISingletonDependency
|
|||||||
|
|
||||||
public async Task CheckUpdateAsync()
|
public async Task CheckUpdateAsync()
|
||||||
{
|
{
|
||||||
|
if (!IsCheckUpdate())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var response = await _warpHttpClient.GetAsync<NewVersionResponse?>(UpdateUrl);
|
var response = await _warpHttpClient.GetAsync<NewVersionResponse?>(UpdateUrl);
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
@@ -36,7 +41,7 @@ public class DefaultUpdater : IUpdater, ISingletonDependency
|
|||||||
}
|
}
|
||||||
|
|
||||||
var importantItem = response.Items?.FirstOrDefault(x => x.ItemType == NewVersionItemType.Important);
|
var importantItem = response.Items?.FirstOrDefault(x => x.ItemType == NewVersionItemType.Important);
|
||||||
if (importantItem != null)
|
if (importantItem?.Url != null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning($"发现了新版本,请点击下面的链接进行更新:{importantItem.Url}");
|
_logger.LogWarning($"发现了新版本,请点击下面的链接进行更新:{importantItem.Url}");
|
||||||
_logger.LogWarning($"最新版本号:{response.NewVersion},当前版本号: ${currentVersion}");
|
_logger.LogWarning($"最新版本号:{response.NewVersion},当前版本号: ${currentVersion}");
|
||||||
@@ -56,4 +61,27 @@ public class DefaultUpdater : IUpdater, ISingletonDependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsCheckUpdate()
|
||||||
|
{
|
||||||
|
var lockFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update.lock");
|
||||||
|
|
||||||
|
if (!File.Exists(lockFile))
|
||||||
|
{
|
||||||
|
File.WriteAllText(lockFile, DateTimeOffset.Now.ToUnixTimeSeconds().ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (long.TryParse(File.ReadAllText(lockFile), out var time))
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||||
|
if (now - time <= 86400 /* 1 Day */)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(lockFile, DateTimeOffset.Now.ToUnixTimeSeconds().ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,27 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>4.0.0.50</Version>
|
<Version>4.0.0.57</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
<PackageReference Include="MusicDecrypto.Library"/>
|
||||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
<PackageReference Include="Newtonsoft.Json"/>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="NetEscapades.Configuration.Yaml"/>
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="Polly"/>
|
||||||
|
<PackageReference Include="QRCoder"/>
|
||||||
|
<PackageReference Include="TagLibSharp"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Lyrics\Providers\Kugeci\KugeciDownloader.cs" />
|
<Compile Remove="Lyrics\Providers\Kugeci\KugeciDownloader.cs"/>
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\vendor\MusicDecrypto\MusicDecrypto.Library\MusicDecrypto.Library.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
15
src/ZonyLrcTools.Desktop/App.axaml
Normal file
15
src/ZonyLrcTools.Desktop/App.axaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="ZonyLrcTools.Desktop.App"
|
||||||
|
xmlns:local="using:ZonyLrcTools.Desktop"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.DataTemplates>
|
||||||
|
<local:ViewLocator/>
|
||||||
|
</Application.DataTemplates>
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
28
src/ZonyLrcTools.Desktop/App.axaml.cs
Normal file
28
src/ZonyLrcTools.Desktop/App.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using ZonyLrcTools.Desktop.ViewModels;
|
||||||
|
using ZonyLrcTools.Desktop.Views;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Desktop;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
BIN
src/ZonyLrcTools.Desktop/Assets/avalonia-logo.ico
Normal file
BIN
src/ZonyLrcTools.Desktop/Assets/avalonia-logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 172 KiB |
23
src/ZonyLrcTools.Desktop/Program.cs
Normal file
23
src/ZonyLrcTools.Desktop/Program.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Desktop;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||||
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace()
|
||||||
|
.UseReactiveUI();
|
||||||
|
}
|
27
src/ZonyLrcTools.Desktop/ViewLocator.cs
Normal file
27
src/ZonyLrcTools.Desktop/ViewLocator.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using ZonyLrcTools.Desktop.ViewModels;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Desktop;
|
||||||
|
|
||||||
|
public class ViewLocator : IDataTemplate
|
||||||
|
{
|
||||||
|
public Control Build(object data)
|
||||||
|
{
|
||||||
|
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||||
|
var type = Type.GetType(name);
|
||||||
|
|
||||||
|
if (type != null)
|
||||||
|
{
|
||||||
|
return (Control)Activator.CreateInstance(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextBlock { Text = "Not Found: " + name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(object data)
|
||||||
|
{
|
||||||
|
return data is ViewModelBase;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ZonyLrcTools.Desktop.ViewModels;
|
||||||
|
|
||||||
|
public class MainWindowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Greeting => "Welcome to Avalonia!";
|
||||||
|
}
|
7
src/ZonyLrcTools.Desktop/ViewModels/ViewModelBase.cs
Normal file
7
src/ZonyLrcTools.Desktop/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Desktop.ViewModels;
|
||||||
|
|
||||||
|
public class ViewModelBase : ReactiveObject
|
||||||
|
{
|
||||||
|
}
|
20
src/ZonyLrcTools.Desktop/Views/MainWindow.axaml
Normal file
20
src/ZonyLrcTools.Desktop/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="ZonyLrcTools.Desktop.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Icon="/Assets/avalonia-logo.ico"
|
||||||
|
Title="ZonyLrcTools.Desktop">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||||
|
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||||
|
<vm:MainWindowViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
</Window>
|
11
src/ZonyLrcTools.Desktop/Views/MainWindow.axaml.cs
Normal file
11
src/ZonyLrcTools.Desktop/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace ZonyLrcTools.Desktop.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
25
src/ZonyLrcTools.Desktop/ZonyLrcTools.Desktop.csproj
Normal file
25
src/ZonyLrcTools.Desktop/ZonyLrcTools.Desktop.csproj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\"/>
|
||||||
|
<AvaloniaResource Include="Assets\**"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia"/>
|
||||||
|
<PackageReference Include="Avalonia.Desktop"/>
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent"/>
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter"/>
|
||||||
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics"/>
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI"/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
18
src/ZonyLrcTools.Desktop/app.manifest
Normal file
18
src/ZonyLrcTools.Desktop/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ZonyLrcTools.Desktop.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
@@ -1,37 +1,36 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.0" />
|
<PackageReference Include="AutoMapper"/>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Hosting"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting"/>
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageReference Include="Serilog.Sinks.File"/>
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="SuperSocket.WebSocket"/>
|
||||||
<PackageReference Include="SuperSocket.WebSocket" Version="2.0.0-beta.11" />
|
<PackageReference Include="SuperSocket.WebSocket.Server"/>
|
||||||
<PackageReference Include="SuperSocket.WebSocket.Server" Version="2.0.0-beta.11" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="UiStaticResources\**" />
|
<EmbeddedResource Include="UiStaticResources\**"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj" />
|
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- <ItemGroup>-->
|
<!-- <ItemGroup>-->
|
||||||
<!-- <ProjectReference Include="..\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj" />-->
|
<!-- <ProjectReference Include="..\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj" />-->
|
||||||
<!-- </ItemGroup>-->
|
<!-- </ItemGroup>-->
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
213
src/ui/.gitignore
vendored
213
src/ui/.gitignore
vendored
@@ -1,213 +0,0 @@
|
|||||||
### VisualStudioCode template
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
*.code-workspace
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
|
||||||
.history/
|
|
||||||
|
|
||||||
### JetBrains template
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/**/usage.statistics.xml
|
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
### Node template
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
### Vue template
|
|
||||||
# gitignore template for Vue.js projects
|
|
||||||
#
|
|
||||||
# Recommended template: Node.gitignore
|
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
|
||||||
docs/_book
|
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
|
||||||
test/
|
|
||||||
|
|
@@ -1,19 +0,0 @@
|
|||||||
# ui
|
|
||||||
|
|
||||||
## Project setup
|
|
||||||
```
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
|
||||||
```
|
|
||||||
yarn serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
```
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ui",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"serve": "vue-cli-service serve",
|
|
||||||
"build": "vue-cli-service build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.1.3",
|
|
||||||
"core-js": "^3.8.3",
|
|
||||||
"vue": "^2.6.14",
|
|
||||||
"vue-router": "^3.5.1",
|
|
||||||
"vuetify": "^2.6.0",
|
|
||||||
"vuex": "^3.6.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
|
||||||
"@vue/cli-plugin-router": "~5.0.0",
|
|
||||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
|
||||||
"@vue/cli-service": "~5.0.0",
|
|
||||||
"sass": "^1.32.7",
|
|
||||||
"sass-loader": "^12.0.0",
|
|
||||||
"vue-cli-plugin-vuetify": "~2.5.8",
|
|
||||||
"vue-template-compiler": "^2.6.14",
|
|
||||||
"vuetify-loader": "^1.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,80 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-app id="inspire">
|
|
||||||
|
|
||||||
<v-navigation-drawer
|
|
||||||
v-model="drawer"
|
|
||||||
app
|
|
||||||
>
|
|
||||||
<v-sheet
|
|
||||||
color="grey lighten-4"
|
|
||||||
class="pa-4"
|
|
||||||
>
|
|
||||||
<div>ZonyLrcTools-X</div>
|
|
||||||
</v-sheet>
|
|
||||||
|
|
||||||
<v-divider></v-divider>
|
|
||||||
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
v-for="[icon, text, to] in links"
|
|
||||||
:key="icon"
|
|
||||||
:to="to"
|
|
||||||
link
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
|
||||||
<v-icon>{{ icon }}</v-icon>
|
|
||||||
</v-list-item-icon>
|
|
||||||
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title>{{ text }}</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<v-app-bar app>
|
|
||||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title>ZonyLrcToolsX</v-toolbar-title>
|
|
||||||
</v-app-bar>
|
|
||||||
|
|
||||||
<v-main>
|
|
||||||
<v-container
|
|
||||||
class="py-8 px-6"
|
|
||||||
fluid
|
|
||||||
>
|
|
||||||
<router-view></router-view>
|
|
||||||
</v-container>
|
|
||||||
</v-main>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Socket from "@/communication/socket";
|
|
||||||
import {mapMutations} from "vuex"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data: () => ({
|
|
||||||
drawer: null,
|
|
||||||
links: [
|
|
||||||
['mdi-play-circle', '开始', '/'],
|
|
||||||
['mdi-multiplication-box', '设置', '/setting'],
|
|
||||||
['mdi-information', '关于', '/about'],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
created() {
|
|
||||||
Socket.$on("message", this.handleGetMessage);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
Socket.$off("message", this.handleGetMessage);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations({
|
|
||||||
setWsRes: "ws/setWsRes",
|
|
||||||
}),
|
|
||||||
handleGetMessage(msg) {
|
|
||||||
this.setWsRes(JSON.parse(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
|
Before Width: | Height: | Size: 539 B |
@@ -1,5 +0,0 @@
|
|||||||
import Vue from "vue";
|
|
||||||
|
|
||||||
const eventBus = new Vue();
|
|
||||||
|
|
||||||
export default eventBus;
|
|
@@ -1,31 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import eventBus from "@/communication/eventbus";
|
|
||||||
|
|
||||||
// const wsUrl = process.env.VUE_APP_WS_URL;
|
|
||||||
const wsUrl = "ws://127.0.0.1:50001"
|
|
||||||
|
|
||||||
let socket = new WebSocket(wsUrl);
|
|
||||||
const emitter = new Vue({
|
|
||||||
methods: {
|
|
||||||
send(message) {
|
|
||||||
if (socket.readyState === 1) {
|
|
||||||
socket.send(JSON.stringify(message));
|
|
||||||
}
|
|
||||||
}, connect() {
|
|
||||||
socket = new WebSocket(wsUrl);
|
|
||||||
socket.onmessage = (msg) => {
|
|
||||||
let event = JSON.parse(msg.data);
|
|
||||||
eventBus.$emit(event.action, event);
|
|
||||||
};
|
|
||||||
socket.onerror = (err) => {
|
|
||||||
eventBus.$emit('error', err);
|
|
||||||
};
|
|
||||||
socket.onclose = () => {
|
|
||||||
emitter.connect()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
emitter.connect();
|
|
||||||
export default emitter;
|
|
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "setting"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,14 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
import store from './store'
|
|
||||||
import vuetify from './plugins/vuetify'
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
vuetify,
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
@@ -1,21 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import Vuetify from 'vuetify/lib/framework';
|
|
||||||
|
|
||||||
Vue.use(Vuetify);
|
|
||||||
|
|
||||||
export default new Vuetify({
|
|
||||||
theme: {
|
|
||||||
dark: false,
|
|
||||||
themes: {
|
|
||||||
light: {
|
|
||||||
primary: '#03a9f4',
|
|
||||||
secondary: '#8bc34a',
|
|
||||||
accent: '#cddc39',
|
|
||||||
error: '#f44336',
|
|
||||||
warning: '#ffc107',
|
|
||||||
info: '#607d8b',
|
|
||||||
success: '#4caf50'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user