Repository: wwh1004/NLyric
Branch: master
Commit: 043c6510eea4
Files: 47
Total size: 112.7 KB
Directory structure:
gitextract_guu2ltr2/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENSE
├── NLyric/
│ ├── Arguments.cs
│ ├── Audio/
│ │ ├── Album.cs
│ │ ├── ITrackOrAlbum.cs
│ │ └── Track.cs
│ ├── CRC32.cs
│ ├── ChineseConverter.cs
│ ├── Database/
│ │ ├── AlbumInfo.cs
│ │ ├── Extensions.cs
│ │ ├── LyricInfo.cs
│ │ ├── NLyricDatabase.cs
│ │ └── TrackInfo.cs
│ ├── FastConsole.cs
│ ├── Levenshtein.cs
│ ├── Lyrics/
│ │ └── Lrc.cs
│ ├── NLyric.csproj
│ ├── NLyricImpl.cs
│ ├── Ncm/
│ │ ├── CloudMusic.cs
│ │ ├── KeywordForbiddenException.cs
│ │ ├── NcmAlbum.cs
│ │ ├── NcmLyric.cs
│ │ └── NcmTrack.cs
│ ├── Program.cs
│ ├── Settings/
│ │ ├── AllSettings.cs
│ │ ├── CharArrayJsonConverter.cs
│ │ ├── EncodingConverter.cs
│ │ ├── FuzzySettings.cs
│ │ ├── LyricSettings.cs
│ │ ├── MatchSettings.cs
│ │ └── SearchSettings.cs
│ ├── Settings.json
│ ├── StringHelper.cs
│ ├── System/
│ │ └── Cli/
│ │ ├── ArgumentAttribute.cs
│ │ └── CommandLine.cs
│ └── The163KeyHelper.cs
├── NLyric.Win/
│ ├── MainForm.Designer.cs
│ ├── MainForm.cs
│ ├── MainForm.resx
│ ├── NLyric.Win.csproj
│ ├── Program.cs
│ └── Properties/
│ └── AssemblyInfo.cs
├── NLyric.sln
├── README.md
└── appveyor.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
[*]
charset = utf-8
end_of_line = crlf
indent_style = tab
trim_trailing_whitespace = true
insert_final_newline = true
# C# files
[*.cs]
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
# this. and Me. preferences
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_prefer_auto_properties = false:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:suggestion
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:suggestion
csharp_style_expression_bodied_methods = false:suggestion
csharp_style_expression_bodied_operators = false:suggestion
csharp_style_expression_bodied_properties = true:suggestion
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_switch_expression = false:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = false:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = unused_local_variable:silent
csharp_style_unused_value_expression_statement_preference = unused_local_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:suggestion
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = false
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = false
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# 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
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# 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
# wwh1004
launchSettings.json
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 文煌
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.
================================================
FILE: NLyric/Arguments.cs
================================================
using System;
using System.Cli;
using System.IO;
namespace NLyric {
public sealed class Arguments {
private string _directory;
private string _account;
private string _password;
private bool _updateOnly;
private bool _useBatch;
[Argument("-d", IsRequired = false, DefaultValue = "", Type = "DIR", Description = "存放音乐的文件夹,可以是相对路径或者绝对路径")]
internal string DirectoryCliSetter {
set {
if (string.IsNullOrEmpty(value))
return;
Directory = value;
}
}
[Argument("-a", IsRequired = false, DefaultValue = "", Type = "STR", Description = "网易云音乐账号(邮箱/手机号)")]
internal string AccountCliSetter {
set {
if (string.IsNullOrEmpty(value))
return;
Account = value;
}
}
[Argument("-p", IsRequired = false, DefaultValue = "", Type = "STR", Description = "网易云音乐密码")]
internal string PasswordCliSetter {
set {
if (string.IsNullOrEmpty(value))
return;
Password = value;
}
}
[Argument("--update-only", Description = "仅更新已有歌词")]
internal bool UpdateOnlyCliSetter {
set => _updateOnly = value;
}
[Argument("--batch", Description = "使用Batch API(实验性)")]
internal bool UseBatchCliSetter {
set => _useBatch = value;
}
public string Directory {
get => _directory;
set {
if (!System.IO.Directory.Exists(value))
throw new DirectoryNotFoundException();
_directory = Path.GetFullPath(value);
}
}
public string Account {
get => _account;
set {
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException(nameof(value));
_account = value;
}
}
public string Password {
get => _password;
set {
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException(nameof(value));
_password = value;
}
}
public bool UpdateOnly {
get => _updateOnly;
set => _updateOnly = value;
}
public bool UseBatch {
get => _useBatch;
set => _useBatch = value;
}
}
}
================================================
FILE: NLyric/Audio/Album.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using TagLib;
namespace NLyric.Audio {
///
/// 专辑
///
public class Album : ITrackOrAlbum {
private readonly string _name;
private readonly string[] _artists;
///
/// 名称
///
public string Name => _name;
///
/// 艺术家
///
public IReadOnlyList Artists => _artists;
public Album(string name, IEnumerable artists) {
if (name is null)
throw new ArgumentNullException(nameof(name));
if (artists is null)
throw new ArgumentNullException(nameof(artists));
_name = name;
_artists = artists.Select(t => t.Trim()).ToArray();
Array.Sort(_artists, StringComparer.Ordinal);
}
///
/// 构造器
///
///
/// 当 为空时,是否从 获取艺术家
public Album(Tag tag, bool getArtistsFromTrack) {
if (tag is null)
throw new ArgumentNullException(nameof(tag));
if (!HasAlbumInfo(tag))
throw new ArgumentException(nameof(tag) + " 中不存在专辑信息");
_name = tag.Album.GetSafeString();
string[] artists = tag.AlbumArtists.SelectMany(t => t.GetSafeString().SplitEx()).ToArray();
if (getArtistsFromTrack && artists.Length == 0)
artists = tag.Performers.SelectMany(t => t.GetSafeString().SplitEx()).ToArray();
Array.Sort(artists, StringComparer.Ordinal);
_artists = artists;
}
///
/// 是否存在专辑信息
///
///
///
public static bool HasAlbumInfo(Tag tag) {
if (tag is null)
throw new ArgumentNullException(nameof(tag));
return !string.IsNullOrWhiteSpace(tag.Album);
}
public override string ToString() {
return "Name:" + _name + " | Artists:" + string.Join(",", _artists);
}
}
}
================================================
FILE: NLyric/Audio/ITrackOrAlbum.cs
================================================
using System.Collections.Generic;
namespace NLyric.Audio {
public interface ITrackOrAlbum {
string Name { get; }
IReadOnlyList Artists { get; }
}
}
================================================
FILE: NLyric/Audio/Track.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using TagLib;
namespace NLyric.Audio {
///
/// 单曲
///
public class Track : ITrackOrAlbum {
private readonly string _name;
private readonly string[] _artists;
///
/// 名称
///
public string Name => _name;
///
/// 艺术家
///
public IReadOnlyList Artists => _artists;
public Track(string name, IEnumerable artists) {
if (name is null)
throw new ArgumentNullException(nameof(name));
if (artists is null)
throw new ArgumentNullException(nameof(artists));
_name = name;
_artists = artists.Select(t => t.Trim()).ToArray();
Array.Sort(_artists, StringComparer.Ordinal);
}
public Track(Tag tag) {
if (tag is null)
throw new ArgumentNullException(nameof(tag));
_name = tag.Title.GetSafeString();
_artists = tag.Performers.SelectMany(s => s.GetSafeString().SplitEx()).ToArray();
Array.Sort(_artists, StringComparer.Ordinal);
}
public override string ToString() {
return "Name:" + _name + " | Artists:" + string.Join(",", _artists);
}
}
}
================================================
FILE: NLyric/CRC32.cs
================================================
using System;
namespace NLyric {
internal static class CRC32 {
private static readonly uint[] _table = GenerateTable(0xEDB88320);
private static uint[] GenerateTable(uint seed) {
uint[] table = new uint[256];
for (int i = 0; i < 256; i++) {
uint crc = (uint)i;
for (int j = 8; j > 0; j--) {
if ((crc & 1) == 1)
crc = (crc >> 1) ^ seed;
else
crc >>= 1;
}
table[i] = crc;
}
return table;
}
public static uint Compute(byte[] data) {
if (data is null)
throw new ArgumentNullException(nameof(data));
uint crc32 = 0xFFFFFFFF;
for (int i = 0; i < data.Length; i++)
crc32 = (crc32 >> 8) ^ _table[(crc32 ^ data[i]) & 0xFF];
return ~crc32;
}
}
}
================================================
FILE: NLyric/ChineseConverter.cs
================================================
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
namespace NLyric {
internal static class ChineseConverter {
private static readonly Dictionary _traditionalToSimplifiedMap = GetTraditionalToSimplifiedMap();
private static Dictionary GetTraditionalToSimplifiedMap() {
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("NLyric.TraditionalToSimplified.map");
using var reader = new BinaryReader(stream);
int count = (int)stream.Length / 4;
var map = new Dictionary(count);
for (int i = 0; i < count; i++)
map.Add((char)reader.ReadUInt16(), (char)reader.ReadUInt16());
return map;
}
public static string TraditionalToSimplified(string s) {
if (s is null)
return null;
var sb = new StringBuilder(s);
for (int i = 0; i < sb.Length; i++) {
if (_traditionalToSimplifiedMap.TryGetValue(sb[i], out char c))
sb[i] = c;
}
return sb.ToString();
}
}
}
================================================
FILE: NLyric/Database/AlbumInfo.cs
================================================
using System;
using Newtonsoft.Json;
using NLyric.Audio;
namespace NLyric.Database {
///
/// 专辑信息
///
public sealed class AlbumInfo {
///
/// 名称
///
public string Name { get; set; }
///
/// 网易云音乐ID
///
public int Id { get; set; }
[JsonConstructor]
[Obsolete("Deserialization only", true)]
public AlbumInfo() {
}
public AlbumInfo(Album album, int id) : this(album.Name, id) {
}
public AlbumInfo(string name, int id) {
if (name is null)
throw new ArgumentNullException(nameof(name));
Name = name;
Id = id;
}
}
}
================================================
FILE: NLyric/Database/Extensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using NLyric.Audio;
namespace NLyric.Database {
public static class Extensions {
public static AlbumInfo Match(this IEnumerable caches, Album album) {
if (album is null)
throw new ArgumentNullException(nameof(album));
return caches.FirstOrDefault(t => IsMatched(t, album));
}
public static TrackInfo Match(this IEnumerable caches, Album album, Track track) {
if (track is null)
throw new ArgumentNullException(nameof(track));
return caches.FirstOrDefault(t => IsMatched(t, album, track));
}
public static bool IsMatched(this AlbumInfo cache, Album album) {
if (album is null)
throw new ArgumentNullException(nameof(album));
return cache.Name == album.Name;
}
public static bool IsMatched(this TrackInfo cache, Album album, Track track) {
if (track is null)
throw new ArgumentNullException(nameof(track));
return cache.Name == track.Name && (album is null ? cache.AlbumName is null : cache.AlbumName == album.Name) && cache.Artists.SequenceEqual(track.Artists);
// 如果album为空,要求cache中AlbumName也为空,如果album不为空,要求cache中AlbumName匹配
}
}
}
================================================
FILE: NLyric/Database/LyricInfo.cs
================================================
using System;
using Newtonsoft.Json;
using NLyric.Ncm;
namespace NLyric.Database {
///
/// 歌词信息
///
public sealed class LyricInfo {
///
/// 原始歌词版本
///
public int RawVersion { get; set; }
///
/// 翻译歌词版本(如果有)
///
public int TranslatedVersion { get; set; }
///
/// 歌词校验值
///
public string CheckSum { get; set; }
[JsonConstructor]
[Obsolete("Deserialization only", true)]
public LyricInfo() {
}
public LyricInfo(NcmLyric lyric, string checkSum) : this(lyric.RawVersion, lyric.TranslatedVersion, checkSum) {
if (!lyric.IsCollected)
throw new ArgumentException("未收录的歌词不能添加到缓存", nameof(lyric));
}
public LyricInfo(int rawVersion, int translatedVersion, string checkSum) {
if (checkSum is null)
throw new ArgumentNullException(nameof(checkSum));
RawVersion = rawVersion;
TranslatedVersion = translatedVersion;
CheckSum = checkSum;
}
}
}
================================================
FILE: NLyric/Database/NLyricDatabase.cs
================================================
using System.Collections.Generic;
namespace NLyric.Database {
///
/// NLyric数据库
///
public sealed class NLyricDatabase {
///
/// 专辑信息
///
public List AlbumInfos { get; set; }
///
/// 单曲信息
///
public List TrackInfos { get; set; }
///
/// 数据库格式版本
///
public int FormatVersion { get; set; }
///
/// 检查
///
///
public bool CheckFormatVersion() {
switch (FormatVersion) {
case 0:
case 1:
return true;
default:
return false;
}
}
///
/// 是否为老版本数据库
///
///
public bool IsOldFormat() {
return FormatVersion < 1;
}
}
}
================================================
FILE: NLyric/Database/TrackInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NLyric.Audio;
namespace NLyric.Database {
///
/// 单曲信息
///
public sealed class TrackInfo {
///
/// 名称
///
public string Name { get; set; }
///
/// 艺术家
///
public IReadOnlyList Artists { get; set; }
///
/// 专辑名
///
public string AlbumName { get; set; }
///
/// 网易云音乐ID
///
public int Id { get; set; }
///
/// 歌词缓存
///
public LyricInfo Lyric { get; set; }
[JsonConstructor]
[Obsolete("Deserialization only", true)]
public TrackInfo() {
}
public TrackInfo(Track track, Album album, int id) : this(track.Name, track.Artists, album?.Name, id) {
}
public TrackInfo(string name, IEnumerable artists, string albumName, int id) {
if (name is null)
throw new ArgumentNullException(nameof(name));
if (artists is null)
throw new ArgumentNullException(nameof(artists));
Name = name;
Artists = artists.Select(t => t.Trim()).ToArray();
AlbumName = albumName;
Id = id;
}
}
}
================================================
FILE: NLyric/FastConsole.cs
================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace NLyric {
internal static class FastConsole {
private const int INTERVAL = 5; // 间隔多少毫秒再次检测是否有新文本
private const int MAX_INTERVAL = 200; // 间隔多少毫秒后强制输出
private const int MAX_TEXT_COUNT = 5000; // 超过多少条文本后后强制输出
private static volatile Thread _singleThread;
private static bool _isIdle = true;
private static readonly Queue<(string Text, ConsoleColor Color)> _queue = new Queue<(string Text, ConsoleColor Color)>();
private static readonly object _ioLock = new object();
private static readonly object _stLock = new object();
private static ConsoleColor _lastColor;
///
/// 设置只允许指定线程写入控制台
///
public static Thread SingleThread {
get => _singleThread;
set {
relock:
lock (_stLock) {
var singleThread = _singleThread;
if (!(singleThread is null) && Thread.CurrentThread != singleThread) {
Monitor.Wait(_stLock);
goto relock;
}
// 如果不符合设置设置SingleThread的条件,需要等待
if (singleThread is null || Thread.CurrentThread == singleThread) {
_singleThread = value;
if (value is null)
Monitor.PulseAll(_stLock);
// 设置为null则取消阻塞其它线程
}
}
}
}
///
/// 单线程锁,化简
///
public static IDisposable SingleThreadLock => new AutoSingleThreadLock();
public static bool IsIdle => _isIdle;
public static int QueueCount => _queue.Count;
static FastConsole() {
new Thread(IOLoop) {
Name = $"{nameof(FastConsole)}.{nameof(IOLoop)}",
IsBackground = true
}.Start();
}
public static void WriteNewLine() {
WriteLine(string.Empty, ConsoleColor.Gray);
}
public static void WriteInfo(string value) {
WriteLine(value, ConsoleColor.Gray);
}
public static void WriteWarning(string value) {
WriteLine(value, ConsoleColor.Yellow);
}
public static void WriteError(string value) {
WriteLine(value, ConsoleColor.Red);
}
public static void WriteLine(string value, ConsoleColor color) {
Write(value + Environment.NewLine, color);
}
public static void Write(string value, ConsoleColor color) {
relock:
lock (_stLock) {
var singleThread = _singleThread;
if (!(singleThread is null) && Thread.CurrentThread != singleThread) {
Monitor.Wait(_stLock);
goto relock;
}
lock (((ICollection)_queue).SyncRoot) {
if (string.IsNullOrEmpty(value))
color = _lastColor;
// 优化空行显示
_queue.Enqueue((value, color));
_lastColor = color;
}
lock (_ioLock)
Monitor.Pulse(_ioLock);
}
}
public static void WriteException(Exception value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
WriteError(ExceptionToString(value));
}
public static void Synchronize() {
while (!_isIdle || _queue.Count != 0)
Thread.Sleep(INTERVAL / 3);
}
private static string ExceptionToString(Exception exception) {
if (exception is null)
throw new ArgumentNullException(nameof(exception));
var sb = new StringBuilder();
DumpException(exception, sb);
return sb.ToString();
}
private static void DumpException(Exception exception, StringBuilder sb) {
sb.AppendLine($"Type: {Environment.NewLine}{exception.GetType().FullName}");
sb.AppendLine($"Message: {Environment.NewLine}{exception.Message}");
sb.AppendLine($"Source: {Environment.NewLine}{exception.Source}");
sb.AppendLine($"StackTrace: {Environment.NewLine}{exception.StackTrace}");
sb.AppendLine($"TargetSite: {Environment.NewLine}{exception.TargetSite}");
sb.AppendLine("----------------------------------------");
if (!(exception.InnerException is null))
DumpException(exception.InnerException, sb);
}
private static void IOLoop() {
var sb = new StringBuilder();
while (true) {
_isIdle = true;
if (_queue.Count == 0) {
lock (_ioLock)
Monitor.Wait(_ioLock);
}
_isIdle = false;
// 等待输出被触发
int delayCount = 0;
int oldCount;
do {
oldCount = _queue.Count;
Thread.Sleep(INTERVAL);
delayCount++;
} while (_queue.Count > oldCount && delayCount < MAX_INTERVAL / INTERVAL && _queue.Count < MAX_TEXT_COUNT);
// 也许此时有其它要输出的内容
var currents = default(Queue<(string Text, ConsoleColor Color)>);
lock (((ICollection)_queue).SyncRoot) {
currents = new Queue<(string, ConsoleColor)>(_queue);
_queue.Clear();
}
// 获取全部要输出的内容
do {
var (text, color) = currents.Dequeue();
sb.Length = 0;
sb.Append(text);
while (true) {
if (currents.Count == 0)
break;
var (nextText, nextColor) = currents.Peek();
if (nextColor != color)
break;
currents.Dequeue();
sb.Append(nextText);
}
// 合并颜色相同,减少重绘带来的性能损失
var oldColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.Write(sb.ToString());
Console.ForegroundColor = oldColor;
} while (currents.Count > 0);
}
}
public static ConsoleKeyInfo ReadKey(bool intercept) {
using (SingleThreadLock)
return Console.ReadKey(intercept);
}
public static string ReadLine() {
using (SingleThreadLock)
return Console.ReadLine();
}
private sealed class AutoSingleThreadLock : IDisposable {
public AutoSingleThreadLock() {
SingleThread = Thread.CurrentThread;
Synchronize();
}
void IDisposable.Dispose() {
if (SingleThread is null)
throw new InvalidOperationException();
SingleThread = null;
}
}
}
}
================================================
FILE: NLyric/Levenshtein.cs
================================================
using System;
namespace NLyric {
internal static class Levenshtein {
///
/// 计算相似度
///
///
///
///
public static double Compute(string x, string y) {
int[,] matrix = new int[x.Length + 1, y.Length + 1];
for (int i = 0; i <= x.Length; i++)
matrix[i, 0] = i;
for (int i = 0; i <= y.Length; i++)
matrix[0, i] = i;
for (int i = 1; i <= x.Length; i++) {
for (int j = 1; j <= y.Length; j++) {
int cost = x[i - 1] == y[j - 1] ? 0 : 1;
matrix[i, j] = Math.Min(Math.Min(matrix[i - 1, j - 1] + cost, matrix[i, j - 1] + 1), matrix[i - 1, j] + 1);
}
}
return 1 - ((double)matrix[x.Length, y.Length] / Math.Max(x.Length, y.Length));
}
///
/// 计算相似度
///
///
///
///
public static double Compute(T[] x, T[] y, Comparison comparison) {
int[,] matrix = new int[x.Length + 1, y.Length + 1];
for (int i = 0; i <= x.Length; i++)
matrix[i, 0] = i;
for (int i = 0; i <= y.Length; i++)
matrix[0, i] = i;
for (int i = 1; i <= x.Length; i++) {
for (int j = 1; j <= y.Length; j++) {
int cost = comparison(x[i - 1], y[j - 1]) == 0 ? 0 : 1;
matrix[i, j] = Math.Min(Math.Min(matrix[i - 1, j - 1] + cost, matrix[i, j - 1] + 1), matrix[i - 1, j] + 1);
}
}
return 1 - ((double)matrix[x.Length, y.Length] / Math.Max(x.Length, y.Length));
}
}
}
================================================
FILE: NLyric/Lyrics/Lrc.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace NLyric.Lyrics {
public sealed class Lrc {
const string TI = "ti:";
const string AR = "ar:";
const string AL = "al:";
const string BY = "by:";
const string OFFSET = "offset:";
private string _title;
private string _artist;
private string _album;
private string _by;
private TimeSpan? _offset;
private IDictionary _lyrics = new Dictionary();
public string Title {
get => _title;
set {
if (value is null) {
_title = value;
return;
}
value = value.Trim();
_title = value.Length == 0 ? null : value;
}
}
public string Artist {
get => _artist;
set {
if (value is null) {
_artist = value;
return;
}
value = value.Trim();
_artist = value.Length == 0 ? null : value;
}
}
public string Album {
get => _album;
set {
if (value is null) {
_album = value;
return;
}
value = value.Trim();
_album = value.Length == 0 ? null : value;
}
}
public string By {
get => _by;
set {
if (value is null) {
_by = value;
return;
}
value = value.Trim();
_by = value.Length == 0 ? null : value;
}
}
public TimeSpan? Offset {
get => _offset;
set => _offset = (value is null || value.Value.Ticks == 0) ? null : value;
}
public IDictionary Lyrics {
get => _lyrics;
set {
if (value is null)
throw new ArgumentNullException(nameof(value));
_lyrics = value;
}
}
public static Lrc Parse(string text) {
if (string.IsNullOrEmpty(text))
throw new ArgumentNullException(nameof(text));
var lrc = new Lrc();
using var reader = new StringReader(text);
string line;
while (!((line = reader.ReadLine()?.Trim()) is null) && !string.IsNullOrEmpty(line)) {
if (!TryParseLine(line, lrc))
throw new FormatException();
}
return lrc;
}
public static Lrc UnsafeParse(string text) {
if (string.IsNullOrEmpty(text))
throw new ArgumentNullException(nameof(text));
var lrc = new Lrc();
using var reader = new StringReader(text);
string line;
while (!((line = reader.ReadLine()?.Trim()) is null))
TryParseLine(line.Trim(), lrc);
return lrc;
}
private static bool TryParseLine(string line, Lrc lrc) {
if (string.IsNullOrEmpty(line) || line[0] != '[')
return false;
int startIndex = 0;
int endIndex;
var times = new List();
do {
endIndex = line.IndexOf(']', startIndex + 1);
if (endIndex == -1)
// 有"["但是没有"]"
return false;
string token = line.Substring(startIndex + 1, endIndex - startIndex - 1);
if (token.StartsWith(TI))
lrc.Title = GetMetadata(token, TI);
else if (token.StartsWith(AR))
lrc.Artist = GetMetadata(token, AR);
else if (token.StartsWith(AL))
lrc.Album = GetMetadata(token, AL);
else if (token.StartsWith(BY))
lrc.By = GetMetadata(token, BY);
else if (token.StartsWith(OFFSET)) {
if (!int.TryParse(GetMetadata(token, OFFSET), out int offset))
return false;
lrc.Offset = new TimeSpan(0, 0, 0, 0, offset);
}
else {
if (!TimeSpan.TryParse("00:" + token, out var time))
return false;
times.Add(time);
}
} while ((startIndex = line.IndexOf('[', endIndex + 1)) != -1);
string lyric = line.Substring(endIndex + 1).Trim();
foreach (var time in times)
lrc._lyrics[time] = lyric;
return true;
string GetMetadata(string _line, string _key) {
return _line.Substring(_key.Length, _line.Length - _key.Length);
}
}
public override string ToString() {
var sb = new StringBuilder();
if (!(_title is null))
AppendLine(sb, TI, _title);
if (!(_artist is null))
AppendLine(sb, AR, _artist);
if (!(_album is null))
AppendLine(sb, AL, _album);
if (!(_by is null))
AppendLine(sb, BY, _by);
if (!(_offset is null))
AppendLine(sb, OFFSET, ((long)_offset.Value.TotalMilliseconds).ToString());
foreach (var lyric in _lyrics)
sb.AppendLine($"[{TimeSpanToLyricString(lyric.Key)}]{lyric.Value}");
return sb.ToString();
void AppendLine(StringBuilder _sb, string key, string value) {
_sb.AppendLine($"[{key}{value}]");
}
string TimeSpanToLyricString(TimeSpan _timeSpan) {
string milliseconds = _timeSpan.Milliseconds.ToString("D3");
return $"{_timeSpan.Minutes:D2}:{_timeSpan.Seconds:D2}.{milliseconds.Substring(0, 2)}";
}
}
}
}
================================================
FILE: NLyric/NLyric.csproj
================================================
$(ProjectName)
2.6.1.0
Copyright © 2019-2021 Wwh
netcoreapp3.1;net472
8.0
..\bin\$(Configuration)
Exe
PreserveNewest
================================================
FILE: NLyric/NLyricImpl.cs
================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NeteaseCloudMusicApi;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLyric.Audio;
using NLyric.Database;
using NLyric.Lyrics;
using NLyric.Ncm;
using NLyric.Settings;
using TagLib;
using File = System.IO.File;
namespace NLyric {
public static class NLyricImpl {
private static readonly SearchSettings _searchSettings = AllSettings.Default.Search;
private static readonly FuzzySettings _fuzzySettings = AllSettings.Default.Fuzzy;
private static readonly MatchSettings _matchSettings = AllSettings.Default.Match;
private static readonly LyricSettings _lyricSettings = AllSettings.Default.Lyric;
private static readonly CloudMusic _cloudMusic = new CloudMusic();
private static readonly HashSet _failMatchAlbums = new HashSet();
// AlbumName
private static readonly Dictionary _cachedNcmTrackses = new Dictionary();
// AlbumId -> Tracks
private static readonly Dictionary _cachedNcmLyrics = new Dictionary();
// TrackId -> Lyric
private static NLyricDatabase _database;
public static async Task ExecuteAsync(Arguments arguments) {
FastConsole.WriteLine("程序会自动过滤相似度为0的结果与歌词未被收集的结果!!!", ConsoleColor.Green);
var loginTask = LoginIfNeedAsync(arguments);
string databasePath = Path.Combine(arguments.Directory, ".nlyric");
LoadDatabase(databasePath);
var audioInfos = LoadAllAudioInfos(arguments.Directory);
var audioInfoCandidates = audioInfos.Where(t => t.TrackInfo is null).ToArray();
await loginTask;
// 登录同时进行
if (!arguments.UpdateOnly) {
if (arguments.UseBatch)
_ = AccelerateAllTracksAsync(audioInfoCandidates);
await LoadAllAudioInfoCandidates(audioInfoCandidates, _ => SaveDatabaseCore(databasePath));
}
audioInfos = audioInfos.Where(t => !(t.TrackInfo is null)).ToArray();
if (arguments.UseBatch)
_ = AccelerateAllLyricsAsync(audioInfos);
await DownloadLyricsAsync(audioInfos);
SaveDatabase(databasePath);
}
private static async Task LoginIfNeedAsync(Arguments arguments) {
if (string.IsNullOrEmpty(arguments.Account) || string.IsNullOrEmpty(arguments.Password)) {
FastConsole.WriteLine("登录可避免出现大部分API错误!!!当前是免登录状态,若软件出错请尝试登录!!!", ConsoleColor.Green);
FastConsole.WriteLine("强烈建议登录使用软件:\"NLyric.exe -d C:\\Music -a example@example.com -p 123456\"", ConsoleColor.Green);
}
else {
FastConsole.WriteLine("登录中...", ConsoleColor.Green);
if (await _cloudMusic.LoginAsync(arguments.Account, arguments.Password)) {
FastConsole.WriteLine("登录成功!", ConsoleColor.Green);
}
else {
FastConsole.WriteError("登录失败,输入任意键以免登录模式运行或重新运行尝试再次登录!");
try {
FastConsole.ReadKey(true);
}
catch {
}
}
}
FastConsole.WriteNewLine();
}
private static bool CanSkip(string audioPath, string lrcPath) {
string extension = Path.GetExtension(audioPath);
if (!IsAudioFile(extension))
return true;
if (File.Exists(lrcPath) && !_lyricSettings.AutoUpdate && !_lyricSettings.Overwriting) {
FastConsole.WriteInfo($"文件\"{Path.GetFileName(audioPath)}\"的歌词已存在,并且自动更新与覆盖已被禁止,正在跳过。");
return true;
}
return false;
}
private static bool IsAudioFile(string extension) {
return _searchSettings.AudioExtensions.Any(s => extension.Equals(s, StringComparison.OrdinalIgnoreCase));
}
private static AudioInfo[] LoadAllAudioInfos(string directory) {
return Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).Where(audioPath => {
string lrcPath = Path.ChangeExtension(audioPath, ".lrc");
return !CanSkip(audioPath, lrcPath);
}).AsParallel().AsOrdered().Select(audioPath => {
var audioFile = default(TagLib.File);
var audioInfo = new AudioInfo {
Path = audioPath
};
var tag = default(Tag);
try {
audioFile = TagLib.File.Create(audioPath);
tag = audioFile.Tag;
if (Album.HasAlbumInfo(tag))
audioInfo.Album = new Album(tag, true);
audioInfo.Track = new Track(tag);
}
catch (Exception ex) {
FastConsole.WriteError("无效音频文件!");
FastConsole.WriteException(ex);
return null;
}
finally {
audioFile?.Dispose();
}
TrackInfo trackInfo;
lock (_database.TrackInfos)
trackInfo = _database.TrackInfos.Match(audioInfo.Album, audioInfo.Track);
if (!(trackInfo is null)) {
audioInfo.TrackInfo = trackInfo;
return audioInfo;
}
// 尝试从数据库获取歌曲
if (The163KeyHelper.TryGetTrackId(tag, out int trackId)) {
trackInfo = new TrackInfo(audioInfo.Track, audioInfo.Album, trackId);
lock (_database.TrackInfos)
_database.TrackInfos.Add(trackInfo);
audioInfo.TrackInfo = trackInfo;
return audioInfo;
}
// 尝试从163Key获取ID
return audioInfo;
}).Where(t => !(t is null)).ToArray();
}
private static async Task LoadAllAudioInfoCandidates(AudioInfo[] audioInfoCandidates, Action callback) {
foreach (var candidate in audioInfoCandidates) {
FastConsole.WriteInfo($"开始获取文件\"{Path.GetFileName(candidate.Path)}\"的网易云音乐ID。");
TrackInfo trackInfo;
try {
trackInfo = await SearchTrackAsync(candidate.Album, candidate.Track);
}
catch (Exception ex) {
FastConsole.WriteException(ex);
trackInfo = null;
}
if (trackInfo is null) {
FastConsole.WriteWarning($"无法找到文件\"{Path.GetFileName(candidate.Path)}\"的网易云音乐ID!");
}
else {
FastConsole.WriteInfo($"已获取文件\"{Path.GetFileName(candidate.Path)}\"的网易云音乐ID: {trackInfo.Id}。");
candidate.TrackInfo = new TrackInfo(candidate.Track, candidate.Album, trackInfo.Id);
_database.TrackInfos.Add(candidate.TrackInfo);
}
callback?.Invoke(candidate);
FastConsole.WriteNewLine();
}
}
private static async Task DownloadLyricsAsync(AudioInfo[] audioInfos) {
foreach (var audioInfo in audioInfos)
await TryDownloadLyricAsync(audioInfo);
}
#region search
///
/// 同时根据专辑信息以及歌曲信息获取网易云音乐上的歌曲
///
///
///
///
private static async Task SearchTrackAsync(Album album, Track track) {
var albumInfo = album is null ? null : await SearchAlbumAsync(album);
// 尝试获取专辑信息
var ncmTrack = default(NcmTrack);
if (!(albumInfo is null)) {
// 网易云音乐收录了歌曲所在专辑
var ncmTracks = (await GetAlbumTracksAsync(albumInfo)).Where(t => ComputeSimilarity(t.Name, track.Name, false) != 0).ToArray();
// 获取网易云音乐上专辑收录的歌曲
ncmTrack = MatchByUser(ncmTracks, track);
}
else {
ncmTrack = null;
}
if (ncmTrack is null)
ncmTrack = await MapToAsync(track);
// 没有对应的专辑信息,使用无专辑匹配,或者网易云音乐上的专辑可能没收录这个歌曲,不清楚为什么,但是确实存在这个情况,比如专辑id:3094396
bool byUser;
int trackId;
if (ncmTrack is null) {
byUser = GetIdByUser("歌曲", out trackId);
}
else {
byUser = false;
trackId = 0;
}
var trackInfo = default(TrackInfo);
if (ncmTrack is null && !byUser) {
trackInfo = null;
FastConsole.WriteWarning("歌曲匹配失败!");
}
else {
trackInfo = new TrackInfo(track, album, byUser ? trackId : ncmTrack.Id);
_database.TrackInfos.Add(trackInfo);
FastConsole.WriteInfo("歌曲匹配成功!");
}
return trackInfo;
}
///
/// 根据专辑信息获取网易云音乐上的专辑
///
///
///
private static async Task SearchAlbumAsync(Album album) {
var albumInfo = _database.AlbumInfos.Match(album);
if (!(albumInfo is null))
return albumInfo;
// 先尝试从数据库获取专辑
string replacedAlbumName = album.Name.ReplaceEx();
if (_failMatchAlbums.Contains(replacedAlbumName))
return null;
// 防止不停重复匹配一个专辑
var ncmAlbum = await MapToAsync(album);
bool byUser;
int albumId;
if (ncmAlbum is null) {
byUser = GetIdByUser("专辑", out albumId);
}
else {
byUser = false;
albumId = 0;
}
if (ncmAlbum is null && !byUser) {
_failMatchAlbums.Add(replacedAlbumName);
FastConsole.WriteWarning("专辑匹配失败!");
}
else {
albumInfo = new AlbumInfo(album, byUser ? albumId : ncmAlbum.Id);
_database.AlbumInfos.Add(albumInfo);
FastConsole.WriteInfo("专辑匹配成功!");
}
return albumInfo;
}
private static async Task GetAlbumTracksAsync(AlbumInfo albumInfo) {
if (!_cachedNcmTrackses.TryGetValue(albumInfo.Id, out var ncmTracks)) {
var list = new List();
foreach (var item in await _cloudMusic.GetTracksAsync(albumInfo.Id)) {
if ((await GetLyricAsync(item.Id)).IsCollected)
list.Add(item);
}
ncmTracks = list.ToArray();
lock (((ICollection)_cachedNcmTrackses).SyncRoot)
_cachedNcmTrackses[albumInfo.Id] = ncmTracks;
}
return ncmTracks;
}
#endregion
#region map
///
/// 获取网易云音乐上的歌曲,自动尝试带艺术家与不带艺术家搜索
///
///
///
private static async Task MapToAsync(Track track) {
if (track is null)
throw new ArgumentNullException(nameof(track));
FastConsole.WriteInfo($"开始搜索歌曲\"{track}\"。");
FastConsole.WriteWarning("正在尝试带艺术家搜索,结果可能将过少!");
var ncmTrack = await MapToAsync(track, true);
if (ncmTrack is null && _fuzzySettings.TryIgnoringArtists) {
FastConsole.WriteWarning("正在尝试忽略艺术家搜索,结果可能将不精确!");
ncmTrack = await MapToAsync(track, false);
}
return ncmTrack;
}
///
/// 获取网易云音乐上的专辑,自动尝试带艺术家与不带艺术家搜索
///
///
///
private static async Task MapToAsync(Album album) {
if (album is null)
throw new ArgumentNullException(nameof(album));
FastConsole.WriteInfo($"开始搜索专辑\"{album}\"。");
FastConsole.WriteWarning("正在尝试带艺术家搜索,结果可能将过少!");
var ncmAlbum = await MapToAsync(album, true);
if (ncmAlbum is null && _fuzzySettings.TryIgnoringArtists) {
FastConsole.WriteWarning("正在尝试忽略艺术家搜索,结果可能将不精确!");
ncmAlbum = await MapToAsync(album, false);
}
return ncmAlbum;
}
///
/// 获取网易云音乐上的歌曲
///
///
/// 是否带艺术家搜索
///
private static async Task MapToAsync(Track track, bool withArtists) {
var ncmTracks = default(NcmTrack[]);
try {
ncmTracks = await _cloudMusic.SearchTrackAsync(track, _searchSettings.Limit, withArtists);
}
catch (KeywordForbiddenException ex1) {
FastConsole.WriteError(ex1.Message);
return null;
}
catch (Exception ex2) {
FastConsole.WriteException(ex2);
return null;
}
var list = new List();
foreach (var item in ncmTracks.Where(t => ComputeSimilarity(t.Name, track.Name, false) != 0)) {
if ((await GetLyricAsync(item.Id)).IsCollected)
list.Add(item);
}
ncmTracks = list.ToArray();
return MatchByUser(ncmTracks, track);
}
///
/// 获取网易云音乐上的专辑
///
///
/// 是否带艺术家搜索
///
private static async Task MapToAsync(Album album, bool withArtists) {
var ncmAlbums = default(NcmAlbum[]);
try {
ncmAlbums = await _cloudMusic.SearchAlbumAsync(album, _searchSettings.Limit, withArtists);
}
catch (KeywordForbiddenException ex1) {
FastConsole.WriteError(ex1.Message);
return null;
}
catch (Exception ex2) {
FastConsole.WriteException(ex2);
return null;
}
ncmAlbums = ncmAlbums.Where(t => ComputeSimilarity(t.Name, album.Name, false) != 0).ToArray();
return MatchByUser(ncmAlbums, album);
}
#endregion
#region database
private static void LoadDatabase(string databasePath) {
if (File.Exists(databasePath)) {
_database = JsonConvert.DeserializeObject(File.ReadAllText(databasePath));
if (!_database.CheckFormatVersion())
throw new InvalidOperationException("尝试加载新格式数据库。");
if (_database.IsOldFormat()) {
FastConsole.WriteWarning("不兼容的老格式数据库,将被覆盖重建!");
}
else {
SortDatabase();
FastConsole.WriteInfo($"搜索数据库\"{databasePath}\"加载成功。");
FastConsole.WriteNewLine();
return;
}
}
_database = new NLyricDatabase() {
AlbumInfos = new List(),
TrackInfos = new List(),
FormatVersion = 1
};
if (File.Exists(databasePath))
File.Delete(databasePath);
SaveDatabaseCore(databasePath);
File.SetAttributes(databasePath, FileAttributes.Hidden);
}
private static void SaveDatabase(string databasePath) {
SortDatabase();
SaveDatabaseCore(databasePath);
FastConsole.WriteInfo($"搜索数据库\"{databasePath}\"已被保存。");
}
private static void SortDatabase() {
_database.AlbumInfos.Sort((x, y) => string.CompareOrdinal(x.Name, y.Name));
_database.TrackInfos.Sort((x, y) => string.CompareOrdinal(x.Name, y.Name));
}
private static void SaveDatabaseCore(string databasePath) {
using var stream = new FileStream(databasePath, FileMode.OpenOrCreate);
using var writer = new StreamWriter(stream);
writer.Write(FormatJson(JsonConvert.SerializeObject(_database)));
}
private static string FormatJson(string json) {
using var writer = new StringWriter();
using var jsonWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
using var reader = new StringReader(json);
using var jsonReader = new JsonTextReader(reader);
jsonWriter.WriteToken(jsonReader);
return writer.ToString();
}
#endregion
#region match
private static TSource MatchByUser(TSource[] sources, TTarget target) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
if (sources.Length == 0)
return null;
var result = MatchByUser(sources, target, false);
if (result is null && _fuzzySettings.TryIgnoringExtraInfo)
result = MatchByUser(sources, target, true);
return result;
}
private static TSource MatchByUser(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
if (sources.Length == 0)
return null;
var result = MatchExactly(sources, target, fuzzy);
if (!fuzzy || !(result is null))
return result;
// 不是fuzzy模式或者result不为空,可以直接返回结果,不需要用户选择了
var nameSimilarities = new Dictionary();
foreach (var source in sources)
nameSimilarities[source] = ComputeSimilarity(source.Name, target.Name, fuzzy);
return Select(sources.Where(t => nameSimilarities[t] > _matchSettings.MinimumSimilarity).OrderByDescending(t => nameSimilarities[t]).ToArray(), target, nameSimilarities);
}
private static TSource MatchExactly(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
foreach (var source in sources) {
string x = source.Name;
string y = target.Name;
if (fuzzy) {
x = x.Fuzzy();
y = y.Fuzzy();
}
if (x != y)
goto not_equal;
if (source.Artists.Count != target.Artists.Count)
goto not_equal;
for (int i = 0; i < source.Artists.Count; i++) {
x = source.Artists[i];
y = target.Artists[i];
if (fuzzy) {
x = x.Fuzzy();
y = y.Fuzzy();
}
if (x != y)
goto not_equal;
}
return source;
not_equal:
continue;
}
return null;
}
private static TSource Select(TSource[] sources, TTarget target, Dictionary nameSimilarities) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {
if (sources.Length == 0)
return null;
FastConsole.WriteInfo("请手动输入1,2,3...选择匹配的项,若不存在,请直接按下回车键。");
FastConsole.WriteInfo("对比项:" + TrackOrAlbumToString(target));
for (int i = 0; i < sources.Length; i++) {
double nameSimilarity = nameSimilarities[sources[i]];
string text = $"{i + 1}. {sources[i]} (s:{nameSimilarity:F2})";
if (nameSimilarity >= 0.85)
FastConsole.WriteLine(text, ConsoleColor.Green);
else if (nameSimilarity >= 0.5)
FastConsole.WriteLine(text, ConsoleColor.Yellow);
else
FastConsole.WriteInfo(text);
}
var result = default(TSource);
do {
string userInput = FastConsole.ReadLine().Trim();
if (userInput.Length == 0)
break;
if (int.TryParse(userInput, out int index)) {
index -= 1;
if (index >= 0 && index < sources.Length) {
result = sources[index];
break;
}
}
FastConsole.WriteWarning("输入有误,请重新输入!");
} while (true);
if (!(result is null))
FastConsole.WriteInfo("已选择:" + result.ToString());
return result;
static string TrackOrAlbumToString(ITrackOrAlbum trackOrAlbum) {
if (trackOrAlbum.Artists.Count == 0)
return trackOrAlbum.Name;
return trackOrAlbum.Name + " by " + string.Join(",", trackOrAlbum.Artists);
}
}
private static double ComputeSimilarity(string x, string y, bool fuzzy) {
x = x.ReplaceEx();
y = y.ReplaceEx();
if (fuzzy) {
x = x.Fuzzy();
y = y.Fuzzy();
}
x = x.Trim();
y = y.Trim();
return Levenshtein.Compute(x, y);
}
private static bool GetIdByUser(string s, out int id) {
FastConsole.WriteInfo($"请输入{s}的网易云音乐ID,若不存在,请直接按下回车键。");
do {
string userInput = FastConsole.ReadLine().Trim();
if (userInput.Length == 0)
break;
if (int.TryParse(userInput, out id))
return true;
FastConsole.WriteWarning("输入有误,请重新输入!");
} while (true);
id = 0;
return false;
}
#endregion
#region lyric
private static async Task TryDownloadLyricAsync(AudioInfo audioInfo) {
string lrcPath = Path.ChangeExtension(audioInfo.Path, ".lrc");
bool hasLrcFile = File.Exists(lrcPath);
var trackInfo = audioInfo.TrackInfo;
NcmLyric ncmLyric;
try {
ncmLyric = await GetLyricAsync(trackInfo.Id);
}
catch (Exception ex) {
FastConsole.WriteException(ex);
return false;
}
FastConsole.WriteInfo($"正在尝试下载\"{Path.GetFileName(audioInfo.Path)} ({audioInfo.Track})\"的歌词。");
if (hasLrcFile) {
// 如果歌词存在,判断是否需要覆盖或更新
var lyricInfo = trackInfo.Lyric;
string lyricCheckSum = hasLrcFile ? ComputeLyricCheckSum(File.ReadAllBytes(lrcPath)) : null;
if (!(lyricInfo is null) && lyricInfo.CheckSum == lyricCheckSum) {
// 歌词由NLyric创建
if (ncmLyric.RawVersion <= lyricInfo.RawVersion && ncmLyric.TranslatedVersion <= lyricInfo.TranslatedVersion) {
// 是最新版本
FastConsole.WriteInfo("本地歌词已是最新版本,正在跳过。");
return false;
}
else {
// 不是最新版本
if (_lyricSettings.AutoUpdate) {
FastConsole.WriteLine("本地歌词不是最新版本,正在更新。", ConsoleColor.Green);
}
else {
FastConsole.WriteLine("本地歌词不是最新版本但是自动更新被禁止,正在跳过。", ConsoleColor.Yellow);
return false;
}
}
}
else {
// 歌词非NLyric创建
if (_lyricSettings.Overwriting) {
FastConsole.WriteLine("本地歌词非NLyric创建,正在更新。", ConsoleColor.Yellow);
}
else {
FastConsole.WriteLine("本地歌词非NLyric创建但是覆盖被禁止,正在跳过。", ConsoleColor.Yellow);
return false;
}
}
}
var lrc = ToLrc(ncmLyric);
if (!(lrc is null)) {
// 歌词已收录,不是纯音乐
string lyric = lrc.ToString();
try {
File.WriteAllText(lrcPath, lyric, _lyricSettings.Encoding);
}
catch (Exception ex) {
FastConsole.WriteException(ex);
return false;
}
trackInfo.Lyric = new LyricInfo(ncmLyric, ComputeLyricCheckSum(_lyricSettings.Encoding.GetBytes(lyric)));
FastConsole.WriteLine("本地歌词下载完毕。", ConsoleColor.Magenta);
}
return true;
}
private static async Task GetLyricAsync(int trackId) {
if (!_cachedNcmLyrics.TryGetValue(trackId, out var lyric)) {
lyric = await _cloudMusic.GetLyricAsync(trackId);
lock (((ICollection)_cachedNcmLyrics).SyncRoot)
_cachedNcmLyrics[trackId] = lyric;
}
return lyric;
}
private static Lrc ToLrc(NcmLyric lyric) {
if (!lyric.IsCollected) {
FastConsole.WriteWarning("当前歌曲的歌词未被收录!");
return null;
}
if (lyric.IsAbsoluteMusic) {
FastConsole.WriteWarning("当前歌曲是纯音乐无歌词!");
return null;
}
if (!(lyric.Raw is null))
NormalizeLyric(lyric.Raw, false);
if (!(lyric.Translated is null))
NormalizeLyric(lyric.Translated, _lyricSettings.SimplifyTranslated);
foreach (string mode in _lyricSettings.Modes) {
switch (mode.ToUpperInvariant()) {
case "MERGED":
if (lyric.Raw is null || lyric.Translated is null)
continue;
FastConsole.WriteInfo("已获取混合歌词。");
return MergeLyric(lyric.Raw, lyric.Translated);
case "RAW":
if (lyric.Raw is null)
continue;
FastConsole.WriteInfo("已获取原始歌词。");
return lyric.Raw;
case "TRANSLATED":
if (lyric.Translated is null)
continue;
FastConsole.WriteInfo("已获取翻译歌词。");
return lyric.Translated;
default:
throw new ArgumentOutOfRangeException(nameof(mode));
}
}
FastConsole.WriteWarning("获取歌词失败(可能歌曲是纯音乐但是未被网易云音乐标记为纯音乐)。");
return null;
}
private static void NormalizeLyric(Lrc lrc, bool simplify) {
var newLyrics = new Dictionary(lrc.Lyrics.Count);
foreach (var lyric in lrc.Lyrics) {
string value = lyric.Value.Trim('/', ' ');
if (simplify)
value = ChineseConverter.TraditionalToSimplified(value);
newLyrics.Add(lyric.Key, value);
}
lrc.Lyrics = newLyrics;
}
private static Lrc MergeLyric(Lrc rawLrc, Lrc translatedLrc) {
var mergedLrc = new Lrc {
Offset = rawLrc.Offset,
Title = rawLrc.Title
};
foreach (var rawLyric in rawLrc.Lyrics)
mergedLrc.Lyrics.Add(rawLyric.Key, rawLyric.Value);
foreach (var translatedLyric in translatedLrc.Lyrics) {
if (translatedLyric.Value.Length == 0)
continue;
// 如果翻译歌词是空字符串,跳过
if (!string.IsNullOrEmpty(translatedLyric.Value) && !mergedLrc.Lyrics.ContainsKey(translatedLyric.Key)) {
// 如果有翻译歌词并且没有对应的未翻译歌词,直接添加
mergedLrc.Lyrics.Add(translatedLyric.Key, translatedLyric.Value);
continue;
}
string rawLyric = mergedLrc.Lyrics[translatedLyric.Key];
if (rawLyric.Length != 0)
mergedLrc.Lyrics[translatedLyric.Key] = $"{rawLyric} 「{translatedLyric.Value}」";
// 如果未翻译歌词是空字符串,表示上一句歌词的结束,那么跳过
}
return mergedLrc;
}
private static string ComputeLyricCheckSum(byte[] lyric) {
return CRC32.Compute(lyric).ToString("X8");
}
#endregion
#region accelerate
private static Task AccelerateAllTracksAsync(AudioInfo[] audioInfos) {
// TODO
return Task.CompletedTask;
}
private static async Task AccelerateAllLyricsAsync(AudioInfo[] audioInfos) {
const int STEP = 50;
int[] trackIds = audioInfos.Select(t => t.TrackInfo.Id).ToArray();
for (int i = 0; i < trackIds.Length; i += STEP) {
var trackIdMap = new Dictionary(STEP);
var queries = new Dictionary(STEP);
int kMax = i + STEP <= trackIds.Length ? STEP : trackIds.Length % STEP;
for (int k = 0; k < kMax; k++) {
string route = "/api/song/lyric" + new string('/', k);
trackIdMap[route] = trackIds[i + k];
queries[route] = JsonConvert.SerializeObject(new Dictionary {
["id"] = trackIds[i + k],
["lv"] = -1,
["kv"] = -1,
["tv"] = -1
});
}
var (isOk, json) = await _cloudMusic.Api.RequestAsync(CloudMusicApiProviders.Batch, queries);
if (!isOk) {
FastConsole.WriteError($"[Experimental] 歌词 {i}+{STEP} 加速失败!");
continue;
}
lock (((ICollection)_cachedNcmLyrics).SyncRoot) {
foreach (var item in trackIdMap) {
int trackId = item.Value;
if (!(json[item.Key] is JObject lyricJson)) {
FastConsole.WriteError($"[Experimental] 歌词 {trackId} at {i}+{STEP} 加速失败!");
continue;
}
_cachedNcmLyrics[trackId] = _cloudMusic.ParseLyric(trackId, lyricJson);
}
}
}
}
#endregion
private sealed class AudioInfo {
public string Path { get; set; }
public Album Album { get; set; }
public Track Track { get; set; }
public TrackInfo TrackInfo { get; set; }
}
}
}
================================================
FILE: NLyric/Ncm/CloudMusic.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NeteaseCloudMusicApi;
using Newtonsoft.Json.Linq;
using NLyric.Audio;
using NLyric.Lyrics;
namespace NLyric.Ncm {
public sealed class CloudMusic {
private readonly CloudMusicApi _api = new CloudMusicApi();
public CloudMusicApi Api => _api;
public async Task LoginAsync(string account, string password) {
var queries = new Dictionary();
bool isPhone = Regex.Match(account, "^[0-9]+$").Success;
queries[isPhone ? "phone" : "email"] = account;
queries["password"] = password;
var (result, _) = await _api.RequestAsync(isPhone ? CloudMusicApiProviders.LoginCellphone : CloudMusicApiProviders.Login, queries);
return result;
}
public async Task SearchTrackAsync(Track track, int limit, bool withArtists) {
var keywords = new List();
if (track.Name.Length != 0)
keywords.Add(track.Name);
if (withArtists)
keywords.AddRange(track.Artists);
if (keywords.Count == 0)
throw new ArgumentException("歌曲信息无效");
for (int i = 0; i < keywords.Count; i++)
keywords[i] = keywords[i].WholeWordReplace();
var (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary {
{ "keywords", string.Join(" ", keywords) },
{ "type", 1 },
{ "limit", limit }
});
if (!isOk)
throw new ApplicationException(nameof(CloudMusicApiProviders.Search) + " API错误");
if ((JObject)json["result"] is null)
throw new KeywordForbiddenException(string.Join(" ", keywords));
return ParseSearchTracks(json);
}
public NcmTrack[] ParseSearchTracks(JObject json) {
json = (JObject)json["result"];
if (!(json["songs"] is JArray songs))
return Array.Empty();
return songs.Select(t => ParseTrack(t, false)).ToArray();
}
public async Task SearchAlbumAsync(Album album, int limit, bool withArtists) {
var keywords = new List();
if (album.Name.Length != 0)
keywords.Add(album.Name);
if (withArtists)
keywords.AddRange(album.Artists);
if (keywords.Count == 0)
throw new ArgumentException("专辑信息无效");
for (int i = 0; i < keywords.Count; i++)
keywords[i] = keywords[i].WholeWordReplace();
var (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary {
{ "keywords", string.Join(" ", keywords) },
{ "type", 10 },
{ "limit", limit }
});
if (!isOk)
throw new ApplicationException(nameof(CloudMusicApiProviders.Search) + " API错误");
if ((JObject)json["result"] is null)
throw new KeywordForbiddenException(string.Join(" ", keywords));
return ParseSearchAlbums(json);
}
public NcmAlbum[] ParseSearchAlbums(JObject json) {
json = (JObject)json["result"];
if (!(json["albums"] is JArray albums))
return Array.Empty();
// albumCount不可信,搜索"U-87 陈奕迅"返回albums有内容,但是albumCount为0
return albums.Select(t => ParseAlbum(t)).ToArray();
}
public async Task GetTracksAsync(int albumId) {
var (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Album, new Dictionary {
{ "id", albumId }
});
if (!isOk)
throw new ApplicationException(nameof(CloudMusicApiProviders.Album) + " API错误");
return ParseTracks(json);
}
public NcmTrack[] ParseTracks(JObject json) {
return json["songs"].Select(t => ParseTrack(t, true)).ToArray();
}
public async Task GetLyricAsync(int trackId) {
var (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary {
{ "id", trackId }
});
if (!isOk)
throw new ApplicationException(nameof(CloudMusicApiProviders.Lyric) + " API错误");
return ParseLyric(trackId, json);
}
public NcmLyric ParseLyric(int trackId, JObject json) {
if (json is null)
throw new ArgumentNullException(nameof(json));
if ((bool?)json["uncollected"] == true)
return new NcmLyric(trackId, false, false, null, 0, null, 0);
// 未收录
if ((bool?)json["nolyric"] == true)
return new NcmLyric(trackId, true, true, null, 0, null, 0);
// 纯音乐
var (rawLrc, rawVersion) = ParseLyric(json["lrc"]);
var (translatedLrc, translatedVersion) = ParseLyric(json["tlyric"]);
return new NcmLyric(trackId, true, false, rawLrc, rawVersion, translatedLrc, translatedVersion);
}
private NcmAlbum ParseAlbum(JToken json) {
var album = new Album((string)json["name"], ParseNames(json["artists"]));
var ncmAlbum = new NcmAlbum(album, (int)json["id"]);
return ncmAlbum;
}
private NcmTrack ParseTrack(JToken json, bool isShortName) {
var track = new Track((string)json["name"], ParseNames(json[isShortName ? "ar" : "artists"]));
var ncmTrack = new NcmTrack(track, (int)json["id"]);
return ncmTrack;
}
private string[] ParseNames(JToken json) {
return json.Select(t => (string)t["name"]).ToArray();
}
private (Lrc, int) ParseLyric(JToken json) {
string lyric = (string)json["lyric"];
var lrc = string.IsNullOrEmpty(lyric) ? null : Lrc.UnsafeParse(lyric);
int version = (int)json["version"];
return (lrc, version);
}
}
}
================================================
FILE: NLyric/Ncm/KeywordForbiddenException.cs
================================================
using System;
using System.Runtime.Serialization;
namespace NLyric.Ncm {
///
/// 关键词被禁止
///
[Serializable]
public sealed class KeywordForbiddenException : Exception {
public KeywordForbiddenException() {
}
public KeywordForbiddenException(string text) : base($"\"{text}\" 中有关键词被屏蔽") {
}
private KeywordForbiddenException(SerializationInfo info, StreamingContext context) : base(info, context) {
}
}
}
================================================
FILE: NLyric/Ncm/NcmAlbum.cs
================================================
using NLyric.Audio;
namespace NLyric.Ncm {
public sealed class NcmAlbum : Album {
private readonly int _id;
public int Id => _id;
public NcmAlbum(Album album, int id) : base(album.Name, album.Artists) {
_id = id;
}
public override string ToString() {
return $"{base.ToString()} | Id:{_id}";
}
}
}
================================================
FILE: NLyric/Ncm/NcmLyric.cs
================================================
using NLyric.Lyrics;
namespace NLyric.Ncm {
public sealed class NcmLyric {
private readonly int _id;
private readonly bool _isCollected;
private readonly bool _isAbsoluteMusic;
private readonly Lrc _raw;
private readonly int _rawVersion;
private readonly Lrc _translated;
private readonly int _translatedVersion;
public int Id => _id;
public bool IsCollected => _isCollected;
public bool IsAbsoluteMusic => _isAbsoluteMusic;
public Lrc Raw => _raw;
public int RawVersion => _rawVersion;
public Lrc Translated => _translated;
public int TranslatedVersion => _translatedVersion;
public NcmLyric(int id, bool isCollected, bool isAbsoluteMusic, Lrc raw, int rawVersion, Lrc translated, int translatedVersion) {
_id = id;
_isCollected = isCollected;
_isAbsoluteMusic = isAbsoluteMusic;
_raw = raw;
_rawVersion = rawVersion;
_translated = translated;
_translatedVersion = translatedVersion;
}
}
}
================================================
FILE: NLyric/Ncm/NcmTrack.cs
================================================
using NLyric.Audio;
namespace NLyric.Ncm {
public sealed class NcmTrack : Track {
private readonly int _id;
public int Id => _id;
public NcmTrack(Track track, int id) : base(track.Name, track.Artists) {
_id = id;
}
public override string ToString() {
return $"{base.ToString()} | Id:{_id}";
}
}
}
================================================
FILE: NLyric/Program.cs
================================================
using System;
using System.Cli;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLyric.Settings;
namespace NLyric {
public static class Program {
private static async Task Main(string[] args) {
if (args is null || args.Length == 0) {
CommandLine.ShowUsage();
return;
}
try {
Console.Title = GetTitle();
}
catch {
}
if (!CommandLine.TryParse(args, out Arguments arguments)) {
CommandLine.ShowUsage();
return;
}
AllSettings.Default = JsonConvert.DeserializeObject(File.ReadAllText("Settings.json"));
await NLyricImpl.ExecuteAsync(arguments);
FastConsole.WriteLine("完成", ConsoleColor.Green);
FastConsole.Synchronize();
if (Debugger.IsAttached) {
Console.WriteLine("按任意键继续...");
try {
Console.ReadKey(true);
}
catch {
}
}
}
private static string GetTitle() {
string productName = GetAssemblyAttribute().Product;
string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
string copyright = GetAssemblyAttribute().Copyright.Substring(12);
int firstBlankIndex = copyright.IndexOf(' ');
string copyrightOwnerName = copyright.Substring(firstBlankIndex + 1);
string copyrightYear = copyright.Substring(0, firstBlankIndex);
return $"{productName} v{version} by {copyrightOwnerName} {copyrightYear}";
}
private static T GetAssemblyAttribute() {
return (T)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(T), false)[0];
}
}
}
================================================
FILE: NLyric/Settings/AllSettings.cs
================================================
using System;
namespace NLyric.Settings {
internal sealed class AllSettings {
private static AllSettings _default;
public static AllSettings Default {
get {
if (_default is null)
throw new InvalidOperationException();
return _default;
}
set {
if (value is null)
throw new ArgumentNullException(nameof(value));
if (!(_default is null))
throw new InvalidOperationException();
_default = value;
}
}
public SearchSettings Search { get; set; }
public FuzzySettings Fuzzy { get; set; }
public MatchSettings Match { get; set; }
public LyricSettings Lyric { get; set; }
}
}
================================================
FILE: NLyric/Settings/CharArrayJsonConverter.cs
================================================
using System;
using Newtonsoft.Json;
namespace NLyric.Settings {
internal sealed class CharArrayJsonConverter : JsonConverter {
public override char[] ReadJson(JsonReader reader, Type objectType, char[] existingValue, bool hasExistingValue, JsonSerializer serializer) {
return ((string)reader.Value).ToCharArray();
}
public override void WriteJson(JsonWriter writer, char[] value, JsonSerializer serializer) {
writer.WriteValue(new string(value));
}
}
}
================================================
FILE: NLyric/Settings/EncodingConverter.cs
================================================
using System;
using System.Text;
using Newtonsoft.Json;
namespace NLyric.Settings {
internal sealed class EncodingConverter : JsonConverter {
public override Encoding ReadJson(JsonReader reader, Type objectType, Encoding existingValue, bool hasExistingValue, JsonSerializer serializer) {
var encoding = Encoding.GetEncoding((string)reader.Value);
if (encoding is UTF8Encoding)
encoding = new UTF8Encoding(false, true);
return encoding;
}
public override void WriteJson(JsonWriter writer, Encoding value, JsonSerializer serializer) {
writer.WriteValue(value.WebName);
}
}
}
================================================
FILE: NLyric/Settings/FuzzySettings.cs
================================================
using Newtonsoft.Json;
namespace NLyric.Settings {
internal sealed class FuzzySettings {
public bool TryIgnoringArtists { get; set; }
public bool TryIgnoringExtraInfo { get; set; }
[JsonConverter(typeof(CharArrayJsonConverter))]
public char[] ExtraInfoStart { get; set; }
public string[] Covers { get; set; }
public string[] Featurings { get; set; }
}
}
================================================
FILE: NLyric/Settings/LyricSettings.cs
================================================
using System.Text;
using Newtonsoft.Json;
namespace NLyric.Settings {
internal sealed class LyricSettings {
public string[] Modes { get; set; }
public bool SimplifyTranslated { get; set; }
[JsonConverter(typeof(EncodingConverter))]
public Encoding Encoding { get; set; }
public bool AutoUpdate { get; set; }
public bool Overwriting { get; set; }
}
}
================================================
FILE: NLyric/Settings/MatchSettings.cs
================================================
using System;
using System.Collections.Generic;
namespace NLyric.Settings {
internal sealed class MatchSettings {
private double _minimumSimilarity;
public double MinimumSimilarity {
get => _minimumSimilarity;
set {
if (value < 0 || value > 1)
throw new ArgumentOutOfRangeException(nameof(value));
_minimumSimilarity = value;
}
}
public Dictionary CharReplace { get; set; }
}
}
================================================
FILE: NLyric/Settings/SearchSettings.cs
================================================
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NLyric.Settings {
internal sealed class SearchSettings {
public string[] AudioExtensions { get; set; }
[JsonConverter(typeof(CharArrayJsonConverter))]
public char[] Separators { get; set; }
public Dictionary WholeWordReplace { get; set; }
public int Limit { get; set; }
}
}
================================================
FILE: NLyric/Settings.json
================================================
{ // 所有匹配都是忽略大小写的!!!
"Search": { // 搜索设置,在每一次搜索生效。
"AudioExtensions": [
".aac",
".ape",
".flac",
".m4a",
".mp3",
".ogg",
".wav",
".wma"
], // 会被识别为歌曲的扩展名。
"Separators": "|;,/\\&:", // 分隔符,用于分割歌手名。
"WholeWordReplace": {}, // 前面是被替换的词,后面是要替换成的词,比如"A": "B",那么在搜索"A"的时候会替换为"B"来搜索。
"Limit": 15 // 搜索结果数量。
},
"Fuzzy": { // 第一次搜不到或者匹配失败的情况下,是否进行模糊搜索与匹配。
"TryIgnoringArtists": true, // 忽略艺术家。
"TryIgnoringExtraInfo": true, // 忽略 括号/空格 + Cover/feat. 之后的内容,支持的括号类型在Filter.OpenBrackets里。
"ExtraInfoStart": " ([{【〖", // 空格和左括号等之后的内容会被过滤,注意,不要随便修改这里的内容,可能导致过滤准确性降低。
"Covers": [
"Cover",
"カバー"
], // Cover的各种写法。
"Featurings": [
"feat.",
"ft."
] // Feat.的各种写法。
},
"Match": { // 匹配设置,在搜索到歌曲信息之后,程序会通过自己的算法再次确认是否匹配。
"MinimumSimilarity": 0.65, // 匹配时的最小相似度,小于设定值的将不予显示,0~1。
"CharReplace": {
"\u00B7": "\u002e",
"\u0387": "\u002e",
"\u05BC": "\u002e",
"\u2022": "\u002e",
"\u2027": "\u002e",
"\u2219": "\u002e",
"\u22C5": "\u002e",
"\u30FB": "\u002e",
"\uFF65": "\u002e",
// .
"\uFF0A": "\u002A",
// *
"\uFF01": "\u0021",
// !
"\uFF1A": "\u003A",
// :
"\u005B": "\u0028",
"\u007B": "\u0028",
"\u3010": "\u0028",
"\u3016": "\u0028",
// (
"\u005D": "\u0029",
"\u007D": "\u0029",
"\u3011": "\u0029",
"\u3017": "\u0029"
// )
} // 前面是被替换的字符,后面是要替换成的字符,只支持单个字符替换,意思就是一个文字,多个文字会报错。
},
"Lyric": {
"Modes": [
"Merged",
"Raw",
"Translated"
], // 歌词模式,依次尝试每一个模式直到成功,Merged表示混合未翻译和翻译后歌词,Raw表示未翻译的歌词,Translated表示翻译后的歌词。
"SimplifyTranslated": true, // 部分翻译后的歌词是繁体的,这个选项可以简体化翻译后的歌词。
"Encoding": "utf-8",
"AutoUpdate": true, // 是否自动更新由NLyric创建的歌词。
"Overwriting": true // 是否覆盖非NLyric创建的歌词。
}
}
================================================
FILE: NLyric/StringHelper.cs
================================================
using System;
using System.Linq;
using System.Text;
using NLyric.Settings;
namespace NLyric {
internal static class StringHelper {
private static readonly SearchSettings _searchSettings = AllSettings.Default.Search;
private static readonly FuzzySettings _fuzzySettings = AllSettings.Default.Fuzzy;
private static readonly MatchSettings _matchSettings = AllSettings.Default.Match;
///
/// 获取非空字符串,并且清除首尾空格
///
///
///
public static string GetSafeString(this string value) {
return value is null ? string.Empty : value.Trim();
}
///
/// 同时调用 , 与
///
///
///
public static string ReplaceEx(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
return value.ToHalfWidth().WholeWordReplace().CharReplace();
}
///
/// 使用 进行全词替换
///
///
///
public static string WholeWordReplace(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
if (value.Length == 0)
return value;
foreach (var pair in _searchSettings.WholeWordReplace) {
if (value.Equals(pair.Key, StringComparison.OrdinalIgnoreCase))
return pair.Value;
}
return value;
}
///
/// 使用 进行字符替换
///
///
///
public static string CharReplace(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
if (value.Length == 0)
return value;
var sb = new StringBuilder(value);
for (int i = 0; i < sb.Length; i++) {
foreach (var pair in _matchSettings.CharReplace) {
if (sb[i] == pair.Key)
sb[i] = pair.Value;
}
}
return sb.ToString();
}
///
/// 使用 进行模糊处理
///
///
///
public static string Fuzzy(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
int fuzzyStartIndex = -1;
while ((fuzzyStartIndex = value.IndexOfAny(_fuzzySettings.ExtraInfoStart, fuzzyStartIndex + 1)) != -1) {
string extraInfo = value.Substring(fuzzyStartIndex + 1);
if (Enumerable.Concat(_fuzzySettings.Covers, _fuzzySettings.Featurings).Any(s => extraInfo.StartsWith(s, StringComparison.OrdinalIgnoreCase)))
return value.Substring(0, fuzzyStartIndex).TrimEnd();
}
return value;
}
///
/// 使用 进行分割字符串并且移除空字符串
///
///
///
public static string[] SplitEx(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
return value.Split(_searchSettings.Separators, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
}
///
/// 全角字符转半角字符
///
///
///
public static string ToHalfWidth(this string value) {
if (value is null)
throw new ArgumentNullException(nameof(value));
char[] chars = value.ToCharArray();
for (int i = 0; i < chars.Length; i++) {
if (chars[i] == '\u3000')
chars[i] = '\u0020';
else if (chars[i] > '\uFF00' && chars[i] < '\uFF5F')
chars[i] = (char)(chars[i] - 0xFEE0);
}
return new string(chars);
}
}
}
================================================
FILE: NLyric/System/Cli/ArgumentAttribute.cs
================================================
namespace System.Cli {
///
/// 表示一个命令行参数。被应用 的属性必须为 类型或 类型且为实例属性。
///
[AttributeUsage(AttributeTargets.Property)]
internal sealed class ArgumentAttribute : Attribute {
private readonly string _name;
private bool _isRequired;
private object _defaultValue;
private string _type;
private string _description;
///
/// 参数名
///
public string Name => _name;
///
/// 是否为必选参数
///
public bool IsRequired {
get => _isRequired;
set => _isRequired = value;
}
///
/// 默认值,当 为 时, 必须为 。
///
public object DefaultValue {
get => _defaultValue;
set => _defaultValue = value;
}
///
/// 参数类型,用于 显示类型来简单描述参数。若应用到返回类型为 的属性上, 必须为 。
///
public string Type {
get => _type;
set => _type = value;
}
///
/// 参数介绍,用于 具体描述参数。
///
public string Description {
get => _description;
set => _description = value;
}
///
/// 构造器
///
/// 参数名
public ArgumentAttribute(string name) {
_name = name;
}
}
}
================================================
FILE: NLyric/System/Cli/CommandLine.cs
================================================
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace System.Cli {
internal static class CommandLine {
public static T Parse(string[] args) where T : new() {
if (args is null)
throw new ArgumentNullException(nameof(args));
if (!TryParse(args, out T result))
throw new FormatException($"Invalid {nameof(args)} or generic parameter {nameof(T)}");
return result;
}
public static bool TryParse(string[] args, out T result) where T : new() {
if (args is null) {
result = default;
return false;
}
if (!TryGetArgumentInfos(typeof(T), out var argumentInfos)) {
result = default;
return false;
}
result = new T();
for (int i = 0; i < args.Length; i++) {
if (!argumentInfos.TryGetValue(args[i], out var argumentInfo)) {
// 不是有效参数名
result = default;
return false;
}
if (argumentInfo.HasSetValue) {
// 重复设置参数
result = default;
return false;
}
if (argumentInfo.IsBoolean) {
// 是 bool 类型,所以不需要其它判断,直接赋值 true
if (!argumentInfo.TrySetValue(result, true)) {
result = default;
return false;
}
argumentInfo.HasSetValue = true;
continue;
}
if (i == args.Length - 1) {
// 需要提供值但是到末尾了,未提供值
result = default;
return false;
}
if (!argumentInfo.TrySetValue(result, args[++i])) {
result = default;
return false;
}
argumentInfo.HasSetValue = true;
}
foreach (var argumentInfo in argumentInfos.Values) {
if (argumentInfo.HasSetValue)
continue;
// 参数已设置值
if (argumentInfo.IsRequired) {
// 是必选参数
result = default;
return false;
}
else {
// 是可选参数
if (!argumentInfo.TrySetValue(result, argumentInfo.DefaultValue)) {
result = default;
return false;
}
}
}
return true;
}
public static bool ShowUsage() {
var type = typeof(T);
var propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (propertyInfos.Length == 0)
return false;
var argumentInfos = new List();
foreach (var propertyInfo in propertyInfos) {
if (!VerifyProperty(propertyInfo, out var attribute))
return false;
if (attribute is null)
continue;
argumentInfos.Add(new ArgumentInfo(attribute, propertyInfo));
}
int maxNameLength = argumentInfos.Max(t => GetArgumentFormat(t).Length);
var sb = new StringBuilder();
sb.AppendLine("Options:");
foreach (var argumentInfo in argumentInfos) {
sb.Append($" {GetArgumentFormat(argumentInfo).PadRight(maxNameLength)} {argumentInfo.Description}");
if (!argumentInfo.IsRequired)
sb.Append(" (Optional)");
sb.AppendLine();
}
Console.WriteLine(sb.ToString());
return true;
}
private static bool TryGetArgumentInfos(Type type, out Dictionary argumentInfos) {
var propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (propertyInfos.Length == 0) {
argumentInfos = null;
return false;
}
argumentInfos = new Dictionary();
foreach (var propertyInfo in propertyInfos) {
if (!VerifyProperty(propertyInfo, out var attribute)) {
argumentInfos = null;
return false;
}
if (!(attribute is null))
argumentInfos.Add(attribute.Name, new ArgumentInfo(attribute, propertyInfo));
}
return true;
}
private static bool VerifyProperty(PropertyInfo propertyInfo, out ArgumentAttribute argumentAttribute) {
argumentAttribute = null;
object[] attributes = propertyInfo.GetCustomAttributes(typeof(ArgumentAttribute), false);
if (attributes is null || attributes.Length == 0)
// 排除未应用 ArgumentAttribute 的属性
return true;
if (attributes.Length != 1)
// ArgumentAttribute 不应该被应用多次
return false;
var propertyType = propertyInfo.PropertyType;
if (propertyType != typeof(string) && propertyType != typeof(bool))
// 检查返回类型
return false;
argumentAttribute = (ArgumentAttribute)attributes[0];
if (string.IsNullOrEmpty(argumentAttribute.Name)) {
// 检查参数名是否为空
argumentAttribute = null;
return false;
}
foreach (char item in argumentAttribute.Name) {
if (!((item >= 'a' && item <= 'z') || (item >= 'A' && item <= 'Z') || (item >= '0' && item <= '9') || item == '-' || item == '_')) {
// 检查参数名是否合法
argumentAttribute = null;
return false;
}
}
if (argumentAttribute.IsRequired && !(argumentAttribute.DefaultValue is null)) {
// 是必选参数但有默认值
argumentAttribute = null;
return false;
}
if (!(argumentAttribute.DefaultValue is null) && argumentAttribute.DefaultValue.GetType() != propertyType) {
// 有默认值但默认值的类型与属性的类型不相同
argumentAttribute = null;
return false;
}
if (!(argumentAttribute.Type is null) && propertyType == typeof(bool)) {
// 返回类型为bool并且Type属性有值
argumentAttribute = null;
return false;
}
return true;
}
private static string GetArgumentFormat(ArgumentInfo argumentInfo) {
return !argumentInfo.IsBoolean ? argumentInfo.Name + " " + (string.IsNullOrEmpty(argumentInfo.Type) ? "VALUE" : argumentInfo.Type) : argumentInfo.Name;
}
private sealed class ArgumentInfo {
private readonly ArgumentAttribute _attribute;
private readonly PropertyInfo _propertyInfo;
private bool? _cachedIsBoolean;
private bool _hasSetValue;
public string Name => _attribute.Name;
public bool IsRequired => _attribute.IsRequired;
public object DefaultValue => _attribute.DefaultValue;
public string Type => _attribute.Type;
public string Description => _attribute.Description;
public bool IsBoolean {
get {
if (_cachedIsBoolean is null)
_cachedIsBoolean = _propertyInfo.PropertyType == typeof(bool);
return _cachedIsBoolean.Value;
}
}
public bool HasSetValue {
get => _hasSetValue;
set => _hasSetValue = value;
}
public ArgumentInfo(ArgumentAttribute attribute, PropertyInfo propertyInfo) {
_attribute = attribute;
_propertyInfo = propertyInfo;
}
public bool TrySetValue(object instance, object value) {
try {
_propertyInfo.SetValue(instance, value, null);
return true;
}
catch {
return false;
}
}
}
}
}
================================================
FILE: NLyric/The163KeyHelper.cs
================================================
using System;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json.Linq;
using TagLib;
namespace NLyric {
///
/// 通过163Key直接获取歌曲ID
///
internal static class The163KeyHelper {
private static readonly Aes _aes = Create163Aes();
private static Aes Create163Aes() {
var aes = Aes.Create();
aes.BlockSize = 128;
aes.Key = Encoding.UTF8.GetBytes(@"#14ljk_!\]&0U<'(");
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
return aes;
}
///
/// 尝试获取网易云音乐ID
///
///
///
///
public static bool TryGetTrackId(Tag tag, out int trackId) {
if (tag is null)
throw new ArgumentNullException(nameof(tag));
trackId = 0;
string the163Key = tag.Comment;
if (!Is163KeyCandidate(the163Key))
the163Key = tag.Description;
if (!Is163KeyCandidate(the163Key))
return false;
try {
the163Key = the163Key.Substring(22);
byte[] byt163Key = Convert.FromBase64String(the163Key);
using var cryptoTransform = _aes.CreateDecryptor();
byt163Key = cryptoTransform.TransformFinalBlock(byt163Key, 0, byt163Key.Length);
trackId = (int)JObject.Parse(Encoding.UTF8.GetString(byt163Key).Substring(6))["musicId"];
}
catch {
return false;
}
return true;
}
private static bool Is163KeyCandidate(string s) {
return !string.IsNullOrEmpty(s) && s.StartsWith("163 key(Don't modify):", StringComparison.Ordinal);
}
}
}
================================================
FILE: NLyric.Win/MainForm.Designer.cs
================================================
namespace NLyric.Win
{
partial class MainForm
{
///
/// 必需的设计器变量。
///
private System.ComponentModel.IContainer components = null;
///
/// 清理所有正在使用的资源。
///
/// 如果应释放托管资源,为 true;否则为 false。
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
///
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
///
private void InitializeComponent()
{
this._btnSetDirectory = new System.Windows.Forms.Button();
this._tbDirectory = new System.Windows.Forms.TextBox();
this._cbLogin = new System.Windows.Forms.CheckBox();
this._tbAccount = new System.Windows.Forms.TextBox();
this._tbPassword = new System.Windows.Forms.TextBox();
this._btnRun = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// _btnSetDirectory
//
this._btnSetDirectory.Location = new System.Drawing.Point(267, 12);
this._btnSetDirectory.Name = "_btnSetDirectory";
this._btnSetDirectory.Size = new System.Drawing.Size(102, 23);
this._btnSetDirectory.TabIndex = 0;
this._btnSetDirectory.Text = "选择音频文件夹";
this._btnSetDirectory.UseVisualStyleBackColor = true;
this._btnSetDirectory.Click += new System.EventHandler(this._btnSetDirectory_Click);
//
// _tbDirectory
//
this._tbDirectory.Location = new System.Drawing.Point(12, 12);
this._tbDirectory.Name = "_tbDirectory";
this._tbDirectory.Size = new System.Drawing.Size(249, 23);
this._tbDirectory.TabIndex = 1;
//
// _cbLogin
//
this._cbLogin.AutoSize = true;
this._cbLogin.Location = new System.Drawing.Point(12, 43);
this._cbLogin.Name = "_cbLogin";
this._cbLogin.Size = new System.Drawing.Size(75, 21);
this._cbLogin.TabIndex = 2;
this._cbLogin.Text = "登录模式";
this._cbLogin.UseVisualStyleBackColor = true;
this._cbLogin.CheckedChanged += new System.EventHandler(this._cbLogin_CheckedChanged);
//
// _tbAccount
//
this._tbAccount.Location = new System.Drawing.Point(93, 41);
this._tbAccount.Name = "_tbAccount";
this._tbAccount.Size = new System.Drawing.Size(135, 23);
this._tbAccount.TabIndex = 3;
this._tbAccount.Text = "网易云音乐账号";
//
// _tbPassword
//
this._tbPassword.Location = new System.Drawing.Point(234, 41);
this._tbPassword.Name = "_tbPassword";
this._tbPassword.Size = new System.Drawing.Size(135, 23);
this._tbPassword.TabIndex = 4;
this._tbPassword.Text = "网易云音乐密码";
//
// _btnRun
//
this._btnRun.Location = new System.Drawing.Point(375, 12);
this._btnRun.Name = "_btnRun";
this._btnRun.Size = new System.Drawing.Size(71, 52);
this._btnRun.TabIndex = 5;
this._btnRun.Text = "启动";
this._btnRun.UseVisualStyleBackColor = true;
this._btnRun.Click += new System.EventHandler(this._btnRun_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(458, 76);
this.Controls.Add(this._btnRun);
this.Controls.Add(this._tbPassword);
this.Controls.Add(this._tbAccount);
this.Controls.Add(this._cbLogin);
this.Controls.Add(this._tbDirectory);
this.Controls.Add(this._btnSetDirectory);
this.Font = new System.Drawing.Font("Microsoft YaHei", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.Name = "MainForm";
this.Text = "NLyric";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button _btnSetDirectory;
private System.Windows.Forms.TextBox _tbDirectory;
private System.Windows.Forms.CheckBox _cbLogin;
private System.Windows.Forms.TextBox _tbAccount;
private System.Windows.Forms.TextBox _tbPassword;
private System.Windows.Forms.Button _btnRun;
}
}
================================================
FILE: NLyric.Win/MainForm.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
namespace NLyric.Win {
public sealed partial class MainForm : Form {
public MainForm() {
InitializeComponent();
Text = GetTitle(Assembly.Load(File.ReadAllBytes("NLyric.exe")));
_cbLogin_CheckedChanged(_cbLogin, EventArgs.Empty);
}
private static string GetTitle(Assembly assembly) {
string productName = GetAssemblyAttribute(assembly).Product;
string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
string copyright = GetAssemblyAttribute(assembly).Copyright.Substring(12);
int firstBlankIndex = copyright.IndexOf(' ');
string copyrightOwnerName = copyright.Substring(firstBlankIndex + 1);
string copyrightYear = copyright.Substring(0, firstBlankIndex);
return $"{productName} v{version} by {copyrightOwnerName} {copyrightYear}";
}
private static T GetAssemblyAttribute(Assembly assembly) {
return (T)assembly.GetCustomAttributes(typeof(T), false)[0];
}
private void _btnSetDirectory_Click(object sender, EventArgs e) {
using (var dialog = new FolderBrowserDialog { ShowNewFolderButton = false }) {
if (dialog.ShowDialog() != DialogResult.OK)
return;
_tbDirectory.Text = dialog.SelectedPath;
}
}
private void _cbLogin_CheckedChanged(object sender, EventArgs e) {
bool state = _cbLogin.Checked;
_tbAccount.Enabled = state;
_tbPassword.Enabled = state;
if (state) {
if (_tbAccount.Text == "网易云音乐账号")
_tbAccount.Text = string.Empty;
if (_tbPassword.Text == "网易云音乐密码")
_tbPassword.Text = string.Empty;
_tbPassword.PasswordChar = '*';
}
}
private void _btnRun_Click(object sender, EventArgs e) {
string arguments = $"-d \"{_tbDirectory.Text}\"";
if (_cbLogin.Checked)
arguments += $" -a {_tbAccount.Text} -p {_tbPassword.Text}";
Process.Start("NLyric.exe", arguments);
}
}
}
================================================
FILE: NLyric.Win/MainForm.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
True
True
True
True
True
True
True
================================================
FILE: NLyric.Win/NLyric.Win.csproj
================================================
Debug
AnyCPU
{AB36D9B3-ECD3-46EA-973E-320648551850}
WinExe
NLyric.Win
NLyric.Win
v4.0
512
true
AnyCPU
true
full
false
..\bin\Debug\net472\
DEBUG;TRACE
prompt
4
AnyCPU
pdbonly
true
..\bin\Release\net472\
TRACE
prompt
4
Form
MainForm.cs
MainForm.cs
================================================
FILE: NLyric.Win/Program.cs
================================================
using System;
using System.Windows.Forms;
namespace NLyric.Win {
internal static class Program {
[STAThread]
private static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
================================================
FILE: NLyric.Win/Properties/AssemblyInfo.cs
================================================
using System.Reflection;
[assembly: AssemblyTitle("NLyric.Win")]
[assembly: AssemblyProduct("NLyric.Win")]
[assembly: AssemblyCopyright("Copyright © 2019 Wwh")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
================================================
FILE: NLyric.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28822.285
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLyric", "NLyric\NLyric.csproj", "{5423689E-6713-4F96-82E3-48B11E2A6412}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLyric.Win", "NLyric.Win\NLyric.Win.csproj", "{AB36D9B3-ECD3-46EA-973E-320648551850}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5423689E-6713-4F96-82E3-48B11E2A6412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5423689E-6713-4F96-82E3-48B11E2A6412}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5423689E-6713-4F96-82E3-48B11E2A6412}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5423689E-6713-4F96-82E3-48B11E2A6412}.Release|Any CPU.Build.0 = Release|Any CPU
{AB36D9B3-ECD3-46EA-973E-320648551850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB36D9B3-ECD3-46EA-973E-320648551850}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB36D9B3-ECD3-46EA-973E-320648551850}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB36D9B3-ECD3-46EA-973E-320648551850}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8F4FF140-407E-434C-8FEC-6736239F77A6}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
# NLyric
网易云音乐歌词自动搜索下载
可选择登录或免登陆下载,避免出现网易云音乐接口异常。
网易云音乐已屏蔽部分关键字,导致搜索出现异常,属于正常现象(网易云音乐客户端内很多歌曲名已经打上\*号)。
**New: Windows用户可以解压后双击"NLyric.Win.exe"启动GUI!!!**

Windows用户专属GUI。

自动下载。

自动匹配专辑。

在非完全精确匹配到的情况下,提供用户选项,并且按照匹配程度排序并高亮显示。

再次运行时,自动判断本地歌词是否需要更新(比如网易云音乐上歌词更新了翻译,NLyric可以检测到)。

提供歌词混合模式,同时显示原始歌词与翻译歌词。

第一次运行后缓存搜索结果,加快以后运行速度(会在音频文件夹创建隐藏文件.nlyric,请勿删除)。
## 使用方式
1. 下载压缩包(下载地址在下面),全部解压。
2. 进入解压后的文件夹(内有NLyric.exe等文件),在文件夹内按住Shift,鼠标单击右键,选"在此处打开命令窗口"
3. 输入命令"NLyric.exe -d *音乐文件夹* -a *网易云音乐账号* -p *网易云音乐密码*"以登录模式启动,或输入命令"NLyric.exe -d *音乐文件夹*"以免登录模式启动(NLyric不会保存您的账号密码或将您的账号密码发送到第三方,NLyric仅会调用网易云音乐官方API)
4. 按照程序提示完成接下来的步骤
命令行参数:
```
Options:
-d DIR 存放音乐的文件夹,可以是相对路径或者绝对路径 (Optional)
-a STR 网易云音乐账号(邮箱/手机号) (Optional)
-p STR 网易云音乐密码 (Optional)
--update-only 仅更新已有歌词 (Optional)
--batch 使用Batch API(实验性) (Optional)
```
例子:
```
NLyric.exe -d C:\Music -a example@example.com -p 123456 --batch
```
## 配置
配置文件是"Settings.json",自己修改即可
默认配置:
``` javascript
{ // 所有匹配都是忽略大小写的!!!
"Search": { // 搜索设置,在每一次搜索生效。
"AudioExtensions": [
".aac",
".ape",
".flac",
".m4a",
".mp3",
".ogg",
".wav",
".wma"
], // 会被识别为歌曲的扩展名。
"Separators": "|;,/\\&:", // 分隔符,用于分割歌手名。
"WholeWordReplace": {}, // 前面是被替换的词,后面是要替换成的词,比如"A": "B",那么在搜索"A"的时候会替换为"B"来搜索。
"Limit": 15 // 搜索结果数量。
},
"Fuzzy": { // 第一次搜不到或者匹配失败的情况下,是否进行模糊搜索与匹配。
"TryIgnoringArtists": true, // 忽略艺术家。
"TryIgnoringExtraInfo": true, // 忽略 括号/空格 + Cover/feat. 之后的内容,支持的括号类型在Filter.OpenBrackets里。
"ExtraInfoStart": " ([{【〖", // 空格和左括号等之后的内容会被过滤,注意,不要随便修改这里的内容,可能导致过滤准确性降低。
"Covers": [
"Cover",
"カバー"
], // Cover的各种写法。
"Featurings": [
"feat.",
"ft."
] // Feat.的各种写法。
},
"Match": { // 匹配设置,在搜索到歌曲信息之后,程序会通过自己的算法再次确认是否匹配。
"MinimumSimilarity": 0.65, // 匹配时的最小相似度,小于设定值的将不予显示,0~1。
"CharReplace": {
"\u00B7": "\u002e",
"\u0387": "\u002e",
"\u05BC": "\u002e",
"\u2022": "\u002e",
"\u2027": "\u002e",
"\u2219": "\u002e",
"\u22C5": "\u002e",
"\u30FB": "\u002e",
"\uFF65": "\u002e",
// .
"\uFF0A": "\u002A",
// *
"\uFF01": "\u0021",
// !
"\uFF1A": "\u003A",
// :
"\u005B": "\u0028",
"\u007B": "\u0028",
"\u3010": "\u0028",
"\u3016": "\u0028",
// (
"\u005D": "\u0029",
"\u007D": "\u0029",
"\u3011": "\u0029",
"\u3017": "\u0029"
// )
} // 前面是被替换的字符,后面是要替换成的字符,只支持单个字符替换,意思就是一个文字,多个文字会报错。
},
"Lyric": {
"Modes": [
"Merged",
"Raw",
"Translated"
], // 歌词模式,依次尝试每一个模式直到成功,Merged表示混合未翻译和翻译后歌词,Raw表示未翻译的歌词,Translated表示翻译后的歌词。
"SimplifyTranslated": true, // 部分翻译后的歌词是繁体的,这个选项可以简体化翻译后的歌词。
"Encoding": "utf-8",
"AutoUpdate": true, // 是否自动更新由NLyric创建的歌词。
"Overwriting": true // 是否覆盖非NLyric创建的歌词。
}
}
```
## 下载
GitHub: [.NET Framework版(Windows请下载这个)](https://github.com/wwh1004/NLyric/releases/latest/download/NLyric-net472.zip) [.NET Core版](https://github.com/wwh1004/NLyric/releases/latest/download/NLyric-netcoreapp3.1.zip)
AppVeyor: [](https://ci.appveyor.com/project/wwh1004/nlyric/branch/master)
## 感谢
混合歌词的思路参考了 [EHfive/Some-js-script-for-FB2](https://github.com/EHfive/Some-js-script-for-FB2K)
================================================
FILE: appveyor.yml
================================================
version: '{build}'
image: Visual Studio 2019
configuration: Release
platform: Any CPU
before_build:
- cmd: appveyor-retry nuget restore
build:
project: NLyric.sln
verbosity: normal
after_build:
- cmd: dotnet publish NLyric\NLyric.csproj -c Release -f netcoreapp3.1
artifacts:
- path: bin\Release\net472
name: NLyric-net472
- path: bin\Release\netcoreapp3.1\publish
name: NLyric-netcoreapp3.1
deploy:
- provider: GitHub
tag: $(APPVEYOR_REPO_TAG_NAME)
release: NLyric
auth_token:
secure: +8UJ1C312inNq+80I8WST34vPMrCylnmTx+9rmuIh1qnsArA5x2b8yc+kcwkXmQC
on:
APPVEYOR_REPO_TAG: true