mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-07-01 12:11:13 +00:00
Compare commits
161 Commits
ZonyLrcToo
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e08d1c7f16 | ||
![]() |
c3d98d60b5 | ||
![]() |
8aac9e16a6 | ||
![]() |
21f25ee500 | ||
![]() |
9a979942e3 | ||
![]() |
32b6c4052b | ||
![]() |
7732ab52d3 | ||
![]() |
1fdbaef7a8 | ||
![]() |
36ef2d2c3a | ||
![]() |
fa4620c37d | ||
![]() |
9ab9cd50e2 | ||
![]() |
e66ef89e7a | ||
![]() |
8b5d5c64b0 | ||
![]() |
2dca5239f5 | ||
![]() |
d5689164b5 | ||
![]() |
b57739f543 | ||
![]() |
a2c44901fd | ||
![]() |
6ad2992144 | ||
![]() |
3b0c55ac00 | ||
![]() |
81bf6ebe3f | ||
![]() |
aa3c45101b | ||
![]() |
d00d735bf0 | ||
![]() |
7fa595a71b | ||
![]() |
4f15d06e63 | ||
![]() |
5ccd8a7c53 | ||
![]() |
14812bceb7 | ||
![]() |
be538eb650 | ||
![]() |
888c7f511b | ||
![]() |
7bd1915f11 | ||
![]() |
aa78ea0713 | ||
![]() |
1342ca936c | ||
![]() |
46ac2d57a2 | ||
![]() |
444f4e75a6 | ||
![]() |
61aa31cc6f | ||
![]() |
46d11a86ea | ||
![]() |
8aaae90ed8 | ||
![]() |
d91cc68a60 | ||
![]() |
b6e3cb6038 | ||
![]() |
093e2879ad | ||
![]() |
fea5e1124f | ||
![]() |
2ce7b00d14 | ||
![]() |
ad6ea732f8 | ||
![]() |
06bf733ae9 | ||
![]() |
558cca3c0e | ||
![]() |
93cc31b1e2 | ||
![]() |
dfe18d8541 | ||
![]() |
40b73c3dfa | ||
![]() |
243a0e2559 | ||
![]() |
d83bff1516 | ||
![]() |
8d946ebbd5 | ||
![]() |
4c3d554885 | ||
![]() |
14c78f9a83 | ||
![]() |
b9d6da5f72 | ||
![]() |
c8ffbc55af | ||
![]() |
b0835dcf01 | ||
![]() |
4e1c4d1519 | ||
![]() |
3d96b96edf | ||
![]() |
7f81a3edea | ||
![]() |
24624a6d21 | ||
![]() |
d860632327 | ||
![]() |
1186369d16 | ||
![]() |
b240564cf7 | ||
![]() |
6e2848a9de | ||
![]() |
6361bf9265 | ||
![]() |
383e2c5939 | ||
![]() |
1e5c41852f | ||
![]() |
41cba02833 | ||
![]() |
98c935ed93 | ||
![]() |
62d08df735 | ||
![]() |
1f312c749d | ||
![]() |
30e10f4c01 | ||
![]() |
35468cfb71 | ||
![]() |
126741f344 | ||
![]() |
22f5bc69ec | ||
![]() |
788ff38be2 | ||
![]() |
ab5f79bd50 | ||
![]() |
f935f07609 | ||
![]() |
493f48cefe | ||
![]() |
4999e4d694 | ||
![]() |
334528eca6 | ||
![]() |
e76ddb75eb | ||
![]() |
d6d384590c | ||
![]() |
949dac7ad1 | ||
![]() |
2ffd75ed1d | ||
![]() |
82af3009d3 | ||
![]() |
572e555f2a | ||
![]() |
286731b1a6 | ||
![]() |
5850614663 | ||
![]() |
c20a280a07 | ||
![]() |
55de00ec1f | ||
![]() |
bc2b314d6a | ||
![]() |
0aafe91f96 | ||
![]() |
2e71914578 | ||
![]() |
b916323986 | ||
![]() |
afe1a7013c | ||
![]() |
fb1f743365 | ||
![]() |
0e5e48cd00 | ||
![]() |
18d9c2d32c | ||
![]() |
5dbabab5a6 | ||
![]() |
bfd89e9d82 | ||
![]() |
9a029225e1 | ||
![]() |
a7ecfbe44f | ||
![]() |
f1a6eefe45 | ||
![]() |
86e341290e | ||
![]() |
0aa8ca5aef | ||
![]() |
2eef72e165 | ||
![]() |
f4b17097fd | ||
![]() |
e68d6b9cf9 | ||
![]() |
84231d9613 | ||
![]() |
8398d80a8e | ||
![]() |
991db8ab48 | ||
![]() |
3e957fdb25 | ||
![]() |
fc09839775 | ||
![]() |
8d5a45c9ef | ||
![]() |
6b538f3e0a | ||
![]() |
d7e11af3ba | ||
![]() |
f25ba90b9a | ||
![]() |
bc5afad599 | ||
![]() |
34e8cf74f3 | ||
![]() |
18338d5989 | ||
![]() |
691cfa159f | ||
![]() |
7dce16fe1f | ||
![]() |
fd86a98995 | ||
![]() |
2f2e67f3e6 | ||
![]() |
a46abba2f9 | ||
![]() |
fdc5f27692 | ||
![]() |
430c66786a | ||
![]() |
75fd004c56 | ||
![]() |
ca40728387 | ||
![]() |
acb9142e5f | ||
![]() |
2c1b5ce533 | ||
![]() |
5d1e90f638 | ||
![]() |
b7b1f36bf5 | ||
![]() |
6b72f919b8 | ||
![]() |
64d26cbc4c | ||
![]() |
f9570508c2 | ||
![]() |
6d37d81015 | ||
![]() |
7778b7be09 | ||
![]() |
7adda14713 | ||
![]() |
7d17fc0b97 | ||
![]() |
3e27e18098 | ||
![]() |
895f68184d | ||
![]() |
d19b1d8d2a | ||
![]() |
a11ef70021 | ||
![]() |
6554be6b6d | ||
![]() |
0ed21ed997 | ||
![]() |
740e8f4c63 | ||
![]() |
ecab0e0f5c | ||
![]() |
aa90e232f7 | ||
![]() |
9f96aa0186 | ||
![]() |
bccfaaaa5b | ||
![]() |
f57afd4238 | ||
![]() |
775e6140ee | ||
![]() |
2ccf4f9da6 | ||
![]() |
dbc1e44d2a | ||
![]() |
cde1d32d33 | ||
![]() |
62777e44bd | ||
![]() |
6c25483f1e | ||
![]() |
b64f83d56b | ||
![]() |
6ad154a62b | ||
![]() |
32adab68b6 |
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
10
.github/workflows/dotnet.yml
vendored
10
.github/workflows/dotnet.yml
vendored
@ -3,8 +3,8 @@ name: .NET
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
# paths:
|
||||
# - "versions/**"
|
||||
paths:
|
||||
- "versions/release.md"
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
|
||||
@ -18,9 +18,9 @@ jobs:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 8.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Publish
|
||||
@ -36,6 +36,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
retention-days: 90
|
||||
name: release-files
|
||||
path: |
|
||||
./TempFiles
|
||||
@ -43,6 +44,7 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ steps.date.outputs.date }}
|
||||
release:
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
454
.gitignore
vendored
Normal file
454
.gitignore
vendored
Normal file
@ -0,0 +1,454 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### VisualStudioCode template
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
### VisualStudio template
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
.idea/
|
||||
/.idea
|
||||
/src/ZonyLrcTools.Cli/TempFiles/
|
||||
|
||||
src/ZonyLrcTools.LocalServer/UiStaticResources/*
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "vendor/MusicDecrypto"]
|
||||
path = vendor/MusicDecrypto
|
||||
url = https://github.com/davidxuang/MusicDecrypto
|
11
Directory.Build.props
Normal file
11
Directory.Build.props
Normal 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
45
Directory.Packages.props
Normal 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>
|
BIN
README.assets/image-20230328223155280.png
Normal file
BIN
README.assets/image-20230328223155280.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
137
README.md
137
README.md
@ -1,130 +1,39 @@
|
||||
简体中文 | [English](./docs/en_US.md)
|
||||
English | [简体中文](./zh_CN.md)
|
||||
|
||||
## 免责声明
|
||||
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
# 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.
|
||||
|
||||
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
|
||||
# Introduction
|
||||
|
||||
🚧 当前版本正在开发当中。
|
||||
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
|
||||
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.
|
||||
|
||||
工具会执行每日构建动作,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
|
||||
# Download
|
||||
|
||||
## 用法
|
||||
To get the latest version, please visit the **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** page for download.
|
||||
|
||||
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell/命令提示符/Windows 终端,根据下述说明执行命令即可。
|
||||
## Arch Linux User Repository
|
||||
|
||||
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
|
||||
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
|
||||
|
||||
#### 歌曲下载
|
||||
|
||||
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
|
||||
|
||||
./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.
|
||||
|
||||
```shell
|
||||
# 下载歌词
|
||||
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2
|
||||
# 下载专辑封面
|
||||
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2
|
||||
```
|
||||
# Donation
|
||||
[爱发电](https://afdian.net/a/zony-lrc-tools)
|
||||
|
||||
#### 加密格式转换
|
||||
# Star History
|
||||
|
||||
子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**。
|
||||
|
||||
目前软件支持 NCM、QCM(开发中...🚧) 格式的音乐文件转换,命令如下。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe util -t=Ncm D:\CloudMusic
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
程序的所有的配置信息,都在 `config.yaml` 进行更改,下面标注了各个配置的说明。
|
||||
|
||||
```yaml
|
||||
globalOption:
|
||||
# 允许扫描的歌曲文件后缀名。
|
||||
supportFileExtensions:
|
||||
- '*.mp3'
|
||||
- '*.flac'
|
||||
- '*.wav'
|
||||
# 网络代理服务设置,仅支持 HTTP 代理。
|
||||
networkOptions:
|
||||
isEnable: false # 是否启用代理。
|
||||
ip: 127.0.0.1 # 代理服务 IP 地址。
|
||||
port: 4780 # 代理服务端口号。
|
||||
|
||||
# 下载器的相关参数配置。
|
||||
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 时禁用。
|
||||
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
||||
priority: 2
|
||||
- name: KuGou # 基于酷狗音乐的歌词下载器。
|
||||
priority: 3
|
||||
# 歌词下载的一些共有配置参数。
|
||||
config:
|
||||
isOneLine: true # 双语歌词是否合并为一行展示。
|
||||
lineBreak: "\n" # 换行符的类型,记得使用双引号指定。
|
||||
isEnableTranslation: true # 是否启用翻译歌词。
|
||||
isSkipExistLyricFiles: false # 如果歌词文件已经存在,是否跳过这些文件。
|
||||
```
|
||||
|
||||
### 屏蔽字典
|
||||
|
||||
屏蔽字典适用于网易云音乐歌词下载,针对某些单词,网易云音乐使用了 * 号进行屏蔽,这个时候可以使用屏蔽字典,设置歌曲名的关键词替换。例如有一首歌曲叫做 *Fucking ABC* ,这个时候网易云实际的名字是 *Fu****ing* ,用户只需要在屏蔽字典加入替换逻辑即可,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"Fuckking": "Fu****ing"
|
||||
}
|
||||
```
|
||||
|
||||
屏蔽字典默认路径为程序所在目录的 *BlockWords.json* 文件,用户可以在 *appsettings.json* 文件中配置其他路径。
|
||||
|
||||
## 捐赠
|
||||
|
||||
<img src="./docs/img/alipay.jpg" width="200"/><img src="./docs/img/wechat.jpg" width="200"/>
|
||||
|
||||
## 路线图
|
||||
|
||||
- [x] 支持跨平台的 CLI 工具。
|
||||
- [ ] 基于 Web GUI 的操作站点。
|
||||
- [ ] 支持插件系统(Lua 引擎)。
|
||||
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
||||
|
@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Cli", "src\Zon
|
||||
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
|
||||
@ -27,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
|
||||
@ -41,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
|
||||
|
3
ZonyLrcTools.sln.DotSettings
Normal file
3
ZonyLrcTools.sln.DotSettings
Normal file
@ -0,0 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QQ/@EntryIndexedValue">QQ</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zony/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,18 +1,39 @@
|
||||
English | [简体中文](./zh_CN.md)
|
||||
|
||||
## Overview
|
||||
# Disclaimer
|
||||
|
||||
ZonyLrcToolX 2.0 is a cross-platform lyric downlaod tool based on 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.
|
||||
|
||||
🚧 The current version is under development.
|
||||
🚧 If you want to see the working code, please switch to the 1.0 branch.
|
||||
# Introduction
|
||||
|
||||
## Usage
|
||||
ZonyLrcToolX 4 is a cross-platform lyrics download tool based on CEF. **QQ Group: 337656932**. Detailed video tutorials are available in the group files.
|
||||
|
||||
## Donation
|
||||
🚧 The current version is under development.
|
||||
🚧 If you want to see working code, please switch to the dev branch.
|
||||
|
||||
## Roadmap
|
||||
# Download
|
||||
|
||||
- [ ] Supports cross-platform CLI tools.
|
||||
- [ ] Web GUI based site (local).
|
||||
- [ ] Support plug-in system (Lua Engine).
|
||||
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
|
||||
|
||||
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 247 KiB |
Binary file not shown.
Before Width: | Height: | Size: 211 KiB |
40
docs/zh_CN.md
Normal file
40
docs/zh_CN.md
Normal 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
|
||||
|
||||
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
@ -1,51 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NAudio.Wave;
|
||||
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;
|
||||
using File = System.IO.File;
|
||||
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 ILogger<DownloadCommand> _logger;
|
||||
private readonly IFileScanner _fileScanner;
|
||||
private readonly ITagLoader _tagLoader;
|
||||
private readonly IEnumerable<ILyricDownloader> _lyricDownloaderList;
|
||||
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
|
||||
private readonly ILyricsDownloader _lyricsDownloader;
|
||||
private readonly IAlbumDownloader _albumDownloader;
|
||||
private readonly IMusicInfoLoader _musicInfoLoader;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly ToolOptions _options;
|
||||
|
||||
public DownloadCommand(ILogger<DownloadCommand> logger,
|
||||
IFileScanner fileScanner,
|
||||
IOptions<ToolOptions> options,
|
||||
ITagLoader tagLoader,
|
||||
IEnumerable<ILyricDownloader> lyricDownloaderList,
|
||||
IEnumerable<IAlbumDownloader> albumDownloaderList)
|
||||
public DownloadCommand(ILyricsDownloader lyricsDownloader,
|
||||
IMusicInfoLoader musicInfoLoader,
|
||||
IAlbumDownloader albumDownloader,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileScanner = fileScanner;
|
||||
_tagLoader = tagLoader;
|
||||
_lyricDownloaderList = lyricDownloaderList;
|
||||
_albumDownloaderList = albumDownloaderList;
|
||||
_options = options.Value;
|
||||
_lyricsDownloader = lyricsDownloader;
|
||||
_musicInfoLoader = musicInfoLoader;
|
||||
_albumDownloader = albumDownloader;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
#region > Options <
|
||||
@ -60,222 +46,92 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
||||
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
|
||||
public bool DownloadAlbum { get; set; }
|
||||
|
||||
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
|
||||
public int ParallelNumber { get; set; } = 2;
|
||||
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 1)")]
|
||||
public int ParallelNumber { get; set; } = 1;
|
||||
|
||||
[Option] public string ErrorMessage { get; set; } = Path.Combine(Directory.GetCurrentDirectory(), "error.log");
|
||||
#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 DownloadLyricFilesAsync(
|
||||
await LoadMusicInfoAsync(
|
||||
RemoveExistLyricFiles(
|
||||
await ScanMusicFilesAsync())));
|
||||
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
||||
}
|
||||
|
||||
if (DownloadAlbum)
|
||||
{
|
||||
await DownloadAlbumAsync(
|
||||
await LoadMusicInfoAsync(
|
||||
await ScanMusicFilesAsync()));
|
||||
await _albumDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ScanMusicFilesAsync()
|
||||
/// <summary>
|
||||
/// Get the music infos by the scanner.
|
||||
/// </summary>
|
||||
private async Task<List<MusicInfo>> GetMusicInfosAsync(string scanner)
|
||||
{
|
||||
var files = (await _fileScanner.ScanAsync(SongsDirectory, _options.SupportFileExtensions))
|
||||
.SelectMany(t => t.FilePaths)
|
||||
.ToList();
|
||||
ValidateScannerOptions(scanner);
|
||||
|
||||
if (files.Count == 0)
|
||||
return scanner switch
|
||||
{
|
||||
_logger.LogError("没有找到任何音乐文件。");
|
||||
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
|
||||
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("当使用非本地文件扫描器时,必须指定歌词文件的输出路径。");
|
||||
}
|
||||
|
||||
_logger.LogInformation($"已经扫描到了 {files.Count} 个音乐文件。");
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private List<string> RemoveExistLyricFiles(List<string> filePaths)
|
||||
{
|
||||
if (!_options.Provider.Lyric.Config.IsSkipExistLyricFiles)
|
||||
if (scanner != MusicScannerConsts.LocalScanner && !Directory.Exists(OutputDirectory))
|
||||
{
|
||||
return filePaths;
|
||||
throw new ArgumentException("指定的歌词文件输出路径不存在。");
|
||||
}
|
||||
|
||||
return filePaths
|
||||
.Where(path =>
|
||||
{
|
||||
if (!File.Exists(Path.ChangeExtension(path, ".lrc")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogWarning($"已经存在歌词文件 {path},跳过。");
|
||||
return false;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
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))
|
||||
.Where(m => m != null)
|
||||
.Where(m => !string.IsNullOrEmpty(m.Name) || !string.IsNullOrEmpty(m.Artist))
|
||||
.ToList();
|
||||
|
||||
// Load music total time info.
|
||||
result.Foreach(m => { m.TotalTime = (long?)new AudioFileReader(m.FilePath).TotalTime.TotalMilliseconds; });
|
||||
|
||||
_logger.LogInformation($"已成功加载 {files.Count} 个音乐文件的标签信息。");
|
||||
|
||||
return result.ToImmutableList();
|
||||
}
|
||||
|
||||
private IEnumerable<ILyricDownloader> GetLyricDownloaderList()
|
||||
{
|
||||
var downloader = _options.Provider.Lyric.Plugin
|
||||
.Where(op => op.Priority != -1)
|
||||
.OrderBy(op => op.Priority)
|
||||
.Join(_lyricDownloaderList,
|
||||
op => op.Name,
|
||||
loader => loader.DownloaderName,
|
||||
(op, loader) => loader);
|
||||
|
||||
return downloader;
|
||||
}
|
||||
|
||||
#region > Lyric download logic <
|
||||
|
||||
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(m => m.IsSuccessful)} 失败{musicInfos.Count(m => m.IsSuccessful == false)}。");
|
||||
}
|
||||
|
||||
private async Task DownloadLyricTaskLogicAsync(IEnumerable<ILyricDownloader> downloaderList, MusicInfo info)
|
||||
{
|
||||
async Task InternalDownloadLogicAsync(ILyricDownloader downloader)
|
||||
switch (scanner)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lyric = await downloader.DownloadAsync(info.Name, info.Artist, info.TotalTime);
|
||||
var lyricFilePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!,
|
||||
$"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
|
||||
|
||||
if (File.Exists(lyricFilePath))
|
||||
{
|
||||
File.Delete(lyricFilePath);
|
||||
}
|
||||
|
||||
info.IsSuccessful = true;
|
||||
|
||||
if (lyric.IsPruneMusic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(lyricFilePath, FileMode.Create);
|
||||
await using var sw = new BinaryWriter(stream);
|
||||
|
||||
sw.Write(EncodingConvert(lyric));
|
||||
await stream.FlushAsync();
|
||||
}
|
||||
catch (ErrorCodeException ex)
|
||||
{
|
||||
info.IsSuccessful = ex.ErrorCode == ErrorCodes.NoMatchingSong;
|
||||
|
||||
_logger.LogWarningInfo(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"下载歌词文件时发生错误:{ex.Message},歌曲名: {info.Name},歌手: {info.Artist}。");
|
||||
info.IsSuccessful = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var downloader in downloaderList)
|
||||
{
|
||||
await InternalDownloadLogicAsync(downloader);
|
||||
|
||||
if (info.IsSuccessful)
|
||||
{
|
||||
_logger.LogSuccessful(info);
|
||||
return;
|
||||
}
|
||||
case MusicScannerConsts.CsvScanner when string.IsNullOrWhiteSpace(CsvFilePath):
|
||||
throw new ArgumentException("当使用 CSV 文件扫描器时,必须指定 CSV 文件的路径。");
|
||||
case MusicScannerConsts.NeteaseScanner when string.IsNullOrWhiteSpace(SongListId):
|
||||
throw new ArgumentException("当使用网易云音乐扫描器时,必须指定歌单的 ID。");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] EncodingConvert(LyricItemCollection lyric)
|
||||
{
|
||||
var supportEncodings = Encoding.GetEncodings();
|
||||
if (supportEncodings.All(x => x.Name != _options.Provider.Lyric.Config.FileEncoding))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
||||
}
|
||||
|
||||
return Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(_options.Provider.Lyric.Config.FileEncoding), lyric.GetUtf8Bytes());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region > Ablum image download logic <
|
||||
|
||||
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(m => m.IsSuccessful)} 失败{musicInfos.Count(m => m.IsSuccessful == false)}。");
|
||||
}
|
||||
|
||||
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
|
||||
{
|
||||
_logger.LogSuccessful(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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
29
src/ZonyLrcTools.Cli/Commands/SubCommand/SearchCommand.cs
Normal file
29
src/ZonyLrcTools.Cli/Commands/SubCommand/SearchCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -1,24 +1,16 @@
|
||||
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;
|
||||
using MusicDecrypto.Library;
|
||||
using ZonyLrcTools.Common.Infrastructure.IO;
|
||||
using ZonyLrcTools.Common.Infrastructure.Threading;
|
||||
using ZonyLrcTools.Common.MusicDecryption;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands.SubCommand
|
||||
{
|
||||
public enum SupportFileType
|
||||
{
|
||||
Ncm = 1,
|
||||
Qcm = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工具类相关命令。
|
||||
/// </summary>
|
||||
@ -28,13 +20,9 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
||||
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; }
|
||||
[Required(ErrorMessage = "请指定需要解密的歌曲文件或文件夹路径。")]
|
||||
[Option("-s|--source", CommandOptionType.SingleValue, Description = "需要解密的歌曲文件或文件夹路径。", ShowInHelpText = true)]
|
||||
public string Source { get; set; }
|
||||
|
||||
private readonly IFileScanner _fileScanner;
|
||||
|
||||
@ -49,57 +37,41 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
||||
|
||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
if (Directory.Exists(FilePath))
|
||||
if (Directory.Exists(Source))
|
||||
{
|
||||
_logger.LogInformation("开始扫描文件夹,请稍等...");
|
||||
|
||||
var files = (await _fileScanner.ScanAsync(FilePath, new[] { "*.ncm" }))
|
||||
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(() => Convert(path)));
|
||||
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(FilePath))
|
||||
else if (File.Exists(Source))
|
||||
{
|
||||
await Convert(FilePath);
|
||||
await _musicDecryptor.ConvertMusicAsync(Source);
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,23 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
[HelpOption("--help|-h", Description = "欢迎使用 ZonyLrcToolsX Command Line Interface。")]
|
||||
[HelpOption("--help|-h",
|
||||
Description = "欢迎使用 ZonyLrcToolsX Command Line Interface,有任何问题请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。",
|
||||
ShowInHelpText = true)]
|
||||
public abstract class ToolCommandBase
|
||||
{
|
||||
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
if (!Environment.UserInteractive)
|
||||
{
|
||||
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
app.ShowHelp();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Config;
|
||||
|
||||
public class ProviderOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 标签加载器相关的配置属性。
|
||||
/// </summary>
|
||||
public TagOption Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌词下载相关的配置信息。
|
||||
/// </summary>
|
||||
public LyricOption Lyric { get; set; }
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using YamlDotNet.Serialization;
|
||||
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 List<string> SupportFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 网络代理相关的配置信息。
|
||||
/// </summary>
|
||||
public NetworkOptions NetworkOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 定义下载器的相关配置信息。
|
||||
/// </summary>
|
||||
public ProviderOption Provider { get; set; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建 <see cref="LyricItemCollection"/> 对象的工厂。
|
||||
/// </summary>
|
||||
public interface ILyricItemCollectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
|
||||
/// </summary>
|
||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
|
||||
LyricItemCollection Build(string sourceLyric);
|
||||
|
||||
/// <summary>
|
||||
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
|
||||
/// </summary>
|
||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||
/// <param name="translationLyric">翻译歌词数据。</param>
|
||||
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
|
||||
LyricItemCollection Build(string sourceLyric, string translationLyric);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
public interface ILyricTextResolver
|
||||
{
|
||||
LyricItemCollection Resolve(string lyricText);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
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 readonly ToolOptions _options;
|
||||
|
||||
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,
|
||||
IOptions<ToolOptions> options)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricItemCollectionFactory = lyricItemCollectionFactory;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl,
|
||||
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
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 readonly ToolOptions _options;
|
||||
|
||||
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
|
||||
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
|
||||
|
||||
private const string NetEaseRequestReferer = @"https://music.163.com";
|
||||
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
|
||||
|
||||
public NetEaseLyricDownloader(IWarpHttpClient warpHttpClient,
|
||||
ILyricItemCollectionFactory lyricItemCollectionFactory,
|
||||
IOptions<ToolOptions> options)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricItemCollectionFactory = lyricItemCollectionFactory;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
|
||||
NetEaseSearchMusicUrl,
|
||||
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
||||
true,
|
||||
msg =>
|
||||
{
|
||||
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
|
||||
if (msg.Content != null)
|
||||
{
|
||||
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
|
||||
}
|
||||
});
|
||||
|
||||
ValidateSongSearchResponse(searchResult, args);
|
||||
|
||||
var lyricResponse = await _warpHttpClient.GetAsync(
|
||||
NetEaseGetLyricUrl,
|
||||
new GetLyricRequest(searchResult.GetFirstMatchSongId(args.SongName, args.Duration)),
|
||||
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 || string.IsNullOrEmpty(json.OriginalLyric.Text))
|
||||
{
|
||||
return new LyricItemCollection(null);
|
||||
}
|
||||
|
||||
if (json.OriginalLyric.Text.Contains("纯音乐,请欣赏"))
|
||||
{
|
||||
return new LyricItemCollection(null);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// ReSharper disable IdentifierTypo
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.MusicScannerOptions;
|
||||
|
||||
public class MusicScannerConsts
|
||||
{
|
||||
public const string LocalScanner = "local";
|
||||
public const string NeteaseScanner = "netease";
|
||||
public const string CsvScanner = "csv";
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using YamlDotNet.Serialization;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public class TagInfoProviderOption
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
public Dictionary<string, string> Extensions { get; set; }
|
||||
}
|
||||
|
||||
public class TagOption
|
||||
{
|
||||
public IEnumerable<TagInfoProviderOption> Plugin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 屏蔽词功能相关配置。
|
||||
/// </summary>
|
||||
public BlockWordOption BlockWord { get; set; }
|
||||
}
|
||||
}
|
26
src/ZonyLrcTools.Cli/Infrastructure/UpdaterHostedService.cs
Normal file
26
src/ZonyLrcTools.Cli/Infrastructure/UpdaterHostedService.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
@ -8,12 +7,15 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using ZonyLrcTools.Cli.Commands;
|
||||
using ZonyLrcTools.Cli.Commands.SubCommand;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Cli.Infrastructure;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Logging;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace ZonyLrcTools.Cli
|
||||
{
|
||||
@ -31,6 +33,7 @@ namespace ZonyLrcTools.Cli
|
||||
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Starting...");
|
||||
return await BuildHostedService(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -61,12 +64,12 @@ namespace ZonyLrcTools.Cli
|
||||
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
|
||||
.WriteTo.Logger(lc =>
|
||||
{
|
||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
|
||||
lc.Filter.ByIncludingOnly(warningLog => warningLog.Level == LogEventLevel.Warning)
|
||||
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
|
||||
})
|
||||
.WriteTo.Logger(lc =>
|
||||
{
|
||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error)
|
||||
lc.Filter.ByIncludingOnly(errLog => errLog.Level == LogEventLevel.Error)
|
||||
.WriteTo.Async(c => c.File("Logs/errors.txt"));
|
||||
})
|
||||
.CreateLogger();
|
||||
@ -75,19 +78,20 @@ namespace ZonyLrcTools.Cli
|
||||
private static Task<int> BuildHostedService(string[] args)
|
||||
{
|
||||
return new HostBuilder()
|
||||
.ConfigureLogging(builder => builder.AddSerilog())
|
||||
.ConfigureLogging(l => l.AddSerilog())
|
||||
.ConfigureHostConfiguration(builder =>
|
||||
{
|
||||
builder
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
builder.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.AddYamlFile("config.yaml");
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
services.AddSingleton(PhysicalConsole.Singleton);
|
||||
services.BeginAutoDependencyInject<Program>();
|
||||
services.BeginAutoDependencyInject<IWarpHttpClient>();
|
||||
services.ConfigureConfiguration();
|
||||
services.ConfigureToolService();
|
||||
services.AddHostedService<UpdaterHostedService>();
|
||||
})
|
||||
.RunCommandLineApplicationAsync<Program>(args);
|
||||
}
|
||||
|
@ -4,7 +4,8 @@
|
||||
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
|
||||
"10003": "不能获取文件的后缀信息。",
|
||||
"10004": "没有扫描到任何音乐文件。",
|
||||
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。"
|
||||
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。",
|
||||
"10006": "无法从网易云音乐获取歌曲列表。"
|
||||
},
|
||||
"Warning": {
|
||||
"50001": "扫描文件时出现了错误。",
|
||||
|
@ -1,43 +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="4.0.1" />
|
||||
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.1.0" />
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Refit" Version="6.3.2" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.3.2" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.3.2" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.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" />
|
||||
<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>
|
||||
<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>
|
||||
|
@ -1,52 +1,61 @@
|
||||
globalOption:
|
||||
# 允许扫描的歌曲文件后缀名。
|
||||
supportFileExtensions:
|
||||
- '*.mp3'
|
||||
- '*.flac'
|
||||
- '*.wav'
|
||||
# 网络代理服务设置,仅支持 HTTP 代理。
|
||||
networkOptions:
|
||||
isEnable: false # 是否启用代理。
|
||||
ip: 127.0.0.1 # 代理服务 IP 地址。
|
||||
port: 4780 # 代理服务端口号。
|
||||
|
||||
# 下载器的相关参数配置。
|
||||
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: 30 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
||||
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
||||
priority: 2
|
||||
# depth: 10 # 暂时不支持。
|
||||
- name: KuGou # 基于酷狗音乐的歌词下载器。
|
||||
priority: 3
|
||||
depth: 10
|
||||
# 歌词下载的一些共有配置参数。
|
||||
config:
|
||||
isOneLine: true # 双语歌词是否合并为一行展示。
|
||||
lineBreak: "\n" # 换行符的类型,记得使用双引号指定。
|
||||
isEnableTranslation: true # 是否启用翻译歌词。
|
||||
isOnlyOutputTranslation: false # 是否只输出翻译歌词。
|
||||
isSkipExistLyricFiles: false # 如果歌词文件已经存在,是否跳过这些文件。
|
||||
fileEncoding: 'utf-8' # 歌词文件的编码格式。
|
||||
# 允许扫描的歌曲文件后缀名。
|
||||
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' # 歌词文件的编码格式。
|
20
src/ZonyLrcTools.Cli/publish.ps1
Normal file
20
src/ZonyLrcTools.Cli/publish.ps1
Normal file
@ -0,0 +1,20 @@
|
||||
$Platforms = @('win-x64', 'linux-x64', 'osx-x64', 'win-arm64', 'linux-arm64', 'osx-arm64')
|
||||
|
||||
if (-not (Test-Path ./TempFiles)) {
|
||||
New-Item -ItemType Directory -Path ./TempFiles | Out-Null
|
||||
}
|
||||
|
||||
Remove-Item ./TempFiles/* -Recurse -Force
|
||||
|
||||
foreach ($platform in $Platforms) {
|
||||
dotnet publish -r $platform -c Release -p:PublishSingleFile=true --self-contained true | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Set-Location ./bin/Release/net7.0/$platform/publish/
|
||||
Compress-Archive -Path ./* -DestinationPath ./ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip | Out-Null
|
||||
Set-Location ../../../../../
|
||||
|
||||
Move-Item ./bin/Release/net7.0/$platform/publish/ZonyLrcTools_${platform}_${Env:PUBLISH_VERSION}.zip ./TempFiles
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
Platforms=('win-x64' 'linux-x64' 'osx-x64')
|
||||
Platforms=('win-x64' 'linux-x64' 'osx-x64' 'win-arm64' 'linux-arm64' 'osx-arm64')
|
||||
|
||||
if ! [ -d './TempFiles' ];
|
||||
then
|
||||
@ -10,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 || exit 1
|
||||
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:DebugType=none --self-contained true || exit 1
|
||||
|
||||
cd ./bin/Release/net6.0/"$platform"/publish/ || exit 1
|
||||
cd ./bin/Release/net8.0/"$platform"/publish/ || exit 1
|
||||
zip -r ./ZonyLrcTools_"$platform"_"${PUBLISH_VERSION}".zip ./ || exit 1
|
||||
cd ../../../../../ || exit 1
|
||||
|
||||
mv ./bin/Release/net6.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
|
||||
mv ./bin/Release/net8.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$PUBLISH_VERSION".zip ./TempFiles
|
||||
done
|
68
src/ZonyLrcTools.Common/Album/AlbumDownloader.cs
Normal file
68
src/ZonyLrcTools.Common/Album/AlbumDownloader.cs
Normal 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)}。");
|
||||
}
|
||||
}
|
10
src/ZonyLrcTools.Common/Album/IAlbumDownloader.cs
Normal file
10
src/ZonyLrcTools.Common/Album/IAlbumDownloader.cs
Normal 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; }
|
||||
}
|
@ -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>
|
||||
/// 下载器的名称。
|
@ -1,9 +1,9 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Album
|
||||
namespace ZonyLrcTools.Common.Album
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义了程序默认提供的专辑图像下载器。
|
||||
/// </summary>
|
||||
public static class InternalAlbumDownloaderNames
|
||||
public static class InternalAlbumProviderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 网易云音乐专辑图像下载器。
|
@ -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,7 +41,7 @@ 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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
namespace ZonyLrcTools.Cli.Config
|
||||
namespace ZonyLrcTools.Common.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 屏蔽词选项类。
|
||||
/// </summary>
|
||||
public class BlockWordOption
|
||||
public class BlockWordOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用本功能。
|
||||
@ -13,6 +13,6 @@
|
||||
/// <summary>
|
||||
/// 屏蔽词字典文件,用于替换歌曲名或者歌手名称。
|
||||
/// </summary>
|
||||
public string FilePath { get; set; }
|
||||
public string FilePath { get; set; } = null!;
|
||||
}
|
||||
}
|
@ -1,22 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Lyric;
|
||||
using ZonyLrcTools.Common.Lyrics;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Config;
|
||||
namespace ZonyLrcTools.Common.Configuration;
|
||||
|
||||
public class LyricOption
|
||||
{
|
||||
public IEnumerable<LyricProviderOption> Plugin { get; set; }
|
||||
|
||||
public LyricConfigOption Config { get; set; }
|
||||
|
||||
public LyricProviderOption GetLyricProviderOption(string name)
|
||||
{
|
||||
return Plugin.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
}
|
||||
|
||||
public class LyricConfigOption
|
||||
public class GlobalLyricsConfigOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 双语歌词是否合并为一行。
|
20
src/ZonyLrcTools.Common/Configuration/GlobalOptions.cs
Normal file
20
src/ZonyLrcTools.Common/Configuration/GlobalOptions.cs
Normal 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!;
|
||||
}
|
||||
}
|
13
src/ZonyLrcTools.Common/Configuration/LyricsOptions.cs
Normal file
13
src/ZonyLrcTools.Common/Configuration/LyricsOptions.cs
Normal 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)!;
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
namespace ZonyLrcTools.Cli.Config
|
||||
namespace ZonyLrcTools.Common.Configuration
|
||||
{
|
||||
public class LyricProviderOption
|
||||
public class LyricsProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌词下载器的唯一标识。
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 歌词下载时的优先级,当值为 -1 时是禁用。
|
||||
@ -16,5 +16,10 @@
|
||||
/// 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
||||
/// </summary>
|
||||
public int Depth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌词下载器的扩展属性。
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Additional { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
namespace ZonyLrcTools.Common.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具网络相关的设定。
|
||||
@ -13,11 +13,16 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
/// <summary>
|
||||
/// 代理服务器的 Ip。
|
||||
/// </summary>
|
||||
public string Ip { get; set; }
|
||||
public string Ip { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 代理服务器的 端口。
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新检查的 Url。
|
||||
/// </summary>
|
||||
public string UpdateUrl { get; set; } = default!;
|
||||
}
|
||||
}
|
14
src/ZonyLrcTools.Common/Configuration/ProviderOptions.cs
Normal file
14
src/ZonyLrcTools.Common/Configuration/ProviderOptions.cs
Normal 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!;
|
||||
}
|
16
src/ZonyLrcTools.Common/Configuration/TagInfoOptions.cs
Normal file
16
src/ZonyLrcTools.Common/Configuration/TagInfoOptions.cs
Normal 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)!;
|
||||
}
|
||||
}
|
@ -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!;
|
||||
}
|
||||
}
|
12
src/ZonyLrcTools.Common/IMusicInfoLoader.cs
Normal file
12
src/ZonyLrcTools.Common/IMusicInfoLoader.cs
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
||||
{
|
||||
/// <summary>
|
||||
/// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。
|
@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
|
||||
{
|
||||
/// <summary>
|
||||
/// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。
|
@ -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,7 +25,7 @@ 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
|
||||
{
|
||||
@ -45,11 +43,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.AddYamlFile("config.yaml")
|
||||
.Build();
|
||||
|
||||
services.Configure<ToolOptions>(configuration.GetSection("globalOption"));
|
||||
services.Configure<GlobalOptions>(configuration);
|
||||
|
||||
return services;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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];
|
@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误码。
|
||||
@ -31,6 +31,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
|
||||
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
|
||||
/// </summary>
|
||||
public const int NotSupportedFileEncoding = 10005;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 无法从网易云音乐获取歌曲列表。
|
||||
/// </summary>
|
||||
public const int UnableGetSongListFromNetEaseCloudMusic = 10006;
|
||||
|
||||
#endregion
|
||||
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Linq 相关的扩展方法。
|
@ -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,7 +37,7 @@ 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>
|
||||
@ -45,9 +45,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器的实例。</param>
|
||||
/// <param name="musicInfo">需要打印的歌曲信息。</param>
|
||||
public static void LogSuccessful(this ILogger logger, MusicInfo musicInfo)
|
||||
public static void LogSuccessful(this IWarpLogger logger, MusicInfo musicInfo)
|
||||
{
|
||||
logger.LogInformation($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.");
|
||||
logger.InfoAsync($"歌曲名: {musicInfo.Name}, 艺术家: {musicInfo.Artist}, 下载成功.").GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串处理相关的工具方法。
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件扫描结果对象。
|
@ -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
|
||||
{
|
@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
namespace ZonyLrcTools.Common.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐文件扫描器,用于扫描音乐文件。
|
@ -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);
|
||||
}
|
@ -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,7 +122,7 @@ 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
|
||||
{
|
||||
@ -134,7 +139,7 @@ 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 });
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"/> 限定并行度。
|
24
src/ZonyLrcTools.Common/Lyrics/ILyricsDownloader.cs
Normal file
24
src/ZonyLrcTools.Common/Lyrics/ILyricsDownloader.cs
Normal 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; }
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。
|
||||
/// </summary>
|
||||
public interface ILyricDownloader
|
||||
public interface ILyricsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载歌词数据。
|
||||
@ -14,7 +12,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
/// <param name="artist">歌曲的作者。</param>
|
||||
/// <param name="duration">歌曲的时长。</param>
|
||||
/// <returns>歌曲的歌词数据对象。</returns>
|
||||
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
|
||||
ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 下载器的名称。
|
7
src/ZonyLrcTools.Common/Lyrics/ILyricsTextResolver.cs
Normal file
7
src/ZonyLrcTools.Common/Lyrics/ILyricsTextResolver.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
public interface ILyricsTextResolver
|
||||
{
|
||||
LyricsItemCollection Resolve(string lyricText);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
/// <summary>
|
||||
/// 换行符格式定义。
|
161
src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs
Normal file
161
src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs
Normal 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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
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()
|
@ -1,38 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
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 LyricConfigOption Option { get; private set; }
|
||||
public GlobalLyricsConfigOptions? Options { get; }
|
||||
|
||||
public LyricItemCollection(LyricConfigOption 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 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)
|
||||
{
|
||||
@ -94,7 +96,7 @@ 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 })
|
||||
@ -103,9 +105,14 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
|
||||
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()
|
@ -1,25 +1,25 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ILyricItemCollectionFactory"/> 的默认实现。
|
||||
/// <see cref="ILyricsItemCollectionFactory"/> 的默认实现。
|
||||
/// </summary>
|
||||
public class LyricItemCollectionFactory : ILyricItemCollectionFactory, ITransientDependency
|
||||
public class LyricsItemCollectionFactory : ILyricsItemCollectionFactory, ITransientDependency
|
||||
{
|
||||
private readonly ToolOptions _options;
|
||||
private readonly GlobalOptions _options;
|
||||
|
||||
public LyricItemCollectionFactory(IOptions<ToolOptions> options)
|
||||
public LyricsItemCollectionFactory(IOptions<GlobalOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public LyricItemCollection Build(string sourceLyric)
|
||||
public LyricsItemCollection Build(string? sourceLyric)
|
||||
{
|
||||
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
|
||||
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||
if (string.IsNullOrEmpty(sourceLyric))
|
||||
{
|
||||
return lyric;
|
||||
@ -30,9 +30,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
return lyric;
|
||||
}
|
||||
|
||||
public LyricItemCollection Build(string sourceLyric, string translationLyric)
|
||||
public LyricsItemCollection Build(string? sourceLyric, string? translationLyric)
|
||||
{
|
||||
var lyric = new LyricItemCollection(_options.Provider.Lyric.Config);
|
||||
var lyric = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||
if (string.IsNullOrEmpty(sourceLyric))
|
||||
{
|
||||
return lyric;
|
||||
@ -42,7 +42,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
|
||||
if (_options.Provider.Lyric.Config.IsEnableTranslation && !string.IsNullOrEmpty(translationLyric))
|
||||
{
|
||||
var translatedLyric = InternalBuildLyricObject(new LyricItemCollection(_options.Provider.Lyric.Config), translationLyric);
|
||||
var translatedLyric = InternalBuildLyricObject(new LyricsItemCollection(_options.Provider.Lyric.Config), translationLyric);
|
||||
if (_options.Provider.Lyric.Config.IsOnlyOutputTranslation)
|
||||
{
|
||||
return translatedLyric;
|
||||
@ -54,15 +54,15 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
return lyric;
|
||||
}
|
||||
|
||||
private LyricItemCollection InternalBuildLyricObject(LyricItemCollection lyric, string sourceText)
|
||||
private LyricsItemCollection InternalBuildLyricObject(LyricsItemCollection lyrics, string sourceText)
|
||||
{
|
||||
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
|
||||
foreach (Match match in regex.Matches(sourceText))
|
||||
{
|
||||
lyric.Add(new LyricItem(match.Value));
|
||||
lyrics.Add(new LyricsItem(match.Value));
|
||||
}
|
||||
|
||||
return lyric;
|
||||
return lyrics;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
@ -18,19 +17,19 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
/// <param name="artist">歌曲作者/艺术家。</param>
|
||||
/// <param name="duration">歌曲的时长。</param>
|
||||
/// <returns>下载完成的歌曲数据。</returns>
|
||||
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
|
||||
public virtual async ValueTask<LyricsItemCollection> DownloadAsync(string songName, string artist, long? duration = null)
|
||||
{
|
||||
var args = new LyricDownloaderArgs(songName, artist, duration ?? 0);
|
||||
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))
|
||||
{
|
||||
@ -48,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);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
namespace ZonyLrcTools.Common.Lyrics
|
||||
{
|
||||
public class LyricDownloaderArgs
|
||||
public class LyricsProviderArgs
|
||||
{
|
||||
public string SongName { get; set; }
|
||||
|
||||
@ -8,7 +8,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
|
||||
public long Duration { get; set; }
|
||||
|
||||
public LyricDownloaderArgs(string songName, string artist, long duration)
|
||||
public LyricsProviderArgs(string songName, string artist, long duration)
|
||||
{
|
||||
SongName = songName;
|
||||
Artist = artist;
|
@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
||||
{
|
||||
public class GetLyricAccessKeyRequest
|
||||
{
|
||||
@ -10,9 +10,9 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
|
||||
[JsonProperty("client")] public string UnknownParameters3 { get; }
|
||||
|
||||
[JsonProperty("hash")] public string FileHash { get; }
|
||||
[JsonProperty("hash")] public string? FileHash { get; }
|
||||
|
||||
public GetLyricAccessKeyRequest(string fileHash)
|
||||
public GetLyricAccessKeyRequest(string? fileHash)
|
||||
{
|
||||
UnknownParameters1 = 1;
|
||||
UnknownParameters2 = "yes";
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
||||
{
|
||||
public class GetLyricAccessKeyResponse
|
||||
{
|
||||
@ -9,13 +8,13 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
|
||||
[JsonProperty("errcode")] public int ErrorCode { get; set; }
|
||||
|
||||
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject> AccessKeyDataObjects { get; set; }
|
||||
[JsonProperty("candidates")] public List<GetLyricAccessKeyDataObject>? AccessKeyDataObjects { get; set; }
|
||||
}
|
||||
|
||||
public class GetLyricAccessKeyDataObject
|
||||
{
|
||||
[JsonProperty("accesskey")] public string AccessKey { get; set; }
|
||||
[JsonProperty("accesskey")] public string? AccessKey { get; set; }
|
||||
|
||||
[JsonProperty("id")] public string Id { get; set; }
|
||||
[JsonProperty("id")] public string? Id { get; set; }
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
||||
{
|
||||
public class GetLyricRequest
|
||||
{
|
||||
@ -12,11 +12,11 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
|
||||
[JsonProperty("charset")] public string UnknownParameters4 { get; }
|
||||
|
||||
[JsonProperty("id")] public string Id { get; }
|
||||
[JsonProperty("id")] public string? Id { get; }
|
||||
|
||||
[JsonProperty("accesskey")] public string AccessKey { get; }
|
||||
[JsonProperty("accesskey")] public string? AccessKey { get; }
|
||||
|
||||
public GetLyricRequest(string id, string accessKey)
|
||||
public GetLyricRequest(string? id, string? accessKey)
|
||||
{
|
||||
UnknownParameters1 = 1;
|
||||
UnknownParameters2 = "iphone";
|
@ -2,7 +2,7 @@
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
||||
{
|
||||
public class SongSearchRequest
|
||||
{
|
@ -1,26 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.KuGou.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel
|
||||
{
|
||||
public class SongSearchResponse
|
||||
{
|
||||
[JsonProperty("status")] public int Status { get; set; }
|
||||
|
||||
[JsonProperty("data")] public SongSearchResponseInnerData Data { get; set; }
|
||||
[JsonProperty("data")] public SongSearchResponseInnerData? Data { get; set; }
|
||||
|
||||
[JsonProperty("error_code")] public int ErrorCode { get; set; }
|
||||
|
||||
[JsonProperty("error_msg")] public string ErrorMessage { get; set; }
|
||||
[JsonProperty("error_msg")] public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class SongSearchResponseInnerData
|
||||
{
|
||||
[JsonProperty("lists")] public List<SongSearchResponseSongDetail> List { get; set; }
|
||||
[JsonProperty("lists")] public List<SongSearchResponseSongDetail>? List { get; set; }
|
||||
}
|
||||
|
||||
public class SongSearchResponseSongDetail
|
||||
{
|
||||
public string FileHash { get; set; }
|
||||
public string? FileHash { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.Lyrics.Providers.KuGou.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuGou
|
||||
{
|
||||
public class KuGouLyricsProvider : LyricsProvider
|
||||
{
|
||||
public override string DownloaderName => InternalLyricsProviderNames.KuGou;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly ILyricsItemCollectionFactory _lyricsItemCollectionFactory;
|
||||
private readonly GlobalOptions _options;
|
||||
|
||||
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 KuGouLyricsProvider(IWarpHttpClient warpHttpClient,
|
||||
ILyricsItemCollectionFactory lyricsItemCollectionFactory,
|
||||
IOptions<GlobalOptions> options)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricsItemCollectionFactory = lyricsItemCollectionFactory;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
||||
{
|
||||
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(KuGouSearchMusicUrl,
|
||||
new SongSearchRequest(args.SongName, args.Artist, _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth));
|
||||
|
||||
ValidateSongSearchResponse(searchResult, args);
|
||||
|
||||
// 获得特殊的 AccessToken 与 Id,真正请求歌词数据。
|
||||
var accessKeyResponse = await _warpHttpClient.GetAsync<GetLyricAccessKeyResponse>(KuGouGetLyricAccessKeyUrl,
|
||||
new GetLyricAccessKeyRequest(searchResult.Data?.List?[0].FileHash));
|
||||
|
||||
if (accessKeyResponse.AccessKeyDataObjects == null || accessKeyResponse.AccessKeyDataObjects.Count == 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||
}
|
||||
|
||||
var accessKeyObject = accessKeyResponse.AccessKeyDataObjects[0];
|
||||
return await _warpHttpClient.GetAsync(KuGouGetLyricUrl,
|
||||
new GetLyricRequest(accessKeyObject.Id, accessKeyObject.AccessKey));
|
||||
}
|
||||
|
||||
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object data, LyricsProviderArgs args)
|
||||
{
|
||||
await ValueTask.CompletedTask;
|
||||
var lyricJsonObj = JObject.Parse((data as string)!);
|
||||
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>() ?? string.Empty));
|
||||
return _lyricsItemCollectionFactory.Build(lyricText);
|
||||
}
|
||||
|
||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||
{
|
||||
if ((response.ErrorCode != 0 && response.Status != 1) || response.Data?.List?.Count == 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
||||
|
||||
public class GetLyricsRequest
|
||||
{
|
||||
[JsonProperty("musicId")] public long MusicId { get; }
|
||||
|
||||
public GetLyricsRequest(long musicId)
|
||||
{
|
||||
MusicId = musicId;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
||||
|
||||
public class GetLyricsResponse
|
||||
{
|
||||
[JsonProperty("status")] public int Status { get; set; }
|
||||
|
||||
[JsonProperty("data")] public GetLyricsResponseInnerData? Data { get; set; }
|
||||
|
||||
[JsonProperty("msg")] public string? ErrorMessage { get; set; }
|
||||
|
||||
[JsonProperty("msgs")] public string? ErrorMessage2 { get; set; }
|
||||
}
|
||||
|
||||
public class GetLyricsResponseInnerData
|
||||
{
|
||||
[JsonProperty("lrclist")] public ICollection<GetLyricsItem>? Lyrics { get; set; }
|
||||
}
|
||||
|
||||
public class GetLyricsItem
|
||||
{
|
||||
[JsonProperty("lineLyric")] public string? Text { get; set; }
|
||||
|
||||
[JsonProperty("time")] public string Position { get; set; } = null!;
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
||||
|
||||
public class SongSearchRequest
|
||||
{
|
||||
[JsonProperty("all")] public string Keyword { get; set; }
|
||||
|
||||
[JsonProperty("pn")] public int PageNumber { get; }
|
||||
|
||||
[JsonProperty("rn")] public int PageSize { get; }
|
||||
|
||||
[JsonProperty("ft")] public string Unknown1 { get; } = "music";
|
||||
|
||||
[JsonProperty("newsearch")] public string Unknown2 { get; } = "1";
|
||||
|
||||
[JsonProperty("alflac")] public string Unknown3 { get; } = "1";
|
||||
|
||||
[JsonProperty("itemset")] public string Unknown4 { get; } = "web_2013";
|
||||
|
||||
[JsonProperty("client")] public string Unknown5 { get; } = "kt";
|
||||
|
||||
[JsonProperty("cluster")] public string Unknown6 { get; } = "0";
|
||||
|
||||
[JsonProperty("vermerge")] public string Unknown7 { get; } = "1";
|
||||
|
||||
[JsonProperty("rformat")] public string Unknown8 { get; } = "json";
|
||||
|
||||
[JsonProperty("encoding")] public string Unknown9 { get; } = "utf8";
|
||||
|
||||
[JsonProperty("show_copyright_off")] public string Unknown10 { get; } = "1";
|
||||
|
||||
[JsonProperty("pcmp4")] public string Unknown11 { get; } = "1";
|
||||
|
||||
[JsonProperty("ver")] public string Unknown12 { get; } = "mbox";
|
||||
|
||||
[JsonProperty("plat")] public string Unknown13 { get; } = "pc";
|
||||
|
||||
[JsonProperty("vipver")] public string Unknown14 { get; } = "MUSIC_9.2.0.0_W6";
|
||||
|
||||
[JsonProperty("devid")] public string Unknown15 { get; } = "11404450";
|
||||
|
||||
[JsonProperty("newver")] public string Unknown16 { get; } = "1";
|
||||
|
||||
[JsonProperty("issubtitle")] public string Unknown17 { get; } = "1";
|
||||
|
||||
[JsonProperty("pcjson")] public string Unknown18 { get; } = "1";
|
||||
|
||||
public SongSearchRequest(string name, string artist, int pageNumber = 0, int pageSize = 20)
|
||||
{
|
||||
Keyword = $"{name} {artist}";
|
||||
PageNumber = pageNumber;
|
||||
PageSize = pageSize;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
||||
|
||||
public class SongSearchResponse
|
||||
{
|
||||
[JsonProperty("TOTAL")] public int TotalCount { get; set; }
|
||||
|
||||
[JsonProperty("abslist")]
|
||||
public IList<SongSearchResponseSongDetail> SongList { get; set; }
|
||||
|
||||
public long GetMatchedMusicId(string musicName, string artistName, long? duration)
|
||||
{
|
||||
var prefectMatch = SongList.FirstOrDefault(x => x.Name == musicName && x.Artist == artistName);
|
||||
if (prefectMatch != null)
|
||||
{
|
||||
return prefectMatch.MusicId;
|
||||
}
|
||||
|
||||
if (duration is null or 0)
|
||||
{
|
||||
return SongList.First().MusicId;
|
||||
}
|
||||
|
||||
return SongList.OrderBy(t => Math.Abs(t.Duration - duration.Value)).First().MusicId;
|
||||
}
|
||||
}
|
||||
|
||||
public class SongSearchResponseSongDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// 专辑名称。
|
||||
/// </summary>
|
||||
[JsonProperty("ALBUM")]
|
||||
public string Album { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌手名称。
|
||||
/// </summary>
|
||||
[JsonProperty("ARTIST")]
|
||||
public string Artist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲名称。
|
||||
/// </summary>
|
||||
[JsonProperty("SONGNAME")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的 ID。
|
||||
/// </summary>
|
||||
[JsonProperty("DC_TARGETID")]
|
||||
public long MusicId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的时间长度。
|
||||
/// </summary>
|
||||
[JsonProperty("DURATION")]
|
||||
public long Duration { get; set; }
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Common.Configuration;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.Lyrics.Providers.KuWo.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.KuWo;
|
||||
|
||||
public class KuWoLyricsProvider : LyricsProvider
|
||||
{
|
||||
public override string DownloaderName => InternalLyricsProviderNames.KuWo;
|
||||
|
||||
private const string KuWoSearchMusicUrl = @"https://search.kuwo.cn/r.s";
|
||||
private const string KuWoSearchLyricsUrl = @"https://m.kuwo.cn/newh5/singles/songinfoandlrc";
|
||||
private const string KuWoDefaultToken = "ABCDE12345";
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly GlobalOptions _options;
|
||||
|
||||
private static readonly ProductInfoHeaderValue UserAgent = new("Chrome", "81.0.4044.138");
|
||||
|
||||
public KuWoLyricsProvider(IWarpHttpClient warpHttpClient,
|
||||
IOptions<GlobalOptions> options)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async ValueTask<object> DownloadDataAsync(LyricsProviderArgs args)
|
||||
{
|
||||
var songSearchResponse = await _warpHttpClient.GetAsync<SongSearchResponse>(KuWoSearchMusicUrl,
|
||||
new SongSearchRequest(args.SongName, args.Artist,
|
||||
pageSize: _options.Provider.Lyric.GetLyricProviderOption(DownloaderName).Depth),
|
||||
op =>
|
||||
{
|
||||
op.Headers.UserAgent.Add(UserAgent);
|
||||
op.Headers.Referrer = new Uri("https://kuwo.cn");
|
||||
op.Headers.Add("csrf", KuWoDefaultToken);
|
||||
op.Headers.Add("Cookie", $"kw_token={KuWoDefaultToken}");
|
||||
});
|
||||
|
||||
ValidateSongSearchResponse(songSearchResponse, args);
|
||||
|
||||
return await _warpHttpClient.GetAsync<GetLyricsResponse>(KuWoSearchLyricsUrl,
|
||||
new GetLyricsRequest(songSearchResponse.GetMatchedMusicId(args.SongName, args.Artist, args.Duration)),
|
||||
op =>
|
||||
{
|
||||
op.Headers.UserAgent.Add(UserAgent);
|
||||
op.Headers.Referrer = new Uri("https://m.kuwo.cn/yinyue/");
|
||||
});
|
||||
}
|
||||
|
||||
protected override async ValueTask<LyricsItemCollection> GenerateLyricAsync(object lyricsObject,
|
||||
LyricsProviderArgs args)
|
||||
{
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
var lyricsResponse = (GetLyricsResponse)lyricsObject;
|
||||
if (lyricsResponse.Data?.Lyrics == null)
|
||||
{
|
||||
return new LyricsItemCollection(null);
|
||||
}
|
||||
|
||||
var items = lyricsResponse.Data.Lyrics.Select(l =>
|
||||
{
|
||||
var position = double.Parse(l.Position);
|
||||
var positionSpan = TimeSpan.FromSeconds(position);
|
||||
return new LyricsItem(positionSpan.Minutes,
|
||||
double.Parse($"{positionSpan.Seconds}.{positionSpan.Milliseconds}"), l.Text);
|
||||
});
|
||||
|
||||
var lyricsItemCollection = new LyricsItemCollection(_options.Provider.Lyric.Config);
|
||||
lyricsItemCollection.AddRange(items);
|
||||
return lyricsItemCollection;
|
||||
}
|
||||
|
||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricsProviderArgs args)
|
||||
{
|
||||
if (response.TotalCount == 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,13 +2,13 @@ using Newtonsoft.Json;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class GetLyricRequest
|
||||
{
|
||||
public GetLyricRequest(long songId)
|
||||
{
|
||||
OS = "ios";
|
||||
OS = "pc";
|
||||
Id = songId;
|
||||
Lv = Kv = Tv = Rv = -1;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class GetLyricResponse
|
||||
{
|
||||
@ -8,31 +8,31 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// 原始的歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("lrc")]
|
||||
public InnerLyric OriginalLyric { get; set; }
|
||||
public InnerLyric? OriginalLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 卡拉 OK 歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("klyric")]
|
||||
public InnerLyric KaraokeLyric { get; set; }
|
||||
public InnerLyric? KaraokeLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 如果存在翻译歌词,则本项内容为翻译歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("tlyric")]
|
||||
public InnerLyric TranslationLyric { get; set; }
|
||||
public InnerLyric? TranslationLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 如果存在罗马音歌词,则本项内容为罗马音歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("romalrc")]
|
||||
public InnerLyric RomaLyric { get; set; }
|
||||
public InnerLyric? RomaLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态码。
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public string StatusCode { get; set; }
|
||||
public string? StatusCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -40,12 +40,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// </summary>
|
||||
public class InnerLyric
|
||||
{
|
||||
[JsonProperty("version")] public string Version { get; set; }
|
||||
[JsonProperty("version")] public string? Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 具体的歌词数据。
|
||||
/// </summary>
|
||||
[JsonProperty("lyric")]
|
||||
public string Text { get; set; }
|
||||
public string? Text { get; set; }
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class GetSongDetailsRequest
|
||||
{
|
||||
public GetSongDetailsRequest(int songId)
|
||||
public GetSongDetailsRequest(long songId)
|
||||
{
|
||||
SongId = songId;
|
||||
SongIds = $"%5B{songId}%5D";
|
||||
}
|
||||
|
||||
[JsonProperty("id")] public int SongId { get; }
|
||||
[JsonProperty("id")] public long SongId { get; }
|
||||
|
||||
[JsonProperty("ids")] public string SongIds { get; }
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user