57 Commits

Author SHA1 Message Date
real-zony
1186369d16 ci: Released a new version.
Version: 4.0.0.55
2023-05-25 09:19:58 +08:00
real-zony
b240564cf7 perf: Optimized the startup time of the tool. 2023-05-25 09:05:39 +08:00
real-zony
6e2848a9de chore: Updated the version number. 2023-05-24 23:14:57 +08:00
real-zony
6361bf9265 feat: Improved the result information for lyrics download. 2023-05-24 23:08:17 +08:00
real-zony
383e2c5939 chore: Fix compilation warning messages. 2023-05-24 22:57:55 +08:00
real-zony
1e5c41852f perf: An error log file will only be created when there are songs that failed to download. 2023-05-24 22:00:22 +08:00
real-zony
41cba02833 feat: NetEase Cloud Music playlist download supports passing in multiple IDs.
Issue: https://github.com/real-zony/ZonyLrcToolsX/issues/139
2023-05-24 21:54:13 +08:00
real-zony
98c935ed93 fix: Fixed the issue where incorrect artist names were obtained when downloading NetEase Cloud Music playlists. 2023-05-24 20:50:24 +08:00
real-zony
62d08df735 fix: Added processing logic for special characters to prevent file write failures. 2023-05-24 20:29:02 +08:00
real-zony
1f312c749d build: Upgraded the version of NuGet packages that are dependent on, as usual. 2023-05-24 19:16:40 +08:00
Zony
30e10f4c01 Update README.md 2023-05-22 20:30:17 +08:00
real-zony
35468cfb71 test: Added three new test cases. 2023-04-29 23:30:38 +08:00
real-zony
126741f344 ci: Released a new version. 2023-04-17 01:20:15 +08:00
Zony
22f5bc69ec Merge pull request #135 from real-zony/routine-maintenance
Routine maintenance - 2023.04.17
2023-04-17 01:15:12 +08:00
real-zony
788ff38be2 fix: Fixed some unhandled exceptions. 2023-04-17 01:13:06 +08:00
real-zony
ab5f79bd50 fix: Fixed the issue where Netease Cloud Music did not throw the correct exception when a song could not be found. 2023-04-17 01:01:16 +08:00
real-zony
f935f07609 feat: Modified the lyrics interface for Netease Cloud Music to support higher concurrency. 2023-04-17 00:47:23 +08:00
real-zony
493f48cefe fix: Fixed the index exception that may be caused by the KuGou music provider. 2023-04-16 23:52:57 +08:00
Zony
4999e4d694 Update README.md 2023-03-30 09:51:09 +08:00
Zony
334528eca6 Merge pull request #134 from real-zony/docs
Docs
2023-03-28 22:42:00 +08:00
real-zony
e76ddb75eb docs: Updated documentation. 2023-03-28 22:36:36 +08:00
real-zony
d6d384590c ci: Adjusted artifact expiration time for Github Actions. 2023-03-28 22:17:28 +08:00
real-zony
949dac7ad1 ci: Updated NuGet package versions. 2023-03-27 23:14:21 +08:00
real-zony
2ffd75ed1d ci: Removed unnecessary pdb files. 2023-03-19 12:19:26 +08:00
Zony
82af3009d3 Merge pull request #132 from real-zony/issue-128
fix: Fixed the path issue #128.
2023-03-19 12:01:18 +08:00
real-zony
572e555f2a fix: Fixed the path issue #128.
We should avoid using the Directory.GetCurrentDirectory() method, as it
can result in retrieving incorrect paths.
2023-03-19 11:34:32 +08:00
real-zony
286731b1a6 ci: Adjusted the Github pipeline.
Prevent artifact publication when performing PR operations.
2023-03-19 11:13:30 +08:00
Zony
5850614663 Merge pull request #131 from real-zony/dependabot/npm_and_yarn/src/ui/webpack-5.76.1
build(deps): bump webpack from 5.74.0 to 5.76.1 in /src/ui
2023-03-15 20:47:34 +08:00
dependabot[bot]
c20a280a07 build(deps): bump webpack from 5.74.0 to 5.76.1 in /src/ui
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 05:59:11 +00:00
real-zony
55de00ec1f Merge remote-tracking branch 'origin/dev' into dev 2023-03-14 22:31:41 +08:00
real-zony
bc2b314d6a docs: Improved comments for the scanner. 2023-03-14 22:31:28 +08:00
Zony
0aafe91f96 Merge pull request #130 from real-zony/dependabot/npm_and_yarn/src/ui/sideway/formula-3.0.1
build(deps): bump @sideway/formula from 3.0.0 to 3.0.1 in /src/ui
2023-03-13 09:06:00 +08:00
dependabot[bot]
2e71914578 build(deps): bump @sideway/formula from 3.0.0 to 3.0.1 in /src/ui
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-12 15:20:01 +00:00
real-zony
b916323986 feat: We have improved the search and download logic for NetEase Cloud Music playlists and added support for logging in. 2023-03-12 23:19:27 +08:00
real-zony
afe1a7013c feat: We have added a new 'POST' method to retrieve cookies. 2023-03-12 18:35:07 +08:00
real-zony
fb1f743365 feat: Added support for reading song information from CSV files and NetEase Cloud Music playlists. 2023-03-12 15:20:55 +08:00
real-zony
0e5e48cd00 feat: Improved the way help information is displayed. 2023-03-12 14:49:47 +08:00
real-zony
18d9c2d32c feat: Now, it's possible to fetch playlists from NetEase Cloud Music and download lyrics. 2023-02-24 00:05:23 +08:00
real-zony
5dbabab5a6 feat: Supports UTF-8 BOM encoding. 2023-02-22 21:45:27 +08:00
real-zony
bfd89e9d82 feat: Added Powershell script for building project. 2023-02-22 21:37:45 +08:00
real-zony
9a029225e1 chore: Working... 2023-02-17 23:05:32 +08:00
real-zony
a7ecfbe44f feat: Added support for reading song list from Csv file. 2023-02-08 20:56:47 +08:00
real-zony
f1a6eefe45 fix: Fixed the issue where classes that don't implement an interface couldn't be injected. 2023-02-08 20:56:04 +08:00
real-zony
86e341290e feat: Added support to output the list of failed songs to a file. 2023-02-07 22:06:44 +08:00
real-zony
0aa8ca5aef docs: Improve the help content. 2023-02-02 22:47:30 +08:00
real-zony
2eef72e165 fix: Avoid null reference exception. 2023-01-29 22:55:20 +08:00
real-zony
f4b17097fd chore: Update NuGet package versions. 2023-01-29 21:29:32 +08:00
Zony
e68d6b9cf9 Merge pull request #122 from real-zony/dependabot/npm_and_yarn/src/ui/loader-utils-1.4.2
build(deps): bump loader-utils from 1.4.0 to 1.4.2 in /src/ui
2023-01-17 09:16:34 +08:00
dependabot[bot]
84231d9613 build(deps): bump loader-utils from 1.4.0 to 1.4.2 in /src/ui
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-17 01:16:19 +00:00
Zony
8398d80a8e Merge pull request #124 from real-zony/dependabot/npm_and_yarn/src/ui/json5-1.0.2
build(deps): bump json5 from 1.0.1 to 1.0.2 in /src/ui
2023-01-17 09:15:29 +08:00
real-zony
991db8ab48 fix: Fixed issue where solution was referencing non-existent projects. 2023-01-16 23:34:51 +08:00
real-zony
3e957fdb25 build: Removed the direct reference to MusicDecrypto.
Directly referencing the NuGet package.
2023-01-08 22:47:09 +08:00
dependabot[bot]
fc09839775 build(deps): bump json5 from 1.0.1 to 1.0.2 in /src/ui
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-04 03:36:16 +00:00
real-zony
8d5a45c9ef fix: fixed some issues. 2023-01-04 11:35:44 +08:00
real-zony
6b538f3e0a build: Updated the code for MusicDecrypto. 2023-01-03 21:39:03 +08:00
real-zony
d7e11af3ba build: Upgraded the version of the NuGet package dependency. 2023-01-03 21:36:52 +08:00
real-zony
f25ba90b9a fix: Do not trim the generated files. 2022-12-13 23:05:27 +08:00
87 changed files with 1165 additions and 268 deletions

