139 Commits

Author SHA1 Message Date
real-zony
bc5afad599 ci: Fixed the path problem 2022-12-13 22:54:19 +08:00
real-zony
34e8cf74f3 ci: Trigger a build action. 2022-12-13 22:49:28 +08:00
real-zony
18338d5989 ci: Should checkout the submodule. 2022-12-13 22:46:55 +08:00
real-zony
691cfa159f ci: Release 4.0.0.50 version. 2022-12-13 22:44:16 +08:00
real-zony
7dce16fe1f ci: Using .NET 7.0 as the compilation backend. 2022-12-13 22:41:00 +08:00
real-zony
fd86a98995 test: Fixed the errors in unit tests. 2022-12-13 22:39:20 +08:00
real-zony
2f2e67f3e6 fix: Fixed the issue of incorrect file extension preventing file search. 2022-12-13 22:33:34 +08:00
real-zony
a46abba2f9 feat: Use MusicDecrypto as a decryption driver. 2022-12-11 21:28:25 +08:00
real-zony
fdc5f27692 build: Updated the .NET runtime to version 7.0. 2022-12-11 20:35:37 +08:00
real-zony
430c66786a build: Updated the code for MusicDecrypto. 2022-12-11 20:28:32 +08:00
real-zony
75fd004c56 chore: Add MusicDecrypto as song decryption library. 2022-11-02 11:06:41 +08:00
real-zony
ca40728387 Merge remote-tracking branch 'origin/dev' into dev 2022-10-28 15:15:49 +08:00
real-zony
acb9142e5f feat: Completed KuWo lyric source. 2022-10-28 15:15:34 +08:00
real-zony
2c1b5ce533 feat(Completed KuWo lyric source.): 2022-10-28 15:09:20 +08:00
real-zony
5d1e90f638 refactor: Use Object to replace byte arrays when generating lyrics. 2022-10-28 10:13:14 +08:00
real-zony
b7b1f36bf5 feat: Added KuWo(酷我) Music lyrics source (incomplete). 2022-10-26 17:05:44 +08:00
real-zony
6b72f919b8 refactor: Refactor the code of the Download command. 2022-10-23 22:48:06 +08:00
real-zony
64d26cbc4c refactor: Refactored lyrics download logic and file scanning logic. 2022-10-23 19:28:57 +08:00
real-zony
f9570508c2 ci: Update github actions yaml file. 2022-10-23 13:53:31 +08:00
real-zony
6d37d81015 chore: Added a release.bak.md file to prevent triggering Github Actions. 2022-10-23 13:52:02 +08:00
real-zony
7778b7be09 chore: Some code tweaks. 2022-10-23 13:51:24 +08:00
real-zony
7adda14713 feat: Added axios library. 2022-10-23 13:50:52 +08:00
real-zony
7d17fc0b97 refactor: Refactor lyrics download logic and abstract a new logger. 2022-10-23 13:38:51 +08:00
real-zony
3e27e18098 refactor: Renamed Lyric to Lyrics. 2022-10-23 12:42:46 +08:00
real-zony
895f68184d feat: Created a RESTFul API endpoint for MusicInfo. 2022-10-23 12:39:56 +08:00
real-zony
d19b1d8d2a feat: Add auto-detect update feature. 2022-10-18 14:40:03 +08:00
real-zony
a11ef70021 feat: Completed early communication testing.
data-table has serious performance problem.
2022-10-06 21:35:55 +08:00
real-zony
6554be6b6d build: Update package version. 2022-10-06 17:29:29 +08:00
real-zony
0ed21ed997 build: Sync package version. 2022-10-06 13:04:12 +08:00
real-zony
740e8f4c63 refactor: Common components are moved to the Common library. 2022-10-06 13:02:20 +08:00
real-zony
ecab0e0f5c refactor: Move some of the dependency injection code to the Common Library. 2022-10-06 12:45:01 +08:00
real-zony
aa90e232f7 chore: Move the related configuration items to the Common library. 2022-10-06 12:29:56 +08:00
real-zony
9f96aa0186 refactor: Move the related configuration items to the Common library. 2022-10-06 12:27:21 +08:00
real-zony
bccfaaaa5b feat: Integrated SPA resources with ASP.NET Core. 2022-10-05 21:10:15 +08:00
real-zony
f57afd4238 feat: Establish Websocket communication. 2022-10-05 20:25:55 +08:00
real-zony
775e6140ee feat: Adjust UI component linkage. 2022-10-05 20:02:15 +08:00
real-zony
2ccf4f9da6 feat: Completed the basic UI layout. 2022-10-05 17:50:23 +08:00
real-zony
dbc1e44d2a Added WebSocket listener service. 2022-10-05 10:36:42 +08:00
real-zony
cde1d32d33 feat: Add new frontend code. 2022-10-01 14:01:46 +08:00
real-zony
62777e44bd fix: Remove frontend code. 2022-10-01 11:51:07 +08:00
real-zony
6c25483f1e ci: Release new version。 2022-09-24 19:20:19 +08:00
real-zony
b64f83d56b fix: Removed NAudio related code. 2022-09-24 19:19:14 +08:00
real-zony
6ad154a62b feat: Add update check logic. 2022-09-24 19:13:57 +08:00
real-zony
32adab68b6 docs: Update README.md file.
Added start history section.
2022-09-23 10:44:05 +08:00
real-zony
ba91108ca4 fix: Fixed jobs variable share problem. 2022-09-22 19:45:34 +08:00
real-zony
f597e24ff7 ci: Fixed path issue. 2022-09-22 19:17:21 +08:00
real-zony
d453d520d4 ci: debug workflow. 2022-09-22 18:50:27 +08:00
real-zony
9f254aae8e ci: debug workflow. 2022-09-22 18:50:11 +08:00
real-zony
c9fa0de95f ci: Build new version. 2022-09-22 18:44:42 +08:00
real-zony
6d7ee04b74 feat: Support song depth search. 2022-09-22 18:43:42 +08:00
real-zony
f519eb1251 feat(Test NetEaseMusic's Romance lyrics.): 2022-09-22 18:08:26 +08:00
real-zony
85f325b300 fix: Fixed null reference exception. 2022-09-22 17:53:19 +08:00
real-zony
140043db79 feat: Enhanced song matching logic (NetEaseMusic). 2022-09-22 17:49:54 +08:00
real-zony
c583a9c278 build: Upgrade dependency nuget packages. 2022-09-22 16:11:53 +08:00
real-zony
8385f6f118 feat: Provide the 'isOnlyOutputTranslation option to output only translated lyrics. 2022-09-22 15:48:20 +08:00
real-zony
be4380c744 fix: Fixed issue #106. 2022-09-22 15:30:28 +08:00
real-zony
87f6a98668 fix: Fixed the issue with #82.
Reason may be that the song name has brackets, causing the search to
failure.
2022-09-22 15:06:54 +08:00
real-zony
83fcc91fb8 ci: Fixed github workflow file errors. 2022-09-22 14:51:17 +08:00
real-zony
b4c229ccca ci: test release. 2022-09-22 14:45:36 +08:00
real-zony
71302c7262 Merge branch 'dev' of https://github.com/real-zony/ZonyLrcToolsX into dev 2022-09-22 14:43:13 +08:00
real-zony
1bec50e409 ci: update workflow. 2022-09-22 14:42:48 +08:00
Zony
f7cae8062c Update release.md 2022-09-22 14:28:37 +08:00
real-zony
c73f0d8eea ci: debug workflow. 2022-09-22 14:28:15 +08:00
real-zony
fe7c9115d5 ci: debug workflow. 2022-09-22 14:24:45 +08:00
real-zony
e13fb47aeb ci: debug workflow. 2022-09-22 13:30:34 +08:00
real-zony
32891d4208 ci: debug workflow. 2022-09-22 13:27:08 +08:00
real-zony
770d246787 ci: debug workflow. 2022-09-22 13:22:59 +08:00
real-zony
589151a77d ci: debug workflow. 2022-09-22 13:21:07 +08:00
real-zony
65af048f72 ci: debug workflow. 2022-09-22 13:12:49 +08:00
real-zony
99fd3b5021 ci: debug workflow. 2022-09-22 13:09:41 +08:00
real-zony
e4420e749a ci: debug workflow. 2022-09-22 13:02:33 +08:00
real-zony
5f22dafb1e ci: debug workflow. 2022-09-22 13:01:03 +08:00
real-zony
c2d8ee4341 ci: debug workflow. 2022-09-22 12:57:14 +08:00
real-zony
6a4435eb0d ci: debug workflow. 2022-09-22 12:51:50 +08:00
real-zony
346e07e9e7 ci: debug workflow. 2022-09-22 12:49:42 +08:00
real-zony
efdb3e8441 ci: debug workflow. 2022-09-22 12:45:25 +08:00
real-zony
0438433284 ci: debug workflow. 2022-09-22 12:40:52 +08:00
real-zony
d34a830660 ci: debug workflow. 2022-09-22 12:39:44 +08:00
real-zony
975f61a8e7 ci: debug workflow. 2022-09-22 12:37:37 +08:00
real-zony
8ba78c2681 ci: debug workflows. 2022-09-22 12:35:16 +08:00
real-zony
019bdb571c ci: debug workflow. 2022-09-22 12:31:23 +08:00
real-zony
a2aeed021d ci: debug workflow. 2022-09-22 12:28:15 +08:00
real-zony
0e8d361ed9 ci: debug workflow. 2022-09-22 12:24:27 +08:00
real-zony
c3b381a5f7 ci: debug workflow. 2022-09-22 12:23:09 +08:00
real-zony
8acba586b0 ci: debug workflow. 2022-09-22 12:12:16 +08:00
real-zony
5905d4b384 ci: debug workflow. 2022-09-22 12:03:06 +08:00
real-zony
2ad771e62e ci: debug workflow. 2022-09-22 11:49:11 +08:00
real-zony
403d7b151d ci: debug workflow. 2022-09-22 11:47:03 +08:00
real-zony
5c8c9af9ab ci: debug workflow. 2022-09-22 11:40:09 +08:00
real-zony
bfd23fc14e ci: debug workflow. 2022-09-22 11:39:01 +08:00
real-zony
81271a5aa5 ci: debug workflow. 2022-09-22 11:36:17 +08:00
real-zony
80a0bcdcee ci: debug workflow. 2022-09-22 11:34:14 +08:00
real-zony
172174d9e3 ci: debug github actions. 2022-09-21 23:00:29 +08:00
real-zony
d4a6a46078 feat: Restore config yaml file. 2022-07-25 20:12:19 +08:00
real-zony
0772f5888f fix: Fix the problem of QQ lyrics downloader prompting error.
The reason for this is that the old API has been deprecated.
2022-07-25 20:11:30 +08:00
real-zony
950652c040 fix: Fixed the issue of inaccurate download results. 2022-07-25 19:45:35 +08:00
real-zony
55f720c1a1 Fixed the null reference exception prompted when downloading pure music. 2022-07-24 23:48:46 +08:00
real-zony
7463709a39 feat: Deleted unused components. 2022-04-28 23:58:03 +08:00
real-zony
07e660e13b Add new frontend code. 2022-04-28 23:53:14 +08:00
real-zony
a51b399a6c Delete frontend code. 2022-04-28 23:50:18 +08:00
real-zony
279eba48f8 feat: Support configure lyrics file encoding. 2022-04-27 21:38:51 +08:00
real-zony
f3b1dacb0c fix: Improve tag loader conditions. 2022-04-27 21:25:15 +08:00
real-zony
559efff928 style: remove ui project from solution file. 2022-04-26 10:28:52 +08:00
real-zony
8370172aef Ignore unit test. 2022-04-26 10:26:38 +08:00
real-zony
cb8572e599 fix: Fixed the number of error hints. 2022-04-26 10:25:53 +08:00
real-zony
3b0f9fa89b fix: Fixed an issue with netease music downloader. 2022-04-25 23:24:13 +08:00
real-zony
c655ac4cbb docs: Update readme doc. 2022-04-25 13:41:40 +08:00
Zony
893b1e7918 Create dotnet.yml 2022-04-25 13:35:55 +08:00
real-zony
e900a92f37 enhanced: change the console theme color. 2022-04-25 12:26:06 +08:00
real-zony
0f84621919 fix: Fixed multi-downloader not working problem. 2022-04-25 12:16:00 +08:00
real-zony
61ca863770 feat: Support skip exist lyric files. 2022-04-25 12:03:59 +08:00
real-zony
2044a0b8fa add web ui initialize code. 2022-04-23 21:11:18 +08:00
real-zony
ffd76f5f2b enhanced: Optimized tag reader workflow.
Closes #93
2022-04-12 10:15:23 +08:00
real-zony
3705c6c8b5 perf: Optimized error log information. 2022-04-12 09:57:49 +08:00
real-zony
f8531e82ff Merge remote-tracking branch 'origin/dev' into dev 2022-04-07 16:50:05 +08:00
real-zony
969d5e9e19 Fixed tag info provider bug. 2022-04-07 16:49:49 +08:00
Zony
88ed247430 Update README.md 2022-03-24 11:49:46 +08:00
real-zony
be68b3474c fix: Fixed the error in the count of failure times. 2022-03-22 22:11:02 +08:00
real-zony
363d104dbe refactor: Configuration class unified storage. 2022-03-20 19:27:26 +08:00
real-zony
a1e9669843 feat: Fixed a possible null reference exception. 2022-03-20 19:10:51 +08:00
real-zony
2b41658783 fix: Fixed the problem that the default line break is invalid. 2022-03-20 19:06:09 +08:00
real-zony
02f82e43d3 docs: Update readme document. 2022-03-20 11:05:28 +08:00
real-zony
dbe63c390f Merge remote-tracking branch 'origin/dev' into dev 2022-03-20 11:04:30 +08:00
real-zony
9f70d5d379 fix: Resolve possible NullReferenceException issues.
Closes https://github.com/real-zony/ZonyLrcToolsX/issues/85
2022-03-20 11:04:17 +08:00
Zony
8010333485 Update README.md 2022-03-19 22:47:47 +08:00
Zony
454cd58f97 Update README.md 2022-03-19 16:22:23 +08:00
real-zony
cf8d2f8515 feat: Add donation QR code. 2022-03-19 16:20:38 +08:00
real-zony
4b91c3e25b docs: Update readme. 2022-03-19 13:39:24 +08:00
real-zony
1840a10f1e fix: Fixed the problem of incorrect lyric data for dual language lyrics when the OneLine option is turned on. 2022-03-19 12:03:29 +08:00
real-zony
d250524208 feat: QQ Music Downloader support dual language lyrics. 2022-03-19 11:42:00 +08:00
real-zony
2c2c34f1a6 feat: NetEase Cloud Music Downloader support dual language lyrics. 2022-03-19 11:38:10 +08:00
real-zony
c380fd83f6 Delete unwanted files. 2022-03-19 11:01:16 +08:00
real-zony
e12264466e docs: Provides new documentation for profile instructions. 2022-03-18 21:11:44 +08:00
real-zony
2b0b14cd7a Use YAML as configuration file; fix a download failure issue with QQ Music. (Reason: QQ Music API changed) 2022-03-18 19:51:08 +08:00
real-zony
d1c50323b1 refactor: Change the file structure. 2022-03-17 22:01:06 +08:00
real-zony
e06f9eaaa9 build: Upgrade the dependent NuGet packages. 2022-03-17 20:18:57 +08:00
real-zony
ed1974dfbc refactor: Rename main method. 2022-02-11 11:52:35 +08:00
real-zony
ba3dd5f1bc refactor: reformat source code. 2022-02-03 18:10:53 +08:00
real-zony
089559d058 test: Try to reproduce the issue 84. 2022-02-03 17:17:49 +08:00
182 changed files with 9043 additions and 1545 deletions

63
.github/workflows/dotnet.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: .NET
on:
push:
branches: [ dev ]
paths:
- "versions/release.md"
pull_request:
branches: [ dev ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get build version
id: date
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
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Publish
working-directory: ./src/ZonyLrcTools.Cli
run: |
ls -a
chmod +x ./publish.sh
./publish.sh
mv ./TempFiles ../../
shell: bash
env:
PUBLISH_VERSION: ${{ steps.date.outputs.date }}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: release-files
path: |
./TempFiles
./versions/release.md
outputs:
version: ${{ steps.date.outputs.date }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: release-files
path: .
- name: Upload Release
uses: ncipollo/release-action@v1
with:
artifacts: "./TempFiles/*.zip"
token: ${{ secrets.GITHUBACTIONS }}
tag: ZonyLrcToolsX_Alpha.${{ needs.build.outputs.version }}
commit: dev
bodyFile: ./versions/release.md

2
.gitignore vendored
View File

@@ -450,3 +450,5 @@ fabric.properties
.idea/ .idea/
/.idea /.idea
/src/ZonyLrcTools.Cli/TempFiles/ /src/ZonyLrcTools.Cli/TempFiles/
src/ZonyLrcTools.LocalServer/UiStaticResources/*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "vendor/MusicDecrypto"]
path = vendor/MusicDecrypto
url = https://github.com/davidxuang/MusicDecrypto

128
README.md
View File

@@ -1,4 +1,9 @@
简体中文 | [English](./en_US.md) 简体中文 | [English](./docs/en_US.md)
## 免责声明
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等)[本人](https://github.com/real-zony) 不承担任何法律责任。
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
## 简介 ## 简介
@@ -7,56 +12,127 @@ ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
🚧 当前版本正在开发当中。 🚧 当前版本正在开发当中。
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。 🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
## 下载
工具会执行每日构建动作,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
## 用法 ## 用法
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell (部分用户可能显示的是 *命令提示符*),根据下述说明执行命令即可。 Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell/命令提示符/Windows 终端,根据下述说明执行命令即可。
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。 macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
### 命令 ### 命令
#### 歌下载 #### 歌下载
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。 子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
```shell ```shell
./ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number] .\ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
./ZonyLrcTools.Cli.exe download -h|--help .\ZonyLrcTools.Cli.exe download -h|--help
``` ```
**例子** **例子**
```shell ```shell
# 下载歌词 # 下载歌词
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2 .\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2
# 下载专辑封面 # 下载专辑封面
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2 .\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2
``` ```
#### 加密格式转换 #### 加密格式转换
目前软件支持 NCM、QCM(开发中...🚧) 格式的音乐文件转换,命令如下 子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**
具体支持的格式请参考项目 [MusicDecrypto](https://github.com/davidxuang/MusicDecrypto/blob/master/MusicDecrypto.Library/DecryptoFactory.cs#L23),本工具仅做一个集成,替换掉原本自己的一些实现。现在不需要指定对应的类型参数,程序会自动根据文件后缀选择适合的解密算法。
命令只需要一个参数 `-s`,指定需要转换的文件夹或者是文件路径。
```shell ```shell
./ZonyLrcTools.Cli.exe util -t=Ncm D:\CloudMusic .\ZonyLrcTools.Cli.exe util -s D:\CloudMusic
``` ```
### 配置文件 ### 配置文件
程序的部分配置信息需要`appsettings.json` 进行更改,下面标注了各个配置的说明。 程序的所有的配置信息,都`config.yaml` 进行更改,下面标注了各个配置的说明。
| 属性 | 说明 | 示例值 | 其中是否开启的可选项为 `true` 或者 `false`,等同于中文的是和否。
| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------- |
| ToolOption.SupportFileExtensions | 允许扫描的歌曲文件后缀名,以 `;` 号隔开多个后缀。 | `*.mp3;*.flac` | ```yaml
| ToolOption.NetworkOptions.Enable | 是否启用 HTTP 网络代理服务true 表示启用false 表示禁用。 | false | globalOption:
| ToolOption.NetworkOptions.ProxyIp | HTTP 网络代理服务的 IP`Enable` 为 false 时会忽略该属性值。 | 127.0.0.1 | # 允许扫描的歌曲文件后缀名。
| ToolOption.NetworkOptions.ProxyPort | HTTP 网络代理服务的 端口,在 `Enable` 为 false 时会忽略该属性值。 | 8080 | supportFileExtensions:
| TagInfoProviderOptions.FileNameRegularExpressions | 文件名 Tag 标签信息读取器使用,使用正则表达式匹配歌曲名和歌手,请使用命名分组编写正则表达式。 | (?'artist'.+)\\s-\\s(?'name'.+) | - '*.mp3'
| LyricDownloader.[n].Name | 指定歌词下载器的配置项标识,对应具体的歌词下载器。 | NetEase 或 QQ | - '*.flac'
| LyricDownloader.[n].Priority | 指定歌词下载器的优先级,按升序排列,如果值设置为 `-1` 则代表禁用。 | `1` | - '*.wav'
| BlockWordOptions.IsEnable | 是否启用屏蔽词词典。 | false | # 网络代理服务设置,仅支持 HTTP 代理。
| BlockWordOptions.BlockWordDictionaryFile | 屏蔽词词典的位置。 | `./BlockWords.json` | networkOptions:
isEnable: false # 是否启用代理。
ip: 127.0.0.1 # 代理服务 IP 地址。
port: 4780 # 代理服务端口号。
updateUrl: https://api.myzony.com/lrc-tools/update # 更新检查地址。
# 下载器的相关参数配置。
provider:
# 标签扫描器的相关参数配置。
tag:
# 支持的标签扫描器。
plugin:
- name: Taglib # 基于 Taglib 库的标签扫描器。
priority: 1 # 优先级,升序排列。
- name: FileName # 基于文件名的标签扫描器。
priority: 2
# 基于文件名扫描器的扩展参数。
extensions:
# 正则表达式,用于匹配文件名中的作者信息和歌曲信息,可根据
# 自己的需求进行调整。
regularExpressions: "(?'artist'.+)\\s-\\s(?'name'.+)"
# 歌曲标签屏蔽字典替换功能。
blockWord:
isEnable: false # 是否启用屏蔽字典。
filePath: 'BlockWords.json' # 屏蔽字典的路径。
# 歌词下载器的相关参数配置。
lyric:
# 支持的歌词下载器。
plugin:
- name: NetEase # 基于网易云音乐的歌词下载器。
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
- name: QQ # 基于 QQ 音乐的歌词下载器。
priority: 2
# depth: 10 # 暂时不支持。
- name: KuGou # 基于酷狗音乐的歌词下载器。
priority: 3
depth: 10
- name: KuWo # 基于酷我音乐的歌词下载器。
priority: 4
depth: 10
# 歌词下载的一些共有配置参数。
config:
isOneLine: true # 双语歌词是否合并为一行展示。
lineBreak: "\n" # 换行符的类型,记得使用双引号指定。
isEnableTranslation: true # 是否启用翻译歌词。
isOnlyOutputTranslation: false # 是否只输出翻译歌词。
isSkipExistLyricFiles: false # 如果歌词文件已经存在,是否跳过这些文件。
fileEncoding: 'utf-8' # 歌词文件的编码格式。
```
#### 支持的编码格式
详细信息请参考: [MSDN Encoding 列表](https://learn.microsoft.com/en-us/dotnet/api/System.Text.Encoding.GetEncodings?view=net-6.0#examples),使用 `identifier and name` 作为参数值填入 `config.yaml` 文件当中的 `fileEncoding`
#### 支持的歌词源
| 歌词源 | 默认优先级 |
| ---------- | ---------- |
| 网易云音乐 | 1 |
| QQ 音乐 | 2 |
| 酷狗音乐 | 3 |
| 酷我音乐 | 4 |
### 屏蔽字典 ### 屏蔽字典
@@ -72,10 +148,14 @@ macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命
## 捐赠 ## 捐赠
暂无 <img src="./docs/img/alipay.jpg" width="200"/><img src="./docs/img/wechat.jpg" width="200"/>
## 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] 支持跨平台的 CLI 工具。
- [ ] 基于 Web GUI 的操作站点。 - [x] 基于 Web GUI 的操作站点。
- [ ] 支持插件系统(Lua 引擎)。 - [ ] 支持插件系统(Lua 引擎)。

View File

@@ -7,13 +7,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C29FB05C-54B
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ui", "ui", "{D6E0DAF5-8171-44C0-817E-2FF9CF574E4F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Cli", "src\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj", "{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Cli", "src\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj", "{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Tests", "tests\ZonyLrcTools.Tests\ZonyLrcTools.Tests.csproj", "{FFBD3200-568F-455B-8390-5E76C51D522C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Tests", "tests\ZonyLrcTools.Tests\ZonyLrcTools.Tests.csproj", "{FFBD3200-568F-455B-8390-5E76C51D522C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.LocalServer", "src\ZonyLrcTools.LocalServer\ZonyLrcTools.LocalServer.csproj", "{312A9DEF-0888-49AB-A963-64C7A2A2AF05}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.LocalServer", "src\ZonyLrcTools.LocalServer\ZonyLrcTools.LocalServer.csproj", "{2875A08A-FFD6-4863-B815-5384DCFE88FC}"
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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -29,10 +33,18 @@ Global
{FFBD3200-568F-455B-8390-5E76C51D522C}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFBD3200-568F-455B-8390-5E76C51D522C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.Build.0 = Release|Any CPU {FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.Build.0 = Release|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2875A08A-FFD6-4863-B815-5384DCFE88FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Debug|Any CPU.Build.0 = Debug|Any CPU {2875A08A-FFD6-4863-B815-5384DCFE88FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Release|Any CPU.ActiveCfg = Release|Any CPU {2875A08A-FFD6-4863-B815-5384DCFE88FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Release|Any CPU.Build.0 = Release|Any CPU {2875A08A-FFD6-4863-B815-5384DCFE88FC}.Release|Any CPU.Build.0 = Release|Any CPU
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -43,6 +55,8 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8} = {C29FB05C-54B1-4020-94D2-87E192EB2F98} {55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B} {FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
{312A9DEF-0888-49AB-A963-64C7A2A2AF05} = {C29FB05C-54B1-4020-94D2-87E192EB2F98} {2875A08A-FFD6-4863-B815-5384DCFE88FC} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
{6BB67593-38CD-42E4-8409-EDAC3C138A6B} = {AB0E3F21-DDBF-459D-995C-04C177BF8995}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -1,6 +0,0 @@
<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/=Decryptor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gour/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zony/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

BIN
docs/img/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
docs/img/wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

View File

@@ -1,234 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure;
using ZonyLrcTools.Cli.Infrastructure.Album;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
using ZonyLrcTools.Cli.Infrastructure.IO;
using ZonyLrcTools.Cli.Infrastructure.Lyric;
using ZonyLrcTools.Cli.Infrastructure.Tag;
using ZonyLrcTools.Cli.Infrastructure.Threading;
namespace ZonyLrcTools.Cli.Commands
{
[Command("download", Description = "下载歌词文件或专辑图像。")]
public class DownloadCommand : ToolCommandBase
{
private readonly ILogger<DownloadCommand> _logger;
private readonly IFileScanner _fileScanner;
private readonly ITagLoader _tagLoader;
private readonly IEnumerable<ILyricDownloader> _lyricDownloaderList;
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
private readonly ToolOptions _options;
public DownloadCommand(ILogger<DownloadCommand> logger,
IFileScanner fileScanner,
IOptions<ToolOptions> options,
ITagLoader tagLoader,
IEnumerable<ILyricDownloader> lyricDownloaderList,
IEnumerable<IAlbumDownloader> albumDownloaderList)
{
_logger = logger;
_fileScanner = fileScanner;
_tagLoader = tagLoader;
_lyricDownloaderList = lyricDownloaderList;
_albumDownloaderList = albumDownloaderList;
_options = options.Value;
}
#region > Options <
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
[DirectoryExists]
public string Directory { get; set; }
[Option("-l|--lyric", CommandOptionType.NoValue, Description = "指定程序需要下载歌词文件。")]
public bool DownloadLyric { get; set; }
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
public bool DownloadAlbum { get; set; }
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
public int ParallelNumber { get; set; } = 2;
#endregion
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
var files = await ScanMusicFilesAsync();
var musicInfos = await LoadMusicInfoAsync(files);
if (DownloadLyric)
{
await DownloadLyricFilesAsync(musicInfos);
}
if (DownloadAlbum)
{
await DownloadAlbumAsync(musicInfos);
}
return 0;
}
private async Task<List<string>> ScanMusicFilesAsync()
{
var files = (await _fileScanner.ScanAsync(Directory, _options.SupportFileExtensions.Split(';')))
.SelectMany(t => t.FilePaths)
.ToList();
if (files.Count == 0)
{
_logger.LogError("没有找到任何音乐文件。");
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
}
_logger.LogInformation($"已经扫描到了 {files.Count} 个音乐文件。");
return files;
}
private async Task<ImmutableList<MusicInfo>> LoadMusicInfoAsync(IReadOnlyCollection<string> files)
{
_logger.LogInformation("开始加载音乐文件的标签信息...");
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = files.Select(file => warpTask.RunAsync(() => Task.Run(async () => await _tagLoader.LoadTagAsync(file))));
var result = await Task.WhenAll(warpTaskList);
_logger.LogInformation($"已成功加载 {files.Count} 个音乐文件的标签信息。");
return result.ToImmutableList();
}
private IEnumerable<ILyricDownloader> GetLyricDownloaderList()
{
var downloader = _options.LyricDownloaderOptions
.Where(op => op.Priority != -1)
.OrderBy(op => op.Priority)
.Join(_lyricDownloaderList,
op => op.Name,
loader => loader.DownloaderName,
(op, loader) => loader);
return downloader;
}
#region > <
private async ValueTask DownloadLyricFilesAsync(ImmutableList<MusicInfo> musicInfos)
{
_logger.LogInformation("开始下载歌词文件数据...");
var downloaderList = GetLyricDownloaderList();
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = musicInfos.Select(info =>
warpTask.RunAsync(() => Task.Run(async () => await DownloadLyricTaskLogicAsync(downloaderList, info))));
await Task.WhenAll(warpTaskList);
_logger.LogInformation($"歌词数据下载完成,成功: {musicInfos.Count} 失败{0}。");
}
private async Task DownloadLyricTaskLogicAsync(IEnumerable<ILyricDownloader> downloaderList, MusicInfo info)
{
async Task<bool> InternalDownloadLogicAsync(ILyricDownloader downloader)
{
_logger.LogMusicInfoWithInformation(info);
try
{
var lyric = await downloader.DownloadAsync(info.Name, info.Artist);
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
if (File.Exists(filePath))
{
return true;
}
if (lyric.IsPruneMusic)
{
return true;
}
await using var stream = new FileStream(filePath, FileMode.Create);
await using var sw = new StreamWriter(stream);
await sw.WriteAsync(lyric.ToString());
await sw.FlushAsync();
}
catch (ErrorCodeException ex)
{
if (ex.ErrorCode == ErrorCodes.NoMatchingSong)
{
return false;
}
_logger.LogWarningInfo(ex);
}
catch (Exception ex)
{
_logger.LogError($"下载歌词文件时发生错误:{ex.Message},歌曲名: {info.Name},歌手: {info.Artist}。");
}
return true;
}
foreach (var downloader in downloaderList)
{
if (await InternalDownloadLogicAsync(downloader))
{
break;
}
}
}
#endregion
#region > <
private async ValueTask DownloadAlbumAsync(ImmutableList<MusicInfo> musicInfos)
{
_logger.LogInformation("开始下载专辑图像数据...");
var downloader = _albumDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalAlbumDownloaderNames.NetEase);
var warpTask = new WarpTask(ParallelNumber);
var warpTaskList = musicInfos.Select(info =>
warpTask.RunAsync(() => Task.Run(async () => await DownloadAlbumTaskLogicAsync(downloader, info))));
await Task.WhenAll(warpTaskList);
_logger.LogInformation($"专辑数据下载完成,成功: {musicInfos.Count} 失败{0}。");
}
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
{
_logger.LogMusicInfoWithInformation(info);
try
{
var album = await downloader.DownloadAsync(info.Name, info.Artist);
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.png");
if (File.Exists(filePath) || album.Length <= 0)
{
return;
}
await new FileStream(filePath, FileMode.Create).WriteBytesToFileAsync(album, 1024);
}
catch (ErrorCodeException ex)
{
_logger.LogWarningInfo(ex);
}
}
#endregion
}
}

View File

@@ -0,0 +1,60 @@
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using ZonyLrcTools.Common;
using ZonyLrcTools.Common.Album;
using ZonyLrcTools.Common.Lyrics;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable MemberCanBePrivate.Global
namespace ZonyLrcTools.Cli.Commands.SubCommand
{
[Command("download", Description = "下载歌词文件或专辑图像。")]
public class DownloadCommand : ToolCommandBase
{
private readonly ILyricsDownloader _lyricsDownloader;
private readonly IAlbumDownloader _albumDownloader;
private readonly IMusicInfoLoader _musicInfoLoader;
public DownloadCommand(ILyricsDownloader lyricsDownloader,
IMusicInfoLoader musicInfoLoader,
IAlbumDownloader albumDownloader)
{
_lyricsDownloader = lyricsDownloader;
_musicInfoLoader = musicInfoLoader;
_albumDownloader = albumDownloader;
}
#region > Options <
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
[DirectoryExists]
public string SongsDirectory { get; set; }
[Option("-l|--lyric", CommandOptionType.NoValue, Description = "指定程序需要下载歌词文件。")]
public bool DownloadLyric { get; set; }
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
public bool DownloadAlbum { get; set; }
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
public int ParallelNumber { get; set; } = 2;
#endregion
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (DownloadLyric)
{
await _lyricsDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
}
if (DownloadAlbum)
{
await _albumDownloader.DownloadAsync(await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber), ParallelNumber);
}
return 0;
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace ZonyLrcTools.Cli.Commands.SubCommand;
[Command("search", Description = "手动指定信息,用于搜索歌词数据。")]
public class SearchCommand : ToolCommandBase
{
private readonly ILogger<SearchCommand> _logger;
#region > Options <
public string Name { get; set; }
public string Artist { get; set; }
#endregion
public SearchCommand(ILogger<SearchCommand> logger)
{
_logger = logger;
}
protected override Task<int> OnExecuteAsync(CommandLineApplication app)
{
return base.OnExecuteAsync(app);
}
}

View File

@@ -0,0 +1,77 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using MusicDecrypto.Library;
using ZonyLrcTools.Common.Infrastructure.IO;
using ZonyLrcTools.Common.Infrastructure.Threading;
using ZonyLrcTools.Common.MusicDecryption;
namespace ZonyLrcTools.Cli.Commands.SubCommand
{
/// <summary>
/// 工具类相关命令。
/// </summary>
[Command("util", Description = "提供常用的工具类功能。")]
public class UtilityCommand : ToolCommandBase
{
private readonly ILogger<UtilityCommand> _logger;
private readonly IMusicDecryptor _musicDecryptor;
[Required(ErrorMessage = "请指定需要解密的歌曲文件或文件夹路径。")]
[Option("-s|--source", CommandOptionType.SingleValue, Description = "需要解密的歌曲文件或文件夹路径。", ShowInHelpText = true)]
public string Source { get; set; }
private readonly IFileScanner _fileScanner;
public UtilityCommand(IFileScanner fileScanner,
ILogger<UtilityCommand> logger,
IMusicDecryptor musicDecryptor)
{
_fileScanner = fileScanner;
_logger = logger;
_musicDecryptor = musicDecryptor;
}
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (Directory.Exists(Source))
{
_logger.LogInformation("开始扫描文件夹,请稍等...");
var files = (await _fileScanner.ScanAsync(Source, DecryptoFactory.KnownExtensions.Select(x => $"*{x}")))
.SelectMany(f => f.FilePaths)
.ToList();
_logger.LogInformation($"扫描完成,共 {files.Count} 个文件,准备转换。");
var wrapTask = new WarpTask(4);
var tasks = files.Select(path => wrapTask.RunAsync(async () =>
{
_logger.LogInformation($"开始转换文件:{path}");
var result = await _musicDecryptor.ConvertMusicAsync(path);
if (result.IsSuccess)
{
_logger.LogInformation($"转换完成,文件保存在:{result.OutputFilePath}");
}
else
{
_logger.LogError($"转换失败,原因:{result.ErrorMessage}");
}
}));
await Task.WhenAll(tasks);
}
else if (File.Exists(Source))
{
await _musicDecryptor.ConvertMusicAsync(Source);
}
_logger.LogInformation("所有文件已经转换完成...");
return 0;
}
}
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils; using McMaster.Extensions.CommandLineUtils;

View File

@@ -1,105 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.IO;
using ZonyLrcTools.Cli.Infrastructure.MusicDecryption;
using ZonyLrcTools.Cli.Infrastructure.Threading;
namespace ZonyLrcTools.Cli.Commands
{
public enum SupportFileType
{
Ncm = 1,
Qcm = 2
}
/// <summary>
/// 工具类相关命令。
/// </summary>
[Command("util", Description = "提供常用的工具类功能。")]
public class UtilityCommand : ToolCommandBase
{
private readonly ILogger<UtilityCommand> _logger;
private readonly IMusicDecryptor _musicDecryptor;
[Required(ErrorMessage = "音乐格式为必须参数,请指定 -t 参数。")]
[Option("-t|--type", CommandOptionType.SingleValue, Description = "需要转换的文件格式,参数[Ncm、Qcm]", ShowInHelpText = true)]
public SupportFileType Type { get; set; }
[Required(ErrorMessage = "文件路径为必须按参数,请传入有效路径。")]
[Argument(0, "FilePath", "指定需要转换的音乐文件路径,支持目录和文件路径。")]
public string FilePath { get; set; }
private readonly IFileScanner _fileScanner;
public UtilityCommand(IFileScanner fileScanner,
ILogger<UtilityCommand> logger,
IMusicDecryptor musicDecryptor)
{
_fileScanner = fileScanner;
_logger = logger;
_musicDecryptor = musicDecryptor;
}
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (Directory.Exists(FilePath))
{
_logger.LogInformation("开始扫描文件夹,请稍等...");
var files = (await _fileScanner.ScanAsync(FilePath, new[] {"*.ncm"}))
.SelectMany(f => f.FilePaths)
.ToList();
_logger.LogInformation($"扫描完成,共 {files.Count} 个文件,准备转换。");
var wrapTask = new WarpTask(4);
var tasks = files.Select(path => wrapTask.RunAsync(() => Convert(path)));
await Task.WhenAll(tasks);
}
else if (File.Exists(FilePath))
{
await Convert(FilePath);
}
_logger.LogInformation("所有文件已经转换完成...");
return 0;
}
private async Task Convert(string filePath)
{
if (Type != SupportFileType.Ncm)
{
throw new ErrorCodeException(ErrorCodes.OnlySupportNcmFormatFile);
}
var memoryStream = new MemoryStream();
await using var file = File.Open(filePath, FileMode.Open);
{
var buffer = new Memory<byte>(new byte[2048]);
while (await file.ReadAsync(buffer) > 0)
{
// TODO: Large Object Issue!!!!!
await memoryStream.WriteAsync(buffer);
}
}
// TODO: Large Object Issue!!!!!
var result = await _musicDecryptor.ConvertMusic(memoryStream.ToArray());
var newFileName = Path.Combine(Path.GetDirectoryName(filePath),
$"{Path.GetFileNameWithoutExtension(filePath)}.{((JObject) result.ExtensionObjects["JSON"]).SelectToken("$.format").Value<string>()}");
await using var musicFileStream = File.Create(newFileName);
await musicFileStream.WriteAsync(result.Data);
await musicFileStream.FlushAsync();
}
}
}

View File

@@ -1,40 +0,0 @@
using System.Collections.Generic;
using ZonyLrcTools.Cli.Infrastructure.Lyric;
using ZonyLrcTools.Cli.Infrastructure.Network;
using ZonyLrcTools.Cli.Infrastructure.Tag;
namespace ZonyLrcTools.Cli.Config
{
public class ToolOptions
{
/// <summary>
/// 支持的音乐文件后缀集合,以 ; 进行分隔。
/// </summary>
public string SupportFileExtensions { get; set; }
/// <summary>
/// 歌词下载相关的配置信息。
/// </summary>
public LyricItemCollectionOption LyricOption { get; set; }
/// <summary>
/// 标签加载器相关的配置属性。
/// </summary>
public IEnumerable<TagInfoProviderInstance> TagInfoProviderOptions { get; set; }
/// <summary>
/// 网络代理相关的配置信息。
/// </summary>
public NetworkOptions NetworkOptions { get; set; }
/// <summary>
/// 歌词下载器相关的配置属性。
/// </summary>
public IEnumerable<LyricDownloaderOption> LyricDownloaderOptions { get; set; }
/// <summary>
/// 屏蔽词功能相关配置。
/// </summary>
public BlockWordOption BlockWordOptions { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Serilog.Sinks.SystemConsole.Themes;
namespace ZonyLrcTools.Cli.Infrastructure.Logging;
public static class CustomConsoleTheme
{
public static AnsiConsoleTheme Code { get; } = new AnsiConsoleTheme(
new Dictionary<ConsoleThemeStyle, string>
{
[ConsoleThemeStyle.Text] = "\x1b[38;5;0253m",
[ConsoleThemeStyle.SecondaryText] = "\x1b[38;5;0246m",
[ConsoleThemeStyle.TertiaryText] = "\x1b[38;5;0242m",
[ConsoleThemeStyle.Invalid] = "\x1b[33;1m",
[ConsoleThemeStyle.Null] = "\x1b[38;5;0038m",
[ConsoleThemeStyle.Name] = "\x1b[38;5;0081m",
[ConsoleThemeStyle.String] = "\x1b[38;5;0216m",
[ConsoleThemeStyle.Number] = "\x1b[38;5;151m",
[ConsoleThemeStyle.Boolean] = "\x1b[38;5;0038m",
[ConsoleThemeStyle.Scalar] = "\x1b[38;5;0079m",
[ConsoleThemeStyle.LevelVerbose] = "\x1b[37m",
[ConsoleThemeStyle.LevelDebug] = "\x1b[37m",
[ConsoleThemeStyle.LevelInformation] = "\x1b[32m\x1b[48;5;0238m",
[ConsoleThemeStyle.LevelWarning] = "\x1b[38;5;0229m",
[ConsoleThemeStyle.LevelError] = "\x1b[38;5;0197m\x1b[48;5;0238m",
[ConsoleThemeStyle.LevelFatal] = "\x1b[38;5;0197m\x1b[48;5;0238m",
});
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Logging;
namespace ZonyLrcTools.Cli.Infrastructure.Logging;
public class SerilogWarpLogger : IWarpLogger, ITransientDependency
{
private readonly ILogger<SerilogWarpLogger> _logger;
public SerilogWarpLogger(ILogger<SerilogWarpLogger> logger)
{
_logger = logger;
}
public Task DebugAsync(string message, Exception exception = null)
{
_logger.LogDebug(message, exception);
return Task.CompletedTask;
}
public Task InfoAsync(string message, Exception exception = null)
{
_logger.LogInformation(message, exception);
return Task.CompletedTask;
}
public Task WarnAsync(string message, Exception exception = null)
{
_logger.LogWarning(message, exception);
return Task.CompletedTask;
}
public Task ErrorAsync(string message, Exception exception = null)
{
_logger.LogError(message, exception);
return Task.CompletedTask;
}
}

View File

@@ -1,16 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// 构建 <see cref="LyricItemCollection"/> 对象的工厂。
/// </summary>
public interface ILyricItemCollectionFactory
{
/// <summary>
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
/// </summary>
/// <param name="sourceLyric">原始歌词数据。</param>
/// <param name="translateLyric">翻译歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
LyricItemCollection Build(string sourceLyric, string translateLyric = null);
}
}

View File

@@ -1,7 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public interface ILyricTextResolver
{
LyricItemCollection Resolve(string lyricText);
}
}

View File

@@ -1,27 +0,0 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
{
public class GetLyricAccessKeyRequest
{
[JsonProperty("ver")]
public int UnknownParameters1 { get; }
[JsonProperty("man")]
public string UnknownParameters2 { get; }
[JsonProperty("client")]
public string UnknownParameters3 { get; }
[JsonProperty("hash")]
public string FileHash { get; }
public GetLyricAccessKeyRequest(string fileHash)
{
UnknownParameters1 = 1;
UnknownParameters2 = "yes";
UnknownParameters3 = "mobi";
FileHash = fileHash;
}
}
}

View File

@@ -1,26 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
{
public class GetLyricAccessKeyResponse
{
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("errcode")]
public int ErrorCode { get; set; }
[JsonProperty("candidates")]
public List<GetLyricAccessKeyDataObject> AccessKeyDataObjects { get; set; }
}
public class GetLyricAccessKeyDataObject
{
[JsonProperty("accesskey")]
public string AccessKey { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
}
}

View File

@@ -1,25 +0,0 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("filter")]
public int Filter { get; }
[JsonProperty("platform")]
public string Platform { get; }
[JsonProperty("keyword")]
public string Keyword { get; }
public SongSearchRequest(string musicName, string artistName)
{
Filter = 2;
Platform = "WebFilter";
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
}
}
}

View File

@@ -1,31 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("status")]
public int Status { 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; }
}
public class SongSearchResponseInnerData
{
[JsonProperty("lists")]
public List<SongSearchResponseSongDetail> List { get; set; }
}
public class SongSearchResponseSongDetail
{
public string FileHash { get; set; }
}
}

View File

@@ -1,15 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricDownloaderArgs
{
public string SongName { get; set; }
public string Artist { get; set; }
public LyricDownloaderArgs(string songName, string artist)
{
SongName = songName;
Artist = artist;
}
}
}

View File

@@ -1,37 +0,0 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
/// <summary>
/// <see cref="ILyricItemCollectionFactory"/> 的默认实现。
/// </summary>
public class LyricItemCollectionFactory : ILyricItemCollectionFactory, ITransientDependency
{
private readonly ToolOptions _options;
public LyricItemCollectionFactory(IOptions<ToolOptions> options)
{
_options = options.Value;
}
public LyricItemCollection Build(string sourceLyric, string translateLyric = null)
{
var items = new LyricItemCollection(_options.LyricOption);
if (string.IsNullOrEmpty(sourceLyric))
{
return items;
}
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
foreach (Match match in regex.Matches(sourceLyric))
{
items.Add(new LyricItem(match.Value));
}
return items;
}
}
}

View File

@@ -1,17 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricItemCollectionOption
{
/// <summary>
/// 双语歌词是否合并为一行。
/// </summary>
public bool IsOneLine { get; set; } = false;
/// <summary>
/// 换行符格式,取值来自 <see cref="LineBreakType"/> 常量类。
/// </summary>
public string LineBreak { get; set; } = LineBreakType.Windows;
public static readonly LyricItemCollectionOption NullInstance = new();
}
}

View File

@@ -1,80 +0,0 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("ct")] public int UnknownParameter1 { get; set; }
[JsonProperty("qqmusic_ver")] public int ClientVersion { get; set; }
[JsonProperty("new_json")] public int UnknownParameter2 { get; set; }
[JsonProperty("remoteplace")] public string RemotePlace { get; set; }
[JsonProperty("t")] public int UnknownParameter3 { get; set; }
[JsonProperty("aggr")] public int UnknownParameter4 { get; set; }
[JsonProperty("cr")] public int UnknownParameter5 { get; set; }
[JsonProperty("catZhida")] public int UnknownParameter6 { get; set; }
[JsonProperty("lossless")] public int LossLess { get; set; }
[JsonProperty("flag_qc")] public int UnknownParameter7 { get; set; }
[JsonProperty("p")] public int Page { get; set; }
[JsonProperty("n")] public int Limit { get; set; }
[JsonProperty("w")] public string Keyword { get; set; }
[JsonProperty("g_tk")] public int UnknownParameter8 { get; set; }
[JsonProperty("hostUin")] public int UnknownParameter9 { get; set; }
[JsonProperty("format")] public string ResultFormat { get; set; }
[JsonProperty("inCharset")] public string InCharset { get; set; }
[JsonProperty("outCharset")] public string OutCharset { get; set; }
[JsonProperty("notice")] public int UnknownParameter10 { get; set; }
[JsonProperty("platform")] public string Platform { get; set; }
[JsonProperty("needNewCode")] public int UnknownParameter11 { get; set; }
protected SongSearchRequest()
{
UnknownParameter1 = 24;
ClientVersion = 1298;
UnknownParameter2 = 1;
RemotePlace = "txt.yqq.song";
UnknownParameter3 = 0;
UnknownParameter4 = 1;
UnknownParameter5 = 1;
UnknownParameter6 = 1;
LossLess = 0;
UnknownParameter7 = 0;
Page = 1;
Limit = 5;
UnknownParameter8 = 5381;
UnknownParameter9 = 0;
ResultFormat = "json";
InCharset = "utf8";
OutCharset = "utf8";
UnknownParameter10 = 0;
Platform = "yqq";
UnknownParameter11 = 0;
}
public SongSearchRequest(string musicName, string artistName) : this()
{
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
}
}
}

View File

@@ -1,16 +0,0 @@
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
{
public class DecryptionResult
{
public byte[] Data { get; protected set; }
public Dictionary<string, object> ExtensionObjects { get; set; }
public DecryptionResult(byte[] data)
{
Data = data;
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
{
/// <summary>
/// 音乐解密器,用于将加密的歌曲数据,转换为可识别的歌曲格式。
/// </summary>
public interface IMusicDecryptor
{
/// <summary>
/// 将加密数据转换为可识别的歌曲格式。
/// </summary>
/// <param name="sourceBytes">源加密的歌曲数据。</param>
/// <returns>解密完成的歌曲数据。</returns>
Task<DecryptionResult> ConvertMusic(byte[] sourceBytes);
}
}

View File

@@ -1,184 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
{
/// <summary>
/// NCM 音乐转换器,用于将 NCM 格式的音乐转换为可播放的格式。
/// </summary>
public class NcmMusicDecryptor : IMusicDecryptor, ITransientDependency
{
protected readonly byte[] AesCoreKey = {0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57};
protected readonly byte[] AesModifyKey = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28};
public async Task<DecryptionResult> ConvertMusic(byte[] sourceBytes)
{
var stream = new MemoryStream(sourceBytes);
var streamReader = new BinaryReader(stream);
var lengthBytes = new byte[4];
lengthBytes = streamReader.ReadBytes(4);
if (BitConverter.ToInt32(lengthBytes) != 0x4e455443)
{
throw new Exception();
}
lengthBytes = streamReader.ReadBytes(4);
if (BitConverter.ToInt32(lengthBytes) != 0x4d414446)
{
throw new Exception();
}
stream.Seek(2, SeekOrigin.Current);
stream.Read(lengthBytes);
var keyBytes = new byte[BitConverter.ToInt32(lengthBytes)];
stream.Read(keyBytes);
// 对已经加密的数据进行异或操作。
for (int i = 0; i < keyBytes.Length; i++)
{
keyBytes[i] ^= 0x64;
}
var coreKeyBytes = GetBytesByOffset(DecryptAes128Ecb(AesCoreKey, keyBytes), 17);
var modifyDataBytes = new byte[streamReader.ReadInt32()];
stream.Read(modifyDataBytes);
for (int i = 0; i < modifyDataBytes.Length; i++)
{
modifyDataBytes[i] ^= 0x63;
}
var decryptBase64Bytes = Convert.FromBase64String(Encoding.UTF8.GetString(GetBytesByOffset(modifyDataBytes, 22)));
var decryptModifyData = DecryptAes128Ecb(AesModifyKey, decryptBase64Bytes);
var musicInfoJson = JObject.Parse(Encoding.UTF8.GetString(GetBytesByOffset(decryptModifyData, 6)));
// CRC 校验
stream.Seek(4, SeekOrigin.Current);
stream.Seek(5, SeekOrigin.Current);
GetAlbumImageBytes(stream, streamReader);
var sBox = BuildKeyBox(coreKeyBytes);
return new DecryptionResult(GetMusicBytes(sBox, stream).ToArray())
{
ExtensionObjects = new Dictionary<string, object>
{
{"JSON", musicInfoJson}
}
};
}
private byte[] GetBytesByOffset(byte[] srcBytes, int offset = 0)
{
var resultBytes = new byte[srcBytes.Length - offset];
Array.Copy(srcBytes, offset, resultBytes, 0, srcBytes.Length - offset);
return resultBytes;
}
private byte[] DecryptAes128Ecb(byte[] keyBytes, byte[] data)
{
var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.ECB;
using var decryptor = aes.CreateDecryptor(keyBytes, null);
var result = decryptor.TransformFinalBlock(data, 0, data.Length);
return result;
}
/// <summary>
/// RC4 加密,生成 KeyBox。
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
private byte[] BuildKeyBox(byte[] key)
{
byte[] box = new byte[256];
for (int i = 0; i < 256; ++i)
{
box[i] = (byte) i;
}
byte keyLength = (byte) key.Length;
byte c;
byte lastByte = 0;
byte keyOffset = 0;
byte swap;
for (int i = 0; i < 256; ++i)
{
swap = box[i];
c = (byte) ((swap + lastByte + key[keyOffset++]) & 0xff);
if (keyOffset >= keyLength)
{
keyOffset = 0;
}
box[i] = box[c];
box[c] = swap;
lastByte = c;
}
return box;
}
/// <summary>
/// 获得歌曲的专辑图像信息。
/// </summary>
/// <param name="stream">原始文件流。</param>
/// <param name="streamReader">二进制读取器。</param>
private byte[] GetAlbumImageBytes(Stream stream, BinaryReader streamReader)
{
var imgLength = streamReader.ReadInt32();
if (imgLength <= 0)
{
return null;
}
var imgBuffer = streamReader.ReadBytes(imgLength);
return imgBuffer;
}
/// <summary>
/// 获得歌曲的完整数据。
/// </summary>
/// <param name="sBox"></param>
/// <param name="stream">原始文件流。</param>
private MemoryStream GetMusicBytes(byte[] sBox, Stream stream)
{
var n = 0x8000;
var memoryStream = new MemoryStream();
while (true)
{
var tb = new byte[n];
var result = stream.Read(tb);
if (result <= 0) break;
for (int i = 0; i < n; i++)
{
var j = (byte) ((i + 1) & 0xff);
tb[i] ^= sBox[sBox[j] + sBox[(sBox[j] + j) & 0xff] & 0xff];
}
memoryStream.Write(tb);
}
memoryStream.Flush();
return memoryStream;
}
}
}

View File

@@ -1,58 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
/// <summary>
/// 默认的标签加载器 <see cref="ITagLoader"/> 实现。
/// </summary>
public class DefaultTagLoader : ITagLoader, ISingletonDependency
{
protected readonly IEnumerable<ITagInfoProvider> TagInfoProviders;
protected readonly IBlockWordDictionary BlockWordDictionary;
protected ToolOptions Options;
public DefaultTagLoader(IEnumerable<ITagInfoProvider> tagInfoProviders,
IBlockWordDictionary blockWordDictionary,
IOptions<ToolOptions> options)
{
TagInfoProviders = tagInfoProviders;
BlockWordDictionary = blockWordDictionary;
Options = options.Value;
}
public virtual async ValueTask<MusicInfo> LoadTagAsync(string filePath)
{
if (!TagInfoProviders.Any())
{
throw new ErrorCodeException(ErrorCodes.LoadTagInfoProviderError);
}
foreach (var provider in TagInfoProviders)
{
var info = await provider.LoadAsync(filePath);
if (info != null)
{
HandleBlockWord(info);
return info;
}
}
return null;
}
protected void HandleBlockWord(MusicInfo info)
{
if (Options.BlockWordOptions.IsEnable)
{
info.Name = BlockWordDictionary.GetValue(info.Name) ?? info.Name;
info.Artist = BlockWordDictionary.GetValue(info.Name) ?? info.Artist;
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using ZonyLrcTools.Common.Updater;
namespace ZonyLrcTools.Cli.Infrastructure;
public class UpdaterHostedService : IHostedService
{
private readonly IUpdater _updater;
public UpdaterHostedService(IUpdater updater)
{
_updater = updater;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _updater.CheckUpdateAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils; using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -8,18 +8,26 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using Serilog.Sinks.SystemConsole.Themes;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Cli.Commands;
using ZonyLrcTools.Cli.Commands.SubCommand;
using ZonyLrcTools.Cli.Infrastructure;
using ZonyLrcTools.Cli.Infrastructure.Logging;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Commands namespace ZonyLrcTools.Cli
{ {
[Command("lyric-tool")] [Command("lyric-tool")]
[Subcommand(typeof(DownloadCommand), [Subcommand(typeof(DownloadCommand),
typeof(UtilityCommand))] typeof(UtilityCommand))]
public class ToolCommand : ToolCommandBase public class Program : ToolCommandBase
{ {
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ConfigureLogger(); ConfigureLogger();
ConfigureErrorMessage(); ConfigureErrorMessage();
@@ -52,7 +60,7 @@ namespace ZonyLrcTools.Cli.Commands
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error) .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.Async(c => c.Console()) .WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
.WriteTo.Logger(lc => .WriteTo.Logger(lc =>
{ {
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning) lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
@@ -74,16 +82,18 @@ namespace ZonyLrcTools.Cli.Commands
{ {
builder builder
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json"); .AddYamlFile("config.yaml");
}) })
.ConfigureServices((_, services) => .ConfigureServices((_, services) =>
{ {
services.AddSingleton(PhysicalConsole.Singleton); services.AddSingleton(PhysicalConsole.Singleton);
services.BeginAutoDependencyInject<ToolCommand>(); services.BeginAutoDependencyInject<Program>();
services.BeginAutoDependencyInject<IWarpHttpClient>();
services.ConfigureConfiguration(); services.ConfigureConfiguration();
services.ConfigureToolService(); services.ConfigureToolService();
services.AddHostedService<UpdaterHostedService>();
}) })
.RunCommandLineApplicationAsync<ToolCommand>(args); .RunCommandLineApplicationAsync<Program>(args);
} }
private static int HandleException(Exception ex) private static int HandleException(Exception ex)
@@ -91,7 +101,8 @@ namespace ZonyLrcTools.Cli.Commands
switch (ex) switch (ex)
{ {
case ErrorCodeException exception: case ErrorCodeException exception:
Log.Logger.Error($"出现了未处理的异常,错误代码: {exception.ErrorCode},错误信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}\n调用栈:\n{exception.StackTrace}"); Log.Logger.Error(
$"出现了未处理的异常。\n错误代码: {exception.ErrorCode}\n错误信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}\n原始信息:{exception.Message}\n调用栈:{exception.StackTrace}");
Environment.Exit(exception.ErrorCode); Environment.Exit(exception.ErrorCode);
return exception.ErrorCode; return exception.ErrorCode;
case { } unknownException: case { } unknownException:

View File

@@ -3,7 +3,8 @@
"10001": "待搜索的后缀不能为空。", "10001": "待搜索的后缀不能为空。",
"10002": "需要扫描的目录不存在,请确认路径是否正确。", "10002": "需要扫描的目录不存在,请确认路径是否正确。",
"10003": "不能获取文件的后缀信息。", "10003": "不能获取文件的后缀信息。",
"10004": "没有扫描到任何音乐文件。" "10004": "没有扫描到任何音乐文件。",
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。"
}, },
"Warning": { "Warning": {
"50001": "扫描文件时出现了错误。", "50001": "扫描文件时出现了错误。",

View File

@@ -1,31 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AssemblyVersion>0.0.0.1</AssemblyVersion>
<FileVersion>0.0.0.1</FileVersion>
<PackageVersion>0.0.0.1</PackageVersion>
<Version>0.0.0.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.0" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.1" />
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.0" /> <PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Refit" Version="6.1.15" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.1.15" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.1.15" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.2.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="appsettings.json" /> <None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="Resources\error_msg.json" /> <None Remove="Resources\error_msg.json" />
<Content Include="Resources\error_msg.json"> <Content Include="Resources\error_msg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -34,6 +32,14 @@
<Content Include="BlockWords.json"> <Content Include="BlockWords.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<None Remove="config.yaml" />
<Content Include="config.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,52 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ToolOption": {
"SupportFileExtensions": "*.mp3;*.flac;*.wav",
"NetworkOptions": {
"Enable": false,
"ProxyIp": "127.0.0.1",
"ProxyPort": 4780
},
"TagInfoProviderOptions": [
{
"Name": "Taglib",
"Priority": 1
},
{
"Name": "FileName",
"Priority": 2,
"Extensions": {
"RegularExpressions": "(?'artist'.+)\\s-\\s(?'name'.+)"
}
}
],
"LyricDownloaderOptions": [
{
"Name": "NetEase",
"Priority": 1
},
{
"Name": "QQ",
"Priority": 2
},
{
"Name": "KuGou",
"Priority": 3
}
],
"LyricOption": {
"IsOneLine": true,
"LineBreak": "\n"
},
"BlockWordOptions": {
"IsEnable": false,
"BlockWordDictionaryFile": "BlockWords.json"
}
}
}

View File

@@ -0,0 +1,56 @@
globalOption:
# 允许扫描的歌曲文件后缀名。
supportFileExtensions:
- '*.mp3'
- '*.flac'
- '*.wav'
# 网络代理服务设置,仅支持 HTTP 代理。
networkOptions:
isEnable: false # 是否启用代理。
ip: 127.0.0.1 # 代理服务 IP 地址。
port: 4780 # 代理服务端口号。
updateUrl: https://api.myzony.com/lrc-tools/update # 更新检查地址。
# 下载器的相关参数配置。
provider:
# 标签扫描器的相关参数配置。
tag:
# 支持的标签扫描器。
plugin:
- name: Taglib # 基于 Taglib 库的标签扫描器。
priority: 1 # 优先级,升序排列。
- name: FileName # 基于文件名的标签扫描器。
priority: 2
# 基于文件名扫描器的扩展参数。
extensions:
# 正则表达式,用于匹配文件名中的作者信息和歌曲信息,可根据
# 自己的需求进行调整。
regularExpressions: "(?'artist'.+)\\s-\\s(?'name'.+)"
# 歌曲标签屏蔽字典替换功能。
blockWord:
isEnable: false # 是否启用屏蔽字典。
filePath: 'BlockWords.json' # 屏蔽字典的路径。
# 歌词下载器的相关参数配置。
lyric:
# 支持的歌词下载器。
plugin:
- name: NetEase # 基于网易云音乐的歌词下载器。
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
- name: QQ # 基于 QQ 音乐的歌词下载器。
priority: 2
# depth: 10 # 暂时不支持。
- name: KuGou # 基于酷狗音乐的歌词下载器。
priority: 3
depth: 10
- name: KuWo # 基于酷我音乐的歌词下载器。
priority: 4
depth: 10
# 歌词下载的一些共有配置参数。
config:
isOneLine: true # 双语歌词是否合并为一行展示。
lineBreak: "\n" # 换行符的类型,记得使用双引号指定。
isEnableTranslation: true # 是否启用翻译歌词。
isOnlyOutputTranslation: false # 是否只输出翻译歌词。
isSkipExistLyricFiles: false # 如果歌词文件已经存在,是否跳过这些文件。
fileEncoding: 'utf-8' # 歌词文件的编码格式。

View File

@@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
read -r -p "请输入版本号:" Version
Platforms=('win-x64' 'linux-x64' 'osx-x64') Platforms=('win-x64' 'linux-x64' 'osx-x64')
if ! [ -d './TempFiles' ]; if ! [ -d './TempFiles' ];
@@ -11,11 +10,11 @@ rm -rf ./TempFiles/*
for platform in "${Platforms[@]}" for platform in "${Platforms[@]}"
do do
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true || exit 1
cd ./bin/Release/net5.0/"$platform"/publish/ cd ./bin/Release/net7.0/"$platform"/publish/ || exit 1
zip -r ./ZonyLrcTools_"$platform"_"$Version".zip ./ zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1
cd ../../../../../ cd ../../../../../ || exit 1
mv ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./TempFiles mv ./bin/Release/net7.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
done done

View File

@@ -0,0 +1,68 @@
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Extensions;
using ZonyLrcTools.Common.Infrastructure.IO;
using ZonyLrcTools.Common.Infrastructure.Logging;
using ZonyLrcTools.Common.Infrastructure.Threading;
namespace ZonyLrcTools.Common.Album;
public class AlbumDownloader : IAlbumDownloader, ISingletonDependency
{
private readonly IEnumerable<IAlbumProvider> _albumProviders;
public IEnumerable<IAlbumProvider> AvailableProviders => new Lazy<IEnumerable<IAlbumProvider>>(() =>
{
return _albumProviders.Where(d => d.DownloaderName == InternalAlbumProviderNames.NetEase);
}).Value;
private readonly IWarpLogger _logger;
public AlbumDownloader(IEnumerable<IAlbumProvider> albumProviders,
IWarpLogger logger)
{
_albumProviders = albumProviders;
_logger = logger;
}
public async Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 2,
CancellationToken cancellationToken = default)
{
await _logger.InfoAsync("开始下载专辑图像数据...");
var provider = AvailableProviders.FirstOrDefault(d => d.DownloaderName == InternalAlbumProviderNames.NetEase);
if (provider == null)
{
return;
}
var warpTask = new WarpTask(parallelCount);
var warpTaskList = needDownloadMusicInfos.Select(info =>
warpTask.RunAsync(() =>
Task.Run(async () =>
{
_logger.LogSuccessful(info);
try
{
var album = await provider.DownloadAsync(info.Name, info.Artist);
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.png");
if (File.Exists(filePath) || album.Length <= 0)
{
return;
}
await new FileStream(filePath, FileMode.Create).WriteBytesToFileAsync(album);
}
catch (ErrorCodeException ex)
{
_logger.LogWarningInfo(ex);
}
}, cancellationToken), cancellationToken));
await Task.WhenAll(warpTaskList);
await _logger.InfoAsync($"专辑数据下载完成,成功: {needDownloadMusicInfos.Count(m => m.IsSuccessful)} 失败{needDownloadMusicInfos.Count(m => m.IsSuccessful == false)}。");
}
}

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Common.Album;
public interface IAlbumDownloader
{
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 2,
CancellationToken cancellationToken = default);
IEnumerable<IAlbumProvider> AvailableProviders { get; }
}

View File

@@ -1,11 +1,9 @@
using System.Threading.Tasks; namespace ZonyLrcTools.Common.Album
namespace ZonyLrcTools.Cli.Infrastructure.Album
{ {
/// <summary> /// <summary>
/// 专辑封面下载器,用于匹配并下载歌曲的专辑封面。 /// 专辑封面下载器,用于匹配并下载歌曲的专辑封面。
/// </summary> /// </summary>
public interface IAlbumDownloader public interface IAlbumProvider
{ {
/// <summary> /// <summary>
/// 下载器的名称。 /// 下载器的名称。

View File

@@ -1,9 +1,9 @@
namespace ZonyLrcTools.Cli.Infrastructure.Album namespace ZonyLrcTools.Common.Album
{ {
/// <summary> /// <summary>
/// 定义了程序默认提供的专辑图像下载器。 /// 定义了程序默认提供的专辑图像下载器。
/// </summary> /// </summary>
public static class InternalAlbumDownloaderNames public static class InternalAlbumProviderNames
{ {
/// <summary> /// <summary>
/// 网易云音乐专辑图像下载器。 /// 网易云音乐专辑图像下载器。

View File

@@ -1,18 +1,15 @@
using System; using System.Net.Http.Headers;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel; using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Cli.Infrastructure.Network; using ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel;
namespace ZonyLrcTools.Cli.Infrastructure.Album.NetEase namespace ZonyLrcTools.Common.Album.NetEase
{ {
public class NetEaseAlbumDownloader : IAlbumDownloader, ITransientDependency public class NetEaseAlbumProvider : IAlbumProvider, ITransientDependency
{ {
public string DownloaderName => InternalAlbumDownloaderNames.NetEase; public string DownloaderName => InternalAlbumProviderNames.NetEase;
private readonly IWarpHttpClient _warpHttpClient; private readonly IWarpHttpClient _warpHttpClient;
private readonly Action<HttpRequestMessage> _defaultOption; private readonly Action<HttpRequestMessage> _defaultOption;
@@ -21,7 +18,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.NetEase
private const string GetMusicInfoApi = @"https://music.163.com/api/song/detail"; private const string GetMusicInfoApi = @"https://music.163.com/api/song/detail";
private const string DefaultReferer = @"https://music.163.com"; private const string DefaultReferer = @"https://music.163.com";
public NetEaseAlbumDownloader(IWarpHttpClient warpHttpClient) public NetEaseAlbumProvider(IWarpHttpClient warpHttpClient)
{ {
_warpHttpClient = warpHttpClient; _warpHttpClient = warpHttpClient;
_defaultOption = message => _defaultOption = message =>
@@ -44,14 +41,14 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.NetEase
true, true,
_defaultOption); _defaultOption);
if (searchResult is not {StatusCode: 200} || searchResult.Items?.SongCount <= 0) if (searchResult is not { StatusCode: 200 } || searchResult.Items?.SongCount <= 0)
{ {
throw new ErrorCodeException(ErrorCodes.NoMatchingSong); throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
} }
var songDetailJsonStr = await _warpHttpClient.GetAsync( var songDetailJsonStr = await _warpHttpClient.GetAsync(
GetMusicInfoApi, GetMusicInfoApi,
new GetSongDetailsRequest(searchResult.GetFirstMatchSongId(songName)), new GetSongDetailsRequest(searchResult.GetFirstMatchSongId(songName, null)),
_defaultOption); _defaultOption);
var url = JObject.Parse(songDetailJsonStr).SelectToken("$.songs[0].album.picUrl")?.Value<string>(); var url = JObject.Parse(songDetailJsonStr).SelectToken("$.songs[0].album.picUrl")?.Value<string>();

View File

@@ -1,16 +1,13 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks; using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel; using ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic namespace ZonyLrcTools.Common.Album.QQMusic
{ {
public class QQMusicAlbumDownloader : IAlbumDownloader, ITransientDependency public class QQMusicAlbumProvider : IAlbumProvider, ITransientDependency
{ {
public string DownloaderName => InternalAlbumDownloaderNames.QQ; public string DownloaderName => InternalAlbumProviderNames.QQ;
private readonly IWarpHttpClient _warpHttpClient; private readonly IWarpHttpClient _warpHttpClient;
@@ -27,7 +24,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic
private const string SearchApi = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp"; private const string SearchApi = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp";
private const string DefaultReferer = "https://y.qq.com"; private const string DefaultReferer = "https://y.qq.com";
public QQMusicAlbumDownloader(IWarpHttpClient warpHttpClient) public QQMusicAlbumProvider(IWarpHttpClient warpHttpClient)
{ {
_warpHttpClient = warpHttpClient; _warpHttpClient = warpHttpClient;
} }
@@ -39,7 +36,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic
SearchApi, SearchApi,
requestParameter, _defaultOption); requestParameter, _defaultOption);
return new byte[] {0x1, 0x2}; return new byte[] { 0x1, 0x2 };
} }
} }
} }

View File

@@ -1,9 +1,9 @@
namespace ZonyLrcTools.Cli.Infrastructure.Tag namespace ZonyLrcTools.Common.Configuration
{ {
/// <summary> /// <summary>
/// 屏蔽词选项类。 /// 屏蔽词选项类。
/// </summary> /// </summary>
public class BlockWordOption public class BlockWordOptions
{ {
/// <summary> /// <summary>
/// 是否启用本功能。 /// 是否启用本功能。
@@ -13,6 +13,6 @@
/// <summary> /// <summary>
/// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。 /// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。
/// </summary> /// </summary>
public string BlockWordDictionaryFile { get; set; } public string FilePath { get; set; }
} }
} }

View File

@@ -0,0 +1,36 @@
using ZonyLrcTools.Common.Lyrics;
namespace ZonyLrcTools.Common.Configuration;
public class GlobalLyricsConfigOptions
{
/// <summary>
/// 双语歌词是否合并为一行。
/// </summary>
public bool IsOneLine { get; set; } = false;
/// <summary>
/// 换行符格式,取值来自 <see cref="LineBreakType"/> 常量类。
/// </summary>
public string LineBreak { get; set; } = LineBreakType.Windows;
/// <summary>
/// 是否启用歌词翻译功能。
/// </summary>
public bool IsEnableTranslation { get; set; } = true;
/// <summary>
/// 如果歌词文件已经存在,是否跳过这些文件
/// </summary>
public bool IsSkipExistLyricFiles { get; set; } = false;
/// <summary>
/// 歌词文件的编码格式。
/// </summary>
public string FileEncoding { get; set; } = "utf-8";
/// <summary>
/// 是否只输出翻译歌词。
/// </summary>
public bool IsOnlyOutputTranslation { get; set; } = false;
}

View File

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

View File

@@ -0,0 +1,13 @@
namespace ZonyLrcTools.Common.Configuration;
public class LyricsOptions
{
public IEnumerable<LyricsProviderOptions> Plugin { get; set; }
public GlobalLyricsConfigOptions Config { get; set; }
public LyricsProviderOptions GetLyricProviderOption(string name)
{
return Plugin.FirstOrDefault(x => x.Name == name);
}
}

View File

@@ -1,6 +1,6 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Configuration
{ {
public class LyricDownloaderOption public class LyricsProviderOptions
{ {
/// <summary> /// <summary>
/// 歌词下载器的唯一标识。 /// 歌词下载器的唯一标识。
@@ -11,5 +11,10 @@
/// 歌词下载时的优先级,当值为 -1 时是禁用。 /// 歌词下载时的优先级,当值为 -1 时是禁用。
/// </summary> /// </summary>
public int Priority { get; set; } public int Priority { get; set; }
/// <summary>
/// 搜索深度,值越大搜索结果越多,但搜索时间越长。
/// </summary>
public int Depth { get; set; }
} }
} }

View File

@@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.Network namespace ZonyLrcTools.Common.Configuration
{ {
/// <summary> /// <summary>
/// 工具网络相关的设定。 /// 工具网络相关的设定。
@@ -8,16 +8,16 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <summary> /// <summary>
/// 是否启用了网络代理功能。 /// 是否启用了网络代理功能。
/// </summary> /// </summary>
public bool Enable { get; set; } public bool IsEnable { get; set; }
/// <summary> /// <summary>
/// 代理服务器的 Ip。 /// 代理服务器的 Ip。
/// </summary> /// </summary>
public string ProxyIp { get; set; } public string Ip { get; set; }
/// <summary> /// <summary>
/// 代理服务器的 端口。 /// 代理服务器的 端口。
/// </summary> /// </summary>
public int ProxyPort { get; set; } public int Port { get; set; }
} }
} }

View File

@@ -0,0 +1,14 @@
namespace ZonyLrcTools.Common.Configuration;
public class ProviderOptions
{
/// <summary>
/// 标签加载器相关的配置属性。
/// </summary>
public TagInfoOptions Tag { get; set; }
/// <summary>
/// 歌词下载相关的配置信息。
/// </summary>
public LyricsOptions Lyric { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace ZonyLrcTools.Common.Configuration;
public class TagInfoOptions
{
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; }
/// <summary>
/// 屏蔽词功能相关配置。
/// </summary>
public BlockWordOptions BlockWord { get; set; }
}

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic; namespace ZonyLrcTools.Common.Configuration
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{ {
public class TagInfoProviderInstance public class TagInfoProviderOptions
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@@ -0,0 +1,12 @@
namespace ZonyLrcTools.Common;
public interface IMusicInfoLoader
{
Task<List<MusicInfo?>> LoadAsync(string dirPath,
int parallelCount = 2,
CancellationToken cancellationToken = default);
Task<List<MusicInfo?>> LoadAsync(IReadOnlyCollection<string> filePaths,
int parallelCount = 2,
CancellationToken cancellationToken = default);
}

View File

@@ -1,11 +1,8 @@
using System; using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Cli.Infrastructure.Extensions; using ZonyLrcTools.Common.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{ {
public static class AutoDependencyInjectExtensions public static class AutoDependencyInjectExtensions
{ {

View File

@@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{ {
/// <summary> /// <summary>
/// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。 /// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。

View File

@@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{ {
/// <summary> /// <summary>
/// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。 /// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。

View File

@@ -1,13 +1,11 @@
using System.IO;
using System.Net; using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config; using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Cli.Infrastructure.Network; using ZonyLrcTools.Common.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{ {
/// <summary> /// <summary>
/// Service 注入的扩展方法。 /// Service 注入的扩展方法。
@@ -27,12 +25,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant) services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant)
.ConfigurePrimaryHttpMessageHandler(provider => .ConfigurePrimaryHttpMessageHandler(provider =>
{ {
var option = provider.GetRequiredService<IOptions<ToolOptions>>().Value; var option = provider.GetRequiredService<IOptions<GlobalOptions>>().Value;
return new HttpClientHandler return new HttpClientHandler
{ {
UseProxy = option.NetworkOptions.Enable, UseProxy = option.NetworkOptions.IsEnable,
Proxy = new WebProxy($"{option.NetworkOptions.ProxyIp}:{option.NetworkOptions.ProxyPort}") Proxy = new WebProxy($"{option.NetworkOptions.Ip}:{option.NetworkOptions.Port}")
}; };
}); });
@@ -46,10 +44,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json") .AddYamlFile("config.yaml")
.Build(); .Build();
services.Configure<ToolOptions>(configuration.GetSection("ToolOption")); services.Configure<GlobalOptions>(configuration.GetSection("globalOption"));
return services; return services;
} }

View File

@@ -1,6 +1,4 @@
using System; namespace ZonyLrcTools.Common.Infrastructure.Exceptions
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
{ {
/// <summary> /// <summary>
/// 带错误码的异常实现。 /// 带错误码的异常实现。
@@ -17,7 +15,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param> /// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
/// <param name="message">错误信息。</param> /// <param name="message">错误信息。</param>
/// <param name="attachObj">附加的对象数据。</param> /// <param name="attachObj">附加的对象数据。</param>
public ErrorCodeException(int errorCode, string message = null, object attachObj = null) : base(message) public ErrorCodeException(int errorCode, string? message = null, object attachObj = null) : base(message)
{ {
ErrorCode = errorCode; ErrorCode = errorCode;
AttachObject = attachObj; AttachObject = attachObj;

View File

@@ -1,10 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{ {
/// <summary> /// <summary>
/// 错误码相关的帮助类。 /// 错误码相关的帮助类。

View File

@@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{ {
/// <summary> /// <summary>
/// 错误码。 /// 错误码。
@@ -27,6 +27,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
/// </summary> /// </summary>
public const int NoFilesWereScanned = 10004; public const int NoFilesWereScanned = 10004;
/// <summary>
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
/// </summary>
public const int NotSupportedFileEncoding = 10005;
#endregion #endregion
#region > < #region > <

View File

@@ -1,7 +1,4 @@
using System; namespace ZonyLrcTools.Common.Infrastructure.Extensions
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
{ {
/// <summary> /// <summary>
/// Linq 相关的扩展方法。 /// Linq 相关的扩展方法。

View File

@@ -1,15 +1,15 @@
using System;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Logging;
namespace ZonyLrcTools.Cli.Infrastructure.Extensions namespace ZonyLrcTools.Common.Infrastructure.Extensions
{ {
/// <summary> /// <summary>
/// 日志记录相关的扩展方法。 /// 日志记录相关的扩展方法。
/// </summary> /// </summary>
public static class LoggerExtensions public static class LoggerHelper
{ {
/// <summary> /// <summary>
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。 /// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
@@ -17,9 +17,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
/// <param name="logger">日志记录器实例。</param> /// <param name="logger">日志记录器实例。</param>
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param> /// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</param>
/// <param name="e">异常实例,可为空。</param> /// <param name="e">异常实例,可为空。</param>
public static void LogWarningWithErrorCode(this ILogger logger, int errorCode, Exception e = null) public static void LogWarningWithErrorCode(this IWarpLogger logger, int errorCode, Exception e = null)
{ {
logger.LogWarning($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}"); logger.WarnAsync($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}").GetAwaiter().GetResult();
} }
/// <summary> /// <summary>
@@ -27,7 +27,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
/// </summary> /// </summary>
/// <param name="logger">日志记录器的实例。</param> /// <param name="logger">日志记录器的实例。</param>
/// <param name="exception">错误码异常实例。</param> /// <param name="exception">错误码异常实例。</param>
public static void LogWarningInfo(this ILogger logger, ErrorCodeException exception) public static void LogWarningInfo(this IWarpLogger logger, ErrorCodeException exception)
{ {
if (exception.ErrorCode < 50000) if (exception.ErrorCode < 50000)
{ {
@@ -37,17 +37,17 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}"); sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}"); sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
logger.LogWarning(sb.ToString()); logger.WarnAsync(sb.ToString()).GetAwaiter().GetResult();
} }
/// <summary> /// <summary>
/// 使用 <see cref="LogLevel.Information"/> 级别打印歌曲信息。 /// 使用 <see cref="LogLevel.Information"/> 级别打印歌曲下载成功信息。
/// </summary> /// </summary>
/// <param name="logger">日志记录器的实例。</param> /// <param name="logger">日志记录器的实例。</param>
/// <param name="musicInfo">需要打印的歌曲信息。</param> /// <param name="musicInfo">需要打印的歌曲信息。</param>
public static void LogMusicInfoWithInformation(this ILogger logger, MusicInfo musicInfo) public static void LogSuccessful(this IWarpLogger logger, MusicInfo musicInfo)
{ {
logger.LogInformation($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 歌曲路径: {musicInfo.FilePath}"); logger.InfoAsync($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.").GetAwaiter().GetResult();
} }
} }
} }

View File

@@ -1,6 +1,4 @@
using System; namespace ZonyLrcTools.Common.Infrastructure.Extensions
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
{ {
/// <summary> /// <summary>
/// 字符串处理相关的工具方法。 /// 字符串处理相关的工具方法。

View File

@@ -1,24 +1,20 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions; using ZonyLrcTools.Common.Infrastructure.Extensions;
using ZonyLrcTools.Common.Infrastructure.Logging;
namespace ZonyLrcTools.Cli.Infrastructure.IO namespace ZonyLrcTools.Common.Infrastructure.IO
{ {
public class FileScanner : IFileScanner, ITransientDependency public class FileScanner : IFileScanner, ITransientDependency
{ {
public ILogger<FileScanner> Logger { get; set; } private readonly IWarpLogger _logger;
public FileScanner() public FileScanner(IWarpLogger logger)
{ {
Logger = NullLogger<FileScanner>.Instance; _logger = logger;
} }
public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions) public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions)
@@ -63,7 +59,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.IO
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e); _logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e);
} }
} }
} }

View File

@@ -0,0 +1,15 @@
namespace ZonyLrcTools.Common.Infrastructure.IO;
public static class FileScannerExtensions
{
public static async Task<IEnumerable<string>> ScanMusicFilesAsync(this IFileScanner fileScanner,
string dirPath,
IEnumerable<string> extensions)
{
var files = (await fileScanner.ScanAsync(dirPath, extensions))
.SelectMany(t => t.FilePaths)
.ToList();
return files;
}
}

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic; namespace ZonyLrcTools.Common.Infrastructure.IO
namespace ZonyLrcTools.Cli.Infrastructure.IO
{ {
/// <summary> /// <summary>
/// 文件扫描结果对象。 /// 文件扫描结果对象。

View File

@@ -1,7 +1,4 @@
using System.IO; namespace ZonyLrcTools.Common.Infrastructure.IO
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.IO
{ {
public static class FileStreamExtensions public static class FileStreamExtensions
{ {

View File

@@ -1,7 +1,4 @@
using System.Collections.Generic; namespace ZonyLrcTools.Common.Infrastructure.IO
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.IO
{ {
/// <summary> /// <summary>
/// 音乐文件扫描器,用于扫描音乐文件。 /// 音乐文件扫描器,用于扫描音乐文件。

View File

@@ -0,0 +1,12 @@
namespace ZonyLrcTools.Common.Infrastructure.Logging;
/// <summary>
/// 日志记录器,包装了 CLI 和网页日志的两种输出方式。
/// </summary>
public interface IWarpLogger
{
Task DebugAsync(string message, Exception? exception = null);
Task InfoAsync(string message, Exception? exception = null);
Task WarnAsync(string message, Exception? exception = null);
Task ErrorAsync(string message, Exception? exception = null);
}

View File

@@ -1,14 +1,11 @@
using System; using System.Net;
using System.Net;
using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Common.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Network namespace ZonyLrcTools.Common.Infrastructure.Network
{ {
public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency
{ {
@@ -123,7 +120,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
{ {
HttpStatusCode.OK => responseString, HttpStatusCode.OK => responseString,
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable), HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
_ => throw new ErrorCodeException(ErrorCodes.HttpRequestFailed, attachObj: new {requestParameters, responseString}) _ => throw new ErrorCodeException(ErrorCodes.HttpRequestFailed, attachObj: new { requestParameters, responseString })
}; };
} }
@@ -136,7 +133,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception> /// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception>
private TResponse ConvertHttpResponseToObject<TResponse>(object requestParameters, string responseString) private TResponse ConvertHttpResponseToObject<TResponse>(object requestParameters, string responseString)
{ {
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {requestParameters, responseString}); var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new { requestParameters, responseString });
try try
{ {

View File

@@ -1,8 +1,4 @@
using System; namespace ZonyLrcTools.Common.Infrastructure.Network
using System.Net.Http;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Network
{ {
/// <summary> /// <summary>
/// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。 /// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。

View File

@@ -1,8 +1,4 @@
using System; namespace ZonyLrcTools.Common.Infrastructure.Threading
using System.Threading;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Threading
{ {
/// <summary> /// <summary>
/// 针对 Task 的包装类,基于信号量 <see cref="SemaphoreSlim"/> 限定并行度。 /// 针对 Task 的包装类,基于信号量 <see cref="SemaphoreSlim"/> 限定并行度。

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Common.Lyrics;
public interface ILyricsDownloader
{
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 2,
CancellationToken cancellationToken = default);
IEnumerable<ILyricsProvider> AvailableProviders { get; }
}

View File

@@ -0,0 +1,23 @@
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 构建 <see cref="LyricsItemCollection"/> 对象的工厂。
/// </summary>
public interface ILyricsItemCollectionFactory
{
/// <summary>
/// 根据指定的歌曲数据构建新的 <see cref="LyricsItemCollection"/> 实例。
/// </summary>
/// <param name="sourceLyric">原始歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
LyricsItemCollection Build(string? sourceLyric);
/// <summary>
/// 根据指定的歌曲数据构建新的 <see cref="LyricsItemCollection"/> 实例。
/// </summary>
/// <param name="sourceLyric">原始歌词数据。</param>
/// <param name="translationLyric">翻译歌词数据。</param>
/// <returns>构建完成的 <see cref="LyricsItemCollection"/> 对象。</returns>
LyricsItemCollection Build(string sourceLyric, string translationLyric);
}
}

View File

@@ -1,19 +1,18 @@
using System.Threading.Tasks; namespace ZonyLrcTools.Common.Lyrics
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{ {
/// <summary> /// <summary>
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。 /// 歌词数据下载器,用于匹配并下载歌曲的歌词。
/// </summary> /// </summary>
public interface ILyricDownloader public interface ILyricsProvider
{ {
/// <summary> /// <summary>
/// 下载歌词数据。 /// 下载歌词数据。
/// </summary> /// </summary>
/// <param name="songName">歌曲的名称。</param> /// <param name="songName">歌曲的名称。</param>
/// <param name="artist">歌曲的作者。</param> /// <param name="artist">歌曲的作者。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>歌曲的歌词数据对象。</returns> /// <returns>歌曲的歌词数据对象。</returns>
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist); ValueTask<LyricsItemCollection> DownloadAsync(string? songName, string? artist, long? duration = null);
/// <summary> /// <summary>
/// 下载器的名称。 /// 下载器的名称。

View File

@@ -0,0 +1,7 @@
namespace ZonyLrcTools.Common.Lyrics
{
public interface ILyricsTextResolver
{
LyricsItemCollection Resolve(string lyricText);
}
}

View File

@@ -1,9 +1,9 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Lyrics
{ {
/// <summary> /// <summary>
/// 定义了程序默认提供的歌词下载器。 /// 定义了程序默认提供的歌词下载器。
/// </summary> /// </summary>
public static class InternalLyricDownloaderNames public static class InternalLyricsProviderNames
{ {
/// <summary> /// <summary>
/// 网易云音乐歌词下载器。 /// 网易云音乐歌词下载器。
@@ -19,5 +19,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// 酷狗音乐歌词下载器。 /// 酷狗音乐歌词下载器。
/// </summary> /// </summary>
public const string KuGou = nameof(KuGou); public const string KuGou = nameof(KuGou);
/// <summary>
/// 酷我音乐歌词下载器。
/// </summary>
public const string KuWo = nameof(KuWo);
} }
} }

View File

@@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Lyrics
{ {
/// <summary> /// <summary>
/// 换行符格式定义。 /// 换行符格式定义。

View File

@@ -0,0 +1,127 @@
using System.Text;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Extensions;
using ZonyLrcTools.Common.Infrastructure.Logging;
using ZonyLrcTools.Common.Infrastructure.Threading;
namespace ZonyLrcTools.Common.Lyrics;
public class LyricsDownloader : ILyricsDownloader, ISingletonDependency
{
private readonly IEnumerable<ILyricsProvider> _lyricsProviders;
private readonly IWarpLogger _logger;
private readonly GlobalOptions _options;
public IEnumerable<ILyricsProvider> AvailableProviders => new Lazy<IEnumerable<ILyricsProvider>>(() =>
{
return _options.Provider.Lyric.Plugin
.Where(op => op.Priority != -1)
.OrderBy(op => op.Priority)
.Join(_lyricsProviders,
op => op.Name,
loader => loader.DownloaderName,
(_, loader) => loader);
}).Value;
public LyricsDownloader(IEnumerable<ILyricsProvider> lyricsProviders,
IOptions<GlobalOptions> options,
IWarpLogger logger)
{
_lyricsProviders = lyricsProviders;
_logger = logger;
_options = options.Value;
}
public async Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 2,
CancellationToken cancellationToken = default)
{
await _logger.InfoAsync("开始下载歌词文件数据...");
if (parallelCount <= 0)
{
parallelCount = 1;
}
var warpTask = new WarpTask(parallelCount);
var downloadTasks = needDownloadMusicInfos.Select(info =>
warpTask.RunAsync(() =>
Task.Run(async () =>
{
// Try to download lyrics from all available providers.
foreach (var lyricsProvider in AvailableProviders)
{
await DownloadAndWriteLyricsAsync(lyricsProvider, info);
if (info.IsSuccessful)
{
_logger.LogSuccessful(info);
return;
}
}
}, cancellationToken), cancellationToken));
await Task.WhenAll(downloadTasks);
await _logger.InfoAsync($"歌词数据下载完成,成功: {needDownloadMusicInfos.Count(m => m.IsSuccessful)} 失败{needDownloadMusicInfos.Count(m => m.IsSuccessful == false)}。");
}
private async Task DownloadAndWriteLyricsAsync(ILyricsProvider provider, MusicInfo info)
{
try
{
var lyrics = await provider.DownloadAsync(info.Name, info.Artist);
if (lyrics.IsPruneMusic)
{
info.IsSuccessful = true;
return;
}
var newLyricsFilePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!,
$"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
if (File.Exists(newLyricsFilePath))
{
File.Delete(newLyricsFilePath);
}
// Write lyrics to file.
await using (var fileStream = new FileStream(newLyricsFilePath, FileMode.CreateNew, FileAccess.Write))
{
await using (var binaryWriter = new BinaryWriter(fileStream, Encoding.UTF8))
{
binaryWriter.Write(Utf8ToSelectedEncoding(lyrics));
binaryWriter.Flush();
}
}
info.IsSuccessful = true;
}
catch (ErrorCodeException ex)
{
_logger.LogWarningInfo(ex);
info.IsSuccessful = false;
}
catch (Exception ex)
{
await _logger.ErrorAsync($"下载歌词文件时发生错误:{ex.Message},歌曲名: {info.Name},歌手: {info.Artist}。");
info.IsSuccessful = false;
}
}
// Convert UTF-8 to selected encoding.
private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics)
{
var supportEncodings = Encoding.GetEncodings();
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
{
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
}
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyrics.GetUtf8Bytes());
}
}

View File

@@ -1,12 +1,11 @@
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Lyrics
{ {
/// <summary> /// <summary>
/// 歌词的行对象,是 <see cref="LyricItemCollection"/> 的最小单位。。 /// 歌词的行对象,是 <see cref="LyricsItemCollection"/> 的最小单位。。
/// </summary> /// </summary>
public class LyricItem : IComparable<LyricItem> public class LyricsItem : IComparable<LyricsItem>
{ {
/// <summary> /// <summary>
/// 原始时间轴,格式类似于 [01:55.12]。 /// 原始时间轴,格式类似于 [01:55.12]。
@@ -34,10 +33,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
public double SortScore => Minute * 60 + Second; public double SortScore => Minute * 60 + Second;
/// <summary> /// <summary>
/// 构建新的 <see cref="LyricItem"/> 对象。 /// 构建新的 <see cref="LyricsItem"/> 对象。
/// </summary> /// </summary>
/// <param name="lyricText">原始的 Lyric 歌词。</param> /// <param name="lyricText">原始的 Lyric 歌词。</param>
public LyricItem(string lyricText) public LyricsItem(string lyricText)
{ {
var timeline = new Regex(@"\[\d+:\d+.\d+\]").Match(lyricText) var timeline = new Regex(@"\[\d+:\d+.\d+\]").Match(lyricText)
.Value.Replace("]", string.Empty) .Value.Replace("]", string.Empty)
@@ -51,19 +50,19 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
} }
/// <summary> /// <summary>
/// 构造新的 <see cref="LyricItem"/> 对象。 /// 构造新的 <see cref="LyricsItem"/> 对象。
/// </summary> /// </summary>
/// <param name="minute">歌词所在的时间(分)。</param> /// <param name="minute">歌词所在的时间(分)。</param>
/// <param name="second">歌词所在的时间(秒)。</param> /// <param name="second">歌词所在的时间(秒)。</param>
/// <param name="lyricText">歌词文本数据。</param> /// <param name="lyricText">歌词文本数据。</param>
public LyricItem(int minute, double second, string lyricText) public LyricsItem(int minute, double second, string lyricText)
{ {
Minute = minute; Minute = minute;
Second = second; Second = second;
LyricText = lyricText; LyricText = lyricText;
} }
public int CompareTo(LyricItem other) public int CompareTo(LyricsItem other)
{ {
if (SortScore > other.SortScore) if (SortScore > other.SortScore)
{ {
@@ -78,32 +77,32 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
return 0; return 0;
} }
public static bool operator >(LyricItem left, LyricItem right) public static bool operator >(LyricsItem left, LyricsItem right)
{ {
return left.SortScore > right.SortScore; return left.SortScore > right.SortScore;
} }
public static bool operator <(LyricItem left, LyricItem right) public static bool operator <(LyricsItem left, LyricsItem right)
{ {
return left.SortScore < right.SortScore; return left.SortScore < right.SortScore;
} }
public static bool operator ==(LyricItem left, LyricItem right) public static bool operator ==(LyricsItem left, LyricsItem right)
{ {
return (int?) left?.SortScore == (int?) right?.SortScore; return (int?)left?.SortScore == (int?)right?.SortScore;
} }
public static bool operator !=(LyricItem item1, LyricItem item2) public static bool operator !=(LyricsItem item1, LyricsItem item2)
{ {
return !(item1 == item2); return !(item1 == item2);
} }
public static LyricItem operator +(LyricItem src, LyricItem dist) public static LyricsItem operator +(LyricsItem src, LyricsItem dist)
{ {
return new LyricItem(src.Minute, src.Second, $"{src.LyricText} {dist.LyricText}"); return new LyricsItem(src.Minute, src.Second, $"{src.LyricText} {dist.LyricText}");
} }
protected bool Equals(LyricItem other) protected bool Equals(LyricsItem other)
{ {
return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second); return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second);
} }
@@ -113,7 +112,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true; if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false; if (obj.GetType() != this.GetType()) return false;
return Equals((LyricItem) obj); return Equals((LyricsItem)obj);
} }
public override int GetHashCode() public override int GetHashCode()

View File

@@ -1,39 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using ZonyLrcTools.Cli.Infrastructure.Extensions; using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Extensions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Lyrics
{ {
/// <summary> /// <summary>
/// 歌词数据,包含多条歌词行对象(<see cref="LyricItem"/>)。 /// 歌词数据,包含多条歌词行对象(<see cref="LyricsItem"/>)。
/// </summary> /// </summary>
public class LyricItemCollection : List<LyricItem> public class LyricsItemCollection : List<LyricsItem>
{ {
/// <summary> /// <summary>
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。 /// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
/// </summary> /// </summary>
public bool IsPruneMusic => Count == 0; public bool IsPruneMusic => Count == 0;
public LyricItemCollectionOption Option { get; private set; } public GlobalLyricsConfigOptions? Options { get; private set; }
public LyricItemCollection(LyricItemCollectionOption option) public LyricsItemCollection(GlobalLyricsConfigOptions? options)
{ {
Option = option; Options = options;
} }
public static LyricItemCollection operator +(LyricItemCollection left, LyricItemCollection right) public static LyricsItemCollection operator +(LyricsItemCollection left, LyricsItemCollection right)
{ {
if (right.IsPruneMusic) if (right.IsPruneMusic)
{ {
return left; return left;
} }
var option = left.Option; var option = left.Options;
var newCollection = new LyricItemCollection(option); var newCollection = new LyricsItemCollection(option);
var indexDiff = left.Count - right.Count; var indexDiff = left.Count - right.Count;
if (!option.IsOneLine) if (!option.IsOneLine)
{ {
left.ForEach(item => newCollection.Add(item)); left.ForEach(item => newCollection.Add(item));
@@ -74,10 +71,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
// 遍历未处理的歌词项,将其添加到返回集合当中。 // 遍历未处理的歌词项,将其添加到返回集合当中。
var leftWaitProcessIndex = leftMarkDict var leftWaitProcessIndex = leftMarkDict
.Where(item => item.Value) .Where(item => !item.Value)
.Select(pair => pair.Key); .Select(pair => pair.Key);
var rightWaitProcessIndex = rightMarkDict var rightWaitProcessIndex = rightMarkDict
.Where(item => item.Value) .Where(item => !item.Value)
.Select(pair => pair.Key); .Select(pair => pair.Key);
leftWaitProcessIndex.Foreach(index => newCollection.Add(left[index])); leftWaitProcessIndex.Foreach(index => newCollection.Add(left[index]));
@@ -94,18 +91,23 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。 /// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
/// </remarks> /// </remarks>
/// <param name="items">等待构建的歌词集合实例。</param> /// <param name="items">等待构建的歌词集合实例。</param>
private static Dictionary<int, bool> BuildMarkDictionary(LyricItemCollection items) private static Dictionary<int, bool> BuildMarkDictionary(LyricsItemCollection items)
{ {
return items return items
.Select((item, index) => new {index, item}) .Select((item, index) => new { index, item })
.ToDictionary(item => item.index, item => false); .ToDictionary(item => item.index, item => false);
} }
public override string ToString() public override string ToString()
{ {
var lyricBuilder = new StringBuilder(); var lyricBuilder = new StringBuilder();
ForEach(lyric => lyricBuilder.Append(lyric).Append(Option.LineBreak)); ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.LineBreak));
return lyricBuilder.ToString().TrimEnd(Option.LineBreak); return lyricBuilder.ToString().TrimEnd(Options.LineBreak);
}
public byte[] GetUtf8Bytes()
{
return Encoding.UTF8.GetBytes(ToString());
} }
} }
} }

View File

@@ -0,0 +1,68 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// <see cref="ILyricsItemCollectionFactory"/> 的默认实现。
/// </summary>
public class LyricsItemCollectionFactory : ILyricsItemCollectionFactory, ITransientDependency
{
private readonly GlobalOptions _options;
public LyricsItemCollectionFactory(IOptions<GlobalOptions> options)
{
_options = options.Value;
}
public LyricsItemCollection Build(string? sourceLyric)
{
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
if (string.IsNullOrEmpty(sourceLyric))
{
return lyric;
}
InternalBuildLyricObject(lyric, sourceLyric);
return lyric;
}
public LyricsItemCollection Build(string sourceLyric, string translationLyric)
{
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
if (string.IsNullOrEmpty(sourceLyric))
{
return lyric;
}
lyric = InternalBuildLyricObject(lyric, sourceLyric);
if (_options.Provider.Lyric.Config.IsEnableTranslation && !string.IsNullOrEmpty(translationLyric))
{
var translatedLyric = InternalBuildLyricObject(new LyricsItemCollection(_options.Provider.Lyric.Config), translationLyric);
if (_options.Provider.Lyric.Config.IsOnlyOutputTranslation)
{
return translatedLyric;
}
return lyric + translatedLyric;
}
return lyric;
}
private LyricsItemCollection InternalBuildLyricObject(LyricsItemCollection lyrics, string sourceText)
{
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
foreach (Match match in regex.Matches(sourceText))
{
lyrics.Add(new LyricsItem(match.Value));
}
return lyrics;
}
}
}

View File

@@ -1,13 +1,12 @@
using System.Threading.Tasks; using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric namespace ZonyLrcTools.Common.Lyrics
{ {
/// <summary> /// <summary>
/// 歌词下载器的基类,定义了歌词下载器的常规逻辑。 /// 歌词下载器的基类,定义了歌词下载器的常规逻辑。
/// </summary> /// </summary>
public abstract class LyricDownloader : ILyricDownloader, ITransientDependency public abstract class LyricsProvider : ILyricsProvider, ITransientDependency
{ {
public abstract string DownloaderName { get; } public abstract string DownloaderName { get; }
@@ -16,20 +15,21 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// </summary> /// </summary>
/// <param name="songName">歌曲名称。</param> /// <param name="songName">歌曲名称。</param>
/// <param name="artist">歌曲作者/艺术家。</param> /// <param name="artist">歌曲作者/艺术家。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>下载完成的歌曲数据。</returns> /// <returns>下载完成的歌曲数据。</returns>
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist) public virtual async ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
{ {
var args = new LyricDownloaderArgs(songName, artist); var args = new LyricsProviderArgs(songName, artist, duration ?? 0);
await ValidateAsync(args); await ValidateAsync(args);
var downloadDataBytes = await DownloadDataAsync(args); var downloadDataObject = await DownloadDataAsync(args);
return await GenerateLyricAsync(downloadDataBytes, args); return await GenerateLyricAsync(downloadDataObject, args);
} }
/// <summary> /// <summary>
/// 通用的验证逻辑,验证基本参数是否正确。 /// 通用的验证逻辑,验证基本参数是否正确。
/// </summary> /// </summary>
/// <param name="args">歌词下载时需要的参数信息。</param> /// <param name="args">歌词下载时需要的参数信息。</param>
protected virtual ValueTask ValidateAsync(LyricDownloaderArgs args) protected virtual ValueTask ValidateAsync(LyricsProviderArgs args)
{ {
if (string.IsNullOrEmpty(args.SongName)) if (string.IsNullOrEmpty(args.SongName))
{ {
@@ -47,12 +47,13 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// <summary> /// <summary>
/// 根据指定的歌曲参数,下载歌词数据。 /// 根据指定的歌曲参数,下载歌词数据。
/// </summary> /// </summary>
protected abstract ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args); protected abstract ValueTask<object> DownloadDataAsync(LyricsProviderArgs args);
/// <summary> /// <summary>
/// 根据指定的歌词二进制数据,生成歌词数据。 /// 根据指定的歌词对象,生成歌词数据,常用于处理不同格式的歌词数据。
/// </summary> /// </summary>
/// <param name="data">歌词的原始二进制数据。</param> /// <param name="lyricsObject">当 <see cref="DownloadDataAsync"/> 完成后,传递的歌词数据对象。</param>
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args); /// <param name="args">生成歌词时,提供的歌曲信息参数。</param>
protected abstract ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args);
} }
} }

View File

@@ -0,0 +1,18 @@
namespace ZonyLrcTools.Common.Lyrics
{
public class LyricsProviderArgs
{
public string SongName { get; set; }
public string Artist { get; set; }
public long Duration { get; set; }
public LyricsProviderArgs(string songName, string artist, long duration)
{
SongName = songName;
Artist = artist;
Duration = duration;
}
}
}

View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class GetLyricAccessKeyRequest
{
[JsonProperty("ver")] public int UnknownParameters1 { get; }
[JsonProperty("man")] public string UnknownParameters2 { get; }
[JsonProperty("client")] public string UnknownParameters3 { get; }
[JsonProperty("hash")] public string FileHash { get; }
public GetLyricAccessKeyRequest(string fileHash)
{
UnknownParameters1 = 1;
UnknownParameters2 = "yes";
UnknownParameters3 = "mobi";
FileHash = fileHash;
}
}
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class GetLyricAccessKeyResponse
{
[JsonProperty("status")] public int Status { get; set; }
[JsonProperty("errcode")] public int ErrorCode { get; set; }
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject> AccessKeyDataObjects { get; set; }
}
public class GetLyricAccessKeyDataObject
{
[JsonProperty("accesskey")] public string AccessKey { get; set; }
[JsonProperty("id")] public string Id { get; set; }
}
}

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{ {
public class GetLyricRequest public class GetLyricRequest
{ {

View File

@@ -0,0 +1,28 @@
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class SongSearchRequest
{
[JsonProperty("filter")] public int Filter { get; }
[JsonProperty("platform")] public string Platform { get; }
[JsonProperty("keyword")] public string Keyword { get; }
[JsonProperty("pagesize")] public int PageSize { get; }
[JsonProperty("page")] public int Page { get; }
public SongSearchRequest(string musicName, string artistName, int pageSize = 30)
{
Filter = 2;
Platform = "WebFilter";
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
PageSize = pageSize;
Page = 1;
}
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("status")] public int Status { 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; }
}
public class SongSearchResponseInnerData
{
[JsonProperty("lists")] public List<SongSearchResponseSongDetail> List { get; set; }
}
public class SongSearchResponseSongDetail
{
public string FileHash { get; set; }
}
}

View File

@@ -1,35 +1,38 @@
using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.Exceptions; using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel; using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Network; using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
{ {
public class KuGourLyricDownloader : LyricDownloader public class KuGourLyricsProvider : LyricsProvider
{ {
public override string DownloaderName => InternalLyricDownloaderNames.KuGou; public override string DownloaderName => InternalLyricsProviderNames.KuGou;
private readonly IWarpHttpClient _warpHttpClient; private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory; private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
private readonly GlobalOptions _options;
private const string KuGouSearchMusicUrl = @"https://songsearch.kugou.com/song_search_v2"; private const string KuGouSearchMusicUrl = @"https://songsearch.kugou.com/song_search_v2";
private const string KuGouGetLyricAccessKeyUrl = @"http://lyrics.kugou.com/search"; private const string KuGouGetLyricAccessKeyUrl = @"http://lyrics.kugou.com/search";
private const string KuGouGetLyricUrl = @"http://lyrics.kugou.com/download"; private const string KuGouGetLyricUrl = @"http://lyrics.kugou.com/download";
public KuGourLyricDownloader(IWarpHttpClient warpHttpClient, public KuGourLyricsProvider(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory) ILyricsItemCollectionFactory lyricsItemCollectionFactory,
IOptions<GlobalOptions> options)
{ {
_warpHttpClient = warpHttpClient; _warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory; _lyricsItemCollectionFactory = lyricsItemCollectionFactory;
_options = options.Value;
} }
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args) protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
{ {
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl, var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist)); new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth));
ValidateSongSearchResponse(searchResult, args); ValidateSongSearchResponse(searchResult, args);
@@ -38,26 +41,24 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou
new GetLyricAccessKeyRequest(searchResult.Data.List[0].FileHash)); new GetLyricAccessKeyRequest(searchResult.Data.List[0].FileHash));
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0]; var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
var lyricResponse = await _warpHttpClient.GetAsync(KuGouGetLyricUrl, return await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
new GetLyricRequest(accessKeyObject.Id, accessKeyObject.AccessKey)); new GetLyricRequest(accessKeyObject.Id, accessKeyObject.AccessKey));
return Encoding.UTF8.GetBytes(lyricResponse);
} }
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args) protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object data, LyricsProviderArgs args)
{ {
await ValueTask.CompletedTask; await ValueTask.CompletedTask;
var lyricJsonObj = JObject.Parse(Encoding.UTF8.GetString(data)); var lyricJsonObj = JObject.Parse((data as string)!);
if (lyricJsonObj.SelectToken("$.status").Value<int>() != 200) if (lyricJsonObj.SelectToken("$.status").Value<int>() != 200)
{ {
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args); throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
} }
var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content").Value<string>())); var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content").Value<string>()));
return _lyricItemCollectionFactory.Build(lyricText); return _lyricsItemCollectionFactory.Build(lyricText);
} }
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args) protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
{ {
if (response.ErrorCode != 0 && response.Status != 1 || response.Data.List == null) if (response.ErrorCode != 0 && response.Status != 1 || response.Data.List == null)
{ {

View File

@@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
public class GetLyricsRequest
{
[JsonProperty("musicId")] public long MusicId { get; }
public GetLyricsRequest(long musicId)
{
MusicId = musicId;
}
}

View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
public class GetLyricsResponse
{
[JsonProperty("status")] public int Status { get; set; }
[JsonProperty("data")] public GetLyricsResponseInnerData Data { get; set; }
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
[JsonProperty("msgs")] public string? ErrorMessage2 { get; set; }
}
public class GetLyricsResponseInnerData
{
[JsonProperty("lrclist")] public ICollection<GetLyricsItem> Lyrics { get; set; }
}
public class GetLyricsItem
{
[JsonProperty("lineLyric")] public string Text { get; set; }
[JsonProperty("time")] public string Position { get; set; }
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
public class SongSearchRequest
{
[JsonProperty("key")] public string Keyword { get; set; }
[JsonProperty("pn")] public int PageNumber { get; }
[JsonProperty("rn")] public int PageSize { get; }
public SongSearchRequest(string name, string artist, int pageNumber = 1, int pageSize = 20)
{
Keyword = $"{name} {artist}";
PageNumber = pageNumber;
PageSize = pageSize;
}
}

View File

@@ -0,0 +1,46 @@
using Newtonsoft.Json;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
public class SongSearchResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("data")] public SongSearchResponseInnerData InnerData { get; set; }
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
public long GetMatchedMusicId(string musicName, string artistName, long? duration)
{
var prefectMatch = InnerData.SongItems.FirstOrDefault(x => x.Name == musicName && x.Artist == artistName);
if (prefectMatch != null)
{
return prefectMatch.MusicId;
}
if (duration is null or 0)
{
return InnerData.SongItems.First().MusicId;
}
return InnerData.SongItems.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().MusicId;
}
}
public class SongSearchResponseInnerData
{
[JsonProperty("total")] public string Total { get; set; }
[JsonProperty("list")] public ICollection<SongSearchResponseDetail> SongItems { get; set; }
}
public class SongSearchResponseDetail
{
[JsonProperty("artist")] public string? Artist { get; set; }
[JsonProperty("name")] public string? Name { get; set; }
[JsonProperty("rid")] public long MusicId { get; set; }
[JsonProperty("duration")] public long Duration { get; set; }
}

View File

@@ -0,0 +1,87 @@
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo;
public class KuWoLyricsProvider : LyricsProvider
{
public override string DownloaderName => InternalLyricsProviderNames.KuWo;
private const string KuWoSearchMusicUrl = @"https://www.kuwo.cn/api/www/search/searchMusicBykeyWord";
private const string KuWoSearchLyricsUrl = @"https://m.kuwo.cn/newh5/singles/songinfoandlrc";
private const string KuWoDefaultToken = "ABCDE12345";
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
private readonly GlobalOptions _options;
private static readonly ProductInfoHeaderValue UserAgent = new("Chrome", "81.0.4044.138");
public KuWoLyricsProvider(IWarpHttpClient warpHttpClient,
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
IOptions<GlobalOptions> options)
{
_warpHttpClient = warpHttpClient;
_lyricsItemCollectionFactory = lyricsItemCollectionFactory;
_options = options.Value;
}
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
{
var songSearchResponse = await _warpHttpClient.GetAsync<SongSearchResponse>(KuWoSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist, pageSize: _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
op =>
{
op.Headers.UserAgent.Add(UserAgent);
op.Headers.Referrer = new Uri("https://kuwo.cn");
op.Headers.Add("csrf", KuWoDefaultToken);
op.Headers.Add("Cookie", $"kw_token={KuWoDefaultToken}");
});
ValidateSongSearchResponse(songSearchResponse, args);
return await _warpHttpClient.GetAsync<GetLyricsResponse>(KuWoSearchLyricsUrl,
new GetLyricsRequest(songSearchResponse.GetMatchedMusicId(args.SongName, args.Artist, args.Duration)),
op =>
{
op.Headers.UserAgent.Add(UserAgent);
op.Headers.Referrer = new Uri("https://m.kuwo.cn/yinyue/");
});
}
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject, LyricsProviderArgs args)
{
await ValueTask.CompletedTask;
var lyricsResponse = (GetLyricsResponse)lyricsObject;
var items = lyricsResponse.Data.Lyrics.Select(l =>
{
var position = double.Parse(l.Position);
var positionSpan = TimeSpan.FromSeconds(position);
return new LyricsItem(positionSpan.Minutes, double.Parse($"{positionSpan.Seconds}.{positionSpan.Milliseconds}"), l.Text);
});
var lyricsItemCollection = new LyricsItemCollection(_options.Provider.Lyric.Config);
lyricsItemCollection.AddRange(items);
return lyricsItemCollection;
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
{
if (response.Code != 200)
{
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, response.ErrorMessage, args);
}
if (response.InnerData.SongItems.Count == 0)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}

View File

@@ -2,15 +2,15 @@ using Newtonsoft.Json;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{ {
public class GetLyricRequest public class GetLyricRequest
{ {
public GetLyricRequest(long songId) public GetLyricRequest(long songId)
{ {
OS = "osx"; OS = "ios";
Id = songId; Id = songId;
Lv = Kv = Tv = -1; Lv = Kv = Tv = Rv = -1;
} }
/// <summary> /// <summary>
@@ -30,5 +30,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
[JsonProperty("kv")] public int Kv { get; } [JsonProperty("kv")] public int Kv { get; }
[JsonProperty("tv")] public int Tv { get; } [JsonProperty("tv")] public int Tv { get; }
[JsonProperty("rv")] public int Rv { get; set; }
[JsonProperty("crypto")] public string Protocol { get; set; } = "api";
} }
} }

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{ {
public class GetLyricResponse public class GetLyricResponse
{ {
@@ -22,6 +22,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
[JsonProperty("tlyric")] [JsonProperty("tlyric")]
public InnerLyric TranslationLyric { get; set; } public InnerLyric TranslationLyric { get; set; }
/// <summary>
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
/// </summary>
[JsonProperty("romalrc")]
public InnerLyric RomaLyric { get; set; }
/// <summary> /// <summary>
/// 状态码。 /// 状态码。
/// </summary> /// </summary>

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
{ {
public class GetSongDetailsRequest public class GetSongDetailsRequest
{ {

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