mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-07-01 04:00:42 +00:00
feat: Reinitialize the Repository.
重新初始化仓库。
This commit is contained in:
parent
a754f419b2
commit
8e1e61764c
233
.gitignore
vendored
233
.gitignore
vendored
@ -1,7 +1,23 @@
|
||||
# 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
|
||||
@ -10,6 +26,9 @@
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
@ -17,42 +36,62 @@
|
||||
[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 cache/options directory
|
||||
# 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
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
# 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
|
||||
*_i.h
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
@ -62,6 +101,7 @@ artifacts/
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
@ -90,6 +130,9 @@ ipch/
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
@ -101,15 +144,25 @@ _ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# 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
|
||||
@ -141,9 +194,9 @@ publish/
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# 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
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
@ -153,13 +206,15 @@ PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
@ -176,12 +231,15 @@ 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/
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
@ -192,9 +250,12 @@ ClientBin/
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
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/
|
||||
@ -209,15 +270,22 @@ _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/
|
||||
@ -227,6 +295,7 @@ FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
@ -234,6 +303,9 @@ FakesAssemblies/
|
||||
# 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
|
||||
@ -249,13 +321,132 @@ paket-files/
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
# 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/
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Zony
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
English | [简体中文](./zh_CN.md)
|
||||
|
||||
## Overview
|
||||
ZonyLrcToolX 2.0 is a cross-platform lyric downlaod tool based on CEF.
|
||||
|
||||
🚧 The current version is under development.
|
||||
🚧 If you want to see the working code, please switch to the 1.0 branch.
|
||||
## Usage
|
||||
|
||||
## Donation
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Supports cross-platform CLI tools.
|
||||
- [ ] Web GUI based site (local).
|
||||
- [ ] Support plug-in system (Lua Engine).
|
41
ZonyLrcTools.sln
Normal file
41
ZonyLrcTools.sln
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29009.5
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C29FB05C-54B1-4020-94D2-87E192EB2F98}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ui", "ui", "{D6E0DAF5-8171-44C0-817E-2FF9CF574E4F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Cli", "src\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj", "{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZonyLrcTools.Tests", "tests\ZonyLrcTools.Tests\ZonyLrcTools.Tests.csproj", "{FFBD3200-568F-455B-8390-5E76C51D522C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FFBD3200-568F-455B-8390-5E76C51D522C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FFBD3200-568F-455B-8390-5E76C51D522C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FFBD3200-568F-455B-8390-5E76C51D522C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7A6191C3-CC25-4732-885C-F4DD32F9E412}
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{55D74323-ABFA-4A73-A3BF-F3E8D5DE6DE8} = {C29FB05C-54B1-4020-94D2-87E192EB2F98}
|
||||
{FFBD3200-568F-455B-8390-5E76C51D522C} = {AF8ADB2F-E46C-4DEE-8316-652D9FE1A69B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
4
ZonyLrcTools.sln.DotSettings
Normal file
4
ZonyLrcTools.sln.DotSettings
Normal file
@ -0,0 +1,4 @@
|
||||
<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/=Gour/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zony/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
196
src/ZonyLrcTools.Cli/Commands/DownloadCommand.cs
Normal file
196
src/ZonyLrcTools.Cli/Commands/DownloadCommand.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Album;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Extensions;
|
||||
using ZonyLrcTools.Cli.Infrastructure.IO;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Lyric;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Threading;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
[Command("download", Description = "下载歌词文件或专辑图像。")]
|
||||
public class DownloadCommand : ToolCommandBase
|
||||
{
|
||||
private readonly ILogger<DownloadCommand> _logger;
|
||||
private readonly IFileScanner _fileScanner;
|
||||
private readonly ITagLoader _tagLoader;
|
||||
private readonly IEnumerable<ILyricDownloader> _lyricDownloaderList;
|
||||
private readonly IEnumerable<IAlbumDownloader> _albumDownloaderList;
|
||||
|
||||
private readonly ToolOptions _options;
|
||||
|
||||
public DownloadCommand(ILogger<DownloadCommand> logger,
|
||||
IFileScanner fileScanner,
|
||||
IOptions<ToolOptions> options,
|
||||
ITagLoader tagLoader,
|
||||
IEnumerable<ILyricDownloader> lyricDownloaderList,
|
||||
IEnumerable<IAlbumDownloader> albumDownloaderList)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileScanner = fileScanner;
|
||||
_tagLoader = tagLoader;
|
||||
_lyricDownloaderList = lyricDownloaderList;
|
||||
_albumDownloaderList = albumDownloaderList;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
#region > Options <
|
||||
|
||||
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
|
||||
[DirectoryExists]
|
||||
public string Directory { get; set; }
|
||||
|
||||
[Option("-l|--lyric", CommandOptionType.NoValue, Description = "指定程序需要下载歌词文件。")]
|
||||
public bool DownloadLyric { get; set; }
|
||||
|
||||
[Option("-a|--album", CommandOptionType.NoValue, Description = "指定程序需要下载专辑图像。")]
|
||||
public bool DownloadAlbum { get; set; }
|
||||
|
||||
[Option("-n|--number", CommandOptionType.SingleValue, Description = "指定下载时候的线程数量。(默认值 2)")]
|
||||
public int ParallelNumber { get; set; } = 2;
|
||||
|
||||
#endregion
|
||||
|
||||
public override List<string> CreateArgs() => new();
|
||||
|
||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
var files = await ScanMusicFilesAsync();
|
||||
var musicInfos = await LoadMusicInfoAsync(files);
|
||||
|
||||
if (DownloadLyric)
|
||||
{
|
||||
await DownloadLyricFilesAsync(musicInfos);
|
||||
}
|
||||
|
||||
if (DownloadAlbum)
|
||||
{
|
||||
await DownloadAlbumAsync(musicInfos);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ScanMusicFilesAsync()
|
||||
{
|
||||
var files = (await _fileScanner.ScanAsync(Directory, _options.SupportFileExtensions.Split(';')))
|
||||
.SelectMany(t => t.FilePaths)
|
||||
.ToList();
|
||||
|
||||
if (files.Count == 0)
|
||||
{
|
||||
_logger.LogError("没有找到任何音乐文件。");
|
||||
throw new ErrorCodeException(ErrorCodes.NoFilesWereScanned);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"已经扫描到了 {files.Count} 个音乐文件。");
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private async Task<ImmutableList<MusicInfo>> LoadMusicInfoAsync(IReadOnlyCollection<string> files)
|
||||
{
|
||||
_logger.LogInformation("开始加载音乐文件的标签信息...");
|
||||
|
||||
var warpTask = new WarpTask(ParallelNumber);
|
||||
var warpTaskList = files.Select(file => warpTask.RunAsync(() => Task.Run(async () => await _tagLoader.LoadTagAsync(file))));
|
||||
var result = await Task.WhenAll(warpTaskList);
|
||||
|
||||
_logger.LogInformation($"已成功加载 {files.Count} 个音乐文件的标签信息。");
|
||||
|
||||
return result.ToImmutableList();
|
||||
}
|
||||
|
||||
#region > 歌词下载逻辑 <
|
||||
|
||||
private async ValueTask DownloadLyricFilesAsync(ImmutableList<MusicInfo> musicInfos)
|
||||
{
|
||||
_logger.LogInformation("开始下载歌词文件数据...");
|
||||
|
||||
var downloader = _lyricDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalLyricDownloaderNames.NetEase);
|
||||
var warpTask = new WarpTask(ParallelNumber);
|
||||
var warpTaskList = musicInfos.Select(info =>
|
||||
warpTask.RunAsync(() => Task.Run(async () => await DownloadLyricTaskLogicAsync(downloader, info))));
|
||||
|
||||
await Task.WhenAll(warpTaskList);
|
||||
|
||||
_logger.LogInformation($"歌词数据下载完成,成功: {musicInfos.Count} 失败{0}。");
|
||||
}
|
||||
|
||||
private async Task DownloadLyricTaskLogicAsync(ILyricDownloader downloader, MusicInfo info)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lyric = await downloader.DownloadAsync(info.Name, info.Artist);
|
||||
var filePath = Path.Combine(Path.GetDirectoryName(info.FilePath)!, $"{Path.GetFileNameWithoutExtension(info.FilePath)}.lrc");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (lyric.IsPruneMusic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await using var sw = new StreamWriter(stream);
|
||||
await sw.WriteAsync(lyric.ToString());
|
||||
await sw.FlushAsync();
|
||||
}
|
||||
catch (ErrorCodeException ex)
|
||||
{
|
||||
_logger.LogWarningInfo(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region > 专辑图像下载逻辑 <
|
||||
|
||||
private async ValueTask DownloadAlbumAsync(ImmutableList<MusicInfo> musicInfos)
|
||||
{
|
||||
_logger.LogInformation("开始下载专辑图像数据...");
|
||||
|
||||
var downloader = _albumDownloaderList.FirstOrDefault(d => d.DownloaderName == InternalAlbumDownloaderNames.NetEase);
|
||||
var warpTask = new WarpTask(ParallelNumber);
|
||||
var warpTaskList = musicInfos.Select(info =>
|
||||
warpTask.RunAsync(() => Task.Run(async () => await DownloadAlbumTaskLogicAsync(downloader, info))));
|
||||
|
||||
await Task.WhenAll(warpTaskList);
|
||||
|
||||
_logger.LogInformation($"专辑数据下载完成,成功: {musicInfos.Count} 失败{0}。");
|
||||
}
|
||||
|
||||
private async Task DownloadAlbumTaskLogicAsync(IAlbumDownloader downloader, MusicInfo info)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
47
src/ZonyLrcTools.Cli/Commands/ScanCommand.cs
Normal file
47
src/ZonyLrcTools.Cli/Commands/ScanCommand.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure.IO;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
[Command("scan", Description = "扫描指定目录下符合条件的音乐文件数量。")]
|
||||
public class ScanCommand : ToolCommandBase
|
||||
{
|
||||
private readonly IFileScanner _fileScanner;
|
||||
private readonly ToolOptions _options;
|
||||
private readonly ILogger<ScanCommand> _logger;
|
||||
|
||||
public ScanCommand(IFileScanner fileScanner,
|
||||
IOptions<ToolOptions> options,
|
||||
ILogger<ScanCommand> logger)
|
||||
{
|
||||
_fileScanner = fileScanner;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
[Option("-d|--dir", CommandOptionType.SingleValue, Description = "指定需要扫描的目录。")]
|
||||
[DirectoryExists]
|
||||
public string DirectoryPath { get; set; }
|
||||
|
||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
var result = await _fileScanner.ScanAsync(
|
||||
DirectoryPath,
|
||||
_options.SupportFileExtensions.Split(';'));
|
||||
|
||||
_logger.LogInformation($"目录扫描完成,共扫描到 {result.Sum(f => f.FilePaths.Count)} 个音乐文件。");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override List<string> CreateArgs()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
113
src/ZonyLrcTools.Cli/Commands/ToolCommand.cs
Normal file
113
src/ZonyLrcTools.Cli/Commands/ToolCommand.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Extensions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
[Command("lyric-tool")]
|
||||
[Subcommand(typeof(ScanCommand), typeof(DownloadCommand))]
|
||||
public class ToolCommand : ToolCommandBase
|
||||
{
|
||||
public override List<string> CreateArgs()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
ConfigureLogger();
|
||||
ConfigureErrorMessage();
|
||||
|
||||
try
|
||||
{
|
||||
return await BuildHostedService(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandleException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
#region > 程序初始化配置 <
|
||||
|
||||
private static void ConfigureErrorMessage() => ErrorCodeHelper.LoadErrorMessage();
|
||||
|
||||
private static void ConfigureLogger()
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
#if DEBUG
|
||||
.MinimumLevel.Debug()
|
||||
#else
|
||||
.MinimumLevel.Information()
|
||||
#endif
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Async(c => c.Console())
|
||||
.WriteTo.Logger(lc =>
|
||||
{
|
||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning)
|
||||
.WriteTo.Async(c => c.File("Logs/warnings.txt"));
|
||||
})
|
||||
.WriteTo.Logger(lc =>
|
||||
{
|
||||
lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error)
|
||||
.WriteTo.Async(c => c.File("Logs/errors.txt"));
|
||||
})
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
private static Task<int> BuildHostedService(string[] args)
|
||||
{
|
||||
return new HostBuilder()
|
||||
.ConfigureLogging(builder => builder.AddSerilog())
|
||||
.ConfigureHostConfiguration(builder =>
|
||||
{
|
||||
builder
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json");
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
services.AddSingleton(PhysicalConsole.Singleton);
|
||||
services.BeginAutoDependencyInject<ToolCommand>();
|
||||
services.ConfigureConfiguration();
|
||||
services.ConfigureToolService();
|
||||
})
|
||||
.RunCommandLineApplicationAsync<ToolCommand>(args);
|
||||
}
|
||||
|
||||
private static int HandleException(Exception ex)
|
||||
{
|
||||
switch (ex)
|
||||
{
|
||||
case ErrorCodeException exception:
|
||||
Log.Logger.Error($"出现了未处理的异常,错误代码: {exception.ErrorCode},错误信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}\n调用栈:\n{exception.StackTrace}");
|
||||
Environment.Exit(exception.ErrorCode);
|
||||
return exception.ErrorCode;
|
||||
case Exception unknownException:
|
||||
Log.Logger.Error($"出现了未处理的异常: {unknownException.Message}\n调用栈:\n{unknownException.StackTrace}");
|
||||
Environment.Exit(-1);
|
||||
return 1;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
17
src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs
Normal file
17
src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
[HelpOption("--help|-h", Description = "欢迎使用 ZonyLrcToolsX Command Line Interface。")]
|
||||
public abstract class ToolCommandBase
|
||||
{
|
||||
public abstract List<string> CreateArgs();
|
||||
|
||||
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
20
src/ZonyLrcTools.Cli/Commands/UtilityCommand.cs
Normal file
20
src/ZonyLrcTools.Cli/Commands/UtilityCommand.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具类相关命令。
|
||||
/// </summary>
|
||||
[Command("util", Description = "提供常用的工具类功能。")]
|
||||
public class UtilityCommand : ToolCommandBase
|
||||
{
|
||||
public override List<string> CreateArgs() => new();
|
||||
|
||||
protected override Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||
{
|
||||
return base.OnExecuteAsync(app);
|
||||
}
|
||||
}
|
||||
}
|
29
src/ZonyLrcTools.Cli/Config/ToolOptions.cs
Normal file
29
src/ZonyLrcTools.Cli/Config/ToolOptions.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using ZonyLrcTools.Cli.Infrastructure.Lyric;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Network;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Config
|
||||
{
|
||||
public class ToolOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的音乐文件后缀集合,以 ; 进行分隔。
|
||||
/// </summary>
|
||||
public string SupportFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌词下载相关的配置信息。
|
||||
/// </summary>
|
||||
public LyricItemCollectionOption LyricOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签加载器的加载配置项。
|
||||
/// </summary>
|
||||
public TagInfoProviderOptions TagInfoProviderOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 网络代理相关的配置信息。
|
||||
/// </summary>
|
||||
public NetworkOptions NetworkOptions { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Album
|
||||
{
|
||||
/// <summary>
|
||||
/// 专辑封面下载器,用于匹配并下载歌曲的专辑封面。
|
||||
/// </summary>
|
||||
public interface IAlbumDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载器的名称。
|
||||
/// </summary>
|
||||
string DownloaderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 下载专辑封面。
|
||||
/// </summary>
|
||||
/// <param name="songName">歌曲的名称。</param>
|
||||
/// <param name="artist">歌曲的作者。</param>
|
||||
/// <returns>专辑封面的图像数据。</returns>
|
||||
ValueTask<byte[]> DownloadAsync(string songName, string artist);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Album
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义了程序默认提供的专辑图像下载器。
|
||||
/// </summary>
|
||||
public static class InternalAlbumDownloaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 网易云音乐专辑图像下载器。
|
||||
/// </summary>
|
||||
public const string NetEase = nameof(NetEase);
|
||||
|
||||
/// <summary>
|
||||
/// QQ 音乐专辑图像下载器。
|
||||
/// </summary>
|
||||
public const string QQ = nameof(QQ);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Album.NetEase
|
||||
{
|
||||
public class NetEaseAlbumDownloader : IAlbumDownloader, ITransientDependency
|
||||
{
|
||||
public string DownloaderName => InternalAlbumDownloaderNames.NetEase;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly Action<HttpRequestMessage> _defaultOption;
|
||||
|
||||
private const string SearchMusicApi = @"https://music.163.com/api/search/get/web";
|
||||
private const string GetMusicInfoApi = @"https://music.163.com/api/song/detail";
|
||||
private const string DefaultReferer = @"https://music.163.com";
|
||||
|
||||
public NetEaseAlbumDownloader(IWarpHttpClient warpHttpClient)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_defaultOption = message =>
|
||||
{
|
||||
message.Headers.Referrer = new Uri(DefaultReferer);
|
||||
|
||||
if (message.Content != null)
|
||||
{
|
||||
message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> DownloadAsync(string songName, string artist)
|
||||
{
|
||||
var requestParameter = new SongSearchRequest(songName, artist);
|
||||
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
|
||||
SearchMusicApi,
|
||||
requestParameter,
|
||||
true,
|
||||
_defaultOption);
|
||||
|
||||
if (searchResult is not {StatusCode: 200} || searchResult.Items?.SongCount <= 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong);
|
||||
}
|
||||
|
||||
var songDetailJsonStr = await _warpHttpClient.GetAsync(
|
||||
GetMusicInfoApi,
|
||||
new GetSongDetailsRequest(searchResult.GetFirstSongId()),
|
||||
_defaultOption);
|
||||
|
||||
var url = JObject.Parse(songDetailJsonStr).SelectToken("$.songs[0].album.picUrl")?.Value<string>();
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal);
|
||||
}
|
||||
|
||||
return await new HttpClient().GetByteArrayAsync(new Uri(url));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Album.QQMusic
|
||||
{
|
||||
public class QQMusicAlbumDownloader : IAlbumDownloader, ITransientDependency
|
||||
{
|
||||
public string DownloaderName => InternalAlbumDownloaderNames.QQ;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly Action<HttpRequestMessage> _defaultOption;
|
||||
|
||||
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)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_defaultOption = message =>
|
||||
{
|
||||
message.Headers.Referrer = new Uri(DefaultReferer);
|
||||
|
||||
if (message.Content != null)
|
||||
{
|
||||
message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> DownloadAsync(string songName, string artist)
|
||||
{
|
||||
var requestParameter = new SongSearchRequest(songName, artist);
|
||||
var searchResult = await _warpHttpClient.GetAsync<SongSearchResponse>(
|
||||
SearchApi,
|
||||
requestParameter);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Extensions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
{
|
||||
public static class AutoDependencyInjectExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始进行自动依赖注入。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 会根据实现了 <see cref="ITransientDependency"/> 或 <see cref="ISingletonDependency"/> 的接口进行自动注入。
|
||||
/// </remarks>
|
||||
/// <param name="services">服务定义集合。</param>
|
||||
/// <typeparam name="TAssemblyType">需要注入的任意类型。</typeparam>
|
||||
public static IServiceCollection BeginAutoDependencyInject<TAssemblyType>(this IServiceCollection services)
|
||||
{
|
||||
var allTypes = typeof(TAssemblyType).Assembly
|
||||
.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
||||
.ToArray();
|
||||
|
||||
var transientTypes = allTypes.Where(t => typeof(ITransientDependency).IsAssignableFrom(t));
|
||||
var singletonTypes = allTypes.Where(t => typeof(ISingletonDependency).IsAssignableFrom(t));
|
||||
|
||||
transientTypes.Foreach(t =>
|
||||
{
|
||||
foreach (var exposedService in GetDefaultExposedTypes(t))
|
||||
{
|
||||
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Transient));
|
||||
}
|
||||
});
|
||||
|
||||
singletonTypes.Foreach(t =>
|
||||
{
|
||||
foreach (var exposedService in GetDefaultExposedTypes(t))
|
||||
{
|
||||
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Singleton));
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static List<Type> GetDefaultExposedTypes(Type type)
|
||||
{
|
||||
var serviceTypes = new List<Type>();
|
||||
|
||||
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
|
||||
{
|
||||
var interfaceName = interfaceType.Name;
|
||||
|
||||
if (interfaceName.StartsWith("I"))
|
||||
{
|
||||
interfaceName = interfaceName.Substring(1, interfaceName.Length - 1);
|
||||
}
|
||||
|
||||
if (type.Name.EndsWith(interfaceName))
|
||||
{
|
||||
serviceTypes.Add(interfaceType);
|
||||
serviceTypes.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
return serviceTypes;
|
||||
}
|
||||
|
||||
public static ServiceDescriptor CreateServiceDescriptor(Type implementationType,
|
||||
Type exposingServiceType,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
return new ServiceDescriptor(exposingServiceType, implementationType, lifetime);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
{
|
||||
/// <summary>
|
||||
/// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。
|
||||
/// </summary>
|
||||
public interface ISingletonDependency
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
{
|
||||
/// <summary>
|
||||
/// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。
|
||||
/// </summary>
|
||||
public interface ITransientDependency
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
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;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.DependencyInject
|
||||
{
|
||||
/// <summary>
|
||||
/// Service 注入的扩展方法。
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置工具会用到的服务。
|
||||
/// </summary>
|
||||
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant)
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
var option = provider.GetRequiredService<IOptions<ToolOptions>>().Value;
|
||||
|
||||
return new HttpClientHandler
|
||||
{
|
||||
UseProxy = option.NetworkOptions.Enable,
|
||||
Proxy = new WebProxy($"{option.NetworkOptions.ProxyIp}:{option.NetworkOptions.ProxyPort}")
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置工具关联的配置信息(<see cref="IConfiguration"/>)。
|
||||
/// </summary>
|
||||
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
services.Configure<ToolOptions>(configuration.GetSection("ToolOption"));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 带错误码的异常实现。
|
||||
/// </summary>
|
||||
public class ErrorCodeException : Exception
|
||||
{
|
||||
public int ErrorCode { get; }
|
||||
|
||||
public object AttachObject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
AttachObject = attachObj;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误码相关的帮助类。
|
||||
/// </summary>
|
||||
public static class ErrorCodeHelper
|
||||
{
|
||||
public static Dictionary<int, string> ErrorMessages { get; }
|
||||
|
||||
static ErrorCodeHelper()
|
||||
{
|
||||
ErrorMessages = new Dictionary<int, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 err_msg.json 文件加载错误信息。
|
||||
/// </summary>
|
||||
public static void LoadErrorMessage()
|
||||
{
|
||||
// 防止重复加载。
|
||||
if (ErrorMessages.Count != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "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>()));
|
||||
}
|
||||
|
||||
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];
|
||||
}
|
||||
}
|
86
src/ZonyLrcTools.Cli/Infrastructure/Exceptions/ErrorCodes.cs
Normal file
86
src/ZonyLrcTools.Cli/Infrastructure/Exceptions/ErrorCodes.cs
Normal file
@ -0,0 +1,86 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误码。
|
||||
/// </summary>
|
||||
public static class ErrorCodes
|
||||
{
|
||||
#region > 错误信息 <
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 待搜索的后缀不能为空。
|
||||
/// </summary>
|
||||
public const int FileSuffixIsEmpty = 10001;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 需要扫描的目录不存在,请确认路径是否正确。。
|
||||
/// </summary>
|
||||
public const int DirectoryNotExist = 10002;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 不能获取文件的后缀信息。
|
||||
/// </summary>
|
||||
public const int UnableToGetTheFileExtension = 10003;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 没有扫描到任何音乐文件。
|
||||
/// </summary>
|
||||
public const int NoFilesWereScanned = 10004;
|
||||
|
||||
#endregion
|
||||
|
||||
#region > 警告信息 <
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 扫描文件时出现了错误。
|
||||
/// </summary>
|
||||
public const int ScanFileError = 50001;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 歌曲名称或歌手名称均为空,无法进行搜索。
|
||||
/// </summary>
|
||||
public const int SongNameAndArtistIsNull = 50002;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 歌曲名称不能为空,无法进行搜索。
|
||||
/// </summary>
|
||||
public const int SongNameIsNull = 50003;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 下载器没有搜索到对应的歌曲信息。
|
||||
/// </summary>
|
||||
public const int NoMatchingSong = 50004;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 下载请求的返回值不合法,可能是服务端故障。
|
||||
/// </summary>
|
||||
public const int TheReturnValueIsIllegal = 50005;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 标签信息读取器为空,无法解析音乐 Tag 信息。
|
||||
/// </summary>
|
||||
public const int LoadTagInfoProviderError = 50006;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: TagLib 标签读取器出现了预期之外的异常。
|
||||
/// </summary>
|
||||
public const int TagInfoProviderLoadInfoFailed = 50007;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 服务接口限制,无法进行请求,请尝试使用代理服务器。
|
||||
/// </summary>
|
||||
public const int ServiceUnavailable = 50008;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 对目标服务器执行 Http 请求失败。
|
||||
/// </summary>
|
||||
public const int HttpRequestFailed = 50009;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: Http 请求的结果反序列化为 Json 失败。
|
||||
/// </summary>
|
||||
public const int HttpResponseConvertJsonFailed = 50010;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
24
src/ZonyLrcTools.Cli/Infrastructure/Extensions/LinqHelper.cs
Normal file
24
src/ZonyLrcTools.Cli/Infrastructure/Extensions/LinqHelper.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Linq 相关的扩展方法。
|
||||
/// </summary>
|
||||
public static class LinqHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Lambda 的形式遍历指定的迭代器。
|
||||
/// </summary>
|
||||
/// <param name="items">等待遍历的迭代器实例。</param>
|
||||
/// <param name="action">遍历时需要执行的操作。</param>
|
||||
public static void Foreach<T>(this IEnumerable<T> items, Action<T> action)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志记录相关的扩展方法。
|
||||
/// </summary>
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
logger.LogWarning($"错误代码: {errorCode}\n堆栈异常: {e?.StackTrace}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="LogLevel.Warning"/> 级别打印错误日志,并记录异常堆栈。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器的实例。</param>
|
||||
/// <param name="exception">错误码异常实例。</param>
|
||||
public static void LogWarningInfo(this ILogger logger, ErrorCodeException exception)
|
||||
{
|
||||
if (exception.ErrorCode < 50000)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"错误代码: {exception.ErrorCode},信息: {ErrorCodeHelper.GetMessage(exception.ErrorCode)}");
|
||||
sb.Append($"\n附加信息:\n {JsonConvert.SerializeObject(exception.AttachObject)}");
|
||||
logger.LogWarning(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串处理相关的工具方法。
|
||||
/// </summary>
|
||||
public static class StringHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 截断指定字符串末尾的匹配字串。
|
||||
/// </summary>
|
||||
/// <param name="string">待截断的字符串。</param>
|
||||
/// <param name="trimEndStr">需要在末尾截断的字符串。</param>
|
||||
/// <returns>截断成功的字符串实例。</returns>
|
||||
public static string TrimEnd(this string @string, string trimEndStr)
|
||||
{
|
||||
if (@string.EndsWith(trimEndStr, StringComparison.Ordinal))
|
||||
{
|
||||
return @string.Substring(0, @string.Length - trimEndStr.Length);
|
||||
}
|
||||
|
||||
return @string;
|
||||
}
|
||||
}
|
||||
}
|
70
src/ZonyLrcTools.Cli/Infrastructure/IO/FileScanner.cs
Normal file
70
src/ZonyLrcTools.Cli/Infrastructure/IO/FileScanner.cs
Normal file
@ -0,0 +1,70 @@
|
||||
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;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
{
|
||||
public class FileScanner : IFileScanner, ITransientDependency
|
||||
{
|
||||
public ILogger<FileScanner> Logger { get; set; }
|
||||
|
||||
public FileScanner()
|
||||
{
|
||||
Logger = NullLogger<FileScanner>.Instance;
|
||||
}
|
||||
|
||||
public Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions)
|
||||
{
|
||||
if (extensions == null || !extensions.Any())
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.FileSuffixIsEmpty);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.DirectoryNotExist);
|
||||
}
|
||||
|
||||
var files = new List<FileScannerResult>();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
var tempResult = new ConcurrentBag<string>();
|
||||
SearchFile(tempResult, path, extension);
|
||||
|
||||
files.Add(new FileScannerResult(
|
||||
Path.GetExtension(extension) ?? throw new ErrorCodeException(ErrorCodes.UnableToGetTheFileExtension),
|
||||
tempResult.ToList()));
|
||||
}
|
||||
|
||||
return Task.FromResult(files);
|
||||
}
|
||||
|
||||
private void SearchFile(ConcurrentBag<string> files, string folder, string extension)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(folder, extension))
|
||||
{
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(folder))
|
||||
{
|
||||
SearchFile(files, directory, extension);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogWarningWithErrorCode(ErrorCodes.ScanFileError, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
src/ZonyLrcTools.Cli/Infrastructure/IO/FileScannerResult.cs
Normal file
31
src/ZonyLrcTools.Cli/Infrastructure/IO/FileScannerResult.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件扫描结果对象。
|
||||
/// </summary>
|
||||
public class FileScannerResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前路径对应的扩展名。
|
||||
/// </summary>
|
||||
public string ExtensionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前扩展名下面的所有文件路径集合。
|
||||
/// </summary>
|
||||
public List<string> FilePaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="FileScannerResult"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="extensionName">当前路径对应的扩展名。</param>
|
||||
/// <param name="filePaths">当前扩展名下面的所有文件路径集合。</param>
|
||||
public FileScannerResult(string extensionName, List<string> filePaths)
|
||||
{
|
||||
ExtensionName = extensionName;
|
||||
FilePaths = filePaths;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
{
|
||||
public static class FileStreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将字节数据通过缓冲区的形式,写入到文件当中。
|
||||
/// </summary>
|
||||
/// <param name="fileStream">需要写入数据的文件流。</param>
|
||||
/// <param name="data">等待写入的数据。</param>
|
||||
/// <param name="bufferSize">缓冲区大小。</param>
|
||||
public static async Task WriteBytesToFileAsync(this FileStream fileStream, byte[] data, int bufferSize = 1024)
|
||||
{
|
||||
await using (fileStream)
|
||||
{
|
||||
var count = data.Length / 1024;
|
||||
var modCount = data.Length % 1024;
|
||||
if (count <= 0)
|
||||
{
|
||||
await fileStream.WriteAsync(data, 0, modCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await fileStream.WriteAsync(data, i * 1024, 1024);
|
||||
}
|
||||
|
||||
if (modCount != 0)
|
||||
{
|
||||
await fileStream.WriteAsync(data, count * 1024, modCount);
|
||||
}
|
||||
}
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/ZonyLrcTools.Cli/Infrastructure/IO/IFileScanner.cs
Normal file
18
src/ZonyLrcTools.Cli/Infrastructure/IO/IFileScanner.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐文件扫描器,用于扫描音乐文件。
|
||||
/// </summary>
|
||||
public interface IFileScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描指定路径下面的歌曲文件。
|
||||
/// </summary>
|
||||
/// <param name="path">等待扫描的路径。</param>
|
||||
/// <param name="extensions">需要搜索的歌曲后缀名。</param>
|
||||
Task<List<FileScannerResult>> ScanAsync(string path, IEnumerable<string> extensions);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌词数据下载器,用于匹配并下载歌曲的歌词。
|
||||
/// </summary>
|
||||
public interface ILyricDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载歌词数据。
|
||||
/// </summary>
|
||||
/// <param name="songName">歌曲的名称。</param>
|
||||
/// <param name="artist">歌曲的作者。</param>
|
||||
/// <returns>歌曲的歌词数据对象。</returns>
|
||||
ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist);
|
||||
|
||||
/// <summary>
|
||||
/// 下载器的名称。
|
||||
/// </summary>
|
||||
string DownloaderName { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建 <see cref="LyricItemCollection"/> 对象的工厂。
|
||||
/// </summary>
|
||||
public interface ILyricItemCollectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据指定的歌曲数据构建新的 <see cref="LyricItemCollection"/> 实例。
|
||||
/// </summary>
|
||||
/// <param name="sourceLyric">原始歌词数据。</param>
|
||||
/// <param name="translateLyric">翻译歌词数据。</param>
|
||||
/// <returns>构建完成的 <see cref="LyricItemCollection"/> 对象。</returns>
|
||||
LyricItemCollection Build(string sourceLyric, string translateLyric = null);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
public interface ILyricTextResolver
|
||||
{
|
||||
LyricItemCollection Resolve(string lyricText);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义了程序默认提供的歌词下载器。
|
||||
/// </summary>
|
||||
public static class InternalLyricDownloaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 网易云音乐歌词下载器。
|
||||
/// </summary>
|
||||
public const string NetEase = nameof(NetEase);
|
||||
|
||||
/// <summary>
|
||||
/// QQ 音乐歌词下载器。
|
||||
/// </summary>
|
||||
public const string QQ = nameof(QQ);
|
||||
|
||||
/// <summary>
|
||||
/// 酷狗音乐歌词下载器。
|
||||
/// </summary>
|
||||
public const string KuGou = nameof(KuGou);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
public KuGourLyricDownloader(IWarpHttpClient warpHttpClient,
|
||||
ILyricItemCollectionFactory lyricItemCollectionFactory)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricItemCollectionFactory = lyricItemCollectionFactory;
|
||||
}
|
||||
|
||||
protected override ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
protected override ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
23
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LineBreakType.cs
Normal file
23
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LineBreakType.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 换行符格式定义。
|
||||
/// </summary>
|
||||
public static class LineBreakType
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows 系统。
|
||||
/// </summary>
|
||||
public const string Windows = "\r\n";
|
||||
|
||||
/// <summary>
|
||||
/// macOS 系统。
|
||||
/// </summary>
|
||||
public const string MacOs = "\r";
|
||||
|
||||
/// <summary>
|
||||
/// UNIX 系统(Linux)。
|
||||
/// </summary>
|
||||
public const string Unix = "\n";
|
||||
}
|
||||
}
|
41
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricDownloader.cs
Normal file
41
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricDownloader.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌词下载器的基类,定义了歌词下载器的常规逻辑。
|
||||
/// </summary>
|
||||
public abstract class LyricDownloader : ILyricDownloader, ITransientDependency
|
||||
{
|
||||
public abstract string DownloaderName { get; }
|
||||
|
||||
public virtual async ValueTask<LyricItemCollection> DownloadAsync(string songName, string artist)
|
||||
{
|
||||
var args = new LyricDownloaderArgs(songName, artist);
|
||||
await ValidateAsync(args);
|
||||
var downloadDataBytes = await DownloadDataAsync(args);
|
||||
return await GenerateLyricAsync(downloadDataBytes);
|
||||
}
|
||||
|
||||
protected virtual ValueTask ValidateAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(args.SongName))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.SongNameIsNull, attachObj: args);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(args.SongName) && string.IsNullOrEmpty(args.Artist))
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.SongNameAndArtistIsNull, attachObj: args);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected abstract ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args);
|
||||
|
||||
protected abstract ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
public class LyricDownloaderArgs
|
||||
{
|
||||
public string SongName { get; set; }
|
||||
|
||||
public string Artist { get; set; }
|
||||
|
||||
public LyricDownloaderArgs(string songName, string artist)
|
||||
{
|
||||
SongName = songName;
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
126
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricItem.cs
Normal file
126
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricItem.cs
Normal file
@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 每一行歌词的对象。
|
||||
/// </summary>
|
||||
public class LyricItem : IComparable<LyricItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// 原始时间轴,格式类似于 [01:55.12]。
|
||||
/// </summary>
|
||||
public string OriginalTimeline => $"[{Minute:00}:{Second:00.00}]";
|
||||
|
||||
/// <summary>
|
||||
/// 歌词文本数据。
|
||||
/// </summary>
|
||||
public string LyricText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌词所在的时间(分)。
|
||||
/// </summary>
|
||||
public int Minute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌词所在的时间(秒)。
|
||||
/// </summary>
|
||||
public double Second { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序分数,用于一组歌词当中的排序权重。<br/>
|
||||
/// </summary>
|
||||
public double SortScore => Minute * 60 + Second;
|
||||
|
||||
/// <summary>
|
||||
/// 构建新的 <see cref="LyricItem"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="lyricText">原始的 Lyric 歌词。</param>
|
||||
public LyricItem(string lyricText)
|
||||
{
|
||||
var timeline = new Regex(@"\[\d+:\d+.\d+\]").Match(lyricText)
|
||||
.Value.Replace("]", string.Empty)
|
||||
.Replace("[", string.Empty)
|
||||
.Split(':');
|
||||
|
||||
if (int.TryParse(timeline[0], out var minute)) Minute = minute;
|
||||
if (double.TryParse(timeline[1], out var second)) Second = second;
|
||||
|
||||
LyricText = new Regex(@"(?<=\[\d+:\d+.\d+\]).+").Match(lyricText).Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造新的 <see cref="LyricItem"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="minute">歌词所在的时间(分)。</param>
|
||||
/// <param name="second">歌词所在的时间(秒)。</param>
|
||||
/// <param name="lyricText">歌词文本数据。</param>
|
||||
public LyricItem(int minute, double second, string lyricText)
|
||||
{
|
||||
Minute = minute;
|
||||
Second = second;
|
||||
LyricText = lyricText;
|
||||
}
|
||||
|
||||
public int CompareTo(LyricItem other)
|
||||
{
|
||||
if (SortScore > other.SortScore)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (SortScore < other.SortScore)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static bool operator >(LyricItem left, LyricItem right)
|
||||
{
|
||||
return left.SortScore > right.SortScore;
|
||||
}
|
||||
|
||||
public static bool operator <(LyricItem left, LyricItem right)
|
||||
{
|
||||
return left.SortScore < right.SortScore;
|
||||
}
|
||||
|
||||
public static bool operator ==(LyricItem left, LyricItem right)
|
||||
{
|
||||
return (int?) left?.SortScore == (int?) right?.SortScore;
|
||||
}
|
||||
|
||||
public static bool operator !=(LyricItem item1, LyricItem item2)
|
||||
{
|
||||
return !(item1 == item2);
|
||||
}
|
||||
|
||||
public static LyricItem operator +(LyricItem src, LyricItem dist)
|
||||
{
|
||||
return new LyricItem(src.Minute, src.Second, $"{src.LyricText} {dist.LyricText}");
|
||||
}
|
||||
|
||||
protected bool Equals(LyricItem other)
|
||||
{
|
||||
return LyricText == other.LyricText && Minute == other.Minute && Second.Equals(other.Second);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(LyricText, Minute, Second);
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{Minute:00}:{Second:00.00}]{LyricText}";
|
||||
}
|
||||
}
|
111
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricItemCollection.cs
Normal file
111
src/ZonyLrcTools.Cli/Infrastructure/Lyric/LyricItemCollection.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Extensions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌词数据,包含多条歌词对象(<see cref="LyricItem"/>)。
|
||||
/// </summary>
|
||||
public class LyricItemCollection : List<LyricItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为纯音乐,当没有任何歌词数据的时候,属性值为 True。
|
||||
/// </summary>
|
||||
public bool IsPruneMusic => Count == 0;
|
||||
|
||||
public LyricItemCollectionOption Option { get; private set; }
|
||||
|
||||
public LyricItemCollection(LyricItemCollectionOption option)
|
||||
{
|
||||
Option = option;
|
||||
}
|
||||
|
||||
public static LyricItemCollection operator +(LyricItemCollection left, LyricItemCollection right)
|
||||
{
|
||||
if (right.IsPruneMusic)
|
||||
{
|
||||
return left;
|
||||
}
|
||||
|
||||
var option = left.Option;
|
||||
var newCollection = new LyricItemCollection(option);
|
||||
var indexDiff = left.Count - right.Count;
|
||||
|
||||
if (!option.IsOneLine)
|
||||
{
|
||||
left.ForEach(item => newCollection.Add(item));
|
||||
right.ForEach(item => newCollection.Add(item));
|
||||
|
||||
newCollection.Sort();
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
// 如果索引相等,直接根据索引快速匹配构建。
|
||||
if (indexDiff == 0)
|
||||
{
|
||||
newCollection.AddRange(left.Select((t, index) => t + right[index]));
|
||||
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
// 首先按照时间轴进行合并。
|
||||
var leftMarkDict = BuildMarkDictionary(left);
|
||||
var rightMarkDict = BuildMarkDictionary(right);
|
||||
|
||||
for (var leftIndex = 0; leftIndex < left.Count; leftIndex++)
|
||||
{
|
||||
var rightItem = right.Find(lyric => Math.Abs(lyric.SortScore - left[leftIndex].SortScore) < 0.001);
|
||||
if (rightItem != null)
|
||||
{
|
||||
newCollection.Add(left[leftIndex] + rightItem);
|
||||
var rightIndex = right.FindIndex(item => item == rightItem);
|
||||
rightMarkDict[rightIndex] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newCollection.Add(left[leftIndex]);
|
||||
}
|
||||
|
||||
leftMarkDict[leftIndex] = true;
|
||||
}
|
||||
|
||||
// 遍历未处理的歌词项,将其添加到返回集合当中。
|
||||
var leftWaitProcessIndex = leftMarkDict
|
||||
.Where(item => item.Value)
|
||||
.Select(pair => pair.Key);
|
||||
var rightWaitProcessIndex = rightMarkDict
|
||||
.Where(item => item.Value)
|
||||
.Select(pair => pair.Key);
|
||||
|
||||
leftWaitProcessIndex.Foreach(index => newCollection.Add(left[index]));
|
||||
rightWaitProcessIndex.Foreach(index => newCollection.Add(right[index]));
|
||||
|
||||
newCollection.Sort();
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据歌词集合构建一个索引状态字典。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 这个索引字典用于标识每个索引的歌词是否被处理,为 True 则为已处理,为 False 为未处理。
|
||||
/// </remarks>
|
||||
/// <param name="items">等待构建的歌词集合实例。</param>
|
||||
private static Dictionary<int, bool> BuildMarkDictionary(LyricItemCollection items)
|
||||
{
|
||||
return items
|
||||
.Select((item, index) => new {index, item})
|
||||
.ToDictionary(item => item.index, item => false);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var lyricBuilder = new StringBuilder();
|
||||
ForEach(lyric => lyricBuilder.Append(lyric).Append("\r\n"));
|
||||
return lyricBuilder.ToString().TrimEnd("\r\n");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
public class LyricItemCollectionFactory : ILyricItemCollectionFactory, ITransientDependency
|
||||
{
|
||||
private readonly ToolOptions _options;
|
||||
|
||||
public LyricItemCollectionFactory(IOptions<ToolOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public LyricItemCollection Build(string sourceLyric, string translateLyric = null)
|
||||
{
|
||||
var items = new LyricItemCollection(_options.LyricOption);
|
||||
if (string.IsNullOrEmpty(sourceLyric))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
var regex = new Regex(@"\[\d+:\d+.\d+\].+\n?");
|
||||
foreach (Match match in regex.Matches(sourceLyric))
|
||||
{
|
||||
items.Add(new LyricItem(match.Value));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric
|
||||
{
|
||||
public class LyricItemCollectionOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 双语歌词是否合并为一行。
|
||||
/// </summary>
|
||||
public bool IsOneLine { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 换行符格式,取值来自 <see cref="LineBreakType"/> 常量类。
|
||||
/// </summary>
|
||||
public string LineBreak { get; set; } = LineBreakType.Windows;
|
||||
|
||||
public static readonly LyricItemCollectionOption NullInstance = new();
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public class GetLyricRequest
|
||||
{
|
||||
public GetLyricRequest(long songId)
|
||||
{
|
||||
OS = "osx";
|
||||
Id = songId;
|
||||
Lv = Kv = Tv = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求的操作系统。
|
||||
/// </summary>
|
||||
[JsonProperty("os")]
|
||||
public string OS { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的 SID 值。
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public long Id { get; }
|
||||
|
||||
[JsonProperty("lv")] public int Lv { get; }
|
||||
|
||||
[JsonProperty("kv")] public int Kv { get; }
|
||||
|
||||
[JsonProperty("tv")] public int Tv { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public class GetLyricResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 原始的歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("lrc")]
|
||||
public InnerLyric OriginalLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 卡拉 OK 歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("klyric")]
|
||||
public InnerLyric KaraokeLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 如果存在翻译歌词,则本项内容为翻译歌词。
|
||||
/// </summary>
|
||||
[JsonProperty("tlyric")]
|
||||
public InnerLyric TranslationLyric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态码。
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public string StatusCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 歌词 JSON 类型
|
||||
/// </summary>
|
||||
public class InnerLyric
|
||||
{
|
||||
[JsonProperty("version")] public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 具体的歌词数据。
|
||||
/// </summary>
|
||||
[JsonProperty("lyric")]
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public class GetSongDetailsRequest
|
||||
{
|
||||
public GetSongDetailsRequest(int songId)
|
||||
{
|
||||
SongId = songId;
|
||||
SongIds = $"%5B{songId}%5D";
|
||||
}
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int SongId { get; }
|
||||
|
||||
[JsonProperty("ids")]
|
||||
public string SongIds { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public class SongSearchRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// CSRF 标识,一般为空即可,接口不会进行校验。
|
||||
/// </summary>
|
||||
[JsonProperty("csrf_token")]
|
||||
public string CsrfToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 需要搜索的内容,一般是歌曲名 + 歌手的格式。
|
||||
/// </summary>
|
||||
[JsonProperty("s")]
|
||||
public string SearchKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页偏移量。
|
||||
/// </summary>
|
||||
[JsonProperty("offset")]
|
||||
public int Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 搜索类型。
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否获取全部的搜索结果。
|
||||
/// </summary>
|
||||
[JsonProperty("total")]
|
||||
public bool IsTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页的最大结果容量。
|
||||
/// </summary>
|
||||
[JsonProperty("limit")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
public SongSearchRequest()
|
||||
{
|
||||
CsrfToken = string.Empty;
|
||||
Type = 1;
|
||||
Offset = 0;
|
||||
IsTotal = true;
|
||||
Limit = 5;
|
||||
}
|
||||
|
||||
public SongSearchRequest(string musicName, string artistName) : this()
|
||||
{
|
||||
SearchKey = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public class SongSearchResponse
|
||||
{
|
||||
[JsonProperty("result")] public InnerListItemModel Items { get; set; }
|
||||
|
||||
[JsonProperty("code")] public int StatusCode { get; set; }
|
||||
|
||||
public int GetFirstSongId()
|
||||
{
|
||||
return Items.SongItems[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
public class InnerListItemModel
|
||||
{
|
||||
[JsonProperty("songs")] public IList<SongModel> SongItems { get; set; }
|
||||
|
||||
[JsonProperty("songCount")] public int SongCount { get; set; }
|
||||
}
|
||||
|
||||
public class SongModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌曲的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的 Sid (Song Id)。
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的演唱者。
|
||||
/// </summary>
|
||||
[JsonProperty("artists")]
|
||||
public IList<SongArtistModel> Artists { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的专辑信息。
|
||||
/// </summary>
|
||||
[JsonProperty("album")]
|
||||
public SongAlbumModel Album { get; set; }
|
||||
}
|
||||
|
||||
public class SongArtistModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌手/艺术家的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class SongAlbumModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 专辑的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 专辑图像的 Url 地址。
|
||||
/// </summary>
|
||||
[JsonProperty("img1v1Url")]
|
||||
public string PictureUrl { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel
|
||||
{
|
||||
public static class SongSearchResponseStatusCode
|
||||
{
|
||||
public const int Success = 200;
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase.JsonModel;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Network;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.NetEase
|
||||
{
|
||||
public class NetEaseLyricDownloader : LyricDownloader
|
||||
{
|
||||
public override string DownloaderName => InternalLyricDownloaderNames.NetEase;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
|
||||
|
||||
private const string NetEaseSearchMusicUrl = @"https://music.163.com/api/search/get/web";
|
||||
private const string NetEaseGetLyricUrl = @"https://music.163.com/api/song/lyric";
|
||||
private const string NetEaseRequestReferer = @"https://music.163.com";
|
||||
private const string NetEaseRequestContentType = @"application/x-www-form-urlencoded";
|
||||
|
||||
public NetEaseLyricDownloader(IWarpHttpClient warpHttpClient,
|
||||
ILyricItemCollectionFactory lyricItemCollectionFactory)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricItemCollectionFactory = lyricItemCollectionFactory;
|
||||
}
|
||||
|
||||
protected override async ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
var searchResult = await _warpHttpClient.PostAsync<SongSearchResponse>(
|
||||
NetEaseSearchMusicUrl,
|
||||
new SongSearchRequest(args.SongName, args.Artist),
|
||||
true,
|
||||
msg =>
|
||||
{
|
||||
msg.Headers.Referrer = new Uri(NetEaseRequestReferer);
|
||||
if (msg.Content != null)
|
||||
{
|
||||
msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(NetEaseRequestContentType);
|
||||
}
|
||||
});
|
||||
|
||||
ValidateSongSearchResponse(searchResult, args);
|
||||
|
||||
var lyricResponse = await _warpHttpClient.GetAsync(
|
||||
NetEaseGetLyricUrl,
|
||||
new GetLyricRequest(searchResult.GetFirstSongId()),
|
||||
msg => msg.Headers.Referrer = new Uri(NetEaseRequestReferer));
|
||||
|
||||
return Encoding.UTF8.GetBytes(lyricResponse);
|
||||
}
|
||||
|
||||
protected override async ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
|
||||
{
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
var json = JsonConvert.DeserializeObject<GetLyricResponse>(Encoding.UTF8.GetString(data));
|
||||
if (json?.OriginalLyric == null)
|
||||
{
|
||||
return new LyricItemCollection(LyricItemCollectionOption.NullInstance);
|
||||
}
|
||||
|
||||
return _lyricItemCollectionFactory.Build(
|
||||
json.OriginalLyric.Text,
|
||||
json.TranslationLyric.Text);
|
||||
}
|
||||
|
||||
protected virtual void ValidateSongSearchResponse(SongSearchResponse response, LyricDownloaderArgs args)
|
||||
{
|
||||
if (response?.StatusCode != SongSearchResponseStatusCode.Success)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.TheReturnValueIsIllegal, attachObj: args);
|
||||
}
|
||||
|
||||
if (response.Items?.SongCount <= 0)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NoMatchingSong, attachObj: args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel
|
||||
{
|
||||
public class SongSearchRequest
|
||||
{
|
||||
[JsonProperty("ct")] public int UnknownParameter1 { get; set; }
|
||||
|
||||
[JsonProperty("qqmusic_ver")] public int ClientVersion { get; set; }
|
||||
|
||||
[JsonProperty("new_json")] public int UnknownParameter2 { get; set; }
|
||||
|
||||
[JsonProperty("remoteplace")] public string RemotePlace { get; set; }
|
||||
|
||||
[JsonProperty("t")] public int UnknownParameter3 { get; set; }
|
||||
|
||||
[JsonProperty("aggr")] public int UnknownParameter4 { get; set; }
|
||||
|
||||
[JsonProperty("cr")] public int UnknownParameter5 { get; set; }
|
||||
|
||||
[JsonProperty("catZhida")] public int UnknownParameter6 { get; set; }
|
||||
|
||||
[JsonProperty("lossless")] public int LossLess { get; set; }
|
||||
|
||||
[JsonProperty("flag_qc")] public int UnknownParameter7 { get; set; }
|
||||
|
||||
[JsonProperty("p")] public int Page { get; set; }
|
||||
|
||||
[JsonProperty("n")] public int Limit { get; set; }
|
||||
|
||||
[JsonProperty("w")] public string Keyword { get; set; }
|
||||
|
||||
[JsonProperty("g_tk")] public int UnknownParameter8 { get; set; }
|
||||
|
||||
[JsonProperty("hostUin")] public int UnknownParameter9 { get; set; }
|
||||
|
||||
[JsonProperty("format")] public string ResultFormat { get; set; }
|
||||
|
||||
[JsonProperty("inCharset")] public string InCharset { get; set; }
|
||||
|
||||
[JsonProperty("outCharset")] public string OutCharset { get; set; }
|
||||
|
||||
[JsonProperty("notice")] public int UnknownParameter10 { get; set; }
|
||||
|
||||
[JsonProperty("platform")] public string Platform { get; set; }
|
||||
|
||||
[JsonProperty("needNewCode")] public int UnknownParameter11 { get; set; }
|
||||
|
||||
public SongSearchRequest()
|
||||
{
|
||||
UnknownParameter1 = 24;
|
||||
ClientVersion = 1298;
|
||||
UnknownParameter2 = 1;
|
||||
RemotePlace = "txt.yqq.song";
|
||||
UnknownParameter3 = 0;
|
||||
UnknownParameter4 = 1;
|
||||
UnknownParameter5 = 1;
|
||||
UnknownParameter6 = 1;
|
||||
LossLess = 0;
|
||||
UnknownParameter7 = 0;
|
||||
Page = 1;
|
||||
Limit = 5;
|
||||
UnknownParameter8 = 5381;
|
||||
UnknownParameter9 = 0;
|
||||
ResultFormat = "json";
|
||||
InCharset = "utf8";
|
||||
OutCharset = "utf8";
|
||||
UnknownParameter10 = 0;
|
||||
Platform = "yqq";
|
||||
UnknownParameter11 = 0;
|
||||
}
|
||||
|
||||
public SongSearchRequest(string musicName, string artistName) : this()
|
||||
{
|
||||
Keyword = HttpUtility.UrlEncode($"{musicName}+{artistName}", Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic.JsonModel
|
||||
{
|
||||
public class SongSearchResponse
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public QQMusicInnerDataModel Data { get; set; }
|
||||
}
|
||||
|
||||
public class QQMusicInnerDataModel
|
||||
{
|
||||
[JsonProperty("song")]
|
||||
public QQMusicInnerSongModel Song { get; set; }
|
||||
}
|
||||
|
||||
public class QQMusicInnerSongModel
|
||||
{
|
||||
[JsonProperty("list")]
|
||||
public List<QQMusicInnerSongItem> SongItems { get; set; }
|
||||
}
|
||||
|
||||
public class QQMusicInnerSongItem
|
||||
{
|
||||
[JsonProperty("mid")]
|
||||
public string SongId { get; set; }
|
||||
}
|
||||
|
||||
public class AlbumInfo
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Network;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Lyric.QQMusic
|
||||
{
|
||||
public class QQLyricDownloader : LyricDownloader
|
||||
{
|
||||
public override string DownloaderName => InternalLyricDownloaderNames.QQ;
|
||||
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
private readonly ILyricItemCollectionFactory _lyricItemCollectionFactory;
|
||||
|
||||
public QQLyricDownloader(IWarpHttpClient warpHttpClient,
|
||||
ILyricItemCollectionFactory lyricItemCollectionFactory)
|
||||
{
|
||||
_warpHttpClient = warpHttpClient;
|
||||
_lyricItemCollectionFactory = lyricItemCollectionFactory;
|
||||
}
|
||||
|
||||
protected override ValueTask<byte[]> DownloadDataAsync(LyricDownloaderArgs args)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
protected override ValueTask<LyricItemCollection> GenerateLyricAsync(byte[] data)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
18
src/ZonyLrcTools.Cli/Infrastructure/MusicInfo.cs
Normal file
18
src/ZonyLrcTools.Cli/Infrastructure/MusicInfo.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure
|
||||
{
|
||||
public class MusicInfo
|
||||
{
|
||||
public string FilePath { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string Artist { get; }
|
||||
|
||||
public MusicInfo(string filePath, string name, string artist)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Name = name;
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
{
|
||||
public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<DefaultWarpHttpClient> _logger;
|
||||
|
||||
public const string HttpClientNameConstant = "WarpClient";
|
||||
|
||||
public DefaultWarpHttpClient(IHttpClientFactory httpClientFactory,
|
||||
ILogger<DefaultWarpHttpClient> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<string> PostAsync(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));
|
||||
requestMessage.Content = new StringContent(parametersStr);
|
||||
|
||||
requestOption?.Invoke(requestMessage);
|
||||
|
||||
using var responseMessage = await BuildHttpClient().SendAsync(requestMessage);
|
||||
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
return responseMessage.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.OK => responseContentString,
|
||||
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
|
||||
_ => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable, attachObj: new {parametersStr, 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);
|
||||
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {parameters, responseString});
|
||||
|
||||
try
|
||||
{
|
||||
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseString);
|
||||
if (responseObj != null) return responseObj;
|
||||
|
||||
throw throwException;
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
throw throwException;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<string> GetAsync(string url,
|
||||
object parameters = null,
|
||||
Action<HttpRequestMessage> requestOption = null)
|
||||
{
|
||||
var requestParamsStr = BuildQueryString(parameters);
|
||||
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
|
||||
requestOption?.Invoke(requestMsg);
|
||||
|
||||
using (var responseMsg = await BuildHttpClient().SendAsync(requestMsg))
|
||||
{
|
||||
var responseContent = await responseMsg.Content.ReadAsStringAsync();
|
||||
|
||||
return responseMsg.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.OK => responseContent,
|
||||
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
|
||||
_ => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable, attachObj: new {requestParamsStr, responseContent})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
|
||||
object parameters = null,
|
||||
Action<HttpRequestMessage> requestOption = null)
|
||||
{
|
||||
var responseStr = await GetAsync(url, parameters, requestOption);
|
||||
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new {parameters, responseStr});
|
||||
try
|
||||
{
|
||||
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseStr);
|
||||
if (responseObj != null) return responseObj;
|
||||
|
||||
throw throwException;
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
throw throwException;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual HttpClient BuildHttpClient()
|
||||
{
|
||||
return _httpClientFactory.CreateClient(HttpClientNameConstant);
|
||||
}
|
||||
|
||||
private string BuildQueryString(object parameters)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var type = parameters.GetType();
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return parameters as string;
|
||||
}
|
||||
|
||||
var properties = type.GetProperties();
|
||||
var paramBuilder = new StringBuilder();
|
||||
|
||||
foreach (var propertyInfo in properties)
|
||||
{
|
||||
var jsonProperty = propertyInfo.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
var propertyName = jsonProperty != null ? jsonProperty.PropertyName : propertyInfo.Name;
|
||||
|
||||
paramBuilder.Append($"{propertyName}={propertyInfo.GetValue(parameters)}&");
|
||||
}
|
||||
|
||||
return paramBuilder.ToString().TrimEnd('&');
|
||||
}
|
||||
|
||||
private string BuildJsonBodyString(object parameters)
|
||||
{
|
||||
if (parameters == null) return string.Empty;
|
||||
if (parameters is string result) return result;
|
||||
|
||||
return JsonConvert.SerializeObject(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。
|
||||
/// </summary>
|
||||
public interface IWarpHttpClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据指定的配置执行 POST 请求,并以 <see cref="string"/> 作为返回值。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的 URL 地址。</param>
|
||||
/// <param name="parameters">请求的参数。</param>
|
||||
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
|
||||
/// <param name="requestOption">请求时的配置动作。</param>
|
||||
/// <returns>服务端的响应结果。</returns>
|
||||
ValueTask<string> PostAsync(string url,
|
||||
object parameters = null,
|
||||
bool isQueryStringParam = false,
|
||||
Action<HttpRequestMessage> requestOption = null);
|
||||
|
||||
/// <summary>
|
||||
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的 URL 地址。</param>
|
||||
/// <param name="parameters">请求的参数。</param>
|
||||
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
|
||||
/// <param name="requestOption">请求时的配置动作。</param>
|
||||
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
||||
/// <returns>服务端的响应结果。</returns>
|
||||
ValueTask<TResponse> PostAsync<TResponse>(string url,
|
||||
object parameters = null,
|
||||
bool isQueryStringParam = false,
|
||||
Action<HttpRequestMessage> requestOption = null);
|
||||
|
||||
/// <summary>
|
||||
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的 URL 地址。</param>
|
||||
/// <param name="parameters">请求的参数。</param>
|
||||
/// <param name="requestOption">请求时的配置动作。</param>
|
||||
/// <returns>服务端的响应结果。</returns>
|
||||
ValueTask<string> GetAsync(string url,
|
||||
object parameters = null,
|
||||
Action<HttpRequestMessage> requestOption = null);
|
||||
|
||||
/// <summary>
|
||||
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="url">请求的 URL 地址。</param>
|
||||
/// <param name="parameters">请求的参数。</param>
|
||||
/// <param name="requestOption">请求时的配置动作。</param>
|
||||
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
|
||||
/// <returns>服务端的响应结果。</returns>
|
||||
ValueTask<TResponse> GetAsync<TResponse>(
|
||||
string url,
|
||||
object parameters = null,
|
||||
Action<HttpRequestMessage> requestOption = null);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具网络相关的设定。
|
||||
/// </summary>
|
||||
public class NetworkOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用了网络代理功能。
|
||||
/// </summary>
|
||||
public bool Enable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 代理服务器的 Ip。
|
||||
/// </summary>
|
||||
public string ProxyIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 代理服务器的 端口。
|
||||
/// </summary>
|
||||
public int ProxyPort { get; set; }
|
||||
}
|
||||
}
|
37
src/ZonyLrcTools.Cli/Infrastructure/Tag/DefaultTagLoader.cs
Normal file
37
src/ZonyLrcTools.Cli/Infrastructure/Tag/DefaultTagLoader.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public class DefaultTagLoader : ITagLoader, ITransientDependency
|
||||
{
|
||||
protected readonly IEnumerable<ITagInfoProvider> TagInfoProviders;
|
||||
|
||||
public DefaultTagLoader(IEnumerable<ITagInfoProvider> tagInfoProviders)
|
||||
{
|
||||
TagInfoProviders = tagInfoProviders;
|
||||
}
|
||||
|
||||
public virtual async ValueTask<MusicInfo> LoadTagAsync(string filePath)
|
||||
{
|
||||
if (!TagInfoProviders.Any())
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.LoadTagInfoProviderError);
|
||||
}
|
||||
|
||||
foreach (var provider in TagInfoProviders.OrderBy(p => p.Priority))
|
||||
{
|
||||
var info = await provider.LoadAsync(filePath);
|
||||
if (info != null)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public class FileNameTagInfoProvider : ITagInfoProvider, ITransientDependency
|
||||
{
|
||||
public int Priority => 2;
|
||||
|
||||
public string Name => ConstantName;
|
||||
public const string ConstantName = "FileName";
|
||||
|
||||
private readonly ToolOptions Options;
|
||||
|
||||
public FileNameTagInfoProvider(IOptions<ToolOptions> options)
|
||||
{
|
||||
Options = options.Value;
|
||||
}
|
||||
|
||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
||||
{
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
var match = Regex.Match(Path.GetFileNameWithoutExtension(filePath), Options.TagInfoProviderOptions.FileNameRegularExpressions);
|
||||
|
||||
if (match.Groups.Count != 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MusicInfo(filePath, match.Groups["name"].Value, match.Groups["artist"].Value);
|
||||
}
|
||||
}
|
||||
}
|
13
src/ZonyLrcTools.Cli/Infrastructure/Tag/ITagInfoProvider.cs
Normal file
13
src/ZonyLrcTools.Cli/Infrastructure/Tag/ITagInfoProvider.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public interface ITagInfoProvider
|
||||
{
|
||||
int Priority { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
ValueTask<MusicInfo> LoadAsync(string filePath);
|
||||
}
|
||||
}
|
14
src/ZonyLrcTools.Cli/Infrastructure/Tag/ITagLoader.cs
Normal file
14
src/ZonyLrcTools.Cli/Infrastructure/Tag/ITagLoader.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public interface ITagLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载歌曲的标签信息。
|
||||
/// </summary>
|
||||
/// <param name="filePath">歌曲文件的路径。</param>
|
||||
/// <returns>加载完成的歌曲信息。</returns>
|
||||
ValueTask<MusicInfo> LoadTagAsync(string filePath);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public class TagInfoProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于从文件名当中提取歌曲名、歌手名。
|
||||
/// </summary>
|
||||
public string FileNameRegularExpressions { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Tag
|
||||
{
|
||||
public class TaglibTagInfoProvider : ITagInfoProvider, ITransientDependency
|
||||
{
|
||||
public int Priority => 1;
|
||||
|
||||
public string Name => ConstantName;
|
||||
public const string ConstantName = "Taglib";
|
||||
|
||||
public async ValueTask<MusicInfo> LoadAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = TagLib.File.Create(filePath);
|
||||
|
||||
var songName = file.Tag.Title;
|
||||
var songArtist = file.Tag.FirstPerformer;
|
||||
|
||||
if (!string.IsNullOrEmpty(file.Tag.FirstAlbumArtist))
|
||||
{
|
||||
songArtist = file.Tag.FirstAlbumArtist;
|
||||
}
|
||||
|
||||
await ValueTask.CompletedTask;
|
||||
|
||||
if (songName == null && songArtist == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MusicInfo(filePath, songName, songArtist);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.TagInfoProviderLoadInfoFailed, ex.Message, filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
89
src/ZonyLrcTools.Cli/Infrastructure/Threading/WarpTask.cs
Normal file
89
src/ZonyLrcTools.Cli/Infrastructure/Threading/WarpTask.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZonyLrcTools.Cli.Infrastructure.Threading
|
||||
{
|
||||
public class WarpTask : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly int _maxDegreeOfParallelism;
|
||||
|
||||
public WarpTask(int maxDegreeOfParallelism)
|
||||
{
|
||||
if (maxDegreeOfParallelism <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
|
||||
}
|
||||
|
||||
_maxDegreeOfParallelism = maxDegreeOfParallelism;
|
||||
_semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
|
||||
}
|
||||
|
||||
public async Task RunAsync(Func<Task> taskFactory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token))
|
||||
{
|
||||
await _semaphore.WaitAsync(cts.Token);
|
||||
try
|
||||
{
|
||||
await taskFactory().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> RunAsync<T>(Func<Task<T>> taskFactory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token))
|
||||
{
|
||||
await _semaphore.WaitAsync(cts.Token);
|
||||
try
|
||||
{
|
||||
return await taskFactory().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposedValue = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cts.Cancel();
|
||||
for (int i = 0; i < _maxDegreeOfParallelism; i++)
|
||||
{
|
||||
_semaphore.WaitAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_semaphore.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~WarpTask()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
20
src/ZonyLrcTools.Cli/Resources/error_msg.json
Normal file
20
src/ZonyLrcTools.Cli/Resources/error_msg.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"Error": {
|
||||
"10001": "待搜索的后缀不能为空。",
|
||||
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
|
||||
"10003": "不能获取文件的后缀信息。",
|
||||
"10004": "没有扫描到任何音乐文件。"
|
||||
},
|
||||
"Warning": {
|
||||
"50001": "扫描文件时出现了错误。",
|
||||
"50002": "歌曲名称或歌手名称均为空,无法进行搜索。",
|
||||
"50003": "歌曲名称不能为空,无法进行搜索。",
|
||||
"50004": "下载器没有搜索到对应的歌曲信息。",
|
||||
"50005": "下载请求的返回值不合法,可能是服务端故障。",
|
||||
"50006": "标签信息读取器为空,无法解析音乐 Tag 信息。",
|
||||
"50007": "TagLib 标签读取器出现了预期之外的异常。",
|
||||
"50008": "服务接口限制,无法进行请求,请尝试使用代理服务器。",
|
||||
"50009": "对目标服务器执行 Http 请求失败。",
|
||||
"50010": "Http 请求的结果反序列化为 Json 失败。"
|
||||
}
|
||||
}
|
39
src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj
Normal file
39
src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj
Normal file
@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
|
||||
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Refit" Version="6.0.38" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.38" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.0.38" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="appsettings.json" />
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="Resources\error_msg.json" />
|
||||
<Content Include="Resources\error_msg.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Infrastructure\Network\Exceptions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
20
src/ZonyLrcTools.Cli/appsettings.json
Normal file
20
src/ZonyLrcTools.Cli/appsettings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"ToolOption": {
|
||||
"SupportFileExtensions": "*.mp3;*.flac",
|
||||
"NetworkOptions": {
|
||||
"Enable": true,
|
||||
"ProxyIp": "127.0.0.1",
|
||||
"ProxyPort": 4780
|
||||
},
|
||||
"TagInfoProviderOptions": {
|
||||
"FileNameRegularExpressions": "(?'artist'.+)\\s-\\s(?'name'.+)"
|
||||
}
|
||||
}
|
||||
}
|
19
src/ZonyLrcTools.Cli/publish.sh
Executable file
19
src/ZonyLrcTools.Cli/publish.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
read -r -p "请输入版本号:" Version
|
||||
Platforms=('win-x64' 'linux-x64' 'osx-x64')
|
||||
|
||||
if ! [ -d './TempFiles' ];
|
||||
then
|
||||
mkdir ./TempFiles
|
||||
fi
|
||||
|
||||
rm -rf ./TempFiles/*
|
||||
|
||||
for platform in "${Platforms[@]}"
|
||||
do
|
||||
dotnet publish -r "$platform" -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
|
||||
|
||||
zip -r -j ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./bin/Release/net5.0/"$platform"/publish/
|
||||
|
||||
mv ./bin/Release/net5.0/"$platform"/publish/ZonyLrcTools_"$platform"_"$Version".zip ./TempFiles
|
||||
done
|
31
tests/ZonyLrcTools.Tests/FileScannerTests.cs
Normal file
31
tests/ZonyLrcTools.Tests/FileScannerTests.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.IO;
|
||||
|
||||
namespace ZonyLrcTools.Tests
|
||||
{
|
||||
public class FileScannerTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task ScanAsync_Test()
|
||||
{
|
||||
var tempMusicFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Temp.mp3");
|
||||
File.Create(tempMusicFilePath);
|
||||
|
||||
var fileScanner = ServiceProvider.GetRequiredService<IFileScanner>();
|
||||
var result = await fileScanner.ScanAsync(
|
||||
Path.GetDirectoryName(tempMusicFilePath),
|
||||
new[] {"*.mp3", "*.flac"});
|
||||
|
||||
result.Count.ShouldBe(2);
|
||||
result.First(e => e.ExtensionName == ".mp3").FilePaths.Count.ShouldNotBe(0);
|
||||
|
||||
File.Delete(tempMusicFilePath);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Album;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Album
|
||||
{
|
||||
public class NetEaseAlbumDownloaderTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task DownloadDataAsync_Test()
|
||||
{
|
||||
var downloader = ServiceProvider.GetRequiredService<IEnumerable<IAlbumDownloader>>()
|
||||
.FirstOrDefault(x => x.DownloaderName == InternalAlbumDownloaderNames.NetEase);
|
||||
|
||||
downloader.ShouldNotBeNull();
|
||||
var albumBytes = await downloader.DownloadAsync("东方红", null);
|
||||
albumBytes.Length.ShouldBeGreaterThan(0);
|
||||
|
||||
// 显示具体的图像。
|
||||
var tempAlbumPath = Path.Combine(Directory.GetCurrentDirectory(), "tempAlbum.png");
|
||||
File.Delete(tempAlbumPath);
|
||||
|
||||
await using var file = File.Create(tempAlbumPath);
|
||||
await using var ws = new BinaryWriter(file);
|
||||
ws.Write(albumBytes);
|
||||
ws.Flush();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Exceptions;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Exceptions
|
||||
{
|
||||
public class ErrorCodeHelperTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void LoadMessage_Test()
|
||||
{
|
||||
ErrorCodeHelper.LoadErrorMessage();
|
||||
|
||||
ErrorCodeHelper.ErrorMessages.ShouldNotBeNull();
|
||||
ErrorCodeHelper.ErrorMessages.Count.ShouldBe(11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMessage_Test()
|
||||
{
|
||||
ErrorCodeHelper.LoadErrorMessage();
|
||||
|
||||
ErrorCodeHelper.GetMessage(ErrorCodes.DirectoryNotExist).ShouldBe("需要扫描的目录不存在,请确认路径是否正确。");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Lyric;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Lyric
|
||||
{
|
||||
public class NetEaseLyricDownloaderTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task DownloadAsync_Test()
|
||||
{
|
||||
var downloaderList = ServiceProvider.GetRequiredService<IEnumerable<ILyricDownloader>>();
|
||||
var netEaseDownloader = downloaderList.FirstOrDefault(t => t.DownloaderName == InternalLyricDownloaderNames.NetEase);
|
||||
|
||||
netEaseDownloader.ShouldNotBeNull();
|
||||
var lyric = await netEaseDownloader.DownloadAsync("Hollow", "Janet Leon");
|
||||
lyric.ShouldNotBeNull();
|
||||
lyric.IsPruneMusic.ShouldBe(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Config;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Network;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Network
|
||||
{
|
||||
public class WarpClientTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task PostAsync_Test()
|
||||
{
|
||||
var client = ServiceProvider.GetRequiredService<IWarpHttpClient>();
|
||||
|
||||
var response = await client.PostAsync(@"https://www.baidu.com");
|
||||
response.ShouldNotBeNull();
|
||||
response.ShouldContain("百度");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_Test()
|
||||
{
|
||||
var client = ServiceProvider.GetRequiredService<IWarpHttpClient>();
|
||||
|
||||
var response = await client.GetAsync(@"https://www.baidu.com");
|
||||
response.ShouldNotBeNull();
|
||||
response.ShouldContain("百度");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsyncWithProxy_Test()
|
||||
{
|
||||
var option = ServiceProvider.GetRequiredService<IOptions<ToolOptions>>();
|
||||
option.Value.NetworkOptions.ProxyIp = "127.0.0.1";
|
||||
option.Value.NetworkOptions.ProxyPort = 4780;
|
||||
|
||||
var client = ServiceProvider.GetRequiredService<IWarpHttpClient>();
|
||||
|
||||
var response = await client.GetAsync(@"https://www.baidu.com");
|
||||
|
||||
response.ShouldNotBeNull();
|
||||
response.ShouldContain("百度");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Tag
|
||||
{
|
||||
public class FileNameTagInfoProviderTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_Test()
|
||||
{
|
||||
var provider = ServiceProvider.GetRequiredService<IEnumerable<ITagInfoProvider>>()
|
||||
.FirstOrDefault(p => p.Name == FileNameTagInfoProvider.ConstantName);
|
||||
|
||||
provider.ShouldNotBeNull();
|
||||
|
||||
var info = await provider.LoadAsync(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "曾经艺也 - 荀彧(纯音乐版).mp3"));
|
||||
info.ShouldNotBeNull();
|
||||
info.Name.ShouldBe("荀彧(纯音乐版)");
|
||||
info.Artist.ShouldBe("曾经艺也");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Tag
|
||||
{
|
||||
public class TagLoaderTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadTagAsync_Test()
|
||||
{
|
||||
var tagLoader = ServiceProvider.GetRequiredService<ITagLoader>();
|
||||
|
||||
tagLoader.ShouldNotBeNull();
|
||||
var info = await tagLoader.LoadTagAsync(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "曾经艺也 - 荀彧(纯音乐版).mp3"));
|
||||
info.ShouldNotBeNull();
|
||||
info.Name.ShouldBe("荀彧(纯音乐版)");
|
||||
info.Artist.ShouldBe("曾经艺也");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Tag;
|
||||
|
||||
namespace ZonyLrcTools.Tests.Infrastructure.Tag
|
||||
{
|
||||
public class TaglibTagInfoProviderTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_Test()
|
||||
{
|
||||
var provider = ServiceProvider.GetRequiredService<IEnumerable<ITagInfoProvider>>()
|
||||
.FirstOrDefault(p => p.Name == TaglibTagInfoProvider.ConstantName);
|
||||
|
||||
provider.ShouldNotBeNull();
|
||||
|
||||
var info = await provider.LoadAsync(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "曾经艺也 - 荀彧(纯音乐版).mp3"));
|
||||
info.ShouldNotBeNull();
|
||||
info.Name.ShouldBe("荀彧(纯音乐版)");
|
||||
info.Artist.ShouldBe("曾经艺也");
|
||||
}
|
||||
}
|
||||
}
|
BIN
tests/ZonyLrcTools.Tests/MusicFiles/曾经艺也 - 荀彧(纯音乐版).mp3
Normal file
BIN
tests/ZonyLrcTools.Tests/MusicFiles/曾经艺也 - 荀彧(纯音乐版).mp3
Normal file
Binary file not shown.
33
tests/ZonyLrcTools.Tests/TestBase.cs
Normal file
33
tests/ZonyLrcTools.Tests/TestBase.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZonyLrcTools.Cli.Commands;
|
||||
using ZonyLrcTools.Cli.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Cli.Infrastructure.Extensions;
|
||||
|
||||
namespace ZonyLrcTools.Tests
|
||||
{
|
||||
public class TestBase
|
||||
{
|
||||
protected IServiceProvider ServiceProvider { get; private set; }
|
||||
protected IServiceCollection ServiceCollection { get; private set; }
|
||||
|
||||
public TestBase()
|
||||
{
|
||||
ServiceCollection = BuildService();
|
||||
BuildServiceProvider();
|
||||
}
|
||||
|
||||
protected virtual IServiceCollection BuildService()
|
||||
{
|
||||
var service = new ServiceCollection();
|
||||
|
||||
service.BeginAutoDependencyInject<ToolCommand>();
|
||||
service.ConfigureToolService();
|
||||
service.ConfigureConfiguration();
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
protected virtual void BuildServiceProvider() => ServiceProvider = ServiceCollection.BuildServiceProvider();
|
||||
}
|
||||
}
|
34
tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj
Normal file
34
tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj
Normal file
@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZonyLrcTools.Cli\ZonyLrcTools.Cli.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="MusicFiles\曾经艺也 - 荀彧(纯音乐版).mp3" />
|
||||
<Content Include="MusicFiles\曾经艺也 - 荀彧(纯音乐版).mp3">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
50
zh_CN.md
Normal file
50
zh_CN.md
Normal file
@ -0,0 +1,50 @@
|
||||
## 简介
|
||||
ZonyLrcToolX 2.0 是一个基于 CEF 的跨平台歌词下载工具。
|
||||
|
||||
🚧 当前版本正在开发当中。
|
||||
🚧 如果你想查看可以工作的代码,请切换到 1.0 分支。
|
||||
## 用法
|
||||
|
||||
### 命令
|
||||
|
||||
#### 文件扫描
|
||||
|
||||
子命令为 `scan`,可用于扫描指定文件夹下的音乐文件数量(好像没什么卵用),下面我以 Windows 的可执行程序为例。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe scan -d|dir <WAIT_SCAN_DIRECTORY>
|
||||
|
||||
./ZonyLrcTools.cli.exe -h|--help
|
||||
```
|
||||
|
||||
#### 歌曲下载
|
||||
|
||||
子命令为 `download`,可用于下载歌词数据[^1]和专辑图像[^2],支持多个下载器[^3]进行下载。
|
||||
|
||||
```shell
|
||||
./ZonyLrcTools.Cli.exe download -d|dir <WAIT_SCAN_DIRECTORY> [-l|--lyric] [-a|--album] [-n|--number]
|
||||
|
||||
./ZonyLrcTools.Cli.exe download -h|--help
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
程序的部分配置信息需要在 `appsettings.json` 进行更改,下面标注了各个配置的说明。
|
||||
|
||||
| 属性 | 说明 | 示例值 |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------- |
|
||||
| ToolOption.SupportFileExtensions | 允许扫描的歌曲文件后缀名,以 `;` 号隔开多个后缀。 | `*.mp3;*.flac` |
|
||||
| ToolOption.NetworkOptions.Enable | 是否启用 HTTP 网络代理服务,true 表示启用,false 表示禁用。 | false |
|
||||
| ToolOption.NetworkOptions.ProxyIp | HTTP 网络代理服务的 IP,在 `Enable` 为 false 时会忽略该属性值。 | 127.0.0.1 |
|
||||
| ToolOption.NetworkOptions.ProxyPort | HTTP 网络代理服务的 端口,在 `Enable` 为 false 时会忽略该属性值。 | 8080 |
|
||||
| TagInfoProviderOptions.FileNameRegularExpressions | 文件名 Tag 标签信息读取器使用,使用正则表达式匹配歌曲名和歌手,请使用命名分组编写正则表达式。 | (?'artist'.+)\\s-\\s(?'name'.+) |
|
||||
|
||||
## 捐赠
|
||||
|
||||
## 路线图
|
||||
|
||||
- [ ] 支持跨平台的 CLI 工具。
|
||||
- [ ] 基于 Web GUI 的操作站点。
|
||||
- [ ] 支持插件系统(Lua 引擎)。
|
||||
|
||||
[^1 ]: 哎是
|
Loading…
x
Reference in New Issue
Block a user