[
  {
    "path": ".editorconfig",
    "content": "# Remove the line below if you want to inherit .editorconfig settings from higher directories\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = crlf\nindent_style = tab\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n# C# files\n[*.cs]\n\n#### .NET Coding Conventions ####\n\n# Organize usings\ndotnet_separate_import_directive_groups = false\ndotnet_sort_system_directives_first = true\n\n# this. and Me. preferences\ndotnet_style_qualification_for_event = false:suggestion\ndotnet_style_qualification_for_field = false:suggestion\ndotnet_style_qualification_for_method = false:suggestion\ndotnet_style_qualification_for_property = false:suggestion\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true:suggestion\ndotnet_style_predefined_type_for_member_access = true:suggestion\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members:silent\n\n# Expression-level preferences\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_prefer_auto_properties = false:suggestion\ndotnet_style_prefer_compound_assignment = true:suggestion\ndotnet_style_prefer_conditional_expression_over_assignment = true:silent\ndotnet_style_prefer_conditional_expression_over_return = true:silent\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\ndotnet_style_prefer_inferred_tuple_names = true:suggestion\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion\ndotnet_style_prefer_simplified_interpolation = true:suggestion\n\n# Field preferences\ndotnet_style_readonly_field = true:suggestion\n\n# Parameter preferences\ndotnet_code_quality_unused_parameters = all:suggestion\n\n#### C# Coding Conventions ####\n\n# var preferences\ncsharp_style_var_elsewhere = true:suggestion\ncsharp_style_var_for_built_in_types = false:suggestion\ncsharp_style_var_when_type_is_apparent = true:suggestion\n\n# Expression-bodied members\ncsharp_style_expression_bodied_accessors = true:suggestion\ncsharp_style_expression_bodied_constructors = false:suggestion\ncsharp_style_expression_bodied_indexers = true:suggestion\ncsharp_style_expression_bodied_lambdas = true:suggestion\ncsharp_style_expression_bodied_local_functions = false:suggestion\ncsharp_style_expression_bodied_methods = false:suggestion\ncsharp_style_expression_bodied_operators = false:suggestion\ncsharp_style_expression_bodied_properties = true:suggestion\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_prefer_switch_expression = false:suggestion\n\n# Null-checking preferences\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Modifier preferences\ncsharp_prefer_static_local_function = true:suggestion\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent\n\n# Code-block preferences\ncsharp_prefer_braces = false:silent\ncsharp_prefer_simple_using_statement = true:suggestion\n\n# Expression-level preferences\ncsharp_prefer_simple_default_expression = true:suggestion\ncsharp_style_deconstructed_variable_declaration = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_pattern_local_over_anonymous_function = true:suggestion\ncsharp_style_prefer_index_operator = true:suggestion\ncsharp_style_prefer_range_operator = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_unused_value_assignment_preference = unused_local_variable:silent\ncsharp_style_unused_value_expression_statement_preference = unused_local_variable:silent\n\n# 'using' directive preferences\ncsharp_using_directive_placement = outside_namespace:suggestion\n\n#### C# Formatting Rules ####\n\n# New line preferences\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_else = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_open_brace = none\ncsharp_new_line_between_query_expression_clauses = false\n\n# Indentation preferences\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = false\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = false\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_before_comma = false\ncsharp_space_before_dot = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_between_square_brackets = false\n\n# Wrapping preferences\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\n\n#### Naming styles ####\n\n# Naming rules\n\ndotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion\ndotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface\ndotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i\n\ndotnet_naming_rule.types_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.types_should_be_pascal_case.symbols = types\ndotnet_naming_rule.types_should_be_pascal_case.style = pascal_case\n\ndotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members\ndotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case\n\n# Symbol specifications\n\ndotnet_naming_symbols.interface.applicable_kinds = interface\ndotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.interface.required_modifiers = \n\ndotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum\ndotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.types.required_modifiers = \n\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.non_field_members.required_modifiers = \n\n# Naming styles\n\ndotnet_naming_style.pascal_case.required_prefix = \ndotnet_naming_style.pascal_case.required_suffix = \ndotnet_naming_style.pascal_case.word_separator = \ndotnet_naming_style.pascal_case.capitalization = pascal_case\n\ndotnet_naming_style.begins_with_i.required_prefix = I\ndotnet_naming_style.begins_with_i.required_suffix = \ndotnet_naming_style.begins_with_i.word_separator = \ndotnet_naming_style.begins_with_i.capitalization = pascal_case\n"
  },
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- Backup*.rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# wwh1004\nlaunchSettings.json"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 文煌\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "NLyric/Arguments.cs",
    "content": "using System;\nusing System.Cli;\nusing System.IO;\n\nnamespace NLyric {\n\tpublic sealed class Arguments {\n\t\tprivate string _directory;\n\t\tprivate string _account;\n\t\tprivate string _password;\n\t\tprivate bool _updateOnly;\n\t\tprivate bool _useBatch;\n\n\t\t[Argument(\"-d\", IsRequired = false, DefaultValue = \"\", Type = \"DIR\", Description = \"存放音乐的文件夹，可以是相对路径或者绝对路径\")]\n\t\tinternal string DirectoryCliSetter {\n\t\t\tset {\n\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\treturn;\n\n\t\t\t\tDirectory = value;\n\t\t\t}\n\t\t}\n\n\t\t[Argument(\"-a\", IsRequired = false, DefaultValue = \"\", Type = \"STR\", Description = \"网易云音乐账号（邮箱/手机号）\")]\n\t\tinternal string AccountCliSetter {\n\t\t\tset {\n\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\treturn;\n\n\t\t\t\tAccount = value;\n\t\t\t}\n\t\t}\n\n\t\t[Argument(\"-p\", IsRequired = false, DefaultValue = \"\", Type = \"STR\", Description = \"网易云音乐密码\")]\n\t\tinternal string PasswordCliSetter {\n\t\t\tset {\n\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\treturn;\n\n\t\t\t\tPassword = value;\n\t\t\t}\n\t\t}\n\n\t\t[Argument(\"--update-only\", Description = \"仅更新已有歌词\")]\n\t\tinternal bool UpdateOnlyCliSetter {\n\t\t\tset => _updateOnly = value;\n\t\t}\n\n\t\t[Argument(\"--batch\", Description = \"使用Batch API（实验性）\")]\n\t\tinternal bool UseBatchCliSetter {\n\t\t\tset => _useBatch = value;\n\t\t}\n\n\t\tpublic string Directory {\n\t\t\tget => _directory;\n\t\t\tset {\n\t\t\t\tif (!System.IO.Directory.Exists(value))\n\t\t\t\t\tthrow new DirectoryNotFoundException();\n\n\t\t\t\t_directory = Path.GetFullPath(value);\n\t\t\t}\n\t\t}\n\n\t\tpublic string Account {\n\t\t\tget => _account;\n\t\t\tset {\n\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\t\t_account = value;\n\t\t\t}\n\t\t}\n\n\t\tpublic string Password {\n\t\t\tget => _password;\n\t\t\tset {\n\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\t\t_password = value;\n\t\t\t}\n\t\t}\n\n\t\tpublic bool UpdateOnly {\n\t\t\tget => _updateOnly;\n\t\t\tset => _updateOnly = value;\n\t\t}\n\n\t\tpublic bool UseBatch {\n\t\t\tget => _useBatch;\n\t\t\tset => _useBatch = value;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Audio/Album.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing TagLib;\n\nnamespace NLyric.Audio {\n\t/// <summary>\n\t/// 专辑\n\t/// </summary>\n\tpublic class Album : ITrackOrAlbum {\n\t\tprivate readonly string _name;\n\t\tprivate readonly string[] _artists;\n\n\t\t/// <summary>\n\t\t/// 名称\n\t\t/// </summary>\n\t\tpublic string Name => _name;\n\n\t\t/// <summary>\n\t\t/// 艺术家\n\t\t/// </summary>\n\t\tpublic IReadOnlyList<string> Artists => _artists;\n\n\t\tpublic Album(string name, IEnumerable<string> artists) {\n\t\t\tif (name is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(name));\n\t\t\tif (artists is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(artists));\n\n\t\t\t_name = name;\n\t\t\t_artists = artists.Select(t => t.Trim()).ToArray();\n\t\t\tArray.Sort(_artists, StringComparer.Ordinal);\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 构造器\n\t\t/// </summary>\n\t\t/// <param name=\"tag\"></param>\n\t\t/// <param name=\"getArtistsFromTrack\">当 <see cref=\"Track.AlbumArtist\"/> 为空时，是否从 <see cref=\"Track.Artist\"/> 获取艺术家</param>\n\t\tpublic Album(Tag tag, bool getArtistsFromTrack) {\n\t\t\tif (tag is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(tag));\n\t\t\tif (!HasAlbumInfo(tag))\n\t\t\t\tthrow new ArgumentException(nameof(tag) + \" 中不存在专辑信息\");\n\n\t\t\t_name = tag.Album.GetSafeString();\n\t\t\tstring[] artists = tag.AlbumArtists.SelectMany(t => t.GetSafeString().SplitEx()).ToArray();\n\t\t\tif (getArtistsFromTrack && artists.Length == 0)\n\t\t\t\tartists = tag.Performers.SelectMany(t => t.GetSafeString().SplitEx()).ToArray();\n\t\t\tArray.Sort(artists, StringComparer.Ordinal);\n\t\t\t_artists = artists;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 是否存在专辑信息\n\t\t/// </summary>\n\t\t/// <param name=\"tag\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static bool HasAlbumInfo(Tag tag) {\n\t\t\tif (tag is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(tag));\n\n\t\t\treturn !string.IsNullOrWhiteSpace(tag.Album);\n\t\t}\n\n\t\tpublic override string ToString() {\n\t\t\treturn \"Name:\" + _name + \" | Artists:\" + string.Join(\",\", _artists);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Audio/ITrackOrAlbum.cs",
    "content": "using System.Collections.Generic;\n\nnamespace NLyric.Audio {\n\tpublic interface ITrackOrAlbum {\n\t\tstring Name { get; }\n\n\t\tIReadOnlyList<string> Artists { get; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Audio/Track.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing TagLib;\n\nnamespace NLyric.Audio {\n\t/// <summary>\n\t/// 单曲\n\t/// </summary>\n\tpublic class Track : ITrackOrAlbum {\n\t\tprivate readonly string _name;\n\t\tprivate readonly string[] _artists;\n\n\t\t/// <summary>\n\t\t/// 名称\n\t\t/// </summary>\n\t\tpublic string Name => _name;\n\n\t\t/// <summary>\n\t\t/// 艺术家\n\t\t/// </summary>\n\t\tpublic IReadOnlyList<string> Artists => _artists;\n\n\t\tpublic Track(string name, IEnumerable<string> artists) {\n\t\t\tif (name is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(name));\n\t\t\tif (artists is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(artists));\n\n\t\t\t_name = name;\n\t\t\t_artists = artists.Select(t => t.Trim()).ToArray();\n\t\t\tArray.Sort(_artists, StringComparer.Ordinal);\n\t\t}\n\n\t\tpublic Track(Tag tag) {\n\t\t\tif (tag is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(tag));\n\n\t\t\t_name = tag.Title.GetSafeString();\n\t\t\t_artists = tag.Performers.SelectMany(s => s.GetSafeString().SplitEx()).ToArray();\n\t\t\tArray.Sort(_artists, StringComparer.Ordinal);\n\t\t}\n\n\t\tpublic override string ToString() {\n\t\t\treturn \"Name:\" + _name + \" | Artists:\" + string.Join(\",\", _artists);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/CRC32.cs",
    "content": "using System;\n\nnamespace NLyric {\n\tinternal static class CRC32 {\n\t\tprivate static readonly uint[] _table = GenerateTable(0xEDB88320);\n\n\t\tprivate static uint[] GenerateTable(uint seed) {\n\t\t\tuint[] table = new uint[256];\n\t\t\tfor (int i = 0; i < 256; i++) {\n\t\t\t\tuint crc = (uint)i;\n\t\t\t\tfor (int j = 8; j > 0; j--) {\n\t\t\t\t\tif ((crc & 1) == 1)\n\t\t\t\t\t\tcrc = (crc >> 1) ^ seed;\n\t\t\t\t\telse\n\t\t\t\t\t\tcrc >>= 1;\n\t\t\t\t}\n\t\t\t\ttable[i] = crc;\n\t\t\t}\n\t\t\treturn table;\n\t\t}\n\n\t\tpublic static uint Compute(byte[] data) {\n\t\t\tif (data is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(data));\n\n\t\t\tuint crc32 = 0xFFFFFFFF;\n\t\t\tfor (int i = 0; i < data.Length; i++)\n\t\t\t\tcrc32 = (crc32 >> 8) ^ _table[(crc32 ^ data[i]) & 0xFF];\n\t\t\treturn ~crc32;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/ChineseConverter.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Text;\n\nnamespace NLyric {\n\tinternal static class ChineseConverter {\n\t\tprivate static readonly Dictionary<char, char> _traditionalToSimplifiedMap = GetTraditionalToSimplifiedMap();\n\n\t\tprivate static Dictionary<char, char> GetTraditionalToSimplifiedMap() {\n\t\t\tvar assembly = Assembly.GetExecutingAssembly();\n\t\t\tusing var stream = assembly.GetManifestResourceStream(\"NLyric.TraditionalToSimplified.map\");\n\t\t\tusing var reader = new BinaryReader(stream);\n\t\t\tint count = (int)stream.Length / 4;\n\t\t\tvar map = new Dictionary<char, char>(count);\n\t\t\tfor (int i = 0; i < count; i++)\n\t\t\t\tmap.Add((char)reader.ReadUInt16(), (char)reader.ReadUInt16());\n\t\t\treturn map;\n\t\t}\n\n\t\tpublic static string TraditionalToSimplified(string s) {\n\t\t\tif (s is null)\n\t\t\t\treturn null;\n\n\t\t\tvar sb = new StringBuilder(s);\n\t\t\tfor (int i = 0; i < sb.Length; i++) {\n\t\t\t\tif (_traditionalToSimplifiedMap.TryGetValue(sb[i], out char c))\n\t\t\t\t\tsb[i] = c;\n\t\t\t}\n\t\t\treturn sb.ToString();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Database/AlbumInfo.cs",
    "content": "using System;\nusing Newtonsoft.Json;\nusing NLyric.Audio;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// 专辑信息\n\t/// </summary>\n\tpublic sealed class AlbumInfo {\n\t\t/// <summary>\n\t\t/// 名称\n\t\t/// </summary>\n\t\tpublic string Name { get; set; }\n\n\t\t/// <summary>\n\t\t/// 网易云音乐ID\n\t\t/// </summary>\n\t\tpublic int Id { get; set; }\n\n\t\t[JsonConstructor]\n\t\t[Obsolete(\"Deserialization only\", true)]\n\t\tpublic AlbumInfo() {\n\t\t}\n\n\t\tpublic AlbumInfo(Album album, int id) : this(album.Name, id) {\n\t\t}\n\n\t\tpublic AlbumInfo(string name, int id) {\n\t\t\tif (name is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(name));\n\n\t\t\tName = name;\n\t\t\tId = id;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Database/Extensions.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing NLyric.Audio;\n\nnamespace NLyric.Database {\n\tpublic static class Extensions {\n\t\tpublic static AlbumInfo Match(this IEnumerable<AlbumInfo> caches, Album album) {\n\t\t\tif (album is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(album));\n\n\t\t\treturn caches.FirstOrDefault(t => IsMatched(t, album));\n\t\t}\n\n\t\tpublic static TrackInfo Match(this IEnumerable<TrackInfo> caches, Album album, Track track) {\n\t\t\tif (track is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(track));\n\n\t\t\treturn caches.FirstOrDefault(t => IsMatched(t, album, track));\n\t\t}\n\n\t\tpublic static bool IsMatched(this AlbumInfo cache, Album album) {\n\t\t\tif (album is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(album));\n\n\t\t\treturn cache.Name == album.Name;\n\t\t}\n\n\t\tpublic static bool IsMatched(this TrackInfo cache, Album album, Track track) {\n\t\t\tif (track is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(track));\n\n\t\t\treturn cache.Name == track.Name && (album is null ? cache.AlbumName is null : cache.AlbumName == album.Name) && cache.Artists.SequenceEqual(track.Artists);\n\t\t\t// 如果album为空，要求cache中AlbumName也为空，如果album不为空，要求cache中AlbumName匹配\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Database/LyricInfo.cs",
    "content": "using System;\nusing Newtonsoft.Json;\nusing NLyric.Ncm;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// 歌词信息\n\t/// </summary>\n\tpublic sealed class LyricInfo {\n\t\t/// <summary>\n\t\t/// 原始歌词版本\n\t\t/// </summary>\n\t\tpublic int RawVersion { get; set; }\n\n\t\t/// <summary>\n\t\t/// 翻译歌词版本（如果有）\n\t\t/// </summary>\n\t\tpublic int TranslatedVersion { get; set; }\n\n\t\t/// <summary>\n\t\t/// 歌词校验值\n\t\t/// </summary>\n\t\tpublic string CheckSum { get; set; }\n\n\t\t[JsonConstructor]\n\t\t[Obsolete(\"Deserialization only\", true)]\n\t\tpublic LyricInfo() {\n\t\t}\n\n\t\tpublic LyricInfo(NcmLyric lyric, string checkSum) : this(lyric.RawVersion, lyric.TranslatedVersion, checkSum) {\n\t\t\tif (!lyric.IsCollected)\n\t\t\t\tthrow new ArgumentException(\"未收录的歌词不能添加到缓存\", nameof(lyric));\n\t\t}\n\n\t\tpublic LyricInfo(int rawVersion, int translatedVersion, string checkSum) {\n\t\t\tif (checkSum is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(checkSum));\n\n\t\t\tRawVersion = rawVersion;\n\t\t\tTranslatedVersion = translatedVersion;\n\t\t\tCheckSum = checkSum;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Database/NLyricDatabase.cs",
    "content": "using System.Collections.Generic;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// NLyric数据库\n\t/// </summary>\n\tpublic sealed class NLyricDatabase {\n\t\t/// <summary>\n\t\t/// 专辑信息\n\t\t/// </summary>\n\t\tpublic List<AlbumInfo> AlbumInfos { get; set; }\n\n\t\t/// <summary>\n\t\t/// 单曲信息\n\t\t/// </summary>\n\t\tpublic List<TrackInfo> TrackInfos { get; set; }\n\n\t\t/// <summary>\n\t\t/// 数据库格式版本\n\t\t/// </summary>\n\t\tpublic int FormatVersion { get; set; }\n\n\t\t/// <summary>\n\t\t/// 检查 <see cref=\"FormatVersion\"/>\n\t\t/// </summary>\n\t\t/// <returns></returns>\n\t\tpublic bool CheckFormatVersion() {\n\t\t\tswitch (FormatVersion) {\n\t\t\tcase 0:\n\t\t\tcase 1:\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 是否为老版本数据库\n\t\t/// </summary>\n\t\t/// <returns></returns>\n\t\tpublic bool IsOldFormat() {\n\t\t\treturn FormatVersion < 1;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Database/TrackInfo.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Newtonsoft.Json;\nusing NLyric.Audio;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// 单曲信息\n\t/// </summary>\n\tpublic sealed class TrackInfo {\n\t\t/// <summary>\n\t\t/// 名称\n\t\t/// </summary>\n\t\tpublic string Name { get; set; }\n\n\t\t/// <summary>\n\t\t/// 艺术家\n\t\t/// </summary>\n\t\tpublic IReadOnlyList<string> Artists { get; set; }\n\n\t\t/// <summary>\n\t\t/// 专辑名\n\t\t/// </summary>\n\t\tpublic string AlbumName { get; set; }\n\n\t\t/// <summary>\n\t\t/// 网易云音乐ID\n\t\t/// </summary>\n\t\tpublic int Id { get; set; }\n\n\t\t/// <summary>\n\t\t/// 歌词缓存\n\t\t/// </summary>\n\t\tpublic LyricInfo Lyric { get; set; }\n\n\t\t[JsonConstructor]\n\t\t[Obsolete(\"Deserialization only\", true)]\n\t\tpublic TrackInfo() {\n\t\t}\n\n\t\tpublic TrackInfo(Track track, Album album, int id) : this(track.Name, track.Artists, album?.Name, id) {\n\t\t}\n\n\t\tpublic TrackInfo(string name, IEnumerable<string> artists, string albumName, int id) {\n\t\t\tif (name is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(name));\n\t\t\tif (artists is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(artists));\n\n\t\t\tName = name;\n\t\t\tArtists = artists.Select(t => t.Trim()).ToArray();\n\t\t\tAlbumName = albumName;\n\t\t\tId = id;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/FastConsole.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading;\n\nnamespace NLyric {\n\tinternal static class FastConsole {\n\t\tprivate const int INTERVAL = 5; // 间隔多少毫秒再次检测是否有新文本\n\t\tprivate const int MAX_INTERVAL = 200; // 间隔多少毫秒后强制输出\n\t\tprivate const int MAX_TEXT_COUNT = 5000; // 超过多少条文本后后强制输出\n\n\t\tprivate static volatile Thread _singleThread;\n\t\tprivate static bool _isIdle = true;\n\t\tprivate static readonly Queue<(string Text, ConsoleColor Color)> _queue = new Queue<(string Text, ConsoleColor Color)>();\n\t\tprivate static readonly object _ioLock = new object();\n\t\tprivate static readonly object _stLock = new object();\n\t\tprivate static ConsoleColor _lastColor;\n\n\t\t/// <summary>\n\t\t/// 设置只允许指定线程写入控制台\n\t\t/// </summary>\n\t\tpublic static Thread SingleThread {\n\t\t\tget => _singleThread;\n\t\t\tset {\n\t\t\trelock:\n\t\t\t\tlock (_stLock) {\n\t\t\t\t\tvar singleThread = _singleThread;\n\t\t\t\t\tif (!(singleThread is null) && Thread.CurrentThread != singleThread) {\n\t\t\t\t\t\tMonitor.Wait(_stLock);\n\t\t\t\t\t\tgoto relock;\n\t\t\t\t\t}\n\t\t\t\t\t// 如果不符合设置设置SingleThread的条件，需要等待\n\t\t\t\t\tif (singleThread is null || Thread.CurrentThread == singleThread) {\n\t\t\t\t\t\t_singleThread = value;\n\t\t\t\t\t\tif (value is null)\n\t\t\t\t\t\t\tMonitor.PulseAll(_stLock);\n\t\t\t\t\t\t// 设置为null则取消阻塞其它线程\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 单线程锁，化简 <see cref=\"SingleThread\"/>\n\t\t/// </summary>\n\t\tpublic static IDisposable SingleThreadLock => new AutoSingleThreadLock();\n\n\t\tpublic static bool IsIdle => _isIdle;\n\n\t\tpublic static int QueueCount => _queue.Count;\n\n\t\tstatic FastConsole() {\n\t\t\tnew Thread(IOLoop) {\n\t\t\t\tName = $\"{nameof(FastConsole)}.{nameof(IOLoop)}\",\n\t\t\t\tIsBackground = true\n\t\t\t}.Start();\n\t\t}\n\n\t\tpublic static void WriteNewLine() {\n\t\t\tWriteLine(string.Empty, ConsoleColor.Gray);\n\t\t}\n\n\t\tpublic static void WriteInfo(string value) {\n\t\t\tWriteLine(value, ConsoleColor.Gray);\n\t\t}\n\n\t\tpublic static void WriteWarning(string value) {\n\t\t\tWriteLine(value, ConsoleColor.Yellow);\n\t\t}\n\n\t\tpublic static void WriteError(string value) {\n\t\t\tWriteLine(value, ConsoleColor.Red);\n\t\t}\n\n\t\tpublic static void WriteLine(string value, ConsoleColor color) {\n\t\t\tWrite(value + Environment.NewLine, color);\n\t\t}\n\n\t\tpublic static void Write(string value, ConsoleColor color) {\n\t\trelock:\n\t\t\tlock (_stLock) {\n\t\t\t\tvar singleThread = _singleThread;\n\t\t\t\tif (!(singleThread is null) && Thread.CurrentThread != singleThread) {\n\t\t\t\t\tMonitor.Wait(_stLock);\n\t\t\t\t\tgoto relock;\n\t\t\t\t}\n\t\t\t\tlock (((ICollection)_queue).SyncRoot) {\n\t\t\t\t\tif (string.IsNullOrEmpty(value))\n\t\t\t\t\t\tcolor = _lastColor;\n\t\t\t\t\t// 优化空行显示\n\t\t\t\t\t_queue.Enqueue((value, color));\n\t\t\t\t\t_lastColor = color;\n\t\t\t\t}\n\t\t\t\tlock (_ioLock)\n\t\t\t\t\tMonitor.Pulse(_ioLock);\n\t\t\t}\n\t\t}\n\n\t\tpublic static void WriteException(Exception value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\tWriteError(ExceptionToString(value));\n\t\t}\n\n\t\tpublic static void Synchronize() {\n\t\t\twhile (!_isIdle || _queue.Count != 0)\n\t\t\t\tThread.Sleep(INTERVAL / 3);\n\t\t}\n\n\t\tprivate static string ExceptionToString(Exception exception) {\n\t\t\tif (exception is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(exception));\n\n\t\t\tvar sb = new StringBuilder();\n\t\t\tDumpException(exception, sb);\n\t\t\treturn sb.ToString();\n\t\t}\n\n\t\tprivate static void DumpException(Exception exception, StringBuilder sb) {\n\t\t\tsb.AppendLine($\"Type: {Environment.NewLine}{exception.GetType().FullName}\");\n\t\t\tsb.AppendLine($\"Message: {Environment.NewLine}{exception.Message}\");\n\t\t\tsb.AppendLine($\"Source: {Environment.NewLine}{exception.Source}\");\n\t\t\tsb.AppendLine($\"StackTrace: {Environment.NewLine}{exception.StackTrace}\");\n\t\t\tsb.AppendLine($\"TargetSite: {Environment.NewLine}{exception.TargetSite}\");\n\t\t\tsb.AppendLine(\"----------------------------------------\");\n\t\t\tif (!(exception.InnerException is null))\n\t\t\t\tDumpException(exception.InnerException, sb);\n\t\t}\n\n\t\tprivate static void IOLoop() {\n\t\t\tvar sb = new StringBuilder();\n\t\t\twhile (true) {\n\t\t\t\t_isIdle = true;\n\t\t\t\tif (_queue.Count == 0) {\n\t\t\t\t\tlock (_ioLock)\n\t\t\t\t\t\tMonitor.Wait(_ioLock);\n\t\t\t\t}\n\t\t\t\t_isIdle = false;\n\t\t\t\t// 等待输出被触发\n\n\t\t\t\tint delayCount = 0;\n\t\t\t\tint oldCount;\n\t\t\t\tdo {\n\t\t\t\t\toldCount = _queue.Count;\n\t\t\t\t\tThread.Sleep(INTERVAL);\n\t\t\t\t\tdelayCount++;\n\t\t\t\t} while (_queue.Count > oldCount && delayCount < MAX_INTERVAL / INTERVAL && _queue.Count < MAX_TEXT_COUNT);\n\t\t\t\t// 也许此时有其它要输出的内容\n\n\t\t\t\tvar currents = default(Queue<(string Text, ConsoleColor Color)>);\n\t\t\t\tlock (((ICollection)_queue).SyncRoot) {\n\t\t\t\t\tcurrents = new Queue<(string, ConsoleColor)>(_queue);\n\t\t\t\t\t_queue.Clear();\n\t\t\t\t}\n\t\t\t\t// 获取全部要输出的内容\n\n\t\t\t\tdo {\n\t\t\t\t\tvar (text, color) = currents.Dequeue();\n\t\t\t\t\tsb.Length = 0;\n\t\t\t\t\tsb.Append(text);\n\t\t\t\t\twhile (true) {\n\t\t\t\t\t\tif (currents.Count == 0)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tvar (nextText, nextColor) = currents.Peek();\n\t\t\t\t\t\tif (nextColor != color)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcurrents.Dequeue();\n\t\t\t\t\t\tsb.Append(nextText);\n\t\t\t\t\t}\n\t\t\t\t\t// 合并颜色相同，减少重绘带来的性能损失\n\t\t\t\t\tvar oldColor = Console.ForegroundColor;\n\t\t\t\t\tConsole.ForegroundColor = color;\n\t\t\t\t\tConsole.Write(sb.ToString());\n\t\t\t\t\tConsole.ForegroundColor = oldColor;\n\t\t\t\t} while (currents.Count > 0);\n\t\t\t}\n\t\t}\n\n\t\tpublic static ConsoleKeyInfo ReadKey(bool intercept) {\n\t\t\tusing (SingleThreadLock)\n\t\t\t\treturn Console.ReadKey(intercept);\n\t\t}\n\n\t\tpublic static string ReadLine() {\n\t\t\tusing (SingleThreadLock)\n\t\t\t\treturn Console.ReadLine();\n\t\t}\n\n\t\tprivate sealed class AutoSingleThreadLock : IDisposable {\n\t\t\tpublic AutoSingleThreadLock() {\n\t\t\t\tSingleThread = Thread.CurrentThread;\n\t\t\t\tSynchronize();\n\t\t\t}\n\n\t\t\tvoid IDisposable.Dispose() {\n\t\t\t\tif (SingleThread is null)\n\t\t\t\t\tthrow new InvalidOperationException();\n\t\t\t\tSingleThread = null;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Levenshtein.cs",
    "content": "using System;\n\nnamespace NLyric {\n\tinternal static class Levenshtein {\n\t\t/// <summary>\n\t\t/// 计算相似度\n\t\t/// </summary>\n\t\t/// <param name=\"x\"></param>\n\t\t/// <param name=\"y\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static double Compute(string x, string y) {\n\t\t\tint[,] matrix = new int[x.Length + 1, y.Length + 1];\n\t\t\tfor (int i = 0; i <= x.Length; i++)\n\t\t\t\tmatrix[i, 0] = i;\n\t\t\tfor (int i = 0; i <= y.Length; i++)\n\t\t\t\tmatrix[0, i] = i;\n\t\t\tfor (int i = 1; i <= x.Length; i++) {\n\t\t\t\tfor (int j = 1; j <= y.Length; j++) {\n\t\t\t\t\tint cost = x[i - 1] == y[j - 1] ? 0 : 1;\n\t\t\t\t\tmatrix[i, j] = Math.Min(Math.Min(matrix[i - 1, j - 1] + cost, matrix[i, j - 1] + 1), matrix[i - 1, j] + 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 1 - ((double)matrix[x.Length, y.Length] / Math.Max(x.Length, y.Length));\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 计算相似度\n\t\t/// </summary>\n\t\t/// <param name=\"x\"></param>\n\t\t/// <param name=\"y\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static double Compute<T>(T[] x, T[] y, Comparison<T> comparison) {\n\t\t\tint[,] matrix = new int[x.Length + 1, y.Length + 1];\n\t\t\tfor (int i = 0; i <= x.Length; i++)\n\t\t\t\tmatrix[i, 0] = i;\n\t\t\tfor (int i = 0; i <= y.Length; i++)\n\t\t\t\tmatrix[0, i] = i;\n\t\t\tfor (int i = 1; i <= x.Length; i++) {\n\t\t\t\tfor (int j = 1; j <= y.Length; j++) {\n\t\t\t\t\tint cost = comparison(x[i - 1], y[j - 1]) == 0 ? 0 : 1;\n\t\t\t\t\tmatrix[i, j] = Math.Min(Math.Min(matrix[i - 1, j - 1] + cost, matrix[i, j - 1] + 1), matrix[i - 1, j] + 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 1 - ((double)matrix[x.Length, y.Length] / Math.Max(x.Length, y.Length));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Lyrics/Lrc.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\n\nnamespace NLyric.Lyrics {\n\tpublic sealed class Lrc {\n\t\tconst string TI = \"ti:\";\n\t\tconst string AR = \"ar:\";\n\t\tconst string AL = \"al:\";\n\t\tconst string BY = \"by:\";\n\t\tconst string OFFSET = \"offset:\";\n\n\t\tprivate string _title;\n\t\tprivate string _artist;\n\t\tprivate string _album;\n\t\tprivate string _by;\n\t\tprivate TimeSpan? _offset;\n\t\tprivate IDictionary<TimeSpan, string> _lyrics = new Dictionary<TimeSpan, string>();\n\n\t\tpublic string Title {\n\t\t\tget => _title;\n\t\t\tset {\n\t\t\t\tif (value is null) {\n\t\t\t\t\t_title = value;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvalue = value.Trim();\n\t\t\t\t_title = value.Length == 0 ? null : value;\n\t\t\t}\n\t\t}\n\n\t\tpublic string Artist {\n\t\t\tget => _artist;\n\t\t\tset {\n\t\t\t\tif (value is null) {\n\t\t\t\t\t_artist = value;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvalue = value.Trim();\n\t\t\t\t_artist = value.Length == 0 ? null : value;\n\t\t\t}\n\t\t}\n\n\t\tpublic string Album {\n\t\t\tget => _album;\n\t\t\tset {\n\t\t\t\tif (value is null) {\n\t\t\t\t\t_album = value;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvalue = value.Trim();\n\t\t\t\t_album = value.Length == 0 ? null : value;\n\t\t\t}\n\t\t}\n\n\t\tpublic string By {\n\t\t\tget => _by;\n\t\t\tset {\n\t\t\t\tif (value is null) {\n\t\t\t\t\t_by = value;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvalue = value.Trim();\n\t\t\t\t_by = value.Length == 0 ? null : value;\n\t\t\t}\n\t\t}\n\n\t\tpublic TimeSpan? Offset {\n\t\t\tget => _offset;\n\t\t\tset => _offset = (value is null || value.Value.Ticks == 0) ? null : value;\n\t\t}\n\n\t\tpublic IDictionary<TimeSpan, string> Lyrics {\n\t\t\tget => _lyrics;\n\t\t\tset {\n\t\t\t\tif (value is null)\n\t\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\t\t_lyrics = value;\n\t\t\t}\n\t\t}\n\n\t\tpublic static Lrc Parse(string text) {\n\t\t\tif (string.IsNullOrEmpty(text))\n\t\t\t\tthrow new ArgumentNullException(nameof(text));\n\n\t\t\tvar lrc = new Lrc();\n\t\t\tusing var reader = new StringReader(text);\n\t\t\tstring line;\n\t\t\twhile (!((line = reader.ReadLine()?.Trim()) is null) && !string.IsNullOrEmpty(line)) {\n\t\t\t\tif (!TryParseLine(line, lrc))\n\t\t\t\t\tthrow new FormatException();\n\t\t\t}\n\t\t\treturn lrc;\n\t\t}\n\n\t\tpublic static Lrc UnsafeParse(string text) {\n\t\t\tif (string.IsNullOrEmpty(text))\n\t\t\t\tthrow new ArgumentNullException(nameof(text));\n\n\t\t\tvar lrc = new Lrc();\n\t\t\tusing var reader = new StringReader(text);\n\t\t\tstring line;\n\t\t\twhile (!((line = reader.ReadLine()?.Trim()) is null))\n\t\t\t\tTryParseLine(line.Trim(), lrc);\n\t\t\treturn lrc;\n\t\t}\n\n\t\tprivate static bool TryParseLine(string line, Lrc lrc) {\n\t\t\tif (string.IsNullOrEmpty(line) || line[0] != '[')\n\t\t\t\treturn false;\n\n\t\t\tint startIndex = 0;\n\t\t\tint endIndex;\n\t\t\tvar times = new List<TimeSpan>();\n\n\t\t\tdo {\n\t\t\t\tendIndex = line.IndexOf(']', startIndex + 1);\n\t\t\t\tif (endIndex == -1)\n\t\t\t\t\t// 有\"[\"但是没有\"]\"\n\t\t\t\t\treturn false;\n\t\t\t\tstring token = line.Substring(startIndex + 1, endIndex - startIndex - 1);\n\t\t\t\tif (token.StartsWith(TI))\n\t\t\t\t\tlrc.Title = GetMetadata(token, TI);\n\t\t\t\telse if (token.StartsWith(AR))\n\t\t\t\t\tlrc.Artist = GetMetadata(token, AR);\n\t\t\t\telse if (token.StartsWith(AL))\n\t\t\t\t\tlrc.Album = GetMetadata(token, AL);\n\t\t\t\telse if (token.StartsWith(BY))\n\t\t\t\t\tlrc.By = GetMetadata(token, BY);\n\t\t\t\telse if (token.StartsWith(OFFSET)) {\n\t\t\t\t\tif (!int.TryParse(GetMetadata(token, OFFSET), out int offset))\n\t\t\t\t\t\treturn false;\n\t\t\t\t\tlrc.Offset = new TimeSpan(0, 0, 0, 0, offset);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tif (!TimeSpan.TryParse(\"00:\" + token, out var time))\n\t\t\t\t\t\treturn false;\n\t\t\t\t\ttimes.Add(time);\n\t\t\t\t}\n\t\t\t} while ((startIndex = line.IndexOf('[', endIndex + 1)) != -1);\n\n\t\t\tstring lyric = line.Substring(endIndex + 1).Trim();\n\t\t\tforeach (var time in times)\n\t\t\t\tlrc._lyrics[time] = lyric;\n\t\t\treturn true;\n\n\t\t\tstring GetMetadata(string _line, string _key) {\n\t\t\t\treturn _line.Substring(_key.Length, _line.Length - _key.Length);\n\t\t\t}\n\t\t}\n\n\t\tpublic override string ToString() {\n\t\t\tvar sb = new StringBuilder();\n\t\t\tif (!(_title is null))\n\t\t\t\tAppendLine(sb, TI, _title);\n\t\t\tif (!(_artist is null))\n\t\t\t\tAppendLine(sb, AR, _artist);\n\t\t\tif (!(_album is null))\n\t\t\t\tAppendLine(sb, AL, _album);\n\t\t\tif (!(_by is null))\n\t\t\t\tAppendLine(sb, BY, _by);\n\t\t\tif (!(_offset is null))\n\t\t\t\tAppendLine(sb, OFFSET, ((long)_offset.Value.TotalMilliseconds).ToString());\n\t\t\tforeach (var lyric in _lyrics)\n\t\t\t\tsb.AppendLine($\"[{TimeSpanToLyricString(lyric.Key)}]{lyric.Value}\");\n\t\t\treturn sb.ToString();\n\n\t\t\tvoid AppendLine(StringBuilder _sb, string key, string value) {\n\t\t\t\t_sb.AppendLine($\"[{key}{value}]\");\n\t\t\t}\n\n\t\t\tstring TimeSpanToLyricString(TimeSpan _timeSpan) {\n\t\t\t\tstring milliseconds = _timeSpan.Milliseconds.ToString(\"D3\");\n\t\t\t\treturn $\"{_timeSpan.Minutes:D2}:{_timeSpan.Seconds:D2}.{milliseconds.Substring(0, 2)}\";\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/NLyric.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\t<PropertyGroup>\n\t\t<Title>$(ProjectName)</Title>\n\t\t<Version>2.6.1.0</Version>\n\t\t<Copyright>Copyright © 2019-2021 Wwh</Copyright>\n\t</PropertyGroup>\n\t<PropertyGroup>\n\t\t<TargetFrameworks>netcoreapp3.1;net472</TargetFrameworks>\n\t\t<LangVersion>8.0</LangVersion>\n\t\t<OutputPath>..\\bin\\$(Configuration)</OutputPath>\n\t\t<OutputType>Exe</OutputType>\n\t</PropertyGroup>\n\t<ItemGroup>\n\t\t<PackageReference Include=\"NeteaseCloudMusicApi\" Version=\"3.25.3.9999\" />\n\t\t<PackageReference Include=\"Newtonsoft.Json\" Version=\"12.0.3\" />\n\t\t<PackageReference Include=\"TagLibSharp\" Version=\"2.2.0\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<EmbeddedResource Include=\"TraditionalToSimplified.map\" />\n\t\t<None Update=\"Settings.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t</ItemGroup>\n</Project>\n"
  },
  {
    "path": "NLyric/NLyricImpl.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing NeteaseCloudMusicApi;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Linq;\nusing NLyric.Audio;\nusing NLyric.Database;\nusing NLyric.Lyrics;\nusing NLyric.Ncm;\nusing NLyric.Settings;\nusing TagLib;\nusing File = System.IO.File;\n\nnamespace NLyric {\n\tpublic static class NLyricImpl {\n\t\tprivate static readonly SearchSettings _searchSettings = AllSettings.Default.Search;\n\t\tprivate static readonly FuzzySettings _fuzzySettings = AllSettings.Default.Fuzzy;\n\t\tprivate static readonly MatchSettings _matchSettings = AllSettings.Default.Match;\n\t\tprivate static readonly LyricSettings _lyricSettings = AllSettings.Default.Lyric;\n\t\tprivate static readonly CloudMusic _cloudMusic = new CloudMusic();\n\t\tprivate static readonly HashSet<string> _failMatchAlbums = new HashSet<string>();\n\t\t// AlbumName\n\t\tprivate static readonly Dictionary<int, NcmTrack[]> _cachedNcmTrackses = new Dictionary<int, NcmTrack[]>();\n\t\t// AlbumId -> Tracks\n\t\tprivate static readonly Dictionary<int, NcmLyric> _cachedNcmLyrics = new Dictionary<int, NcmLyric>();\n\t\t// TrackId -> Lyric\n\t\tprivate static NLyricDatabase _database;\n\n\t\tpublic static async Task ExecuteAsync(Arguments arguments) {\n\t\t\tFastConsole.WriteLine(\"程序会自动过滤相似度为0的结果与歌词未被收集的结果！！！\", ConsoleColor.Green);\n\t\t\tvar loginTask = LoginIfNeedAsync(arguments);\n\t\t\tstring databasePath = Path.Combine(arguments.Directory, \".nlyric\");\n\t\t\tLoadDatabase(databasePath);\n\t\t\tvar audioInfos = LoadAllAudioInfos(arguments.Directory);\n\t\t\tvar audioInfoCandidates = audioInfos.Where(t => t.TrackInfo is null).ToArray();\n\t\t\tawait loginTask;\n\t\t\t// 登录同时进行\n\t\t\tif (!arguments.UpdateOnly) {\n\t\t\t\tif (arguments.UseBatch)\n\t\t\t\t\t_ = AccelerateAllTracksAsync(audioInfoCandidates);\n\t\t\t\tawait LoadAllAudioInfoCandidates(audioInfoCandidates, _ => SaveDatabaseCore(databasePath));\n\t\t\t}\n\t\t\taudioInfos = audioInfos.Where(t => !(t.TrackInfo is null)).ToArray();\n\t\t\tif (arguments.UseBatch)\n\t\t\t\t_ = AccelerateAllLyricsAsync(audioInfos);\n\t\t\tawait DownloadLyricsAsync(audioInfos);\n\t\t\tSaveDatabase(databasePath);\n\t\t}\n\n\t\tprivate static async Task LoginIfNeedAsync(Arguments arguments) {\n\t\t\tif (string.IsNullOrEmpty(arguments.Account) || string.IsNullOrEmpty(arguments.Password)) {\n\t\t\t\tFastConsole.WriteLine(\"登录可避免出现大部分API错误！！！当前是免登录状态，若软件出错请尝试登录！！！\", ConsoleColor.Green);\n\t\t\t\tFastConsole.WriteLine(\"强烈建议登录使用软件：\\\"NLyric.exe -d C:\\\\Music -a example@example.com -p 123456\\\"\", ConsoleColor.Green);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tFastConsole.WriteLine(\"登录中...\", ConsoleColor.Green);\n\t\t\t\tif (await _cloudMusic.LoginAsync(arguments.Account, arguments.Password)) {\n\t\t\t\t\tFastConsole.WriteLine(\"登录成功！\", ConsoleColor.Green);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tFastConsole.WriteError(\"登录失败，输入任意键以免登录模式运行或重新运行尝试再次登录！\");\n\t\t\t\t\ttry {\n\t\t\t\t\t\tFastConsole.ReadKey(true);\n\t\t\t\t\t}\n\t\t\t\t\tcatch {\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tFastConsole.WriteNewLine();\n\t\t}\n\n\t\tprivate static bool CanSkip(string audioPath, string lrcPath) {\n\t\t\tstring extension = Path.GetExtension(audioPath);\n\t\t\tif (!IsAudioFile(extension))\n\t\t\t\treturn true;\n\t\t\tif (File.Exists(lrcPath) && !_lyricSettings.AutoUpdate && !_lyricSettings.Overwriting) {\n\t\t\t\tFastConsole.WriteInfo($\"文件\\\"{Path.GetFileName(audioPath)}\\\"的歌词已存在，并且自动更新与覆盖已被禁止，正在跳过。\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\tprivate static bool IsAudioFile(string extension) {\n\t\t\treturn _searchSettings.AudioExtensions.Any(s => extension.Equals(s, StringComparison.OrdinalIgnoreCase));\n\t\t}\n\n\t\tprivate static AudioInfo[] LoadAllAudioInfos(string directory) {\n\t\t\treturn Directory.EnumerateFiles(directory, \"*\", SearchOption.AllDirectories).Where(audioPath => {\n\t\t\t\tstring lrcPath = Path.ChangeExtension(audioPath, \".lrc\");\n\t\t\t\treturn !CanSkip(audioPath, lrcPath);\n\t\t\t}).AsParallel().AsOrdered().Select(audioPath => {\n\t\t\t\tvar audioFile = default(TagLib.File);\n\t\t\t\tvar audioInfo = new AudioInfo {\n\t\t\t\t\tPath = audioPath\n\t\t\t\t};\n\t\t\t\tvar tag = default(Tag);\n\t\t\t\ttry {\n\t\t\t\t\taudioFile = TagLib.File.Create(audioPath);\n\t\t\t\t\ttag = audioFile.Tag;\n\t\t\t\t\tif (Album.HasAlbumInfo(tag))\n\t\t\t\t\t\taudioInfo.Album = new Album(tag, true);\n\t\t\t\t\taudioInfo.Track = new Track(tag);\n\t\t\t\t}\n\t\t\t\tcatch (Exception ex) {\n\t\t\t\t\tFastConsole.WriteError(\"无效音频文件！\");\n\t\t\t\t\tFastConsole.WriteException(ex);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tfinally {\n\t\t\t\t\taudioFile?.Dispose();\n\t\t\t\t}\n\t\t\t\tTrackInfo trackInfo;\n\t\t\t\tlock (_database.TrackInfos)\n\t\t\t\t\ttrackInfo = _database.TrackInfos.Match(audioInfo.Album, audioInfo.Track);\n\t\t\t\tif (!(trackInfo is null)) {\n\t\t\t\t\taudioInfo.TrackInfo = trackInfo;\n\t\t\t\t\treturn audioInfo;\n\t\t\t\t}\n\t\t\t\t// 尝试从数据库获取歌曲\n\t\t\t\tif (The163KeyHelper.TryGetTrackId(tag, out int trackId)) {\n\t\t\t\t\ttrackInfo = new TrackInfo(audioInfo.Track, audioInfo.Album, trackId);\n\t\t\t\t\tlock (_database.TrackInfos)\n\t\t\t\t\t\t_database.TrackInfos.Add(trackInfo);\n\t\t\t\t\taudioInfo.TrackInfo = trackInfo;\n\t\t\t\t\treturn audioInfo;\n\t\t\t\t}\n\t\t\t\t// 尝试从163Key获取ID\n\t\t\t\treturn audioInfo;\n\t\t\t}).Where(t => !(t is null)).ToArray();\n\t\t}\n\n\t\tprivate static async Task LoadAllAudioInfoCandidates(AudioInfo[] audioInfoCandidates, Action<AudioInfo> callback) {\n\t\t\tforeach (var candidate in audioInfoCandidates) {\n\t\t\t\tFastConsole.WriteInfo($\"开始获取文件\\\"{Path.GetFileName(candidate.Path)}\\\"的网易云音乐ID。\");\n\t\t\t\tTrackInfo trackInfo;\n\t\t\t\ttry {\n\t\t\t\t\ttrackInfo = await SearchTrackAsync(candidate.Album, candidate.Track);\n\t\t\t\t}\n\t\t\t\tcatch (Exception ex) {\n\t\t\t\t\tFastConsole.WriteException(ex);\n\t\t\t\t\ttrackInfo = null;\n\t\t\t\t}\n\t\t\t\tif (trackInfo is null) {\n\t\t\t\t\tFastConsole.WriteWarning($\"无法找到文件\\\"{Path.GetFileName(candidate.Path)}\\\"的网易云音乐ID！\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tFastConsole.WriteInfo($\"已获取文件\\\"{Path.GetFileName(candidate.Path)}\\\"的网易云音乐ID: {trackInfo.Id}。\");\n\t\t\t\t\tcandidate.TrackInfo = new TrackInfo(candidate.Track, candidate.Album, trackInfo.Id);\n\t\t\t\t\t_database.TrackInfos.Add(candidate.TrackInfo);\n\t\t\t\t}\n\t\t\t\tcallback?.Invoke(candidate);\n\t\t\t\tFastConsole.WriteNewLine();\n\t\t\t}\n\t\t}\n\n\t\tprivate static async Task DownloadLyricsAsync(AudioInfo[] audioInfos) {\n\t\t\tforeach (var audioInfo in audioInfos)\n\t\t\t\tawait TryDownloadLyricAsync(audioInfo);\n\t\t}\n\n\t\t#region search\n\t\t/// <summary>\n\t\t/// 同时根据专辑信息以及歌曲信息获取网易云音乐上的歌曲\n\t\t/// </summary>\n\t\t/// <param name=\"album\"></param>\n\t\t/// <param name=\"track\"></param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<TrackInfo> SearchTrackAsync(Album album, Track track) {\n\t\t\tvar albumInfo = album is null ? null : await SearchAlbumAsync(album);\n\t\t\t// 尝试获取专辑信息\n\t\t\tvar ncmTrack = default(NcmTrack);\n\t\t\tif (!(albumInfo is null)) {\n\t\t\t\t// 网易云音乐收录了歌曲所在专辑\t\n\t\t\t\tvar ncmTracks = (await GetAlbumTracksAsync(albumInfo)).Where(t => ComputeSimilarity(t.Name, track.Name, false) != 0).ToArray();\n\t\t\t\t// 获取网易云音乐上专辑收录的歌曲\n\t\t\t\tncmTrack = MatchByUser(ncmTracks, track);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tncmTrack = null;\n\t\t\t}\n\t\t\tif (ncmTrack is null)\n\t\t\t\tncmTrack = await MapToAsync(track);\n\t\t\t// 没有对应的专辑信息，使用无专辑匹配，或者网易云音乐上的专辑可能没收录这个歌曲，不清楚为什么，但是确实存在这个情况，比如专辑id:3094396\n\t\t\tbool byUser;\n\t\t\tint trackId;\n\t\t\tif (ncmTrack is null) {\n\t\t\t\tbyUser = GetIdByUser(\"歌曲\", out trackId);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbyUser = false;\n\t\t\t\ttrackId = 0;\n\t\t\t}\n\t\t\tvar trackInfo = default(TrackInfo);\n\t\t\tif (ncmTrack is null && !byUser) {\n\t\t\t\ttrackInfo = null;\n\t\t\t\tFastConsole.WriteWarning(\"歌曲匹配失败！\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttrackInfo = new TrackInfo(track, album, byUser ? trackId : ncmTrack.Id);\n\t\t\t\t_database.TrackInfos.Add(trackInfo);\n\t\t\t\tFastConsole.WriteInfo(\"歌曲匹配成功！\");\n\t\t\t}\n\t\t\treturn trackInfo;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 根据专辑信息获取网易云音乐上的专辑\n\t\t/// </summary>\n\t\t/// <param name=\"album\"></param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<AlbumInfo> SearchAlbumAsync(Album album) {\n\t\t\tvar albumInfo = _database.AlbumInfos.Match(album);\n\t\t\tif (!(albumInfo is null))\n\t\t\t\treturn albumInfo;\n\t\t\t// 先尝试从数据库获取专辑\n\t\t\tstring replacedAlbumName = album.Name.ReplaceEx();\n\t\t\tif (_failMatchAlbums.Contains(replacedAlbumName))\n\t\t\t\treturn null;\n\t\t\t// 防止不停重复匹配一个专辑\n\t\t\tvar ncmAlbum = await MapToAsync(album);\n\t\t\tbool byUser;\n\t\t\tint albumId;\n\t\t\tif (ncmAlbum is null) {\n\t\t\t\tbyUser = GetIdByUser(\"专辑\", out albumId);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbyUser = false;\n\t\t\t\talbumId = 0;\n\t\t\t}\n\t\t\tif (ncmAlbum is null && !byUser) {\n\t\t\t\t_failMatchAlbums.Add(replacedAlbumName);\n\t\t\t\tFastConsole.WriteWarning(\"专辑匹配失败！\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\talbumInfo = new AlbumInfo(album, byUser ? albumId : ncmAlbum.Id);\n\t\t\t\t_database.AlbumInfos.Add(albumInfo);\n\t\t\t\tFastConsole.WriteInfo(\"专辑匹配成功！\");\n\t\t\t}\n\t\t\treturn albumInfo;\n\t\t}\n\n\t\tprivate static async Task<NcmTrack[]> GetAlbumTracksAsync(AlbumInfo albumInfo) {\n\t\t\tif (!_cachedNcmTrackses.TryGetValue(albumInfo.Id, out var ncmTracks)) {\n\t\t\t\tvar list = new List<NcmTrack>();\n\t\t\t\tforeach (var item in await _cloudMusic.GetTracksAsync(albumInfo.Id)) {\n\t\t\t\t\tif ((await GetLyricAsync(item.Id)).IsCollected)\n\t\t\t\t\t\tlist.Add(item);\n\t\t\t\t}\n\t\t\t\tncmTracks = list.ToArray();\n\t\t\t\tlock (((ICollection)_cachedNcmTrackses).SyncRoot)\n\t\t\t\t\t_cachedNcmTrackses[albumInfo.Id] = ncmTracks;\n\t\t\t}\n\t\t\treturn ncmTracks;\n\t\t}\n\t\t#endregion\n\n\t\t#region map\n\t\t/// <summary>\n\t\t/// 获取网易云音乐上的歌曲，自动尝试带艺术家与不带艺术家搜索\n\t\t/// </summary>\n\t\t/// <param name=\"track\"></param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<NcmTrack> MapToAsync(Track track) {\n\t\t\tif (track is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(track));\n\n\t\t\tFastConsole.WriteInfo($\"开始搜索歌曲\\\"{track}\\\"。\");\n\t\t\tFastConsole.WriteWarning(\"正在尝试带艺术家搜索，结果可能将过少！\");\n\t\t\tvar ncmTrack = await MapToAsync(track, true);\n\t\t\tif (ncmTrack is null && _fuzzySettings.TryIgnoringArtists) {\n\t\t\t\tFastConsole.WriteWarning(\"正在尝试忽略艺术家搜索，结果可能将不精确！\");\n\t\t\t\tncmTrack = await MapToAsync(track, false);\n\t\t\t}\n\t\t\treturn ncmTrack;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 获取网易云音乐上的专辑，自动尝试带艺术家与不带艺术家搜索\n\t\t/// </summary>\n\t\t/// <param name=\"album\"></param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<NcmAlbum> MapToAsync(Album album) {\n\t\t\tif (album is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(album));\n\n\t\t\tFastConsole.WriteInfo($\"开始搜索专辑\\\"{album}\\\"。\");\n\t\t\tFastConsole.WriteWarning(\"正在尝试带艺术家搜索，结果可能将过少！\");\n\t\t\tvar ncmAlbum = await MapToAsync(album, true);\n\t\t\tif (ncmAlbum is null && _fuzzySettings.TryIgnoringArtists) {\n\t\t\t\tFastConsole.WriteWarning(\"正在尝试忽略艺术家搜索，结果可能将不精确！\");\n\t\t\t\tncmAlbum = await MapToAsync(album, false);\n\t\t\t}\n\t\t\treturn ncmAlbum;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 获取网易云音乐上的歌曲\n\t\t/// </summary>\n\t\t/// <param name=\"track\"></param>\n\t\t/// <param name=\"withArtists\">是否带艺术家搜索</param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<NcmTrack> MapToAsync(Track track, bool withArtists) {\n\t\t\tvar ncmTracks = default(NcmTrack[]);\n\t\t\ttry {\n\t\t\t\tncmTracks = await _cloudMusic.SearchTrackAsync(track, _searchSettings.Limit, withArtists);\n\t\t\t}\n\t\t\tcatch (KeywordForbiddenException ex1) {\n\t\t\t\tFastConsole.WriteError(ex1.Message);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tcatch (Exception ex2) {\n\t\t\t\tFastConsole.WriteException(ex2);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tvar list = new List<NcmTrack>();\n\t\t\tforeach (var item in ncmTracks.Where(t => ComputeSimilarity(t.Name, track.Name, false) != 0)) {\n\t\t\t\tif ((await GetLyricAsync(item.Id)).IsCollected)\n\t\t\t\t\tlist.Add(item);\n\t\t\t}\n\t\t\tncmTracks = list.ToArray();\n\t\t\treturn MatchByUser(ncmTracks, track);\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 获取网易云音乐上的专辑\n\t\t/// </summary>\n\t\t/// <param name=\"album\"></param>\n\t\t/// <param name=\"withArtists\">是否带艺术家搜索</param>\n\t\t/// <returns></returns>\n\t\tprivate static async Task<NcmAlbum> MapToAsync(Album album, bool withArtists) {\n\t\t\tvar ncmAlbums = default(NcmAlbum[]);\n\t\t\ttry {\n\t\t\t\tncmAlbums = await _cloudMusic.SearchAlbumAsync(album, _searchSettings.Limit, withArtists);\n\t\t\t}\n\t\t\tcatch (KeywordForbiddenException ex1) {\n\t\t\t\tFastConsole.WriteError(ex1.Message);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tcatch (Exception ex2) {\n\t\t\t\tFastConsole.WriteException(ex2);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tncmAlbums = ncmAlbums.Where(t => ComputeSimilarity(t.Name, album.Name, false) != 0).ToArray();\n\t\t\treturn MatchByUser(ncmAlbums, album);\n\t\t}\n\t\t#endregion\n\n\t\t#region database\n\t\tprivate static void LoadDatabase(string databasePath) {\n\t\t\tif (File.Exists(databasePath)) {\n\t\t\t\t_database = JsonConvert.DeserializeObject<NLyricDatabase>(File.ReadAllText(databasePath));\n\t\t\t\tif (!_database.CheckFormatVersion())\n\t\t\t\t\tthrow new InvalidOperationException(\"尝试加载新格式数据库。\");\n\n\t\t\t\tif (_database.IsOldFormat()) {\n\t\t\t\t\tFastConsole.WriteWarning(\"不兼容的老格式数据库，将被覆盖重建！\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tSortDatabase();\n\t\t\t\t\tFastConsole.WriteInfo($\"搜索数据库\\\"{databasePath}\\\"加载成功。\");\n\t\t\t\t\tFastConsole.WriteNewLine();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_database = new NLyricDatabase() {\n\t\t\t\tAlbumInfos = new List<AlbumInfo>(),\n\t\t\t\tTrackInfos = new List<TrackInfo>(),\n\t\t\t\tFormatVersion = 1\n\t\t\t};\n\t\t\tif (File.Exists(databasePath))\n\t\t\t\tFile.Delete(databasePath);\n\t\t\tSaveDatabaseCore(databasePath);\n\t\t\tFile.SetAttributes(databasePath, FileAttributes.Hidden);\n\t\t}\n\n\t\tprivate static void SaveDatabase(string databasePath) {\n\t\t\tSortDatabase();\n\t\t\tSaveDatabaseCore(databasePath);\n\t\t\tFastConsole.WriteInfo($\"搜索数据库\\\"{databasePath}\\\"已被保存。\");\n\t\t}\n\n\t\tprivate static void SortDatabase() {\n\t\t\t_database.AlbumInfos.Sort((x, y) => string.CompareOrdinal(x.Name, y.Name));\n\t\t\t_database.TrackInfos.Sort((x, y) => string.CompareOrdinal(x.Name, y.Name));\n\t\t}\n\n\t\tprivate static void SaveDatabaseCore(string databasePath) {\n\t\t\tusing var stream = new FileStream(databasePath, FileMode.OpenOrCreate);\n\t\t\tusing var writer = new StreamWriter(stream);\n\t\t\twriter.Write(FormatJson(JsonConvert.SerializeObject(_database)));\n\t\t}\n\n\t\tprivate static string FormatJson(string json) {\n\t\t\tusing var writer = new StringWriter();\n\t\t\tusing var jsonWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };\n\t\t\tusing var reader = new StringReader(json);\n\t\t\tusing var jsonReader = new JsonTextReader(reader);\n\t\t\tjsonWriter.WriteToken(jsonReader);\n\t\t\treturn writer.ToString();\n\t\t}\n\t\t#endregion\n\n\t\t#region match\n\t\tprivate static TSource MatchByUser<TSource, TTarget>(TSource[] sources, TTarget target) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {\n\t\t\tif (sources.Length == 0)\n\t\t\t\treturn null;\n\t\t\tvar result = MatchByUser(sources, target, false);\n\t\t\tif (result is null && _fuzzySettings.TryIgnoringExtraInfo)\n\t\t\t\tresult = MatchByUser(sources, target, true);\n\t\t\treturn result;\n\t\t}\n\n\t\tprivate static TSource MatchByUser<TSource, TTarget>(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {\n\t\t\tif (sources.Length == 0)\n\t\t\t\treturn null;\n\n\t\t\tvar result = MatchExactly(sources, target, fuzzy);\n\t\t\tif (!fuzzy || !(result is null))\n\t\t\t\treturn result;\n\t\t\t// 不是fuzzy模式或者result不为空，可以直接返回结果，不需要用户选择了\n\n\t\t\tvar nameSimilarities = new Dictionary<TSource, double>();\n\t\t\tforeach (var source in sources)\n\t\t\t\tnameSimilarities[source] = ComputeSimilarity(source.Name, target.Name, fuzzy);\n\t\t\treturn Select(sources.Where(t => nameSimilarities[t] > _matchSettings.MinimumSimilarity).OrderByDescending(t => nameSimilarities[t]).ToArray(), target, nameSimilarities);\n\t\t}\n\n\t\tprivate static TSource MatchExactly<TSource, TTarget>(TSource[] sources, TTarget target, bool fuzzy) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {\n\t\t\tforeach (var source in sources) {\n\t\t\t\tstring x = source.Name;\n\t\t\t\tstring y = target.Name;\n\t\t\t\tif (fuzzy) {\n\t\t\t\t\tx = x.Fuzzy();\n\t\t\t\t\ty = y.Fuzzy();\n\t\t\t\t}\n\n\t\t\t\tif (x != y)\n\t\t\t\t\tgoto not_equal;\n\t\t\t\tif (source.Artists.Count != target.Artists.Count)\n\t\t\t\t\tgoto not_equal;\n\n\t\t\t\tfor (int i = 0; i < source.Artists.Count; i++) {\n\t\t\t\t\tx = source.Artists[i];\n\t\t\t\t\ty = target.Artists[i];\n\t\t\t\t\tif (fuzzy) {\n\t\t\t\t\t\tx = x.Fuzzy();\n\t\t\t\t\t\ty = y.Fuzzy();\n\t\t\t\t\t}\n\t\t\t\t\tif (x != y)\n\t\t\t\t\t\tgoto not_equal;\n\t\t\t\t}\n\t\t\t\treturn source;\n\n\t\t\tnot_equal:\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tprivate static TSource Select<TSource, TTarget>(TSource[] sources, TTarget target, Dictionary<TSource, double> nameSimilarities) where TSource : class, ITrackOrAlbum where TTarget : class, ITrackOrAlbum {\n\t\t\tif (sources.Length == 0)\n\t\t\t\treturn null;\n\n\t\t\tFastConsole.WriteInfo(\"请手动输入1,2,3...选择匹配的项，若不存在，请直接按下回车键。\");\n\t\t\tFastConsole.WriteInfo(\"对比项：\" + TrackOrAlbumToString(target));\n\t\t\tfor (int i = 0; i < sources.Length; i++) {\n\t\t\t\tdouble nameSimilarity = nameSimilarities[sources[i]];\n\t\t\t\tstring text = $\"{i + 1}. {sources[i]} (s:{nameSimilarity:F2})\";\n\t\t\t\tif (nameSimilarity >= 0.85)\n\t\t\t\t\tFastConsole.WriteLine(text, ConsoleColor.Green);\n\t\t\t\telse if (nameSimilarity >= 0.5)\n\t\t\t\t\tFastConsole.WriteLine(text, ConsoleColor.Yellow);\n\t\t\t\telse\n\t\t\t\t\tFastConsole.WriteInfo(text);\n\t\t\t}\n\n\t\t\tvar result = default(TSource);\n\t\t\tdo {\n\t\t\t\tstring userInput = FastConsole.ReadLine().Trim();\n\t\t\t\tif (userInput.Length == 0)\n\t\t\t\t\tbreak;\n\t\t\t\tif (int.TryParse(userInput, out int index)) {\n\t\t\t\t\tindex -= 1;\n\t\t\t\t\tif (index >= 0 && index < sources.Length) {\n\t\t\t\t\t\tresult = sources[index];\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tFastConsole.WriteWarning(\"输入有误，请重新输入！\");\n\t\t\t} while (true);\n\n\t\t\tif (!(result is null))\n\t\t\t\tFastConsole.WriteInfo(\"已选择：\" + result.ToString());\n\t\t\treturn result;\n\n\t\t\tstatic string TrackOrAlbumToString(ITrackOrAlbum trackOrAlbum) {\n\t\t\t\tif (trackOrAlbum.Artists.Count == 0)\n\t\t\t\t\treturn trackOrAlbum.Name;\n\t\t\t\treturn trackOrAlbum.Name + \" by \" + string.Join(\",\", trackOrAlbum.Artists);\n\t\t\t}\n\t\t}\n\n\t\tprivate static double ComputeSimilarity(string x, string y, bool fuzzy) {\n\t\t\tx = x.ReplaceEx();\n\t\t\ty = y.ReplaceEx();\n\t\t\tif (fuzzy) {\n\t\t\t\tx = x.Fuzzy();\n\t\t\t\ty = y.Fuzzy();\n\t\t\t}\n\t\t\tx = x.Trim();\n\t\t\ty = y.Trim();\n\t\t\treturn Levenshtein.Compute(x, y);\n\t\t}\n\n\t\tprivate static bool GetIdByUser(string s, out int id) {\n\t\t\tFastConsole.WriteInfo($\"请输入{s}的网易云音乐ID，若不存在，请直接按下回车键。\");\n\t\t\tdo {\n\t\t\t\tstring userInput = FastConsole.ReadLine().Trim();\n\t\t\t\tif (userInput.Length == 0)\n\t\t\t\t\tbreak;\n\t\t\t\tif (int.TryParse(userInput, out id))\n\t\t\t\t\treturn true;\n\t\t\t\tFastConsole.WriteWarning(\"输入有误，请重新输入！\");\n\t\t\t} while (true);\n\t\t\tid = 0;\n\t\t\treturn false;\n\t\t}\n\t\t#endregion\n\n\t\t#region lyric\n\t\tprivate static async Task<bool> TryDownloadLyricAsync(AudioInfo audioInfo) {\n\t\t\tstring lrcPath = Path.ChangeExtension(audioInfo.Path, \".lrc\");\n\t\t\tbool hasLrcFile = File.Exists(lrcPath);\n\t\t\tvar trackInfo = audioInfo.TrackInfo;\n\t\t\tNcmLyric ncmLyric;\n\t\t\ttry {\n\t\t\t\tncmLyric = await GetLyricAsync(trackInfo.Id);\n\t\t\t}\n\t\t\tcatch (Exception ex) {\n\t\t\t\tFastConsole.WriteException(ex);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tFastConsole.WriteInfo($\"正在尝试下载\\\"{Path.GetFileName(audioInfo.Path)} ({audioInfo.Track})\\\"的歌词。\");\n\t\t\tif (hasLrcFile) {\n\t\t\t\t// 如果歌词存在，判断是否需要覆盖或更新\n\t\t\t\tvar lyricInfo = trackInfo.Lyric;\n\t\t\t\tstring lyricCheckSum = hasLrcFile ? ComputeLyricCheckSum(File.ReadAllBytes(lrcPath)) : null;\n\t\t\t\tif (!(lyricInfo is null) && lyricInfo.CheckSum == lyricCheckSum) {\n\t\t\t\t\t// 歌词由NLyric创建\n\t\t\t\t\tif (ncmLyric.RawVersion <= lyricInfo.RawVersion && ncmLyric.TranslatedVersion <= lyricInfo.TranslatedVersion) {\n\t\t\t\t\t\t// 是最新版本\n\t\t\t\t\t\tFastConsole.WriteInfo(\"本地歌词已是最新版本，正在跳过。\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// 不是最新版本\n\t\t\t\t\t\tif (_lyricSettings.AutoUpdate) {\n\t\t\t\t\t\t\tFastConsole.WriteLine(\"本地歌词不是最新版本，正在更新。\", ConsoleColor.Green);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tFastConsole.WriteLine(\"本地歌词不是最新版本但是自动更新被禁止，正在跳过。\", ConsoleColor.Yellow);\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// 歌词非NLyric创建\n\t\t\t\t\tif (_lyricSettings.Overwriting) {\n\t\t\t\t\t\tFastConsole.WriteLine(\"本地歌词非NLyric创建，正在更新。\", ConsoleColor.Yellow);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tFastConsole.WriteLine(\"本地歌词非NLyric创建但是覆盖被禁止，正在跳过。\", ConsoleColor.Yellow);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tvar lrc = ToLrc(ncmLyric);\n\t\t\tif (!(lrc is null)) {\n\t\t\t\t// 歌词已收录，不是纯音乐\n\t\t\t\tstring lyric = lrc.ToString();\n\t\t\t\ttry {\n\t\t\t\t\tFile.WriteAllText(lrcPath, lyric, _lyricSettings.Encoding);\n\t\t\t\t}\n\t\t\t\tcatch (Exception ex) {\n\t\t\t\t\tFastConsole.WriteException(ex);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\ttrackInfo.Lyric = new LyricInfo(ncmLyric, ComputeLyricCheckSum(_lyricSettings.Encoding.GetBytes(lyric)));\n\t\t\t\tFastConsole.WriteLine(\"本地歌词下载完毕。\", ConsoleColor.Magenta);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate static async Task<NcmLyric> GetLyricAsync(int trackId) {\n\t\t\tif (!_cachedNcmLyrics.TryGetValue(trackId, out var lyric)) {\n\t\t\t\tlyric = await _cloudMusic.GetLyricAsync(trackId);\n\t\t\t\tlock (((ICollection)_cachedNcmLyrics).SyncRoot)\n\t\t\t\t\t_cachedNcmLyrics[trackId] = lyric;\n\t\t\t}\n\t\t\treturn lyric;\n\t\t}\n\n\t\tprivate static Lrc ToLrc(NcmLyric lyric) {\n\t\t\tif (!lyric.IsCollected) {\n\t\t\t\tFastConsole.WriteWarning(\"当前歌曲的歌词未被收录！\");\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (lyric.IsAbsoluteMusic) {\n\t\t\t\tFastConsole.WriteWarning(\"当前歌曲是纯音乐无歌词！\");\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif (!(lyric.Raw is null))\n\t\t\t\tNormalizeLyric(lyric.Raw, false);\n\t\t\tif (!(lyric.Translated is null))\n\t\t\t\tNormalizeLyric(lyric.Translated, _lyricSettings.SimplifyTranslated);\n\n\t\t\tforeach (string mode in _lyricSettings.Modes) {\n\t\t\t\tswitch (mode.ToUpperInvariant()) {\n\t\t\t\tcase \"MERGED\":\n\t\t\t\t\tif (lyric.Raw is null || lyric.Translated is null)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tFastConsole.WriteInfo(\"已获取混合歌词。\");\n\t\t\t\t\treturn MergeLyric(lyric.Raw, lyric.Translated);\n\n\t\t\t\tcase \"RAW\":\n\t\t\t\t\tif (lyric.Raw is null)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tFastConsole.WriteInfo(\"已获取原始歌词。\");\n\t\t\t\t\treturn lyric.Raw;\n\n\t\t\t\tcase \"TRANSLATED\":\n\t\t\t\t\tif (lyric.Translated is null)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tFastConsole.WriteInfo(\"已获取翻译歌词。\");\n\t\t\t\t\treturn lyric.Translated;\n\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new ArgumentOutOfRangeException(nameof(mode));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tFastConsole.WriteWarning(\"获取歌词失败（可能歌曲是纯音乐但是未被网易云音乐标记为纯音乐）。\");\n\t\t\treturn null;\n\t\t}\n\n\t\tprivate static void NormalizeLyric(Lrc lrc, bool simplify) {\n\t\t\tvar newLyrics = new Dictionary<TimeSpan, string>(lrc.Lyrics.Count);\n\t\t\tforeach (var lyric in lrc.Lyrics) {\n\t\t\t\tstring value = lyric.Value.Trim('/', ' ');\n\t\t\t\tif (simplify)\n\t\t\t\t\tvalue = ChineseConverter.TraditionalToSimplified(value);\n\t\t\t\tnewLyrics.Add(lyric.Key, value);\n\t\t\t}\n\t\t\tlrc.Lyrics = newLyrics;\n\t\t}\n\n\t\tprivate static Lrc MergeLyric(Lrc rawLrc, Lrc translatedLrc) {\n\t\t\tvar mergedLrc = new Lrc {\n\t\t\t\tOffset = rawLrc.Offset,\n\t\t\t\tTitle = rawLrc.Title\n\t\t\t};\n\n\t\t\tforeach (var rawLyric in rawLrc.Lyrics)\n\t\t\t\tmergedLrc.Lyrics.Add(rawLyric.Key, rawLyric.Value);\n\n\t\t\tforeach (var translatedLyric in translatedLrc.Lyrics) {\n\t\t\t\tif (translatedLyric.Value.Length == 0)\n\t\t\t\t\tcontinue;\n\t\t\t\t// 如果翻译歌词是空字符串，跳过\n\n\t\t\t\tif (!string.IsNullOrEmpty(translatedLyric.Value) && !mergedLrc.Lyrics.ContainsKey(translatedLyric.Key)) {\n\t\t\t\t\t// 如果有翻译歌词并且没有对应的未翻译歌词，直接添加\n\t\t\t\t\tmergedLrc.Lyrics.Add(translatedLyric.Key, translatedLyric.Value);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tstring rawLyric = mergedLrc.Lyrics[translatedLyric.Key];\n\t\t\t\tif (rawLyric.Length != 0)\n\t\t\t\t\tmergedLrc.Lyrics[translatedLyric.Key] = $\"{rawLyric} 「{translatedLyric.Value}」\";\n\t\t\t\t// 如果未翻译歌词是空字符串，表示上一句歌词的结束，那么跳过\n\t\t\t}\n\n\t\t\treturn mergedLrc;\n\t\t}\n\n\t\tprivate static string ComputeLyricCheckSum(byte[] lyric) {\n\t\t\treturn CRC32.Compute(lyric).ToString(\"X8\");\n\t\t}\n\t\t#endregion\n\n\t\t#region accelerate\n\t\tprivate static Task AccelerateAllTracksAsync(AudioInfo[] audioInfos) {\n\t\t\t// TODO\n\t\t\treturn Task.CompletedTask;\n\t\t}\n\n\t\tprivate static async Task AccelerateAllLyricsAsync(AudioInfo[] audioInfos) {\n\t\t\tconst int STEP = 50;\n\n\t\t\tint[] trackIds = audioInfos.Select(t => t.TrackInfo.Id).ToArray();\n\t\t\tfor (int i = 0; i < trackIds.Length; i += STEP) {\n\t\t\t\tvar trackIdMap = new Dictionary<string, int>(STEP);\n\t\t\t\tvar queries = new Dictionary<string, object>(STEP);\n\t\t\t\tint kMax = i + STEP <= trackIds.Length ? STEP : trackIds.Length % STEP;\n\t\t\t\tfor (int k = 0; k < kMax; k++) {\n\t\t\t\t\tstring route = \"/api/song/lyric\" + new string('/', k);\n\t\t\t\t\ttrackIdMap[route] = trackIds[i + k];\n\t\t\t\t\tqueries[route] = JsonConvert.SerializeObject(new Dictionary<string, object> {\n\t\t\t\t\t\t[\"id\"] = trackIds[i + k],\n\t\t\t\t\t\t[\"lv\"] = -1,\n\t\t\t\t\t\t[\"kv\"] = -1,\n\t\t\t\t\t\t[\"tv\"] = -1\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tvar (isOk, json) = await _cloudMusic.Api.RequestAsync(CloudMusicApiProviders.Batch, queries);\n\t\t\t\tif (!isOk) {\n\t\t\t\t\tFastConsole.WriteError($\"[Experimental] 歌词 {i}+{STEP} 加速失败！\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlock (((ICollection)_cachedNcmLyrics).SyncRoot) {\n\t\t\t\t\tforeach (var item in trackIdMap) {\n\t\t\t\t\t\tint trackId = item.Value;\n\t\t\t\t\t\tif (!(json[item.Key] is JObject lyricJson)) {\n\t\t\t\t\t\t\tFastConsole.WriteError($\"[Experimental] 歌词 {trackId} at {i}+{STEP} 加速失败！\");\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_cachedNcmLyrics[trackId] = _cloudMusic.ParseLyric(trackId, lyricJson);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t#endregion\n\n\t\tprivate sealed class AudioInfo {\n\t\t\tpublic string Path { get; set; }\n\n\t\t\tpublic Album Album { get; set; }\n\n\t\t\tpublic Track Track { get; set; }\n\n\t\t\tpublic TrackInfo TrackInfo { get; set; }\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Ncm/CloudMusic.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusing NeteaseCloudMusicApi;\nusing Newtonsoft.Json.Linq;\nusing NLyric.Audio;\nusing NLyric.Lyrics;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class CloudMusic {\n\t\tprivate readonly CloudMusicApi _api = new CloudMusicApi();\n\n\t\tpublic CloudMusicApi Api => _api;\n\n\t\tpublic async Task<bool> LoginAsync(string account, string password) {\n\t\t\tvar queries = new Dictionary<string, object>();\n\t\t\tbool isPhone = Regex.Match(account, \"^[0-9]+$\").Success;\n\t\t\tqueries[isPhone ? \"phone\" : \"email\"] = account;\n\t\t\tqueries[\"password\"] = password;\n\t\t\tvar (result, _) = await _api.RequestAsync(isPhone ? CloudMusicApiProviders.LoginCellphone : CloudMusicApiProviders.Login, queries);\n\t\t\treturn result;\n\t\t}\n\n\t\tpublic async Task<NcmTrack[]> SearchTrackAsync(Track track, int limit, bool withArtists) {\n\t\t\tvar keywords = new List<string>();\n\t\t\tif (track.Name.Length != 0)\n\t\t\t\tkeywords.Add(track.Name);\n\t\t\tif (withArtists)\n\t\t\t\tkeywords.AddRange(track.Artists);\n\t\t\tif (keywords.Count == 0)\n\t\t\t\tthrow new ArgumentException(\"歌曲信息无效\");\n\t\t\tfor (int i = 0; i < keywords.Count; i++)\n\t\t\t\tkeywords[i] = keywords[i].WholeWordReplace();\n\t\t\tvar (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary<string, object> {\n\t\t\t\t{ \"keywords\", string.Join(\" \", keywords) },\n\t\t\t\t{ \"type\", 1 },\n\t\t\t\t{ \"limit\", limit }\n\t\t\t});\n\t\t\tif (!isOk)\n\t\t\t\tthrow new ApplicationException(nameof(CloudMusicApiProviders.Search) + \" API错误\");\n\t\t\tif ((JObject)json[\"result\"] is null)\n\t\t\t\tthrow new KeywordForbiddenException(string.Join(\" \", keywords));\n\t\t\treturn ParseSearchTracks(json);\n\t\t}\n\n\t\tpublic NcmTrack[] ParseSearchTracks(JObject json) {\n\t\t\tjson = (JObject)json[\"result\"];\n\t\t\tif (!(json[\"songs\"] is JArray songs))\n\t\t\t\treturn Array.Empty<NcmTrack>();\n\t\t\treturn songs.Select(t => ParseTrack(t, false)).ToArray();\n\t\t}\n\n\t\tpublic async Task<NcmAlbum[]> SearchAlbumAsync(Album album, int limit, bool withArtists) {\n\t\t\tvar keywords = new List<string>();\n\t\t\tif (album.Name.Length != 0)\n\t\t\t\tkeywords.Add(album.Name);\n\t\t\tif (withArtists)\n\t\t\t\tkeywords.AddRange(album.Artists);\n\t\t\tif (keywords.Count == 0)\n\t\t\t\tthrow new ArgumentException(\"专辑信息无效\");\n\t\t\tfor (int i = 0; i < keywords.Count; i++)\n\t\t\t\tkeywords[i] = keywords[i].WholeWordReplace();\n\t\t\tvar (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Search, new Dictionary<string, object> {\n\t\t\t\t{ \"keywords\", string.Join(\" \", keywords) },\n\t\t\t\t{ \"type\", 10 },\n\t\t\t\t{ \"limit\", limit }\n\t\t\t});\n\t\t\tif (!isOk)\n\t\t\t\tthrow new ApplicationException(nameof(CloudMusicApiProviders.Search) + \" API错误\");\n\t\t\tif ((JObject)json[\"result\"] is null)\n\t\t\t\tthrow new KeywordForbiddenException(string.Join(\" \", keywords));\n\t\t\treturn ParseSearchAlbums(json);\n\t\t}\n\n\t\tpublic NcmAlbum[] ParseSearchAlbums(JObject json) {\n\t\t\tjson = (JObject)json[\"result\"];\n\t\t\tif (!(json[\"albums\"] is JArray albums))\n\t\t\t\treturn Array.Empty<NcmAlbum>();\n\t\t\t// albumCount不可信，搜索\"U-87 陈奕迅\"返回albums有内容，但是albumCount为0\n\t\t\treturn albums.Select(t => ParseAlbum(t)).ToArray();\n\t\t}\n\n\t\tpublic async Task<NcmTrack[]> GetTracksAsync(int albumId) {\n\t\t\tvar (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Album, new Dictionary<string, object> {\n\t\t\t\t{ \"id\", albumId }\n\t\t\t});\n\t\t\tif (!isOk)\n\t\t\t\tthrow new ApplicationException(nameof(CloudMusicApiProviders.Album) + \" API错误\");\n\t\t\treturn ParseTracks(json);\n\t\t}\n\n\t\tpublic NcmTrack[] ParseTracks(JObject json) {\n\t\t\treturn json[\"songs\"].Select(t => ParseTrack(t, true)).ToArray();\n\t\t}\n\n\t\tpublic async Task<NcmLyric> GetLyricAsync(int trackId) {\n\t\t\tvar (isOk, json) = await _api.RequestAsync(CloudMusicApiProviders.Lyric, new Dictionary<string, object> {\n\t\t\t\t{ \"id\", trackId }\n\t\t\t});\n\t\t\tif (!isOk)\n\t\t\t\tthrow new ApplicationException(nameof(CloudMusicApiProviders.Lyric) + \" API错误\");\n\t\t\treturn ParseLyric(trackId, json);\n\t\t}\n\n\t\tpublic NcmLyric ParseLyric(int trackId, JObject json) {\n\t\t\tif (json is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(json));\n\n\t\t\tif ((bool?)json[\"uncollected\"] == true)\n\t\t\t\treturn new NcmLyric(trackId, false, false, null, 0, null, 0);\n\t\t\t// 未收录\n\t\t\tif ((bool?)json[\"nolyric\"] == true)\n\t\t\t\treturn new NcmLyric(trackId, true, true, null, 0, null, 0);\n\t\t\t// 纯音乐\n\t\t\tvar (rawLrc, rawVersion) = ParseLyric(json[\"lrc\"]);\n\t\t\tvar (translatedLrc, translatedVersion) = ParseLyric(json[\"tlyric\"]);\n\t\t\treturn new NcmLyric(trackId, true, false, rawLrc, rawVersion, translatedLrc, translatedVersion);\n\t\t}\n\n\t\tprivate NcmAlbum ParseAlbum(JToken json) {\n\t\t\tvar album = new Album((string)json[\"name\"], ParseNames(json[\"artists\"]));\n\t\t\tvar ncmAlbum = new NcmAlbum(album, (int)json[\"id\"]);\n\t\t\treturn ncmAlbum;\n\t\t}\n\n\t\tprivate NcmTrack ParseTrack(JToken json, bool isShortName) {\n\t\t\tvar track = new Track((string)json[\"name\"], ParseNames(json[isShortName ? \"ar\" : \"artists\"]));\n\t\t\tvar ncmTrack = new NcmTrack(track, (int)json[\"id\"]);\n\t\t\treturn ncmTrack;\n\t\t}\n\n\t\tprivate string[] ParseNames(JToken json) {\n\t\t\treturn json.Select(t => (string)t[\"name\"]).ToArray();\n\t\t}\n\n\t\tprivate (Lrc, int) ParseLyric(JToken json) {\n\t\t\tstring lyric = (string)json[\"lyric\"];\n\t\t\tvar lrc = string.IsNullOrEmpty(lyric) ? null : Lrc.UnsafeParse(lyric);\n\t\t\tint version = (int)json[\"version\"];\n\t\t\treturn (lrc, version);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Ncm/KeywordForbiddenException.cs",
    "content": "using System;\nusing System.Runtime.Serialization;\n\nnamespace NLyric.Ncm {\n\t/// <summary>\n\t/// 关键词被禁止\n\t/// </summary>\n\t[Serializable]\n\tpublic sealed class KeywordForbiddenException : Exception {\n\t\tpublic KeywordForbiddenException() {\n\t\t}\n\n\t\tpublic KeywordForbiddenException(string text) : base($\"\\\"{text}\\\" 中有关键词被屏蔽\") {\n\t\t}\n\n\t\tprivate KeywordForbiddenException(SerializationInfo info, StreamingContext context) : base(info, context) {\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Ncm/NcmAlbum.cs",
    "content": "using NLyric.Audio;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmAlbum : Album {\n\t\tprivate readonly int _id;\n\n\t\tpublic int Id => _id;\n\n\t\tpublic NcmAlbum(Album album, int id) : base(album.Name, album.Artists) {\n\t\t\t_id = id;\n\t\t}\n\n\t\tpublic override string ToString() {\n\t\t\treturn $\"{base.ToString()} | Id:{_id}\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Ncm/NcmLyric.cs",
    "content": "using NLyric.Lyrics;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmLyric {\n\t\tprivate readonly int _id;\n\t\tprivate readonly bool _isCollected;\n\t\tprivate readonly bool _isAbsoluteMusic;\n\t\tprivate readonly Lrc _raw;\n\t\tprivate readonly int _rawVersion;\n\t\tprivate readonly Lrc _translated;\n\t\tprivate readonly int _translatedVersion;\n\n\t\tpublic int Id => _id;\n\n\t\tpublic bool IsCollected => _isCollected;\n\n\t\tpublic bool IsAbsoluteMusic => _isAbsoluteMusic;\n\n\t\tpublic Lrc Raw => _raw;\n\n\t\tpublic int RawVersion => _rawVersion;\n\n\t\tpublic Lrc Translated => _translated;\n\n\t\tpublic int TranslatedVersion => _translatedVersion;\n\n\t\tpublic NcmLyric(int id, bool isCollected, bool isAbsoluteMusic, Lrc raw, int rawVersion, Lrc translated, int translatedVersion) {\n\t\t\t_id = id;\n\t\t\t_isCollected = isCollected;\n\t\t\t_isAbsoluteMusic = isAbsoluteMusic;\n\t\t\t_raw = raw;\n\t\t\t_rawVersion = rawVersion;\n\t\t\t_translated = translated;\n\t\t\t_translatedVersion = translatedVersion;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Ncm/NcmTrack.cs",
    "content": "using NLyric.Audio;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmTrack : Track {\n\t\tprivate readonly int _id;\n\n\t\tpublic int Id => _id;\n\n\t\tpublic NcmTrack(Track track, int id) : base(track.Name, track.Artists) {\n\t\t\t_id = id;\n\t\t}\n\n\t\tpublic override string ToString() {\n\t\t\treturn $\"{base.ToString()} | Id:{_id}\";\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Program.cs",
    "content": "using System;\nusing System.Cli;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Newtonsoft.Json;\nusing NLyric.Settings;\n\nnamespace NLyric {\n\tpublic static class Program {\n\t\tprivate static async Task Main(string[] args) {\n\t\t\tif (args is null || args.Length == 0) {\n\t\t\t\tCommandLine.ShowUsage<Arguments>();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tConsole.Title = GetTitle();\n\t\t\t}\n\t\t\tcatch {\n\t\t\t}\n\t\t\tif (!CommandLine.TryParse(args, out Arguments arguments)) {\n\t\t\t\tCommandLine.ShowUsage<Arguments>();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tAllSettings.Default = JsonConvert.DeserializeObject<AllSettings>(File.ReadAllText(\"Settings.json\"));\n\t\t\tawait NLyricImpl.ExecuteAsync(arguments);\n\t\t\tFastConsole.WriteLine(\"完成\", ConsoleColor.Green);\n\t\t\tFastConsole.Synchronize();\n\t\t\tif (Debugger.IsAttached) {\n\t\t\t\tConsole.WriteLine(\"按任意键继续...\");\n\t\t\t\ttry {\n\t\t\t\t\tConsole.ReadKey(true);\n\t\t\t\t}\n\t\t\t\tcatch {\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tprivate static string GetTitle() {\n\t\t\tstring productName = GetAssemblyAttribute<AssemblyProductAttribute>().Product;\n\t\t\tstring version = Assembly.GetExecutingAssembly().GetName().Version.ToString();\n\t\t\tstring copyright = GetAssemblyAttribute<AssemblyCopyrightAttribute>().Copyright.Substring(12);\n\t\t\tint firstBlankIndex = copyright.IndexOf(' ');\n\t\t\tstring copyrightOwnerName = copyright.Substring(firstBlankIndex + 1);\n\t\t\tstring copyrightYear = copyright.Substring(0, firstBlankIndex);\n\t\t\treturn $\"{productName} v{version} by {copyrightOwnerName} {copyrightYear}\";\n\t\t}\n\n\t\tprivate static T GetAssemblyAttribute<T>() {\n\t\t\treturn (T)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(T), false)[0];\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/AllSettings.cs",
    "content": "using System;\n\nnamespace NLyric.Settings {\n\tinternal sealed class AllSettings {\n\t\tprivate static AllSettings _default;\n\n\t\tpublic static AllSettings Default {\n\t\t\tget {\n\t\t\t\tif (_default is null)\n\t\t\t\t\tthrow new InvalidOperationException();\n\n\t\t\t\treturn _default;\n\t\t\t}\n\t\t\tset {\n\t\t\t\tif (value is null)\n\t\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\t\t\t\tif (!(_default is null))\n\t\t\t\t\tthrow new InvalidOperationException();\n\n\t\t\t\t_default = value;\n\t\t\t}\n\t\t}\n\n\t\tpublic SearchSettings Search { get; set; }\n\n\t\tpublic FuzzySettings Fuzzy { get; set; }\n\n\t\tpublic MatchSettings Match { get; set; }\n\n\t\tpublic LyricSettings Lyric { get; set; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/CharArrayJsonConverter.cs",
    "content": "using System;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class CharArrayJsonConverter : JsonConverter<char[]> {\n\t\tpublic override char[] ReadJson(JsonReader reader, Type objectType, char[] existingValue, bool hasExistingValue, JsonSerializer serializer) {\n\t\t\treturn ((string)reader.Value).ToCharArray();\n\t\t}\n\n\t\tpublic override void WriteJson(JsonWriter writer, char[] value, JsonSerializer serializer) {\n\t\t\twriter.WriteValue(new string(value));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/EncodingConverter.cs",
    "content": "using System;\nusing System.Text;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class EncodingConverter : JsonConverter<Encoding> {\n\t\tpublic override Encoding ReadJson(JsonReader reader, Type objectType, Encoding existingValue, bool hasExistingValue, JsonSerializer serializer) {\n\t\t\tvar encoding = Encoding.GetEncoding((string)reader.Value);\n\t\t\tif (encoding is UTF8Encoding)\n\t\t\t\tencoding = new UTF8Encoding(false, true);\n\t\t\treturn encoding;\n\t\t}\n\n\t\tpublic override void WriteJson(JsonWriter writer, Encoding value, JsonSerializer serializer) {\n\t\t\twriter.WriteValue(value.WebName);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/FuzzySettings.cs",
    "content": "using Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class FuzzySettings {\n\t\tpublic bool TryIgnoringArtists { get; set; }\n\n\t\tpublic bool TryIgnoringExtraInfo { get; set; }\n\n\t\t[JsonConverter(typeof(CharArrayJsonConverter))]\n\t\tpublic char[] ExtraInfoStart { get; set; }\n\n\t\tpublic string[] Covers { get; set; }\n\n\t\tpublic string[] Featurings { get; set; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/LyricSettings.cs",
    "content": "using System.Text;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class LyricSettings {\n\t\tpublic string[] Modes { get; set; }\n\n\t\tpublic bool SimplifyTranslated { get; set; }\n\n\t\t[JsonConverter(typeof(EncodingConverter))]\n\t\tpublic Encoding Encoding { get; set; }\n\n\t\tpublic bool AutoUpdate { get; set; }\n\n\t\tpublic bool Overwriting { get; set; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/MatchSettings.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace NLyric.Settings {\n\tinternal sealed class MatchSettings {\n\t\tprivate double _minimumSimilarity;\n\n\t\tpublic double MinimumSimilarity {\n\t\t\tget => _minimumSimilarity;\n\t\t\tset {\n\t\t\t\tif (value < 0 || value > 1)\n\t\t\t\t\tthrow new ArgumentOutOfRangeException(nameof(value));\n\n\t\t\t\t_minimumSimilarity = value;\n\t\t\t}\n\t\t}\n\n\t\tpublic Dictionary<char, char> CharReplace { get; set; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings/SearchSettings.cs",
    "content": "using System.Collections.Generic;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class SearchSettings {\n\t\tpublic string[] AudioExtensions { get; set; }\n\n\t\t[JsonConverter(typeof(CharArrayJsonConverter))]\n\t\tpublic char[] Separators { get; set; }\n\n\t\tpublic Dictionary<string, string> WholeWordReplace { get; set; }\n\n\t\tpublic int Limit { get; set; }\n\t}\n}\n"
  },
  {
    "path": "NLyric/Settings.json",
    "content": "{ // 所有匹配都是忽略大小写的！！！\n\t\"Search\": { // 搜索设置，在每一次搜索生效。\n\t\t\"AudioExtensions\": [\n\t\t\t\".aac\",\n\t\t\t\".ape\",\n\t\t\t\".flac\",\n\t\t\t\".m4a\",\n\t\t\t\".mp3\",\n\t\t\t\".ogg\",\n\t\t\t\".wav\",\n\t\t\t\".wma\"\n\t\t], // 会被识别为歌曲的扩展名。\n\t\t\"Separators\": \"|;,/\\\\&:\", // 分隔符，用于分割歌手名。\n\t\t\"WholeWordReplace\": {}, // 前面是被替换的词，后面是要替换成的词，比如\"A\": \"B\"，那么在搜索\"A\"的时候会替换为\"B\"来搜索。\n\t\t\"Limit\": 15 // 搜索结果数量。\n\t},\n\t\"Fuzzy\": { // 第一次搜不到或者匹配失败的情况下，是否进行模糊搜索与匹配。\n\t\t\"TryIgnoringArtists\": true, // 忽略艺术家。\n\t\t\"TryIgnoringExtraInfo\": true, // 忽略 括号/空格 + Cover/feat. 之后的内容，支持的括号类型在Filter.OpenBrackets里。\n\t\t\"ExtraInfoStart\": \" ([{【〖\", // 空格和左括号等之后的内容会被过滤，注意，不要随便修改这里的内容，可能导致过滤准确性降低。\n\t\t\"Covers\": [\n\t\t\t\"Cover\",\n\t\t\t\"カバー\"\n\t\t], // Cover的各种写法。\n\t\t\"Featurings\": [\n\t\t\t\"feat.\",\n\t\t\t\"ft.\"\n\t\t] // Feat.的各种写法。\n\t},\n\t\"Match\": { // 匹配设置，在搜索到歌曲信息之后，程序会通过自己的算法再次确认是否匹配。\n\t\t\"MinimumSimilarity\": 0.65, // 匹配时的最小相似度，小于设定值的将不予显示，0~1。\n\t\t\"CharReplace\": {\n\t\t\t\"\\u00B7\": \"\\u002e\",\n\t\t\t\"\\u0387\": \"\\u002e\",\n\t\t\t\"\\u05BC\": \"\\u002e\",\n\t\t\t\"\\u2022\": \"\\u002e\",\n\t\t\t\"\\u2027\": \"\\u002e\",\n\t\t\t\"\\u2219\": \"\\u002e\",\n\t\t\t\"\\u22C5\": \"\\u002e\",\n\t\t\t\"\\u30FB\": \"\\u002e\",\n\t\t\t\"\\uFF65\": \"\\u002e\",\n\t\t\t// .\n\t\t\t\"\\uFF0A\": \"\\u002A\",\n\t\t\t// *\n\t\t\t\"\\uFF01\": \"\\u0021\",\n\t\t\t// !\n\t\t\t\"\\uFF1A\": \"\\u003A\",\n\t\t\t// :\n\t\t\t\"\\u005B\": \"\\u0028\",\n\t\t\t\"\\u007B\": \"\\u0028\",\n\t\t\t\"\\u3010\": \"\\u0028\",\n\t\t\t\"\\u3016\": \"\\u0028\",\n\t\t\t// (\n\t\t\t\"\\u005D\": \"\\u0029\",\n\t\t\t\"\\u007D\": \"\\u0029\",\n\t\t\t\"\\u3011\": \"\\u0029\",\n\t\t\t\"\\u3017\": \"\\u0029\"\n\t\t\t// )\n\t\t} // 前面是被替换的字符，后面是要替换成的字符，只支持单个字符替换，意思就是一个文字，多个文字会报错。\n\t},\n\t\"Lyric\": {\n\t\t\"Modes\": [\n\t\t\t\"Merged\",\n\t\t\t\"Raw\",\n\t\t\t\"Translated\"\n\t\t], // 歌词模式，依次尝试每一个模式直到成功，Merged表示混合未翻译和翻译后歌词，Raw表示未翻译的歌词，Translated表示翻译后的歌词。\n\t\t\"SimplifyTranslated\": true, // 部分翻译后的歌词是繁体的，这个选项可以简体化翻译后的歌词。\n\t\t\"Encoding\": \"utf-8\",\n\t\t\"AutoUpdate\": true, // 是否自动更新由NLyric创建的歌词。\n\t\t\"Overwriting\": true // 是否覆盖非NLyric创建的歌词。\n\t}\n}\n"
  },
  {
    "path": "NLyric/StringHelper.cs",
    "content": "using System;\nusing System.Linq;\nusing System.Text;\nusing NLyric.Settings;\n\nnamespace NLyric {\n\tinternal static class StringHelper {\n\t\tprivate static readonly SearchSettings _searchSettings = AllSettings.Default.Search;\n\t\tprivate static readonly FuzzySettings _fuzzySettings = AllSettings.Default.Fuzzy;\n\t\tprivate static readonly MatchSettings _matchSettings = AllSettings.Default.Match;\n\n\t\t/// <summary>\n\t\t/// 获取非空字符串，并且清除首尾空格\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string GetSafeString(this string value) {\n\t\t\treturn value is null ? string.Empty : value.Trim();\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 同时调用 <see cref=\"ToHalfWidth(string)\"/>, <see cref=\"WholeWordReplace(string)\"/> 与 <see cref=\"CharReplace(string)\"/>\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string ReplaceEx(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\treturn value.ToHalfWidth().WholeWordReplace().CharReplace();\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 使用 <see cref=\"SearchSettings.WholeWordReplace\"/> 进行全词替换\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string WholeWordReplace(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\tif (value.Length == 0)\n\t\t\t\treturn value;\n\t\t\tforeach (var pair in _searchSettings.WholeWordReplace) {\n\t\t\t\tif (value.Equals(pair.Key, StringComparison.OrdinalIgnoreCase))\n\t\t\t\t\treturn pair.Value;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 使用 <see cref=\"MatchSettings.CharReplace\"/> 进行字符替换\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string CharReplace(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\tif (value.Length == 0)\n\t\t\t\treturn value;\n\t\t\tvar sb = new StringBuilder(value);\n\t\t\tfor (int i = 0; i < sb.Length; i++) {\n\t\t\t\tforeach (var pair in _matchSettings.CharReplace) {\n\t\t\t\t\tif (sb[i] == pair.Key)\n\t\t\t\t\t\tsb[i] = pair.Value;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sb.ToString();\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 使用 <see cref=\"FuzzySettings\"/> 进行模糊处理\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string Fuzzy(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\tint fuzzyStartIndex = -1;\n\t\t\twhile ((fuzzyStartIndex = value.IndexOfAny(_fuzzySettings.ExtraInfoStart, fuzzyStartIndex + 1)) != -1) {\n\t\t\t\tstring extraInfo = value.Substring(fuzzyStartIndex + 1);\n\t\t\t\tif (Enumerable.Concat(_fuzzySettings.Covers, _fuzzySettings.Featurings).Any(s => extraInfo.StartsWith(s, StringComparison.OrdinalIgnoreCase)))\n\t\t\t\t\treturn value.Substring(0, fuzzyStartIndex).TrimEnd();\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 使用 <see cref=\"SearchSettings.Separators\"/> 进行分割字符串并且移除空字符串\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string[] SplitEx(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\treturn value.Split(_searchSettings.Separators, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 全角字符转半角字符\n\t\t/// </summary>\n\t\t/// <param name=\"value\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static string ToHalfWidth(this string value) {\n\t\t\tif (value is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(value));\n\n\t\t\tchar[] chars = value.ToCharArray();\n\t\t\tfor (int i = 0; i < chars.Length; i++) {\n\t\t\t\tif (chars[i] == '\\u3000')\n\t\t\t\t\tchars[i] = '\\u0020';\n\t\t\t\telse if (chars[i] > '\\uFF00' && chars[i] < '\\uFF5F')\n\t\t\t\t\tchars[i] = (char)(chars[i] - 0xFEE0);\n\t\t\t}\n\t\t\treturn new string(chars);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/System/Cli/ArgumentAttribute.cs",
    "content": "namespace System.Cli {\n\t/// <summary>\n\t/// 表示一个命令行参数。被应用 <see cref=\"ArgumentAttribute\"/> 的属性必须为 <see cref=\"string\"/> 类型或 <see cref=\"bool\"/> 类型且为实例属性。\n\t/// </summary>\n\t[AttributeUsage(AttributeTargets.Property)]\n\tinternal sealed class ArgumentAttribute : Attribute {\n\t\tprivate readonly string _name;\n\t\tprivate bool _isRequired;\n\t\tprivate object _defaultValue;\n\t\tprivate string _type;\n\t\tprivate string _description;\n\n\t\t/// <summary>\n\t\t/// 参数名\n\t\t/// </summary>\n\t\tpublic string Name => _name;\n\n\t\t/// <summary>\n\t\t/// 是否为必选参数\n\t\t/// </summary>\n\t\tpublic bool IsRequired {\n\t\t\tget => _isRequired;\n\t\t\tset => _isRequired = value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 默认值，当 <see cref=\"IsRequired\"/> 为 <see langword=\"true\"/> 时，<see cref=\"DefaultValue\"/> 必须为 <see langword=\"null\"/>。\n\t\t/// </summary>\n\t\tpublic object DefaultValue {\n\t\t\tget => _defaultValue;\n\t\t\tset => _defaultValue = value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 参数类型，用于 <see cref=\"CommandLine.ShowUsage{T}\"/> 显示类型来简单描述参数。若应用到返回类型为 <see cref=\"bool\"/> 的属性上，<see cref=\"Type\"/> 必须为 <see langword=\"null\"/>。\n\t\t/// </summary>\n\t\tpublic string Type {\n\t\t\tget => _type;\n\t\t\tset => _type = value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 参数介绍，用于 <see cref=\"CommandLine.ShowUsage{T}\"/> 具体描述参数。\n\t\t/// </summary>\n\t\tpublic string Description {\n\t\t\tget => _description;\n\t\t\tset => _description = value;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 构造器\n\t\t/// </summary>\n\t\t/// <param name=\"name\">参数名</param>\n\t\tpublic ArgumentAttribute(string name) {\n\t\t\t_name = name;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/System/Cli/CommandLine.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text;\n\nnamespace System.Cli {\n\tinternal static class CommandLine {\n\t\tpublic static T Parse<T>(string[] args) where T : new() {\n\t\t\tif (args is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(args));\n\n\t\t\tif (!TryParse(args, out T result))\n\t\t\t\tthrow new FormatException($\"Invalid {nameof(args)} or generic parameter {nameof(T)}\");\n\t\t\treturn result;\n\t\t}\n\n\t\tpublic static bool TryParse<T>(string[] args, out T result) where T : new() {\n\t\t\tif (args is null) {\n\t\t\t\tresult = default;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (!TryGetArgumentInfos(typeof(T), out var argumentInfos)) {\n\t\t\t\tresult = default;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tresult = new T();\n\t\t\tfor (int i = 0; i < args.Length; i++) {\n\t\t\t\tif (!argumentInfos.TryGetValue(args[i], out var argumentInfo)) {\n\t\t\t\t\t// 不是有效参数名\n\t\t\t\t\tresult = default;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (argumentInfo.HasSetValue) {\n\t\t\t\t\t// 重复设置参数\n\t\t\t\t\tresult = default;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (argumentInfo.IsBoolean) {\n\t\t\t\t\t// 是 bool 类型，所以不需要其它判断，直接赋值 true\n\t\t\t\t\tif (!argumentInfo.TrySetValue(result, true)) {\n\t\t\t\t\t\tresult = default;\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\targumentInfo.HasSetValue = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (i == args.Length - 1) {\n\t\t\t\t\t// 需要提供值但是到末尾了，未提供值\n\t\t\t\t\tresult = default;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (!argumentInfo.TrySetValue(result, args[++i])) {\n\t\t\t\t\tresult = default;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\targumentInfo.HasSetValue = true;\n\t\t\t}\n\n\t\t\tforeach (var argumentInfo in argumentInfos.Values) {\n\t\t\t\tif (argumentInfo.HasSetValue)\n\t\t\t\t\tcontinue;\n\t\t\t\t// 参数已设置值\n\n\t\t\t\tif (argumentInfo.IsRequired) {\n\t\t\t\t\t// 是必选参数\n\t\t\t\t\tresult = default;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// 是可选参数\n\t\t\t\t\tif (!argumentInfo.TrySetValue(result, argumentInfo.DefaultValue)) {\n\t\t\t\t\t\tresult = default;\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tpublic static bool ShowUsage<T>() {\n\t\t\tvar type = typeof(T);\n\t\t\tvar propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);\n\t\t\tif (propertyInfos.Length == 0)\n\t\t\t\treturn false;\n\t\t\tvar argumentInfos = new List<ArgumentInfo>();\n\t\t\tforeach (var propertyInfo in propertyInfos) {\n\t\t\t\tif (!VerifyProperty(propertyInfo, out var attribute))\n\t\t\t\t\treturn false;\n\t\t\t\tif (attribute is null)\n\t\t\t\t\tcontinue;\n\t\t\t\targumentInfos.Add(new ArgumentInfo(attribute, propertyInfo));\n\t\t\t}\n\n\t\t\tint maxNameLength = argumentInfos.Max(t => GetArgumentFormat(t).Length);\n\t\t\tvar sb = new StringBuilder();\n\t\t\tsb.AppendLine(\"Options:\");\n\t\t\tforeach (var argumentInfo in argumentInfos) {\n\t\t\t\tsb.Append($\"  {GetArgumentFormat(argumentInfo).PadRight(maxNameLength)}  {argumentInfo.Description}\");\n\t\t\t\tif (!argumentInfo.IsRequired)\n\t\t\t\t\tsb.Append(\"  (Optional)\");\n\t\t\t\tsb.AppendLine();\n\t\t\t}\n\n\t\t\tConsole.WriteLine(sb.ToString());\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate static bool TryGetArgumentInfos(Type type, out Dictionary<string, ArgumentInfo> argumentInfos) {\n\t\t\tvar propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);\n\t\t\tif (propertyInfos.Length == 0) {\n\t\t\t\targumentInfos = null;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\targumentInfos = new Dictionary<string, ArgumentInfo>();\n\t\t\tforeach (var propertyInfo in propertyInfos) {\n\t\t\t\tif (!VerifyProperty(propertyInfo, out var attribute)) {\n\t\t\t\t\targumentInfos = null;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (!(attribute is null))\n\t\t\t\t\targumentInfos.Add(attribute.Name, new ArgumentInfo(attribute, propertyInfo));\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate static bool VerifyProperty(PropertyInfo propertyInfo, out ArgumentAttribute argumentAttribute) {\n\t\t\targumentAttribute = null;\n\t\t\tobject[] attributes = propertyInfo.GetCustomAttributes(typeof(ArgumentAttribute), false);\n\t\t\tif (attributes is null || attributes.Length == 0)\n\t\t\t\t// 排除未应用 ArgumentAttribute 的属性\n\t\t\t\treturn true;\n\t\t\tif (attributes.Length != 1)\n\t\t\t\t// ArgumentAttribute 不应该被应用多次\n\t\t\t\treturn false;\n\t\t\tvar propertyType = propertyInfo.PropertyType;\n\t\t\tif (propertyType != typeof(string) && propertyType != typeof(bool))\n\t\t\t\t// 检查返回类型\n\t\t\t\treturn false;\n\t\t\targumentAttribute = (ArgumentAttribute)attributes[0];\n\t\t\tif (string.IsNullOrEmpty(argumentAttribute.Name)) {\n\t\t\t\t// 检查参数名是否为空\n\t\t\t\targumentAttribute = null;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tforeach (char item in argumentAttribute.Name) {\n\t\t\t\tif (!((item >= 'a' && item <= 'z') || (item >= 'A' && item <= 'Z') || (item >= '0' && item <= '9') || item == '-' || item == '_')) {\n\t\t\t\t\t// 检查参数名是否合法\n\t\t\t\t\targumentAttribute = null;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (argumentAttribute.IsRequired && !(argumentAttribute.DefaultValue is null)) {\n\t\t\t\t// 是必选参数但有默认值\n\t\t\t\targumentAttribute = null;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (!(argumentAttribute.DefaultValue is null) && argumentAttribute.DefaultValue.GetType() != propertyType) {\n\t\t\t\t// 有默认值但默认值的类型与属性的类型不相同\n\t\t\t\targumentAttribute = null;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (!(argumentAttribute.Type is null) && propertyType == typeof(bool)) {\n\t\t\t\t// 返回类型为bool并且Type属性有值\n\t\t\t\targumentAttribute = null;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate static string GetArgumentFormat(ArgumentInfo argumentInfo) {\n\t\t\treturn !argumentInfo.IsBoolean ? argumentInfo.Name + \" \" + (string.IsNullOrEmpty(argumentInfo.Type) ? \"VALUE\" : argumentInfo.Type) : argumentInfo.Name;\n\t\t}\n\n\t\tprivate sealed class ArgumentInfo {\n\t\t\tprivate readonly ArgumentAttribute _attribute;\n\t\t\tprivate readonly PropertyInfo _propertyInfo;\n\t\t\tprivate bool? _cachedIsBoolean;\n\t\t\tprivate bool _hasSetValue;\n\n\t\t\tpublic string Name => _attribute.Name;\n\n\t\t\tpublic bool IsRequired => _attribute.IsRequired;\n\n\t\t\tpublic object DefaultValue => _attribute.DefaultValue;\n\n\t\t\tpublic string Type => _attribute.Type;\n\n\t\t\tpublic string Description => _attribute.Description;\n\n\t\t\tpublic bool IsBoolean {\n\t\t\t\tget {\n\t\t\t\t\tif (_cachedIsBoolean is null)\n\t\t\t\t\t\t_cachedIsBoolean = _propertyInfo.PropertyType == typeof(bool);\n\t\t\t\t\treturn _cachedIsBoolean.Value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpublic bool HasSetValue {\n\t\t\t\tget => _hasSetValue;\n\t\t\t\tset => _hasSetValue = value;\n\t\t\t}\n\n\t\t\tpublic ArgumentInfo(ArgumentAttribute attribute, PropertyInfo propertyInfo) {\n\t\t\t\t_attribute = attribute;\n\t\t\t\t_propertyInfo = propertyInfo;\n\t\t\t}\n\n\t\t\tpublic bool TrySetValue(object instance, object value) {\n\t\t\t\ttry {\n\t\t\t\t\t_propertyInfo.SetValue(instance, value, null);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tcatch {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric/The163KeyHelper.cs",
    "content": "using System;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Newtonsoft.Json.Linq;\nusing TagLib;\n\nnamespace NLyric {\n\t/// <summary>\n\t/// 通过163Key直接获取歌曲ID\n\t/// </summary>\n\tinternal static class The163KeyHelper {\n\t\tprivate static readonly Aes _aes = Create163Aes();\n\n\t\tprivate static Aes Create163Aes() {\n\t\t\tvar aes = Aes.Create();\n\t\t\taes.BlockSize = 128;\n\t\t\taes.Key = Encoding.UTF8.GetBytes(@\"#14ljk_!\\]&0U<'(\");\n\t\t\taes.Mode = CipherMode.ECB;\n\t\t\taes.Padding = PaddingMode.PKCS7;\n\t\t\treturn aes;\n\t\t}\n\n\t\t/// <summary>\n\t\t/// 尝试获取网易云音乐ID\n\t\t/// </summary>\n\t\t/// <param name=\"tag\"></param>\n\t\t/// <param name=\"trackId\"></param>\n\t\t/// <returns></returns>\n\t\tpublic static bool TryGetTrackId(Tag tag, out int trackId) {\n\t\t\tif (tag is null)\n\t\t\t\tthrow new ArgumentNullException(nameof(tag));\n\n\t\t\ttrackId = 0;\n\t\t\tstring the163Key = tag.Comment;\n\t\t\tif (!Is163KeyCandidate(the163Key))\n\t\t\t\tthe163Key = tag.Description;\n\t\t\tif (!Is163KeyCandidate(the163Key))\n\t\t\t\treturn false;\n\t\t\ttry {\n\t\t\t\tthe163Key = the163Key.Substring(22);\n\t\t\t\tbyte[] byt163Key = Convert.FromBase64String(the163Key);\n\t\t\t\tusing var cryptoTransform = _aes.CreateDecryptor();\n\t\t\t\tbyt163Key = cryptoTransform.TransformFinalBlock(byt163Key, 0, byt163Key.Length);\n\t\t\t\ttrackId = (int)JObject.Parse(Encoding.UTF8.GetString(byt163Key).Substring(6))[\"musicId\"];\n\t\t\t}\n\t\t\tcatch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tprivate static bool Is163KeyCandidate(string s) {\n\t\t\treturn !string.IsNullOrEmpty(s) && s.StartsWith(\"163 key(Don't modify):\", StringComparison.Ordinal);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric.Win/MainForm.Designer.cs",
    "content": "namespace NLyric.Win\n{\n    partial class MainForm\n    {\n        /// <summary>\n        /// 必需的设计器变量。\n        /// </summary>\n        private System.ComponentModel.IContainer components = null;\n\n        /// <summary>\n        /// 清理所有正在使用的资源。\n        /// </summary>\n        /// <param name=\"disposing\">如果应释放托管资源，为 true；否则为 false。</param>\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing && (components != null))\n            {\n                components.Dispose();\n            }\n            base.Dispose(disposing);\n        }\n\n        #region Windows 窗体设计器生成的代码\n\n        /// <summary>\n        /// 设计器支持所需的方法 - 不要修改\n        /// 使用代码编辑器修改此方法的内容。\n        /// </summary>\n        private void InitializeComponent()\n        {\n            this._btnSetDirectory = new System.Windows.Forms.Button();\n            this._tbDirectory = new System.Windows.Forms.TextBox();\n            this._cbLogin = new System.Windows.Forms.CheckBox();\n            this._tbAccount = new System.Windows.Forms.TextBox();\n            this._tbPassword = new System.Windows.Forms.TextBox();\n            this._btnRun = new System.Windows.Forms.Button();\n            this.SuspendLayout();\n            // \n            // _btnSetDirectory\n            // \n            this._btnSetDirectory.Location = new System.Drawing.Point(267, 12);\n            this._btnSetDirectory.Name = \"_btnSetDirectory\";\n            this._btnSetDirectory.Size = new System.Drawing.Size(102, 23);\n            this._btnSetDirectory.TabIndex = 0;\n            this._btnSetDirectory.Text = \"选择音频文件夹\";\n            this._btnSetDirectory.UseVisualStyleBackColor = true;\n            this._btnSetDirectory.Click += new System.EventHandler(this._btnSetDirectory_Click);\n            // \n            // _tbDirectory\n            // \n            this._tbDirectory.Location = new System.Drawing.Point(12, 12);\n            this._tbDirectory.Name = \"_tbDirectory\";\n            this._tbDirectory.Size = new System.Drawing.Size(249, 23);\n            this._tbDirectory.TabIndex = 1;\n            // \n            // _cbLogin\n            // \n            this._cbLogin.AutoSize = true;\n            this._cbLogin.Location = new System.Drawing.Point(12, 43);\n            this._cbLogin.Name = \"_cbLogin\";\n            this._cbLogin.Size = new System.Drawing.Size(75, 21);\n            this._cbLogin.TabIndex = 2;\n            this._cbLogin.Text = \"登录模式\";\n            this._cbLogin.UseVisualStyleBackColor = true;\n            this._cbLogin.CheckedChanged += new System.EventHandler(this._cbLogin_CheckedChanged);\n            // \n            // _tbAccount\n            // \n            this._tbAccount.Location = new System.Drawing.Point(93, 41);\n            this._tbAccount.Name = \"_tbAccount\";\n            this._tbAccount.Size = new System.Drawing.Size(135, 23);\n            this._tbAccount.TabIndex = 3;\n            this._tbAccount.Text = \"网易云音乐账号\";\n            // \n            // _tbPassword\n            // \n            this._tbPassword.Location = new System.Drawing.Point(234, 41);\n            this._tbPassword.Name = \"_tbPassword\";\n            this._tbPassword.Size = new System.Drawing.Size(135, 23);\n            this._tbPassword.TabIndex = 4;\n            this._tbPassword.Text = \"网易云音乐密码\";\n            // \n            // _btnRun\n            // \n            this._btnRun.Location = new System.Drawing.Point(375, 12);\n            this._btnRun.Name = \"_btnRun\";\n            this._btnRun.Size = new System.Drawing.Size(71, 52);\n            this._btnRun.TabIndex = 5;\n            this._btnRun.Text = \"启动\";\n            this._btnRun.UseVisualStyleBackColor = true;\n            this._btnRun.Click += new System.EventHandler(this._btnRun_Click);\n            // \n            // MainForm\n            // \n            this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F);\n            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;\n            this.ClientSize = new System.Drawing.Size(458, 76);\n            this.Controls.Add(this._btnRun);\n            this.Controls.Add(this._tbPassword);\n            this.Controls.Add(this._tbAccount);\n            this.Controls.Add(this._cbLogin);\n            this.Controls.Add(this._tbDirectory);\n            this.Controls.Add(this._btnSetDirectory);\n            this.Font = new System.Drawing.Font(\"Microsoft YaHei\", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));\n            this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);\n            this.Name = \"MainForm\";\n            this.Text = \"NLyric\";\n            this.ResumeLayout(false);\n            this.PerformLayout();\n\n        }\n\n\t\t#endregion\n\n\t\tprivate System.Windows.Forms.Button _btnSetDirectory;\n\t\tprivate System.Windows.Forms.TextBox _tbDirectory;\n\t\tprivate System.Windows.Forms.CheckBox _cbLogin;\n\t\tprivate System.Windows.Forms.TextBox _tbAccount;\n\t\tprivate System.Windows.Forms.TextBox _tbPassword;\n\t\tprivate System.Windows.Forms.Button _btnRun;\n\t}\n}\n\n"
  },
  {
    "path": "NLyric.Win/MainForm.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Windows.Forms;\n\nnamespace NLyric.Win {\n\tpublic sealed partial class MainForm : Form {\n\t\tpublic MainForm() {\n\t\t\tInitializeComponent();\n\t\t\tText = GetTitle(Assembly.Load(File.ReadAllBytes(\"NLyric.exe\")));\n\t\t\t_cbLogin_CheckedChanged(_cbLogin, EventArgs.Empty);\n\t\t}\n\n\t\tprivate static string GetTitle(Assembly assembly) {\n\t\t\tstring productName = GetAssemblyAttribute<AssemblyProductAttribute>(assembly).Product;\n\t\t\tstring version = Assembly.GetExecutingAssembly().GetName().Version.ToString();\n\t\t\tstring copyright = GetAssemblyAttribute<AssemblyCopyrightAttribute>(assembly).Copyright.Substring(12);\n\t\t\tint firstBlankIndex = copyright.IndexOf(' ');\n\t\t\tstring copyrightOwnerName = copyright.Substring(firstBlankIndex + 1);\n\t\t\tstring copyrightYear = copyright.Substring(0, firstBlankIndex);\n\t\t\treturn $\"{productName} v{version} by {copyrightOwnerName} {copyrightYear}\";\n\t\t}\n\n\t\tprivate static T GetAssemblyAttribute<T>(Assembly assembly) {\n\t\t\treturn (T)assembly.GetCustomAttributes(typeof(T), false)[0];\n\t\t}\n\n\t\tprivate void _btnSetDirectory_Click(object sender, EventArgs e) {\n\t\t\tusing (var dialog = new FolderBrowserDialog { ShowNewFolderButton = false }) {\n\t\t\t\tif (dialog.ShowDialog() != DialogResult.OK)\n\t\t\t\t\treturn;\n\t\t\t\t_tbDirectory.Text = dialog.SelectedPath;\n\t\t\t}\n\t\t}\n\n\t\tprivate void _cbLogin_CheckedChanged(object sender, EventArgs e) {\n\t\t\tbool state = _cbLogin.Checked;\n\t\t\t_tbAccount.Enabled = state;\n\t\t\t_tbPassword.Enabled = state;\n\t\t\tif (state) {\n\t\t\t\tif (_tbAccount.Text == \"网易云音乐账号\")\n\t\t\t\t\t_tbAccount.Text = string.Empty;\n\t\t\t\tif (_tbPassword.Text == \"网易云音乐密码\")\n\t\t\t\t\t_tbPassword.Text = string.Empty;\n\t\t\t\t_tbPassword.PasswordChar = '*';\n\t\t\t}\n\t\t}\n\n\t\tprivate void _btnRun_Click(object sender, EventArgs e) {\n\t\t\tstring arguments = $\"-d \\\"{_tbDirectory.Text}\\\"\";\n\t\t\tif (_cbLogin.Checked)\n\t\t\t\targuments += $\" -a {_tbAccount.Text} -p {_tbPassword.Text}\";\n\t\t\tProcess.Start(\"NLyric.exe\", arguments);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric.Win/MainForm.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <metadata name=\"_btnSetDirectory.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"_tbDirectory.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"_cbLogin.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"_tbAccount.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"_tbPassword.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"_btnRun.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n  <metadata name=\"$this.Locked\" type=\"System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\">\n    <value>True</value>\n  </metadata>\n</root>"
  },
  {
    "path": "NLyric.Win/NLyric.Win.csproj",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <Import Project=\"$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props\" Condition=\"Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props')\" />\n  <PropertyGroup>\n    <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>\n    <Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>\n    <ProjectGuid>{AB36D9B3-ECD3-46EA-973E-320648551850}</ProjectGuid>\n    <OutputType>WinExe</OutputType>\n    <RootNamespace>NLyric.Win</RootNamespace>\n    <AssemblyName>NLyric.Win</AssemblyName>\n    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>\n    <FileAlignment>512</FileAlignment>\n    <Deterministic>true</Deterministic>\n  </PropertyGroup>\n  <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' \">\n    <PlatformTarget>AnyCPU</PlatformTarget>\n    <DebugSymbols>true</DebugSymbols>\n    <DebugType>full</DebugType>\n    <Optimize>false</Optimize>\n    <OutputPath>..\\bin\\Debug\\net472\\</OutputPath>\n    <DefineConstants>DEBUG;TRACE</DefineConstants>\n    <ErrorReport>prompt</ErrorReport>\n    <WarningLevel>4</WarningLevel>\n  </PropertyGroup>\n  <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' \">\n    <PlatformTarget>AnyCPU</PlatformTarget>\n    <DebugType>pdbonly</DebugType>\n    <Optimize>true</Optimize>\n    <OutputPath>..\\bin\\Release\\net472\\</OutputPath>\n    <DefineConstants>TRACE</DefineConstants>\n    <ErrorReport>prompt</ErrorReport>\n    <WarningLevel>4</WarningLevel>\n  </PropertyGroup>\n  <ItemGroup>\n    <Reference Include=\"System\" />\n    <Reference Include=\"System.Drawing\" />\n    <Reference Include=\"System.Windows.Forms\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Compile Include=\"MainForm.cs\">\n      <SubType>Form</SubType>\n    </Compile>\n    <Compile Include=\"MainForm.Designer.cs\">\n      <DependentUpon>MainForm.cs</DependentUpon>\n    </Compile>\n    <Compile Include=\"Program.cs\" />\n    <Compile Include=\"Properties\\AssemblyInfo.cs\" />\n  </ItemGroup>\n  <ItemGroup>\n    <EmbeddedResource Include=\"MainForm.resx\">\n      <DependentUpon>MainForm.cs</DependentUpon>\n    </EmbeddedResource>\n  </ItemGroup>\n  <Import Project=\"$(MSBuildToolsPath)\\Microsoft.CSharp.targets\" />\n</Project>"
  },
  {
    "path": "NLyric.Win/Program.cs",
    "content": "using System;\nusing System.Windows.Forms;\n\nnamespace NLyric.Win {\n\tinternal static class Program {\n\t\t[STAThread]\n\t\tprivate static void Main() {\n\t\t\tApplication.EnableVisualStyles();\n\t\t\tApplication.SetCompatibleTextRenderingDefault(false);\n\t\t\tApplication.Run(new MainForm());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "NLyric.Win/Properties/AssemblyInfo.cs",
    "content": "using System.Reflection;\n\n[assembly: AssemblyTitle(\"NLyric.Win\")]\n[assembly: AssemblyProduct(\"NLyric.Win\")]\n[assembly: AssemblyCopyright(\"Copyright © 2019 Wwh\")]\n[assembly: AssemblyVersion(\"1.0.0.0\")]\n[assembly: AssemblyFileVersion(\"1.0.0.0\")]\n"
  },
  {
    "path": "NLyric.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.28822.285\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"NLyric\", \"NLyric\\NLyric.csproj\", \"{5423689E-6713-4F96-82E3-48B11E2A6412}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"NLyric.Win\", \"NLyric.Win\\NLyric.Win.csproj\", \"{AB36D9B3-ECD3-46EA-973E-320648551850}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{5423689E-6713-4F96-82E3-48B11E2A6412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5423689E-6713-4F96-82E3-48B11E2A6412}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5423689E-6713-4F96-82E3-48B11E2A6412}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5423689E-6713-4F96-82E3-48B11E2A6412}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AB36D9B3-ECD3-46EA-973E-320648551850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AB36D9B3-ECD3-46EA-973E-320648551850}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AB36D9B3-ECD3-46EA-973E-320648551850}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AB36D9B3-ECD3-46EA-973E-320648551850}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {8F4FF140-407E-434C-8FEC-6736239F77A6}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "README.md",
    "content": "# NLyric\n\n网易云音乐歌词自动搜索下载\n\n可选择登录或免登陆下载，避免出现网易云音乐接口异常。\n\n网易云音乐已屏蔽部分关键字，导致搜索出现异常，属于正常现象（网易云音乐客户端内很多歌曲名已经打上\\*号）。\n\n**New: Windows用户可以解压后双击\"NLyric.Win.exe\"启动GUI！！！**\n\n![GUI](./Images/GUI.png)\nWindows用户专属GUI。\n\n![自动下载](./Images/自动下载.png)\n自动下载。\n\n![匹配专辑](./Images/匹配专辑.png)\n自动匹配专辑。\n\n![用户选项](./Images/用户选项.png)\n在非完全精确匹配到的情况下，提供用户选项，并且按照匹配程度排序并高亮显示。\n\n![自动更新](./Images/自动更新.png)\n再次运行时，自动判断本地歌词是否需要更新（比如网易云音乐上歌词更新了翻译，NLyric可以检测到）。\n\n![歌词混合](./Images/歌词混合.png)\n提供歌词混合模式，同时显示原始歌词与翻译歌词。\n\n![缓存结果](./Images/缓存结果.png)\n第一次运行后缓存搜索结果，加快以后运行速度（会在音频文件夹创建隐藏文件.nlyric，请勿删除）。\n\n## 使用方式\n\n1. 下载压缩包（下载地址在下面），全部解压。\n\n2. 进入解压后的文件夹（内有NLyric.exe等文件），在文件夹内按住Shift，鼠标单击右键，选\"在此处打开命令窗口\"\n\n3. 输入命令\"NLyric.exe -d *音乐文件夹* -a *网易云音乐账号* -p *网易云音乐密码*\"以登录模式启动，或输入命令\"NLyric.exe -d *音乐文件夹*\"以免登录模式启动（NLyric不会保存您的账号密码或将您的账号密码发送到第三方，NLyric仅会调用网易云音乐官方API）\n\n4. 按照程序提示完成接下来的步骤\n\n命令行参数：\n\n```\nOptions:\n  -d DIR         存放音乐的文件夹，可以是相对路径或者绝对路径  (Optional)\n  -a STR         网易云音乐账号（邮箱/手机号）  (Optional)\n  -p STR         网易云音乐密码  (Optional)\n  --update-only  仅更新已有歌词  (Optional)\n  --batch        使用Batch API（实验性）  (Optional)\n```\n\n例子：\n\n```\nNLyric.exe -d C:\\Music -a example@example.com -p 123456 --batch\n```\n\n## 配置\n\n配置文件是\"Settings.json\"，自己修改即可\n\n默认配置：\n\n``` javascript\n{ // 所有匹配都是忽略大小写的！！！\n\t\"Search\": { // 搜索设置，在每一次搜索生效。\n\t\t\"AudioExtensions\": [\n\t\t\t\".aac\",\n\t\t\t\".ape\",\n\t\t\t\".flac\",\n\t\t\t\".m4a\",\n\t\t\t\".mp3\",\n\t\t\t\".ogg\",\n\t\t\t\".wav\",\n\t\t\t\".wma\"\n\t\t], // 会被识别为歌曲的扩展名。\n\t\t\"Separators\": \"|;,/\\\\&:\", // 分隔符，用于分割歌手名。\n\t\t\"WholeWordReplace\": {}, // 前面是被替换的词，后面是要替换成的词，比如\"A\": \"B\"，那么在搜索\"A\"的时候会替换为\"B\"来搜索。\n\t\t\"Limit\": 15 // 搜索结果数量。\n\t},\n\t\"Fuzzy\": { // 第一次搜不到或者匹配失败的情况下，是否进行模糊搜索与匹配。\n\t\t\"TryIgnoringArtists\": true, // 忽略艺术家。\n\t\t\"TryIgnoringExtraInfo\": true, // 忽略 括号/空格 + Cover/feat. 之后的内容，支持的括号类型在Filter.OpenBrackets里。\n\t\t\"ExtraInfoStart\": \" ([{【〖\", // 空格和左括号等之后的内容会被过滤，注意，不要随便修改这里的内容，可能导致过滤准确性降低。\n\t\t\"Covers\": [\n\t\t\t\"Cover\",\n\t\t\t\"カバー\"\n\t\t], // Cover的各种写法。\n\t\t\"Featurings\": [\n\t\t\t\"feat.\",\n\t\t\t\"ft.\"\n\t\t] // Feat.的各种写法。\n\t},\n\t\"Match\": { // 匹配设置，在搜索到歌曲信息之后，程序会通过自己的算法再次确认是否匹配。\n\t\t\"MinimumSimilarity\": 0.65, // 匹配时的最小相似度，小于设定值的将不予显示，0~1。\n\t\t\"CharReplace\": {\n\t\t\t\"\\u00B7\": \"\\u002e\",\n\t\t\t\"\\u0387\": \"\\u002e\",\n\t\t\t\"\\u05BC\": \"\\u002e\",\n\t\t\t\"\\u2022\": \"\\u002e\",\n\t\t\t\"\\u2027\": \"\\u002e\",\n\t\t\t\"\\u2219\": \"\\u002e\",\n\t\t\t\"\\u22C5\": \"\\u002e\",\n\t\t\t\"\\u30FB\": \"\\u002e\",\n\t\t\t\"\\uFF65\": \"\\u002e\",\n\t\t\t// .\n\t\t\t\"\\uFF0A\": \"\\u002A\",\n\t\t\t// *\n\t\t\t\"\\uFF01\": \"\\u0021\",\n\t\t\t// !\n\t\t\t\"\\uFF1A\": \"\\u003A\",\n\t\t\t// :\n\t\t\t\"\\u005B\": \"\\u0028\",\n\t\t\t\"\\u007B\": \"\\u0028\",\n\t\t\t\"\\u3010\": \"\\u0028\",\n\t\t\t\"\\u3016\": \"\\u0028\",\n\t\t\t// (\n\t\t\t\"\\u005D\": \"\\u0029\",\n\t\t\t\"\\u007D\": \"\\u0029\",\n\t\t\t\"\\u3011\": \"\\u0029\",\n\t\t\t\"\\u3017\": \"\\u0029\"\n\t\t\t// )\n\t\t} // 前面是被替换的字符，后面是要替换成的字符，只支持单个字符替换，意思就是一个文字，多个文字会报错。\n\t},\n\t\"Lyric\": {\n\t\t\"Modes\": [\n\t\t\t\"Merged\",\n\t\t\t\"Raw\",\n\t\t\t\"Translated\"\n\t\t], // 歌词模式，依次尝试每一个模式直到成功，Merged表示混合未翻译和翻译后歌词，Raw表示未翻译的歌词，Translated表示翻译后的歌词。\n\t\t\"SimplifyTranslated\": true, // 部分翻译后的歌词是繁体的，这个选项可以简体化翻译后的歌词。\n\t\t\"Encoding\": \"utf-8\",\n\t\t\"AutoUpdate\": true, // 是否自动更新由NLyric创建的歌词。\n\t\t\"Overwriting\": true // 是否覆盖非NLyric创建的歌词。\n\t}\n}\n```\n\n## 下载\n\nGitHub: [.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)\n\nAppVeyor: [![Build status](https://ci.appveyor.com/api/projects/status/vu5vyq11cm38pd7r/branch/master?svg=true)](https://ci.appveyor.com/project/wwh1004/nlyric/branch/master)\n\n## 感谢\n\n混合歌词的思路参考了 [EHfive/Some-js-script-for-FB2](https://github.com/EHfive/Some-js-script-for-FB2K)\n"
  },
  {
    "path": "appveyor.yml",
    "content": "version: '{build}'\nimage: Visual Studio 2019\nconfiguration: Release\nplatform: Any CPU\nbefore_build:\n- cmd: appveyor-retry nuget restore\nbuild:\n  project: NLyric.sln\n  verbosity: normal\nafter_build:\n- cmd: dotnet publish NLyric\\NLyric.csproj -c Release -f netcoreapp3.1\nartifacts:\n- path: bin\\Release\\net472\n  name: NLyric-net472\n- path: bin\\Release\\netcoreapp3.1\\publish\n  name: NLyric-netcoreapp3.1\ndeploy:\n- provider: GitHub\n  tag: $(APPVEYOR_REPO_TAG_NAME)\n  release: NLyric\n  auth_token:\n    secure: +8UJ1C312inNq+80I8WST34vPMrCylnmTx+9rmuIh1qnsArA5x2b8yc+kcwkXmQC\n  on:\n    APPVEYOR_REPO_TAG: true"
  }
]