diff --git a/MusicLyricApp/AUTHORS b/MusicLyricApp/AUTHORS new file mode 100644 index 0000000..995f5c2 --- /dev/null +++ b/MusicLyricApp/AUTHORS @@ -0,0 +1,6 @@ +Taku Kudo +chasen@is.aist-nara.ac.jp + +Masayuki Asahara:masayu-a@is.aist-nara.ac.jp +Yuji Matsumoto:matsu@is.aist-nara.ac.jp + diff --git a/MusicLyricApp/Api/NetEaseMusicApiV2.cs b/MusicLyricApp/Api/NetEaseMusicApiV2.cs index c44da9c..5aee7b8 100644 --- a/MusicLyricApp/Api/NetEaseMusicApiV2.cs +++ b/MusicLyricApp/Api/NetEaseMusicApiV2.cs @@ -75,11 +75,11 @@ protected override LyricVo GetLyricVo0(string songId) var lyricVo = new LyricVo(); if (resp.Lrc != null) { - lyricVo.Lyric = resp.Lrc.Lyric; + lyricVo.SetLyric(resp.Lrc.Lyric); } if (resp.Tlyric != null) { - lyricVo.TranslateLyric = resp.Tlyric.Lyric; + lyricVo.SetTranslateLyric(resp.Tlyric.Lyric); } return lyricVo; diff --git a/MusicLyricApp/Api/QQMusicApiV2.cs b/MusicLyricApp/Api/QQMusicApiV2.cs index 09d1feb..ecad0eb 100644 --- a/MusicLyricApp/Api/QQMusicApiV2.cs +++ b/MusicLyricApp/Api/QQMusicApiV2.cs @@ -65,16 +65,17 @@ protected override LyricVo GetLyricVo0(string songId) { var resp = _api.GetLyric(songId); - if (resp.Code == 0) + if (resp.Code != 0) { - return new LyricVo - { - Lyric = resp.lyric ?? string.Empty, - TranslateLyric = resp.trans ?? string.Empty - }; + throw new MusicLyricException(ErrorMsg.LRC_NOT_EXIST); } - - throw new MusicLyricException(ErrorMsg.LRC_NOT_EXIST); + + var lyricVo = new LyricVo(); + + lyricVo.SetLyric(resp.lyric); + lyricVo.SetTranslateLyric(resp.trans); + + return lyricVo; } /// diff --git a/MusicLyricApp/Bean/Constants.cs b/MusicLyricApp/Bean/Constants.cs index abeddb2..a9e2cb2 100644 --- a/MusicLyricApp/Bean/Constants.cs +++ b/MusicLyricApp/Bean/Constants.cs @@ -4,10 +4,19 @@ namespace MusicLyricApp.Bean { public static class Constants { - public const string Version = "v4.1"; + public const string Version = "v4.2"; public static readonly string SettingPath = Environment.CurrentDirectory + "\\MusicLyricAppSetting.json"; + + public static readonly string IpaDicPath = Environment.CurrentDirectory + "\\IpaDic"; public const int SettingFormOffset = 20; + + public static readonly string[] IpaDicDependency = { + IpaDicPath + "\\char.bin", + IpaDicPath + "\\matrix.bin", + IpaDicPath + "\\sys.dic", + IpaDicPath + "\\unk.dic", + }; } } \ No newline at end of file diff --git a/MusicLyricApp/Bean/MusicLyricsVO.cs b/MusicLyricApp/Bean/MusicLyricsVO.cs index 4b96ad1..a0f8516 100644 --- a/MusicLyricApp/Bean/MusicLyricsVO.cs +++ b/MusicLyricApp/Bean/MusicLyricsVO.cs @@ -2,58 +2,63 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Web; +using MusicLyricApp.Exception; +using MusicLyricApp.Utils; namespace MusicLyricApp.Bean { // 双语歌词类型 public enum ShowLrcTypeEnum { - ONLY_ORIGIN = 0, // 仅显示原文 - ONLY_TRANSLATE = 1, // 仅显示译文 - ORIGIN_PRIOR = 2, // 优先原文 - TRANSLATE_PRIOR = 3, // 优先译文 - MERGE_ORIGIN = 4, // 合并显示,优先原文 - MERGE_TRANSLATE = 5, // 合并显示,优先译文 + [Description("仅显示原文")] ONLY_ORIGIN = 0, + [Description("仅显示译文")] ONLY_TRANSLATE = 1, + [Description("优先原文(交错)")] ORIGIN_PRIOR_STAGGER = 2, + [Description("优先译文(交错)")] TRANSLATE_PRIOR_STAGGER = 3, + [Description("优先原文(独立)")] ORIGIN_PRIOR_ISOLATED = 4, + [Description("优先译文(独立)")] TRANSLATE_PRIOR_ISOLATED = 5, + [Description("优先原文(合并)")] ORIGIN_PRIOR_MERGE = 6, + [Description("优先译文(合并)")] TRANSLATE_PRIOR_MERGE = 7, } // 输出文件名类型 public enum OutputFilenameTypeEnum { - NAME_SINGER = 0, // 歌曲名 - 歌手 - SINGER_NAME = 1, // 歌手 - 歌曲名 - NAME = 2 // 歌曲名 + [Description("歌曲名 - 歌手")] NAME_SINGER = 0, + [Description("歌手 - 歌曲名")] SINGER_NAME = 1, + [Description("歌曲名")] NAME = 2 } // 搜索来源 public enum SearchSourceEnum { - NET_EASE_MUSIC = 0, // 网易云音乐 - QQ_MUSIC = 1 // QQ音乐 + [Description("网易云")] NET_EASE_MUSIC = 0, + [Description("QQ音乐")] QQ_MUSIC = 1 } // 搜索类型 public enum SearchTypeEnum { - SONG_ID = 0, // 歌曲ID - ALBUM_ID = 1 // 专辑ID + [Description("单曲")] SONG_ID = 0, + [Description("专辑")] ALBUM_ID = 1 } // 强制两位类型 public enum DotTypeEnum { - DISABLE = 0, // 不启用 - DOWN = 1, // 截位 - HALF_UP = 2 // 四舍五入 + [Description("不启用")] DISABLE = 0, + [Description("截位")] DOWN = 1, + [Description("四舍五入")] HALF_UP = 2 } // 输出文件格式 public enum OutputEncodingEnum { - UTF_8 = 0, - UTF_8_BOM = 1, - GB_2312 = 2, - GBK = 3, - UNICODE = 4 + [Description("UTF-8")] UTF_8 = 0, + [Description("UTF-8-BOM")] UTF_8_BOM = 1, + [Description("GB-2312")] GB_2312 = 2, + [Description("GBK")] GBK = 3, + [Description("UNICODE")] UNICODE = 4 } public enum OutputFormatEnum @@ -63,6 +68,23 @@ public enum OutputFormatEnum [Description("srt文件(*.srt)|*.srt")] SRT = 1 } + // 罗马音转换模式 + public enum RomajiModeEnum + { + [Description("标准模式")] NORMAL = 0, + [Description("空格分组")] SPACED = 1, + [Description("送假名")] OKURIGANA = 2, + [Description("注音假名")] FURIGANA = 3, + } + + // 罗马音字体系 + public enum RomajiSystemEnum + { + [Description("日本式")] NIPPON = 0, + [Description("护照式")] PASSPORT = 1, + [Description("平文式")] HEPBURN = 2, + } + /** * 错误码 */ @@ -79,6 +101,7 @@ public static class ErrorMsg public const string FUNCTION_NOT_SUPPORT = "该功能暂不可用,请等待后续更新"; public const string SONG_URL_COPY_SUCCESS = "歌曲直链,已复制到剪切板"; public const string SONG_URL_GET_FAILED = "歌曲直链,获取失败"; + public const string ROMAJI_DEPENDENCY_LOSS = "罗马音相关依赖缺失,请重新下载"; public const string SAVE_COMPLETE = "保存完毕,成功 {0} 跳过 {1}"; public const string GET_LATEST_VERSION_FAILED = "获取最新版本失败"; @@ -146,54 +169,111 @@ public class LyricVo /// /// 歌词内容 /// - public string Lyric { get; set; } + public string Lyric; /// /// 译文歌词内容 /// - public string TranslateLyric { get; set; } + public string TranslateLyric; /// /// 歌曲时长 ms /// public long Duration { get; set; } - /// - /// 实际输出的歌词 - /// - public string Output { get; set; } + public void SetLyric(string content) + { + Lyric = HttpUtility.HtmlDecode(content); + } + + public void SetTranslateLyric(string content) + { + TranslateLyric = HttpUtility.HtmlDecode(content); + } public bool IsEmpty() { return string.IsNullOrEmpty(Lyric) && string.IsNullOrEmpty(TranslateLyric); } } - + /// - /// 搜索信息 + /// 当行歌词信息 /// - public class SearchInfo + public class LyricLineVo : IComparable { /// - /// 搜索来源 + /// 时间戳字符串 /// - public SearchSourceEnum SearchSource { get; set; } + public string Timestamp { get; set; } + + /** + * 时间偏移量 + */ + public long TimeOffset { get; set; } /// - /// 搜索类型 + /// 歌词正文 /// - public SearchTypeEnum SearchType { get; set; } + public string Content { get; set; } - /// - /// 输出文件名类型 - /// - public OutputFilenameTypeEnum OutputFileNameType { get; set; } + public LyricLineVo(string lyricLine) + { + var index = lyricLine.IndexOf("]"); + if (index == -1) + { + Timestamp = ""; + TimeOffset = -1; + Content = lyricLine; + } + else + { + Timestamp = lyricLine.Substring(0, index + 1); + Content = lyricLine.Substring(index + 1); + TimeOffset = GlobalUtils.TimestampStrToLong(Timestamp); + } + } - /// - /// 歌词展示格式 - /// - public ShowLrcTypeEnum ShowLrcType { get; set; } + public LyricLineVo() + { + } + + public int CompareTo(object input) + { + if (!(input is LyricLineVo obj)) + { + throw new MusicLyricException(ErrorMsg.SYSTEM_ERROR); + } + + if (TimeOffset == -1 && obj.TimeOffset == -1) + { + return 0; + } + + if (TimeOffset == -1) + { + return -1; + } + + if (obj.TimeOffset == -1) + { + return 1; + } + + return (int) (TimeOffset - obj.TimeOffset); + } + + public override string ToString() + { + return Timestamp + Content; + } + } + /// + /// 搜索信息 + /// + public class SearchInfo + { /// /// 输入 ID 列表 /// @@ -204,25 +284,9 @@ public class SearchInfo /// public readonly HashSet SongIds = new HashSet(); - /// - /// 输出文件编码 - /// - public OutputEncodingEnum Encoding { get; set; } - - /// - /// 指定歌词合并的分隔符 - /// - public string LrcMergeSeparator { get; set; } - - /// - /// 小数位处理策略 - /// - public DotTypeEnum DotType { get; set; } + public SettingBean SettingBeanBackup { get; set; } - /// - /// 输出文件格式 - /// - public OutputFormatEnum OutputFileFormat { get; set; } + public SettingBean SettingBean { get; set; } } public static class EnumHelper diff --git a/MusicLyricApp/Bean/SettingBase.cs b/MusicLyricApp/Bean/SettingBase.cs index 9532599..2cddb33 100644 --- a/MusicLyricApp/Bean/SettingBase.cs +++ b/MusicLyricApp/Bean/SettingBase.cs @@ -6,16 +6,48 @@ public class SettingBean { public readonly ConfigBean Config = new ConfigBean(); - public readonly PersistParamBean Param = new PersistParamBean(); + public PersistParamBean Param = new PersistParamBean(); } public class ConfigBean { + /// + /// 参数记忆 + /// public bool RememberParam = false; + /// + /// 自读读取剪切板 + /// public bool AutoReadClipboard = false; + /// + /// 自动检查更新 + /// public bool AutoCheckUpdate = true; + + /// + /// 罗马音相关配置 + /// + public RomajiConfigBean RomajiConfig = new RomajiConfigBean(); + } + + public class RomajiConfigBean + { + /// + /// 译文显示罗马音 + /// + public bool Enable = false; + + /// + /// 罗马音转换模式 + /// + public RomajiModeEnum ModeEnum = RomajiModeEnum.SPACED; + + /// + /// 罗马音字体系 + /// + public RomajiSystemEnum SystemEnum = RomajiSystemEnum.HEPBURN; } public class PersistParamBean @@ -59,19 +91,5 @@ public class PersistParamBean /// 输出文件编码 /// public OutputEncodingEnum Encoding = OutputEncodingEnum.UTF_8; - - public void Update(SearchInfo searchInfo) - { - SearchSource = searchInfo.SearchSource; - SearchType = searchInfo.SearchType; - - ShowLrcType = searchInfo.ShowLrcType; - LrcMergeSeparator = searchInfo.LrcMergeSeparator; - DotType = searchInfo.DotType; - - OutputFileNameType = searchInfo.OutputFileNameType; - OutputFileFormat = searchInfo.OutputFileFormat; - Encoding = searchInfo.Encoding; - } } } \ No newline at end of file diff --git a/MusicLyricApp/COPYING b/MusicLyricApp/COPYING new file mode 100644 index 0000000..dc7db62 --- /dev/null +++ b/MusicLyricApp/COPYING @@ -0,0 +1,71 @@ +Copyright 2000, 2001, 2002, 2003 Nara Institute of Science +and Technology. All Rights Reserved. + +Use, reproduction, and distribution of this software is permitted. +Any copy of this software, whether in its original form or modified, +must include both the above copyright notice and the following +paragraphs. + +Nara Institute of Science and Technology (NAIST), +the copyright holders, disclaims all warranties with regard to this +software, including all implied warranties of merchantability and +fitness, in no event shall NAIST be liable for +any special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether in an +action of contract, negligence or other tortuous action, arising out +of or in connection with the use or performance of this software. + +A large portion of the dictionary entries +originate from ICOT Free Software. The following conditions for ICOT +Free Software applies to the current dictionary as well. + +Each User may also freely distribute the Program, whether in its +original form or modified, to any third party or parties, PROVIDED +that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear +on, or be attached to, the Program, which is distributed substantially +in the same form as set out herein and that such intended +distribution, if actually made, will neither violate or otherwise +contravene any of the laws and regulations of the countries having +jurisdiction over the User or the intended distribution itself. + +NO WARRANTY + +The program was produced on an experimental basis in the course of the +research and development conducted during the project and is provided +to users as so produced on an experimental basis. Accordingly, the +program is provided without any warranty whatsoever, whether express, +implied, statutory or otherwise. The term "warranty" used herein +includes, but is not limited to, any warranty of the quality, +performance, merchantability and fitness for a particular purpose of +the program and the nonexistence of any infringement or violation of +any right of any third party. + +Each user of the program will agree and understand, and be deemed to +have agreed and understood, that there is no warranty whatsoever for +the program and, accordingly, the entire risk arising from or +otherwise connected with the program is assumed by the user. + +Therefore, neither ICOT, the copyright holder, or any other +organization that participated in or was otherwise related to the +development of the program and their respective officials, directors, +officers and other employees shall be held liable for any and all +damages, including, without limitation, general, special, incidental +and consequential damages, arising out of or otherwise in connection +with the use or inability to use the program or any product, material +or result produced or otherwise obtained by using the program, +regardless of whether they have been advised of, or otherwise had +knowledge of, the possibility of such damages at any time during the +project or thereafter. Each user will be deemed to have agreed to the +foregoing by his or her commencement of use of the program. The term +"use" as used herein includes, but is not limited to, the use, +modification, copying and distribution of the program and the +production of secondary products from the program. + +In the case where the program, whether in its original form or +modified, was distributed or delivered to or received by a user from +any person, organization or entity other than ICOT, unless it makes or +grants independently of ICOT any specific warranty to the user in +writing, such person, organization or entity, will also be exempted +from and not be held liable to the user for any such damages as noted +above as far as the program is concerned. + diff --git a/MusicLyricApp/MainForm.Designer.cs b/MusicLyricApp/MainForm.Designer.cs index 20bf539..1d49811 100644 --- a/MusicLyricApp/MainForm.Designer.cs +++ b/MusicLyricApp/MainForm.Designer.cs @@ -126,7 +126,7 @@ private void InitializeComponent() // this.OutputName_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.OutputName_ComboBox.FormattingEnabled = true; - this.OutputName_ComboBox.Items.AddRange(new object[] { "歌曲名 - 歌手", "歌手 - 歌曲名", "歌曲名" }); + this.OutputName_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.OutputName_ComboBox.Location = new System.Drawing.Point(101, 374); this.OutputName_ComboBox.Name = "OutputName_ComboBox"; this.OutputName_ComboBox.Size = new System.Drawing.Size(101, 20); @@ -137,7 +137,7 @@ private void InitializeComponent() // this.LrcType_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.LrcType_ComboBox.FormattingEnabled = true; - this.LrcType_ComboBox.Items.AddRange(new object[] { "不显示译文", "仅显示译文", "优先原文", "优先译文", "合并显示,优先原文", "合并显示,优先译文" }); + this.LrcType_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.LrcType_ComboBox.Location = new System.Drawing.Point(89, 42); this.LrcType_ComboBox.Name = "LrcType_ComboBox"; this.LrcType_ComboBox.Size = new System.Drawing.Size(120, 20); @@ -198,10 +198,9 @@ private void InitializeComponent() // // OutputEncoding_ComboBox // - this.OutputEncoding_ComboBox.AutoCompleteCustomSource.AddRange(new string[] { "UTF-8", "GB2312" }); this.OutputEncoding_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.OutputEncoding_ComboBox.FormattingEnabled = true; - this.OutputEncoding_ComboBox.Items.AddRange(new object[] { "UTF-8", "UTF-8-BOM", "GB2312", "GBK", "UNICODE" }); + this.OutputEncoding_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.OutputEncoding_ComboBox.Location = new System.Drawing.Point(100, 440); this.OutputEncoding_ComboBox.Name = "OutputEncoding_ComboBox"; this.OutputEncoding_ComboBox.Size = new System.Drawing.Size(102, 20); @@ -239,7 +238,7 @@ private void InitializeComponent() // this.SearchType_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.SearchType_ComboBox.FormattingEnabled = true; - this.SearchType_ComboBox.Items.AddRange(new object[] { "单曲", "专辑" }); + this.SearchType_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.SearchType_ComboBox.Location = new System.Drawing.Point(11, 81); this.SearchType_ComboBox.Name = "SearchType_ComboBox"; this.SearchType_ComboBox.Size = new System.Drawing.Size(62, 20); @@ -294,7 +293,7 @@ private void InitializeComponent() // this.Dot_TextBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.Dot_TextBox.FormattingEnabled = true; - this.Dot_TextBox.Items.AddRange(new object[] { "不开启", "截位", "四舍五入" }); + this.Dot_TextBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.Dot_TextBox.Location = new System.Drawing.Point(298, 80); this.Dot_TextBox.Name = "Dot_TextBox"; this.Dot_TextBox.Size = new System.Drawing.Size(71, 20); @@ -334,7 +333,7 @@ private void InitializeComponent() // this.SearchSource_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.SearchSource_ComboBox.FormattingEnabled = true; - this.SearchSource_ComboBox.Items.AddRange(new object[] { "网易云", "QQ音乐" }); + this.SearchSource_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); this.SearchSource_ComboBox.Location = new System.Drawing.Point(11, 42); this.SearchSource_ComboBox.Name = "SearchSource_ComboBox"; this.SearchSource_ComboBox.Size = new System.Drawing.Size(62, 20); diff --git a/MusicLyricApp/MainForm.cs b/MusicLyricApp/MainForm.cs index d0e300d..2c1325b 100644 --- a/MusicLyricApp/MainForm.cs +++ b/MusicLyricApp/MainForm.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; using System.Text; using System.Threading; @@ -16,7 +15,6 @@ using MusicLyricApp.Exception; using MusicLyricApp.Utils; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NLog; namespace MusicLyricApp @@ -29,33 +27,10 @@ public partial class MainForm : Form private readonly SearchInfo _globalSearchInfo = new SearchInfo(); - // 输出文件编码 - private OutputEncodingEnum _outputEncodingEnum; - - // 搜索来源 - private SearchSourceEnum _searchSourceEnum; - - // 搜索类型 - private SearchTypeEnum _searchTypeEnum; - - // 强制两位类型 - private DotTypeEnum _dotTypeEnum; - - // 展示歌词类型 - private ShowLrcTypeEnum _showLrcTypeEnum; - - // 输出文件名类型 - private OutputFilenameTypeEnum _outputFilenameTypeEnum; - - // 输出文件类型 - private OutputFormatEnum _outputFormatEnum; - private IMusicApiV2 _api; private SettingForm _settingForm; - private SettingBean _settingBean; - private UpgradeForm _upgradeForm; public MainForm() @@ -76,18 +51,23 @@ public MainForm() private void InitialConfig() { // 1、加载配置 + SettingBean settingBean; if (File.Exists(Constants.SettingPath)) { var text = File.ReadAllText(Constants.SettingPath); - _settingBean = text.ToEntity(); + settingBean = text.ToEntity(); } else { - _settingBean = new SettingBean(); + settingBean = new SettingBean(); } + + _globalSearchInfo.SettingBean = settingBean; + _globalSearchInfo.SettingBeanBackup = settingBean.ToJson().ToEntity(); // 2、配置应用 - var paramConfig = _settingBean.Config.RememberParam ? _settingBean.Param : new PersistParamBean(); + var paramConfig = settingBean.Config.RememberParam ? settingBean.Param : new PersistParamBean(); + OutputName_ComboBox.SelectedIndex = (int) paramConfig.OutputFileNameType; OutputEncoding_ComboBox.SelectedIndex = (int) paramConfig.Encoding; LrcType_ComboBox.SelectedIndex = (int) paramConfig.ShowLrcType; @@ -98,13 +78,13 @@ private void InitialConfig() LrcMergeSeparator_TextBox.Text = paramConfig.LrcMergeSeparator; // 3、自动检查更新 - if (_settingBean.Config.AutoCheckUpdate) + if (settingBean.Config.AutoCheckUpdate) { ThreadPool.QueueUserWorkItem(p => CheckLatestVersion(false)); } } - private void TrySetHighDPIFont(String fontName) + private void TrySetHighDPIFont(string fontName) { //缩放比例大于100%才更改字体 if (DeviceDpi <= 96) return; @@ -114,7 +94,10 @@ private void TrySetHighDPIFont(String fontName) { font = new Font(fontName, 9F, FontStyle.Regular, GraphicsUnit.Point); } - catch (System.Exception) { } + catch (System.Exception) + { + // ignored + } if (font == null || !fontName.Equals(font.Name)) return; @@ -131,7 +114,10 @@ private void TrySetHighDPIFont(String fontName) Object obj = fieldInfo.GetValue(this); propertyInfo.SetValue(obj, font); } - catch (System.Exception) { } + catch (System.Exception) + { + // ignored + } } } } @@ -149,16 +135,12 @@ private void ReloadConfig() } _globalSearchInfo.SongIds.Clear(); - _globalSearchInfo.SearchSource = _searchSourceEnum; - _globalSearchInfo.SearchType = _searchTypeEnum; - _globalSearchInfo.OutputFileNameType = _outputFilenameTypeEnum; - _globalSearchInfo.ShowLrcType = _showLrcTypeEnum; - _globalSearchInfo.Encoding = _outputEncodingEnum; - _globalSearchInfo.DotType = _dotTypeEnum; - _globalSearchInfo.OutputFileFormat = _outputFormatEnum; - _globalSearchInfo.LrcMergeSeparator = LrcMergeSeparator_TextBox.Text; + + var param = _globalSearchInfo.SettingBean.Param; - if (_searchSourceEnum == SearchSourceEnum.QQ_MUSIC) + param.LrcMergeSeparator = LrcMergeSeparator_TextBox.Text; + + if (param.SearchSource == SearchSourceEnum.QQ_MUSIC) { _api = new QQMusicApiV2(); } @@ -212,8 +194,8 @@ private void SearchBySongId(IEnumerable songIds, out Dictionary saveVo.LyricVo)) { - Console_TextBox.Text = lyricVo.IsEmpty() ? ErrorMsg.LRC_NOT_EXIST : LyricUtils.GetOutputContent(lyricVo, _globalSearchInfo); + if (lyricVo.IsEmpty()) + { + Console_TextBox.Text = ErrorMsg.LRC_NOT_EXIST; + } + else + { + Console_TextBox.Text = await LyricUtils.GetOutputContent(lyricVo, _globalSearchInfo); + } } } } @@ -588,19 +580,19 @@ private void CleanTextBox() private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { - // 记住最终的参数配置 - if (_settingBean.Config.RememberParam) + // 如果参数不记住,需要回滚 + if (!_globalSearchInfo.SettingBean.Config.RememberParam) { - _settingBean.Param.Update(_globalSearchInfo); + _globalSearchInfo.SettingBean.Param = _globalSearchInfo.SettingBeanBackup.Param; } // 配置持久化 - File.WriteAllText(Constants.SettingPath, _settingBean.ToJson(), Encoding.UTF8); + File.WriteAllText(Constants.SettingPath, _globalSearchInfo.SettingBean.ToJson(), Encoding.UTF8); } private void MainForm_MouseEnter(object sender, EventArgs e) { - if (_settingBean.Config.AutoReadClipboard) + if (_globalSearchInfo.SettingBean.Config.AutoReadClipboard) { Search_Text.Text = Clipboard.GetText(); } @@ -611,7 +603,7 @@ private void MainForm_MouseEnter(object sender, EventArgs e) /// private void SearchSource_ComboBox_SelectedIndexChanged(object sender, EventArgs e) { - _searchSourceEnum = (SearchSourceEnum)SearchSource_ComboBox.SelectedIndex; + _globalSearchInfo.SettingBean.Param.SearchSource = (SearchSourceEnum)SearchSource_ComboBox.SelectedIndex; ReloadConfig(); UpdateLrcTextBox(string.Empty); @@ -622,7 +614,7 @@ private void SearchSource_ComboBox_SelectedIndexChanged(object sender, EventArgs /// private void SearchType_ComboBox_SelectedIndexChanged(object sender, EventArgs e) { - _searchTypeEnum = (SearchTypeEnum)SearchType_ComboBox.SelectedIndex; + _globalSearchInfo.SettingBean.Param.SearchType = (SearchTypeEnum)SearchType_ComboBox.SelectedIndex; ReloadConfig(); UpdateLrcTextBox(string.Empty); @@ -633,19 +625,18 @@ private void SearchType_ComboBox_SelectedIndexChanged(object sender, EventArgs e /// private void LrcType_ComboBox_SelectedIndexChanged(object sender, EventArgs e) { - _showLrcTypeEnum = (ShowLrcTypeEnum)LrcType_ComboBox.SelectedIndex; - - if (_showLrcTypeEnum == ShowLrcTypeEnum.MERGE_ORIGIN || - _showLrcTypeEnum == ShowLrcTypeEnum.MERGE_TRANSLATE) - { - LrcMergeSeparator_TextBox.ReadOnly = false; - LrcMergeSeparator_TextBox.BackColor = Color.White; - } - else + switch (_globalSearchInfo.SettingBean.Param.ShowLrcType = (ShowLrcTypeEnum)LrcType_ComboBox.SelectedIndex) { - LrcMergeSeparator_TextBox.Text = null; - LrcMergeSeparator_TextBox.ReadOnly = true; - LrcMergeSeparator_TextBox.BackColor = Color.FromArgb(240, 240, 240); + case ShowLrcTypeEnum.ORIGIN_PRIOR_MERGE: + case ShowLrcTypeEnum.TRANSLATE_PRIOR_MERGE: + LrcMergeSeparator_TextBox.ReadOnly = false; + LrcMergeSeparator_TextBox.BackColor = Color.White; + break; + default: + LrcMergeSeparator_TextBox.Text = null; + LrcMergeSeparator_TextBox.ReadOnly = true; + LrcMergeSeparator_TextBox.BackColor = Color.FromArgb(240, 240, 240); + break; } ReloadConfig(); @@ -666,7 +657,7 @@ private void LrcMergeSeparator_TextBox_TextChanged(object sender, EventArgs e) /// private void Dot_TextBox_SelectedIndexChanged(object sender, EventArgs e) { - _dotTypeEnum = (DotTypeEnum)Dot_TextBox.SelectedIndex; + _globalSearchInfo.SettingBean.Param.DotType = (DotTypeEnum)Dot_TextBox.SelectedIndex; ReloadConfig(); UpdateLrcTextBox(string.Empty); } @@ -697,15 +688,15 @@ private void Config_ComboBox_SelectedIndexChanged(object sender, EventArgs e) if (input == OutputEncoding_ComboBox) { - _outputEncodingEnum = (OutputEncodingEnum)input.SelectedIndex; + _globalSearchInfo.SettingBean.Param.Encoding = (OutputEncodingEnum)input.SelectedIndex; } else if (input == OutputFormat_CombBox) { - _outputFormatEnum = (OutputFormatEnum)input.SelectedIndex; + _globalSearchInfo.SettingBean.Param.OutputFileFormat = (OutputFormatEnum)input.SelectedIndex; } else if (input == OutputName_ComboBox) { - _outputFilenameTypeEnum = (OutputFilenameTypeEnum)input.SelectedIndex; + _globalSearchInfo.SettingBean.Param.OutputFileNameType = (OutputFilenameTypeEnum)input.SelectedIndex; } ReloadConfig(); @@ -749,7 +740,7 @@ private async void Top_MItem_Click(object sender, EventArgs e) { if (_settingForm == null || _settingForm.IsDisposed) { - _settingForm = new SettingForm(_settingBean.Config); + _settingForm = new SettingForm(_globalSearchInfo.SettingBean.Config); _settingForm.Location = new Point(Left + Constants.SettingFormOffset, Top + Constants.SettingFormOffset); _settingForm.StartPosition = FormStartPosition.Manual; _settingForm.Show(); diff --git a/MusicLyricApp/MusicLyricApp.csproj b/MusicLyricApp/MusicLyricApp.csproj index 22100e7..8b03027 100644 --- a/MusicLyricApp/MusicLyricApp.csproj +++ b/MusicLyricApp/MusicLyricApp.csproj @@ -1,5 +1,6 @@  + @@ -74,6 +75,15 @@ ..\packages\Costura.Fody.5.8.0-alpha0098\lib\netstandard1.0\Costura.dll True + + ..\packages\Kawazu.1.1.4\lib\netstandard2.0\Kawazu.dll + + + ..\packages\LibNMeCab.0.10.1\lib\netstandard2.0\LibNMeCab.dll + + + ..\packages\LibNMeCab.IpaDicBin.0.10.0\lib\netstandard2.0\LibNMeCab.IpaDicBin.dll + ..\packages\Markdig.0.28.1\lib\net452\Markdig.dll True @@ -210,6 +220,7 @@ True + @@ -264,6 +275,7 @@ + MainForm.cs @@ -318,18 +330,27 @@ + + + + + + - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - + + + + + \ No newline at end of file diff --git a/MusicLyricApp/SettingForm.Designer.cs b/MusicLyricApp/SettingForm.Designer.cs index 111f503..4364c4b 100644 --- a/MusicLyricApp/SettingForm.Designer.cs +++ b/MusicLyricApp/SettingForm.Designer.cs @@ -1,4 +1,6 @@ using System.ComponentModel; +using MusicLyricApp.Bean; +using MusicLyricApp.Utils; namespace MusicLyricApp { @@ -33,33 +35,40 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SettingForm)); this.Save_Btn = new System.Windows.Forms.Button(); - this.RememberParam_ComboBox = new System.Windows.Forms.CheckBox(); + this.RememberParam_CheckBox = new System.Windows.Forms.CheckBox(); this.AutoReadClipboard_CheckBox = new System.Windows.Forms.CheckBox(); this.AutoCheckUpdate_CheckBox = new System.Windows.Forms.CheckBox(); + this.label1 = new System.Windows.Forms.Label(); + this.ShowRomaji_CheckBox = new System.Windows.Forms.CheckBox(); + this.RomajiMode_ComboBox = new System.Windows.Forms.ComboBox(); + this.label3 = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.RomajiSystem_ComboBox = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); this.SuspendLayout(); // // Save_Btn // - this.Save_Btn.Location = new System.Drawing.Point(173, 114); + this.Save_Btn.Location = new System.Drawing.Point(148, 161); this.Save_Btn.Name = "Save_Btn"; - this.Save_Btn.Size = new System.Drawing.Size(102, 44); + this.Save_Btn.Size = new System.Drawing.Size(166, 44); this.Save_Btn.TabIndex = 0; this.Save_Btn.Text = "保存"; this.Save_Btn.UseVisualStyleBackColor = true; this.Save_Btn.Click += new System.EventHandler(this.Save_Btn_Click); // - // RememberParam_ComboBox + // RememberParam_CheckBox // - this.RememberParam_ComboBox.Location = new System.Drawing.Point(27, 25); - this.RememberParam_ComboBox.Name = "RememberParam_ComboBox"; - this.RememberParam_ComboBox.Size = new System.Drawing.Size(78, 24); - this.RememberParam_ComboBox.TabIndex = 1; - this.RememberParam_ComboBox.Text = "参数记忆"; - this.RememberParam_ComboBox.UseVisualStyleBackColor = true; + this.RememberParam_CheckBox.Location = new System.Drawing.Point(12, 17); + this.RememberParam_CheckBox.Name = "RememberParam_CheckBox"; + this.RememberParam_CheckBox.Size = new System.Drawing.Size(78, 24); + this.RememberParam_CheckBox.TabIndex = 1; + this.RememberParam_CheckBox.Text = "参数记忆"; + this.RememberParam_CheckBox.UseVisualStyleBackColor = true; // // AutoReadClipboard_CheckBox // - this.AutoReadClipboard_CheckBox.Location = new System.Drawing.Point(27, 77); + this.AutoReadClipboard_CheckBox.Location = new System.Drawing.Point(148, 15); this.AutoReadClipboard_CheckBox.Name = "AutoReadClipboard_CheckBox"; this.AutoReadClipboard_CheckBox.Size = new System.Drawing.Size(112, 26); this.AutoReadClipboard_CheckBox.TabIndex = 2; @@ -68,21 +77,92 @@ private void InitializeComponent() // // AutoCheckUpdate_CheckBox // - this.AutoCheckUpdate_CheckBox.Location = new System.Drawing.Point(27, 126); + this.AutoCheckUpdate_CheckBox.Location = new System.Drawing.Point(12, 168); this.AutoCheckUpdate_CheckBox.Name = "AutoCheckUpdate_CheckBox"; this.AutoCheckUpdate_CheckBox.Size = new System.Drawing.Size(97, 32); this.AutoCheckUpdate_CheckBox.TabIndex = 3; this.AutoCheckUpdate_CheckBox.Text = "自动检查更新"; this.AutoCheckUpdate_CheckBox.UseVisualStyleBackColor = true; // + // label1 + // + this.label1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; + this.label1.Location = new System.Drawing.Point(8, 49); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(315, 2); + this.label1.TabIndex = 4; + this.label1.Text = "label1"; + // + // ShowRomaji_CheckBox + // + this.ShowRomaji_CheckBox.Location = new System.Drawing.Point(12, 61); + this.ShowRomaji_CheckBox.Name = "ShowRomaji_CheckBox"; + this.ShowRomaji_CheckBox.Size = new System.Drawing.Size(120, 30); + this.ShowRomaji_CheckBox.TabIndex = 5; + this.ShowRomaji_CheckBox.Text = "译文显示罗马音"; + this.ShowRomaji_CheckBox.UseVisualStyleBackColor = true; + this.ShowRomaji_CheckBox.CheckedChanged += new System.EventHandler(this.ShowRomaji_CheckBox_CheckedChanged); + // + // RomajiMode_ComboBox + // + this.RomajiMode_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.RomajiMode_ComboBox.FormattingEnabled = true; + this.RomajiMode_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); + this.RomajiMode_ComboBox.Location = new System.Drawing.Point(232, 61); + this.RomajiMode_ComboBox.Name = "RomajiMode_ComboBox"; + this.RomajiMode_ComboBox.Size = new System.Drawing.Size(82, 20); + this.RomajiMode_ComboBox.TabIndex = 7; + // + // label3 + // + this.label3.Location = new System.Drawing.Point(148, 66); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(68, 18); + this.label3.TabIndex = 8; + this.label3.Text = "转换模式"; + // + // label4 + // + this.label4.Location = new System.Drawing.Point(148, 107); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(68, 18); + this.label4.TabIndex = 9; + this.label4.Text = "罗马字体系"; + // + // RomajiSystem_ComboBox + // + this.RomajiSystem_ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.RomajiSystem_ComboBox.FormattingEnabled = true; + this.RomajiSystem_ComboBox.Items.AddRange(GlobalUtils.GetEnumDescArray()); + this.RomajiSystem_ComboBox.Location = new System.Drawing.Point(232, 107); + this.RomajiSystem_ComboBox.Name = "RomajiSystem_ComboBox"; + this.RomajiSystem_ComboBox.Size = new System.Drawing.Size(82, 20); + this.RomajiSystem_ComboBox.TabIndex = 10; + // + // label2 + // + this.label2.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; + this.label2.Location = new System.Drawing.Point(12, 144); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(315, 2); + this.label2.TabIndex = 11; + this.label2.Text = "label2"; + // // SettingForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(307, 170); + this.ClientSize = new System.Drawing.Size(328, 217); + this.Controls.Add(this.label2); + this.Controls.Add(this.RomajiSystem_ComboBox); + this.Controls.Add(this.label4); + this.Controls.Add(this.label3); + this.Controls.Add(this.RomajiMode_ComboBox); + this.Controls.Add(this.ShowRomaji_CheckBox); + this.Controls.Add(this.label1); this.Controls.Add(this.AutoCheckUpdate_CheckBox); this.Controls.Add(this.AutoReadClipboard_CheckBox); - this.Controls.Add(this.RememberParam_ComboBox); + this.Controls.Add(this.RememberParam_CheckBox); this.Controls.Add(this.Save_Btn); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); @@ -92,11 +172,23 @@ private void InitializeComponent() this.ResumeLayout(false); } + private System.Windows.Forms.ComboBox RomajiSystem_ComboBox; + + private System.Windows.Forms.Label label4; + + private System.Windows.Forms.Label label2; + private System.Windows.Forms.ComboBox RomajiMode_ComboBox; + private System.Windows.Forms.Label label3; + + private System.Windows.Forms.CheckBox ShowRomaji_CheckBox; + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.CheckBox AutoCheckUpdate_CheckBox; private System.Windows.Forms.CheckBox AutoReadClipboard_CheckBox; - private System.Windows.Forms.CheckBox RememberParam_ComboBox; + private System.Windows.Forms.CheckBox RememberParam_CheckBox; private System.Windows.Forms.Button Save_Btn; diff --git a/MusicLyricApp/SettingForm.cs b/MusicLyricApp/SettingForm.cs index 421116b..4afc57f 100644 --- a/MusicLyricApp/SettingForm.cs +++ b/MusicLyricApp/SettingForm.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Linq; using System.Windows.Forms; using MusicLyricApp.Bean; @@ -7,25 +9,56 @@ namespace MusicLyricApp public partial class SettingForm : Form { private readonly ConfigBean _configBean; - + public SettingForm(ConfigBean configBean) { InitializeComponent(); _configBean = configBean; - RememberParam_ComboBox.Checked = _configBean.RememberParam; + RememberParam_CheckBox.Checked = _configBean.RememberParam; AutoReadClipboard_CheckBox.Checked = _configBean.AutoReadClipboard; AutoCheckUpdate_CheckBox.Checked = _configBean.AutoCheckUpdate; + + var romajiConfig = _configBean.RomajiConfig; + ShowRomaji_CheckBox.Checked = romajiConfig.Enable; + RomajiMode_ComboBox.SelectedIndex = (int)romajiConfig.ModeEnum; + RomajiSystem_ComboBox.SelectedIndex = (int)romajiConfig.SystemEnum; + ShowRomajiChangeListener(ShowRomaji_CheckBox.Checked); } private void Save_Btn_Click(object sender, EventArgs e) { - _configBean.RememberParam = RememberParam_ComboBox.Checked; + _configBean.RememberParam = RememberParam_CheckBox.Checked; _configBean.AutoReadClipboard = AutoReadClipboard_CheckBox.Checked; _configBean.AutoCheckUpdate = AutoCheckUpdate_CheckBox.Checked; - + + var romajiConfig = _configBean.RomajiConfig; + romajiConfig.Enable = ShowRomaji_CheckBox.Checked; + romajiConfig.ModeEnum = (RomajiModeEnum)RomajiMode_ComboBox.SelectedIndex; + romajiConfig.SystemEnum = (RomajiSystemEnum)RomajiSystem_ComboBox.SelectedIndex; + Close(); } + + private void ShowRomaji_CheckBox_CheckedChanged(object sender, EventArgs e) + { + ShowRomajiChangeListener(ShowRomaji_CheckBox.Checked); + } + + private void ShowRomajiChangeListener(bool isEnable) + { + // 检查依赖 + if (isEnable && Constants.IpaDicDependency.Any(e => !File.Exists(e))) + { + MessageBox.Show(ErrorMsg.ROMAJI_DEPENDENCY_LOSS, "提示"); + ShowRomaji_CheckBox.Checked = false; + + isEnable = false; + } + + RomajiMode_ComboBox.Enabled = isEnable; + RomajiSystem_ComboBox.Enabled = isEnable; + } } } \ No newline at end of file diff --git a/MusicLyricApp/UpgradeForm.cs b/MusicLyricApp/UpgradeForm.cs index a54e0b9..c4f4d89 100644 --- a/MusicLyricApp/UpgradeForm.cs +++ b/MusicLyricApp/UpgradeForm.cs @@ -27,7 +27,7 @@ public UpgradeForm(GitHubInfo info) _gitHubInfo = info; UpgradeTag_Label.Text = info.TagName; - UpgradeSpan1_Label.Text = $"更新日期:{info.PublishedAt.DateTime}" + SEPARATOR + $"下载次数:{info.Assets[0].DownloadCount}"; + UpgradeSpan1_Label.Text = $"更新日期:{info.PublishedAt.DateTime.AddHours(8)}" + SEPARATOR + $"下载次数:{info.Assets[0].DownloadCount}"; UpgradeSpan2_Label.Text = $"作者:{info.Author.Login}" + SEPARATOR + $"文件大小:{GetKb(info.Assets[0].Size):F1} KB"; UpgradeLog_Browser.DocumentText = Markdown.ToHtml(info.Body); } diff --git a/MusicLyricApp/Utils/GlobalUtils.cs b/MusicLyricApp/Utils/GlobalUtils.cs index 1fb5db6..d48fc2d 100644 --- a/MusicLyricApp/Utils/GlobalUtils.cs +++ b/MusicLyricApp/Utils/GlobalUtils.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using MusicLyricApp.Bean; @@ -105,10 +107,56 @@ private static bool CheckNum(string s) return Regex.IsMatch(s, "^\\d+$"); } + public static long TimestampStrToLong(string timestamp) + { + // 不支持的格式 + if (string.IsNullOrWhiteSpace(timestamp) || timestamp[0] != '[' || timestamp[timestamp.Length - 1] != ']') + { + return -1; + } + + timestamp = timestamp.Substring(1, timestamp.Length - 2); + + var split = timestamp.Split(':'); + + var min = toInt(split[0], 0); + + split = split[1].Split('.'); + + var second = toInt(split[0], 0); + + var ms = 0; + + if (split.Length > 1) + { + ms = toInt(split[1], 0); + } + + return (min * 60 + second) * 1000 + ms; + } + + public static string TimestampLongToStr(long timestamp, string msScale) + { + if (timestamp < 0) + { + throw new MusicLyricException(ErrorMsg.SYSTEM_ERROR); + } + + var ms = timestamp % 1000; + + timestamp /= 1000; + + var seconds = timestamp % 60; + + var min = timestamp / 60; + + return "[" + min.ToString("00") + ":" + seconds.ToString("00") + "." + ms.ToString(msScale) + "]"; + } + /** * 获取输出文件名 */ - public static string GetOutputName(SongVo songVo, SearchInfo searchInfo) + public static string GetOutputName(SongVo songVo, OutputFilenameTypeEnum typeEnum) { if (songVo == null) { @@ -118,7 +166,7 @@ public static string GetOutputName(SongVo songVo, SearchInfo searchInfo) } string outputName; - switch (searchInfo.OutputFileNameType) + switch (typeEnum) { case OutputFilenameTypeEnum.NAME_SINGER: outputName = songVo.Name + " - " + songVo.Singer; @@ -220,5 +268,32 @@ public static Encoding GetEncoding(OutputEncodingEnum encodingEnum) return new UTF8Encoding(false); } } + + public static int toInt(string str, int defaultValue) + { + var result = defaultValue; + + int.TryParse(str, out result); + + return result; + } + + public static List GetEnumList() where T : Enum + { + return Enum.GetValues(typeof(T)).OfType().ToList(); + } + + public static string[] GetEnumDescArray() where T : Enum + { + var list = GetEnumList(); + var result = new string[list.Count]; + + for (var i = 0; i < list.Count; i++) + { + result[i] = list[i].ToDescription(); + } + + return result; + } } } \ No newline at end of file diff --git a/MusicLyricApp/Utils/LyricUtils.cs b/MusicLyricApp/Utils/LyricUtils.cs index efc0652..598e4dd 100644 --- a/MusicLyricApp/Utils/LyricUtils.cs +++ b/MusicLyricApp/Utils/LyricUtils.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; +using System.Threading.Tasks; using MusicLyricApp.Bean; namespace MusicLyricApp.Utils @@ -11,29 +11,17 @@ namespace MusicLyricApp.Utils /// public abstract class LyricUtils { - /// - /// 填充歌词信息属性 - /// - /// - /// - /// - public static void FillingLyricVo(LyricVo lyricVo, SongVo songVo, SearchInfo searchInfo) - { - lyricVo.Duration = songVo.Duration; - lyricVo.Output = GetOutputContent(lyricVo, searchInfo); - } - /// /// 获取输出结果 /// /// /// /// - public static string GetOutputContent(LyricVo lyricVo, SearchInfo searchInfo) + public static async Task GetOutputContent(LyricVo lyricVo, SearchInfo searchInfo) { - var output = GenerateOutput(lyricVo.Lyric, lyricVo.TranslateLyric, searchInfo); + var output = await GenerateOutput(lyricVo.Lyric, lyricVo.TranslateLyric, searchInfo); - if (searchInfo.OutputFileFormat == OutputFormatEnum.SRT) + if (searchInfo.SettingBean.Param.OutputFileFormat == OutputFormatEnum.SRT) { output = SrtUtils.LrcToSrt(output, lyricVo.Duration); } @@ -48,19 +36,16 @@ public static string GetOutputContent(LyricVo lyricVo, SearchInfo searchInfo) /// 原始的译文内容 /// 处理参数 /// - private static string GenerateOutput(string originLyric, string originTLyric, SearchInfo searchInfo) + private static async Task GenerateOutput(string originLyric, string originTLyric, SearchInfo searchInfo) { - // 歌词合并 - var formatLyrics = FormatLyric(originLyric, originTLyric, searchInfo); - - // 两位小数 - SetTimeStamp2Dot(ref formatLyrics, searchInfo.DotType); + var formatLyrics = await FormatLyric(originLyric, originTLyric, searchInfo); var result = new StringBuilder(); foreach (var i in formatLyrics) { result.Append(i).Append(Environment.NewLine); } + return result.ToString(); } @@ -71,207 +56,216 @@ private static string GenerateOutput(string originLyric, string originTLyric, Se /// 原始的译文内容 /// 处理参数 /// - private static string[] FormatLyric(string originLrc, string translateLrc, SearchInfo searchInfo) + private static async Task> FormatLyric(string originLrc, string translateLrc, SearchInfo searchInfo) { - var showLrcType = searchInfo.ShowLrcType; + var showLrcType = searchInfo.SettingBean.Param.ShowLrcType; + var searchSource = searchInfo.SettingBean.Param.SearchSource; + var dotType = searchInfo.SettingBean.Param.DotType; - // 如果不存在翻译歌词,或者选择返回原歌词 - var originLrcs = SplitLrc(originLrc); - if (string.IsNullOrEmpty(translateLrc) || showLrcType == ShowLrcTypeEnum.ONLY_ORIGIN) + var originLyrics = SplitLrc(originLrc, searchSource, dotType); + + /* + * 1、原文歌词不存在 + * 2、不存在翻译歌词 + * 3、选择仅原歌词 + */ + if (originLyrics.Count == 0 || string.IsNullOrEmpty(translateLrc) || + showLrcType == ShowLrcTypeEnum.ONLY_ORIGIN) { - return originLrcs; + return originLyrics; } - // 如果选择仅译文 - var translateLrcs = SplitLrc(translateLrc); - if (showLrcType == ShowLrcTypeEnum.ONLY_TRANSLATE) + // 译文处理,启用罗马音进行转换,否则使用原始的译文 + var romajiConfig = searchInfo.SettingBean.Config.RomajiConfig; + + var translateLyrics = SplitLrc(translateLrc, searchSource, dotType); + + if (romajiConfig.Enable) { - return translateLrcs; + translateLyrics = await RomajiUtils.ToRomaji(originLyrics, translateLyrics, romajiConfig); } - string[] res = null; + /* + * 1、译文歌词不存在 + * 2、选择仅译文歌词 + */ + if (translateLyrics.Count == 0 || showLrcType == ShowLrcTypeEnum.ONLY_TRANSLATE) + { + return translateLyrics; + } + + List res = null; switch (showLrcType) { - case ShowLrcTypeEnum.ORIGIN_PRIOR: - res = SortLrc(originLrcs, translateLrcs, true); + case ShowLrcTypeEnum.ORIGIN_PRIOR_ISOLATED: + res = originLyrics; + res.AddRange(translateLyrics); + break; + case ShowLrcTypeEnum.TRANSLATE_PRIOR_ISOLATED: + res = translateLyrics; + res.AddRange(originLyrics); + break; + case ShowLrcTypeEnum.ORIGIN_PRIOR_STAGGER: + res = SortLrc(originLyrics, translateLyrics, true); break; - case ShowLrcTypeEnum.TRANSLATE_PRIOR: - res = SortLrc(originLrcs, translateLrcs, false); + case ShowLrcTypeEnum.TRANSLATE_PRIOR_STAGGER: + res = SortLrc(originLyrics, translateLyrics, false); break; - case ShowLrcTypeEnum.MERGE_ORIGIN: - res = MergeLrc(originLrcs, translateLrcs, searchInfo.LrcMergeSeparator, true); + case ShowLrcTypeEnum.ORIGIN_PRIOR_MERGE: + res = MergeLrc(originLyrics, translateLyrics, searchInfo.SettingBean.Param.LrcMergeSeparator, true); break; - case ShowLrcTypeEnum.MERGE_TRANSLATE: - res = MergeLrc(originLrcs, translateLrcs, searchInfo.LrcMergeSeparator, false); + case ShowLrcTypeEnum.TRANSLATE_PRIOR_MERGE: + res = MergeLrc(originLyrics, translateLyrics, searchInfo.SettingBean.Param.LrcMergeSeparator, false); break; } return res; } - + /** - * 将歌词切割为数组 + * 切割歌词 */ - private static string[] SplitLrc(string lrc) + private static List SplitLrc(string lrc, SearchSourceEnum searchSource, DotTypeEnum dotType) { - return lrc.Replace("\r\n", "\n").Split('\n').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); + // 换行符统一 + var temp = lrc + .Replace("\r\n", "\n") + .Replace("\r", "") + .Split('\n'); + + var resultList = new List(); + + foreach (var line in temp) + { + // 跳过空行 + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + // QQ 音乐歌词正式开始标识符 + if (searchSource == SearchSourceEnum.QQ_MUSIC && "[offset:0]".Equals(line)) + { + resultList.Clear(); + continue; + } + + var lyricLineVo = new LyricLineVo(line); + + SetTimeStamp2Dot(lyricLineVo, dotType); + + // 跳过无效行 + if (string.IsNullOrWhiteSpace(lyricLineVo.Content)) + { + continue; + } + + resultList.Add(lyricLineVo); + } + + return resultList; } /** * 双语歌词排序 */ - private static string[] SortLrc(string[] originLrcs, string[] translateLrcs, bool hasOriginLrcPrior) + private static List SortLrc(List originLyrics, List translateLyrics, bool hasOriginLrcPrior) { - int lena = originLrcs.Length; - int lenb = translateLrcs.Length; - string[] c = new string[lena + lenb]; - //分别代表数组a ,b , c 的索引 - int i = 0, j = 0, k = 0; + int lenA = originLyrics.Count, lenB = translateLyrics.Count; + var c = new List(); + + int i = 0, j = 0; - while (i < lena && j < lenb) + while (i < lenA && j < lenB) { - if (Compare(originLrcs[i], translateLrcs[j], hasOriginLrcPrior) == 1) + var compare = Compare(originLyrics[i], translateLyrics[j], hasOriginLrcPrior); + + if (compare > 0) { - c[k++] = translateLrcs[j++]; + c.Add(translateLyrics[j++]); } - else if (Compare(originLrcs[i], translateLrcs[j], hasOriginLrcPrior) == -1) + else if (compare < 0) { - c[k++] = originLrcs[i++]; + c.Add(originLyrics[i++]); } else { - c[k++] = hasOriginLrcPrior ? originLrcs[i++] : translateLrcs[j++]; + c.Add(hasOriginLrcPrior ? originLyrics[i++] : translateLyrics[j++]); } } - while (i < lena) - c[k++] = originLrcs[i++]; - while (j < lenb) - c[k++] = translateLrcs[j++]; + while (i < lenA) + c.Add(originLyrics[i++]); + while (j < lenB) + c.Add(translateLyrics[j++]); return c; } /** * 双语歌词合并 */ - private static string[] MergeLrc(string[] originLrcs, string[] translateLrcs, string splitStr, bool hasOriginLrcPrior) + private static List MergeLrc(List originLrcs, List translateLrcs, string splitStr, bool hasOriginLrcPrior) { - string[] c = SortLrc(originLrcs, translateLrcs, hasOriginLrcPrior); - List list = new List + var c = SortLrc(originLrcs, translateLrcs, hasOriginLrcPrior); + + var list = new List { c[0] }; - for (int i = 1; i < c.Length; i++) + for (var i = 1; i < c.Count - 1; i++) { - int str1Index = c[i - 1].IndexOf("]") + 1; - int str2Index = c[i].IndexOf("]") + 1; - string str1Timestamp = c[i - 1].Substring(0, str1Index); - string str2Timestamp = c[i].Substring(0, str2Index); - if (str1Timestamp != str2Timestamp) + if (c[i].TimeOffset != c[i + 1].TimeOffset) { list.Add(c[i]); } else { - int index = list.Count - 1; - string subStr1 = list[index]; - string subStr2 = c[i].Substring(str2Index); - - // Fix: https://github.com/jitwxs/Application/issues/7 - if (string.IsNullOrEmpty(subStr1) || string.IsNullOrEmpty(subStr2)) - { - list[index] = subStr1 + subStr2; - } - else - { - list[index] = subStr1 + splitStr + subStr2; - } + var index = list.Count - 1; + + list[index].Content = list[index].Content + splitStr + c[i].Content; } } - return list.ToArray(); + return list; } /** * 歌词排序函数 */ - private static int Compare(string originLrc, string translateLrc, bool hasOriginLrcPrior) + private static int Compare(LyricLineVo originLrc, LyricLineVo translateLrc, bool hasOriginLrcPrior) { - int str1Index = originLrc.IndexOf("]"); - string str1Timestamp = originLrc.Substring(0, str1Index + 1); - int str2Index = translateLrc.IndexOf("]"); - string str2Timestamp = translateLrc.Substring(0, str2Index + 1); - - // Fix: https://github.com/jitwxs/Application/issues/8 - if (string.IsNullOrEmpty(str1Timestamp) || string.IsNullOrEmpty(str2Timestamp)) - { - return 1; - } - - if (str1Timestamp != str2Timestamp) - { - str1Timestamp = str1Timestamp.Substring(1, str1Timestamp.Length - 2); - str2Timestamp = str2Timestamp.Substring(1, str2Timestamp.Length - 2); - string[] t1s = str1Timestamp.Split(':'); - string[] t2s = str2Timestamp.Split(':'); - for (int i = 0; i < t1s.Length; i++) - { - if (double.TryParse(t1s[i], out double t1)) - { - if (double.TryParse(t2s[i], out double t2)) - { - if (t1 > t2) - return 1; - else if (t1 < t2) - return -1; - } - else - { - return 1; - } - } - else - { - return -1; - } - } + var compareTo = originLrc.CompareTo(translateLrc); - return 0; - } - else + if (compareTo == 0) { return hasOriginLrcPrior ? -1 : 1; } + + return compareTo; } - + /** * 设置时间戳小数位数 */ - private static void SetTimeStamp2Dot(ref string[] lrcStr, DotTypeEnum dotTypeEnum) + private static void SetTimeStamp2Dot(LyricLineVo vo, DotTypeEnum dotTypeEnum) { - for (int i = 0; i < lrcStr.Length; i++) + if (dotTypeEnum == DotTypeEnum.DISABLE) { - int index = lrcStr[i].IndexOf("]"); - int dot = lrcStr[i].IndexOf("."); - if (index == -1 || dot == -1) - { - continue; - } + return; + } + + var round = vo.TimeOffset % 1000 / 100; + if (round > 0 && dotTypeEnum == DotTypeEnum.HALF_UP) + { + round = round >= 5 ? 1 : 0; + } - string ms = lrcStr[i].Substring(dot + 1, index - dot - 1); - if (ms.Length == 3) - { - if (dotTypeEnum == DotTypeEnum.DOWN) - { - ms = ms.Substring(0, 2); - } - else if (dotTypeEnum == DotTypeEnum.HALF_UP) - { - ms = Convert.ToDouble("0." + ms).ToString("0.00").Substring(2); - } - } - lrcStr[i] = lrcStr[i].Substring(0, dot) + "." + ms + lrcStr[i].Substring(index); + if (round == 1) + { + vo.TimeOffset = (vo.TimeOffset / 10 + round) * 10; } + + vo.Timestamp = GlobalUtils.TimestampLongToStr(vo.TimeOffset, "00"); } } } \ No newline at end of file diff --git a/MusicLyricApp/Utils/RomajiUtils.cs b/MusicLyricApp/Utils/RomajiUtils.cs new file mode 100644 index 0000000..3de9038 --- /dev/null +++ b/MusicLyricApp/Utils/RomajiUtils.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kawazu; +using MusicLyricApp.Bean; +using MusicLyricApp.Exception; + +namespace MusicLyricApp.Utils +{ + public static class RomajiUtils + { + public static async Task> ToRomaji(List inputList, List faultList, + RomajiConfigBean romajiConfig) + { + if (inputList.Any(vo => Utilities.HasKana(vo.Content))) + { + var converter = new KawazuConverter(); + var mode = ConvertModeEnum(romajiConfig.ModeEnum); + var system = ConvertSystemEnum(romajiConfig.SystemEnum); + + var resultList = new List(); + + foreach (var vo in inputList) + { + var content = await converter.Convert(vo.Content, To.Romaji, mode, system, "(", ")"); + resultList.Add(new LyricLineVo + { + Content = content, + Timestamp = vo.Timestamp, + TimeOffset = vo.TimeOffset + }); + } + + return resultList; + } + + return faultList; + } + + private static Mode ConvertModeEnum(RomajiModeEnum modeEnum) + { + switch (modeEnum) + { + case RomajiModeEnum.NORMAL: + return Mode.Normal; + case RomajiModeEnum.SPACED: + return Mode.Spaced; + case RomajiModeEnum.OKURIGANA: + return Mode.Okurigana; + case RomajiModeEnum.FURIGANA: + return Mode.Furigana; + default: + throw new MusicLyricException(ErrorMsg.FUNCTION_NOT_SUPPORT); + } + } + + private static RomajiSystem ConvertSystemEnum(RomajiSystemEnum systemEnum) + { + switch (systemEnum) + { + case RomajiSystemEnum.NIPPON: + return RomajiSystem.Nippon; + case RomajiSystemEnum.PASSPORT: + return RomajiSystem.Passport; + case RomajiSystemEnum.HEPBURN: + return RomajiSystem.Hepburn; + default: + throw new MusicLyricException(ErrorMsg.FUNCTION_NOT_SUPPORT); + } + } + } +} \ No newline at end of file diff --git a/MusicLyricApp/Utils/SrtUtils.cs b/MusicLyricApp/Utils/SrtUtils.cs index 0fd2dee..c8ceba4 100644 --- a/MusicLyricApp/Utils/SrtUtils.cs +++ b/MusicLyricApp/Utils/SrtUtils.cs @@ -4,7 +4,7 @@ namespace MusicLyricApp.Utils { - public class SrtUtils + public static class SrtUtils { /// /// 将 Lrc 格式,转换为 Srt 格式 diff --git a/MusicLyricApp/char.bin b/MusicLyricApp/char.bin new file mode 100644 index 0000000..09451e3 Binary files /dev/null and b/MusicLyricApp/char.bin differ diff --git a/MusicLyricApp/matrix.bin b/MusicLyricApp/matrix.bin new file mode 100644 index 0000000..79ac347 Binary files /dev/null and b/MusicLyricApp/matrix.bin differ diff --git a/MusicLyricApp/packages.config b/MusicLyricApp/packages.config index 1d033d7..d53eb93 100644 --- a/MusicLyricApp/packages.config +++ b/MusicLyricApp/packages.config @@ -1,7 +1,10 @@  - + + + + diff --git a/MusicLyricApp/sys.dic b/MusicLyricApp/sys.dic new file mode 100644 index 0000000..c1a96a8 Binary files /dev/null and b/MusicLyricApp/sys.dic differ diff --git a/MusicLyricApp/unk.dic b/MusicLyricApp/unk.dic new file mode 100644 index 0000000..c20b42c Binary files /dev/null and b/MusicLyricApp/unk.dic differ diff --git a/MusicLyricAppTest/MusicLyricAppTest.csproj b/MusicLyricAppTest/MusicLyricAppTest.csproj index 82d84fe..7d58b23 100644 --- a/MusicLyricAppTest/MusicLyricAppTest.csproj +++ b/MusicLyricAppTest/MusicLyricAppTest.csproj @@ -31,10 +31,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -45,6 +42,7 @@ + diff --git a/MusicLyricAppTest/Utils/GlobalUtils.cs b/MusicLyricAppTest/Utils/GlobalUtils.cs index 30e6167..035ec48 100644 --- a/MusicLyricAppTest/Utils/GlobalUtils.cs +++ b/MusicLyricAppTest/Utils/GlobalUtils.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using static MusicLyricApp.Utils.GlobalUtils; -namespace ApplicationTest.Utils +namespace MusicLyricAppTest.Utils { [TestFixture] public class GlobalUtils @@ -12,12 +12,12 @@ public class GlobalUtils public void TestCheckInputIdWithNumber() { Assert.AreEqual("1", CheckInputId("1", SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.SONG_ID)); - - var exception = Assert.Throws(typeof(MusicLyricException), + + var exception = Assert.Throws(typeof(MusicLyricException), () => CheckInputId(string.Empty, SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.SONG_ID)); Assert.AreEqual(ErrorMsg.INPUT_ID_ILLEGAL, exception.Message); - - exception = Assert.Throws(typeof(MusicLyricException), + + exception = Assert.Throws(typeof(MusicLyricException), () => CheckInputId(null, SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.SONG_ID)); Assert.AreEqual(ErrorMsg.INPUT_ID_ILLEGAL, exception.Message); } @@ -25,25 +25,35 @@ public void TestCheckInputIdWithNumber() [Test] public void TestCheckInputIdWithWord() { - var exception = Assert.Throws(typeof(MusicLyricException), + var exception = Assert.Throws(typeof(MusicLyricException), () => CheckInputId("abc", SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.SONG_ID)); Assert.AreEqual(ErrorMsg.INPUT_ID_ILLEGAL, exception.Message); - + Assert.AreEqual("abc", CheckInputId("abc", SearchSourceEnum.QQ_MUSIC, SearchTypeEnum.SONG_ID)); } [Test] public void TestCheckInputIdWithUrl() { - Assert.AreEqual("1815969317", CheckInputId("https://music.163.com/#/song?id=1815969317", SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.SONG_ID)); - Assert.AreEqual("122305109", CheckInputId("https://music.163.com/#/album?id=122305109", SearchSourceEnum.NET_EASE_MUSIC, SearchTypeEnum.ALBUM_ID)); - - Assert.AreEqual("002owtOq052wu9", CheckInputId("https://y.qq.com/n/ryqq/songDetail/002owtOq052wu9", SearchSourceEnum.QQ_MUSIC, SearchTypeEnum.SONG_ID)); - Assert.AreEqual("000k0h474UtgAL", CheckInputId("https://y.qq.com/n/ryqq/albumDetail/000k0h474UtgAL", SearchSourceEnum.QQ_MUSIC, SearchTypeEnum.ALBUM_ID)); - - Assert.Throws(typeof(MusicLyricException), () => CheckInputId("https://y.qq.com/n/ryqq/singer/004cyCLc1ByKPx", SearchSourceEnum.QQ_MUSIC, SearchTypeEnum.SONG_ID)); + Assert.AreEqual("1815969317", + CheckInputId("https://music.163.com/#/song?id=1815969317", SearchSourceEnum.NET_EASE_MUSIC, + SearchTypeEnum.SONG_ID)); + Assert.AreEqual("122305109", + CheckInputId("https://music.163.com/#/album?id=122305109", SearchSourceEnum.NET_EASE_MUSIC, + SearchTypeEnum.ALBUM_ID)); + + Assert.AreEqual("002owtOq052wu9", + CheckInputId("https://y.qq.com/n/ryqq/songDetail/002owtOq052wu9", SearchSourceEnum.QQ_MUSIC, + SearchTypeEnum.SONG_ID)); + Assert.AreEqual("000k0h474UtgAL", + CheckInputId("https://y.qq.com/n/ryqq/albumDetail/000k0h474UtgAL", SearchSourceEnum.QQ_MUSIC, + SearchTypeEnum.ALBUM_ID)); + + Assert.Throws(typeof(MusicLyricException), + () => CheckInputId("https://y.qq.com/n/ryqq/singer/004cyCLc1ByKPx", SearchSourceEnum.QQ_MUSIC, + SearchTypeEnum.SONG_ID)); } - + [Test] public void GetOutputNameTest() { @@ -52,12 +62,44 @@ public void GetOutputNameTest() Name = "name", Singer = "singer" }; - Assert.AreEqual("name - singer", - GetOutputName(songVo, new SearchInfo() { OutputFileNameType = OutputFilenameTypeEnum.NAME_SINGER })); - Assert.AreEqual("singer - name", - GetOutputName(songVo, new SearchInfo() { OutputFileNameType = OutputFilenameTypeEnum.SINGER_NAME })); - Assert.AreEqual("name", - GetOutputName(songVo, new SearchInfo() { OutputFileNameType = OutputFilenameTypeEnum.NAME })); + Assert.AreEqual("name - singer", GetOutputName(songVo, OutputFilenameTypeEnum.NAME_SINGER)); + Assert.AreEqual("singer - name", GetOutputName(songVo, OutputFilenameTypeEnum.SINGER_NAME)); + Assert.AreEqual("name", GetOutputName(songVo, OutputFilenameTypeEnum.NAME)); + } + + [Test] + public void TestTimestampToLong() + { + // 不合法 + Assert.AreEqual(-1, TimestampStrToLong("")); + Assert.AreEqual(-1, TimestampStrToLong("[12131")); + Assert.AreEqual(-1, TimestampStrToLong("-1]")); + + // 合法 + Assert.AreEqual(62 * 1000 + 3, TimestampStrToLong("[1:2.3]")); + Assert.AreEqual((12 * 60 + 59) * 1000 + 311, TimestampStrToLong("[12:59.311]")); + + // [00:39.17] + Assert.AreEqual(39 * 1000 + 17, TimestampStrToLong("[00:39.17]")); + } + + [Test] + public void TestTimestampLongToStr() + { + // 非法 + Assert.Throws(() => TimestampLongToStr(-1, "00")); + + Assert.AreEqual("[00:00.000]", TimestampLongToStr(0, "000")); + + Assert.AreEqual("[00:00.00]", TimestampLongToStr(0, "00")); + + Assert.AreEqual("[01:02.003]", TimestampLongToStr(62 * 1000 + 3, "000")); + + Assert.AreEqual("[01:02.03]", TimestampLongToStr(62 * 1000 + 3, "00")); + + Assert.AreEqual("[12:59.311]", TimestampLongToStr((12 * 60 + 59) * 1000 + 311, "000")); + + Assert.AreEqual("[12:59.0311]", TimestampLongToStr((12 * 60 + 59) * 1000 + 311, "0000")); } } } \ No newline at end of file diff --git a/MusicLyricAppTest/Utils/SrtUtils.cs b/MusicLyricAppTest/Utils/SrtUtils.cs new file mode 100644 index 0000000..ff22d23 --- /dev/null +++ b/MusicLyricAppTest/Utils/SrtUtils.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kawazu; +using NUnit.Framework; + +namespace MusicLyricAppTest.Utils +{ + [TestFixture] + public class SrtUtils + { + [Test] + public void TestJapaneseLanguageJudge() + { + Main("ああだのこうだの知ったもんか 幸先の空は悪天候"); + + Main("[00:29.720]いつもどおりの通り独り こんな日々もはや懲り懲り"); + + Thread.Sleep(1000); + } + + private static async Task Main(string content) + { + var converter = new KawazuConverter(); + + Console.WriteLine(await converter.Convert(content, To.Romaji, Mode.Spaced, RomajiSystem.Hepburn, "(", ")")); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 4af6d4c..dc808e2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - [x] 支持多种歌词原文和译文的组织方式 Support multiple original lyrics and translation lyrics organization - [x] 支持提取(部分)歌曲试听链接 Support extraction (part) song audition link - [x] 支持多种保存命名规则、文件编码、输出格式 Support multiple saving naming rules, file encoding, output format +- [x] 日文歌曲支持罗马音 Support romaji in japanese songs ### Downloads diff --git a/images/latest_version.png b/images/latest_version.png index 87b0d66..fc9e8b4 100644 Binary files a/images/latest_version.png and b/images/latest_version.png differ