mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-09-05 04:56:52 +00:00
Compare commits
119 Commits
ZonyLrcToo
...
ZonyLrcToo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
093e2879ad | ||
![]() |
fea5e1124f | ||
![]() |
2ce7b00d14 | ||
![]() |
ad6ea732f8 | ||
![]() |
06bf733ae9 | ||
![]() |
558cca3c0e | ||
![]() |
93cc31b1e2 | ||
![]() |
dfe18d8541 | ||
![]() |
40b73c3dfa | ||
![]() |
243a0e2559 | ||
![]() |
d83bff1516 | ||
![]() |
8d946ebbd5 | ||
![]() |
4c3d554885 | ||
![]() |
14c78f9a83 | ||
![]() |
b9d6da5f72 | ||
![]() |
c8ffbc55af | ||
![]() |
b0835dcf01 | ||
![]() |
4e1c4d1519 | ||
![]() |
3d96b96edf | ||
![]() |
7f81a3edea | ||
![]() |
24624a6d21 | ||
![]() |
d860632327 | ||
![]() |
1186369d16 | ||
![]() |
b240564cf7 | ||
![]() |
6e2848a9de | ||
![]() |
6361bf9265 | ||
![]() |
383e2c5939 | ||
![]() |
1e5c41852f | ||
![]() |
41cba02833 | ||
![]() |
98c935ed93 | ||
![]() |
62d08df735 | ||
![]() |
1f312c749d | ||
![]() |
30e10f4c01 | ||
![]() |
35468cfb71 | ||
![]() |
126741f344 | ||
![]() |
22f5bc69ec | ||
![]() |
788ff38be2 | ||
![]() |
ab5f79bd50 | ||
![]() |
f935f07609 | ||
![]() |
493f48cefe | ||
![]() |
4999e4d694 | ||
![]() |
334528eca6 | ||
![]() |
e76ddb75eb | ||
![]() |
d6d384590c | ||
![]() |
949dac7ad1 | ||
![]() |
2ffd75ed1d | ||
![]() |
82af3009d3 | ||
![]() |
572e555f2a | ||
![]() |
286731b1a6 | ||
![]() |
5850614663 | ||
![]() |
c20a280a07 | ||
![]() |
55de00ec1f | ||
![]() |
bc2b314d6a | ||
![]() |
0aafe91f96 | ||
![]() |
2e71914578 | ||
![]() |
b916323986 | ||
![]() |
afe1a7013c | ||
![]() |
fb1f743365 | ||
![]() |
0e5e48cd00 | ||
![]() |
18d9c2d32c | ||
![]() |
5dbabab5a6 | ||
![]() |
bfd89e9d82 | ||
![]() |
9a029225e1 | ||
![]() |
a7ecfbe44f | ||
![]() |
f1a6eefe45 | ||
![]() |
86e341290e | ||
![]() |
0aa8ca5aef | ||
![]() |
2eef72e165 | ||
![]() |
f4b17097fd | ||
![]() |
e68d6b9cf9 | ||
![]() |
84231d9613 | ||
![]() |
8398d80a8e | ||
![]() |
991db8ab48 | ||
![]() |
3e957fdb25 | ||
![]() |
fc09839775 | ||
![]() |
8d5a45c9ef | ||
![]() |
6b538f3e0a | ||
![]() |
d7e11af3ba | ||
![]() |
f25ba90b9a | ||
![]() |
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 |
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
|
8
.github/workflows/dotnet.yml
vendored
8
.github/workflows/dotnet.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
paths:
|
||||
- "versions/**"
|
||||
- "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: 7.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
|
46
Directory.Packages.props
Normal file
46
Directory.Packages.props
Normal file
@@ -0,0 +1,46 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.0"/>
|
||||
<PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
|
||||
<PackageVersion Include="QRCoder" Version="1.4.3"/>
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0"/>
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
|
||||
<PackageVersion Include="Polly" Version="8.2.0"/>
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0"/>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageVersion Include="NetEscapades.Configuration.Yaml" Version="3.1.0"/>
|
||||
<PackageVersion Include="MusicDecrypto.Library" Version="2.3.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0"/>
|
||||
<PackageVersion Include="AutoMapper" Version="12.0.1"/>
|
||||
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.0"/>
|
||||
<PackageVersion Include="SuperSocket.WebSocket" Version="2.0.0-beta.11"/>
|
||||
<PackageVersion Include="SuperSocket.WebSocket.Server" Version="2.0.0-beta.11"/>
|
||||
|
||||
<!-- Testing Projects -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||
<PackageVersion Include="Shouldly" Version="4.2.1"/>
|
||||
<PackageVersion Include="xunit" Version="2.6.3"/>
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.5"/>
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
|
||||
|
||||
<!-- Avalonia -->
|
||||
<PackageVersion Include="Avalonia" Version="11.0.6"/>
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.6"/>
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.0.6"/>
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.6"/>
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.0.6"/>
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.6"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
BIN
README.assets/image-20230328223155280.png
Normal file
BIN
README.assets/image-20230328223155280.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
137
README.md
137
README.md
@@ -1,145 +1,30 @@
|
||||
简体中文 | [English](./docs/en_US.md)
|
||||
|
||||
## 免责声明
|
||||
# 免责声明
|
||||
|
||||
- 本工具仅作个人学习研究使用,可运行的二进制文件仅用于演示功能,不得将源码及其产物用于商业用途,否则由此造成的相关法律问题,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
- 任何单位或个人因下载使用软件所产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
- 用户明确并同意本声明条款列举的全部内容,对使用本工具可能存在的风险和相关后果将完全由用户自行承担,[本人](https://github.com/real-zony) 不承担任何法律责任。
|
||||
|
||||
## 简介
|
||||
# 简介
|
||||
|
||||
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具。
|
||||
ZonyLrcToolX 4 是一个基于 CEF 的跨平台歌词下载工具,**QQ 群:337656932**,群文件里面有详细的视频教程。
|
||||
|
||||
🚧 当前版本正在开发当中。
|
||||
🚧 当前版本正在开发当中。
|
||||
🚧 如果你想查看可以工作的代码,请切换到 dev 分支。
|
||||
|
||||
## 下载
|
||||
# 下载
|
||||
|
||||
工具会执行每日构建动作,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
|
||||
如果你要获取最新版本,请访问 **[Release](https://github.com/real-zony/ZonyLrcToolsX/releases)** 页面进行下载。
|
||||
|
||||
## 用法
|
||||
# 用法
|
||||
|
||||
Windows 用户请在软件目录当中,按住 Shift + 右键呼出菜单,然后选择 PowerShell/命令提示符/Windows 终端,根据下述说明执行命令即可。
|
||||
软件的具体使用方法已经迁移到了全新的文档站点,请跳转到 [https://docs.myzony.com](https://docs.myzony.com),里面包含完整的说明文档。
|
||||
|
||||
macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命令即可。
|
||||
|
||||
### 子命令
|
||||
|
||||
#### 歌曲下载
|
||||
|
||||
子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
|
||||
|
||||
./ZonyLrcTools.Cli.exe download -h|--help
|
||||
```
|
||||
|
||||
**例子**
|
||||
|
||||
```shell
|
||||
# 下载歌词
|
||||
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2
|
||||
# 下载专辑封面
|
||||
./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2
|
||||
```
|
||||
|
||||
#### 加密格式转换
|
||||
|
||||
子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**。
|
||||
|
||||
目前软件支持 NCM、QCM(开发中...🚧) 格式的音乐文件转换,命令如下。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe util -t=Ncm D:\CloudMusic
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
程序的所有的配置信息,都在 `config.yaml` 进行更改,下面标注了各个配置的说明。
|
||||
|
||||
其中是否开启的可选项为 `true` 或者 `false`,等同于中文的是和否。
|
||||
|
||||
```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 时禁用。
|
||||
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' # 歌词文件的编码格式。
|
||||
```
|
||||
|
||||
#### 支持的编码格式
|
||||
|
||||
详细信息请参考: [MSDN Encoding 列表](https://learn.microsoft.com/en-us/dotnet/api/System.Text.Encoding.GetEncodings?view=net-6.0#examples),使用 `identifier and name` 作为参数值填入 `config.yaml` 文件当中的 `fileEncoding`。
|
||||
|
||||
### 屏蔽字典
|
||||
|
||||
屏蔽字典适用于网易云音乐歌词下载,针对某些单词,网易云音乐使用了 * 号进行屏蔽,这个时候可以使用屏蔽字典,设置歌曲名的关键词替换。例如有一首歌曲叫做 *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"/>
|
||||
|
||||
## Star History
|
||||
# Star History
|
||||
|
||||
[](https://star-history.com/#real-zony/ZonyLrcToolsX&Timeline)
|
||||
|
||||
## 路线图
|
||||
|
||||
- [x] 支持跨平台的 CLI 工具。
|
||||
- [ ] 基于 Web GUI 的操作站点。
|
||||
- [ ] 支持插件系统(Lua 引擎)。
|
||||
|
@@ -11,7 +11,11 @@ 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.LocalServer", "src\ZonyLrcTools.LocalServer\ZonyLrcTools.LocalServer.csproj", "{2875A08A-FFD6-4863-B815-5384DCFE88FC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Common", "src\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj", "{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Desktop", "src\ZonyLrcTools.Desktop\ZonyLrcTools.Desktop.csproj", "{DC92902B-4303-4E43-AFB3-3F93FD3986AD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -27,10 +31,18 @@ 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
|
||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC92902B-4303-4E43-AFB3-3F93FD3986AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -41,6 +53,8 @@ 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}
|
||||
{2875A08A-FFD6-4863-B815-5384DCFE88FC} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||
{9B42E4CA-61AA-4798-8D2B-2D8A7035EB67} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||
{DC92902B-4303-4E43-AFB3-3F93FD3986AD} = {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,50 +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 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 <
|
||||
@@ -59,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; }
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Updater.JsonModel;
|
||||
|
||||
public class NewVersionItem
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public NewVersionItemType ItemType { get; set; }
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Updater.JsonModel;
|
||||
|
||||
public enum NewVersionItemType
|
||||
{
|
||||
Important,
|
||||
NormalBinaryFile
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Updater.JsonModel;
|
||||
|
||||
public class NewVersionResponse
|
||||
{
|
||||
public Version NewVersion { get; set; }
|
||||
|
||||
public string NewVersionDescription { get; set; }
|
||||
|
||||
public List<NewVersionItem> Items { get; set; }
|
||||
|
||||
public DateTime UpdateTime { 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,21 @@ 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())
|
||||
.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,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyVersion>0.0.0.1</AssemblyVersion>
|
||||
<FileVersion>0.0.0.1</FileVersion>
|
||||
@@ -10,37 +10,34 @@
|
||||
</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="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"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="appsettings.json" />
|
||||
<None Remove="Resources\error_msg.json" />
|
||||
<None Remove="appsettings.json"/>
|
||||
<None Remove="Resources\error_msg.json"/>
|
||||
<Content Include="Resources\error_msg.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="BlockWords.json" />
|
||||
<None Remove="BlockWords.json"/>
|
||||
<Content Include="BlockWords.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="config.yaml" />
|
||||
<None Remove="config.yaml"/>
|
||||
<Content Include="config.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -4,12 +4,15 @@ globalOption:
|
||||
- '*.mp3'
|
||||
- '*.flac'
|
||||
- '*.wav'
|
||||
- '*.m4a'
|
||||
- '*.ogg'
|
||||
- '*.opus'
|
||||
# 网络代理服务设置,仅支持 HTTP 代理。
|
||||
networkOptions:
|
||||
isEnable: false # 是否启用代理。
|
||||
ip: 127.0.0.1 # 代理服务 IP 地址。
|
||||
port: 4780 # 代理服务端口号。
|
||||
updateUrl: https://api.zony.me/lrc-tools/update # 更新检查地址。
|
||||
updateUrl: https://api.myzony.com/lrc-tools/update # 更新检查地址。
|
||||
|
||||
# 下载器的相关参数配置。
|
||||
provider:
|
||||
@@ -36,13 +39,18 @@ globalOption:
|
||||
plugin:
|
||||
- name: NetEase # 基于网易云音乐的歌词下载器。
|
||||
priority: 1 # 优先级,升序排列,改为 -1 时禁用。
|
||||
depth: 30 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
||||
depth: 10 # 搜索深度,值越大搜索结果越多,但搜索时间越长。
|
||||
additional:
|
||||
isEnableRomanOutput: false # 是否启用罗马音输出,本参数仅当对应歌曲有罗马音歌词时有效。
|
||||
- name: QQ # 基于 QQ 音乐的歌词下载器。
|
||||
priority: 2
|
||||
# depth: 10 # 暂时不支持。
|
||||
# depth: 10 # 暂时不支持。
|
||||
- name: KuGou # 基于酷狗音乐的歌词下载器。
|
||||
priority: 3
|
||||
depth: 10
|
||||
- name: KuWo # 基于酷我音乐的歌词下载器。
|
||||
priority: 4
|
||||
depth: 10
|
||||
# 歌词下载的一些共有配置参数。
|
||||
config:
|
||||
isOneLine: true # 双语歌词是否合并为一行展示。
|
||||
|
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 时是禁用。
|
@@ -1,4 +1,4 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
namespace ZonyLrcTools.Common.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具网络相关的设定。
|
||||
@@ -13,7 +13,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
/// <summary>
|
||||
/// 代理服务器的 Ip。
|
||||
/// </summary>
|
||||
public string Ip { get; set; }
|
||||
public string Ip { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 代理服务器的 端口。
|
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!;
|
||||
}
|
11
src/ZonyLrcTools.Common/Configuration/TagInfoOptions.cs
Normal file
11
src/ZonyLrcTools.Common/Configuration/TagInfoOptions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ZonyLrcTools.Common.Configuration;
|
||||
|
||||
public class TagInfoOptions
|
||||
{
|
||||
public IEnumerable<TagInfoProviderOptions> Plugin { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 屏蔽词功能相关配置。
|
||||
/// </summary>
|
||||
public BlockWordOptions BlockWord { get; set; } = null!;
|
||||
}
|
@@ -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.GetSection("globalOption"));
|
||||
|
||||
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"/> 限定并行度。
|
22
src/ZonyLrcTools.Common/Lyrics/ILyricsDownloader.cs
Normal file
22
src/ZonyLrcTools.Common/Lyrics/ILyricsDownloader.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace ZonyLrcTools.Common.Lyrics;
|
||||
|
||||
/// <summary>
|
||||
/// 歌词下载核心逻辑的接口定义。
|
||||
/// </summary>
|
||||
public interface ILyricsDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用给定的歌词信息下载歌词,并输出文件到指定的路径。
|
||||
/// </summary>
|
||||
/// <param name="needDownloadMusicInfos">需要下载的歌词信息。</param>
|
||||
/// <param name="parallelCount">下载线程/并发量。</param>
|
||||
/// <param name="cancellationToken">任务取消标记。</param>
|
||||
Task DownloadAsync(List<MusicInfo> needDownloadMusicInfos,
|
||||
int parallelCount = 2,
|
||||
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>
|
||||
/// 换行符格式定义。
|
159
src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs
Normal file
159
src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
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;
|
||||
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _logger.InfoAsync("开始下载歌词文件数据...");
|
||||
|
||||
if (parallelCount <= 0)
|
||||
{
|
||||
parallelCount = 1;
|
||||
}
|
||||
|
||||
var warpTask = new WarpTask(parallelCount);
|
||||
var downloadTasks = needDownloadMusicInfos.Select(info =>
|
||||
warpTask.RunAsync(() =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Try to download lyrics from all available providers.
|
||||
foreach (var lyricsProvider in AvailableProviders)
|
||||
{
|
||||
await DownloadAndWriteLyricsAsync(lyricsProvider, info);
|
||||
|
||||
if (info.IsSuccessful)
|
||||
{
|
||||
_logger.LogSuccessful(info);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, cancellationToken), cancellationToken));
|
||||
|
||||
await Task.WhenAll(downloadTasks);
|
||||
|
||||
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 KuGourLyricsProvider : 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 KuGourLyricsProvider(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,6 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class GetSongDetailsRequest
|
||||
{
|
@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class SongSearchRequest
|
||||
{
|
||||
@@ -45,22 +45,19 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
|
||||
[JsonProperty("crypto")] public string Crypto { get; set; } = "weapi";
|
||||
|
||||
public SongSearchRequest()
|
||||
public SongSearchRequest(string musicName, string artistName, int limit = 10)
|
||||
{
|
||||
CsrfToken = string.Empty;
|
||||
Type = 1;
|
||||
Offset = 0;
|
||||
IsTotal = true;
|
||||
Limit = 10;
|
||||
}
|
||||
|
||||
public SongSearchRequest(string musicName, string artistName, int limit = 10) : this()
|
||||
{
|
||||
// Remove all the brackets and the content inside them.
|
||||
var regex = new Regex(@"\([^)]*\)");
|
||||
musicName = regex.Replace(musicName, string.Empty);
|
||||
|
||||
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
|
||||
SearchKey = $"{musicName}+{artistName}";
|
||||
Limit = limit;
|
||||
}
|
||||
}
|
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
namespace ZonyLrcTools.Common.Lyrics.Providers.NetEase.JsonModel
|
||||
{
|
||||
public class SongSearchResponse
|
||||
{
|
||||
[JsonProperty("result")] public InnerListItemModel Items { get; set; }
|
||||
[JsonProperty("result")] public InnerListItemModel Items { get; set; } = null!;
|
||||
|
||||
[JsonProperty("code")] public int StatusCode { get; set; }
|
||||
|
||||
@@ -30,7 +27,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
|
||||
public class InnerListItemModel
|
||||
{
|
||||
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; }
|
||||
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; } = null!;
|
||||
|
||||
[JsonProperty("songCount")] public int SongCount { get; set; }
|
||||
}
|
||||
@@ -41,7 +38,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// 歌曲的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的 Sid (Song Id)。
|
||||
@@ -53,13 +50,13 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// 歌曲的演唱者。
|
||||
/// </summary>
|
||||
[JsonProperty("artists")]
|
||||
public IList<SongArtistModel> Artists { get; set; }
|
||||
public IList<SongArtistModel>? Artists { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的专辑信息。
|
||||
/// </summary>
|
||||
[JsonProperty("album")]
|
||||
public SongAlbumModel Album { get; set; }
|
||||
public SongAlbumModel? Album { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的实际长度。
|
||||
@@ -74,7 +71,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// 歌手/艺术家的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class SongAlbumModel
|
||||
@@ -83,12 +80,12 @@ namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
/// 专辑的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 专辑图像的 Url 地址。
|
||||
/// </summary>
|
||||
[JsonProperty("img1v1Url")]
|
||||
public string PictureUrl { get; set; }
|
||||
public string? PictureUrl { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user