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

Windows用户专属GUI。

自动下载。

自动匹配专辑。

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

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

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

第一次运行后缓存搜索结果,加快以后运行速度(会在音频文件夹创建隐藏文件.nlyric,请勿删除)。
## 使用方式
1. 下载压缩包(下载地址在下面),全部解压。
2. 进入解压后的文件夹(内有NLyric.exe等文件),在文件夹内按住Shift,鼠标单击右键,选"在此处打开命令窗口"
3. 输入命令"NLyric.exe -d *音乐文件夹* -a *网易云音乐账号* -p *网易云音乐密码*"以登录模式启动,或输入命令"NLyric.exe -d *音乐文件夹*"以免登录模式启动(NLyric不会保存您的账号密码或将您的账号密码发送到第三方,NLyric仅会调用网易云音乐官方API)
4. 按照程序提示完成接下来的步骤
命令行参数:
```
Options:
-d DIR 存放音乐的文件夹,可以是相对路径或者绝对路径 (Optional)
-a STR 网易云音乐账号(邮箱/手机号) (Optional)
-p STR 网易云音乐密码 (Optional)
--update-only 仅更新已有歌词 (Optional)
--batch 使用Batch API(实验性) (Optional)
```
例子:
```
NLyric.exe -d C:\Music -a example@example.com -p 123456 --batch
```
## 配置
配置文件是"Settings.json",自己修改即可
默认配置:
``` javascript
{ // 所有匹配都是忽略大小写的!!!
"Search": { // 搜索设置,在每一次搜索生效。
"AudioExtensions": [
".aac",
".ape",
".flac",
".m4a",
".mp3",
".ogg",
".wav",
".wma"
], // 会被识别为歌曲的扩展名。
"Separators": "|;,/\\&:", // 分隔符,用于分割歌手名。
"WholeWordReplace": {}, // 前面是被替换的词,后面是要替换成的词,比如"A": "B",那么在搜索"A"的时候会替换为"B"来搜索。
"Limit": 15 // 搜索结果数量。
},
"Fuzzy": { // 第一次搜不到或者匹配失败的情况下,是否进行模糊搜索与匹配。
"TryIgnoringArtists": true, // 忽略艺术家。
"TryIgnoringExtraInfo": true, // 忽略 括号/空格 + Cover/feat. 之后的内容,支持的括号类型在Filter.OpenBrackets里。
"ExtraInfoStart": " ([{【〖", // 空格和左括号等之后的内容会被过滤,注意,不要随便修改这里的内容,可能导致过滤准确性降低。
"Covers": [
"Cover",
"カバー"
], // Cover的各种写法。
"Featurings": [
"feat.",
"ft."
] // Feat.的各种写法。
},
"Match": { // 匹配设置,在搜索到歌曲信息之后,程序会通过自己的算法再次确认是否匹配。
"MinimumSimilarity": 0.65, // 匹配时的最小相似度,小于设定值的将不予显示,0~1。
"CharReplace": {
"\u00B7": "\u002e",
"\u0387": "\u002e",
"\u05BC": "\u002e",
"\u2022": "\u002e",
"\u2027": "\u002e",
"\u2219": "\u002e",
"\u22C5": "\u002e",
"\u30FB": "\u002e",
"\uFF65": "\u002e",
// .
"\uFF0A": "\u002A",
// *
"\uFF01": "\u0021",
// !
"\uFF1A": "\u003A",
// :
"\u005B": "\u0028",
"\u007B": "\u0028",
"\u3010": "\u0028",
"\u3016": "\u0028",
// (
"\u005D": "\u0029",
"\u007D": "\u0029",
"\u3011": "\u0029",
"\u3017": "\u0029"
// )
} // 前面是被替换的字符,后面是要替换成的字符,只支持单个字符替换,意思就是一个文字,多个文字会报错。
},
"Lyric": {
"Modes": [
"Merged",
"Raw",
"Translated"
], // 歌词模式,依次尝试每一个模式直到成功,Merged表示混合未翻译和翻译后歌词,Raw表示未翻译的歌词,Translated表示翻译后的歌词。
"SimplifyTranslated": true, // 部分翻译后的歌词是繁体的,这个选项可以简体化翻译后的歌词。
"Encoding": "utf-8",
"AutoUpdate": true, // 是否自动更新由NLyric创建的歌词。
"Overwriting": true // 是否覆盖非NLyric创建的歌词。
}
}
```
## 下载
GitHub: [.NET Framework版(Windows请下载这个)](https://github.com/wwh1004/NLyric/releases/latest/download/NLyric-net472.zip) [.NET Core版](https://github.com/wwh1004/NLyric/releases/latest/download/NLyric-netcoreapp3.1.zip)
AppVeyor: [](https://ci.appveyor.com/project/wwh1004/nlyric/branch/master)
## 感谢
混合歌词的思路参考了 [EHfive/Some-js-script-for-FB2](https://github.com/EHfive/Some-js-script-for-FB2K)
================================================
FILE: appveyor.yml
================================================
version: '{build}'
image: Visual Studio 2019
configuration: Release
platform: Any CPU
before_build:
- cmd: appveyor-retry nuget restore
build:
project: NLyric.sln
verbosity: normal
after_build:
- cmd: dotnet publish NLyric\NLyric.csproj -c Release -f netcoreapp3.1
artifacts:
- path: bin\Release\net472
name: NLyric-net472
- path: bin\Release\netcoreapp3.1\publish
name: NLyric-netcoreapp3.1
deploy:
- provider: GitHub
tag: $(APPVEYOR_REPO_TAG_NAME)
release: NLyric
auth_token:
secure: +8UJ1C312inNq+80I8WST34vPMrCylnmTx+9rmuIh1qnsArA5x2b8yc+kcwkXmQC
on:
APPVEYOR_REPO_TAG: true
gitextract_guu2ltr2/ ├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── NLyric/ │ ├── Arguments.cs │ ├── Audio/ │ │ ├── Album.cs │ │ ├── ITrackOrAlbum.cs │ │ └── Track.cs │ ├── CRC32.cs │ ├── ChineseConverter.cs │ ├── Database/ │ │ ├── AlbumInfo.cs │ │ ├── Extensions.cs │ │ ├── LyricInfo.cs │ │ ├── NLyricDatabase.cs │ │ └── TrackInfo.cs │ ├── FastConsole.cs │ ├── Levenshtein.cs │ ├── Lyrics/ │ │ └── Lrc.cs │ ├── NLyric.csproj │ ├── NLyricImpl.cs │ ├── Ncm/ │ │ ├── CloudMusic.cs │ │ ├── KeywordForbiddenException.cs │ │ ├── NcmAlbum.cs │ │ ├── NcmLyric.cs │ │ └── NcmTrack.cs │ ├── Program.cs │ ├── Settings/ │ │ ├── AllSettings.cs │ │ ├── CharArrayJsonConverter.cs │ │ ├── EncodingConverter.cs │ │ ├── FuzzySettings.cs │ │ ├── LyricSettings.cs │ │ ├── MatchSettings.cs │ │ └── SearchSettings.cs │ ├── Settings.json │ ├── StringHelper.cs │ ├── System/ │ │ └── Cli/ │ │ ├── ArgumentAttribute.cs │ │ └── CommandLine.cs │ └── The163KeyHelper.cs ├── NLyric.Win/ │ ├── MainForm.Designer.cs │ ├── MainForm.cs │ ├── MainForm.resx │ ├── NLyric.Win.csproj │ ├── Program.cs │ └── Properties/ │ └── AssemblyInfo.cs ├── NLyric.sln ├── README.md └── appveyor.yml
SYMBOL INDEX (175 symbols across 35 files)
FILE: NLyric.Win/MainForm.Designer.cs
class MainForm (line 3) | partial class MainForm
method Dispose (line 14) | protected override void Dispose(bool disposing)
method InitializeComponent (line 29) | private void InitializeComponent()
FILE: NLyric.Win/MainForm.cs
class MainForm (line 8) | public sealed partial class MainForm : Form {
method MainForm (line 9) | public MainForm() {
method GetTitle (line 15) | private static string GetTitle(Assembly assembly) {
method GetAssemblyAttribute (line 25) | private static T GetAssemblyAttribute<T>(Assembly assembly) {
method _btnSetDirectory_Click (line 29) | private void _btnSetDirectory_Click(object sender, EventArgs e) {
method _cbLogin_CheckedChanged (line 37) | private void _cbLogin_CheckedChanged(object sender, EventArgs e) {
method _btnRun_Click (line 50) | private void _btnRun_Click(object sender, EventArgs e) {
FILE: NLyric.Win/Program.cs
class Program (line 5) | internal static class Program {
method Main (line 6) | [STAThread]
FILE: NLyric/Arguments.cs
class Arguments (line 6) | public sealed class Arguments {
FILE: NLyric/Audio/Album.cs
class Album (line 10) | public class Album : ITrackOrAlbum {
method Album (line 24) | public Album(string name, IEnumerable<string> artists) {
method Album (line 40) | public Album(Tag tag, bool getArtistsFromTrack) {
method HasAlbumInfo (line 59) | public static bool HasAlbumInfo(Tag tag) {
method ToString (line 66) | public override string ToString() {
FILE: NLyric/Audio/ITrackOrAlbum.cs
type ITrackOrAlbum (line 4) | public interface ITrackOrAlbum {
FILE: NLyric/Audio/Track.cs
class Track (line 10) | public class Track : ITrackOrAlbum {
method Track (line 24) | public Track(string name, IEnumerable<string> artists) {
method Track (line 35) | public Track(Tag tag) {
method ToString (line 44) | public override string ToString() {
FILE: NLyric/CRC32.cs
class CRC32 (line 4) | internal static class CRC32 {
method GenerateTable (line 7) | private static uint[] GenerateTable(uint seed) {
method Compute (line 22) | public static uint Compute(byte[] data) {
FILE: NLyric/ChineseConverter.cs
class ChineseConverter (line 7) | internal static class ChineseConverter {
method GetTraditionalToSimplifiedMap (line 10) | private static Dictionary<char, char> GetTraditionalToSimplifiedMap() {
method TraditionalToSimplified (line 21) | public static string TraditionalToSimplified(string s) {
FILE: NLyric/Database/AlbumInfo.cs
class AlbumInfo (line 9) | public sealed class AlbumInfo {
method AlbumInfo (line 20) | [JsonConstructor]
method AlbumInfo (line 25) | public AlbumInfo(Album album, int id) : this(album.Name, id) {
method AlbumInfo (line 28) | public AlbumInfo(string name, int id) {
FILE: NLyric/Database/Extensions.cs
class Extensions (line 7) | public static class Extensions {
method Match (line 8) | public static AlbumInfo Match(this IEnumerable<AlbumInfo> caches, Albu...
method Match (line 15) | public static TrackInfo Match(this IEnumerable<TrackInfo> caches, Albu...
method IsMatched (line 22) | public static bool IsMatched(this AlbumInfo cache, Album album) {
method IsMatched (line 29) | public static bool IsMatched(this TrackInfo cache, Album album, Track ...
FILE: NLyric/Database/LyricInfo.cs
class LyricInfo (line 9) | public sealed class LyricInfo {
method LyricInfo (line 25) | [JsonConstructor]
method LyricInfo (line 30) | public LyricInfo(NcmLyric lyric, string checkSum) : this(lyric.RawVers...
method LyricInfo (line 35) | public LyricInfo(int rawVersion, int translatedVersion, string checkSu...
FILE: NLyric/Database/NLyricDatabase.cs
class NLyricDatabase (line 7) | public sealed class NLyricDatabase {
method CheckFormatVersion (line 27) | public bool CheckFormatVersion() {
method IsOldFormat (line 41) | public bool IsOldFormat() {
FILE: NLyric/Database/TrackInfo.cs
class TrackInfo (line 11) | public sealed class TrackInfo {
method TrackInfo (line 37) | [JsonConstructor]
method TrackInfo (line 42) | public TrackInfo(Track track, Album album, int id) : this(track.Name, ...
method TrackInfo (line 45) | public TrackInfo(string name, IEnumerable<string> artists, string albu...
FILE: NLyric/FastConsole.cs
class FastConsole (line 8) | internal static class FastConsole {
method FastConsole (line 53) | static FastConsole() {
method WriteNewLine (line 60) | public static void WriteNewLine() {
method WriteInfo (line 64) | public static void WriteInfo(string value) {
method WriteWarning (line 68) | public static void WriteWarning(string value) {
method WriteError (line 72) | public static void WriteError(string value) {
method WriteLine (line 76) | public static void WriteLine(string value, ConsoleColor color) {
method Write (line 80) | public static void Write(string value, ConsoleColor color) {
method WriteException (line 100) | public static void WriteException(Exception value) {
method Synchronize (line 107) | public static void Synchronize() {
method ExceptionToString (line 112) | private static string ExceptionToString(Exception exception) {
method DumpException (line 121) | private static void DumpException(Exception exception, StringBuilder s...
method IOLoop (line 132) | private static void IOLoop() {
method ReadKey (line 181) | public static ConsoleKeyInfo ReadKey(bool intercept) {
method ReadLine (line 186) | public static string ReadLine() {
class AutoSingleThreadLock (line 191) | private sealed class AutoSingleThreadLock : IDisposable {
method AutoSingleThreadLock (line 192) | public AutoSingleThreadLock() {
method Dispose (line 197) | void IDisposable.Dispose() {
FILE: NLyric/Levenshtein.cs
class Levenshtein (line 4) | internal static class Levenshtein {
method Compute (line 11) | public static double Compute(string x, string y) {
method Compute (line 32) | public static double Compute<T>(T[] x, T[] y, Comparison<T> comparison) {
FILE: NLyric/Lyrics/Lrc.cs
class Lrc (line 7) | public sealed class Lrc {
method Parse (line 84) | public static Lrc Parse(string text) {
method UnsafeParse (line 98) | public static Lrc UnsafeParse(string text) {
method TryParseLine (line 110) | private static bool TryParseLine(string line, Lrc lrc) {
method ToString (line 154) | public override string ToString() {
FILE: NLyric/NLyricImpl.cs
class NLyricImpl (line 19) | public static class NLyricImpl {
method ExecuteAsync (line 33) | public static async Task ExecuteAsync(Arguments arguments) {
method LoginIfNeedAsync (line 54) | private static async Task LoginIfNeedAsync(Arguments arguments) {
method CanSkip (line 76) | private static bool CanSkip(string audioPath, string lrcPath) {
method IsAudioFile (line 87) | private static bool IsAudioFile(string extension) {
method LoadAllAudioInfos (line 91) | private static AudioInfo[] LoadAllAudioInfos(string directory) {
method LoadAllAudioInfoCandidates (line 136) | private static async Task LoadAllAudioInfoCandidates(AudioInfo[] audio...
method DownloadLyricsAsync (line 160) | private static async Task DownloadLyricsAsync(AudioInfo[] audioInfos) {
method SearchTrackAsync (line 172) | private static async Task<TrackInfo> SearchTrackAsync(Album album, Tra...
method SearchAlbumAsync (line 215) | private static async Task<AlbumInfo> SearchAlbumAsync(Album album) {
method GetAlbumTracksAsync (line 246) | private static async Task<NcmTrack[]> GetAlbumTracksAsync(AlbumInfo al...
method MapToAsync (line 267) | private static async Task<NcmTrack> MapToAsync(Track track) {
method MapToAsync (line 286) | private static async Task<NcmAlbum> MapToAsync(Album album) {
method MapToAsync (line 306) | private static async Task<NcmTrack> MapToAsync(Track track, bool withA...
method MapToAsync (line 334) | private static async Task<NcmAlbum> MapToAsync(Album album, bool withA...
method LoadDatabase (line 353) | private static void LoadDatabase(string databasePath) {
method SaveDatabase (line 381) | private static void SaveDatabase(string databasePath) {
method SortDatabase (line 387) | private static void SortDatabase() {
method SaveDatabaseCore (line 392) | private static void SaveDatabaseCore(string databasePath) {
method FormatJson (line 398) | private static string FormatJson(string json) {
method MatchByUser (line 409) | private static TSource MatchByUser<TSource, TTarget>(TSource[] sources...
method MatchByUser (line 418) | private static TSource MatchByUser<TSource, TTarget>(TSource[] sources...
method MatchExactly (line 433) | private static TSource MatchExactly<TSource, TTarget>(TSource[] source...
method Select (line 465) | private static TSource Select<TSource, TTarget>(TSource[] sources, TTa...
method ComputeSimilarity (line 508) | private static double ComputeSimilarity(string x, string y, bool fuzzy) {
method GetIdByUser (line 520) | private static bool GetIdByUser(string s, out int id) {
method TryDownloadLyricAsync (line 536) | private static async Task<bool> TryDownloadLyricAsync(AudioInfo audioI...
method GetLyricAsync (line 599) | private static async Task<NcmLyric> GetLyricAsync(int trackId) {
method ToLrc (line 608) | private static Lrc ToLrc(NcmLyric lyric) {
method NormalizeLyric (line 652) | private static void NormalizeLyric(Lrc lrc, bool simplify) {
method MergeLyric (line 663) | private static Lrc MergeLyric(Lrc rawLrc, Lrc translatedLrc) {
method ComputeLyricCheckSum (line 692) | private static string ComputeLyricCheckSum(byte[] lyric) {
method AccelerateAllTracksAsync (line 698) | private static Task AccelerateAllTracksAsync(AudioInfo[] audioInfos) {
method AccelerateAllLyricsAsync (line 703) | private static async Task AccelerateAllLyricsAsync(AudioInfo[] audioIn...
class AudioInfo (line 740) | private sealed class AudioInfo {
FILE: NLyric/Ncm/CloudMusic.cs
class CloudMusic (line 12) | public sealed class CloudMusic {
method LoginAsync (line 17) | public async Task<bool> LoginAsync(string account, string password) {
method SearchTrackAsync (line 26) | public async Task<NcmTrack[]> SearchTrackAsync(Track track, int limit,...
method ParseSearchTracks (line 48) | public NcmTrack[] ParseSearchTracks(JObject json) {
method SearchAlbumAsync (line 55) | public async Task<NcmAlbum[]> SearchAlbumAsync(Album album, int limit,...
method ParseSearchAlbums (line 77) | public NcmAlbum[] ParseSearchAlbums(JObject json) {
method GetTracksAsync (line 85) | public async Task<NcmTrack[]> GetTracksAsync(int albumId) {
method ParseTracks (line 94) | public NcmTrack[] ParseTracks(JObject json) {
method GetLyricAsync (line 98) | public async Task<NcmLyric> GetLyricAsync(int trackId) {
method ParseLyric (line 107) | public NcmLyric ParseLyric(int trackId, JObject json) {
method ParseAlbum (line 122) | private NcmAlbum ParseAlbum(JToken json) {
method ParseTrack (line 128) | private NcmTrack ParseTrack(JToken json, bool isShortName) {
method ParseNames (line 134) | private string[] ParseNames(JToken json) {
method ParseLyric (line 138) | private (Lrc, int) ParseLyric(JToken json) {
FILE: NLyric/Ncm/KeywordForbiddenException.cs
class KeywordForbiddenException (line 8) | [Serializable]
method KeywordForbiddenException (line 10) | public KeywordForbiddenException() {
method KeywordForbiddenException (line 13) | public KeywordForbiddenException(string text) : base($"\"{text}\" 中有关键...
method KeywordForbiddenException (line 16) | private KeywordForbiddenException(SerializationInfo info, StreamingCon...
FILE: NLyric/Ncm/NcmAlbum.cs
class NcmAlbum (line 4) | public sealed class NcmAlbum : Album {
method NcmAlbum (line 9) | public NcmAlbum(Album album, int id) : base(album.Name, album.Artists) {
method ToString (line 13) | public override string ToString() {
FILE: NLyric/Ncm/NcmLyric.cs
class NcmLyric (line 4) | public sealed class NcmLyric {
method NcmLyric (line 27) | public NcmLyric(int id, bool isCollected, bool isAbsoluteMusic, Lrc ra...
FILE: NLyric/Ncm/NcmTrack.cs
class NcmTrack (line 4) | public sealed class NcmTrack : Track {
method NcmTrack (line 9) | public NcmTrack(Track track, int id) : base(track.Name, track.Artists) {
method ToString (line 13) | public override string ToString() {
FILE: NLyric/Program.cs
class Program (line 11) | public static class Program {
method Main (line 12) | private static async Task Main(string[] args) {
method GetTitle (line 41) | private static string GetTitle() {
method GetAssemblyAttribute (line 51) | private static T GetAssemblyAttribute<T>() {
FILE: NLyric/Settings/AllSettings.cs
class AllSettings (line 4) | internal sealed class AllSettings {
FILE: NLyric/Settings/CharArrayJsonConverter.cs
class CharArrayJsonConverter (line 5) | internal sealed class CharArrayJsonConverter : JsonConverter<char[]> {
method ReadJson (line 6) | public override char[] ReadJson(JsonReader reader, Type objectType, ch...
method WriteJson (line 10) | public override void WriteJson(JsonWriter writer, char[] value, JsonSe...
FILE: NLyric/Settings/EncodingConverter.cs
class EncodingConverter (line 6) | internal sealed class EncodingConverter : JsonConverter<Encoding> {
method ReadJson (line 7) | public override Encoding ReadJson(JsonReader reader, Type objectType, ...
method WriteJson (line 14) | public override void WriteJson(JsonWriter writer, Encoding value, Json...
FILE: NLyric/Settings/FuzzySettings.cs
class FuzzySettings (line 4) | internal sealed class FuzzySettings {
FILE: NLyric/Settings/LyricSettings.cs
class LyricSettings (line 5) | internal sealed class LyricSettings {
FILE: NLyric/Settings/MatchSettings.cs
class MatchSettings (line 5) | internal sealed class MatchSettings {
FILE: NLyric/Settings/SearchSettings.cs
class SearchSettings (line 5) | internal sealed class SearchSettings {
FILE: NLyric/StringHelper.cs
class StringHelper (line 7) | internal static class StringHelper {
method GetSafeString (line 17) | public static string GetSafeString(this string value) {
method ReplaceEx (line 26) | public static string ReplaceEx(this string value) {
method WholeWordReplace (line 38) | public static string WholeWordReplace(this string value) {
method CharReplace (line 56) | public static string CharReplace(this string value) {
method Fuzzy (line 77) | public static string Fuzzy(this string value) {
method SplitEx (line 95) | public static string[] SplitEx(this string value) {
method ToHalfWidth (line 107) | public static string ToHalfWidth(this string value) {
FILE: NLyric/System/Cli/ArgumentAttribute.cs
class ArgumentAttribute (line 5) | [AttributeUsage(AttributeTargets.Property)]
method ArgumentAttribute (line 54) | public ArgumentAttribute(string name) {
FILE: NLyric/System/Cli/CommandLine.cs
class CommandLine (line 7) | internal static class CommandLine {
method Parse (line 8) | public static T Parse<T>(string[] args) where T : new() {
method TryParse (line 17) | public static bool TryParse<T>(string[] args, out T result) where T : ...
method ShowUsage (line 86) | public static bool ShowUsage<T>() {
method TryGetArgumentInfos (line 114) | private static bool TryGetArgumentInfos(Type type, out Dictionary<stri...
method VerifyProperty (line 133) | private static bool VerifyProperty(PropertyInfo propertyInfo, out Argu...
method GetArgumentFormat (line 177) | private static string GetArgumentFormat(ArgumentInfo argumentInfo) {
class ArgumentInfo (line 181) | private sealed class ArgumentInfo {
method ArgumentInfo (line 210) | public ArgumentInfo(ArgumentAttribute attribute, PropertyInfo proper...
method TrySetValue (line 215) | public bool TrySetValue(object instance, object value) {
FILE: NLyric/The163KeyHelper.cs
class The163KeyHelper (line 11) | internal static class The163KeyHelper {
method Create163Aes (line 14) | private static Aes Create163Aes() {
method TryGetTrackId (line 29) | public static bool TryGetTrackId(Tag tag, out int trackId) {
method Is163KeyCandidate (line 52) | private static bool Is163KeyCandidate(string s) {
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (137K chars).
[
{
"path": ".editorconfig",
"chars": 8306,
"preview": "# Remove the line below if you want to inherit .editorconfig settings from higher directories\nroot = true\n\n[*]\ncharset ="
},
{
"path": ".gitattributes",
"chars": 2518,
"preview": "###############################################################################\n# Set default behavior to automatically "
},
{
"path": ".gitignore",
"chars": 5776,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
},
{
"path": "LICENSE",
"chars": 1059,
"preview": "MIT License\n\nCopyright (c) 2019 文煌\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this"
},
{
"path": "NLyric/Arguments.cs",
"chars": 1932,
"preview": "using System;\nusing System.Cli;\nusing System.IO;\n\nnamespace NLyric {\n\tpublic sealed class Arguments {\n\t\tprivate string _"
},
{
"path": "NLyric/Audio/Album.cs",
"chars": 1912,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing TagLib;\n\nnamespace NLyric.Audio {\n\t/// <summary"
},
{
"path": "NLyric/Audio/ITrackOrAlbum.cs",
"chars": 164,
"preview": "using System.Collections.Generic;\n\nnamespace NLyric.Audio {\n\tpublic interface ITrackOrAlbum {\n\t\tstring Name { get; }\n\n\t\t"
},
{
"path": "NLyric/Audio/Track.cs",
"chars": 1155,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing TagLib;\n\nnamespace NLyric.Audio {\n\t/// <summary"
},
{
"path": "NLyric/CRC32.cs",
"chars": 722,
"preview": "using System;\n\nnamespace NLyric {\n\tinternal static class CRC32 {\n\t\tprivate static readonly uint[] _table = GenerateTable"
},
{
"path": "NLyric/ChineseConverter.cs",
"chars": 1041,
"preview": "using System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Text;\n\nnamespace NLyric {\n\tinte"
},
{
"path": "NLyric/Database/AlbumInfo.cs",
"chars": 629,
"preview": "using System;\nusing Newtonsoft.Json;\nusing NLyric.Audio;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// 专辑信息\n\t/// </su"
},
{
"path": "NLyric/Database/Extensions.cs",
"chars": 1190,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing NLyric.Audio;\n\nnamespace NLyric.Database {\n\tpub"
},
{
"path": "NLyric/Database/LyricInfo.cs",
"chars": 988,
"preview": "using System;\nusing Newtonsoft.Json;\nusing NLyric.Ncm;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// 歌词信息\n\t/// </summ"
},
{
"path": "NLyric/Database/NLyricDatabase.cs",
"chars": 809,
"preview": "using System.Collections.Generic;\n\nnamespace NLyric.Database {\n\t/// <summary>\n\t/// NLyric数据库\n\t/// </summary>\n\tpublic sea"
},
{
"path": "NLyric/Database/TrackInfo.cs",
"chars": 1193,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Newtonsoft.Json;\nusing NLyric.Audio;\n\nnamespace"
},
{
"path": "NLyric/FastConsole.cs",
"chars": 5608,
"preview": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading;\n\nna"
},
{
"path": "NLyric/Levenshtein.cs",
"chars": 1523,
"preview": "using System;\n\nnamespace NLyric {\n\tinternal static class Levenshtein {\n\t\t/// <summary>\n\t\t/// 计算相似度\n\t\t/// </summary>\n\t\t//"
},
{
"path": "NLyric/Lyrics/Lrc.cs",
"chars": 4544,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\n\nnamespace NLyric.Lyrics {\n\tpublic s"
},
{
"path": "NLyric/NLyric.csproj",
"chars": 827,
"preview": "<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<Copyr"
},
{
"path": "NLyric/NLyricImpl.cs",
"chars": 24157,
"preview": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing Syst"
},
{
"path": "NLyric/Ncm/CloudMusic.cs",
"chars": 5253,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.Th"
},
{
"path": "NLyric/Ncm/KeywordForbiddenException.cs",
"chars": 443,
"preview": "using System;\nusing System.Runtime.Serialization;\n\nnamespace NLyric.Ncm {\n\t/// <summary>\n\t/// 关键词被禁止\n\t/// </summary>\n\t[S"
},
{
"path": "NLyric/Ncm/NcmAlbum.cs",
"chars": 321,
"preview": "using NLyric.Audio;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmAlbum : Album {\n\t\tprivate readonly int _id;\n\n\t\tpubli"
},
{
"path": "NLyric/Ncm/NcmLyric.cs",
"chars": 955,
"preview": "using NLyric.Lyrics;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmLyric {\n\t\tprivate readonly int _id;\n\t\tprivate reado"
},
{
"path": "NLyric/Ncm/NcmTrack.cs",
"chars": 321,
"preview": "using NLyric.Audio;\n\nnamespace NLyric.Ncm {\n\tpublic sealed class NcmTrack : Track {\n\t\tprivate readonly int _id;\n\n\t\tpubli"
},
{
"path": "NLyric/Program.cs",
"chars": 1644,
"preview": "using System;\nusing System.Cli;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Threadi"
},
{
"path": "NLyric/Settings/AllSettings.cs",
"chars": 637,
"preview": "using System;\n\nnamespace NLyric.Settings {\n\tinternal sealed class AllSettings {\n\t\tprivate static AllSettings _default;\n\n"
},
{
"path": "NLyric/Settings/CharArrayJsonConverter.cs",
"chars": 480,
"preview": "using System;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class CharArrayJsonConverter : JsonCo"
},
{
"path": "NLyric/Settings/EncodingConverter.cs",
"chars": 611,
"preview": "using System;\nusing System.Text;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class EncodingConv"
},
{
"path": "NLyric/Settings/FuzzySettings.cs",
"chars": 373,
"preview": "using Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class FuzzySettings {\n\t\tpublic bool TryIgnoringArti"
},
{
"path": "NLyric/Settings/LyricSettings.cs",
"chars": 369,
"preview": "using System.Text;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class LyricSettings {\n\t\tpublic s"
},
{
"path": "NLyric/Settings/MatchSettings.cs",
"chars": 426,
"preview": "using System;\nusing System.Collections.Generic;\n\nnamespace NLyric.Settings {\n\tinternal sealed class MatchSettings {\n\t\tpr"
},
{
"path": "NLyric/Settings/SearchSettings.cs",
"chars": 373,
"preview": "using System.Collections.Generic;\nusing Newtonsoft.Json;\n\nnamespace NLyric.Settings {\n\tinternal sealed class SearchSetti"
},
{
"path": "NLyric/Settings.json",
"chars": 1731,
"preview": "{ // 所有匹配都是忽略大小写的!!!\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"
},
{
"path": "NLyric/StringHelper.cs",
"chars": 3738,
"preview": "using System;\nusing System.Linq;\nusing System.Text;\nusing NLyric.Settings;\n\nnamespace NLyric {\n\tinternal static class St"
},
{
"path": "NLyric/System/Cli/ArgumentAttribute.cs",
"chars": 1451,
"preview": "namespace System.Cli {\n\t/// <summary>\n\t/// 表示一个命令行参数。被应用 <see cref=\"ArgumentAttribute\"/> 的属性必须为 <see cref=\"string\"/> 类型或"
},
{
"path": "NLyric/System/Cli/CommandLine.cs",
"chars": 6374,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text;\n\nnamespace System.Cli {"
},
{
"path": "NLyric/The163KeyHelper.cs",
"chars": 1541,
"preview": "using System;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Newtonsoft.Json.Linq;\nusing TagLib;\n\nnamespac"
},
{
"path": "NLyric.Win/MainForm.Designer.cs",
"chars": 4956,
"preview": "namespace NLyric.Win\n{\n partial class MainForm\n {\n /// <summary>\n /// 必需的设计器变量。\n /// </summar"
},
{
"path": "NLyric.Win/MainForm.cs",
"chars": 1995,
"preview": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Windows.Forms;\n\nnamespace"
},
{
"path": "NLyric.Win/MainForm.resx",
"chars": 6920,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n <!-- \n Microsoft ResX Schema \n \n Version 2.0\n \n The prim"
},
{
"path": "NLyric.Win/NLyric.Win.csproj",
"chars": 2325,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"15.0\" xmlns=\"http://schemas.microsoft.com/developer/msbui"
},
{
"path": "NLyric.Win/Program.cs",
"chars": 283,
"preview": "using System;\nusing System.Windows.Forms;\n\nnamespace NLyric.Win {\n\tinternal static class Program {\n\t\t[STAThread]\n\t\tpriva"
},
{
"path": "NLyric.Win/Properties/AssemblyInfo.cs",
"chars": 244,
"preview": "using System.Reflection;\n\n[assembly: AssemblyTitle(\"NLyric.Win\")]\n[assembly: AssemblyProduct(\"NLyric.Win\")]\n[assembly: A"
},
{
"path": "NLyric.sln",
"chars": 1575,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.2882"
},
{
"path": "README.md",
"chars": 3434,
"preview": "# NLyric\n\n网易云音乐歌词自动搜索下载\n\n可选择登录或免登陆下载,避免出现网易云音乐接口异常。\n\n网易云音乐已屏蔽部分关键字,导致搜索出现异常,属于正常现象(网易云音乐客户端内很多歌曲名已经打上\\*号)。\n\n**New: Windo"
},
{
"path": "appveyor.yml",
"chars": 602,
"preview": "version: '{build}'\nimage: Visual Studio 2019\nconfiguration: Release\nplatform: Any CPU\nbefore_build:\n- cmd: appveyor-retr"
}
]
About this extraction
This page contains the full source code of the wwh1004/NLyric GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (112.7 KB), approximately 33.8k tokens, and a symbol index with 175 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.