Compare commits

...

260 Commits

Author SHA1 Message Date
real-zony
e08d1c7f16 ci: Publish new version.(4.0.2)
Some checks failed
.NET / build (push) Has been cancelled
.NET / release (push) Has been cancelled
2024-12-15 15:29:32 +08:00
real-zony
c3d98d60b5 fix: Fixed the garbled text issue caused by the CSV file encoding. (#156) 2024-12-15 15:20:33 +08:00
real-zony
8aac9e16a6 ci: Release new version.
Some checks failed
.NET / build (push) Has been cancelled
.NET / release (push) Has been cancelled
2024-11-29 23:20:04 +08:00
real-zony
21f25ee500 Merge remote-tracking branch 'origin/dev' into dev 2024-11-29 23:12:45 +08:00
real-zony
9a979942e3 test: Removed irrelevant tests. 2024-11-29 23:12:26 +08:00
real-zony
32b6c4052b build: Upgraded package version. 2024-11-29 23:11:40 +08:00
real-zony
7732ab52d3 refactor: Removed avalonia code. 2024-11-29 22:54:38 +08:00
Zony
1fdbaef7a8
Set English as the default language for project README. 2024-07-10 10:08:29 +08:00
Zony
36ef2d2c3a
Update English documentation. 2024-07-10 10:07:08 +08:00
Zony
fa4620c37d
Add separate Chinese documentation. 2024-07-10 10:00:57 +08:00
real-zony
9ab9cd50e2 feat: Mostly completed bidirectional binding for settings page and configuration. 2024-07-03 23:38:33 +08:00
real-zony
e66ef89e7a build: Upgrade the YamlDotNet version. 2024-07-02 21:51:55 +08:00
real-zony
8b5d5c64b0 refactor: Adjusted the structure of the configuration file.
BREAKING CHANGE: Adjusted the internal structure of the config.yaml file, removed the top-level hierarchy.
2024-07-02 21:22:40 +08:00
real-zony
2dca5239f5 fix: Fixed exception throw when NumericUpDown is empty. 2024-07-02 21:00:03 +08:00
real-zony
d5689164b5 fix(Fixed some issues in the settings page.): 2024-07-02 18:27:52 +08:00
real-zony
b57739f543 feat: Completed the adjustments and bindings of global settings. 2024-07-02 10:43:23 +08:00
real-zony
a2c44901fd refactor: Fixed incorrect naming. 2024-06-30 22:02:33 +08:00
real-zony
6ad2992144 feat: Added some UI animation effects. 2024-06-30 21:58:41 +08:00
real-zony
3b0c55ac00 feat: Completed the download lyrics feature. 2024-06-30 17:52:12 +08:00
real-zony
81bf6ebe3f refactor: Adjusted the view model to support reactive programming. 2024-06-30 17:51:53 +08:00
real-zony
aa3c45101b feat: Added a comparator for MusicInfo. 2024-06-30 17:50:49 +08:00
real-zony
d00d735bf0 feat: When downloading lyrics, a callback was added to facilitate UI updates. 2024-06-30 17:50:26 +08:00
real-zony
7fa595a71b refactor: Removed unused styles. 2024-06-28 15:13:00 +08:00
real-zony
4f15d06e63 feat: Added a settings page. 2024-06-28 15:07:06 +08:00
real-zony
5ccd8a7c53 feat: Complete the basic GUI layout. 2024-06-28 12:33:14 +08:00
real-zony
14812bceb7 refactor: Removed the local server project. 2024-06-28 12:29:31 +08:00
real-zony
be538eb650 feat: Upgraded the versions of the packages that the project depends on. 2024-06-28 12:27:24 +08:00
real-zony
888c7f511b refactor: Removed unused files. 2024-06-28 12:23:48 +08:00
Zony
7bd1915f11
Merge pull request #150 from w568w/patch-1
在 README 中添加 AUR 安装方式介绍
2024-06-19 17:37:47 +08:00
w568w
aa78ea0713
Update README.md 2024-06-08 07:16:29 +00:00
real-zony
1342ca936c build: Upgrade dependency packages. 2024-06-03 00:41:47 +08:00
Zony
46ac2d57a2
Merge pull request #149 from ZaneYork/dev
修复网易云歌曲ID溢出问题
2024-05-31 16:20:48 +08:00
ZaneYork
444f4e75a6 修复网易云歌曲ID溢出问题 2024-05-21 17:41:40 +08:00
Zony
61aa31cc6f
Removed alipay and wechatpay QR code image. 2024-04-12 16:09:06 +08:00
Zony
46d11a86ea
Update README.md 2024-04-12 16:08:19 +08:00
Zony
8aaae90ed8
Upgraded MusicDecrypto.Library version to 2.4.0. 2024-04-12 15:34:38 +08:00
real-zony
d91cc68a60 Merge remote-tracking branch 'origin/dev' into dev 2023-12-22 15:06:32 +08:00
real-zony
b6e3cb6038 chore: Upgrade version. 2023-12-22 14:59:48 +08:00
Zony
093e2879ad
Update release.md 2023-12-22 13:54:26 +08:00
real-zony
fea5e1124f fix: Fixed the issue where Kuwo Music could not download lyrics. 2023-12-22 13:52:28 +08:00
real-zony
2ce7b00d14 ci: Release new version. 2023-12-12 22:47:19 +08:00
real-zony
ad6ea732f8 Merge remote-tracking branch 'origin/dev' into dev 2023-12-12 22:46:20 +08:00
real-zony
06bf733ae9 fix: Fixed the issue where logs were not displaying. 2023-12-12 22:45:37 +08:00
Zony
558cca3c0e
Update release.md 2023-12-12 22:07:20 +08:00
Zony
93cc31b1e2
Update release.md 2023-12-12 22:06:56 +08:00
real-zony
dfe18d8541 ci: Fixed the script error. 2023-12-12 22:05:12 +08:00
real-zony
40b73c3dfa chore: Triggered the release of a new version. 2023-12-12 22:02:45 +08:00
real-zony
243a0e2559 fix: Fixed the issue where NetEase Cloud Music could not download lyrics. 2023-12-12 21:56:49 +08:00
real-zony
d83bff1516 build: Upgrade all packages. 2023-12-12 21:55:35 +08:00
real-zony
8d946ebbd5 build: Upgraded the dependency framework to .NET 8.0. 2023-12-12 19:22:50 +08:00
Zony
4c3d554885
Merge pull request #147 from travislee89/dev
Support for more file extensions; Building arm64 binaries
2023-12-11 20:45:28 +08:00
Travis Lee
14c78f9a83 feat: build arm64 binaries 2023-12-11 15:36:12 +08:00
Travis Lee
b9d6da5f72 feat: support file extensions m4a, ogg and opus 2023-12-11 15:35:55 +08:00
real-zony
c8ffbc55af fix: Fixed the issue mentioned in #145.
Added a new line break.('\n')
2023-09-11 09:49:02 +08:00
real-zony
b0835dcf01 docs: Adjusted README.md . 2023-08-13 22:31:35 +08:00
real-zony
4e1c4d1519 chore: Corrected description information. 2023-08-13 21:25:43 +08:00
real-zony
3d96b96edf build: Fixed NuGet package reference issue. 2023-08-13 21:24:11 +08:00
real-zony
7f81a3edea refactor: Managed dependencies with unified package management. 2023-07-14 21:20:01 +08:00
real-zony
24624a6d21 feat: Added Avalonia UI project. 2023-07-14 19:01:53 +08:00
real-zony
d860632327 chore: Removed the frontend web page code. 2023-07-14 19:01:25 +08:00
real-zony
1186369d16 ci: Released a new version.
Version: 4.0.0.55
2023-05-25 09:19:58 +08:00
real-zony
b240564cf7 perf: Optimized the startup time of the tool. 2023-05-25 09:05:39 +08:00
real-zony
6e2848a9de chore: Updated the version number. 2023-05-24 23:14:57 +08:00
real-zony
6361bf9265 feat: Improved the result information for lyrics download. 2023-05-24 23:08:17 +08:00
real-zony
383e2c5939 chore: Fix compilation warning messages. 2023-05-24 22:57:55 +08:00
real-zony
1e5c41852f perf: An error log file will only be created when there are songs that failed to download. 2023-05-24 22:00:22 +08:00
real-zony
41cba02833 feat: NetEase Cloud Music playlist download supports passing in multiple IDs.
Issue: https://github.com/real-zony/ZonyLrcToolsX/issues/139
2023-05-24 21:54:13 +08:00
real-zony
98c935ed93 fix: Fixed the issue where incorrect artist names were obtained when downloading NetEase Cloud Music playlists. 2023-05-24 20:50:24 +08:00
real-zony
62d08df735 fix: Added processing logic for special characters to prevent file write failures. 2023-05-24 20:29:02 +08:00
real-zony
1f312c749d build: Upgraded the version of NuGet packages that are dependent on, as usual. 2023-05-24 19:16:40 +08:00
Zony
30e10f4c01
Update README.md 2023-05-22 20:30:17 +08:00
real-zony
35468cfb71 test: Added three new test cases. 2023-04-29 23:30:38 +08:00
real-zony
126741f344 ci: Released a new version. 2023-04-17 01:20:15 +08:00
Zony
22f5bc69ec
Merge pull request #135 from real-zony/routine-maintenance
Routine maintenance - 2023.04.17
2023-04-17 01:15:12 +08:00
real-zony
788ff38be2 fix: Fixed some unhandled exceptions. 2023-04-17 01:13:06 +08:00
real-zony
ab5f79bd50 fix: Fixed the issue where Netease Cloud Music did not throw the correct exception when a song could not be found. 2023-04-17 01:01:16 +08:00
real-zony
f935f07609 feat: Modified the lyrics interface for Netease Cloud Music to support higher concurrency. 2023-04-17 00:47:23 +08:00
real-zony
493f48cefe fix: Fixed the index exception that may be caused by the KuGou music provider. 2023-04-16 23:52:57 +08:00
Zony
4999e4d694
Update README.md 2023-03-30 09:51:09 +08:00
Zony
334528eca6
Merge pull request #134 from real-zony/docs
Docs
2023-03-28 22:42:00 +08:00
real-zony
e76ddb75eb docs: Updated documentation. 2023-03-28 22:36:36 +08:00
real-zony
d6d384590c ci: Adjusted artifact expiration time for Github Actions. 2023-03-28 22:17:28 +08:00
real-zony
949dac7ad1 ci: Updated NuGet package versions. 2023-03-27 23:14:21 +08:00
real-zony
2ffd75ed1d ci: Removed unnecessary pdb files. 2023-03-19 12:19:26 +08:00
Zony
82af3009d3
Merge pull request #132 from real-zony/issue-128
fix: Fixed the path issue #128.
2023-03-19 12:01:18 +08:00
real-zony
572e555f2a fix: Fixed the path issue #128.
We should avoid using the Directory.GetCurrentDirectory() method, as it
can result in retrieving incorrect paths.
2023-03-19 11:34:32 +08:00
real-zony
286731b1a6 ci: Adjusted the Github pipeline.
Prevent artifact publication when performing PR operations.
2023-03-19 11:13:30 +08:00
Zony
5850614663
Merge pull request #131 from real-zony/dependabot/npm_and_yarn/src/ui/webpack-5.76.1
build(deps): bump webpack from 5.74.0 to 5.76.1 in /src/ui
2023-03-15 20:47:34 +08:00
dependabot[bot]
c20a280a07
build(deps): bump webpack from 5.74.0 to 5.76.1 in /src/ui
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-04 03:36:16 +00:00
real-zony
8d5a45c9ef fix: fixed some issues. 2023-01-04 11:35:44 +08:00
real-zony
6b538f3e0a build: Updated the code for MusicDecrypto. 2023-01-03 21:39:03 +08:00
real-zony
d7e11af3ba build: Upgraded the version of the NuGet package dependency. 2023-01-03 21:36:52 +08:00
real-zony
f25ba90b9a fix: Do not trim the generated files. 2022-12-13 23:05:27 +08:00
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
real-zony
9ef700df46 fix: 修复当翻译歌词没有的时候,导致 NULL 引用异常。
Closes https://github.com/real-zony/ZonyLrcToolsX/issues/82
Closes https://github.com/real-zony/ZonyLrcToolsX/issues/83
2022-01-06 15:27:57 +08:00
real-zony
c6a275ce07 feat: 增加歌曲错误提示。 2022-01-05 22:41:14 +08:00
real-zony
1f505f2f72 build: 升级依赖的 McMaster 库。 2021-12-29 11:20:50 +08:00
real-zony
a63115f402 docs: 更新使用文档。
增加使用用例。
2021-12-29 11:20:26 +08:00
167 changed files with 3651 additions and 1878 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
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.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:
retention-days: 90
name: release-files
path: |
./TempFiles
./versions/release.md
outputs:
version: ${{ steps.date.outputs.date }}
release:
if: github.event_name == 'push'
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
/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

11
Directory.Build.props Normal file
View File

@ -0,0 +1,11 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<Version>4.0.2</Version>
<Authors>Zony(real-zony)</Authors>
<RepositoryUrl>https://github.com/real-zony/ZonyLrcToolsX</RepositoryUrl>
</PropertyGroup>
</Project>

45
Directory.Packages.props Normal file
View File

@ -0,0 +1,45 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="QRCoder" Version="1.6.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="Polly" Version="8.5.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageVersion Include="MusicDecrypto.Library" Version="2.4.1" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="AutoMapper" Version="13.0.1" />
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.6" />
<PackageVersion Include="SuperSocket.WebSocket" Version="2.0.0-beta.11" />
<PackageVersion Include="SuperSocket.WebSocket.Server" Version="2.0.0-beta.11" />
<!-- Testing Projects -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="xunit" Version="2.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<!-- Avalonia -->
<PackageVersion Include="Ude.NetStandard" Version="1.2.0" />
<PackageVersion Include="YamlDotNet" Version="16.2.0" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -1,72 +1,39 @@
简体中文 | [English](./en_US.md)
English | [简体中文](./zh_CN.md)
## 简介
# Disclaimer
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
- This tool is for personal learning and research purposes only. The executable binary files are for demonstration purposes only and the source code and its products must not be used for commercial purposes. Otherwise, I [here](https://github.com/real-zony) will not be responsible for any related legal issues.
- Any unit or individual that downloads and uses the software resulting in any accidents, negligence, contract breaches, defamation, copyright or intellectual property infringement, and their resulting losses (including but not limited to direct, indirect, incidental or derivative losses), I [here](https://github.com/real-zony) will not bear any legal responsibility.
- Users clearly agree to all the terms listed in this statement and fully assume the risks and consequences of using this tool. I [here](https://github.com/real-zony) will not bear any legal responsibility.
🚧 当前版本正在开发当中。
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
# Introduction
## 用法
ZonyLrcToolX 4 is a cross-platform lyrics download tool based on CEF. **QQ Group: 337656932**. Detailed video tutorials are available in the group files.
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell (部分用户可能显示的是 *命令提示符*),根据下述说明执行命令即可。
🚧 The current version is under development.
🚧 If you want to see working code, please switch to the dev branch.
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
# Download
### 命令
To get the latest version, please visit the **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** page for download.
#### 歌曲下载
## Arch Linux User Repository
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
This software has been packaged into [AUR](https://aur.archlinux.org/packages/zonylrctoolsx-bin). Arch Linux and its derivative distribution users can install it with the following command:
```shell
./ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
```bash
# yay or other AUR Helpers
yay -S zonylrctoolsx-bin
./ZonyLrcTools.Cli.exe download -h|--help
ZonyLrcTools.Cli --help
```
#### 加密格式转换
# Usage
Detailed usage instructions have been moved to a new documentation site. Please visit [https://docs.myzony.com](https://docs.myzony.com) for complete documentation.
目前软件支持 NCM、QCM(开发中...🚧) 格式的音乐文件转换,命令如下。
# Donation
[爱发电](https://afdian.net/a/zony-lrc-tools)
```shell
./ZonyLrcTools.Cli.exe util -t=Ncm D:\CloudMusic
```
# Star History
### 配置文件
程序的部分配置信息需要在 `appsettings.json` 进行更改,下面标注了各个配置的说明。
| 属性 | 说明 | 示例值 |
| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------- |
| ToolOption.SupportFileExtensions | 允许扫描的歌曲文件后缀名,以 `;` 号隔开多个后缀。 | `*.mp3;*.flac` |
| ToolOption.NetworkOptions.Enable | 是否启用 HTTP 网络代理服务true 表示启用false 表示禁用。 | false |
| ToolOption.NetworkOptions.ProxyIp | HTTP 网络代理服务的 IP`Enable` 为 false 时会忽略该属性值。 | 127.0.0.1 |
| ToolOption.NetworkOptions.ProxyPort | HTTP 网络代理服务的 端口,在 `Enable` 为 false 时会忽略该属性值。 | 8080 |
| TagInfoProviderOptions.FileNameRegularExpressions | 文件名 Tag 标签信息读取器使用,使用正则表达式匹配歌曲名和歌手,请使用命名分组编写正则表达式。 | (?'artist'.+)\\s-\\s(?'name'.+) |
| LyricDownloader.[n].Name | 指定歌词下载器的配置项标识,对应具体的歌词下载器。 | NetEase 或 QQ |
| LyricDownloader.[n].Priority | 指定歌词下载器的优先级,按升序排列,如果值设置为 `-1` 则代表禁用。 | `1` |
| BlockWordOptions.IsEnable | 是否启用屏蔽词词典。 | false |
| BlockWordOptions.BlockWordDictionaryFile | 屏蔽词词典的位置。 | `./BlockWords.json` |
### 屏蔽字典
屏蔽字典适用于网易云音乐歌词下载,针对某些单词,网易云音乐使用了 * 号进行屏蔽,这个时候可以使用屏蔽字典,设置歌曲名的关键词替换。例如有一首歌曲叫做 *Fucking ABC* ,这个时候网易云实际的名字是 *Fu****ing* ,用户只需要在屏蔽字典加入替换逻辑即可,例如:
```json
{
"Fuckking": "Fu****ing"
}
```
屏蔽字典默认路径为程序所在目录的 *BlockWords.json* 文件,用户可以在 *appsettings.json* 文件中配置其他路径。
## 捐赠
暂无
## 路线图
- [x] 支持跨平台的 CLI 工具。
- [ ] 基于 Web GUI 的操作站点。
- [ ] 支持插件系统(Lua 引擎)。
[![Star History Chart](https://api.star-history.com/svg?repos=real-zony/ZonyLrcToolsX&type=Timeline)](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)

View File

@ -7,13 +7,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C29FB05C-54B
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}"
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Tests", "tests\ZonyLrcTools.Tests\ZonyLrcTools.Tests.csproj", "{FFBD3200-568F-455B-8390-5E76C51D522C}"
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.Common", "src\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj", "{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -29,10 +27,10 @@ Global
{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.Build.0 = Release|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{312A9DEF-0888-49AB-A963-64C7A2A2AF05}.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -43,6 +41,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
{312A9DEF-0888-49AB-A963-64C7A2A2AF05} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
EndGlobalSection
EndGlobal

View File

@ -1,6 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QQ/@EntryIndexedValue">QQ</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=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>

39
docs/en_US.md Normal file
View File

@ -0,0 +1,39 @@
English | [简体中文](./zh_CN.md)
# Disclaimer
- This tool is for personal learning and research purposes only. The executable binary files are for demonstration purposes only and the source code and its products must not be used for commercial purposes. Otherwise, I [here](https://github.com/real-zony) will not be responsible for any related legal issues.
- Any unit or individual that downloads and uses the software resulting in any accidents, negligence, contract breaches, defamation, copyright or intellectual property infringement, and their resulting losses (including but not limited to direct, indirect, incidental or derivative losses), I [here](https://github.com/real-zony) will not bear any legal responsibility.
- Users clearly agree to all the terms listed in this statement and fully assume the risks and consequences of using this tool. I [here](https://github.com/real-zony) will not bear any legal responsibility.
# Introduction
ZonyLrcToolX 4 is a cross-platform lyrics download tool based on CEF. **QQ Group: 337656932**. Detailed video tutorials are available in the group files.
🚧 The current version is under development.
🚧 If you want to see working code, please switch to the dev branch.
# Download
To get the latest version, please visit the **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** page for download.
## Arch Linux User Repository
This software has been packaged into [AUR](https://aur.archlinux.org/packages/zonylrctoolsx-bin). Arch Linux and its derivative distribution users can install it with the following command:
```bash
# yay or other AUR Helpers
yay -S zonylrctoolsx-bin
ZonyLrcTools.Cli --help
```
# Usage
Detailed usage instructions have been moved to a new documentation site. Please visit [https://docs.myzony.com](https://docs.myzony.com) for complete documentation.
# Donation
[爱发电](https://afdian.net/a/zony-lrc-tools)
# 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)

40
docs/zh_CN.md Normal file
View File

@ -0,0 +1,40 @@
简体中文 | [English](./docs/en_US.md)
# 免责声明
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等)[本人](https://github.com/real-zony) 不承担任何法律责任。
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
# 简介
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具,**QQ 群:337656932**,群文件里面有详细的视频教程。
🚧 当前版本正在开发当中。
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
# 下载
如果你要获取最新版本,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
## Arch Linux 用户仓库
本软件已打包到 [AUR](https://aur.archlinux.org/packages/zonylrctoolsx-bin)。Arch Linux 及其衍生发行版用户可用如下命令安装:
```bash
# yay 或其他 AUR Helper
yay -S zonylrctoolsx-bin
ZonyLrcTools.Cli --help
```
# 用法
软件的具体使用方法已经迁移到了全新的文档站点,请跳转到 [https://docs.myzony.com](https://docs.myzony.com),里面包含完整的说明文档。
# 捐赠
[爱发电](https://afdian.net/a/zony-lrc-tools)
# 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)

View File

@ -1,18 +0,0 @@
English | [简体中文](./zh_CN.md)
## Overview
ZonyLrcToolX 2.0 is a cross-platform lyric downlaod tool based on CEF.
🚧 The current version is under development.
🚧 If you want to see the working code, please switch to the 1.0 branch.
## Usage
## Donation
## Roadmap
- [ ] Supports cross-platform CLI tools.
- [ ] Web GUI based site (local).
- [ ] Support plug-in system (Lua Engine).

View File

@ -1,229 +0,0 @@
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);
}
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,137 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
using ZonyLrcTools.Common;
using ZonyLrcTools.Common.Album;
using ZonyLrcTools.Common.Lyrics;
using ZonyLrcTools.Common.MusicScanner;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable MemberCanBePrivate.Global
namespace ZonyLrcTools.Cli.Commands.SubCommand
{
[Command("download", Description = "下载歌词文件或专辑图像。")]
public class DownloadCommand : ToolCommandBase
{
private readonly ILyricsDownloader _lyricsDownloader;
private readonly IAlbumDownloader _albumDownloader;
private readonly IMusicInfoLoader _musicInfoLoader;
private readonly IServiceProvider _serviceProvider;
public DownloadCommand(ILyricsDownloader lyricsDownloader,
IMusicInfoLoader musicInfoLoader,
IAlbumDownloader albumDownloader,
IServiceProvider serviceProvider)
{
_lyricsDownloader = lyricsDownloader;
_musicInfoLoader = musicInfoLoader;
_albumDownloader = albumDownloader;
_serviceProvider = serviceProvider;
}
#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 = "指定下载时候的线程数量。(默认值 1)")]
public int ParallelNumber { get; set; } = 1;
#endregion
#region > Scanner Options <
[Option("-sc|--scanner", CommandOptionType.SingleValue, Description = "指定歌词文件扫描器,目前支持本地文件(local),网易云音乐(netease)csv 文件(csv),默认值为 local。")]
public string Scanner { get; set; } = "local";
[Option("-o|--output", Description = "指定歌词文件的输出路径。")]
public string OutputDirectory { get; set; } = "DownloadedLrc";
[Option("-p|--pattern", Description = "指定歌词文件的输出文件名模式。")]
public string OutputFileNamePattern { get; set; } = "{Artist} - {Name}.lrc";
[Option("-f|--file", Description = "指定 CSV 文件的路径。")]
public string CsvFilePath { get; set; }
[Option("-s|--song-list-id", Description = "指定网易云音乐歌单的 ID如果有多个歌单请使用 ';' 分割 ID。")]
public string SongListId { get; set; }
#endregion
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (!DownloadAlbum && !DownloadLyric)
{
throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。");
}
if (DownloadLyric)
{
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
}
if (DownloadAlbum)
{
await _albumDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
}
return 0;
}
/// <summary>
/// Get the music infos by the scanner.
/// </summary>
private async Task<List<MusicInfo>> GetMusicInfosAsync(string scanner)
{
ValidateScannerOptions(scanner);
return scanner switch
{
MusicScannerConsts.LocalScanner => await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber),
MusicScannerConsts.CsvScanner => await _serviceProvider.GetService<CsvFileMusicScanner>()
.GetMusicInfoFromCsvFileAsync(CsvFilePath, OutputDirectory, OutputFileNamePattern),
MusicScannerConsts.NeteaseScanner => await _serviceProvider.GetService<NetEaseMusicSongListMusicScanner>()
.GetMusicInfoFromNetEaseMusicSongListAsync(SongListId, OutputDirectory, OutputFileNamePattern),
_ => await _musicInfoLoader.LoadAsync(SongsDirectory, ParallelNumber)
};
}
/// <summary>
/// Manually validate the options.
/// </summary>
/// <param name="scanner">Scanner Name.</param>
/// <exception cref="ArgumentException">If the options are invalid.</exception>
private void ValidateScannerOptions(string scanner)
{
if (scanner != MusicScannerConsts.LocalScanner && string.IsNullOrEmpty(OutputDirectory))
{
throw new ArgumentException("当使用非本地文件扫描器时,必须指定歌词文件的输出路径。");
}
if (scanner != MusicScannerConsts.LocalScanner && !Directory.Exists(OutputDirectory))
{
throw new ArgumentException("指定的歌词文件输出路径不存在。");
}
switch (scanner)
{
case MusicScannerConsts.CsvScanner when string.IsNullOrWhiteSpace(CsvFilePath):
throw new ArgumentException("当使用 CSV 文件扫描器时,必须指定 CSV 文件的路径。");
case MusicScannerConsts.NeteaseScanner when string.IsNullOrWhiteSpace(SongListId):
throw new ArgumentException("当使用网易云音乐扫描器时,必须指定歌单的 ID。");
}
}
}
}

View File

@ -0,0 +1,15 @@
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace ZonyLrcTools.Cli.Commands.SubCommand;
[Command("manual", Description = "手动指定歌曲信息,然后下载对应的歌词数据。")]
public class ManualDownloadCommand : ToolCommandBase
{
private readonly ILogger<ManualDownloadCommand> _logger;
public ManualDownloadCommand(ILogger<ManualDownloadCommand> logger)
{
_logger = logger;
}
}

View File

@ -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,14 +1,23 @@
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
namespace ZonyLrcTools.Cli.Commands
{
[HelpOption("--help|-h", Description = "欢迎使用 ZonyLrcToolsX Command Line Interface。")]
[HelpOption("--help|-h",
Description = "欢迎使用 ZonyLrcToolsX Command Line Interface有任何问题请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。",
ShowInHelpText = true)]
public abstract class ToolCommandBase
{
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (!Environment.UserInteractive)
{
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
Console.ReadKey();
}
app.ShowHelp();
return Task.FromResult(0);
}
}

View File

@ -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,68 +0,0 @@
using System;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou
{
public class KuGourLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.KuGou;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
private const string KuGouSearchMusicUrl = @"https://songsearch.kugou.com/song_search_v2";
private const string KuGouGetLyricAccessKeyUrl = @"http://lyrics.kugou.com/search";
private const string KuGouGetLyricUrl = @"http://lyrics.kugou.com/download";
public KuGourLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist));
ValidateSongSearchResponse(searchResult, args);
// 获得特殊的 AccessToken 与 Id真正请求歌词数据。
var accessKeyResponse = await _warpHttpClient.GetAsync<GetLyricAccessKeyResponse>(KuGouGetLyricAccessKeyUrl,
new GetLyricAccessKeyRequest(searchResult.Data.List[0].FileHash));
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
var lyricResponse = await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
new GetLyricRequest(accessKeyObject.Id, accessKeyObject.AccessKey));
return Encoding.UTF8.GetBytes(lyricResponse);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args)
{
await ValueTask.CompletedTask;
var lyricJsonObj = JObject.Parse(Encoding.UTF8.GetString(data));
if (lyricJsonObj.SelectToken("$.status").Value<int>() != 200)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
var lyricText = Encoding.UTF8.GetString(Convert.FromBase64String(lyricJsonObj.SelectToken("$.content").Value<string>()));
return _lyricItemCollectionFactory.Build(lyricText);
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
{
if (response.ErrorCode != 0 && response.Status != 1 || response.Data.List == null)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}
}

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,15 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
{
public class LyricDownloaderOption
{
/// <summary>
/// 歌词下载器的唯一标识。
/// </summary>
public string Name { get; set; }
/// <summary>
/// 歌词下载时的优先级,当值为 -1 时是禁用。
/// </summary>
public int Priority { get; set; }
}
}

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,85 +0,0 @@
using System;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase
{
public class NetEaseLyricDownloader : LyricDownloader
{
public override string DownloaderName => InternalLyricDownloaderNames.NetEase;
private readonly IWarpHttpClient _warpHttpClient;
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
private const string NetEaseRequestReferer = @"https://music.163.com";
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
public NetEaseLyricDownloader(IWarpHttpClient warpHttpClient,
ILyricItemCollectionFactory lyricItemCollectionFactory)
{
_warpHttpClient = warpHttpClient;
_lyricItemCollectionFactory = lyricItemCollectionFactory;
}
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
{
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
NetEaseSearchMusicUrl,
new SongSearchRequest(args.SongName, args.Artist),
true,
msg =>
{
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
if (msg.Content != null)
{
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
}
});
ValidateSongSearchResponse(searchResult, args);
var lyricResponse = await _warpHttpClient.GetAsync(
NetEaseGetLyricUrl,
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName)),
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
return Encoding.UTF8.GetBytes(lyricResponse);
}
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args)
{
await ValueTask.CompletedTask;
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
if (json?.OriginalLyric == null)
{
return new LyricItemCollection(LyricItemCollectionOption.NullInstance);
}
return _lyricItemCollectionFactory.Build(
json.OriginalLyric.Text,
json.TranslationLyric.Text);
}
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
{
if (response?.StatusCode != SongSearchResponseStatusCode.Success)
{
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
}
if (response.Items?.SongCount <= 0)
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
}
}
}
}

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,32 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel
{
public class SongSearchResponse
{
[JsonProperty("code")] public int StatusCode { get; set; }
[JsonProperty("data")] public QQMusicInnerDataModel Data { get; set; }
}
public class QQMusicInnerDataModel
{
[JsonProperty("song")] public QQMusicInnerSongModel Song { get; set; }
}
public class QQMusicInnerSongModel
{
[JsonProperty("list")] public List<QQMusicInnerSongItem> SongItems { get; set; }
}
public class QQMusicInnerSongItem
{
[JsonProperty("mid")] public string SongId { get; set; }
}
public class AlbumInfo
{
[JsonProperty("id")] public long Id { get; set; }
}
}

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,36 +0,0 @@
namespace ZonyLrcTools.Cli.Infrastructure
{
/// <summary>
/// 歌曲信息的承载类,携带歌曲的相关数据。
/// </summary>
public class MusicInfo
{
/// <summary>
/// 歌曲对应的物理文件路径。
/// </summary>
public string FilePath { get; }
/// <summary>
/// 歌曲的名称。
/// </summary>
public string Name { get; set; }
/// <summary>
/// 歌曲的作者。
/// </summary>
public string Artist { get; set; }
/// <summary>
/// 构建一个新的 <see cref="MusicInfo"/> 对象。
/// </summary>
/// <param name="filePath">歌曲对应的物理文件路径。</param>
/// <param name="name">歌曲的名称。</param>
/// <param name="artist">歌曲的作者。</param>
public MusicInfo(string filePath, string name, string artist)
{
FilePath = filePath;
Name = name;
Artist = artist;
}
}
}

View File

@ -0,0 +1,10 @@
// ReSharper disable IdentifierTypo
namespace ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
public class MusicScannerConsts
{
public const string LocalScanner = "local";
public const string NeteaseScanner = "netease";
public const string CsvScanner = "csv";
}

View File

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

@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.Infrastructure.Tag
{
public class TagInfoProviderInstance
{
public string Name { get; set; }
public int Priority { get; set; }
public Dictionary<string, string> Extensions { get; set; }
}
}

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,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Configuration;
@ -8,23 +7,33 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
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
// ReSharper disable ClassNeverInstantiated.Global
namespace ZonyLrcTools.Cli
{
[Command("lyric-tool")]
[Subcommand(typeof(DownloadCommand),
typeof(UtilityCommand))]
public class ToolCommand : ToolCommandBase
public class Program : ToolCommandBase
{
public static async Task<int> Main(string[] args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ConfigureLogger();
ConfigureErrorMessage();
try
{
Log.Logger.Information("Starting...");
return await BuildHostedService(args);
}
catch (Exception ex)
@ -52,15 +61,15 @@ namespace ZonyLrcTools.Cli.Commands
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.Console())
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
.WriteTo.Logger(lc =>
{
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
lc.Filter.ByIncludingOnly(warningLog => warningLog.Level == LogEventLevel.Warning)
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
})
.WriteTo.Logger(lc =>
{
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error)
lc.Filter.ByIncludingOnly(errLog => errLog.Level == LogEventLevel.Error)
.WriteTo.Async(c => c.File("Logs/errors.txt"));
})
.CreateLogger();
@ -69,21 +78,22 @@ namespace ZonyLrcTools.Cli.Commands
private static Task<int> BuildHostedService(string[] args)
{
return new HostBuilder()
.ConfigureLogging(builder => builder.AddSerilog())
.ConfigureLogging(l => l.AddSerilog())
.ConfigureHostConfiguration(builder =>
{
builder
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
builder.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddYamlFile("config.yaml");
})
.ConfigureServices((_, services) =>
{
services.AddSingleton(PhysicalConsole.Singleton);
services.BeginAutoDependencyInject<ToolCommand>();
services.BeginAutoDependencyInject<Program>();
services.BeginAutoDependencyInject<IWarpHttpClient>();
services.ConfigureConfiguration();
services.ConfigureToolService();
services.AddHostedService<UpdaterHostedService>();
})
.RunCommandLineApplicationAsync<ToolCommand>(args);
.RunCommandLineApplicationAsync<Program>(args);
}
private static int HandleException(Exception ex)
@ -91,7 +101,8 @@ namespace ZonyLrcTools.Cli.Commands
switch (ex)
{
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);
return exception.ErrorCode;
case { } unknownException:

View File

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

View File

@ -1,39 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Refit" Version="6.1.15" />
<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.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.2.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils"/>
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine"/>
<PackageReference Include="Microsoft.Extensions.Hosting"/>
<PackageReference Include="Serilog.Extensions.Hosting"/>
<PackageReference Include="Serilog.Sinks.Async"/>
<PackageReference Include="Serilog.Sinks.Console"/>
<PackageReference Include="Serilog.Sinks.File"/>
<PackageReference Include="System.Text.Encoding.CodePages"/>
<PackageReference Include="Ude.NetStandard"/>
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="Resources\error_msg.json" />
<Content Include="Resources\error_msg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="BlockWords.json" />
<Content Include="BlockWords.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="appsettings.json"/>
<None Remove="Resources\error_msg.json"/>
<Content Include="Resources\error_msg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="BlockWords.json"/>
<Content Include="BlockWords.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="config.yaml"/>
<Content Include="config.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj"/>
</ItemGroup>
</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,61 @@
# 允许扫描的歌曲文件后缀名。
supportFileExtensions:
- '*.mp3'
- '*.flac'
- '*.wav'
- '*.m4a'
- '*.ogg'
- '*.opus'
# 网络代理服务设置,仅支持 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 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
additional:
isEnableRomanOutput: false # 是否启用罗马音输出,本参数仅当对应歌曲有罗马音歌词时有效。
- 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

@ -0,0 +1,20 @@
$Platforms = @('win-x64', 'linux-x64', 'osx-x64', 'win-arm64', 'linux-arm64', 'osx-arm64')
if (-not (Test-Path ./TempFiles)) {
New-Item -ItemType Directory -Path ./TempFiles | Out-Null
}
Remove-Item ./TempFiles/* -Recurse -Force
foreach ($platform in $Platforms) {
dotnet publish -r $platform -c Release -p:PublishSingleFile=true --self-contained true | Out-Null
if ($LASTEXITCODE -ne 0) {
exit 1
}
Set-Location ./bin/Release/net7.0/$platform/publish/
Compress-Archive -Path ./* -DestinationPath ./ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip | Out-Null
Set-Location ../../../../../
Move-Item ./bin/Release/net7.0/$platform/publish/ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip ./TempFiles
}

View File

@ -1,6 +1,5 @@
#!/bin/bash
read -r -p "请输入版本号:" Version
Platforms=('win-x64' 'linux-x64' 'osx-x64')
Platforms=('win-x64' 'linux-x64' 'osx-x64' 'win-arm64' 'linux-arm64' 'osx-arm64')
if ! [ -d './TempFiles' ];
then
@ -11,11 +10,11 @@ rm -rf ./TempFiles/*
for platform in "${Platforms[@]}"
do
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
cd ./bin/Release/net5.0/"$platform"/publish/
zip -r ./ZonyLrcTools_"$platform"_"$Version".zip ./
cd ../../../../../
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:DebugType=none --self-contained true || exit 1
mv ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./TempFiles
cd ./bin/Release/net8.0/"$platform"/publish/ || exit 1
zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1
cd ../../../../../ || exit 1
mv ./bin/Release/net8.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
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.Cli.Infrastructure.Album
namespace ZonyLrcTools.Common.Album
{
/// <summary>
/// 专辑封面下载器,用于匹配并下载歌曲的专辑封面。
/// </summary>
public interface IAlbumDownloader
public interface IAlbumProvider
{
/// <summary>
/// 下载器的名称。

View File

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

View File

@ -1,18 +1,15 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
using ZonyLrcTools.Common.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 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 DefaultReferer = @"https://music.163.com";
public NetEaseAlbumDownloader(IWarpHttpClient warpHttpClient)
public NetEaseAlbumProvider(IWarpHttpClient warpHttpClient)
{
_warpHttpClient = warpHttpClient;
_defaultOption = message =>
@ -44,14 +41,14 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.NetEase
true,
_defaultOption);
if (searchResult is not {StatusCode: 200} || searchResult.Items?.SongCount <= 0)
if (searchResult is not { StatusCode: 200 } || searchResult.Items is not { SongCount: > 0 })
{
throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
}
var songDetailJsonStr = await _warpHttpClient.GetAsync(
GetMusicInfoApi,
new GetSongDetailsRequest(searchResult.GetFirstMatchSongId(songName)),
new GetSongDetailsRequest(searchResult.GetFirstMatchSongId(songName, null)),
_defaultOption);
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.Threading.Tasks;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel;
using ZonyLrcTools.Cli.Infrastructure.Network;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Common.Lyrics.Providers.QQMusic.JsonModel;
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;
@ -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 DefaultReferer = "https://y.qq.com";
public QQMusicAlbumDownloader(IWarpHttpClient warpHttpClient)
public QQMusicAlbumProvider(IWarpHttpClient warpHttpClient)
{
_warpHttpClient = warpHttpClient;
}
@ -39,7 +36,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic
SearchApi,
requestParameter, _defaultOption);
return new byte[] {0x1, 0x2};
return new byte[] { 0x1, 0x2 };
}
}
}

View File

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

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; } = null!;
/// <summary>
/// 网络代理相关的配置信息。
/// </summary>
public NetworkOptions NetworkOptions { get; set; } = null!;
/// <summary>
/// 定义下载器的相关配置信息。
/// </summary>
public ProviderOptions Provider { get; set; } = null!;
}
}

View File

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

View File

@ -0,0 +1,25 @@
namespace ZonyLrcTools.Common.Configuration
{
public class LyricsProviderOptions
{
/// <summary>
/// 歌词下载器的唯一标识。
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// 歌词下载时的优先级,当值为 -1 时是禁用。
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 搜索深度,值越大搜索结果越多,但搜索时间越长。
/// </summary>
public int Depth { get; set; }
/// <summary>
/// 歌词下载器的扩展属性。
/// </summary>
public Dictionary<string, string>? Additional { get; set; }
}
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
namespace ZonyLrcTools.Common.Configuration;
public class TagInfoOptions
{
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; } = null!;
/// <summary>
/// 屏蔽词功能相关配置。
/// </summary>
public BlockWordOptions BlockWord { get; set; } = null!;
public TagInfoProviderOptions GetTagProviderOption(string name)
{
return Plugin.FirstOrDefault(x => x.Name == name)!;
}
}

View File

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

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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection;
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
{
@ -48,7 +45,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
public static List<Type> GetDefaultExposedTypes(Type type)
{
var serviceTypes = new List<Type>();
var serviceTypes = new List<Type> { type };
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
{
@ -62,7 +59,6 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
if (type.Name.EndsWith(interfaceName))
{
serviceTypes.Add(interfaceType);
serviceTypes.Add(type);
}
}

View File

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

View File

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

View File

@ -1,13 +1,11 @@
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Cli.Config;
using ZonyLrcTools.Cli.Infrastructure.Network;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Network;
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{
/// <summary>
/// Service 注入的扩展方法。
@ -17,7 +15,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
/// <summary>
/// 配置工具会用到的服务。
/// </summary>
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
public static IServiceCollection? ConfigureToolService(this IServiceCollection? services)
{
if (services == null)
{
@ -27,12 +25,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant)
.ConfigurePrimaryHttpMessageHandler(provider =>
{
var option = provider.GetRequiredService<IOptions<ToolOptions>>().Value;
var option = provider.GetRequiredService<IOptions<GlobalOptions>>().Value;
return new HttpClientHandler
{
UseProxy = option.NetworkOptions.Enable,
Proxy = new WebProxy($"{option.NetworkOptions.ProxyIp}:{option.NetworkOptions.ProxyPort}")
UseProxy = option.NetworkOptions.IsEnable,
Proxy = new WebProxy($"{option.NetworkOptions.Ip}:{option.NetworkOptions.Port}")
};
});
@ -45,11 +43,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddYamlFile("config.yaml")
.Build();
services.Configure<ToolOptions>(configuration.GetSection("ToolOption"));
services.Configure<GlobalOptions>(configuration);
return services;
}

View File

@ -0,0 +1,109 @@
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
namespace ZonyLrcTools.Common.Infrastructure.Encryption;
/// <summary>
/// 提供网易云音乐 API 的相关加密方法。
/// </summary>
/// <remarks>
/// 加密方法参考以下开源项目:
/// 1. https://github.com/jitwxs/163MusicLyrics/blob/master/MusicLyricApp/Api/Music/NetEaseMusicNativeApi.cs
/// 2. https://github.com/mos9527/pyncm/blob/ad0a84b2ed5f1affa9890d5f54f6170c2cf99bbb/pyncm/utils/crypto.py#L53
/// </remarks>
public static class NetEaseMusicEncryptionHelper
{
public const string Modulus =
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
public const string Nonce = "0CoJUm6Qyw8W8jud";
public const string PubKey = "010001";
public const string Vi = "0102030405060708";
public static readonly byte[] ID_XOR_KEY_1 = "3go8&$8*3*3h0k(2)2"u8.ToArray();
public static string RsaEncode(string text)
{
var srText = new string(text.Reverse().ToArray());
var a = BCHexDec(BitConverter.ToString(Encoding.Default.GetBytes(srText)).Replace("-", string.Empty));
var b = BCHexDec(PubKey);
var c = BCHexDec(Modulus);
var key = BigInteger.ModPow(a, b, c).ToString("x");
key = key.PadLeft(256, '0');
return key.Length > 256 ? key.Substring(key.Length - 256, 256) : key;
}
public static BigInteger BCHexDec(string hex)
{
var dec = new BigInteger(0);
var len = hex.Length;
for (var i = 0; i < len; i++)
{
dec += BigInteger.Multiply(new BigInteger(Convert.ToInt32(hex[i].ToString(), 16)),
BigInteger.Pow(new BigInteger(16), len - i - 1));
}
return dec;
}
public static string AesEncode(string secretData, string secret = "TA3YiYCfY2dDJQgg")
{
byte[] encrypted;
var iv = Encoding.UTF8.GetBytes(Vi);
using (var aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(secret);
aes.IV = iv;
aes.Mode = CipherMode.CBC;
using (var encryptor = aes.CreateEncryptor())
{
using (var stream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(stream, encryptor, CryptoStreamMode.Write))
{
using (var sw = new StreamWriter(cryptoStream))
{
sw.Write(secretData);
}
encrypted = stream.ToArray();
}
}
}
}
return Convert.ToBase64String(encrypted);
}
public static string CreateSecretKey(int length)
{
const string str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var sb = new StringBuilder(length);
var rnd = new Random();
for (var i = 0; i < length; ++i)
{
sb.Append(str[rnd.Next(0, str.Length)]);
}
return sb.ToString();
}
public static string CloudMusicDllEncode(string deviceId)
{
var xored = new byte[deviceId.Length];
for (var i = 0; i < deviceId.Length; i++)
{
xored[i] = (byte)(deviceId[i] ^ ID_XOR_KEY_1[i % ID_XOR_KEY_1.Length]);
}
using (var md5 = MD5.Create())
{
var digest = md5.ComputeHash(xored);
return Convert.ToBase64String(digest);
}
}
}

View File

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

View File

@ -1,10 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
/// <summary>
/// 错误码相关的帮助类。
@ -29,14 +26,14 @@ namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
return;
}
var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "error_msg.json");
var jsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "error_msg.json");
using var jsonReader = new JsonTextReader(File.OpenText(jsonPath));
var jsonObj = JObject.Load(jsonReader);
var errors = jsonObj.SelectTokens("$.Error.*");
var warnings = jsonObj.SelectTokens("$.Warning.*");
errors.Union(warnings).Select(m => m.Parent).OfType<JProperty>().ToList()
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>()));
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>() ?? string.Empty));
}
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];

View File

@ -1,4 +1,4 @@
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
/// <summary>
/// 错误码。
@ -27,6 +27,16 @@ namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
/// </summary>
public const int NoFilesWereScanned = 10004;
/// <summary>
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
/// </summary>
public const int NotSupportedFileEncoding = 10005;
/// <summary>
/// 文本: 无法从网易云音乐获取歌曲列表。
/// </summary>
public const int UnableGetSongListFromNetEaseCloudMusic = 10006;
#endregion
#region > <

View File

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

View File

@ -1,15 +1,15 @@
using System;
using System.Text;
using Microsoft.Extensions.Logging;
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>
public static class LoggerExtensions
public static class LoggerHelper
{
/// <summary>
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
@ -17,9 +17,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
/// <param name="logger">日志记录器实例。</param>
/// <param name="errorCode">错误码,具体请参考 <see cref="ErrorCodes"/> 类的定义。</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>
@ -27,7 +27,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
/// </summary>
/// <param name="logger">日志记录器的实例。</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)
{
@ -37,17 +37,17 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
var sb = new StringBuilder();
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
logger.LogWarning(sb.ToString());
logger.WarnAsync(sb.ToString()).GetAwaiter().GetResult();
}
/// <summary>
/// 使用 <see cref="LogLevel.Information"/> 级别打印歌曲信息。
/// 使用 <see cref="LogLevel.Information"/> 级别打印歌曲下载成功信息。
/// </summary>
/// <param name="logger">日志记录器的实例。</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.Cli.Infrastructure.Extensions
namespace ZonyLrcTools.Common.Infrastructure.Extensions
{
/// <summary>
/// 字符串处理相关的工具方法。

View File

@ -1,24 +1,20 @@
using System;
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.Abstractions;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Cli.Infrastructure.Extensions;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
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 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)
@ -63,7 +59,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.IO
}
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.Cli.Infrastructure.IO
namespace ZonyLrcTools.Common.Infrastructure.IO
{
/// <summary>
/// 文件扫描结果对象。

View File

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

View File

@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.IO
namespace ZonyLrcTools.Common.Infrastructure.IO
{
/// <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.Http;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Network
namespace ZonyLrcTools.Common.Infrastructure.Network
{
public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency
{
@ -22,9 +19,29 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
}
public async ValueTask<string> PostAsync(string url,
object parameters = null,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
Action<HttpRequestMessage>? requestOption = null)
{
using var responseMessage = await PostReturnHttpResponseAsync(url, parameters, isQueryStringParam, requestOption);
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
}
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage>? requestOption = null)
{
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
}
public async ValueTask<HttpResponseMessage> PostReturnHttpResponseAsync(string url,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage>? requestOption = null)
{
var parametersStr = isQueryStringParam ? BuildQueryString(parameters) : BuildJsonBodyString(parameters);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
@ -32,24 +49,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
requestOption?.Invoke(requestMessage);
using var responseMessage = await BuildHttpClient().SendAsync(requestMessage);
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
}
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
{
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
return await BuildHttpClient().SendAsync(requestMessage);
}
public async ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null)
{
var requestParamsStr = BuildQueryString(parameters);
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
@ -62,8 +67,8 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
}
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null)
{
var responseString = await GetAsync(url, parameters, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
@ -74,7 +79,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
return _httpClientFactory.CreateClient(HttpClientNameConstant);
}
private string BuildQueryString(object parameters)
private string BuildQueryString(object? parameters)
{
if (parameters == null)
{
@ -84,7 +89,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
var type = parameters.GetType();
if (type == typeof(string))
{
return parameters as string;
return parameters as string ?? string.Empty;
}
var properties = type.GetProperties();
@ -101,7 +106,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
return paramBuilder.ToString().TrimEnd('&');
}
private string BuildJsonBodyString(object parameters)
private string BuildJsonBodyString(object? parameters)
{
if (parameters == null) return string.Empty;
if (parameters is string result) return result;
@ -117,13 +122,13 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <returns>如果响应正常,则返回具体的响应内容。</returns>
/// <exception cref="ErrorCodeException">如果 Http 响应不正常,则可能抛出本异常。</exception>
private string ValidateHttpResponse(HttpResponseMessage responseMessage, object requestParameters, string responseString)
private string ValidateHttpResponse(HttpResponseMessage responseMessage, object? requestParameters, string responseString)
{
return responseMessage.StatusCode switch
{
HttpStatusCode.OK => responseString,
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 })
};
}
@ -134,9 +139,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception>
private TResponse ConvertHttpResponseToObject<TResponse>(object requestParameters, string responseString)
private TResponse ConvertHttpResponseToObject<TResponse>(object? requestParameters, string responseString)
{
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {requestParameters, responseString});
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new { requestParameters, responseString });
try
{

View File

@ -1,8 +1,4 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace ZonyLrcTools.Cli.Infrastructure.Network
namespace ZonyLrcTools.Common.Infrastructure.Network
{
/// <summary>
/// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。
@ -18,9 +14,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> PostAsync(string url,
object parameters = null,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
Action<HttpRequestMessage>? requestOption = null);
/// <summary>
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
@ -32,9 +28,14 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
Action<HttpRequestMessage>? requestOption = null);
ValueTask<HttpResponseMessage> PostReturnHttpResponseAsync(string url,
object? parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage>? requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
@ -44,8 +45,8 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
@ -57,7 +58,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> GetAsync<TResponse>(
string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
object? parameters = null,
Action<HttpRequestMessage>? requestOption = null);
}
}

View File

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

View File

@ -0,0 +1,24 @@
namespace ZonyLrcTools.Common.Lyrics;
/// <summary>
/// 歌词下载核心逻辑的接口定义。
/// </summary>
public interface ILyricsDownloader
{
/// <summary>
/// 使用给定的歌词信息下载歌词,并输出文件到指定的路径。
/// </summary>
/// <param name="needDownloadMusicInfos">需要下载的歌词信息。</param>
/// <param name="parallelCount">下载线程/并发量。</param>
/// <param name="callback">任务完成之后的回调方法。</param>
/// <param name="cancellationToken">任务取消标记。</param>
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
int parallelCount = 1,
Func<MusicInfo, Task>? callback = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取目前可用的歌词下载器。
/// </summary>
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.Cli.Infrastructure.Lyric
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。
/// </summary>
public interface ILyricDownloader
public interface ILyricsProvider
{
/// <summary>
/// 下载歌词数据。
/// </summary>
/// <param name="songName">歌曲的名称。</param>
/// <param name="artist">歌曲的作者。</param>
/// <param name="duration">歌曲的时长。</param>
/// <returns>歌曲的歌词数据对象。</returns>
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist);
ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
/// <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>
public static class InternalLyricDownloaderNames
public static class InternalLyricsProviderNames
{
/// <summary>
/// 网易云音乐歌词下载器。
@ -19,5 +19,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// 酷狗音乐歌词下载器。
/// </summary>
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>
/// 换行符格式定义。

View File

@ -0,0 +1,161 @@
using System.Collections.Immutable;
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;
/// <inheritdoc cref="ZonyLrcTools.Common.Lyrics.ILyricsDownloader" />
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 = 1,
Func<MusicInfo, Task>? callback = null,
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) continue;
_logger.LogSuccessful(info);
break;
}
if (callback != null) await callback(info);
}, cancellationToken), cancellationToken));
await Task.WhenAll(downloadTasks);
var successfulCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: false });
var skippedCount = needDownloadMusicInfos.Count(m => m is { IsSuccessful: true, IsPruneMusic: true });
var failedCount = needDownloadMusicInfos.Count(m => m.IsSuccessful == false);
await _logger.InfoAsync($"歌词数据下载完成,成功: {successfulCount} 跳过(纯音乐): {skippedCount} 失败{failedCount}。");
await LogFailedSongFilesInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"歌词下载失败列表_{DateTime.Now:yyyyMMddHHmmss}.txt"), needDownloadMusicInfos);
}
private async Task DownloadAndWriteLyricsAsync(ILyricsProvider provider, MusicInfo info)
{
try
{
var lyrics = await provider.DownloadAsync(info.Name, info.Artist);
if (lyrics.IsPruneMusic)
{
info.IsSuccessful = true;
info.IsPruneMusic = 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 the encoding is UTF-8 BOM, add the BOM to the front of the lyrics.
if (_options.Provider.Lyric.Config.FileEncoding == "utf-8-bom")
{
return Encoding.UTF8.GetPreamble().Concat(lyrics.GetUtf8Bytes()).ToArray();
}
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
{
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
}
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyrics.GetUtf8Bytes());
}
private async Task LogFailedSongFilesInfo(string outFilePath, IEnumerable<MusicInfo> musicInfos)
{
var failedSongList = musicInfos.Where(m => m.IsSuccessful == false).ToImmutableList();
if (!failedSongList.Any())
{
return;
}
var failedSongFiles = new StringBuilder();
failedSongFiles.AppendLine("歌曲名,歌手,路径");
foreach (var failedSongFile in failedSongList)
{
failedSongFiles.AppendLine($"{failedSongFile.Name},{failedSongFile.Artist},{failedSongFile.FilePath}");
}
await File.WriteAllTextAsync(outFilePath, failedSongFiles.ToString());
}
}

View File

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

View File

@ -1,39 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
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>
/// 歌词数据,包含多条歌词行对象(<see cref="LyricItem"/>)。
/// 歌词数据,包含多条歌词行对象(<see cref="LyricsItem"/>)。
/// </summary>
public class LyricItemCollection : List<LyricItem>
public class LyricsItemCollection : List<LyricsItem>
{
/// <summary>
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
/// </summary>
public bool IsPruneMusic => Count == 0;
public LyricItemCollectionOption Option { get; private set; }
public GlobalLyricsConfigOptions? Options { get; }
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)
{
return left;
}
var option = left.Option;
var newCollection = new LyricItemCollection(option);
var indexDiff = left.Count - right.Count;
var option = left.Options;
if (option == null)
{
throw new NullReferenceException("LyricsItemCollection.Options");
}
var newCollection = new LyricsItemCollection(option);
var indexDiff = left.Count - right.Count;
if (!option.IsOneLine)
{
left.ForEach(item => newCollection.Add(item));
@ -74,10 +76,10 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
// 遍历未处理的歌词项,将其添加到返回集合当中。
var leftWaitProcessIndex = leftMarkDict
.Where(item => item.Value)
.Where(item => !item.Value)
.Select(pair => pair.Key);
var rightWaitProcessIndex = rightMarkDict
.Where(item => item.Value)
.Where(item => !item.Value)
.Select(pair => pair.Key);
leftWaitProcessIndex.Foreach(index => newCollection.Add(left[index]));
@ -94,18 +96,28 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
/// </remarks>
/// <param name="items">等待构建的歌词集合实例。</param>
private static Dictionary<int, bool> BuildMarkDictionary(LyricItemCollection items)
private static Dictionary<int, bool> BuildMarkDictionary(LyricsItemCollection items)
{
return items
.Select((item, index) => new {index, item})
.Select((item, index) => new { index, item })
.ToDictionary(item => item.index, item => false);
}
public override string ToString()
{
if (Options == null)
{
throw new NullReferenceException("LyricsItemCollection.Options");
}
var lyricBuilder = new StringBuilder();
ForEach(lyric => lyricBuilder.Append(lyric).Append(Option.LineBreak));
return lyricBuilder.ToString().TrimEnd(Option.LineBreak);
ForEach(lyric => lyricBuilder.Append(lyric).Append(Options.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.Cli.Infrastructure.DependencyInject;
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
namespace ZonyLrcTools.Common.Lyrics
{
/// <summary>
/// 歌词下载器的基类,定义了歌词下载器的常规逻辑。
/// </summary>
public abstract class LyricDownloader : ILyricDownloader, ITransientDependency
public abstract class LyricsProvider : ILyricsProvider, ITransientDependency
{
public abstract string DownloaderName { get; }
@ -16,20 +15,21 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// </summary>
/// <param name="songName">歌曲名称。</param>
/// <param name="artist">歌曲作者/艺术家。</param>
/// <param name="duration">歌曲的时长。</param>
/// <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);
var downloadDataBytes = await DownloadDataAsync(args);
return await GenerateLyricAsync(downloadDataBytes, args);
var downloadDataObject = await DownloadDataAsync(args);
return await GenerateLyricAsync(downloadDataObject, args);
}
/// <summary>
/// 通用的验证逻辑,验证基本参数是否正确。
/// </summary>
/// <param name="args">歌词下载时需要的参数信息。</param>
protected virtual ValueTask ValidateAsync(LyricDownloaderArgs args)
protected virtual ValueTask ValidateAsync(LyricsProviderArgs args)
{
if (string.IsNullOrEmpty(args.SongName))
{
@ -47,12 +47,13 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
/// <summary>
/// 根据指定的歌曲参数,下载歌词数据。
/// </summary>
protected abstract ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args);
protected abstract ValueTask<object> DownloadDataAsync(LyricsProviderArgs args);
/// <summary>
/// 根据指定的歌词二进制数据,生成歌词数据。
/// 根据指定的歌词对象,生成歌词数据,常用于处理不同格式的歌词数据。
/// </summary>
/// <param name="data">歌词的原始二进制数据。</param>
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data, LyricDownloaderArgs args);
/// <param name="lyricsObject">当 <see cref="DownloadDataAsync"/> 完成后,传递的歌词数据对象。</param>
/// <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;
}
}
}

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