View File

@@ -17,10 +17,8 @@ jobs:
run: echo "::set-output name=date::$(date +'%Y%m%d')${{github.run_number}}"
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
@@ -38,6 +36,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
retention-days: 90
name: release-files
path: |
./TempFiles
@@ -45,6 +44,7 @@ jobs:
outputs:
version: ${{ steps.date.outputs.date }}
release:
if: github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
steps:

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,30 +1,31 @@
简体中文 | [English](./docs/en_US.md)
## 免责声明
# 免责声明
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等)[本人](https://github.com/real-zony) 不承担任何法律责任。
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
## 简介
# 简介
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具**QQ 群:337656932**,群文件里面有详细的视频教程
🚧 当前版本正在开发当中。
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
## 下载
# 下载
工具会执行每日构建动作,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
## 用法
# 用法
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell/命令提示符/Windows 终端,根据下述说明执行命令即可。
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
### 子命令
## 子命令
#### 歌词下载
### 下载功能
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
@@ -44,7 +45,62 @@ macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命
.\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2
```
#### 加密格式转换
#### 指定歌曲文件源
目前程序支持从本地目录、CSV 文件、网易云歌单获取歌曲数据,然后下载 LRC 歌词数据。
指定歌词源的参数是 `-sc|--scanner`,可以通过这个参数指定歌曲信息的来源。它拥有以下选值,默认情况下是 `local`
```shell
-sc local # 从目录获取歌曲信息。
-sc netease # 从网易云歌单获取歌曲信息。
-sc csv # 从 CSV 文件获取歌曲信息。
```
**从网易云歌单获取歌曲数据**
完整的命令如下:
```shell
.\ZonyLrcTools.Cli.exe download -sc netease -o "D:\TempFiles" -s "7224428149" -l
```
其中 `-sc` 参数用于指定歌词信息的来源是网易云歌单; `-o` 参数指定的是歌词文件的输出目录,请尽量使用绝对路径; `-s` 参数指定的的是歌单的 ID该 ID 可以从网页版的网易云音乐获得。
例如获取地址 [https://music.163.com/#/playlist?id=158010361](https://music.163.com/#/playlist?id=158010361) 的歌单信息,那么歌单 ID 就应该传递 158010361。
如果你需要获取多个歌单的歌曲数据,请在传递歌单 ID 时使用分号 `;` 指定多个 ID例如下面的命令就是下载两个歌单的歌曲数据。
```shell
.\ZonyLrcTools.Cli.exe download -sc netease -o "D:\TempFiles" -s "7224428149;158010361" -l
```
由于网易云音乐的限制,要想获取完整的歌单信息,必须扫码登录程序,还是以最上面的为例,我需要下载歌单内的歌词数据,就必须扫码之后程序才会执行。
![image-20230328223155280](./README.assets/image-20230328223155280.png)
**从 CSV 文件获取歌曲数据**
应 [Issue 126](https://github.com/real-zony/ZonyLrcToolsX/issues/126) 的请求,增加了从 CSV 获取歌曲信息的方式。这样可以在没有原始歌曲的情况下载歌词数据。
示例命令:
```shell
.\ZonyLrcTools.Cli.exe download -sc csv -o "D:\TempFiles" -f "D:\TempFiles\demo.csv" -l
```
其中 `-f` 参数用于指定 csv 文件的路径csv 文件的格式应该如下所示,保证第一行是列信息,一共要包含两列。
```csv
Song,Artist
刀马旦,李玟
发如雪,周杰伦
说书人,寅子
爱的供养,张国荣
七里香,周杰伦
```
### 加密格式转换
子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**。
@@ -56,7 +112,7 @@ macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命
.\ZonyLrcTools.Cli.exe util -s D:\CloudMusic
```
### 配置文件
## 配置文件
程序的所有的配置信息,都在 `config.yaml` 进行更改,下面标注了各个配置的说明。
@@ -121,11 +177,13 @@ globalOption:
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`
#### 支持的歌词源
> 针对 UTF-8 BOM 格式,程序进行了特殊处理,请在 `fileEncoding` 里面填写 "utf-8-bom" 以支持。
### 支持的歌词源
| 歌词源 | 默认优先级 |
| ---------- | ---------- |
@@ -134,7 +192,7 @@ globalOption:
| 酷狗音乐 | 3 |
| 酷我音乐 | 4 |
### 屏蔽字典
## 屏蔽字典
屏蔽字典适用于网易云音乐歌词下载,针对某些单词,网易云音乐使用了 * 号进行屏蔽,这个时候可以使用屏蔽字典,设置歌曲名的关键词替换。例如有一首歌曲叫做 *Fucking ABC* ,这个时候网易云实际的名字是 *Fu****ing* ,用户只需要在屏蔽字典加入替换逻辑即可,例如:
@@ -146,15 +204,15 @@ globalOption:
屏蔽字典默认路径为程序所在目录的 *BlockWords.json* 文件,用户可以在 *appsettings.json* 文件中配置其他路径。
## 捐赠
# 捐赠
<img src="./docs/img/alipay.jpg" width="200"/><img src="./docs/img/wechat.jpg" width="200"/>
## Star History
# Star History
[![Star History Chart](https://api.star-history.com/svg?repos=real-zony/ZonyLrcToolsX&type=Timeline)](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
## 路线图
# 路线图
- [x] 支持跨平台的 CLI 工具。
- [x] 基于 Web GUI 的操作站点。

View File

@@ -15,10 +15,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.LocalServer",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Common", "src\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj", "{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vendor", "vendor", "{AB0E3F21-DDBF-459D-995C-04C177BF8995}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicDecrypto.Library", "vendor\MusicDecrypto\MusicDecrypto.Library\MusicDecrypto.Library.csproj", "{6BB67593-38CD-42E4-8409-EDAC3C138A6B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,10 +37,6 @@ Global
{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.Build.0 = Release|Any CPU
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6BB67593-38CD-42E4-8409-EDAC3C138A6B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -57,6 +49,5 @@ Global
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
{2875A08A-FFD6-4863-B815-5384DCFE88FC} = {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}
EndGlobalSection
EndGlobal

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

View File

@@ -1,8 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
using ZonyLrcTools.Common;
using ZonyLrcTools.Common.Album;
using ZonyLrcTools.Common.Lyrics;
using ZonyLrcTools.Common.MusicScanner;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable MemberCanBePrivate.Global
@@ -15,14 +21,17 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
private readonly ILyricsDownloader _lyricsDownloader;
private readonly IAlbumDownloader _albumDownloader;
private readonly IMusicInfoLoader _musicInfoLoader;
private readonly IServiceProvider _serviceProvider;
public DownloadCommand(ILyricsDownloader lyricsDownloader,
IMusicInfoLoader musicInfoLoader,
IAlbumDownloader albumDownloader)
IAlbumDownloader albumDownloader,
IServiceProvider serviceProvider)
{
_lyricsDownloader = lyricsDownloader;
_musicInfoLoader = musicInfoLoader;
_albumDownloader = albumDownloader;
_serviceProvider = serviceProvider;
}
#region > Options <
@@ -38,23 +47,91 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
public bool DownloadAlbum { get; set; }
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
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
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (!DownloadAlbum && !DownloadLyric)
{
throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。");
}
if (DownloadLyric)
{
await _lyricsDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
}
if (DownloadAlbum)
{
await _albumDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
await _albumDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
}
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。");
}
}
}
}

View File

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

View File

@@ -1,13 +1,23 @@
using System;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
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
{
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);
}
}

View File

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

View File

@@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using ZonyLrcTools.Cli.Commands;
using ZonyLrcTools.Cli.Commands.SubCommand;
using ZonyLrcTools.Cli.Infrastructure;
@@ -17,6 +16,8 @@ using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
// ReSharper disable ClassNeverInstantiated.Global
namespace ZonyLrcTools.Cli
{
[Command("lyric-tool")]
@@ -33,6 +34,7 @@ namespace ZonyLrcTools.Cli
try
{
Log.Logger.Information("Starting...");
return await BuildHostedService(args);
}
catch (Exception ex)
@@ -63,12 +65,12 @@ namespace ZonyLrcTools.Cli
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
.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.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"));
})
.CreateLogger();
@@ -81,7 +83,7 @@ namespace ZonyLrcTools.Cli
.ConfigureHostConfiguration(builder =>
{
builder
.SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddYamlFile("config.yaml");
})
.ConfigureServices((_, services) =>

View File

@@ -4,7 +4,8 @@
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
"10003": "不能获取文件的后缀信息。",
"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": {
"50001": "扫描文件时出现了错误。",

View File

@@ -10,16 +10,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.1" />
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -37,6 +37,8 @@ globalOption:
- name: NetEase # 基于网易云音乐的歌词下载器。
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
additional:
isEnableRomanOutput: false # 是否启用罗马音输出,本参数仅当对应歌曲有罗马音歌词时有效。
- name: QQ # 基于 QQ 音乐的歌词下载器。
priority: 2
# depth: 10 # 暂时不支持。

View File

@@ -0,0 +1,20 @@
$Platforms = @('win-x64', 'linux-x64', 'osx-x64')
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
}

View File

@@ -10,7 +10,7 @@ rm -rf ./TempFiles/*
for platform in "${Platforms[@]}"
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
zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1

View File

@@ -41,7 +41,7 @@ namespace ZonyLrcTools.Common.Album.NetEase
true,
_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);
}

View File

@@ -13,6 +13,6 @@
/// <summary>
/// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。
/// </summary>
public string FilePath { get; set; }
public string FilePath { get; set; } = null!;
}
}

View File

@@ -5,16 +5,16 @@ namespace ZonyLrcTools.Common.Configuration
/// <summary>
/// 支持的音乐文件后缀集合。
/// </summary>
public List<string> SupportFileExtensions { get; set; }
public List<string> SupportFileExtensions { get; set; } = null!;
/// <summary>
/// 网络代理相关的配置信息。
/// </summary>
public NetworkOptions NetworkOptions { get; set; }
public NetworkOptions NetworkOptions { get; set; } = null!;
/// <summary>
/// 定义下载器的相关配置信息。
/// </summary>
public ProviderOptions Provider { get; set; }
public ProviderOptions Provider { get; set; } = null!;
}
}

View File

@@ -2,12 +2,12 @@ namespace ZonyLrcTools.Common.Configuration;
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)
{
return Plugin.FirstOrDefault(x => x.Name == name);
return Plugin.FirstOrDefault(x => x.Name == name)!;
}
}

View File

@@ -5,7 +5,7 @@
/// <summary>
/// 歌词下载器的唯一标识。
/// </summary>
public string Name { get; set; }
public string Name { get; set; } = null!;
/// <summary>
/// 歌词下载时的优先级,当值为 -1 时是禁用。

View File

@@ -13,7 +13,7 @@ namespace ZonyLrcTools.Common.Configuration
/// <summary>
/// 代理服务器的 Ip。
/// </summary>
public string Ip { get; set; }
public string Ip { get; set; } = null!;
/// <summary>
/// 代理服务器的 端口。

View File

@@ -5,10 +5,10 @@ public class ProviderOptions
/// <summary>
/// 标签加载器相关的配置属性。
/// </summary>
public TagInfoOptions Tag { get; set; }
public TagInfoOptions Tag { get; set; } = null!;
/// <summary>
/// 歌词下载相关的配置信息。
/// </summary>
public LyricsOptions Lyric { get; set; }
public LyricsOptions Lyric { get; set; } = null!;
}

View File

@@ -2,10 +2,10 @@
public class TagInfoOptions
{
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; }
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; } = null!;
/// <summary>
/// 屏蔽词功能相关配置。
/// </summary>
public BlockWordOptions BlockWord { get; set; }
public BlockWordOptions BlockWord { get; set; } = null!;
}

View File

@@ -2,10 +2,10 @@ namespace ZonyLrcTools.Common.Configuration
{
public class TagInfoProviderOptions
{
public string Name { get; set; }
public string Name { get; set; } = null!;
public int Priority { get; set; }
public Dictionary<string, string> Extensions { get; set; }
public Dictionary<string, string> Extensions { get; set; } = null!;
}
}

View File

@@ -45,7 +45,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
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())
{
@@ -59,7 +59,6 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
if (type.Name.EndsWith(interfaceName))
{
serviceTypes.Add(interfaceType);
serviceTypes.Add(type);
}
}

View File

@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
/// <summary>
/// 配置工具会用到的服务。
/// </summary>
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
public static IServiceCollection? ConfigureToolService(this IServiceCollection? services)
{
if (services == null)
{
@@ -43,7 +43,7 @@ namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddYamlFile("config.yaml")
.Build();

View File

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

View File

@@ -7,7 +7,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
public int ErrorCode { get; }
public object AttachObject { get; }
public object? AttachObject { get; }
/// <summary>
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
/// <param name="message">错误信息。</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;
AttachObject = attachObj;

View File

@@ -26,14 +26,14 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
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));
var jsonObj = JObject.Load(jsonReader);
var errors = jsonObj.SelectTokens("$.Error.*");
var warnings = jsonObj.SelectTokens("$.Warning.*");
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];

View File

@@ -32,6 +32,11 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
/// </summary>
public const int NotSupportedFileEncoding = 10005;
/// <summary>
/// 文本: 无法从网易云音乐获取歌曲列表。
/// </summary>
public const int UnableGetSongListFromNetEaseCloudMusic = 10006;
#endregion
#region > <

View File

@@ -17,7 +17,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Extensions
/// <param name="logger">日志记录器实例。</param>
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</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();
}

View File

@@ -19,9 +19,29 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
}
public async ValueTask<string> PostAsync(string url,
object parameters = null,
object? parameters = null,
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 requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
@@ -29,24 +49,12 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
requestOption?.Invoke(requestMessage);
using var responseMessage = 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);
return await BuildHttpClient().SendAsync(requestMessage);
}
public async ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null)
{
var requestParamsStr = BuildQueryString(parameters);
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,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null)
{
var responseString = await GetAsync(url, parameters, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
@@ -71,7 +79,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
return _httpClientFactory.CreateClient(HttpClientNameConstant);
}
private string BuildQueryString(object parameters)
private string BuildQueryString(object? parameters)
{
if (parameters == null)
{
@@ -81,7 +89,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
var type = parameters.GetType();
if (type == typeof(string))
{
return parameters as string;
return parameters as string ?? string.Empty;
}
var properties = type.GetProperties();
@@ -98,7 +106,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
return paramBuilder.ToString().TrimEnd('&');
}
private string BuildJsonBodyString(object parameters)
private string BuildJsonBodyString(object? parameters)
{
if (parameters == null) return string.Empty;
if (parameters is string result) return result;
@@ -114,7 +122,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <returns>如果响应正常,则返回具体的响应内容。</returns>
/// <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
{
@@ -131,7 +139,7 @@ namespace ZonyLrcTools.Common.Infrastructure.Network
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <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 });

View File

@@ -14,9 +14,9 @@
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> PostAsync(string url,
object parameters = null,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
Action<HttpRequestMessage>? requestOption = null);
/// <summary>
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
@@ -28,9 +28,14 @@
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
object? parameters = null,
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>
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
@@ -40,8 +45,8 @@
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
@@ -53,7 +58,7 @@
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> GetAsync<TResponse>(
string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null);
}
}

View File

@@ -18,6 +18,6 @@ namespace ZonyLrcTools.Common.Lyrics
/// <param name="sourceLyric">原始歌词数据。</param>
/// <param name="translationLyric">翻译歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
LyricsItemCollection Build(string sourceLyric, string translationLyric);
LyricsItemCollection Build(string? sourceLyric, string? translationLyric);
}
}

View File

@@ -12,7 +12,7 @@ namespace ZonyLrcTools.Common.Lyrics
/// <param name="artist">歌曲的作者。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>歌曲的歌词数据对象。</returns>
ValueTask<LyricsItemCollection> DownloadAsync(string? songName, string? artist, long? duration = null);
ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
/// <summary>
/// 下载器的名称。

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Collections.Immutable;
using System.Text;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
@@ -36,7 +37,7 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
}
public async Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 2,
int parallelCount = 1,
CancellationToken cancellationToken = default)
{
await _logger.InfoAsync("开始下载歌词文件数据...");
@@ -66,7 +67,11 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
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)
@@ -78,6 +83,7 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
if (lyrics.IsPruneMusic)
{
info.IsSuccessful = true;
info.IsPruneMusic = true;
return;
}
@@ -117,6 +123,13 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics)
{
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))
{
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());
}
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());
}
}

View File

@@ -15,7 +15,7 @@ namespace ZonyLrcTools.Common.Lyrics
/// <summary>
/// 歌词文本数据。
/// </summary>
public string LyricText { get; }
public string? LyricText { get; }
/// <summary>
/// 歌词所在的时间(分)。
@@ -55,21 +55,21 @@ namespace ZonyLrcTools.Common.Lyrics
/// <param name="minute">歌词所在的时间(分)。</param>
/// <param name="second">歌词所在的时间(秒)。</param>
/// <param name="lyricText">歌词文本数据。</param>
public LyricsItem(int minute, double second, string lyricText)
public LyricsItem(int minute, double second, string? lyricText)
{
Minute = minute;
Second = second;
LyricText = lyricText;
}
public int CompareTo(LyricsItem other)
public int CompareTo(LyricsItem? other)
{
if (SortScore > other.SortScore)
if (SortScore > other?.SortScore)
{
return 1;
}
if (SortScore < other.SortScore)
if (SortScore < other?.SortScore)
{
return -1;
}
@@ -87,12 +87,12 @@ namespace ZonyLrcTools.Common.Lyrics
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;
}
public static bool operator !=(LyricsItem item1, LyricsItem item2)
public static bool operator !=(LyricsItem? item1, LyricsItem? item2)
{
return !(item1 == item2);
}
@@ -107,7 +107,7 @@ namespace ZonyLrcTools.Common.Lyrics
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(this, obj)) return true;

View File

@@ -14,7 +14,7 @@ namespace ZonyLrcTools.Common.Lyrics
/// </summary>
public bool IsPruneMusic => Count == 0;
public GlobalLyricsConfigOptions? Options { get; private set; }
public GlobalLyricsConfigOptions? Options { get; }
public LyricsItemCollection(GlobalLyricsConfigOptions? options)
{
@@ -29,6 +29,11 @@ namespace ZonyLrcTools.Common.Lyrics
}
var option = left.Options;
if (option == null)
{
throw new NullReferenceException("LyricsItemCollection.Options");
}
var newCollection = new LyricsItemCollection(option);
var indexDiff = left.Count - right.Count;
if (!option.IsOneLine)
@@ -100,6 +105,11 @@ namespace ZonyLrcTools.Common.Lyrics
public override string ToString()
{
if (Options == null)
{
throw new NullReferenceException("LyricsItemCollection.Options");
}
var lyricBuilder = new StringBuilder();
ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.LineBreak));
return lyricBuilder.ToString().TrimEnd(Options.LineBreak);

View File

@@ -30,7 +30,7 @@ namespace ZonyLrcTools.Common.Lyrics
return lyric;
}
public LyricsItemCollection Build(string sourceLyric, string translationLyric)
public LyricsItemCollection Build(string? sourceLyric, string? translationLyric)
{
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
if (string.IsNullOrEmpty(sourceLyric))

View File

@@ -10,9 +10,9 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
[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;
UnknownParameters2 = "yes";

View File

@@ -8,13 +8,13 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
[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
{
[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; }
}
}

View File

@@ -12,11 +12,11 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
[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;
UnknownParameters2 = "iphone";

View File

@@ -6,20 +6,20 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
[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_msg")] public string ErrorMessage { get; set; }
[JsonProperty("error_msg")] public string? ErrorMessage { get; set; }
}
public class SongSearchResponseInnerData
{
[JsonProperty("lists")] public List<SongSearchResponseSongDetail> List { get; set; }
[JsonProperty("lists")] public List<SongSearchResponseSongDetail>? List { get; set; }
}
public class SongSearchResponseSongDetail
{
public string FileHash { get; set; }
public string? FileHash { get; set; }
}
}

View File

@@ -38,7 +38,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
// 获得特殊的 AccessToken 与 Id真正请求歌词数据。
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];
return await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
@@ -49,18 +54,18 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
{
await ValueTask.CompletedTask;
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);
}
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);
}
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);
}

View File

@@ -6,7 +6,7 @@ public class GetLyricsResponse
{
[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; }
@@ -15,12 +15,12 @@ public class GetLyricsResponse
public class GetLyricsResponseInnerData
{
[JsonProperty("lrclist")] public ICollection<GetLyricsItem> Lyrics { get; set; }
[JsonProperty("lrclist")] public ICollection<GetLyricsItem>? Lyrics { get; set; }
}
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!;
}

View File

@@ -6,7 +6,7 @@ public class SongSearchResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("data")] public SongSearchResponseInnerData InnerData { get; set; }
[JsonProperty("data")] public SongSearchResponseInnerData InnerData { get; set; } = null!;
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
@@ -29,9 +29,9 @@ public class SongSearchResponse
public class SongSearchResponseInnerData
{
[JsonProperty("total")] public string Total { get; set; }
[JsonProperty("total")] public string? Total { get; set; }
[JsonProperty("list")] public ICollection<SongSearchResponseDetail> SongItems { get; set; }
[JsonProperty("list")] public ICollection<SongSearchResponseDetail> SongItems { get; set; } = null!;
}
public class SongSearchResponseDetail

View File

@@ -60,6 +60,11 @@ public class KuWoLyricsProvider : LyricsProvider
await ValueTask.CompletedTask;
var lyricsResponse = (GetLyricsResponse)lyricsObject;
if (lyricsResponse.Data?.Lyrics == null)
{
return new LyricsItemCollection(null);
}
var items = lyricsResponse.Data.Lyrics.Select(l =>
{
var position = double.Parse(l.Position);

View File

@@ -8,7 +8,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
public GetLyricRequest(long songId)
{
OS = "ios";
OS = "pc";
Id = songId;
Lv = Kv = Tv = Rv = -1;
}

View File

@@ -8,31 +8,31 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// 原始的歌词。
/// </summary>
[JsonProperty("lrc")]
public InnerLyric OriginalLyric { get; set; }
public InnerLyric? OriginalLyric { get; set; }
/// <summary>
/// 卡拉 OK 歌词。
/// </summary>
[JsonProperty("klyric")]
public InnerLyric KaraokeLyric { get; set; }
public InnerLyric? KaraokeLyric { get; set; }
/// <summary>
/// 如果存在翻译歌词,则本项内容为翻译歌词。
/// </summary>
[JsonProperty("tlyric")]
public InnerLyric TranslationLyric { get; set; }
public InnerLyric? TranslationLyric { get; set; }
/// <summary>
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
/// </summary>
[JsonProperty("romalrc")]
public InnerLyric RomaLyric { get; set; }
public InnerLyric? RomaLyric { get; set; }
/// <summary>
/// 状态码。
/// </summary>
[JsonProperty("code")]
public string StatusCode { get; set; }
public string? StatusCode { get; set; }
}
/// <summary>
@@ -40,12 +40,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// </summary>
public class InnerLyric
{
[JsonProperty("version")] public string Version { get; set; }
[JsonProperty("version")] public string? Version { get; set; }
/// <summary>
/// 具体的歌词数据。
/// </summary>
[JsonProperty("lyric")]
public string Text { get; set; }
public string? Text { get; set; }
}
}

View File

@@ -45,22 +45,19 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
[JsonProperty("crypto")] public string Crypto { get; set; } = "weapi";
public SongSearchRequest()
public SongSearchRequest(string musicName, string artistName, int limit = 10)
{
CsrfToken = string.Empty;
Type = 1;
Offset = 0;
IsTotal = true;
Limit = 10;
}
public SongSearchRequest(string musicName, string artistName, int limit = 10) : this()
{
// Remove all the brackets and the content inside them.
var regex = new Regex(@"\([^)]*\)");
musicName = regex.Replace(musicName, string.Empty);
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
SearchKey = $"{musicName}+{artistName}";
Limit = limit;
}
}

View File

@@ -4,7 +4,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{
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; }
@@ -27,7 +27,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
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; }
}
@@ -38,7 +38,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// 歌曲的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// 歌曲的 Sid (Song Id)。
@@ -50,13 +50,13 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// 歌曲的演唱者。
/// </summary>
[JsonProperty("artists")]
public IList<SongArtistModel> Artists { get; set; }
public IList<SongArtistModel>? Artists { get; set; }
/// <summary>
/// 歌曲的专辑信息。
/// </summary>
[JsonProperty("album")]
public SongAlbumModel Album { get; set; }
public SongAlbumModel? Album { get; set; }
/// <summary>
/// 歌曲的实际长度。
@@ -71,7 +71,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// 歌手/艺术家的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
public string? Name { get; set; }
}
public class SongAlbumModel
@@ -80,12 +80,12 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
/// 专辑的名称。
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// 专辑图像的 Url 地址。
/// </summary>
[JsonProperty("img1v1Url")]
public string PictureUrl { get; set; }
public string? PictureUrl { get; set; }
}
}

View File

@@ -2,6 +2,7 @@ using System.Net.Http.Headers;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Encryption;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel;
@@ -16,11 +17,10 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
private readonly GlobalOptions _options;
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
private const string NetEaseSearchMusicUrl = @"https://music.163.com/weapi/cloudsearch/get/web";
private const string NetEaseGetLyricUrl = @"https://music.163.com/weapi/song/lyric?csrf_token=";
private const string NetEaseRequestReferer = @"https://music.163.com";
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
public NetEaseLyricsProvider(IWarpHttpClient warpHttpClient,
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
@@ -33,25 +33,30 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
{
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
NetEaseSearchMusicUrl,
var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16);
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(NetEaseSearchMusicUrl,
requestOption: request =>
{
request.Headers.Referrer = new Uri(NetEaseRequestReferer);
request.Content = new FormUrlEncodedContent(HandleRequest(
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
true,
msg =>
{
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
if (msg.Content != null)
{
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
}
secretKey,
encSecKey));
});
ValidateSongSearchResponse(searchResult, args);
return await _warpHttpClient.GetAsync(
NetEaseGetLyricUrl,
return await _warpHttpClient.PostAsync(NetEaseGetLyricUrl,
requestOption: request =>
{
request.Headers.Referrer = new Uri(NetEaseRequestReferer);
request.Content = new FormUrlEncodedContent(HandleRequest(
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName, args.Duration)),
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
secretKey,
encSecKey));
});
}
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args)
@@ -71,7 +76,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase
return _lyricsItemCollectionFactory.Build(
json.OriginalLyric.Text,
json.TranslationLyric.Text);
json.TranslationLyric?.Text);
}
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);
}
if (response.Items?.SongCount <= 0)
if (response.Items is not { SongCount: > 0 })
{
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 }
};
}
}
}

View File

@@ -16,10 +16,6 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
[JsonProperty("g_tk")] public int Gtk { get; set; }
protected GetLyricRequest()
{
}
public GetLyricRequest(string? songId)
{
IsNoBase64Encoding = 1;

View File

@@ -19,7 +19,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
public string Platform { get; protected set; }
[JsonProperty("key")]
public string Keyword { get; protected set; }
public string Keyword { get; protected set; } = null!;
protected SongSearchRequest()
{

View File

@@ -6,21 +6,21 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel
{
[JsonProperty("code")] public int StatusCode { get; set; }
[JsonProperty("data")] public QQMusicInnerDataModel Data { get; set; }
[JsonProperty("data")] public QQMusicInnerDataModel? Data { get; set; }
}
public class QQMusicInnerDataModel
{
[JsonProperty("song")] public QQMusicInnerSongModel Song { get; set; }
[JsonProperty("song")] public QQMusicInnerSongModel? Song { get; set; }
}
public class QQMusicInnerSongModel
{
[JsonProperty("itemlist")] public List<QQMusicInnerSongItem> SongItems { get; set; }
[JsonProperty("itemlist")] public List<QQMusicInnerSongItem>? SongItems { get; set; }
}
public class QQMusicInnerSongItem
{
[JsonProperty("mid")] public string SongId { get; set; }
[JsonProperty("mid")] public string? SongId { get; set; }
}
}

View File

@@ -35,7 +35,7 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
ValidateSongSearchResponse(searchResult, args);
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));
}
@@ -57,15 +57,15 @@ namespace ZonyLrcTools.Common.Lyrics.Providers.QQMusic
}
var lyricJsonObj = JObject.Parse(lyricJsonString);
var sourceLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.lyric").Value<string>()));
var translateLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.trans").Value<string>()));
var sourceLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.lyric")!.Value<string>()));
var translateLyric = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(lyricJsonObj.SelectToken("$.trans")!.Value<string>()));
return _lyricsItemCollectionFactory.Build(sourceLyric, translateLyric);
}
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);
}

View File

@@ -24,7 +24,7 @@ namespace ZonyLrcTools.Common.MusicDecryption
await file.CopyToAsync(buffer);
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);
if (!File.Exists(outFilePath))

View File

@@ -1,9 +1,11 @@
using System.Text.RegularExpressions;
namespace ZonyLrcTools.Common
{
/// <summary>
/// 歌曲信息的承载类,携带歌曲的相关数据。
/// </summary>
public class MusicInfo
public partial class MusicInfo
{
/// <summary>
/// 歌曲对应的物理文件路径。
@@ -30,6 +32,11 @@ namespace ZonyLrcTools.Common
/// </summary>
public bool IsSuccessful { get; set; } = true;
/// <summary>
/// 是否时纯音乐?
/// </summary>
public bool IsPruneMusic { get; set; } = false;
/// <summary>
/// 构建一个新的 <see cref="MusicInfo"/> 对象。
/// </summary>
@@ -38,9 +45,17 @@ namespace ZonyLrcTools.Common
/// <param name="artist">歌曲的作者。</param>
public MusicInfo(string filePath, string name, string artist)
{
FilePath = filePath;
FilePath = Path.Combine(Path.GetDirectoryName(filePath)!, HandleInvalidFilePath(Path.GetFileName(filePath)));
Name = name;
Artist = artist;
}
private string HandleInvalidFilePath(string srcText)
{
return InvalidFilePathRegex().Replace(srcText, "");
}
[GeneratedRegex(@"[<>:""/\\|?*]")]
private static partial Regex InvalidFilePathRegex();
}
}

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

View File

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

View File

@@ -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(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");
});
}
}

View File

@@ -0,0 +1,5 @@
// namespace ZonyLrcTools.Common.MusicScanner;
//
// public class QQMusicSongListMusicScanner : IMusicScanner
// {
// }

View File

@@ -22,11 +22,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
_wordsDictionary = new Lazy<Dictionary<string, string>>(() =>
{
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))
{

View File

@@ -25,7 +25,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
_options = options.Value;
}
public async ValueTask<MusicInfo> LoadAsync(string filePath)
public async ValueTask<MusicInfo?> LoadAsync(string filePath)
{
await ValueTask.CompletedTask;

View File

@@ -13,6 +13,6 @@
/// </remarks>
/// <param name="key">原始单词。</param>
/// <returns>原始单词对应的屏蔽词。</returns>
string GetValue(string key);
string? GetValue(string key);
}
}

View File

@@ -18,6 +18,6 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
/// </summary>
/// <param name="filePath">歌曲文件的路径。</param>
/// <returns>加载完成的歌曲信息。</returns>
ValueTask<MusicInfo> LoadAsync(string filePath);
ValueTask<MusicInfo?> LoadAsync(string filePath);
}
}

View File

@@ -14,7 +14,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
public string Name => ConstantName;
public const string ConstantName = "Taglib";
public async ValueTask<MusicInfo> LoadAsync(string filePath)
public async ValueTask<MusicInfo?> LoadAsync(string filePath)
{
try
{
@@ -30,12 +30,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Tag
await ValueTask.CompletedTask;
if (songName == null && songArtist == null)
{
return null;
}
return new MusicInfo(filePath, songName, songArtist);
return songName == null ? null : new MusicInfo(filePath, songName, songArtist);
}
catch (Exception ex)
{

View File

@@ -23,6 +23,11 @@ public class DefaultUpdater : IUpdater, ISingletonDependency
public async Task CheckUpdateAsync()
{
if (!IsCheckUpdate())
{
return;
}
var response = await _warpHttpClient.GetAsync<NewVersionResponse?>(UpdateUrl);
if (response == null)
{
@@ -36,7 +41,7 @@ public class DefaultUpdater : IUpdater, ISingletonDependency
}
var importantItem = response.Items?.FirstOrDefault(x => x.ItemType == NewVersionItemType.Important);
if (importantItem != null)
if (importantItem?.Url != null)
{
_logger.LogWarning($"发现了新版本,请点击下面的链接进行更新:{importantItem.Url}");
_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;
}
}

View File

@@ -4,15 +4,18 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>4.0.0.50</Version>
<Version>4.0.0.55</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="MusicDecrypto.Library" Version="2.2.0" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>
@@ -20,8 +23,4 @@
<Compile Remove="Lyrics\Providers\Kugeci\KugeciDownloader.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\vendor\MusicDecrypto\MusicDecrypto.Library\MusicDecrypto.Library.csproj" />
</ItemGroup>
</Project>

View File

@@ -8,14 +8,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SuperSocket.WebSocket" Version="2.0.0-beta.11" />

View File

@@ -1055,9 +1055,9 @@
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
@@ -1877,7 +1877,7 @@ batch@0.6.1:
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
binary-extensions@^2.0.0:
@@ -2650,7 +2650,7 @@ emoji-regex@^8.0.0:
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
encodeurl@~1.0.2:
@@ -3484,9 +3484,9 @@ json-schema-traverse@^1.0.0:
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
@@ -3545,9 +3545,9 @@ loader-runner@^4.1.0, loader-runner@^4.2.0:
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
version "1.4.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
@@ -3749,9 +3749,9 @@ minimatch@^3.1.1:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minipass@^3.1.1:
version "3.3.4"
@@ -5508,9 +5508,9 @@ webpack-virtual-modules@^0.4.2:
integrity sha512-8bWq0Iluiv9lVf9YaqWQ9+liNgXSHICm+rg544yRgGYaR8yXZTVBaHZkINZSB2yZSWo4b0F6MIxqJezVfOEAlg==
webpack@^5.54.0:
version "5.74.0"
resolved "https://registry.npmmirror.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"

View File

@@ -2,6 +2,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using QRCoder;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Infrastructure.IO;
@@ -27,5 +28,14 @@ namespace ZonyLrcTools.Tests
File.Delete(tempMusicFilePath);
}
[Fact]
public void TestConsole()
{
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode("https://y.music.163.com/m/login?codekey=2f0da1d0-759e-478b-9153-35058b3", QRCodeGenerator.ECCLevel.L);
AsciiQRCode qrCode = new AsciiQRCode(qrCodeData);
string qrCodeAsAsciiArt = qrCode.GetGraphic(1, drawQuietZones: false);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -20,7 +21,7 @@ namespace ZonyLrcTools.Tests.Infrastructure.Album
albumBytes.Length.ShouldBeGreaterThan(0);
// 显示具体的图像。
var tempAlbumPath = Path.Combine(Directory.GetCurrentDirectory(), "tempAlbum.png");
var tempAlbumPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tempAlbum.png");
File.Delete(tempAlbumPath);
await using var file = File.Create(tempAlbumPath);

View File

@@ -12,7 +12,7 @@ namespace ZonyLrcTools.Tests.Infrastructure.Exceptions
ErrorCodeHelper.LoadErrorMessage();
ErrorCodeHelper.ErrorMessages.ShouldNotBeNull();
ErrorCodeHelper.ErrorMessages.Count.ShouldBe(16);
ErrorCodeHelper.ErrorMessages.Count.ShouldBe(17);
}
[Fact]

View File

@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Lyrics;
namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
@@ -25,5 +26,17 @@ namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
lyric.ShouldNotBeNull();
lyric.IsPruneMusic.ShouldBe(false);
}
[Fact]
public async Task DownloadAsync_Issue133_Test()
{
await Should.ThrowAsync<ErrorCodeException>(_lyricsProvider.DownloadAsync("天ノ弱", "漆柚").AsTask);
}
[Fact]
public async Task DownloadAsync_Index_Exception_Test()
{
await Should.ThrowAsync<ErrorCodeException>(async () => await _lyricsProvider.DownloadAsync("40'z", "ZOOLY"));
}
}
}

View File

@@ -25,4 +25,13 @@ public class KuWoLyricsProviderTests : TestBase
lyric.ShouldNotBeNull();
lyric.IsPruneMusic.ShouldBeFalse();
}
[Fact]
public async Task DownloadAsync_Source_Null_Test()
{
var lyric = await _kuwoLyricsProvider.DownloadAsync("Concerto for Piano and Orchestra No. 12 in A major, K414 - 1. Allegro",
"Wolfgang Amadeus Mozart");
lyric.IsPruneMusic.ShouldBeTrue();
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Lyrics;
namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
@@ -103,7 +104,51 @@ namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
public async Task DownloadAsync_Issue123_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("橄榄树", "苏曼");
lyric.ToString().ShouldNotBeNullOrEmpty();
}
[Fact]
public async Task DownloadAsync_Issue133_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("Everything", "Yinyues");
lyric.IsPruneMusic.ShouldBeTrue();
}
[Fact]
public async Task DownloadAsync_NullException_Test()
{
var result = await Should.ThrowAsync<ErrorCodeException>(_lyricsProvider.DownloadAsync("創世記", "りりィ").AsTask);
result.ErrorCode.ShouldBe(ErrorCodes.NoMatchingSong);
}
[Fact]
public async Task DownloadAsync_Source_Null_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("Concerto for Piano and Orchestra No. 12 in A major, K414 - 1. Allegro",
"Wolfgang Amadeus Mozart");
lyric.IsPruneMusic.ShouldBeTrue();
}
[Fact]
public async Task DownloadAsync_Issue_20230429_01_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("Don't Go Away", "By2 Twins+《Twins》");
lyric.ShouldContain(x => x.LyricText.Contains("吴庆隆"));
}
[Fact]
public async Task DownloadAsync_Issue_20230429_02_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("Hanging On (2009)", "Britt Nicole");
lyric.ShouldContain(x => x.LyricText.Contains("You see my anxious heart"));
}
[Fact]
public async Task DownloadAsync_Issue_20230429_03_Test()
{
var lyric = await _lyricsProvider.DownloadAsync("Your Heart Is A Muscle (2012)", "Carly Rae Jepsen");
lyric.IsPruneMusic.ShouldBeFalse();
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Lyrics;
namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
@@ -37,5 +38,11 @@ namespace ZonyLrcTools.Tests.Infrastructure.Lyrics
lyric.IsPruneMusic.ShouldBeFalse();
lyric.ToString().ShouldContain("你好像快要不能呼吸");
}
[Fact]
public async Task DownloadAsync_Issue133_Test()
{
await Should.ThrowAsync<ErrorCodeException>(async () => await _lyricsProvider.DownloadAsync("天ノ弱", "漆柚"));
}
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
using ZonyLrcTools.Cli.Infrastructure.Tag;
using ZonyLrcTools.Common.TagInfo;
namespace ZonyLrcTools.Tests.Infrastructure.Tag
@@ -16,7 +16,7 @@ namespace ZonyLrcTools.Tests.Infrastructure.Tag
var tagLoader = ServiceProvider.GetRequiredService<ITagLoader>();
tagLoader.ShouldNotBeNull();
var info = await tagLoader.LoadTagAsync(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "曾经艺也 - 荀彧(纯音乐版).mp3"));
var info = await tagLoader.LoadTagAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MusicFiles", "曾经艺也 - 荀彧(纯音乐版).mp3"));
info.ShouldNotBeNull();
info.Name.ShouldBe("荀彧(纯音乐版)");
info.Artist.ShouldBe("曾经艺也");

View File

@@ -0,0 +1,15 @@
using Shouldly;
using Xunit;
using ZonyLrcTools.Common;
namespace ZonyLrcTools.Tests;
public class MusicInfoTests
{
[Fact]
public void InvalidFilePathTest()
{
var musicInfo = new MusicInfo("C:\\Users\\Zony\\Music\\[ZonyLrcTools]:? - 01. 你好.mp3", "你好", "Zony");
musicInfo.FilePath.ShouldBe(@"C:\Users\Zony\Music\[ZonyLrcTools] - 01. 你好.mp3");
}
}

View File

@@ -0,0 +1,21 @@
using System.IO;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.MusicScanner;
namespace ZonyLrcTools.Tests.MusicScanner;
public class CsvFileMusicScannerTests : TestBase
{
[Fact]
public async Task GetMusicInfoFromCsvFileAsync_Test()
{
var musicScanner = GetService<CsvFileMusicScanner>();
var musicInfo = await musicScanner.GetMusicInfoFromCsvFileAsync(Path.Combine("TestData", "test.csv"), "DownloadedLrc", "{Artist} - {Name}.lrc");
musicInfo.ShouldNotBeNull();
musicInfo.Count.ShouldBeGreaterThan(0);
musicInfo.Count.ShouldBe(5);
}
}

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZonyLrcTools.Common.MusicScanner;
namespace ZonyLrcTools.Tests.MusicScanner;
public class NetEaseMusicSongListMusicScannerTests : TestBase
{
// [Fact]
// public async Task GetMusicInfoFromNetEaseMusicSongListAsync_Test()
// {
// var musicScanner = GetService<NetEaseMusicSongListMusicScanner>();
// var musicInfo = await musicScanner.GetMusicInfoFromNetEaseMusicSongListAsync("7224428149", "DownloadedLrc", "{Artist} - {Name}.lrc");
//
// musicInfo.ShouldNotBeNull();
// musicInfo.Count.ShouldBeGreaterThan(10);
// }
}

View File

@@ -0,0 +1,6 @@
Song,Artist
刀马旦,李玟
发如雪,周杰伦
说书人,寅子
爱的供养,张国荣
七里香,周杰伦
1 Song Artist
2 刀马旦 李玟
3 发如雪 周杰伦
4 说书人 寅子
5 爱的供养 张国荣
6 七里香 周杰伦

View File

@@ -7,14 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Shouldly" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -33,6 +33,9 @@
<Content Include="MusicFiles\Loren Gray - Queen.ncm">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Update="TestData\test.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Submodule vendor/MusicDecrypto deleted from c6f18309dd

View File

@@ -1,11 +1,8 @@
New Features:
- CLI 提供了一个更新检查功能,在程序启动的时候会检测最新的版本。
- 支持从酷我音乐搜索歌词。
New Features: NONE
Breaking Changes: None
Enhancement:
- 基于 MusicDecrypto 提供歌曲解密功能,支持 40 种格式的文件,具体支持格式请自己尝试
- 支持 UTF-8 BOM 编码,以解决部分设备乱码的问题
Fixed Bugs:
- None

View File

@@ -1,13 +1,13 @@
New Features:
- CLI 提供了一个更新检查功能,在程序启动的时候会检测最新的版本。
- 支持从酷我音乐搜索歌词。
New Features: None
Breaking Changes: None
Enhancement:
Enhancement: :
- 基于 MusicDecrypto 提供歌曲解密功能,支持 40 种格式的文件,具体支持格式请自己尝试。
- 网易云歌单下载歌词时,支持使用 `;` 传递多个歌单 ID。(`15445646;468435123;4131357`) - [`41cba02`](https://github.com/real-zony/ZonyLrcToolsX/commit/41cba028333d0cf65f7dc3ee466660cc20e558fc)
- 优化了结果提示信息,更加明确。- [`1e5c418`](https://github.com/real-zony/ZonyLrcToolsX/commit/1e5c41852f70df4b824d2795f175e57c82a0d104)
- 优化了程序的启动时间,不会每次启动都去检测更新了。- [`b240564`](https://github.com/real-zony/ZonyLrcToolsX/commit/b240564cf7ac3432715dd54c280d73f9f985c4fc)
Fixed Bugs:
- None
- 更新了依赖的 NuGet 程序包版本。- [`1f312c7`](https://github.com/real-zony/ZonyLrcToolsX/commit/1f312c749d8e7784e7670ec3821db0b4b430ce2e)
- 过滤了包含特殊符号的文件路径,避免报错。 - [`62d08df`](https://github.com/real-zony/ZonyLrcToolsX/commit/62d08df7353343711741b23a6ecb9c3908754075)
- 修复了从网易云歌单下载歌曲时,将专辑名称当作歌手名称进行取值。 - [`98c935e`](https://github.com/real-zony/ZonyLrcToolsX/commit/98c935ed93f9278e8606c056868824ea8335ee5d)