├── Lyricify.Lyrics.Helper ├── Resources │ └── icon.png ├── Parsers │ ├── TtmlParser.cs │ ├── Models │ │ ├── Yrc.cs │ │ ├── Musixmatch.cs │ │ └── Spotify.cs │ ├── LyricifyLinesParser.cs │ ├── QrcParser.cs │ ├── SpotifyParser.cs │ ├── AttributesHelper.cs │ ├── MusixmatchParser.cs │ └── LyricifySyllableParser.cs ├── Providers │ ├── IProvider.cs │ ├── Provider.cs │ ├── KugouProviderResult.cs │ ├── NeteaseProviderResult.cs │ ├── QQMusicProviderResult.cs │ ├── IProviderResult.cs │ └── Web │ │ ├── Providers.cs │ │ ├── Kugou │ │ ├── Api.cs │ │ └── Response.cs │ │ ├── Proxy.cs │ │ ├── SodaMusic │ │ └── Api.cs │ │ ├── BaseApi.cs │ │ └── Netease │ │ └── EapiHelper.cs ├── Models │ ├── FileInfo.cs │ ├── ISyllableInfo.cs │ ├── LyricsData.cs │ ├── SyncTypes.cs │ ├── ITrackMetadata.cs │ ├── AdditionalFileInfo.cs │ ├── LyricsTypes.cs │ ├── SyllableInfo.cs │ ├── TrackMetadata.cs │ ├── ILineInfo.cs │ └── LineInfo.cs ├── Searchers │ ├── Searchers.cs │ ├── SearcherHelper.cs │ ├── SodaMusicSearcher.cs │ ├── NeteaseSearchResult.cs │ ├── KugouSearchResult.cs │ ├── KugouSearcher.cs │ ├── SodaMusicSearchResult.cs │ ├── QQMusicSearchResult.cs │ ├── QQMusicSearcher.cs │ ├── MusixmatchSearchResult.cs │ ├── ISearchResult.cs │ ├── ISearcher.cs │ ├── Helpers │ │ ├── MatchHelpers │ │ │ ├── DurationMatch.cs │ │ │ ├── ArtistMatch.cs │ │ │ └── NameMatch.cs │ │ └── CompareHelper.cs │ ├── SearchersHelper.cs │ ├── NeteaseSearcher.cs │ ├── MusixmatchSearcher.cs │ └── Searcher.cs ├── Decrypter │ ├── Qrc │ │ ├── Model.cs │ │ ├── Helper.cs │ │ ├── Decrypter.cs │ │ └── XmlUtils.cs │ └── Krc │ │ ├── Model.cs │ │ ├── Decrypter.cs │ │ └── Helper.cs ├── Helpers │ ├── Types │ │ ├── Lrc.cs │ │ └── LyricifyLines.cs │ ├── ProviderHelper.cs │ ├── Optimization │ │ ├── Musixmatch.cs │ │ └── Yrc.cs │ ├── GeneratorHelper.cs │ ├── OffsetHelper.cs │ ├── ParseHelper.cs │ ├── SearchHelper.cs │ ├── TypeHelper.cs │ └── General │ │ └── MathHelper.cs ├── Lyricify.Lyrics.Helper.csproj └── Generators │ ├── LyricifyLinesGenerator.cs │ ├── QrcGenerator.cs │ ├── YrcGenerator.cs │ ├── KrcGenerator.cs │ ├── LyricifySyllableGenerator.cs │ └── LrcGenerator.cs ├── Lyricify Lyrics Helper.sln ├── README.md ├── Lyricify.Lyrics.Demo ├── Lyricify.Lyrics.Demo.csproj └── RawLyrics │ ├── SpotifyDemo.txt │ ├── LyricifyLinesDemo.txt │ ├── SpotifyUnsyncedDemo.txt │ ├── LrcDemo.txt │ ├── LsMixQrcDemo.txt │ ├── QrcDemo.txt │ └── LyricifySyllableDemo.txt └── .gitattributes /Lyricify.Lyrics.Helper/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WXRIW/Lyricify-Lyrics-Helper/HEAD/Lyricify.Lyrics.Helper/Resources/icon.png -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/TtmlParser.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Parsers 2 | { 3 | public static class TtmlParser 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/IProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Providers 2 | { 3 | /// 4 | /// 歌词提供者接口 5 | /// 6 | public interface IProvider 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/FileInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public class FileInfo 4 | { 5 | public LyricsTypes Type { get; set; } 6 | 7 | public SyncTypes SyncTypes { get; set; } 8 | 9 | public IAdditionalFileInfo? AdditionalInfo { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Searchers.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers 2 | { 3 | /// 4 | /// 所有搜索提供者的枚举 5 | /// 6 | public enum Searchers 7 | { 8 | QQMusic, 9 | Netease, 10 | Kugou, 11 | Musixmatch, 12 | SodaMusic, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Provider.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Providers 2 | { 3 | public abstract class Provider : IProvider 4 | { 5 | public abstract string Name { get; } 6 | 7 | public abstract string DisplayName { get; } 8 | 9 | public abstract IProviderResult ObtainLyrics(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/ISyllableInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public interface ISyllableInfo 4 | { 5 | public string Text { get; } 6 | 7 | public int StartTime { get; } 8 | 9 | public int EndTime { get; } 10 | 11 | public int Duration => EndTime - StartTime; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/LyricsData.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public class LyricsData 4 | { 5 | public FileInfo? File { get; set; } 6 | 7 | public List? Lines { get; set; } 8 | 9 | public List? Writers { get; set; } 10 | 11 | public ITrackMetadata? TrackMetadata { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/KugouProviderResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Searchers; 2 | 3 | namespace Lyricify.Lyrics.Providers 4 | { 5 | public class KugouProviderResult : IProviderResult 6 | { 7 | public IProvider Provider => throw new NotImplementedException(); 8 | 9 | public ISearchResult SearchResult => throw new NotImplementedException(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/NeteaseProviderResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Searchers; 2 | 3 | namespace Lyricify.Lyrics.Providers 4 | { 5 | public class NeteaseProviderResult : IProviderResult 6 | { 7 | public IProvider Provider => throw new NotImplementedException(); 8 | 9 | public ISearchResult SearchResult => throw new NotImplementedException(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/QQMusicProviderResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Searchers; 2 | 3 | namespace Lyricify.Lyrics.Providers 4 | { 5 | public class QQMusicProviderResult : IProviderResult 6 | { 7 | public IProvider Provider => throw new NotImplementedException(); 8 | 9 | public ISearchResult SearchResult => throw new NotImplementedException(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Qrc/Model.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Decrypter.Qrc 2 | { 3 | public class QqLyricsResponse 4 | { 5 | public string? Lyrics { get; set; } 6 | 7 | public string? Trans { get; set; } 8 | } 9 | 10 | public class SongResponse 11 | { 12 | public long Code { get; set; } 13 | 14 | public Song[]? Data { get; set; } 15 | 16 | public class Song 17 | { 18 | public string? Id { get; set; } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/IProviderResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Searchers; 2 | 3 | namespace Lyricify.Lyrics.Providers 4 | { 5 | /// 6 | /// 歌词提供结果接口 7 | /// 8 | public interface IProviderResult 9 | { 10 | /// 11 | /// 歌词提供者 12 | /// 13 | public IProvider Provider { get; } 14 | 15 | /// 16 | /// 搜索结果 (曲目) 17 | /// 18 | public ISearchResult SearchResult { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/Types/Lrc.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Lyricify.Lyrics.Helpers.Types 4 | { 5 | public static class Lrc 6 | { 7 | public static bool IsLrc(string input) 8 | { 9 | if (input == null) return false; 10 | 11 | var pattern = @"\[\d+:\d+\.\d+\](.+)"; 12 | var regex = new Regex(pattern); 13 | 14 | MatchCollection matches = regex.Matches(input); 15 | return matches.Count > 0; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/Models/Yrc.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace Lyricify.Lyrics.Parsers.Models.Yrc 5 | { 6 | public class CreditsInfo 7 | { 8 | [JsonProperty("t")] 9 | public int Timestamp { get; set; } 10 | 11 | [JsonProperty("c")] 12 | public List Credits { get; set; } 13 | 14 | public class Credit 15 | { 16 | [JsonProperty("tx")] 17 | public string Text { get; set; } 18 | 19 | [JsonProperty("li")] 20 | public string Image { get; set; } 21 | 22 | [JsonProperty("or")] 23 | public string Orpheus { get; set; } 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/ProviderHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Helpers 2 | { 3 | /// 4 | /// 提供 API 实例化后的静态类 5 | /// 6 | public static class ProviderHelper 7 | { 8 | public static Providers.Web.QQMusic.Api QQMusicApi => Providers.Web.Providers.QQMusicApi; 9 | 10 | public static Providers.Web.Netease.Api NeteaseApi => Providers.Web.Providers.NeteaseApi; 11 | 12 | public static Providers.Web.Kugou.Api KugouApi => Providers.Web.Providers.KugouApi; 13 | 14 | public static Providers.Web.Musixmatch.Api MusixmatchApi => Providers.Web.Providers.MusixmatchApi; 15 | 16 | public static Providers.Web.SodaMusic.Api SodaMusicApi => Providers.Web.Providers.SodaMusicApi; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/Models/Musixmatch.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace Lyricify.Lyrics.Parsers.Models 5 | { 6 | public class RichSyncedLine 7 | { 8 | [JsonProperty("ts")] 9 | public float TimeStart { get; set; } 10 | 11 | [JsonProperty("te")] 12 | public float TimeEnd { get; set; } 13 | 14 | [JsonProperty("l")] 15 | public List Words { get; set; } 16 | 17 | public class Word 18 | { 19 | [JsonProperty("c")] 20 | public string Chars { get; set; } 21 | 22 | [JsonProperty("o")] 23 | public double Position { get; set; } 24 | } 25 | 26 | [JsonProperty("x")] 27 | public string Text { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/Providers.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Providers.Web 2 | { 3 | internal static class Providers 4 | { 5 | private static QQMusic.Api? _qqMusicApi; 6 | 7 | public static QQMusic.Api QQMusicApi => _qqMusicApi ??= new(); 8 | 9 | private static Netease.Api? _neteaseApi; 10 | 11 | public static Netease.Api NeteaseApi => _neteaseApi ??= new(); 12 | 13 | private static Kugou.Api? _kugouApi; 14 | 15 | public static Kugou.Api KugouApi => _kugouApi ??= new(); 16 | 17 | private static Musixmatch.Api? _musixmatchApi; 18 | 19 | public static Musixmatch.Api MusixmatchApi => _musixmatchApi ??= new(); 20 | 21 | private static SodaMusic.Api? _sodaMusicApi; 22 | 23 | public static SodaMusic.Api SodaMusicApi => _sodaMusicApi ??= new(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/Types/LyricifyLines.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Lyricify.Lyrics.Helpers.Types 4 | { 5 | public static class LyricifyLines 6 | { 7 | public static bool IsLyricifyLines(string input) 8 | { 9 | if (input == null) return false; 10 | 11 | if (input.Contains("[type:LyricifyLines]")) return true; 12 | 13 | var regex = new Regex(@"^\[\d+,\d+\].*"); 14 | var matches = regex.Matches(input); 15 | if (matches.Count > 0) 16 | { 17 | var syllableRegex = new Regex(@"\w+\(\d+,\d+\)"); 18 | var syllableMatches = syllableRegex.Matches(input); 19 | return syllableMatches.Count <= matches.Count; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/SyncTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | /// 4 | /// Lyrics type enumerates 5 | /// 6 | public enum SyncTypes 7 | { 8 | /// 9 | /// Sync type unknown 10 | /// 11 | Unknown = 0, 12 | 13 | /// 14 | /// Lyrics lines are syllable synced 15 | /// 16 | SyllableSynced = 1, 17 | 18 | /// 19 | /// Lyrics are line synced 20 | /// 21 | LineSynced = 2, 22 | 23 | /// 24 | /// Lyrics line are mixed synced (contains both line and syllable sync) 25 | /// 26 | MixedSynced = 3, 27 | 28 | /// 29 | /// Lyrics are not synced 30 | /// 31 | Unsynced = 4, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/SearcherHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers 2 | { 3 | /// 4 | /// 实例化搜索提供者的静态类 5 | /// 6 | public static class SearcherHelper 7 | { 8 | private static QQMusicSearcher? _qqMusicSearcher; 9 | 10 | public static QQMusicSearcher QQMusicSearcher => _qqMusicSearcher ??= new(); 11 | 12 | private static NeteaseSearcher? _neteaseSearcher; 13 | 14 | public static NeteaseSearcher NeteaseSearcher => _neteaseSearcher ??= new(); 15 | 16 | private static KugouSearcher? _kugouSearcher; 17 | 18 | public static KugouSearcher KugouSearcher => _kugouSearcher ??= new(); 19 | 20 | private static MusixmatchSearcher? _musixmatchSearcher; 21 | 22 | public static MusixmatchSearcher MusixmatchSearcher => _musixmatchSearcher ??= new(); 23 | 24 | private static SodaMusicSearcher? _sodaMusicSearcher; 25 | 26 | public static SodaMusicSearcher SodaMusicSearcher => _sodaMusicSearcher ??= new(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/ITrackMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public interface ITrackMetadata 4 | { 5 | /// 6 | /// Title of the track 7 | /// 8 | public string? Title { get; set; } 9 | 10 | /// 11 | /// Artist(s) of the track 12 | /// 13 | public string? Artist { get; set; } 14 | 15 | /// 16 | /// Album name of the track 17 | /// 18 | public string? Album { get; set; } 19 | 20 | /// 21 | /// Artist(s) of the album 22 | /// 23 | public string? AlbumArtist { get; set; } 24 | 25 | /// 26 | /// Track length in milliseconds 27 | /// 28 | public int? DurationMs { get; set; } 29 | 30 | /// 31 | /// ISRC of the track 32 | /// 33 | public string? Isrc { get; set; } 34 | 35 | /// 36 | /// Languages of the lyrics 37 | /// 38 | public List? Language { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/SodaMusicSearcher.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers 2 | { 3 | public class SodaMusicSearcher : Searcher, ISearcher 4 | { 5 | public override string Name => "SodaMusic"; 6 | 7 | public override string DisplayName => "Soda Music"; 8 | 9 | public override Searchers SearcherType => Searchers.SodaMusic; 10 | 11 | public override async Task?> SearchForResults(string searchString) 12 | { 13 | var search = new List(); 14 | 15 | try 16 | { 17 | var result = await Providers.Web.Providers.SodaMusicApi.Search(searchString); 18 | var resultDataList = result?.ResultGroups[0]?.Data; 19 | if (resultDataList == null) return null; 20 | foreach (var resultData in resultDataList) 21 | { 22 | if (resultData.Meta?.ItemType != "track") continue; 23 | search.Add(new SodaMusicSearchResult(resultData)); 24 | } 25 | } 26 | catch 27 | { 28 | return null; 29 | } 30 | 31 | return search; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/Optimization/Musixmatch.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Helpers.Optimization 4 | { 5 | public static class Musixmatch 6 | { 7 | /// 8 | /// 针对 Musixmatch 逐音节歌词格式的优化 (合并空格) 9 | /// 10 | public static void StandardizeMusixmatchLyrics(List list) 11 | { 12 | foreach (ILineInfo line in list) 13 | { 14 | if (line is SyllableLineInfo syllableLine) 15 | { 16 | StandardizeMusixmatchLyrics(syllableLine); 17 | } 18 | } 19 | } 20 | 21 | /// 22 | /// 针对 Musixmatch 逐音节歌词格式的优化 (合并空格) 23 | ///
注意:此方法无法处理经过逐音节合并后的单词,会抛出 InvalidCastException 异常。 24 | ///
25 | public static void StandardizeMusixmatchLyrics(SyllableLineInfo syllableLine) 26 | { 27 | for (int i = 1; i < syllableLine.Syllables.Count; i++) 28 | { 29 | if (syllableLine.Syllables[i].Text == " ") 30 | { 31 | ((SyllableInfo)syllableLine.Syllables[i - 1]).Text += " "; 32 | syllableLine.Syllables.RemoveAt(i--); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/NeteaseSearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.Netease; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class NeteaseSearchResult : ISearchResult 7 | { 8 | public ISearcher Searcher => new NeteaseSearcher(); 9 | 10 | public NeteaseSearchResult(string title, string[] artists, string album, string[]? albumArtists, int durationMs, string id) 11 | { 12 | Title = title; 13 | Artists = artists; 14 | Album = album; 15 | AlbumArtists = albumArtists; 16 | DurationMs = durationMs; 17 | Id = id; 18 | } 19 | 20 | public NeteaseSearchResult(Song song) : this( 21 | song.Name, 22 | song.Artists.Select(s => s.Name).ToArray(), 23 | song.Album.Name, 24 | null, 25 | (int)song.Duration, 26 | song.Id 27 | ) 28 | { } 29 | 30 | public string Title { get; } 31 | 32 | public string[] Artists { get; } 33 | 34 | public string Album { get; } 35 | 36 | public string Id { get; } 37 | 38 | public string[]? AlbumArtists { get; } 39 | 40 | public int? DurationMs { get; } 41 | 42 | public CompareHelper.MatchType? MatchType { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/GeneratorHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Helpers 2 | { 3 | /// 4 | /// 生成帮助类 5 | /// 6 | public static class GenerateHelper 7 | { 8 | /// 9 | /// 生成歌词字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 需要生成的歌词字符串的类型 13 | /// 生成出的歌词字符串, 若为空或生成失败 14 | public static string? GenerateString(Models.LyricsData lyrics, Models.LyricsTypes lyricsType) 15 | { 16 | var result = lyricsType switch 17 | { 18 | Models.LyricsTypes.LyricifySyllable => Generators.LyricifySyllableGenerator.Generate(lyrics), 19 | Models.LyricsTypes.LyricifyLines => Generators.LyricifyLinesGenerator.Generate(lyrics), 20 | Models.LyricsTypes.Lrc => Generators.LrcGenerator.Generate(lyrics), 21 | Models.LyricsTypes.Qrc => Generators.QrcGenerator.Generate(lyrics), 22 | Models.LyricsTypes.Krc => Generators.KrcGenerator.Generate(lyrics), 23 | Models.LyricsTypes.Yrc => Generators.YrcGenerator.Generate(lyrics), 24 | _ => null, 25 | }; 26 | if (string.IsNullOrWhiteSpace(result)) return null; 27 | return result; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/KugouSearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.Kugou; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class KugouSearchResult : ISearchResult 7 | { 8 | public ISearcher Searcher => new KugouSearcher(); 9 | 10 | public KugouSearchResult(string title, string[] artists, string album, string[]? albumArtists, int durationMs, string hash) 11 | { 12 | Title = title; 13 | Artists = artists; 14 | Album = album; 15 | AlbumArtists = albumArtists; 16 | DurationMs = durationMs; 17 | Hash = hash; 18 | } 19 | 20 | public KugouSearchResult(SearchSongResponse.DataItem.InfoItem song) : this( 21 | song.SongName, 22 | song.SingerName.Split('、'), 23 | song.AlbumName, // 很可能会包含中文译名 24 | null, 25 | song.Duration * 1000, 26 | song.Hash 27 | ) 28 | { } 29 | 30 | public string Title { get; } 31 | 32 | public string[] Artists { get; } 33 | 34 | public string Album { get; } 35 | 36 | public string Hash { get; } 37 | 38 | public string[]? AlbumArtists { get; } 39 | 40 | public int? DurationMs { get; } 41 | 42 | public CompareHelper.MatchType? MatchType { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/Kugou/Api.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Lyricify.Lyrics.Providers.Web.Kugou 4 | { 5 | public class Api : BaseApi 6 | { 7 | protected override string? HttpRefer => null; 8 | 9 | protected override Dictionary? AdditionalHeaders => null; 10 | 11 | public async Task GetSearchSong(string keywords) 12 | { 13 | var response = await HttpClient.GetStringAsync($"http://mobilecdn.kugou.com/api/v3/search/song?format=json&keyword={keywords}&page=1&pagesize=20&showtype=1"); 14 | var resp = JsonConvert.DeserializeObject(response); 15 | return resp; 16 | } 17 | 18 | public async Task GetSearchLyrics(string? keywords = null, int? duration = null, string? hash = null) 19 | { 20 | string durationPara = string.Empty; 21 | if (duration != null) 22 | { 23 | durationPara = $"&duration={duration}"; 24 | } 25 | hash ??= string.Empty; 26 | var response = await HttpClient.GetStringAsync($"https://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword={keywords}{durationPara}&hash={hash}"); 27 | var resp = JsonConvert.DeserializeObject(response); 28 | return resp; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/KugouSearcher.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers 2 | { 3 | public class KugouSearcher : Searcher, ISearcher 4 | { 5 | public override string Name => "Kugou"; 6 | 7 | public override string DisplayName => "Kugou Music"; 8 | 9 | public override Searchers SearcherType => Searchers.Kugou; 10 | 11 | public override async Task?> SearchForResults(string searchString) 12 | { 13 | var search = new List(); 14 | 15 | try 16 | { 17 | var result = await Providers.Web.Providers.KugouApi.GetSearchSong(searchString); 18 | var results = result?.Data?.Info; 19 | if (results == null) return null; 20 | foreach (var track in results) 21 | { 22 | search.Add(new KugouSearchResult(track)); 23 | if (track.Group is { Count: > 0 } group) 24 | { 25 | foreach (var subTrack in group) 26 | { 27 | search.Add(new KugouSearchResult(subTrack)); 28 | } 29 | } 30 | } 31 | } 32 | catch 33 | { 34 | return null; 35 | } 36 | 37 | return search; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Krc/Model.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Lyricify.Lyrics.Decrypter.Krc 4 | { 5 | public class KugouLyricsResponse 6 | { 7 | [JsonProperty("content")] 8 | public string? Content { get; set; } 9 | 10 | [JsonProperty("info")] 11 | public string? Info { get; set; } 12 | 13 | [JsonProperty("_source")] 14 | public string? Source { get; set; } 15 | 16 | [JsonProperty("status")] 17 | public int Status { get; set; } 18 | 19 | [JsonProperty("contenttype")] 20 | public int ContentType { get; set; } 21 | 22 | [JsonProperty("error_code")] 23 | public int ErrorCode { get; set; } 24 | 25 | [JsonProperty("fmt")] 26 | public string? Format { get; set; } 27 | } 28 | 29 | public class KugouTranslation 30 | { 31 | [JsonProperty("content")] 32 | public List? Content { get; set; } 33 | 34 | public class ContentItem 35 | { 36 | [JsonProperty("language")] 37 | public int Language { get; set; } 38 | 39 | [JsonProperty("type")] 40 | public int Type { get; set; } 41 | 42 | [JsonProperty("lyricContent")] 43 | public List?>? LyricContent { get; set; } 44 | } 45 | 46 | [JsonProperty("version")] 47 | public int Version { get; set; } 48 | } 49 | } -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Krc/Decrypter.cs: -------------------------------------------------------------------------------- 1 | using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Decrypter.Krc 5 | { 6 | public class Decrypter 7 | { 8 | protected readonly static byte[] DecryptKey = { 0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69 }; 9 | 10 | /// 11 | /// 解密 KRC 歌词 12 | /// 13 | /// 加密的歌词 14 | /// 解密后的 KRC 歌词 15 | public static string? DecryptLyrics(string encryptedLyrics) 16 | { 17 | var data = Convert.FromBase64String(encryptedLyrics)[4..]; 18 | 19 | for (var i = 0; i < data.Length; ++i) 20 | { 21 | data[i] = (byte)(data[i] ^ DecryptKey[i % DecryptKey.Length]); 22 | } 23 | 24 | var res = Encoding.UTF8.GetString(SharpZipLibDecompress(data)); 25 | return res[1..]; 26 | } 27 | 28 | protected static byte[] SharpZipLibDecompress(byte[] data) 29 | { 30 | var compressed = new MemoryStream(data); 31 | var decompressed = new MemoryStream(); 32 | var inputStream = new InflaterInputStream(compressed); 33 | 34 | inputStream.CopyTo(decompressed); 35 | 36 | return decompressed.ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/Proxy.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Lyricify.Lyrics.Providers.Web 4 | { 5 | public static class Proxy 6 | { 7 | /// 8 | /// 设置代理 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static void SetProxy(string host, int port, string? username, string? password) 15 | { 16 | var handler = new HttpClientHandler 17 | { 18 | Proxy = new WebProxy(host, port) 19 | }; 20 | if (!string.IsNullOrEmpty(username)) 21 | handler.Proxy.Credentials = new NetworkCredential(username, password); 22 | 23 | BaseApi.HttpClient = new(handler); 24 | } 25 | 26 | /// 27 | /// 禁用代理,不使用系统代理 28 | /// 29 | public static void DisableProxy() 30 | { 31 | var handler = new HttpClientHandler 32 | { 33 | Proxy = null, 34 | UseProxy = false 35 | }; 36 | BaseApi.HttpClient = new(handler); 37 | } 38 | 39 | /// 40 | /// 清除代理设定 41 | /// 42 | public static void ClearProxy() 43 | { 44 | BaseApi.HttpClient = new(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/SodaMusicSearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.SodaMusic; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class SodaMusicSearchResult : ISearchResult 7 | { 8 | public ISearcher Searcher => new SodaMusicSearcher(); 9 | 10 | public SodaMusicSearchResult(string title, string[] artists, string album, string[]? albumArtists, int durationMs, string id) 11 | { 12 | Title = title; 13 | Artists = artists; 14 | Album = album; 15 | AlbumArtists = albumArtists; 16 | DurationMs = durationMs; 17 | Id = id; 18 | } 19 | 20 | public SodaMusicSearchResult(ResultGroupItem Track) : this( 21 | Track.Entity.Track.Name, 22 | Track.Entity.Track.Artists.Select(a => a.Name).ToArray(), 23 | Track.Entity.Track.Album.Name, 24 | null, 25 | (int)Track.Entity.Track.Duration, 26 | Track.Entity.Track.Id 27 | ) 28 | { } 29 | 30 | public string Title { get; } 31 | 32 | public string[] Artists { get; } 33 | 34 | public string Album { get; } 35 | 36 | public string Id { get; } 37 | 38 | public string[]? AlbumArtists { get; } 39 | 40 | public int? DurationMs { get; } 41 | 42 | public CompareHelper.MatchType? MatchType { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/QQMusicSearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.QQMusic; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class QQMusicSearchResult : ISearchResult 7 | { 8 | public ISearcher Searcher => new QQMusicSearcher(); 9 | 10 | public QQMusicSearchResult(string title, string[] artists, string album, string[]? albumArtists, int durationMs, string id, string mid) 11 | { 12 | Title = title; 13 | Artists = artists; 14 | Album = album; 15 | AlbumArtists = albumArtists; 16 | DurationMs = durationMs; 17 | Id = id; 18 | Mid = mid; 19 | } 20 | 21 | public QQMusicSearchResult(Song song) : this( 22 | song.Title, 23 | song.Singer.Select(s => s.Name).ToArray(), 24 | song.Album.Title, 25 | null, 26 | song.Interval * 1000, 27 | song.Id, 28 | song.Mid 29 | ) 30 | { } 31 | 32 | public string Title { get; } 33 | 34 | public string[] Artists { get; } 35 | 36 | public string Album { get; } 37 | 38 | public string Id { get; } 39 | 40 | public string Mid { get; } 41 | 42 | public string[]? AlbumArtists { get; } 43 | 44 | public int? DurationMs { get; } 45 | 46 | public CompareHelper.MatchType? MatchType { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/QQMusicSearcher.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.QQMusic; 2 | 3 | namespace Lyricify.Lyrics.Searchers 4 | { 5 | public class QQMusicSearcher : Searcher, ISearcher 6 | { 7 | public override string Name => "QQ Music"; 8 | 9 | public override string DisplayName => "QQ Music"; 10 | 11 | public override Searchers SearcherType => Searchers.QQMusic; 12 | 13 | public override async Task?> SearchForResults(string searchString) 14 | { 15 | var search = new List(); 16 | 17 | try 18 | { 19 | var result = await Providers.Web.Providers.QQMusicApi.Search(searchString, Api.SearchTypeEnum.SONG_ID); 20 | var results = result?.Req_1?.Data?.Body?.Song?.List; 21 | if (results == null) return null; 22 | foreach (var track in results) 23 | { 24 | search.Add(new QQMusicSearchResult(track)); 25 | if (track.Group is { Count: > 0 } group) 26 | { 27 | foreach (var subTrack in group) 28 | { 29 | search.Add(new QQMusicSearchResult(subTrack)); 30 | } 31 | } 32 | } 33 | } 34 | catch 35 | { 36 | return null; 37 | } 38 | 39 | return search; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/MusixmatchSearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.Musixmatch; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class MusixmatchSearchResult : ISearchResult 7 | { 8 | public ISearcher Searcher => new MusixmatchSearcher(); 9 | 10 | public MusixmatchSearchResult(string title, string[] artists, string album, string[]? albumArtists, int durationMs, int id, string isrc, string vanityId) 11 | { 12 | Title = title; 13 | Artists = artists; 14 | Album = album; 15 | AlbumArtists = albumArtists; 16 | DurationMs = durationMs; 17 | Id = id; 18 | Isrc = isrc; 19 | VanityId = vanityId; 20 | } 21 | 22 | public MusixmatchSearchResult(GetTrackResponse.Track track) : this( 23 | track.TrackName, 24 | track.ArtistName.Split(new string[] { " feat. ", " & " }, StringSplitOptions.RemoveEmptyEntries), 25 | track.AlbumName, 26 | null, 27 | track.TrackLength * 1000, 28 | track.TrackId, 29 | track.TrackIsrc, 30 | track.CommontrackVanityId 31 | ) 32 | { } 33 | 34 | public string Title { get; } 35 | 36 | public string[] Artists { get; } 37 | 38 | public string Album { get; } 39 | 40 | public int Id { get; } 41 | 42 | public string Isrc { get; } 43 | 44 | public string[]? AlbumArtists { get; } 45 | 46 | public int? DurationMs { get; } 47 | 48 | public string VanityId { get; set; } 49 | 50 | public CompareHelper.MatchType? MatchType { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/ISearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Searchers.Helpers; 2 | 3 | namespace Lyricify.Lyrics.Searchers 4 | { 5 | /// 6 | /// 搜索结果接口 7 | /// 8 | public interface ISearchResult 9 | { 10 | /// 11 | /// 搜索提供者 12 | /// 13 | public ISearcher Searcher { get; } 14 | 15 | /// 16 | /// 曲目名 17 | /// 18 | public string Title { get; } 19 | 20 | /// 21 | /// 艺人列表 22 | /// 23 | public string[] Artists { get; } 24 | 25 | /// 26 | /// 艺人名 27 | /// 28 | public string Artist => string.Join(", ", Artists); 29 | 30 | /// 31 | /// 专辑 32 | /// 33 | public string Album { get; } 34 | 35 | /// 36 | /// 专辑艺人列表 37 | /// 38 | public string[]? AlbumArtists { get; } 39 | 40 | /// 41 | /// 专辑艺人名 42 | /// 43 | public string? AlbumArtist => string.Join(", ", AlbumArtists ?? new string[0]); 44 | 45 | /// 46 | /// 曲目时长 47 | /// 48 | public int? DurationMs { get; } 49 | 50 | /// 51 | /// 匹配程度 52 | /// 53 | public CompareHelper.MatchType? MatchType { get; protected set; } 54 | 55 | /// 56 | /// 设置匹配程度 57 | /// 58 | /// 59 | internal void SetMatchType(CompareHelper.MatchType? matchType) 60 | { 61 | MatchType = matchType; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Lyricify Lyrics Helper.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lyricify.Lyrics.Helper", "Lyricify.Lyrics.Helper\Lyricify.Lyrics.Helper.csproj", "{746AD7F2-C4F5-48D1-B077-6DD4E6568B39}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lyricify.Lyrics.Demo", "Lyricify.Lyrics.Demo\Lyricify.Lyrics.Demo.csproj", "{BE7F184B-5228-4669-A4FB-2BA3B20C1744}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {746AD7F2-C4F5-48D1-B077-6DD4E6568B39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {746AD7F2-C4F5-48D1-B077-6DD4E6568B39}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {746AD7F2-C4F5-48D1-B077-6DD4E6568B39}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {746AD7F2-C4F5-48D1-B077-6DD4E6568B39}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {BE7F184B-5228-4669-A4FB-2BA3B20C1744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {BE7F184B-5228-4669-A4FB-2BA3B20C1744}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {BE7F184B-5228-4669-A4FB-2BA3B20C1744}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {BE7F184B-5228-4669-A4FB-2BA3B20C1744}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {19B924E3-FFBF-4514-B4C0-F50EBF136B4A} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/OffsetHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Helpers 4 | { 5 | public static class OffsetHelper 6 | { 7 | public static void AddOffset(List lines, int offset) 8 | { 9 | foreach (var line in lines) 10 | { 11 | AddOffset(line, offset); 12 | } 13 | } 14 | 15 | public static void AddOffset(ILineInfo lines, int offset) 16 | { 17 | if (lines is LineInfo lineInfo) 18 | { 19 | AddOffset(lineInfo, offset); 20 | } 21 | else if (lines is SyllableLineInfo syllableLineInfo) 22 | { 23 | AddOffset(syllableLineInfo, offset); 24 | } 25 | } 26 | 27 | public static void AddOffset(LineInfo line, int offset) 28 | { 29 | line.StartTime -= offset; 30 | line.EndTime -= offset; 31 | } 32 | 33 | public static void AddOffset(SyllableLineInfo line, int offset) 34 | { 35 | var syllables = line.Syllables; 36 | foreach (var syllable in syllables) 37 | { 38 | if (syllable is SyllableInfo syllableInfo) 39 | { 40 | syllableInfo.StartTime -= offset; 41 | syllableInfo.EndTime -= offset; 42 | } 43 | else if (syllable is FullSyllableInfo fullSyllableInfo) 44 | { 45 | foreach (var subItems in fullSyllableInfo.SubItems) 46 | { 47 | subItems.StartTime -= offset; 48 | subItems.EndTime -= offset; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/ParseHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Helpers 4 | { 5 | /// 6 | /// 解析帮助类 7 | /// 8 | public static class ParseHelper 9 | { 10 | /// 11 | /// 解析歌词 12 | /// 13 | /// 歌词字符串 14 | /// 解析后的歌词数据 15 | public static LyricsData? ParseLyrics(string lyrics) 16 | { 17 | var type = TypeHelper.GetLyricsTypes(lyrics); 18 | if (type != LyricsRawTypes.Unknown) 19 | { 20 | ParseLyrics(lyrics, type); 21 | } 22 | return null; 23 | } 24 | 25 | /// 26 | /// 解析歌词 27 | /// 28 | /// 歌词字符串 29 | /// 该歌词字符串的原始类型 30 | /// 解析后的歌词数据 31 | public static LyricsData? ParseLyrics(string lyrics, LyricsRawTypes lyricsRawType) 32 | { 33 | return lyricsRawType switch 34 | { 35 | LyricsRawTypes.LyricifySyllable => Parsers.LyricifySyllableParser.Parse(lyrics), 36 | LyricsRawTypes.LyricifyLines => Parsers.LyricifyLinesParser.Parse(lyrics), 37 | LyricsRawTypes.Lrc => Parsers.LrcParser.Parse(lyrics), 38 | LyricsRawTypes.Qrc => Parsers.QrcParser.Parse(lyrics), 39 | LyricsRawTypes.Krc => Parsers.KrcParser.Parse(lyrics), 40 | LyricsRawTypes.Yrc => Parsers.YrcParser.Parse(lyrics), 41 | LyricsRawTypes.Spotify => Parsers.SpotifyParser.Parse(lyrics), 42 | LyricsRawTypes.Musixmatch => Parsers.MusixmatchParser.Parse(lyrics), 43 | _ => null, 44 | }; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Lyricify.Lyrics.Helper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 10.0 6 | Lyricify.Lyrics 7 | enable 8 | enable 9 | true 10 | 0.1.4 11 | 0.1.4 12 | XY Wang 13 | WXRIW 14 | Lyricify Lyrics Helper 15 | https://github.com/WXRIW/Lyricify-Lyrics-Helper 16 | Lyrics toolset for Lyricify. With lyrics parsers, generators, decrypters, searchers and useful helpers. 17 | true 18 | true 19 | snupkg 20 | true 21 | 22 | 23 | 24 | AllEnabledByDefault 25 | 0.1.4 26 | 27 | icon.png 28 | https://github.com/WXRIW/Lyricify-Lyrics-Helper/ 29 | git 30 | lyrics;chinese;convert;search 31 | Apache-2.0 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Qrc/Helper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Decrypter.Qrc 2 | { 3 | public class Helper 4 | { 5 | /// 6 | /// 通过 Mid 获取解密后的歌词 7 | /// 8 | /// QQ 音乐歌曲 Mid 9 | /// 10 | public static QqLyricsResponse? GetLyricsByMid(string mid) 11 | { 12 | var song = Providers.Web.Providers.QQMusicApi.GetSong(mid).Result; 13 | if (song == null || song.Data is not { Length: > 0 }) return null; 14 | var id = song.Data?[0].Id; 15 | return Providers.Web.Providers.QQMusicApi.GetLyricsAsync(id!).Result; 16 | } 17 | 18 | /// 19 | /// 通过 Mid 获取解密后的歌词 20 | /// 21 | /// QQ 音乐歌曲 Mid 22 | /// 23 | public static async Task GetLyricsByMidAsync(string mid) 24 | { 25 | var song = await Providers.Web.Providers.QQMusicApi.GetSong(mid); 26 | if (song == null || song.Data is not { Length: > 0 }) return null; 27 | var id = song.Data?[0].Id; 28 | return await Providers.Web.Providers.QQMusicApi.GetLyricsAsync(id!); 29 | } 30 | 31 | /// 32 | /// 通过 ID 获取解密后的歌词 33 | /// 34 | /// QQ 音乐歌曲 ID 35 | /// 36 | public static QqLyricsResponse? GetLyrics(string id) 37 | { 38 | return Providers.Web.Providers.QQMusicApi.GetLyricsAsync(id).Result; 39 | } 40 | 41 | /// 42 | /// 通过 ID 获取解密后的歌词 43 | /// 44 | /// QQ 音乐歌曲 ID 45 | /// 46 | public static async Task GetLyricsAsync(string id) 47 | { 48 | return await Providers.Web.Providers.QQMusicApi.GetLyricsAsync(id); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/ISearcher.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | /// 7 | /// 搜索提供者接口 8 | /// 9 | public interface ISearcher 10 | { 11 | /// 12 | /// 搜索源名称 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// 搜索源显示名称 (in English) 18 | /// 19 | public string DisplayName { get; } 20 | 21 | public Searchers SearcherType { get; } 22 | 23 | /// 24 | /// 搜索最佳匹配的曲目 25 | /// 26 | /// 27 | /// 28 | public Task SearchForResult(ITrackMetadata track); 29 | 30 | /// 31 | /// 搜索最佳匹配的曲目 32 | /// 33 | /// 34 | /// 最低匹配要求 35 | /// 36 | public Task SearchForResult(ITrackMetadata track, CompareHelper.MatchType minimumMatch); 37 | 38 | /// 39 | /// 搜索匹配的曲目列表 40 | /// 41 | /// 42 | /// 43 | public Task> SearchForResults(ITrackMetadata track); 44 | 45 | /// 46 | /// 搜索匹配的曲目列表 47 | /// 48 | /// 49 | /// 是否是完整搜索 50 | /// 51 | public Task> SearchForResults(ITrackMetadata track, bool fullSearch); 52 | 53 | /// 54 | /// 搜索关键字的曲目列表 55 | /// 56 | /// 57 | /// 58 | public Task?> SearchForResults(string searchString); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lyricify Lyrics Helper 2 | 3 | 为 Lyricify 歌词相关功能竭力打造。 4 | 5 | ## 主要功能 6 | - 歌词解析 7 | - Lyricify Syllable 8 | - Lyricify Lines 9 | - LRC 10 | - QRC 11 | - KRC 12 | - YRC 13 | - TTML (暂不支持) 14 | - Spotify Lyrics (原始 JSON) 15 | - Musixmatch (原始 JSON) 16 | - 歌词生成 17 | - Lyricify Syllable 18 | - Lyricify Lines 19 | - LRC 20 | - QRC 21 | - KRC 22 | - YRC 23 | - 歌词歌曲搜索 24 | - QQ 音乐 25 | - 网易云音乐 26 | - 酷狗音乐 27 | - Spotify (暂不支持) 28 | - Musixmatch 29 | - 汽水音乐 30 | - 歌词处理优化 31 | - Explicit 歌词处理及修复 32 | - YRC 歌词优化 33 | - 对唱识别 (暂不支持) 34 | - 标题行识别 (暂不支持) 35 | - 歌词解密 36 | - QRC 37 | - KRC 38 | - 内嵌通用帮助类 39 | - 中文帮助类 (简繁转换等) 40 | - 字符串帮助类 41 | - 数学帮助类 42 | 43 | ## 项目架构 44 | ### Lyricify.Lyrics.Helper 45 | - Decrypter // 歌词解密相关 46 | - Krc 47 | - Qrc 48 | - Generators // 歌词生成 49 | - Helpers // 帮助静态类 50 | - General // 内嵌通用帮助 51 | - ChineseHelper // 中文帮助 52 | - StringHelper // 字符串帮助 53 | - Optimization // 歌词处理优化 54 | - Explicit // Explicit 歌词处理及修复 55 | - Yrc // YRC 歌词优化 56 | - Musixmatch // Musixmatch 歌词优化 57 | - Types // 歌词类型 58 | - Lrc // LRC 歌词类型特性 59 | - GeneratorHelper // 生成帮助 60 | - OffsetHelper // 偏移帮助 (用于对歌词添加 Offset 偏移) 61 | - ParserHelper // 解析帮助 62 | - SearchHelper // 搜索帮助 63 | - TypeHelper // 歌词类型帮助 64 | - Models // 歌词模型 65 | - Parsers // 歌词解析 66 | - Providers // 歌词提供者 67 | - Web // 提供者相关接口 68 | - Searchers // 歌曲搜索 69 | - Helpers 70 | - ArtistHelper // 艺人帮助 (艺人中英文名对照) 71 | - CompareHelper // 信息匹配帮助 72 | - SearcherHelper // 实例化的搜索类 73 | 74 | ### Lyricify.Lyrics.Demo 75 | - Program 76 | - ParsersDemo // 歌词解析演示 77 | - GeneratorsDemo // 歌词生成演示 78 | - TypeDetectorDemo // 歌词类型判断演示 79 | - SearchDemo // 歌曲搜索演示 80 | 81 | ## 感谢与支持 82 | 特别感谢 [@cnbluefire](https://github.com/cnbluefire), [@Raspberry Kan](https://github.com/Raspberry-Monster) 提供的帮助和支持。 83 | #### 感谢以下第三方代码 84 | - LyricParser (MIT License): https://github.com/HyPlayer/LyricParser 85 | - 163MusicLyrics (Apache-2.0 License): https://github.com/jitwxs/163MusicLyrics 86 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/AdditionalFileInfo.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Parsers.Models.Spotify; 2 | 3 | namespace Lyricify.Lyrics.Models 4 | { 5 | public interface IAdditionalFileInfo { } 6 | 7 | /// 8 | /// 通用附加信息 (适用于 LRC、KRC、QRC、Lyricify Syllable、Lyricify Lines 等多种歌词) 9 | /// 10 | public class GeneralAdditionalInfo : IAdditionalFileInfo 11 | { 12 | public List>? Attributes { get; set; } 13 | } 14 | 15 | /// 16 | /// KRC 附加信息 17 | /// 18 | public class KrcAdditionalInfo : GeneralAdditionalInfo 19 | { 20 | public string? Hash { get; set; } 21 | } 22 | 23 | /// 24 | /// 适用于 Spotify 歌词的附加信息 25 | /// 26 | public class SpotifyAdditionalInfo : IAdditionalFileInfo 27 | { 28 | public SpotifyAdditionalInfo() { } 29 | 30 | public SpotifyAdditionalInfo(SpotifyLyrics lyrics) 31 | { 32 | Provider = lyrics.Provider; 33 | ProviderLyricsId = lyrics.ProviderLyricsId; 34 | ProviderDisplayName = lyrics.ProviderDisplayName; 35 | if (!string.IsNullOrEmpty(lyrics.Language)) 36 | LyricsLanguage = lyrics.Language; 37 | } 38 | 39 | public SpotifyAdditionalInfo(string provider, string providerLyricsId, string providerDisplayName) 40 | { 41 | Provider = provider; 42 | ProviderLyricsId = providerLyricsId; 43 | ProviderDisplayName = providerDisplayName; 44 | } 45 | 46 | public SpotifyAdditionalInfo(string provider, string providerLyricsId, string providerDisplayName, string? language) : this(provider, providerLyricsId, providerDisplayName) 47 | { 48 | LyricsLanguage = language; 49 | } 50 | 51 | public string? Provider { get; } 52 | 53 | public string? ProviderLyricsId { get; } 54 | 55 | public string? ProviderDisplayName { get; } 56 | 57 | public string? LyricsLanguage { get; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Helpers/MatchHelpers/DurationMatch.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers.Helpers 2 | { 3 | public static partial class CompareHelper 4 | { 5 | /// 6 | /// 比较时长匹配程度 7 | /// 8 | /// 原曲目时长 9 | /// 搜索得到的曲目时长 10 | /// 时长匹配程度 11 | public static DurationMatchType? CompareDuration(int? duration1, int? duration2) 12 | { 13 | if (duration1 == null || duration2 == null || duration1 == 0 || duration2 == 0) return null; 14 | 15 | return Math.Abs(duration1.Value - duration2.Value) switch 16 | { 17 | 0 => DurationMatchType.Perfect, 18 | < 300 => DurationMatchType.VeryHigh, 19 | < 700 => DurationMatchType.High, 20 | < 1500 => DurationMatchType.Medium, 21 | < 3500 => DurationMatchType.Low, 22 | _ => DurationMatchType.NoMatch, 23 | }; 24 | } 25 | 26 | public static int GetMatchScore(this DurationMatchType matchType) 27 | { 28 | return GetMatchScore(matchType); 29 | } 30 | 31 | public static int GetMatchScore(this DurationMatchType? matchType) 32 | { 33 | return matchType switch 34 | { 35 | DurationMatchType.Perfect => 7, 36 | DurationMatchType.VeryHigh => 6, 37 | DurationMatchType.High => 5, 38 | DurationMatchType.Medium => 4, 39 | DurationMatchType.Low => 2, 40 | DurationMatchType.NoMatch => 0, 41 | _ => 0, 42 | }; 43 | } 44 | 45 | /// 46 | /// 时长匹配程度 47 | /// 48 | public enum DurationMatchType 49 | { 50 | Perfect, 51 | VeryHigh, 52 | High, 53 | Medium, 54 | Low, 55 | NoMatch = -1, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/SearchHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Searchers; 3 | using Lyricify.Lyrics.Searchers.Helpers; 4 | 5 | namespace Lyricify.Lyrics.Helpers 6 | { 7 | /// 8 | /// 搜索帮助类 9 | /// 10 | public static class SearchHelper 11 | { 12 | /// 13 | /// 搜索指定曲目的对应曲目 14 | /// 15 | /// 指定曲目 16 | /// 搜索提供者 17 | /// 对应曲目 18 | public static async Task Search(ITrackMetadata track, Searchers.Searchers searcher) 19 | => await Search(track, searcher.GetSearcher()); 20 | 21 | /// 22 | /// 搜索指定曲目的对应曲目 23 | /// 24 | /// 指定曲目 25 | /// 搜索提供者 26 | /// 最低匹配要求 27 | /// 对应曲目 28 | public static async Task Search(ITrackMetadata track, Searchers.Searchers searcher, CompareHelper.MatchType minimumMatch) 29 | => await Search(track, searcher.GetSearcher(), minimumMatch); 30 | 31 | /// 32 | /// 搜索指定曲目的对应曲目 33 | /// 34 | /// 指定曲目 35 | /// 搜索提供者 36 | /// 对应曲目 37 | public static async Task Search(ITrackMetadata track, ISearcher searcher) 38 | => await searcher.SearchForResult(track); 39 | 40 | /// 41 | /// 搜索指定曲目的对应曲目 42 | /// 43 | /// 指定曲目 44 | /// 搜索提供者 45 | /// 最低匹配要求 46 | /// 对应曲目 47 | public static async Task Search(ITrackMetadata track, ISearcher searcher, CompareHelper.MatchType minimumMatch) 48 | => await searcher.SearchForResult(track, minimumMatch); 49 | } 50 | } -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/Lyricify.Lyrics.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | Always 24 | 25 | 26 | Always 27 | 28 | 29 | Always 30 | 31 | 32 | Always 33 | 34 | 35 | Always 36 | 37 | 38 | Always 39 | 40 | 41 | Always 42 | 43 | 44 | Always 45 | 46 | 47 | Always 48 | 49 | 50 | Always 51 | 52 | 53 | Always 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/LyricifyLinesGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Generators 5 | { 6 | public static class LyricifyLinesGenerator 7 | { 8 | /// 9 | /// 生成 Lyricify Lines 字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 子行的输出方式 13 | /// 生成出的 Lyricify Lines 字符串 14 | public static string Generate(LyricsData lyricsData, SubLinesOutputType subLinesOutputType = SubLinesOutputType.InMainLine) 15 | { 16 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 17 | 18 | var sb = new StringBuilder(); 19 | var lines = lyricsData.Lines; 20 | for (int i = 0; i < lines.Count; i++) 21 | { 22 | var line = lines[i]; 23 | if (subLinesOutputType == SubLinesOutputType.InDiffLine) 24 | { 25 | AppendLine(sb, line); 26 | if (line.SubLine is not null) 27 | AppendLine(sb, line.SubLine); 28 | } 29 | else 30 | { 31 | if (line.StartTimeWithSubLine.HasValue) 32 | sb.AppendLine($"[{line.StartTimeWithSubLine},{line.EndTimeWithSubLine ?? 0}]{line.FullText}"); 33 | } 34 | 35 | } 36 | 37 | return sb.ToString(); 38 | 39 | static void AppendLine(StringBuilder sb, ILineInfo line) 40 | { 41 | if (line.StartTime.HasValue) 42 | sb.AppendLine($"[{line.StartTime},{line.EndTime ?? 0}]{line.Text}"); 43 | } 44 | } 45 | 46 | /// 47 | /// 子行的输出方式 48 | /// 49 | public enum SubLinesOutputType 50 | { 51 | /// 52 | /// 通过括号嵌在主行中 53 | /// 54 | InMainLine, 55 | 56 | /// 57 | /// 子行单独成行 58 | /// 59 | InDiffLine, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/Optimization/Yrc.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Helpers.Optimization 4 | { 5 | public static class Yrc 6 | { 7 | /// 8 | /// 针对 YRC 歌词格式的优化 9 | /// 10 | public static void StandardizeYrcLyrics(List list) 11 | { 12 | foreach (ILineInfo line in list) 13 | { 14 | if (line is SyllableLineInfo syllableLine) 15 | { 16 | StandardizeYrcLyrics(syllableLine); 17 | } 18 | } 19 | } 20 | 21 | /// 22 | /// 针对 YRC 歌词格式的优化 23 | ///
注意:此方法无法处理经过逐音节合并后的单词,会抛出 InvalidCastException 异常。 24 | ///
25 | public static void StandardizeYrcLyrics(SyllableLineInfo line) 26 | { 27 | var list = line.Syllables; 28 | 29 | // 移除最后的空格 30 | while (list.Last().Text == " ") 31 | { 32 | list.RemoveAt(list.Count - 1); 33 | } 34 | 35 | for (int i = 0; i < list.Count; i++) 36 | { 37 | // 移除空白格 38 | if (list[i].Text.Length == 0) 39 | { 40 | list.RemoveAt(i); 41 | i--; continue; 42 | } 43 | 44 | // 合并单独的空格 45 | if (list[i].Text == " ") 46 | { 47 | if (i - 1 >= 0) ((SyllableInfo)list[i - 1]).Text += list[i].Text; 48 | 49 | list.RemoveAt(i); 50 | i--; continue; 51 | } 52 | 53 | // 合并标点符号 54 | if (i > 0 && list[i].Text.Length <= 2 && 55 | (list[i].Text[0] == ',' || list[i].Text[0] == '.' 56 | || list[i].Text[0] == '?' || list[i].Text[0] == '!' 57 | || list[i].Text[0] == '\"')) 58 | { 59 | if (i - 1 >= 0) ((SyllableInfo)list[i - 1]).Text += list[i].Text; 60 | 61 | list.RemoveAt(i); 62 | i--; continue; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/LyricifyLinesParser.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | using Lyricify.Lyrics.Models; 3 | 4 | namespace Lyricify.Lyrics.Parsers 5 | { 6 | public static class LyricifyLinesParser 7 | { 8 | public static LyricsData Parse(string lyrics) 9 | { 10 | var lyricsLines = lyrics.Replace("[type:LyricifyLines]", "").Trim().Split('\n').ToList(); 11 | var data = new LyricsData 12 | { 13 | TrackMetadata = new TrackMetadata(), 14 | File = new() 15 | { 16 | SyncTypes = SyncTypes.LineSynced, 17 | Type = LyricsTypes.LyricifyLines, 18 | AdditionalInfo = new GeneralAdditionalInfo() 19 | { 20 | Attributes = new(), 21 | } 22 | } 23 | }; 24 | 25 | // 处理 Attributes 26 | var offset = AttributesHelper.ParseGeneralAttributesToLyricsData(data, lyricsLines); 27 | 28 | var lines = ParseLyrics(lyricsLines, offset); 29 | data.Lines = lines.Cast().ToList(); 30 | 31 | return data; 32 | } 33 | 34 | public static List ParseLyrics(List lines, int? offset = null) 35 | { 36 | offset ??= 0; 37 | var lyricsArray = new List(); 38 | foreach (var line in lines) 39 | { 40 | if (!line.StartsWith('[') || !line.Contains(',') || !line.Contains(']')) continue; 41 | try 42 | { 43 | int begin = int.Parse(line.Between("[", ",")); 44 | int end = int.Parse(line.Between(",", "]")); 45 | string text = line[(line.IndexOf(']') + 1)..].Trim(); 46 | lyricsArray.Add(new() 47 | { 48 | Text = text, 49 | StartTime = begin - offset, 50 | EndTime = end - offset, 51 | }); 52 | } 53 | catch { } 54 | } 55 | return lyricsArray; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/LyricsTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | /// 4 | /// Lyrics type enumerates 5 | /// 6 | public enum LyricsTypes 7 | { 8 | Unknown = 0, 9 | LyricifySyllable = 1, 10 | LyricifyLines = 2, 11 | Lrc = 3, 12 | Qrc = 4, 13 | Krc = 5, 14 | Yrc = 6, 15 | Ttml = 7, 16 | Spotify = 8, 17 | Musixmatch = 9, 18 | } 19 | 20 | /// 21 | /// Lyrics raw string type enumerates 22 | /// 23 | public enum LyricsRawTypes 24 | { 25 | /// 26 | /// Unknown lyrics 27 | /// 28 | Unknown = 0, 29 | 30 | /// 31 | /// Lyricify Syllable 32 | /// 33 | LyricifySyllable = 1, 34 | 35 | /// 36 | /// Lyricify Lines 37 | /// 38 | LyricifyLines = 2, 39 | 40 | /// 41 | /// Regular LRC 42 | /// 43 | Lrc = 3, 44 | 45 | /// 46 | /// Main QRC 47 | /// 48 | Qrc = 4, 49 | 50 | /// 51 | /// Raw QRC (in XML format) 52 | /// 53 | QrcFull = 10, 54 | 55 | /// 56 | /// Raw KRC 57 | /// 58 | Krc = 5, 59 | 60 | /// 61 | /// Main YRC 62 | /// 63 | Yrc = 6, 64 | 65 | /// 66 | /// Netease Cloud Music API raw JSON data (with translation etc) 67 | /// 68 | YrcFull = 11, 69 | 70 | /// 71 | /// TTML 72 | /// 73 | Ttml = 7, 74 | 75 | /// 76 | /// Apple Music API raw JSON data (with ID and more info) 77 | /// 78 | AppleJson = 12, 79 | 80 | /// 81 | /// Spotify Desktop Client API Color-Lyrics raw JSON data 82 | /// 83 | Spotify = 8, 84 | 85 | /// 86 | /// Musixmatch Desktop Client API raw JSON data 87 | /// 88 | Musixmatch = 9, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/SyllableInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public class SyllableInfo : ISyllableInfo 4 | { 5 | #pragma warning disable CS8618 6 | public SyllableInfo() { } 7 | #pragma warning restore CS8618 8 | 9 | public SyllableInfo(string text, int startTime, int endTime) 10 | { 11 | Text = text; 12 | StartTime = startTime; 13 | EndTime = endTime; 14 | } 15 | 16 | public string Text { get; set; } 17 | 18 | public int StartTime { get; set; } 19 | 20 | public int EndTime { get; set; } 21 | } 22 | 23 | public class FullSyllableInfo : ISyllableInfo 24 | { 25 | #pragma warning disable CS8618 26 | public FullSyllableInfo() { } 27 | #pragma warning restore CS8618 28 | 29 | public FullSyllableInfo(IEnumerable syllableInfos) 30 | { 31 | SubItems = syllableInfos.ToList(); 32 | } 33 | 34 | private string? _text = null; 35 | public string Text => _text ??= SyllableHelper.GetTextFromSyllableList(SubItems); 36 | 37 | private int? _startTime = null; 38 | public int StartTime => _startTime ??= SubItems.First().StartTime; 39 | 40 | private int? _endTime = null; 41 | public int EndTime => _endTime ??= SubItems.Last().EndTime; 42 | 43 | public List SubItems { get; set; } 44 | 45 | /// 46 | /// Refresh preloaded properties if SubItems have been updated 47 | /// 48 | public void RefreshProperties() 49 | { 50 | _text = null; 51 | _startTime = null; 52 | _endTime = null; 53 | } 54 | } 55 | 56 | public static class SyllableHelper 57 | { 58 | public static string GetTextFromSyllableList(List syllableList) => string.Concat(syllableList.Select(t => t.Text).ToArray()); 59 | 60 | public static string GetTextFromSyllableList(List syllableList) => string.Concat(syllableList.Select(t => t.Text).ToArray()); 61 | 62 | public static string GetTextFromSyllableList(List syllableList) => string.Concat(syllableList.Select(t => t.Text).ToArray()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/TrackMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public class TrackMetadata : ITrackMetadata 4 | { 5 | public string? Title { get; set; } 6 | 7 | public string? Artist { get; set; } 8 | 9 | public string? Album { get; set; } 10 | 11 | public string? AlbumArtist { get; set; } 12 | 13 | public int? DurationMs { get; set; } 14 | 15 | public string? Isrc { get; set; } 16 | 17 | public List? Language { get; set; } 18 | } 19 | 20 | public class TrackMultiArtistMetadata : ITrackMetadata 21 | { 22 | public string? Title { get; set; } 23 | 24 | public string? Artist 25 | { 26 | get => string.Join(", ", Artists); 27 | set => Artists = (value ?? string.Empty).Split(", ").ToList(); 28 | } 29 | 30 | public List Artists { get; set; } = new(); 31 | 32 | public string? Album { get; set; } 33 | 34 | public string? AlbumArtist 35 | { 36 | get => string.Join(", ", AlbumArtists); 37 | set => AlbumArtists = (value ?? string.Empty).Split(", ").ToList(); 38 | } 39 | 40 | public List AlbumArtists { get; set; } = new(); 41 | 42 | public int? DurationMs { get; set; } 43 | 44 | public string? Isrc { get; set; } 45 | 46 | public List? Language { get; set; } 47 | 48 | public static TrackMultiArtistMetadata GetTrackMultiArtistMetadata(ITrackMetadata track) 49 | { 50 | if (track is TrackMultiArtistMetadata trackMultiArtist) 51 | return trackMultiArtist; 52 | 53 | return new TrackMultiArtistMetadata 54 | { 55 | Artist = track.Artist, 56 | Album = track.Album, 57 | AlbumArtist = track.AlbumArtist, 58 | DurationMs = track.DurationMs, 59 | Isrc = track.Isrc, 60 | Language = track.Language, 61 | Title = track.Title 62 | }; 63 | } 64 | } 65 | 66 | public class SpotifyTrackMetadata : TrackMultiArtistMetadata, ITrackMetadata 67 | { 68 | /// 69 | /// Spotify ID of the track 70 | /// 71 | public string? Id { get; set; } 72 | 73 | /// 74 | /// Spotify URI of the track 75 | /// 76 | public string? Uri => "spotify:track:" + Id; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/QrcGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Generators 5 | { 6 | public static class QrcGenerator 7 | { 8 | /// 9 | /// 生成 QRC 字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 生成出的 QRC 字符串 13 | public static string Generate(LyricsData lyricsData) 14 | { 15 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 16 | 17 | var sb = new StringBuilder(); 18 | var lines = lyricsData.Lines; 19 | for (int i = 0; i < lines.Count; i++) 20 | { 21 | if (lines[i] is SyllableLineInfo line) 22 | { 23 | AppendLine(sb, line); 24 | if (line.SubLine is SyllableLineInfo subLine) 25 | AppendLine(sb, subLine, true); 26 | } 27 | } 28 | 29 | return sb.ToString(); 30 | 31 | static void AppendLine(StringBuilder sb, SyllableLineInfo line, bool isSubLine = false) 32 | { 33 | // 添加行信息 34 | sb.Append('['); 35 | sb.Append(line.StartTime); 36 | sb.Append(','); 37 | sb.Append(((ILineInfo)line).Duration); 38 | sb.Append(']'); 39 | 40 | // 添加音节信息 41 | foreach (var syllable in line.Syllables) 42 | { 43 | if (syllable is SyllableInfo syllableInfo) 44 | { 45 | Append(syllableInfo); 46 | } 47 | else if (syllable is FullSyllableInfo fullSyllableInfo) 48 | { 49 | foreach (var item in fullSyllableInfo.SubItems) 50 | { 51 | Append(item); 52 | } 53 | } 54 | } 55 | sb.AppendLine(); 56 | 57 | void Append(SyllableInfo item) 58 | { 59 | sb.Append(item.Text); 60 | sb.Append('('); 61 | sb.Append(item.StartTime); 62 | sb.Append(','); 63 | sb.Append(((ISyllableInfo)item).Duration); 64 | sb.Append(')'); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/YrcGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Generators 5 | { 6 | public static class YrcGenerator 7 | { 8 | /// 9 | /// 生成 YRC 字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 生成出的 YRC 字符串 13 | public static string Generate(LyricsData lyricsData) 14 | { 15 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 16 | 17 | var sb = new StringBuilder(); 18 | var lines = lyricsData.Lines; 19 | for (int i = 0; i < lines.Count; i++) 20 | { 21 | if (lines[i] is SyllableLineInfo line) 22 | { 23 | AppendLine(sb, line); 24 | if (line.SubLine is SyllableLineInfo subLine) 25 | AppendLine(sb, subLine, true); 26 | } 27 | } 28 | 29 | return sb.ToString(); 30 | 31 | static void AppendLine(StringBuilder sb, SyllableLineInfo line, bool isSubLine = false) 32 | { 33 | // 添加行信息 34 | sb.Append('['); 35 | sb.Append(line.StartTime); 36 | sb.Append(','); 37 | sb.Append(((ILineInfo)line).Duration); 38 | sb.Append(']'); 39 | 40 | // 添加音节信息 41 | foreach (var syllable in line.Syllables) 42 | { 43 | if (syllable is SyllableInfo syllableInfo) 44 | { 45 | Append(syllableInfo); 46 | } 47 | else if (syllable is FullSyllableInfo fullSyllableInfo) 48 | { 49 | foreach (var item in fullSyllableInfo.SubItems) 50 | { 51 | Append(item); 52 | } 53 | } 54 | } 55 | sb.AppendLine(); 56 | 57 | void Append(SyllableInfo item) 58 | { 59 | sb.Append('('); 60 | sb.Append(item.StartTime); 61 | sb.Append(','); 62 | sb.Append(((ISyllableInfo)item).Duration); 63 | sb.Append(",0)"); 64 | sb.Append(item.Text); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/SearchersHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Searchers 2 | { 3 | /// 4 | /// 搜索提供者的静态帮助类 5 | /// 6 | public static class SearchersHelper 7 | { 8 | /// 9 | /// 获取枚举的对应类实例 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static ISearcher GetSearcher(this Searchers searcher) 15 | { 16 | return searcher switch 17 | { 18 | Searchers.QQMusic => SearcherHelper.QQMusicSearcher, 19 | Searchers.Netease => SearcherHelper.NeteaseSearcher, 20 | Searchers.Kugou => SearcherHelper.KugouSearcher, 21 | Searchers.Musixmatch => SearcherHelper.MusixmatchSearcher, 22 | Searchers.SodaMusic => SearcherHelper.SodaMusicSearcher, 23 | _ => throw new NotImplementedException(), 24 | }; 25 | } 26 | 27 | /// 28 | /// 获取枚举的对应类新实例 29 | /// 30 | /// 31 | /// 32 | /// 33 | public static ISearcher GetNewSearcher(this Searchers searcher) 34 | { 35 | return searcher switch 36 | { 37 | Searchers.QQMusic => new QQMusicSearcher(), 38 | Searchers.Netease => new NeteaseSearcher(), 39 | Searchers.Kugou => new KugouSearcher(), 40 | Searchers.Musixmatch => new MusixmatchSearcher(), 41 | Searchers.SodaMusic => new SodaMusicSearcher(), 42 | _ => throw new NotImplementedException(), 43 | }; 44 | } 45 | 46 | /// 47 | /// 获取搜索类的对应枚举 48 | /// 49 | /// 50 | /// 51 | public static Searchers? GetSearchers(this ISearcher searcher) 52 | { 53 | if (searcher is QQMusicSearcher) return Searchers.QQMusic; 54 | if (searcher is NeteaseSearcher) return Searchers.Netease; 55 | if (searcher is KugouSearcher) return Searchers.Kugou; 56 | if (searcher is MusixmatchSearcher) return Searchers.Musixmatch; 57 | if (searcher is SodaMusicSearcher) return Searchers.SodaMusic; 58 | return null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/NeteaseSearcher.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Providers.Web.Netease; 2 | 3 | namespace Lyricify.Lyrics.Searchers 4 | { 5 | public class NeteaseSearcher : Searcher, ISearcher 6 | { 7 | public override string Name => "Netease"; 8 | 9 | public override string DisplayName => "Netease Cloud Music"; 10 | 11 | public override Searchers SearcherType => Searchers.Netease; 12 | 13 | private bool useNewSearchFirst = false; 14 | 15 | public override async Task?> SearchForResults(string searchString) 16 | { 17 | var search = new List(); 18 | 19 | SearchResult? result = null; 20 | if (useNewSearchFirst) 21 | { 22 | try { result = await Providers.Web.Providers.NeteaseApi.SearchNew(searchString); } 23 | catch 24 | { 25 | useNewSearchFirst = !useNewSearchFirst; 26 | try 27 | { 28 | result = await Providers.Web.Providers.NeteaseApi.Search(searchString, Api.SearchTypeEnum.SONG_ID); 29 | if (result?.Code == -460) throw new Exception(); 30 | } 31 | catch 32 | { 33 | useNewSearchFirst = !useNewSearchFirst; 34 | } 35 | } 36 | } 37 | else 38 | { 39 | try 40 | { 41 | result = await Providers.Web.Providers.NeteaseApi.Search(searchString, Api.SearchTypeEnum.SONG_ID); 42 | if (result?.Code == -460) throw new Exception(); 43 | } 44 | catch 45 | { 46 | useNewSearchFirst = !useNewSearchFirst; 47 | // 尝试新接口,可以在外网使用 48 | try { result = await Providers.Web.Providers.NeteaseApi.SearchNew(searchString); } 49 | catch { } 50 | } 51 | } 52 | 53 | try 54 | { 55 | var results = result?.Result.Songs; 56 | if (results == null) return null; 57 | foreach (var track in results) 58 | { 59 | search.Add(new NeteaseSearchResult(track)); 60 | } 61 | } 62 | catch 63 | { 64 | return null; 65 | } 66 | 67 | return search; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/KrcGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Generators 5 | { 6 | public static class KrcGenerator 7 | { 8 | /// 9 | /// 生成 YRC 字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 生成出的 YRC 字符串 13 | public static string Generate(LyricsData lyricsData) 14 | { 15 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 16 | 17 | var sb = new StringBuilder(); 18 | var lines = lyricsData.Lines; 19 | for (int i = 0; i < lines.Count; i++) 20 | { 21 | if (lines[i] is SyllableLineInfo line) 22 | { 23 | AppendLine(sb, line); 24 | if (line.SubLine is SyllableLineInfo subLine) 25 | AppendLine(sb, subLine, true); 26 | } 27 | } 28 | 29 | return sb.ToString(); 30 | 31 | static void AppendLine(StringBuilder sb, SyllableLineInfo line, bool isSubLine = false) 32 | { 33 | // 添加行信息 34 | sb.Append('['); 35 | sb.Append(line.StartTime); 36 | sb.Append(','); 37 | sb.Append(((ILineInfo)line).Duration); 38 | sb.Append(']'); 39 | 40 | // 添加音节信息 41 | foreach (var syllable in line.Syllables) 42 | { 43 | if (syllable is SyllableInfo syllableInfo) 44 | { 45 | Append(syllableInfo, line.StartTime!.Value); 46 | } 47 | else if (syllable is FullSyllableInfo fullSyllableInfo) 48 | { 49 | foreach (var item in fullSyllableInfo.SubItems) 50 | { 51 | Append(item, line.StartTime!.Value); 52 | } 53 | } 54 | } 55 | sb.AppendLine(); 56 | 57 | void Append(SyllableInfo item, int startTime) 58 | { 59 | sb.Append('<'); 60 | sb.Append(item.StartTime - startTime); 61 | sb.Append(','); 62 | sb.Append(((ISyllableInfo)item).Duration); 63 | sb.Append(",0>"); 64 | sb.Append(item.Text); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/ILineInfo.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Models 5 | { 6 | public interface ILineInfo : IComparable 7 | { 8 | public string Text { get; } 9 | 10 | public int? StartTime { get; } 11 | 12 | public int? EndTime { get; } 13 | 14 | public int? Duration => EndTime - StartTime; 15 | 16 | public int? StartTimeWithSubLine => MathHelper.Min(StartTime, SubLine?.StartTime); 17 | 18 | public int? EndTimeWithSubLine => MathHelper.Max(EndTime, SubLine?.EndTime); 19 | 20 | public int? DurationWithSubLine => EndTimeWithSubLine - StartTimeWithSubLine; 21 | 22 | public LyricsAlignment LyricsAlignment { get; } 23 | 24 | public ILineInfo? SubLine { get; } 25 | 26 | /// 27 | /// if SubLine not exist, or full lyrics with bracketed subline lyrics 28 | /// 29 | public string FullText 30 | { 31 | get 32 | { 33 | if (SubLine == null) 34 | { 35 | return Text; 36 | } 37 | else 38 | { 39 | var sb = new StringBuilder(); 40 | if (SubLine.StartTime < StartTime) 41 | { 42 | sb.Append('('); 43 | sb.Append(SubLine.Text.RemoveFrontBackBrackets()); 44 | sb.Append(") "); 45 | sb.Append(Text.Trim()); 46 | } 47 | else 48 | { 49 | sb.Append(Text.Trim()); 50 | sb.Append(" ("); 51 | sb.Append(SubLine.Text.RemoveFrontBackBrackets()); 52 | sb.Append(')'); 53 | } 54 | return sb.ToString(); 55 | } 56 | } 57 | } 58 | } 59 | 60 | public interface IFullLineInfo : ILineInfo 61 | { 62 | public Dictionary Translations { get; } 63 | 64 | public string? ChineseTranslation 65 | { 66 | get => Translations.ContainsKey("zh") ? Translations["zh"] : null; 67 | set 68 | { 69 | if (string.IsNullOrEmpty(value)) 70 | { 71 | Translations.Remove("zh"); 72 | } 73 | else 74 | { 75 | Translations["zh"] = value; 76 | } 77 | } 78 | } 79 | 80 | public string? Pronunciation { get; set; } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Qrc/Decrypter.cs: -------------------------------------------------------------------------------- 1 | using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Decrypter.Qrc 5 | { 6 | public class Decrypter 7 | { 8 | private readonly static byte[] QQKey = Encoding.ASCII.GetBytes("!@#)(*$%123ZXC!@!@#)(NHL"); 9 | 10 | /// 11 | /// 解密 QRC 歌词 12 | /// 13 | /// 加密的歌词 14 | /// 解密后的 QRC 歌词 15 | public static string? DecryptLyrics(string encryptedLyrics) 16 | { 17 | var encryptedTextByte = HexStringToByteArray(encryptedLyrics); // parse text to bites array 18 | byte[] data = new byte[encryptedTextByte.Length]; 19 | byte[][][] schedule = new byte[3][][]; 20 | for (int i = 0; i < 3; i++) 21 | { 22 | schedule[i] = new byte[16][]; 23 | for (int j = 0; j < 16; j++) 24 | { 25 | schedule[i][j] = new byte[6]; 26 | } 27 | } 28 | DESHelper.TripleDESKeySetup(QQKey, schedule, DESHelper.DECRYPT); 29 | for (int i = 0; i < encryptedTextByte.Length; i += 8) 30 | { 31 | var temp = new byte[8]; 32 | DESHelper.TripleDESCrypt(encryptedTextByte[i..], temp, schedule); 33 | for (int j = 0; j < 8; j++) 34 | { 35 | data[i + j] = temp[j]; 36 | } 37 | } 38 | 39 | Span unzip = SharpZipLibDecompress(data); 40 | 41 | // 移除字符串头部的 BOM 标识 (如果有) 42 | var utf8Bom = Encoding.UTF8.GetPreamble(); 43 | if (unzip[..utf8Bom.Length].SequenceEqual(utf8Bom)) 44 | { 45 | unzip = unzip[utf8Bom.Length..]; 46 | } 47 | 48 | var result = Encoding.UTF8.GetString(unzip); 49 | return result; 50 | } 51 | 52 | protected static byte[] SharpZipLibDecompress(byte[] data) 53 | { 54 | using var compressed = new MemoryStream(data); 55 | using var decompressed = new MemoryStream(); 56 | using var inputStream = new InflaterInputStream(compressed); 57 | 58 | inputStream.CopyTo(decompressed); 59 | 60 | return decompressed.ToArray(); 61 | } 62 | 63 | protected static byte[] HexStringToByteArray(string hexString) 64 | { 65 | int length = hexString.Length; 66 | byte[] bytes = new byte[length / 2]; 67 | for (int i = 0; i < length; i += 2) 68 | { 69 | bytes[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16); 70 | } 71 | return bytes; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/SpotifyDemo.txt: -------------------------------------------------------------------------------- 1 | {"lyrics":{"syncType":"LINE_SYNCED","lines":[{"startTimeMs":"50500","words":"Today I\u0027m not myself","syllables":[],"endTimeMs":"0"},{"startTimeMs":"54440","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"58560","words":"And you, you\u0027re someone else","syllables":[],"endTimeMs":"0"},{"startTimeMs":"63560","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"67010","words":"And all these rules don\u0027t fit","syllables":[],"endTimeMs":"0"},{"startTimeMs":"71800","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"75270","words":"And all that starts can quit","syllables":[],"endTimeMs":"0"},{"startTimeMs":"81630","words":"What a peculiar state we\u0027re in","syllables":[],"endTimeMs":"0"},{"startTimeMs":"89880","words":"What a peculiar state we\u0027re in","syllables":[],"endTimeMs":"0"},{"startTimeMs":"96780","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"100310","words":"Let\u0027s play a game where all of the lives we lead can change","syllables":[],"endTimeMs":"0"},{"startTimeMs":"113070","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"116340","words":"Let\u0027s play a game where nothing that we can see, the same","syllables":[],"endTimeMs":"0"},{"startTimeMs":"130320","words":"But we\u0027ll find other pieces to the puzzles","syllables":[],"endTimeMs":"0"},{"startTimeMs":"134910","words":"Slipping out under the locks","syllables":[],"endTimeMs":"0"},{"startTimeMs":"138860","words":"I could show you how many moves to checkmate, right now","syllables":[],"endTimeMs":"0"},{"startTimeMs":"147170","words":"We could take apart this life we\u0027re building and pack it up inside a box","syllables":[],"endTimeMs":"0"},{"startTimeMs":"155250","words":"All that really matters is we\u0027re doing it right now, right now","syllables":[],"endTimeMs":"0"},{"startTimeMs":"165690","words":"♪","syllables":[],"endTimeMs":"0"},{"startTimeMs":"229110","words":"But we\u0027ll find other pieces to the puzzles","syllables":[],"endTimeMs":"0"},{"startTimeMs":"233270","words":"Slipping out under the locks","syllables":[],"endTimeMs":"0"},{"startTimeMs":"236820","words":"I could show you how many moves to checkmate, right now","syllables":[],"endTimeMs":"0"},{"startTimeMs":"245790","words":"We could take apart this life we\u0027re building and pack it up inside a box","syllables":[],"endTimeMs":"0"},{"startTimeMs":"254130","words":"All that really matters is we\u0027re doing it right now, right now","syllables":[],"endTimeMs":"0"},{"startTimeMs":"262840","words":"","syllables":[],"endTimeMs":"0"}],"provider":"MusixMatch","providerLyricsId":"11912324","providerDisplayName":"Musixmatch","syncLyricsUri":"","isDenseTypeface":false,"alternatives":[],"language":"en","isRtlLanguage":false,"fullscreenAction":"FULLSCREEN_LYRICS"},"colors":{"background":-3453414,"text":-16777216,"highlightText":-1},"hasVocalRemoval":false} -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Krc/Helper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Lyricify.Lyrics.Decrypter.Krc 4 | { 5 | public class Helper 6 | { 7 | public readonly static HttpClient Client = new(); 8 | 9 | /// 10 | /// 通过 ID 和 AccessKey 获取解密后的歌词 11 | /// 12 | /// 13 | /// 14 | /// 15 | public static string? GetLyrics(string id, string accessKey) 16 | { 17 | var encryptedLyrics = GetEncryptedLyrics(id, accessKey); 18 | var lyrics = Decrypter.DecryptLyrics(encryptedLyrics!); 19 | return lyrics; 20 | } 21 | 22 | /// 23 | /// 通过 ID 和 AccessKey 获取加密的歌词 24 | /// 25 | /// 26 | /// 27 | /// 28 | public static string? GetEncryptedLyrics(string id, string accessKey) 29 | { 30 | var json = Client.GetStringAsync($"https://lyrics.kugou.com/download?ver=1&client=pc&id={id}&accesskey={accessKey}&fmt=krc&charset=utf8").Result; 31 | try 32 | { 33 | var response = JsonConvert.DeserializeObject(json); 34 | return response?.Content; 35 | } 36 | catch 37 | { 38 | return null; 39 | } 40 | } 41 | 42 | /// 43 | /// 通过 ID 和 AccessKey 获取解密后的歌词 44 | /// 45 | /// 46 | /// 47 | /// 48 | public static async Task GetLyricsAsync(string id, string accessKey) 49 | { 50 | var encryptedLyrics = await GetEncryptedLyricsAsync(id, accessKey); 51 | var lyrics = Decrypter.DecryptLyrics(encryptedLyrics!); 52 | return lyrics; 53 | } 54 | 55 | /// 56 | /// 通过 ID 和 AccessKey 获取加密的歌词 57 | /// 58 | /// 59 | /// 60 | /// 61 | public static async Task GetEncryptedLyricsAsync(string id, string accessKey) 62 | { 63 | var json = await Client.GetStringAsync($"https://lyrics.kugou.com/download?ver=1&client=pc&id={id}&accesskey={accessKey}&fmt=krc&charset=utf8"); 64 | try 65 | { 66 | var response = JsonConvert.DeserializeObject(json); 67 | return response?.Content; 68 | } 69 | catch 70 | { 71 | return null; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/TypeHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Helpers 4 | { 5 | public static class TypeHelper 6 | { 7 | /// 8 | /// 识别歌词的类型 9 | /// 10 | /// 歌词字符串 11 | /// , 如果没有识别成功则会返回 . 12 | public static LyricsRawTypes GetLyricsTypes(string lyrics) 13 | { 14 | return LyricsRawTypes.Unknown; 15 | } 16 | 17 | /// 18 | /// 将 LyricsRawType 转换为 LyricsType 19 | /// 20 | /// 21 | /// 22 | public static LyricsTypes GetLyricsType(this LyricsRawTypes type) => type switch 23 | { 24 | LyricsRawTypes.Unknown => LyricsTypes.Unknown, 25 | LyricsRawTypes.LyricifySyllable => LyricsTypes.LyricifySyllable, 26 | LyricsRawTypes.LyricifyLines => LyricsTypes.LyricifyLines, 27 | LyricsRawTypes.Lrc => LyricsTypes.Lrc, 28 | LyricsRawTypes.Qrc => LyricsTypes.Qrc, 29 | LyricsRawTypes.QrcFull => LyricsTypes.Qrc, 30 | LyricsRawTypes.Krc => LyricsTypes.Krc, 31 | LyricsRawTypes.Yrc => LyricsTypes.Yrc, 32 | LyricsRawTypes.YrcFull => LyricsTypes.Yrc, 33 | LyricsRawTypes.Ttml => LyricsTypes.Ttml, 34 | LyricsRawTypes.AppleJson => LyricsTypes.Ttml, 35 | LyricsRawTypes.Spotify => LyricsTypes.Spotify, 36 | LyricsRawTypes.Musixmatch => LyricsTypes.Musixmatch, 37 | _ => LyricsTypes.Unknown, 38 | }; 39 | 40 | /// 41 | /// 字符串是否是指定的歌词类型 42 | /// 43 | /// 歌词字符串 44 | /// 歌词类型 45 | public static bool IsLyricsType(string lyrics, LyricsTypes type) 46 | { 47 | return type switch 48 | { 49 | LyricsTypes.LyricifyLines => Types.LyricifyLines.IsLyricifyLines(lyrics), 50 | LyricsTypes.Lrc => Types.Lrc.IsLrc(lyrics), 51 | _ => false, // 暂不支持类型判断的,返回 false 52 | }; 53 | } 54 | 55 | /// 56 | /// 字符串的歌词类型是否在指定类型列表中 57 | /// 58 | /// 歌词字符串 59 | /// 歌词类型列表 60 | public static bool IsLyricsType(string lyrics, LyricsTypes[] types) 61 | { 62 | if (types.Length < 1) return false; 63 | 64 | foreach (var type in types) 65 | { 66 | if (IsLyricsType(lyrics, type)) 67 | { 68 | return true; 69 | } 70 | } 71 | return false; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/LyricifySyllableGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Generators 5 | { 6 | public static class LyricifySyllableGenerator 7 | { 8 | /// 9 | /// 生成 Lyricify Syllable 字符串 10 | /// 11 | /// 用于生成的源歌词数据 12 | /// 生成出的 Lyricify Syllable 字符串 13 | public static string Generate(LyricsData lyricsData) 14 | { 15 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 16 | 17 | var sb = new StringBuilder(); 18 | var lines = lyricsData.Lines; 19 | for (int i = 0; i < lines.Count; i++) 20 | { 21 | if (lines[i] is SyllableLineInfo line) 22 | { 23 | AppendLine(sb, line); 24 | if (line.SubLine is SyllableLineInfo subLine) 25 | AppendLine(sb, subLine, true); 26 | } 27 | } 28 | 29 | return sb.ToString(); 30 | 31 | static void AppendLine(StringBuilder sb, SyllableLineInfo line, bool isSubLine = false) 32 | { 33 | // 添加属性信息 34 | sb.Append('['); 35 | sb.Append(isSubLine 36 | ? line.LyricsAlignment switch 37 | { 38 | LyricsAlignment.Left => 7, 39 | LyricsAlignment.Right => 8, 40 | _ => 6, 41 | } 42 | : line.LyricsAlignment switch 43 | { 44 | LyricsAlignment.Left => 4, 45 | LyricsAlignment.Right => 5, 46 | _ => 3, 47 | }); 48 | sb.Append(']'); 49 | 50 | // 添加音节信息 51 | foreach (var syllable in line.Syllables) 52 | { 53 | if (syllable is SyllableInfo syllableInfo) 54 | { 55 | Append(syllableInfo); 56 | } 57 | else if (syllable is FullSyllableInfo fullSyllableInfo) 58 | { 59 | foreach (var item in fullSyllableInfo.SubItems) 60 | { 61 | Append(item); 62 | } 63 | } 64 | } 65 | sb.AppendLine(); 66 | 67 | void Append(SyllableInfo item) 68 | { 69 | sb.Append(item.Text); 70 | sb.Append('('); 71 | sb.Append(item.StartTime); 72 | sb.Append(','); 73 | sb.Append(((ISyllableInfo)item).Duration); 74 | sb.Append(')'); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/QrcParser.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers; 2 | using Lyricify.Lyrics.Models; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Lyricify.Lyrics.Parsers 6 | { 7 | public static class QrcParser 8 | { 9 | public static LyricsData Parse(string lyrics) 10 | { 11 | var lyricsLines = lyrics.Trim().Split('\n').ToList(); 12 | var data = new LyricsData 13 | { 14 | TrackMetadata = new TrackMetadata(), 15 | File = new() 16 | { 17 | Type = LyricsTypes.Qrc, 18 | SyncTypes = SyncTypes.SyllableSynced, 19 | AdditionalInfo = new GeneralAdditionalInfo() 20 | { 21 | Attributes = new(), 22 | } 23 | } 24 | }; 25 | 26 | // 处理 Attributes 27 | var offset = AttributesHelper.ParseGeneralAttributesToLyricsData(data, lyricsLines); 28 | 29 | // 处理歌词行 30 | var lines = ParseLyrics(lyricsLines, offset); 31 | 32 | data.Lines = lines; 33 | return data; 34 | } 35 | 36 | /// 37 | /// 解析 QRC 歌词 38 | /// 39 | public static List ParseLyrics(List lines, int? offset = null) 40 | { 41 | var list = new List(); 42 | 43 | foreach (var line in lines) 44 | { 45 | // 处理歌词行 46 | var item = ParseLyricsLine(line); 47 | if (item != null) 48 | { 49 | list.Add(item); 50 | } 51 | } 52 | 53 | var returnList = list.Cast().ToList(); 54 | if (offset.HasValue && offset.Value != 0) 55 | { 56 | OffsetHelper.AddOffset(returnList, offset.Value); 57 | } 58 | 59 | return returnList; 60 | } 61 | 62 | /// 63 | /// 解析 QRC 歌词行 64 | /// 65 | public static SyllableLineInfo? ParseLyricsLine(string line) 66 | { 67 | if (line.IndexOf(']') != -1) 68 | { 69 | line = line[(line.IndexOf("]") + 1)..]; 70 | } 71 | 72 | List lyricItems = new(); 73 | MatchCollection matches = Regex.Matches(line, @"(.*?)\((\d+),(\d+)\)"); 74 | 75 | foreach (Match match in matches.Cast()) 76 | { 77 | if (match.Groups.Count == 4) 78 | { 79 | string text = match.Groups[1].Value; 80 | int startTime = int.Parse(match.Groups[2].Value); 81 | int duration = int.Parse(match.Groups[3].Value); 82 | 83 | int endTime = startTime + duration; 84 | 85 | lyricItems.Add(new() { Text = text, StartTime = startTime, EndTime = endTime }); 86 | } 87 | } 88 | 89 | return new(lyricItems); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/MusixmatchSearcher.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | public class MusixmatchSearcher : ISearcher 7 | { 8 | public string Name => "Musixmatch"; 9 | 10 | public string DisplayName => "Musixmatch"; 11 | 12 | public Searchers SearcherType => Searchers.Musixmatch; 13 | 14 | public async Task SearchForResult(ITrackMetadata track) 15 | { 16 | var result = await SearchForResults(track); 17 | if (result is { Count: > 0 }) 18 | return result[0]; 19 | return null; 20 | } 21 | 22 | public async Task SearchForResult(ITrackMetadata track, CompareHelper.MatchType minimumMatch) 23 | { 24 | var result = await SearchForResults(track); 25 | if (result is { Count: > 0 } && (int)result[0].MatchType! >= (int)minimumMatch) 26 | return result[0]; 27 | return null; 28 | } 29 | 30 | public async Task?> SearchForResults(string track, string artist, int? duration = null) 31 | { 32 | var search = new List(); 33 | 34 | try 35 | { 36 | var result = await Providers.Web.Providers.MusixmatchApi.GetTrack(track, artist, duration / 1000); 37 | var t = result?.Message?.Body?.Track; 38 | if (t == null) return null; 39 | var r = new MusixmatchSearchResult(t) 40 | { 41 | MatchType = result!.Message.Header.Confidence switch 42 | { 43 | 1000 => CompareHelper.MatchType.Perfect, 44 | >= 950 => CompareHelper.MatchType.VeryHigh, 45 | >= 900 => CompareHelper.MatchType.High, 46 | >= 750 => CompareHelper.MatchType.PrettyHigh, 47 | >= 600 => CompareHelper.MatchType.Medium, 48 | >= 400 => CompareHelper.MatchType.Low, 49 | >= 200 => CompareHelper.MatchType.VeryLow, 50 | _ => CompareHelper.MatchType.NoMatch, 51 | } 52 | }; 53 | search.Add(r); 54 | } 55 | catch 56 | { 57 | return null; 58 | } 59 | 60 | return search; 61 | } 62 | 63 | public async Task> SearchForResults(ITrackMetadata track) 64 | { 65 | return await SearchForResults(track.Title!, track.Artist!, track.DurationMs) ?? new(); 66 | } 67 | 68 | public async Task> SearchForResults(ITrackMetadata track, bool fullSearch) 69 | { 70 | return await SearchForResults(track); 71 | } 72 | 73 | public Task?> SearchForResults(string searchString) 74 | { 75 | return SearchForResults(searchString, string.Empty); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/LyricifyLinesDemo.txt: -------------------------------------------------------------------------------- 1 | [type:LyricifyLines] 2 | [ti:Cruel Summer] 3 | [ar:Taylor Swift] 4 | [al:Lover] 5 | [offset:0] 6 | [5841,8298]Fever dream high in the quiet of the night 7 | [8298,11511]You know that I caught it (Oh yeah, you're right, I want it) 8 | [11511,13978]Bad, bad boy shiny toy with a price 9 | [13978,16986]You know that I bought it (Oh yeah, you're right, I want it) 10 | [16433,19108]Killing me slow, out the window 11 | [19108,22076]I'm always waiting for you to be waiting below 12 | [22076,24749]Devils roll the dice, angels roll their eyes 13 | [24749,27882]What doesn't kill me makes me want you more 14 | [27882,32289]And it's new, the shape of your body, it's blue 15 | [32289,36605]The feeling I got and it's, ooh, whoa-oh 16 | [36561,39383]It's a cruel summer 17 | [39383,42231]It's cool, that's what I tell 'em 18 | [42231,47866]No rules in breakable heaven but, ooh, whoa-oh 19 | [47866,51468]It's a cruel summer with you 20 | [53866,56948]Hang your head low in the glow of the vending machine 21 | [56948,59492]I'm not dying (Oh yeah, you're right, I want it) 22 | [59492,62465]Say that we'll just screw it up in these trying times 23 | [62465,64920]We're not trying (Oh yeah, you're right, I want it) 24 | [64291,67098]So cut the headlights, summer's a knife 25 | [67098,70065]I'm always waiting for you just to cut to the bone 26 | [70065,72725]Devils roll the dice, angels roll their eyes 27 | [72725,75511]And if I bleed, you'll be the last to know 28 | [75511,80303]Oh, it's new, the shape of your body, it's blue 29 | [80303,84546]The feeling I got and it's, ooh, whoa-oh 30 | [84546,87376]It's a cruel summer 31 | [87376,90217]It's cool, that's what I tell 'em 32 | [90217,95837]No rules in breakable heaven but, ooh, whoa-oh 33 | [95837,99417]It's a cruel summer with you 34 | [99384,101509]I'm drunk in the back of the car 35 | [101509,104815]And I cried like a baby coming home from the bar, oh 36 | [104815,107127]Said, "I'm fine," but it wasn't true 37 | [107127,109981]I don't wanna keep secrets just to keep you 38 | [109981,112776]And I snuck in through the garden gate 39 | [112776,116130]Every night that summer just to seal my fate, oh 40 | [116130,118600]And I scream, for whatever it's worth 41 | [118600,121953]"I love you, ain't that the worst thing you ever heard?" 42 | [121953,124100]He looks up, grinning like a devil 43 | [124100,128285]It's new, the shape of your body, it's blue 44 | [128285,132550]The feeling I got and it's, ooh, whoa-oh 45 | [132550,135367]It's a cruel summer 46 | [135367,138181]It's cool, that's what I tell 'em 47 | [138181,143813]No rules in breakable heaven but, ooh, whoa-oh 48 | [143813,147376]It's a cruel summer with you 49 | [147367,149495]I'm drunk in the back of the car 50 | [149495,152852]And I cried like a baby coming home from the bar, oh 51 | [152852,155113]Said, "I'm fine" but it wasn't true 52 | [155113,157961]I don't wanna keep secrets just to keep you 53 | [157961,160796]And I snuck in through the garden gate 54 | [160796,164137]Every night that summer just to seal my fate, oh 55 | [164137,166589]And I scream, for whatever it's worth 56 | [166589,170316]"I love you, ain't that the worst thing you ever heard?" 57 | [170316,173101]Yeah, yeah 58 | [173101,175023]Yeah, yeah 59 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/SpotifyUnsyncedDemo.txt: -------------------------------------------------------------------------------- 1 | {"lyrics":{"syncType":"UNSYNCED","lines":[{"startTimeMs":"0","words":"Six on the second hand to New Year\u0027s resolutions","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"And there\u0027s just no question what this man should do","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Take all the time lost, all the days that I cost","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Take what I took and give it back to you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time we were waiting for each other","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time I was waiting for you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"We got all these words, can\u0027t waste them on another","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"So I\u0027m straight in a straight line, running back to you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"I don\u0027t know what day it is, I had to check the paper","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"I don\u0027t know the city, but it isn\u0027t home","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"But you say I\u0027m lucky to love something that loves me","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"But I\u0027m torn as I could be, wherever I roam, hear me say","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time we were waiting for each other","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time I was waiting for you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"We got all these words, can\u0027t waste them on another","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"So I\u0027m straight in a straight line, running back to you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Yeah, oh, running back to you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Oh-oh, running back to you, yeah","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Oh, and we jumped so far","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"And we jumped so far","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"To get back where you are","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time we were waiting for each other","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"All this time I was waiting for you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"We got all this love, can\u0027t waste it on another","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"So I\u0027m straight in a straight line, running back to you","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"I\u0027m straight in a straight line, running back to you, yeah","syllables":[],"endTimeMs":"0"},{"startTimeMs":"0","words":"Straight in a straight line, running back to you","syllables":[],"endTimeMs":"0"}],"provider":"MusixMatch","providerLyricsId":"8631538","providerDisplayName":"Musixmatch","syncLyricsUri":"","isDenseTypeface":false,"alternatives":[],"language":"en","isRtlLanguage":false,"fullscreenAction":"FULLSCREEN_LYRICS"},"colors":{"background":-15368526,"text":-16777216,"highlightText":-1},"hasVocalRemoval":false} -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Helpers/CompareHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | 3 | namespace Lyricify.Lyrics.Searchers.Helpers 4 | { 5 | public static partial class CompareHelper 6 | { 7 | /// 8 | /// 比较曲目匹配程度 9 | /// 10 | /// 原曲目 11 | /// 搜索得到的曲目 12 | /// 曲目匹配程度 13 | public static MatchType CompareTrack(ITrackMetadata track, ISearchResult searchResult) 14 | { 15 | return CompareTrack(TrackMultiArtistMetadata.GetTrackMultiArtistMetadata(track), searchResult); 16 | } 17 | 18 | /// 19 | /// 比较曲目匹配程度 20 | /// 21 | /// 原曲目 22 | /// 搜索得到的曲目 23 | /// 曲目匹配程度 24 | public static MatchType CompareTrack(TrackMultiArtistMetadata track, ISearchResult searchResult) 25 | { 26 | var trackMatch = CompareName(track.Title, searchResult.Title); 27 | var artistMatch = CompareArtist(track.Artists, searchResult.Artists); 28 | var albumMatch = CompareName(track.Album, searchResult.Album); 29 | var albumArtistMatch = CompareArtist(track.AlbumArtists, searchResult.AlbumArtists); 30 | var durationMatch = CompareDuration(track.DurationMs, searchResult.DurationMs); 31 | 32 | var totalScore = 0d; 33 | totalScore += trackMatch.GetMatchScore(); 34 | totalScore += artistMatch.GetMatchScore(); 35 | totalScore += albumMatch.GetMatchScore() * 0.4; 36 | totalScore += albumArtistMatch.GetMatchScore() * 0.2; 37 | totalScore += durationMatch.GetMatchScore(); 38 | 39 | // 针对 MatchType 为 null 的进行按比例拉伸调整 40 | var nullCount = 0d; 41 | const int fullScore = 30; // 25.2 42 | nullCount += albumMatch is null ? 0.4 : 0; 43 | nullCount += albumArtistMatch is null ? 0.2 : 0; 44 | nullCount += durationMatch is null ? 1 : 0; 45 | totalScore = totalScore * fullScore / (fullScore - nullCount * 7); 46 | 47 | return totalScore switch 48 | { 49 | > 21 => MatchType.Perfect, 50 | > 19 => MatchType.VeryHigh, 51 | > 17 => MatchType.High, 52 | > 15 => MatchType.PrettyHigh, 53 | > 11 => MatchType.Medium, 54 | > 8 => MatchType.Low, 55 | > 3 => MatchType.VeryLow, 56 | _ => MatchType.NoMatch, 57 | }; 58 | } 59 | 60 | /// 61 | /// 曲目匹配程度 62 | /// 63 | public enum MatchType 64 | { 65 | Perfect = 100, 66 | VeryHigh = 99, 67 | High = 95, 68 | PrettyHigh = 90, 69 | Medium = 70, 70 | Low = 30, 71 | VeryLow = 10, 72 | NoMatch = -1, 73 | } 74 | } 75 | 76 | public class MatchTypeComparer : IComparer 77 | { 78 | public int Compare(CompareHelper.MatchType x, CompareHelper.MatchType y) 79 | { 80 | return ((int)x).CompareTo((int)y); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/SodaMusic/Api.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Lyricify.Lyrics.Providers.Web.SodaMusic 4 | { 5 | public class Api : BaseApi 6 | { 7 | protected override string HttpRefer => "https://api.qishui.com/"; 8 | 9 | protected override Dictionary? AdditionalHeaders => null; 10 | 11 | public new const string UserAgent = "LunaPC/2.6.5(197449790)"; 12 | 13 | public async Task Search(string keyword) 14 | { 15 | // search/{类型} 已知有all, track 16 | string url = $"https://api.qishui.com/luna/pc/search/track?aid=386088&app_name=®ion=&geo_region=&os_region=&sim_region=&device_id=&cdid=&iid=&version_name=&version_code=&channel=&build_mode=&network_carrier=&ac=&tz_name=&resolution=&device_platform=&device_type=&os_version=&fp=&q={Uri.EscapeDataString(keyword)}&cursor=&search_id=&search_method=input&debug_params=&from_search_id=&search_scene="; 17 | 18 | var res = await GetAsync(url); 19 | 20 | return JsonConvert.DeserializeObject(res); 21 | } 22 | 23 | public async Task GetDetail(string id) 24 | { 25 | string url = $"https://api.qishui.com/luna/pc/track_v2"; 26 | 27 | var data = new Dictionary 28 | { 29 | { "track_id", id }, 30 | { "media_type", "track" }, 31 | { "queue_type", "" }, 32 | }; 33 | 34 | try 35 | { 36 | var resp = await PostAsync(url, data); 37 | 38 | return resp.ToEntity(); 39 | } 40 | catch 41 | { 42 | return null; 43 | } 44 | } 45 | 46 | // 待后期歌词获取模块整理开源后并入 47 | //public async Task GetLyric(string id) 48 | //{ 49 | // var result = new LyricResult 50 | // { 51 | // Lyric = new LyricsData(), 52 | // Translate = new LyricsData(), 53 | // }; 54 | 55 | // var detail = await GetDetail(id); 56 | // // Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(detail, Newtonsoft.Json.Formatting.Indented)); 57 | 58 | // var lyricDetail = detail?.Lyric; 59 | // if (lyricDetail != null) 60 | // { 61 | // Enum.TryParse(lyricDetail.Type, true, out LyricsRawTypes lyricType); 62 | // result.Lyric = ParseHelper.ParseLyrics(lyricDetail.Content, lyricType); 63 | // // Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(result.Lyric, Newtonsoft.Json.Formatting.Indented)); 64 | // } 65 | 66 | // if (lyricDetail.LangTranslations.ContainsKey("ZH-HANS-CN")) 67 | // { 68 | // var lyricTrans = lyricDetail.LangTranslations["ZH-HANS-CN"]; 69 | // Enum.TryParse(lyricTrans.Type, true, out LyricsRawTypes lyricTransType); 70 | // result.Translate = ParseHelper.ParseLyrics(lyricTrans.Content, lyricTransType); 71 | // // Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(result.Translate, Newtonsoft.Json.Formatting.Indented)); 72 | // } 73 | 74 | // return result; 75 | //} 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/SpotifyParser.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Parsers.Models.Spotify; 3 | using Newtonsoft.Json; 4 | 5 | namespace Lyricify.Lyrics.Parsers 6 | { 7 | public static class SpotifyParser 8 | { 9 | public static LyricsData? Parse(string rawJson) 10 | { 11 | var colorLyrics = JsonConvert.DeserializeObject(rawJson); 12 | if (colorLyrics != null && colorLyrics.Lyrics != null) 13 | { 14 | var lyrics = ParseLyrics(colorLyrics.Lyrics); 15 | var lyricsData = new LyricsData 16 | { 17 | File = new(), 18 | Lines = lyrics, 19 | }; 20 | lyricsData.File.Type = LyricsTypes.Spotify; 21 | lyricsData.File.SyncTypes = colorLyrics.Lyrics.SyncType switch 22 | { 23 | "UNSYNCED" => SyncTypes.Unsynced, 24 | "LINE_SYNCED" => SyncTypes.LineSynced, 25 | "SYLLABLE_SYNCED" => SyncTypes.SyllableSynced, 26 | _ => SyncTypes.Unknown, 27 | }; 28 | lyricsData.File.AdditionalInfo = new SpotifyAdditionalInfo(colorLyrics.Lyrics); 29 | return lyricsData; 30 | } 31 | return null; 32 | } 33 | 34 | public static List ParseLyrics(SpotifyLyrics lyrics) 35 | { 36 | if (lyrics.SyncType == "UNSYNCED") 37 | { 38 | return ParseUnsyncedLyrics(lyrics.Lines).Cast().ToList(); 39 | } 40 | else 41 | { 42 | return ParseSyncedLyrics(lyrics.Lines); 43 | } 44 | } 45 | 46 | public static List ParseUnsyncedLyrics(List lyrics) 47 | { 48 | var list = new List(); 49 | foreach (var line in lyrics) 50 | { 51 | list.Add(new(line.Words)); 52 | } 53 | return list; 54 | } 55 | 56 | public static List ParseSyncedLyrics(List lyrics) 57 | { 58 | var list = new List(); 59 | foreach (var line in lyrics) 60 | { 61 | if (line.Syllables is { Count: > 0 }) 62 | { 63 | var syllables = new List(); 64 | int i = 0; 65 | foreach (var syllable in line.Syllables) 66 | { 67 | syllables.Add(new(line.Words[i..(i + syllable.CharsCount)], syllable.StartTime, syllable.EndTime)); 68 | i += syllable.CharsCount; 69 | } 70 | list.Add(new SyllableLineInfo(syllables.Cast().ToList())); 71 | } 72 | else 73 | { 74 | if (line.EndTime != 0) 75 | { 76 | list.Add(new LineInfo(line.Words, line.StartTime, line.EndTime)); 77 | } 78 | else 79 | { 80 | list.Add(new LineInfo(line.Words, line.StartTime)); 81 | } 82 | } 83 | } 84 | return list; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Helpers/MatchHelpers/ArtistMatch.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | 3 | namespace Lyricify.Lyrics.Searchers.Helpers 4 | { 5 | public static partial class CompareHelper 6 | { 7 | /// 8 | /// 比较艺人匹配程度 9 | /// 10 | /// 原曲目的艺人 11 | /// 搜索得到的曲目的艺人 12 | /// 艺人匹配程度 13 | public static ArtistMatchType? CompareArtist(IEnumerable? artist1, IEnumerable? artist2) 14 | { 15 | if (artist1 == null || artist2 == null) return null; 16 | 17 | var list1 = artist1.ToList(); 18 | var list2 = artist2.ToList(); 19 | 20 | // 预处理:转小写 & 中文转换 21 | for (int i = 0; i < list1.Count; i++) 22 | list1[i] = list1[i].ToLower().ToSC(true); 23 | for (int i = 0; i < list2.Count; i++) 24 | list2[i] = list2[i].ToLower().ToSC(true); 25 | 26 | // 比较匹配数量 27 | int count = 0; 28 | foreach (var art in list2) 29 | if (list1.Contains(art)) count++; 30 | 31 | if (count == list1.Count && list1.Count == list2.Count) 32 | return ArtistMatchType.Perfect; 33 | 34 | if (count + 1 >= list1.Count && list1.Count >= 2 || list1.Count > 6 && (double)count / list1.Count > 0.8) 35 | return ArtistMatchType.VeryHigh; 36 | 37 | if (count == 1 && list1.Count == 1 && list2.Count == 2) 38 | return ArtistMatchType.High; 39 | 40 | if (list1.Count > 5 && (list2[0].Contains("Various") || list2[0].Contains("群星"))) 41 | return ArtistMatchType.VeryHigh; 42 | 43 | if (list1.Count > 7 && list2.Count > 7 && (double)count / list1.Count > 0.66) 44 | return ArtistMatchType.High; 45 | 46 | if (list1.Count == 1 && list2.Count > 1 && list1[0].StartsWith(list2[0])) 47 | return ArtistMatchType.High; 48 | 49 | if (list1.Count == 1 && list2.Count > 1 && list2[0].Length > 3 && list1[0].Contains(list2[0])) 50 | return ArtistMatchType.High; 51 | 52 | if (list1.Count == 1 && list2.Count > 1 && list2[0].Length > 1 && list1[0].Contains(list2[0])) 53 | return ArtistMatchType.Medium; 54 | 55 | if (count == 1 && list1.Count == 1 && list2.Count >= 3) 56 | return ArtistMatchType.Medium; 57 | 58 | if (count >= 2) 59 | return ArtistMatchType.Low; 60 | 61 | return ArtistMatchType.NoMatch; 62 | } 63 | 64 | public static int GetMatchScore(this ArtistMatchType matchType) 65 | { 66 | return GetMatchScore(matchType); 67 | } 68 | 69 | public static int GetMatchScore(this ArtistMatchType? matchType) 70 | { 71 | return matchType switch 72 | { 73 | ArtistMatchType.Perfect => 7, 74 | ArtistMatchType.VeryHigh => 6, 75 | ArtistMatchType.High => 5, 76 | ArtistMatchType.Medium => 4, 77 | ArtistMatchType.Low => 2, 78 | ArtistMatchType.NoMatch => 0, 79 | _ => 0, 80 | }; 81 | } 82 | 83 | /// 84 | /// 艺人匹配程度 85 | /// 86 | public enum ArtistMatchType 87 | { 88 | Perfect, 89 | VeryHigh, 90 | High, 91 | Medium, 92 | Low, 93 | NoMatch = -1, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Searcher.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Searchers.Helpers; 3 | 4 | namespace Lyricify.Lyrics.Searchers 5 | { 6 | /// 7 | /// 搜索提供者抽象类,提供统一搜索方法 8 | /// 9 | public abstract class Searcher : ISearcher 10 | { 11 | public abstract string Name { get; } 12 | 13 | public abstract string DisplayName { get; } 14 | 15 | public abstract Searchers SearcherType { get; } 16 | 17 | public abstract Task?> SearchForResults(string searchString); 18 | 19 | public async Task SearchForResult(ITrackMetadata track) 20 | { 21 | var search = await SearchForResults(track); 22 | 23 | // 没有搜到时,尝试完整搜索 24 | if (search is not { Count: > 0 }) 25 | search = await SearchForResults(track, true); 26 | 27 | // 仍然没有搜到,直接返回 null 28 | if (search is not { Count: > 0 }) 29 | return null; 30 | 31 | return search[0]; 32 | } 33 | 34 | public async Task SearchForResult(ITrackMetadata track, CompareHelper.MatchType minimumMatch) 35 | { 36 | var search = await SearchForResults(track); 37 | 38 | // 没有搜到时,尝试完整搜索 39 | if (search is not { Count: > 0 } || (int)search[0].MatchType! < (int)minimumMatch) 40 | search = await SearchForResults(track, true); 41 | 42 | // 仍然没有搜到,直接返回 null 43 | if (search is not { Count: > 0 }) 44 | return null; 45 | 46 | if ((int)search[0].MatchType! >= (int)minimumMatch) 47 | return search[0]; 48 | else 49 | return null; 50 | } 51 | 52 | public async Task> SearchForResults(ITrackMetadata track) 53 | { 54 | return await SearchForResults(track, false); 55 | } 56 | 57 | public async Task> SearchForResults(ITrackMetadata track, bool fullSearch) 58 | { 59 | string searchString = $"{track.Title} {track.Artist?.Replace(", ", " ")} {track.Album}".Replace(" - ", " ").Trim(); 60 | var searchResults = new List(); 61 | 62 | var level = 1; 63 | do 64 | { 65 | var results = await SearchForResults(searchString); 66 | if (results is { Count: > 0 }) 67 | searchResults.AddRange(results); 68 | 69 | var newTitle = track.Title; 70 | if (newTitle?.Contains("(feat.") == true) 71 | newTitle = newTitle[..newTitle.IndexOf("(feat.")].Trim(); 72 | if (newTitle?.Contains(" - feat.") == true) 73 | newTitle = newTitle[..newTitle.IndexOf(" - feat.")].Trim(); 74 | 75 | if (fullSearch || results is not { Count: > 0 }) 76 | { 77 | var newSearchString = level switch 78 | { 79 | 1 => $"{newTitle} {track.Artist?.Replace(", ", " ")}".Replace(" - ", " ").Trim(), 80 | 2 => $"{newTitle}".Replace(" - ", " ").Trim(), 81 | _ => string.Empty, 82 | }; 83 | if (newSearchString != searchString) 84 | searchString = newSearchString; 85 | else 86 | break; 87 | } 88 | else 89 | { 90 | break; 91 | } 92 | 93 | } while (++level < 3); 94 | 95 | foreach (var result in searchResults) 96 | result.SetMatchType(CompareHelper.CompareTrack(track, result)); 97 | 98 | searchResults.Sort((x, y) => -((int)x.MatchType!).CompareTo((int)y.MatchType!)); 99 | 100 | return searchResults; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/LrcDemo.txt: -------------------------------------------------------------------------------- 1 | [offset:0] 2 | [00:00.000] 作词 : Ryan Tedder 3 | [00:00.835] 作曲 : Ryan Tedder 4 | [00:01.670][01:17.870]Lately, I've been, I've been losing sleep 5 | [00:04.850][01:21.390]Dreaming about the things that we could be 6 | [00:09.480][01:25.390]But baby, I've been, I've been praying hard 7 | [00:13.480][01:29.390]Said no more counting dollars 8 | [00:18.920]Yeah, we'll be counting stars 9 | [00:16.040]We'll be counting stars 10 | [00:37.720]I see this life 11 | [00:39.030]Like a swinging vine 12 | [00:40.340]Swing my heart across the line 13 | [00:42.090]And in my face is flashing signs 14 | [00:43.970]Seek it out and ye' shall find 15 | [00:46.160]Old, but I'm not that old 16 | [00:48.160]Young, but I'm not that bold 17 | [00:49.910]And I don't think the world is sold 18 | [00:51.850]I'm just doing what we're told 19 | [00:54.440]I-I-I-I feel something so right 20 | [00:58.750]doing the wrong thing 21 | [01:01.750]I-I-I-I feel something so wrong 22 | [01:06.310]doing the right thing 23 | [01:09.880]I couldn't lie, couldn't lie, couldn't lie 24 | [01:13.500]Everything that kills me makes me feel alive 25 | [01:31.210]We'll be counting stars 26 | [01:33.210]Lately, I've been, I've been losing sleep 27 | [01:37.200]Dreaming about the things that we could be 28 | [01:41.210]But baby, I've been, I've been praying hard 29 | [01:45.210]Said no more counting dollars 30 | [01:47.140]We'll be, we'll be counting stars 31 | [01:56.390]I feel the love 32 | [01:57.640]And I feel it burn 33 | [01:59.080]Down this river, every turn 34 | [02:00.950]Hope is a four-letter word 35 | [02:02.950]Make that money 36 | [02:03.640]Watch it burn 37 | [02:04.950]Old, but I'm not that old 38 | [02:06.830]Young, but I'm not that bold 39 | [02:08.700]And I don't think the world is sold 40 | [02:10.960]I'm just doing what we're told 41 | [02:13.020]I-I-I-I feel something so wrong 42 | [02:17.210]by doing the right thing 43 | [02:20.400]I couldn't lie, couldn't lie, couldn't lie 44 | [02:24.640]Everything that drowns me makes me wanna fly 45 | [02:28.640]Lately, I've been, I've been losing sleep 46 | [02:32.250]Dreaming about the things that we could be 47 | [02:36.160]But baby, I've been, I've been praying hard 48 | [02:40.100]Said no more counting dollars 49 | [02:42.030]We'll be counting stars 50 | [02:44.220]Lately, I've been, I've been losing sleep 51 | [02:48.160]Dreaming about the things that we could be 52 | [02:51.910]But baby, I've been, I've been praying hard 53 | [02:55.970]Said no more counting dollars 54 | [02:57.860]We'll be, we'll be counting stars 55 | [03:04.100]Take that money 56 | [03:04.730]Watch it burn 57 | [03:05.600]Sink in the river 58 | [03:06.480]The lessons I've learned 59 | [03:07.610]Take that money 60 | [03:08.490]Watch it burn 61 | [03:09.420]Sink in the river 62 | [03:10.420]The lessons I've learned 63 | [03:11.540]Take that money 64 | [03:12.290]Watch it burn 65 | [03:13.290]Sink in the river 66 | [03:14.290]The lessons I've learned 67 | [03:15.420]Take that money 68 | [03:16.170]Watch it burn 69 | [03:17.230]Sink in the river 70 | [03:18.170]The lessons I've learned 71 | [03:19.610]Everything that kills me makes me feel alive 72 | [03:26.670]Lately, I've been, I've been losing sleep 73 | [03:30.350]Dreaming about the things that we could be 74 | [03:34.220]But baby, I've been, I've been praying hard 75 | [03:38.290]Said no more counting dollars 76 | [03:40.040]We'll be counting stars 77 | [03:42.100]Lately, I've been, I've been losing sleep 78 | [03:46.220]Dreaming about the things that we could be 79 | [03:49.970]But baby, I've been, I've been praying hard 80 | [03:54.100]Said no more counting dollars 81 | [03:55.910]We'll be, we'll be counting stars 82 | [03:58.400]Take that money 83 | [03:58.910]Watch it burn 84 | [03:59.720]Sink in the river 85 | [04:00.650]The lessons I've learned 86 | [04:01.720]Take that money 87 | [04:02.400]Watch it burn 88 | [04:03.470]Sink in the river 89 | [04:04.530]The lessons I've learned 90 | [04:05.650]Take that money 91 | [04:06.410]Watch it burn 92 | [04:07.470]Sink in the river 93 | [04:08.340]The lessons I've learned 94 | [04:09.530]Take that money 95 | [04:10.150]Watch it burn 96 | [04:11.280]Sink in the river 97 | [04:12.350]The lessons I've learned -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/Kugou/Response.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace Lyricify.Lyrics.Providers.Web.Kugou 5 | { 6 | public class SearchSongResponse 7 | { 8 | public int Status { get; set; } 9 | 10 | public string Error { get; set; } 11 | 12 | public DataItem Data { get; set; } 13 | 14 | public class DataItem 15 | { 16 | public int Timestamp { get; set; } 17 | 18 | public int Total { get; set; } 19 | 20 | public List Info { get; set; } 21 | 22 | public class InfoItem 23 | { 24 | public string Hash { get; set; } 25 | 26 | [JsonProperty("songname")] 27 | public string SongName { get; set; } 28 | 29 | [JsonProperty("album_name")] 30 | public string AlbumName { get; set; } 31 | 32 | [JsonProperty("songname_original")] 33 | public string SongNameOriginal { get; set; } 34 | 35 | [JsonProperty("singername")] 36 | public string SingerName { get; set; } 37 | 38 | public int Duration { get; set; } 39 | 40 | [JsonProperty("filename")] 41 | public string Filename { get; set; } 42 | 43 | /// 44 | /// 歌曲组 (同歌曲的多个版本) 45 | /// 46 | public List Group { get; set; } 47 | } 48 | } 49 | 50 | [JsonProperty("errcode")] 51 | public int ErrorCode { get; set; } 52 | } 53 | 54 | public class SearchLyricsResponse 55 | { 56 | public int Status { get; set; } 57 | 58 | public string Info { get; set; } 59 | 60 | [JsonProperty("errcode")] 61 | public int ErrorCode { get; set; } 62 | 63 | [JsonProperty("errmsg")] 64 | public string ErrorMessage { get; set; } 65 | 66 | public string Keywork { get; set; } 67 | 68 | public string Proposal { get; set; } 69 | 70 | [JsonProperty("has_complete_right")] 71 | public int HasCompleteRight { get; set; } 72 | 73 | public int Ugc { get; set; } 74 | 75 | [JsonProperty("ugccount")] 76 | public int UgcCount { get; set; } 77 | 78 | public int Expire { get; set; } 79 | 80 | public List Candidates { get; set; } 81 | 82 | public class Candidate 83 | { 84 | public string Id { get; set; } 85 | 86 | [JsonProperty("product_from")] 87 | public string ProductFrom { get; set; } 88 | 89 | [JsonProperty("accesskey")] 90 | public string AccessKey { get; set; } 91 | 92 | [JsonProperty("can_score")] 93 | public bool CanScore { get; set; } 94 | 95 | public string Singer { get; set; } 96 | 97 | public string Song { get; set; } 98 | 99 | public int Duration { get; set; } 100 | 101 | public string Uid { get; set; } 102 | 103 | public string Nickname { get; set; } 104 | 105 | public string Origiuid { get; set; } 106 | 107 | public string Originame { get; set; } 108 | 109 | public string Transuid { get; set; } 110 | 111 | public string Transname { get; set; } 112 | 113 | public string Sounduid { get; set; } 114 | 115 | public string Soundname { get; set; } 116 | 117 | public string Language { get; set; } 118 | 119 | [JsonProperty("krctype")] 120 | public int KrcType { get; set; } 121 | 122 | public int Hitlayer { get; set; } 123 | 124 | public int Hitcasemask { get; set; } 125 | 126 | public int Adjust { get; set; } 127 | 128 | public int Score { get; set; } 129 | 130 | [JsonProperty("contenttype")] 131 | public int ContentType { get; set; } 132 | 133 | [JsonProperty("content_format")] 134 | public int ContentFormat { get; set; } 135 | 136 | [JsonProperty("trans_id")] 137 | public string TransId { get; set; } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/BaseApi.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text; 3 | 4 | namespace Lyricify.Lyrics.Providers.Web 5 | { 6 | public abstract class BaseApi 7 | { 8 | public static HttpClient HttpClient = new(); 9 | 10 | public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"; 11 | 12 | public const string Cookie = "os=pc;osver=Microsoft-Windows-10-Professional-build-16299.125-64bit;appver=2.0.3.131777;channel=netease;__remember_me=true"; 13 | 14 | protected abstract string? HttpRefer { get; } 15 | 16 | protected abstract Dictionary? AdditionalHeaders { get; } 17 | 18 | protected async Task GetAsync(string url) 19 | { 20 | SetRequestHeaders(); 21 | 22 | var response = await HttpClient.GetAsync(url); 23 | 24 | response.EnsureSuccessStatusCode(); 25 | var result = await response.Content.ReadAsStringAsync(); 26 | 27 | return result; 28 | } 29 | 30 | protected async Task PostAsync(string url, Dictionary paramDict) 31 | { 32 | SetRequestHeaders(); 33 | 34 | var content = new FormUrlEncodedContent(paramDict); 35 | var response = await HttpClient.PostAsync(url, content); 36 | 37 | response.EnsureSuccessStatusCode(); 38 | var result = await response.Content.ReadAsStringAsync(); 39 | 40 | return result; 41 | } 42 | 43 | protected async Task PostJsonAsync(string url, object param) 44 | { 45 | SetRequestHeaders(); 46 | 47 | var content = new StringContent(JsonConvert.SerializeObject(param), Encoding.UTF8, "application/json"); 48 | 49 | var response = await HttpClient.PostAsync(url, content); 50 | 51 | response.EnsureSuccessStatusCode(); 52 | var result = await response.Content.ReadAsStringAsync(); 53 | 54 | return result; 55 | } 56 | 57 | protected async Task PostAsync(string url, Dictionary paramDict) 58 | { 59 | SetRequestHeaders(); 60 | 61 | var jsonContent = new StringContent(paramDict.ToJson(), Encoding.UTF8, "application/json"); 62 | var response = await HttpClient.PostAsync(url, jsonContent); 63 | 64 | response.EnsureSuccessStatusCode(); 65 | var result = await response.Content.ReadAsStringAsync(); 66 | 67 | return result; 68 | } 69 | 70 | protected async Task PostAsync(string url, string param) 71 | { 72 | SetRequestHeaders(); 73 | 74 | var jsonContent = new StringContent(param, Encoding.UTF8, "application/json"); 75 | var response = await HttpClient.PostAsync(url, jsonContent); 76 | 77 | response.EnsureSuccessStatusCode(); 78 | var result = await response.Content.ReadAsStringAsync(); 79 | 80 | return result; 81 | } 82 | 83 | private void SetRequestHeaders() 84 | { 85 | HttpClient.DefaultRequestHeaders.Clear(); 86 | 87 | if (!string.IsNullOrEmpty(UserAgent)) 88 | HttpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); 89 | if (!string.IsNullOrEmpty(HttpRefer)) 90 | HttpClient.DefaultRequestHeaders.Add("Referer", HttpRefer); 91 | if (!string.IsNullOrEmpty(Cookie)) 92 | HttpClient.DefaultRequestHeaders.Add("Cookie", Cookie); 93 | 94 | if (AdditionalHeaders is not null) 95 | { 96 | foreach (var pair in AdditionalHeaders) 97 | { 98 | HttpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value); 99 | } 100 | } 101 | } 102 | } 103 | 104 | public static class JsonUtils 105 | { 106 | public static T? ToEntity(this string val) => JsonConvert.DeserializeObject(val); 107 | 108 | public static List? ToEntityList(this string val) => JsonConvert.DeserializeObject>(val); 109 | 110 | public static string? ToJson(this T entity, Formatting formatting = Formatting.None) => JsonConvert.SerializeObject(entity, formatting); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Decrypter/Qrc/XmlUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | using System.Xml; 4 | 5 | namespace Lyricify.Lyrics.Decrypter.Qrc 6 | { 7 | public static class XmlUtils 8 | { 9 | private static readonly Regex AmpRegex = new Regex("&(?![a-zA-Z]{2,6};|#[0-9]{2,4};)"); 10 | 11 | private static readonly Regex QuotRegex = 12 | new Regex( 13 | "(\\s+[\\w:.-]+\\s*=\\s*\")(([^\"]*)((\")((?!\\s+[\\w:.-]+\\s*=\\s*\"|\\s*(?:/?|\\?)>))[^\"]*)*)\""); 14 | 15 | /// 16 | /// 创建 XML DOM 17 | /// 18 | /// 19 | /// 20 | public static XmlDocument Create(string content) 21 | { 22 | content = RemoveIllegalContent(content); 23 | 24 | content = ReplaceAmp(content); 25 | 26 | var _content = ReplaceQuot(content); 27 | 28 | var doc = new XmlDocument(); 29 | 30 | try 31 | { 32 | doc.LoadXml(_content); 33 | } 34 | catch 35 | { 36 | doc.LoadXml(content); 37 | } 38 | 39 | return doc; 40 | } 41 | 42 | private static string ReplaceAmp(string content) 43 | { 44 | // replace & symbol 45 | return AmpRegex.Replace(content, "&"); 46 | } 47 | 48 | private static string ReplaceQuot(string content) 49 | { 50 | var sb = new StringBuilder(); 51 | 52 | int currentPos = 0; 53 | foreach (Match match in QuotRegex.Matches(content)) 54 | { 55 | sb.Append(content.Substring(currentPos, match.Index - currentPos)); 56 | 57 | var f = match.Result(match.Groups[1].Value + match.Groups[2].Value.Replace("\"", """)) + "\""; 58 | 59 | sb.Append(f); 60 | 61 | currentPos = match.Index + match.Length; 62 | } 63 | 64 | sb.Append(content.Substring(currentPos)); 65 | 66 | return sb.ToString(); 67 | } 68 | 69 | /// 70 | /// 移除 XML 内容中无效的部分 71 | /// 72 | /// 原始 XML 内容 73 | /// 移除后的内容 74 | private static string RemoveIllegalContent(string content) 75 | { 76 | int left = 0, i = 0; 77 | while (i < content.Length) 78 | { 79 | if (content[i] == '<') 80 | { 81 | left = i; 82 | } 83 | 84 | // 闭区间 85 | if (i > 0 && content[i] == '>' && content[i - 1] == '/') 86 | { 87 | var part = content.Substring(left, i - left + 1); 88 | 89 | // 存在有且只有一个等号 90 | if (part.Contains("=") && part.IndexOf("=") == part.LastIndexOf("=")) 91 | { 92 | // 等号和左括号之间没有空格 93 | var part1 = content.Substring(left, part.IndexOf("=")); 94 | if (!part1.Trim().Contains(" ")) 95 | { 96 | content = content.Substring(0, left) + content.Substring(i + 1); 97 | i = 0; 98 | continue; 99 | } 100 | } 101 | } 102 | 103 | i++; 104 | } 105 | 106 | return content.Trim(); 107 | } 108 | 109 | /// 110 | /// 递归查找 XML DOM 111 | /// 112 | /// 根节点 113 | /// 节点名和结果名的映射 114 | /// 结果集 115 | public static void RecursionFindElement(XmlNode xmlNode, Dictionary mappingDict, 116 | Dictionary resDict) 117 | { 118 | if (mappingDict.TryGetValue(xmlNode.Name, out var value)) 119 | { 120 | resDict[value] = xmlNode; 121 | } 122 | 123 | if (!xmlNode.HasChildNodes) 124 | { 125 | return; 126 | } 127 | 128 | for (var i = 0; i < xmlNode.ChildNodes.Count; i++) 129 | { 130 | RecursionFindElement(xmlNode.ChildNodes.Item(i), mappingDict, resDict); 131 | } 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Generators/LrcGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | using Lyricify.Lyrics.Models; 3 | using System.Text; 4 | 5 | namespace Lyricify.Lyrics.Generators 6 | { 7 | public static class LrcGenerator 8 | { 9 | /// 10 | /// 生成 LRC 字符串 11 | /// 12 | /// 用于生成的源歌词数据 13 | /// 作为行末时间的空行的输出类型 14 | /// 子行的输出方式 15 | /// 生成出的 LRC 字符串 16 | public static string Generate(LyricsData lyricsData, EndTimeOutputType endTimeOutputType = EndTimeOutputType.Huge, SubLinesOutputType subLinesOutputType = SubLinesOutputType.InMainLine) 17 | { 18 | if (lyricsData?.Lines is not { Count: > 0 }) return string.Empty; 19 | 20 | var sb = new StringBuilder(); 21 | var lines = lyricsData.Lines; 22 | 23 | for (int i = 0; i < lines.Count; i++) 24 | { 25 | var line = lines[i]; 26 | if (subLinesOutputType == SubLinesOutputType.InDiffLine) 27 | { 28 | AppendLine(line); 29 | if (ShouldAddLine(line, false, i)) 30 | AppendEmptyLine(line.EndTime!.Value); 31 | 32 | if (line.SubLine is not null) 33 | { 34 | AppendLine(line.SubLine); 35 | if (ShouldAddLine(line.SubLine, false, i)) 36 | AppendEmptyLine(line.SubLine.EndTime!.Value); 37 | } 38 | } 39 | else 40 | { 41 | AppendLineWithSub(line); 42 | if (ShouldAddLine(line, true, i)) 43 | AppendEmptyLine(line.EndTimeWithSubLine!.Value); 44 | } 45 | 46 | } 47 | 48 | return sb.ToString(); 49 | 50 | void AppendLine(ILineInfo line) 51 | { 52 | if (!line.StartTime.HasValue) 53 | return; 54 | sb.AppendLine($"[{StringHelper.FormatTimeMsToTimestampString(line.StartTime.Value)}]{line.Text}"); 55 | } 56 | 57 | void AppendLineWithSub(ILineInfo line) 58 | { 59 | if (!line.StartTimeWithSubLine.HasValue) 60 | return; 61 | sb.AppendLine($"[{StringHelper.FormatTimeMsToTimestampString(line.StartTimeWithSubLine.Value)}]{line.FullText}"); 62 | } 63 | 64 | void AppendEmptyLine(int timeStamp) 65 | { 66 | sb.AppendLine($"[{StringHelper.FormatTimeMsToTimestampString(timeStamp)}]"); 67 | } 68 | 69 | bool ShouldAddLine(ILineInfo line, bool withSub, int index) 70 | { 71 | var endTime = withSub ? line.EndTimeWithSubLine : line.EndTime; 72 | if (lines is null || endTime.HasValue == false) 73 | return false; 74 | if (line.EndTime <= 0) 75 | return false; 76 | if (endTimeOutputType == EndTimeOutputType.All) 77 | return true; 78 | if (endTimeOutputType == EndTimeOutputType.Huge) 79 | { 80 | if (index + 1 >= lines.Count) 81 | return true; 82 | if (lines[index + 1].StartTimeWithSubLine - endTime > 5000) 83 | return true; 84 | } 85 | return false; 86 | } 87 | } 88 | 89 | /// 90 | /// 作为行末时间的空行的输出类型 91 | /// 92 | public enum EndTimeOutputType 93 | { 94 | /// 95 | /// 不输出作为行末时间的空行 96 | /// 97 | None, 98 | 99 | /// 100 | /// 输出间距 (5s 以上) 较大的行末时间空行 101 | /// 102 | Huge, 103 | 104 | /// 105 | /// 输出所有行末时间空行 106 | /// 107 | All, 108 | } 109 | 110 | /// 111 | /// 子行的输出方式 112 | /// 113 | public enum SubLinesOutputType 114 | { 115 | /// 116 | /// 通过括号嵌在主行中 117 | /// 118 | InMainLine, 119 | 120 | /// 121 | /// 子行单独成行 122 | /// 123 | InDiffLine, 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/LsMixQrcDemo.txt: -------------------------------------------------------------------------------- 1 | [ti:Stop and Stare] 2 | [ar:OneRepublic] 3 | [al:Dreaming Out Loud] 4 | [by:] 5 | [offset:0] 6 | [13181,5234]This (13181,474)town (13655,275)is (13930,319)colder (14249,618)now (14867,940)I (15807,403)think (16210,344)it's (16554,322)sick (16876,276)of (17152,313)us(17465,941) 7 | [4]It's (18115,649)time (18764,336)to (19100,330)make (19430,650)our (20080,297)move (20377,688)I'm (21065,290)shaking (21355,590)off (21945,334)the (22279,345)rust(22624,971) 8 | [4]I've (23304,651)got (23955,355)my (24310,359)heart (24669,585)set (25254,926)on (26180,339)anywhere (26519,935)but (27454,362)here(27816,969) 9 | [7]I'm (28494,692)staring (29186,588)down (29827,608)myself (30435,1331)counting (31766,646)up (32412,705)the (33117,277)years(33394,1868) 10 | [4]Steady (34967,964)hands (35931,1000)just (36931,681)take (37612,641)the (38253,280)wheel(38533,1946) 11 | [40285,5534]And (40285,760)every (41045,235)glance (41280,916)is (42196,720)killing (42916,984)me(43900,1916) 12 | [45519,8989]Time (45519,647)to (46166,354)make (46520,910)one (47430,658)last (48088,682)appeal (48770,2235)for (51005,407)the (51412,288)life (51700,1120)I (52820,715)lead(53535,971) 13 | [8]Stop (54208,962)and (55170,670)stare(55840,1623) 14 | [57164,5810]I (57164,661)think (57825,320)I'm (58145,334)moving (58479,671)but (59150,296)I (59446,844)go (60290,279)nowhere(60569,2398) 15 | [62674,5255]Yeah (62674,675)I (63349,350)know (63699,325)that (64024,320)everyone (64344,1234)gets (65578,682)scared(66260,1667) 16 | [67629,7670]But (67629,576)I've (68205,304)become (68509,655)what (69164,370)I (69534,297)can't (69831,888)be (70719,2032)oh(73651,1648) 17 | [74999,3220](Stop (74999,956)and (75955,629)stare)(76584,1633) 18 | [77919,5490]You (77919,640)start (78559,355)to (78914,348)wonder (79262,597)why (79859,349)you're (80208,366)here (80574,630)not (81204,625)there(81829,1579) 19 | [83109,5547]And (83109,690)you'd (83799,314)give (84113,446)anything (84559,815)to (85374,334)get (85708,630)what's (86338,651)fair(86989,1658) 20 | [88356,4848]But (88356,663)fair (89019,310)ain't (89329,330)what (89659,310)you (89969,355)really (90324,902)need(91226,1977) 21 | [92904,8930]Oh (92904,1267)can (94171,393)you (94564,264)see (94828,319)what (95147,324)I (95471,573)see(96044,2000) 22 | [101534,5460]They're (101534,480)trying (102014,510)to (102524,410)come (102934,403)back (103337,844)all (104181,668)my (104849,355)senses (105204,902)push(106106,879) 23 | [106699,6565](Un-tie (106699,612)the (107311,476)weight (107787,683)bags (108470,974)I (109444,370)never (109814,560)thought (110374,350)I (110724,665)could)(111389,1869) 24 | [112964,5490](Steady (112964,985)feet (113949,1050)don't (114999,595)fail (115594,635)me (116229,335)now)(116564,1886) 25 | [118154,5480]I'ma (118154,1035)run (119189,895)till (120084,675)you (120759,324)can't (121083,667)walk(121750,1876) 26 | [7]Something (123334,1105)pulls (124529,780)my (125309,705)focus (126014,935)out(126949,1956) 27 | [128614,3796](And (128614,650)I'm (129264,455)standing (129719,1115)down)(130834,1574) 28 | [132110,3233]Stop (132110,958)and (133068,645)stare(133713,1623) 29 | [135043,5810]I (135043,650)think (135693,365)I'm (136058,295)moving (136353,625)but (136978,307)I (137285,334)go (137619,689)nowhere(138308,2538) 30 | [140553,5366]Yeah (140553,650)I (141203,355)know (141558,320)that (141878,314)everyone (142192,1271)gets (143463,685)scared(144148,1767) 31 | [145619,7531]But (145619,504)I've (146123,280)become (146403,605)what (147008,371)I (147379,305)can't (147684,900)be (148584,1709)oh(150293,2848) 32 | [152850,3238]Stop (152850,1003)and (153853,600)stare(154453,1633) 33 | [155788,5505]You (155788,630)start (156418,340)to (156758,320)wonder (157078,650)why (157728,340)you're (158068,346)here (158414,609)not (159023,665)there(159688,1599) 34 | [160993,5460]And (160993,615)you'd (161608,365)give (161973,340)anything (162313,880)to (163193,373)get (163566,648)what's (164214,659)fair(164873,1578) 35 | [166153,4867]But (166153,660)fair (166813,355)aren't (167168,355)what (167523,290)you (167813,325)really (168138,940)need(169078,1937) 36 | [170720,23942]Oh (170720,1578)you (172298,680)don't (172978,685)need(173663,2800) 37 | [194362,3213]Stop (194362,985)and (195347,642)stare(195989,1583) 38 | [197275,5831]I (197275,672)think (197947,374)I'm (198321,351)moving (198672,600)but (199272,325)I (199597,320)go (199917,685)nowhere(200602,2498) 39 | [202806,5291]Yeah (202806,668)I (203474,348)know (203822,335)that (204157,315)everyone (204472,1260)gets (205732,735)scared(206467,1627) 40 | [208231,4601]But (208231,470)I've (208701,165)become (208866,524)what (209390,322)I (209712,294)can't (210006,970)be(210976,1847) 41 | [212532,8689]Oh (212532,1357)do (213889,144)you (214033,140)see (214173,438)what (214611,375)I (214986,1020)see(215901,1000) -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Models/LineInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Models 2 | { 3 | public class LineInfo : ILineInfo 4 | { 5 | #pragma warning disable CS8618 6 | public LineInfo() { } 7 | #pragma warning restore CS8618 8 | 9 | public LineInfo(string text) 10 | { 11 | Text = text; 12 | } 13 | 14 | public LineInfo(string text, int startTime) 15 | { 16 | Text = text; 17 | StartTime = startTime; 18 | } 19 | 20 | public LineInfo(string text, int startTime, int? endTime) : this(text, startTime) 21 | { 22 | EndTime = endTime; 23 | } 24 | 25 | public string Text { get; set; } 26 | 27 | public int? StartTime { get; set; } 28 | 29 | public int? EndTime { get; set; } 30 | 31 | public LyricsAlignment LyricsAlignment { get; set; } = LyricsAlignment.Unspecified; 32 | 33 | public ILineInfo? SubLine { get; set; } 34 | 35 | public int CompareTo(object obj) 36 | { 37 | if (obj is ILineInfo line) 38 | { 39 | if (StartTime is null || line.StartTime is null) return 0; 40 | if (StartTime == line.StartTime) return 0; 41 | if (StartTime < line.StartTime) return -1; 42 | else return 1; 43 | } 44 | return 0; 45 | } 46 | } 47 | 48 | public class SyllableLineInfo : ILineInfo 49 | { 50 | #pragma warning disable CS8618 51 | public SyllableLineInfo() { } 52 | #pragma warning restore CS8618 53 | 54 | public SyllableLineInfo(IEnumerable syllables) 55 | { 56 | Syllables = syllables.ToList(); 57 | } 58 | 59 | private string? _text = null; 60 | public string Text => _text ??= SyllableHelper.GetTextFromSyllableList(Syllables); 61 | 62 | private int? _startTime = null; 63 | public int? StartTime => _startTime ??= Syllables.First().StartTime; 64 | 65 | private int? _endTime = null; 66 | public int? EndTime => _endTime ??= Syllables.Last().EndTime; 67 | 68 | public LyricsAlignment LyricsAlignment { get; set; } = LyricsAlignment.Unspecified; 69 | 70 | public ILineInfo? SubLine { get; set; } 71 | 72 | public List Syllables { get; set; } 73 | 74 | public bool IsSyllable => Syllables is { Count: > 0 }; 75 | 76 | public int CompareTo(object obj) 77 | { 78 | if (obj is ILineInfo line) 79 | { 80 | if (StartTime is null || line.StartTime is null) return 0; 81 | if (StartTime == line.StartTime) return 0; 82 | if (StartTime < line.StartTime) return -1; 83 | else return 1; 84 | } 85 | return 0; 86 | } 87 | 88 | /// 89 | /// Refresh preloaded properties if Syllables have been updated 90 | /// 91 | public void RefreshProperties() 92 | { 93 | _text = null; 94 | _startTime = null; 95 | _endTime = null; 96 | } 97 | } 98 | 99 | public class FullLineInfo : LineInfo, IFullLineInfo 100 | { 101 | public FullLineInfo() { } 102 | 103 | public FullLineInfo(LineInfo lineInfo) 104 | { 105 | Text = lineInfo.Text; 106 | StartTime = lineInfo.StartTime; 107 | EndTime = lineInfo.EndTime; 108 | LyricsAlignment = lineInfo.LyricsAlignment; 109 | SubLine = lineInfo.SubLine; 110 | } 111 | 112 | public Dictionary Translations { get; set; } = new(); 113 | 114 | public string? Pronunciation { get; set; } 115 | } 116 | 117 | public class FullSyllableLineInfo : SyllableLineInfo, IFullLineInfo 118 | { 119 | public FullSyllableLineInfo() { } 120 | 121 | public FullSyllableLineInfo(SyllableLineInfo lineInfo) 122 | { 123 | LyricsAlignment = lineInfo.LyricsAlignment; 124 | SubLine = lineInfo.SubLine; 125 | Syllables = lineInfo.Syllables; 126 | } 127 | 128 | public FullSyllableLineInfo(SyllableLineInfo lineInfo, string? chineseTranslation = null, string? pronunciation = null) : this(lineInfo) 129 | { 130 | if (!string.IsNullOrEmpty(chineseTranslation)) 131 | { 132 | Translations["zh"] = chineseTranslation; 133 | } 134 | 135 | if (!string.IsNullOrEmpty(pronunciation)) 136 | { 137 | Pronunciation = pronunciation; 138 | } 139 | } 140 | 141 | public Dictionary Translations { get; set; } = new(); 142 | 143 | public string? Pronunciation { get; set; } 144 | } 145 | 146 | public enum LyricsAlignment 147 | { 148 | Unspecified, 149 | Left, 150 | Right 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/Models/Spotify.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace Lyricify.Lyrics.Parsers.Models.Spotify 5 | { 6 | public class SpotifyColorLyrics 7 | { 8 | [JsonProperty("lyrics")] 9 | public SpotifyLyrics Lyrics { get; set; } 10 | 11 | [JsonProperty("colors")] 12 | public SpotifyColors Colors { get; set; } 13 | 14 | [JsonProperty("hasVocalRemoval")] 15 | public bool HasVocalRemoval { get; set; } 16 | } 17 | 18 | public class SpotifyLyrics 19 | { 20 | [JsonProperty("syncType")] 21 | public string SyncType { get; set; } 22 | 23 | [JsonProperty("lines")] 24 | public List Lines { get; set; } 25 | 26 | [JsonProperty("provider")] 27 | public string Provider { get; set; } 28 | 29 | [JsonProperty("providerLyricsId")] 30 | public string ProviderLyricsId { get; set; } 31 | 32 | [JsonProperty("providerDisplayName")] 33 | public string ProviderDisplayName { get; set; } 34 | 35 | [JsonProperty("syncLyricsUri")] 36 | public string SyncLyricsUri { get; set; } 37 | 38 | [JsonProperty("isDenseTypeface")] 39 | public bool IsDenseTypeface { get; set; } 40 | 41 | [JsonProperty("alternatives")] 42 | public List Alternatives { get; set; } 43 | 44 | [JsonProperty("language")] 45 | public string Language { get; set; } 46 | 47 | [JsonProperty("isRtlLanguage")] 48 | public bool IsRtlLanguage { get; set; } 49 | 50 | [JsonProperty("fullscreenAction")] 51 | public string FullscreenAction { get; set; } 52 | } 53 | 54 | public class SpotifyLyricsLine 55 | { 56 | [JsonProperty("startTimeMs")] 57 | public string StartTimeMs { get; set; } 58 | 59 | public int StartTime 60 | { 61 | get 62 | { 63 | try 64 | { 65 | return int.Parse(StartTimeMs); 66 | } 67 | catch 68 | { 69 | return 0; 70 | } 71 | } 72 | } 73 | 74 | [JsonProperty("endTimeMs")] 75 | public string EndTimeMs { get; set; } 76 | 77 | public int EndTime 78 | { 79 | get 80 | { 81 | try 82 | { 83 | return int.Parse(EndTimeMs); 84 | } 85 | catch 86 | { 87 | return 0; 88 | } 89 | } 90 | } 91 | 92 | [JsonProperty("words")] 93 | public string Words { get; set; } 94 | 95 | [JsonProperty("syllables")] 96 | public List Syllables { get; set; } 97 | } 98 | 99 | public class SyllableItem 100 | { 101 | [JsonProperty("startTimeMs")] 102 | public string StartTimeMs { get; set; } 103 | 104 | public int StartTime 105 | { 106 | get 107 | { 108 | try 109 | { 110 | return int.Parse(StartTimeMs); 111 | } 112 | catch 113 | { 114 | return 0; 115 | } 116 | } 117 | } 118 | 119 | [JsonProperty("endTimeMs")] 120 | public string EndTimeMs { get; set; } 121 | 122 | public int EndTime 123 | { 124 | get 125 | { 126 | try 127 | { 128 | return int.Parse(EndTimeMs); 129 | } 130 | catch 131 | { 132 | return 0; 133 | } 134 | } 135 | } 136 | 137 | [JsonProperty("numChars")] 138 | public string NumberChars { get; set; } 139 | 140 | public int CharsCount 141 | { 142 | get 143 | { 144 | try 145 | { 146 | return int.Parse(NumberChars); 147 | } 148 | catch 149 | { 150 | return 0; 151 | } 152 | } 153 | } 154 | } 155 | 156 | public class AlternativeItem 157 | { 158 | [JsonProperty("language")] 159 | public string Language { get; set; } 160 | 161 | [JsonProperty("lines")] 162 | public List Lines { get; set; } 163 | 164 | [JsonProperty("isRtlLanguage")] 165 | public bool IsRtlLanguage { get; set; } 166 | } 167 | 168 | public class SpotifyColors 169 | { 170 | [JsonProperty("background")] 171 | public int Background { get; set; } 172 | 173 | [JsonProperty("text")] 174 | public int Text { get; set; } 175 | 176 | [JsonProperty("highlightText")] 177 | public int HighlightText { get; set; } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/QrcDemo.txt: -------------------------------------------------------------------------------- 1 | [ti:Stop and Stare] 2 | [ar:OneRepublic] 3 | [al:Dreaming Out Loud] 4 | [by:] 5 | [offset:0] 6 | [0,4390]Stop(0,274) (274,274)And(548,274) (822,274)Stare(1096,274) (1370,274)-(1644,274) (1918,274)OneRepublic(2192,274) (2466,274)((2740,274)共(3014,274)和(3288,274)时(3562,274)代(3836,274))(4110,274) 7 | [4390,4390]Lyrics(4390,190) (4580,190)by(4770,190):(4960,190)Ryan(5150,190) (5340,190)Tedder(5530,190)/(5720,190)Zachary(5910,190) (6100,190)Filkins(6290,190)/(6480,190)Andrew(6670,190) (6860,190)Brown(7050,190)/(7240,190)Eddie(7430,190) (7620,190)Fisher(7810,190)/(8000,190)Tim(8190,190) (8380,190)Myers(8570,190) 8 | [8780,4390]Composed(8780,190) (8970,190)by(9160,190):(9350,190)Ryan(9540,190) (9730,190)Tedder(9920,190)/(10110,190)Zachary(10300,190) (10490,190)Filkins(10680,190)/(10870,190)Andrew(11060,190) (11250,190)Brown(11440,190)/(11630,190)Eddie(11820,190) (12010,190)Fisher(12200,190)/(12390,190)Tim(12580,190) (12770,190)Myers(12960,190) 9 | [13181,5234]This (13181,474)town (13655,275)is (13930,319)colder (14249,618)now (14867,940)I (15807,403)think (16210,344)it's (16554,322)sick (16876,276)of (17152,313)us(17465,941) 10 | [18115,5489]It's (18115,649)time (18764,336)to (19100,330)make (19430,650)our (20080,297)move (20377,688)I'm (21065,290)shaking (21355,590)off (21945,334)the (22279,345)rust(22624,971) 11 | [23304,5490]I've (23304,651)got (23955,355)my (24310,359)heart (24669,585)set (25254,926)on (26180,339)anywhere (26519,935)but (27454,362)here(27816,969) 12 | [28494,6773]I'm (28494,692)staring (29186,588)down (29827,608)myself (30435,1331)counting (31766,646)up (32412,705)the (33117,277)years(33394,1868) 13 | [34967,5520]Steady (34967,964)hands (35931,1000)just (36931,681)take (37612,641)the (38253,280)wheel(38533,1946) 14 | [40285,5534]And (40285,760)every (41045,235)glance (41280,916)is (42196,720)killing (42916,984)me(43900,1916) 15 | [45519,8989]Time (45519,647)to (46166,354)make (46520,910)one (47430,658)last (48088,682)appeal (48770,2235)for (51005,407)the (51412,288)life (51700,1120)I (52820,715)lead(53535,971) 16 | [54208,3256]Stop (54208,962)and (55170,670)stare(55840,1623) 17 | [57164,5810]I (57164,661)think (57825,320)I'm (58145,334)moving (58479,671)but (59150,296)I (59446,844)go (60290,279)nowhere(60569,2398) 18 | [62674,5255]Yeah (62674,675)I (63349,350)know (63699,325)that (64024,320)everyone (64344,1234)gets (65578,682)scared(66260,1667) 19 | [67629,7670]But (67629,576)I've (68205,304)become (68509,655)what (69164,370)I (69534,297)can't (69831,888)be (70719,2032)oh(73651,1648) 20 | [74999,3220]Stop (74999,956)and (75955,629)stare(76584,1633) 21 | [77919,5490]You (77919,640)start (78559,355)to (78914,348)wonder (79262,597)why (79859,349)you're (80208,366)here (80574,630)not (81204,625)there(81829,1579) 22 | [83109,5547]And (83109,690)you'd (83799,314)give (84113,446)anything (84559,815)to (85374,334)get (85708,630)what's (86338,651)fair(86989,1658) 23 | [88356,4848]But (88356,663)fair (89019,310)ain't (89329,330)what (89659,310)you (89969,355)really (90324,902)need(91226,1977) 24 | [92904,8930]Oh (92904,1267)can (94171,393)you (94564,264)see (94828,319)what (95147,324)I (95471,573)see(96044,2000) 25 | [101534,5460]They're (101534,480)trying (102014,510)to (102524,410)come (102934,403)back (103337,844)all (104181,668)my (104849,355)senses (105204,902)push(106106,879) 26 | [106699,6565]Un-tie (106699,612)the (107311,476)weight (107787,683)bags (108470,974)I (109444,370)never (109814,560)thought (110374,350)I (110724,665)could(111389,1869) 27 | [112964,5490]Steady (112964,985)feet (113949,1050)don't (114999,595)fail (115594,635)me (116229,335)now(116564,1886) 28 | [118154,5480]I'ma (118154,1035)run (119189,895)till (120084,675)you (120759,324)can't (121083,667)walk(121750,1876) 29 | [123334,5580]Something (123334,1105)pulls (124529,780)my (125309,705)focus (126014,935)out(126949,1956) 30 | [128614,3796]And (128614,650)I'm (129264,455)standing (129719,1115)down(130834,1574) 31 | [132110,3233]Stop (132110,958)and (133068,645)stare(133713,1623) 32 | [135043,5810]I (135043,650)think (135693,365)I'm (136058,295)moving (136353,625)but (136978,307)I (137285,334)go (137619,689)nowhere(138308,2538) 33 | [140553,5366]Yeah (140553,650)I (141203,355)know (141558,320)that (141878,314)everyone (142192,1271)gets (143463,685)scared(144148,1767) 34 | [145619,7531]But (145619,504)I've (146123,280)become (146403,605)what (147008,371)I (147379,305)can't (147684,900)be (148584,1709)oh(150293,2848) 35 | [152850,3238]Stop (152850,1003)and (153853,600)stare(154453,1633) 36 | [155788,5505]You (155788,630)start (156418,340)to (156758,320)wonder (157078,650)why (157728,340)you're (158068,346)here (158414,609)not (159023,665)there(159688,1599) 37 | [160993,5460]And (160993,615)you'd (161608,365)give (161973,340)anything (162313,880)to (163193,373)get (163566,648)what's (164214,659)fair(164873,1578) 38 | [166153,4867]But (166153,660)fair (166813,355)aren't (167168,355)what (167523,290)you (167813,325)really (168138,940)need(169078,1937) 39 | [170720,23942]Oh (170720,1578)you (172298,680)don't (172978,685)need(173663,2800) 40 | [194362,3213]Stop (194362,985)and (195347,642)stare(195989,1583) 41 | [197275,5831]I (197275,672)think (197947,374)I'm (198321,351)moving (198672,600)but (199272,325)I (199597,320)go (199917,685)nowhere(200602,2498) 42 | [202806,5291]Yeah (202806,668)I (203474,348)know (203822,335)that (204157,315)everyone (204472,1260)gets (205732,735)scared(206467,1627) 43 | [208231,4601]But (208231,470)I've (208701,165)become (208866,524)what (209390,322)I (209712,294)can't (210006,970)be(210976,1847) 44 | [212532,8689]Oh (212532,1357)do (213889,144)you (214033,140)see (214173,438)what (214611,375)I (214986,1020)see(215901,1000) -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/AttributesHelper.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | using Lyricify.Lyrics.Models; 3 | 4 | namespace Lyricify.Lyrics.Parsers 5 | { 6 | internal static class AttributesHelper 7 | { 8 | /// 9 | /// 将 Attributes 信息解析到 LyricsData 中 10 | /// 11 | /// Offset 值,若 Attributes 中没有,则为 12 | public static int? ParseGeneralAttributesToLyricsData(LyricsData data, string input, out int index) 13 | { 14 | int? offset = null; 15 | data.TrackMetadata ??= new TrackMetadata(); 16 | 17 | index = 0; 18 | for (; index < input.Length; index++) 19 | { 20 | if (input[index] == '[') 21 | { 22 | var endIndex = input.IndexOf('\n', index); 23 | var infoLine = input[index..endIndex]; 24 | if (IsAttributeLine(infoLine)) 25 | { 26 | var attribute = GetAttribute(infoLine); 27 | switch (attribute.Key) 28 | { 29 | case "ar": 30 | data.TrackMetadata.Artist = attribute.Value; 31 | break; 32 | case "al": 33 | data.TrackMetadata.Album = attribute.Value; 34 | break; 35 | case "ti": 36 | data.TrackMetadata.Title = attribute.Value; 37 | break; 38 | case "length": 39 | if (int.TryParse(attribute.Value, out int result)) 40 | data.TrackMetadata.DurationMs = result; 41 | break; 42 | case "offset": 43 | try { offset = int.Parse(attribute.Value); } catch { } 44 | break; 45 | } 46 | ((GeneralAdditionalInfo)data.File!.AdditionalInfo!).Attributes!.Add(attribute); 47 | 48 | index = endIndex; 49 | } 50 | else 51 | { 52 | break; 53 | } 54 | } 55 | else 56 | { 57 | break; 58 | } 59 | } 60 | return offset; 61 | } 62 | 63 | /// 64 | /// 将 Attributes 信息解析到 LyricsData 中 65 | /// 66 | /// Offset 值,若 Attributes 中没有,则为 67 | public static int? ParseGeneralAttributesToLyricsData(LyricsData data, List lines) 68 | { 69 | int? offset = null; 70 | data.TrackMetadata ??= new TrackMetadata(); 71 | for (int i = 0; i < lines.Count; i++) 72 | { 73 | if (IsAttributeLine(lines[i])) 74 | { 75 | var attribute = GetAttribute(lines[i]); 76 | switch (attribute.Key) 77 | { 78 | case "ar": 79 | data.TrackMetadata.Artist = attribute.Value; 80 | break; 81 | case "al": 82 | data.TrackMetadata.Album = attribute.Value; 83 | break; 84 | case "ti": 85 | data.TrackMetadata.Title = attribute.Value; 86 | break; 87 | case "length": 88 | if (int.TryParse(attribute.Value, out int result)) 89 | data.TrackMetadata.DurationMs = result; 90 | break; 91 | case "offset": 92 | try { offset = int.Parse(attribute.Value); } catch { } 93 | break; 94 | } 95 | if (attribute.Key == "hash" && data.File!.AdditionalInfo is KrcAdditionalInfo krcAdditionalInfo) 96 | krcAdditionalInfo.Hash = attribute.Value; 97 | else 98 | ((GeneralAdditionalInfo)data.File!.AdditionalInfo!).Attributes!.Add(attribute); 99 | 100 | lines.RemoveAt(i--); 101 | } 102 | else 103 | { 104 | break; 105 | } 106 | } 107 | return offset; 108 | } 109 | 110 | /// 111 | /// 是否是 Attribute 信息行 112 | /// 113 | public static bool IsAttributeLine(string line) 114 | { 115 | line = line.Trim(); // 防止 \r 干扰 116 | return line.StartsWith('[') && line.EndsWith(']') && line.Contains(':'); 117 | } 118 | 119 | /// 120 | /// 获取 Attribute 信息 121 | /// 122 | private static KeyValuePair GetAttribute(string line) 123 | { 124 | line = line.Trim(); // 防止 \r 干扰 125 | string key = line.Between("[", ":"); 126 | string value = line[(line.IndexOf(':') + 1)..^1]; 127 | return new KeyValuePair(key, value); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Helpers/General/MathHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lyricify.Lyrics.Helpers.General 2 | { 3 | public static class MathHelper 4 | { 5 | /// 6 | /// Returns the smaller of two 32-bit signed integers. 7 | /// 8 | /// The first of two 32-bit signed integers to compare. 9 | /// The second of two 32-bit signed integers to compare. 10 | /// Parameter val1 or val2, whichever is smaller. 11 | public static int? Min(int? val1, int? val2) 12 | { 13 | if (val1.HasValue && val2.HasValue) return Math.Min(val1.Value, val2.Value); 14 | if (val1.HasValue) return val1.Value; 15 | if (val2.HasValue) return val2.Value; 16 | return null; 17 | } 18 | 19 | /// 20 | /// Returns the larger of two 32-bit signed integers. 21 | /// 22 | /// The first of two 32-bit signed integers to compare. 23 | /// The second of two 32-bit signed integers to compare. 24 | /// Parameter val1 or val2, whichever is larger. 25 | public static int? Max(int? val1, int? val2) 26 | { 27 | if (val1.HasValue && val2.HasValue) return Math.Max(val1.Value, val2.Value); 28 | if (val1.HasValue) return val1.Value; 29 | if (val2.HasValue) return val2.Value; 30 | return null; 31 | } 32 | 33 | /// 34 | /// Returns x if x is greater than zero, otherwise zero will be returned 35 | /// 36 | public static int GreaterThanZero(int x) 37 | { 38 | if (x < 0) return 0; 39 | else return x; 40 | } 41 | 42 | /// 43 | /// Returns x if x is greater than zero, otherwise zero will be returned 44 | /// 45 | public static double GreaterThanZero(double x) 46 | { 47 | if (x < 0) return 0; 48 | else return x; 49 | } 50 | 51 | /// 52 | /// Returns x if x is greater than zero, otherwise zero will be returned 53 | /// 54 | public static float GreaterThanZero(float x) 55 | { 56 | if (x < 0) return 0; 57 | else return x; 58 | } 59 | 60 | /// 61 | /// Returns x if x is greater than a minimum value, otherwise the minimum value will be returned 62 | /// 63 | public static int GreaterThan(int x, int min) 64 | { 65 | if (x < min) return min; 66 | else return x; 67 | } 68 | 69 | /// 70 | /// Returns x if x is greater than a minimum value, otherwise the minimum value will be returned 71 | /// 72 | public static double GreaterThan(double x, double min) 73 | { 74 | if (x < min) return min; 75 | else return x; 76 | } 77 | 78 | /// 79 | /// Returns x if x is greater than a minimum value, otherwise the minimum value will be returned 80 | /// 81 | public static float GreaterThan(float x, float min) 82 | { 83 | if (x < min) return min; 84 | else return x; 85 | } 86 | 87 | /// 88 | /// Returns whether x is between a and b 89 | /// 90 | /// Contain a and b or not 91 | public static bool IsBetween(this int x, int a, int b, bool containEdge = true) 92 | { 93 | if (a > b) Swap(ref a, ref b); 94 | if (!containEdge) 95 | { 96 | if (x < b && x > a) return true; 97 | else return false; 98 | } 99 | if (x <= b && x >= a) return true; 100 | else return false; 101 | } 102 | 103 | /// 104 | /// Returns whether x is between a and b 105 | /// 106 | /// Contain a and b or not 107 | public static bool IsBetween(this double x, double a, double b, bool containEdge = true) 108 | { 109 | if (a > b) Swap(ref a, ref b); 110 | if (!containEdge) 111 | { 112 | if (x < b && x > a) return true; 113 | else return false; 114 | } 115 | if (x <= b && x >= a) return true; 116 | else return false; 117 | } 118 | 119 | /// 120 | /// Returns whether x is between a and b 121 | /// 122 | /// Contain a and b or not 123 | public static bool IsBetween(this float x, float a, float b, bool containEdge = true) 124 | { 125 | if (a > b) Swap(ref a, ref b); 126 | if (!containEdge) 127 | { 128 | if (x < b && x > a) return true; 129 | else return false; 130 | } 131 | if (x <= b && x >= a) return true; 132 | else return false; 133 | } 134 | 135 | public static void Swap(ref int a, ref int b) 136 | { 137 | (b, a) = (a, b); 138 | } 139 | 140 | public static void Swap(ref double a, ref double b) 141 | { 142 | (b, a) = (a, b); 143 | } 144 | 145 | public static void Swap(ref float a, ref float b) 146 | { 147 | (b, a) = (a, b); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/MusixmatchParser.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Models; 2 | using Lyricify.Lyrics.Parsers.Models; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Lyricify.Lyrics.Parsers 7 | { 8 | public static class MusixmatchParser 9 | { 10 | public static LyricsData? Parse(string rawJson) 11 | { 12 | return Parse(rawJson, false); 13 | } 14 | 15 | /// 忽略逐字歌词 16 | public static LyricsData? Parse(string rawJson, bool ignoreSyllable) 17 | { 18 | var jsonObj = JObject.Parse(rawJson); 19 | if (jsonObj?["message"]?["body"]?["macro_calls"] is not JObject calls) return null; 20 | 21 | static bool CheckHeader200(JObject? getObj) 22 | { 23 | if (getObj?["message"]?["header"]?["status_code"]?.Type != JTokenType.Integer) return false; 24 | if (getObj?["message"]?["header"]?.Value("status_code") != 200) return false; 25 | return true; 26 | } 27 | 28 | var track_get = calls["track.richsync.get"] as JObject; 29 | if (!ignoreSyllable && CheckHeader200(track_get)) 30 | { 31 | var lyrics = track_get?["message"]?["body"]?["richsync"]?["richsync_body"]?.Value(); 32 | 33 | if (!string.IsNullOrEmpty(lyrics) && JsonConvert.DeserializeObject>(lyrics) is List list) 34 | { 35 | var lines = new List(); 36 | foreach (var line in list) 37 | { 38 | var syllables = new List(); 39 | var start = (int)(line.TimeStart * 1000); 40 | for (int i = 0; i < line.Words.Count; i++) 41 | { 42 | syllables.Add(new() 43 | { 44 | StartTime = start + (int)(line.Words[i].Position * 1000), 45 | EndTime = i + 1 < line.Words.Count ? start + (int)(line.Words[i + 1].Position * 1000) : (int)(line.TimeEnd * 1000), 46 | Text = line.Words[i].Chars, 47 | }); 48 | } 49 | lines.Add(new SyllableLineInfo() 50 | { 51 | Syllables = syllables.Cast().ToList(), 52 | }); 53 | } 54 | 55 | var lyricsData = new LyricsData 56 | { 57 | File = new(), 58 | Lines = lines, 59 | TrackMetadata = new TrackMetadata(), 60 | }; 61 | lyricsData.File.Type = LyricsTypes.Musixmatch; 62 | lyricsData.File.SyncTypes = SyncTypes.SyllableSynced; 63 | var language = track_get?["message"]?["body"]?["richsync"]?["richssync_language"]?.Value() 64 | ?? track_get?["message"]?["body"]?["richsync"]?["richsync_language"]?.Value(); 65 | if (language is not null) 66 | { 67 | lyricsData.TrackMetadata.Language = new() { language }; 68 | } 69 | return lyricsData; 70 | } 71 | } 72 | 73 | track_get = calls["track.subtitles.get"] as JObject; 74 | if (CheckHeader200(track_get)) 75 | { 76 | var list = track_get?["message"]?["body"]?["subtitle_list"] as JArray; 77 | if (list is { Count: > 0 }) 78 | { 79 | var subtitle = list[0]["subtitle"]?["subtitle_body"]?.Value(); 80 | if (!string.IsNullOrEmpty(subtitle)) 81 | { 82 | var lines = LrcParser.ParseLyrics(subtitle); 83 | var lyricsData = new LyricsData 84 | { 85 | File = new(), 86 | Lines = lines, 87 | TrackMetadata = new TrackMetadata(), 88 | }; 89 | lyricsData.File.Type = LyricsTypes.Musixmatch; 90 | lyricsData.File.SyncTypes = SyncTypes.LineSynced; 91 | var language = list[0]["subtitle"]?["subtitle_language"]?.Value(); 92 | if (language is not null) 93 | { 94 | lyricsData.TrackMetadata.Language = new() { language }; 95 | } 96 | return lyricsData; 97 | } 98 | } 99 | } 100 | 101 | track_get = calls["track.lyrics.get"] as JObject; 102 | if (CheckHeader200(track_get)) 103 | { 104 | var lyrics = track_get?["message"]?["body"]?["lyrics"]?["lyrics_body"]?.Value(); 105 | 106 | if (!string.IsNullOrEmpty(lyrics)) 107 | { 108 | var list = lyrics.Trim() 109 | .Split('\n') 110 | .Select(line => new LineInfo { Text = line }) 111 | .Cast() 112 | .ToList(); 113 | 114 | var lyricsData = new LyricsData 115 | { 116 | File = new(), 117 | Lines = list, 118 | }; 119 | lyricsData.File.Type = LyricsTypes.Musixmatch; 120 | lyricsData.File.SyncTypes = SyncTypes.Unsynced; 121 | return lyricsData; 122 | } 123 | } 124 | 125 | return null; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Providers/Web/Netease/EapiHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Net; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | 7 | #nullable disable 8 | namespace Lyricify.Lyrics.Providers.Web.Netease 9 | { 10 | internal class EapiHelper 11 | { 12 | public static async Task PostAsync(string url, HttpClient httpClient, Dictionary data) 13 | { 14 | var headers = new Dictionary 15 | { 16 | ["User-Agent"] = userAgent, 17 | ["Referer"] = "https://music.163.com/", 18 | }; 19 | var header = new Dictionary() 20 | { 21 | ["__csrf"] = "", 22 | ["appver"] = "8.0.0", 23 | ["buildver"] = GetCurrentTotalSeconds().ToString(), 24 | ["channel"] = string.Empty, 25 | ["deviceId"] = "", 26 | ["mobilename"] = string.Empty, 27 | ["resolution"] = "1920x1080", 28 | ["os"] = "android", 29 | ["osver"] = "", 30 | ["requestId"] = $"{GetCurrentTotalMilliseconds()}_{Math.Floor(new Random().NextDouble() * 1000).ToString().PadLeft(4, '0')}", 31 | ["versioncode"] = "140", 32 | ["MUSIC_U"] = "", 33 | }; 34 | headers["Cookie"] = string.Join("; ", header.Select(t => t.Key + "=" + t.Value)); 35 | data["header"] = JsonConvert.SerializeObject(header); 36 | var data2 = EApi(url, data); 37 | url = Regex.Replace(url, @"\w*api", "eapi"); 38 | 39 | httpClient.DefaultRequestHeaders.Clear(); 40 | foreach (var h in headers) 41 | { 42 | httpClient.DefaultRequestHeaders.Add(h.Key, h.Value); 43 | } 44 | using var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(data2)); 45 | response.EnsureSuccessStatusCode(); 46 | byte[] buffer = await response.Content.ReadAsByteArrayAsync(); 47 | return Encoding.UTF8.GetString(buffer); 48 | } 49 | 50 | private static ulong GetCurrentTotalSeconds() 51 | { 52 | var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1); 53 | return (ulong)timeSpan.TotalSeconds; 54 | } 55 | 56 | private static ulong GetCurrentTotalMilliseconds() 57 | { 58 | var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1); 59 | return (ulong)timeSpan.TotalMilliseconds; 60 | } 61 | 62 | private static readonly string userAgent = "Mozilla/5.0 (Linux; Android 9; PCT-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.64 HuaweiBrowser/10.0.3.311 Mobile Safari/537.36"; 63 | 64 | private static readonly byte[] eapiKey = Encoding.ASCII.GetBytes("e82ckenh8dichen8"); 65 | 66 | public static Dictionary EApi(string url, object @object) 67 | { 68 | url = url.Replace("https://interface3.music.163.com/e", "/"); 69 | url = url.Replace("https://interface.music.163.com/e", "/"); 70 | string text = JsonConvert.SerializeObject(@object); 71 | string message = $"nobody{url}use{text}md5forencrypt"; 72 | string digest = message.ToByteArrayUtf8().ComputeMd5().ToHexStringLower(); 73 | string data = $"{url}-36cd479b6b5-{text}-36cd479b6b5-{digest}"; 74 | return new Dictionary 75 | { 76 | ["params"] = AesEncrypt(data.ToByteArrayUtf8(), CipherMode.ECB, eapiKey, null).ToHexStringUpper() 77 | }; 78 | } 79 | 80 | public static byte[] Decrypt(byte[] cipherBuffer) 81 | { 82 | return AesDecrypt(cipherBuffer, CipherMode.ECB, eapiKey, null); 83 | } 84 | 85 | private static byte[] AesEncrypt(byte[] buffer, CipherMode mode, byte[] key, byte[] iv) 86 | { 87 | using var aes = Aes.Create(); 88 | aes.BlockSize = 128; 89 | aes.Key = key; 90 | if (iv is not null) 91 | aes.IV = iv; 92 | aes.Mode = mode; 93 | using var cryptoTransform = aes.CreateEncryptor(); 94 | return cryptoTransform.TransformFinalBlock(buffer, 0, buffer.Length); 95 | } 96 | 97 | private static byte[] AesDecrypt(byte[] buffer, CipherMode mode, byte[] key, byte[] iv) 98 | { 99 | using var aes = Aes.Create(); 100 | aes.BlockSize = 128; 101 | aes.Key = key; 102 | if (iv is not null) 103 | aes.IV = iv; 104 | aes.Mode = mode; 105 | using var cryptoTransform = aes.CreateDecryptor(); 106 | return cryptoTransform.TransformFinalBlock(buffer, 0, buffer.Length); 107 | } 108 | 109 | } 110 | internal static class Extensions 111 | { 112 | public static byte[] ToByteArrayUtf8(this string value) 113 | { 114 | return Encoding.UTF8.GetBytes(value); 115 | } 116 | 117 | public static string ToHexStringLower(this byte[] value) 118 | { 119 | var sb = new StringBuilder(); 120 | for (int i = 0; i < value.Length; i++) 121 | sb.Append(value[i].ToString("x2")); 122 | return sb.ToString(); 123 | } 124 | 125 | public static string ToHexStringUpper(this byte[] value) 126 | { 127 | var sb = new StringBuilder(); 128 | for (int i = 0; i < value.Length; i++) 129 | sb.Append(value[i].ToString("X2")); 130 | return sb.ToString(); 131 | } 132 | 133 | public static string ToBase64String(this byte[] value) 134 | { 135 | return Convert.ToBase64String(value); 136 | } 137 | 138 | public static byte[] ComputeMd5(this byte[] value) 139 | { 140 | using var md5 = MD5.Create(); 141 | return md5.ComputeHash(value); 142 | } 143 | 144 | public static byte[] RandomBytes(this Random random, int length) 145 | { 146 | byte[] buffer = new byte[length]; 147 | random.NextBytes(buffer); 148 | return buffer; 149 | } 150 | 151 | public static string Get(this CookieCollection cookies, string name, string defaultValue) 152 | { 153 | return cookies[name]?.Value ?? defaultValue; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Demo/RawLyrics/LyricifySyllableDemo.txt: -------------------------------------------------------------------------------- 1 | [from:AppleSyllable] 2 | [4]Hate (14872,393)to (15265,190)give (15455,262)the (15717,113)satisfaction (15830,833)asking (16663,422)how (17085,250)you're (17335,173)doing (17508,363)now(17871,553) 3 | [4]How's (18424,327)the (18751,185)castle (18936,470)built (19406,238)off (19644,205)people (19849,466)you (20315,209)pretend (20524,506)to (21030,124)care (21154,239)about(21393,439) 4 | [4]Just (21832,256)what (22088,219)you (22307,190)wanted(22497,703) 5 | [4]Look (24566,256)at (24822,196)you, (25018,423)cool (25441,333)guy, (25774,459)you (26233,189)got (26422,375)it(26797,603) 6 | [4]I (28864,202)see (29066,381)the (29447,244)parties (29691,494)and (30185,160)the (30345,100)diamonds (30445,638)sometimes (31083,434)when (31517,137)I (31654,161)close (31815,268)my (32083,100)eyes(32183,254) 7 | [4]Six (32437,256)months (32693,190)of (32883,179)torture (33062,565)you (33627,131)sold (33758,541)as (34299,208)some (34507,209)forbidden (34716,648)paradise(35364,827) 8 | [4]I (36191,190)loved (36381,226)you (36607,149)truly(36756,751) 9 | [4]Gotta (38691,501)laugh (39192,549)at (39741,211)the (39952,100)stupidity(40052,1103) 10 | [4]Cuz (41387,220)i've (41607,440)made (42047,328)some (42375,374)real (42749,655)big (43404,523)mistakes(43927,1100) 11 | [4]But (45027,244)you (45271,238)make (45509,589)the (46098,274)worst (46372,425)one (47023,342)look (47365,417)fine(47782,932) 12 | [4]I (48714,244)should've (48958,1095)known (50053,470)it (50523,470)was (50993,397)strange(51390,870) 13 | [4]You (52315,280)only (52595,1130)come (53725,506)out (54231,464)at (54695,410)night(55105,898) 14 | [4]I (56003,369)used (56372,398)to (56770,244)think (57014,491)I (57693,443)was (58136,199)smart(58335,903) 15 | [4]But (59259,292)you (59551,237)made (59788,595)me (60383,450)look (60833,366)so (61199,624)naïve(61823,1177) 16 | [4]The (63000,490)way (63490,400)you (63890,451)sold (64341,397)me (64738,451)for (65189,416)parts(65605,838) 17 | [4]As (66443,196)you (66639,280)sunk (66919,589)your (67508,369)teeth (67877,607)into (68484,880)me, (69364,904)oh(70268,977) 18 | [4]Blood(71245,940)sucker, (72185,829)fame(73125,836)fucker(73961,836) 19 | [4]Bleeding (75064,440)me (75504,399)dry (75903,512)like (76415,237)a (76652,209)god(76861,341)damn (77202,419)vam(77621,450)pire(78071,1014) 20 | [4]Every (82291,595)girl (82886,232)I (83118,214)ever (83332,292)talked (83624,398)to (84022,226)told (84248,268)me (84516,179)you (84695,130)were (84825,197)bad, (85022,303)bad (85325,298)news(85623,207) 21 | [4]You (85830,178)called (86008,250)them (86258,220)crazy (86478,465)god (86943,255)I (87198,191)hate (87389,190)the (87579,137)way (87716,119)I (87835,232)called (88067,326)them (88393,208)crazy (88601,388)too(88989,299) 22 | [4]You're (89288,238)so (89526,131)convincing(89657,1032) 23 | [4]How (91773,309)do (92082,102)you (92184,196)lie (92380,375)without (92755,529)flinching? (93284,939) 24 | [7](How (93600,114)do (93714,208)you (93922,176)lie, (94098,358)how (94456,176)do (94632,168)you (94800,173)lie, (94973,317)how (95290,176)do (95466,174)you (95640,201)lie?)(95841,3611) 25 | [4]Oh, (95475,928)what (96403,184)a (96587,200)mesmerizing, (96787,880)paralyzing, (97667,851)fucked (98518,324)up (98842,161)little (99003,184)thrill(99187,479) 26 | [4]Can't (99666,309)figure (99975,298)out (100273,357)just (100630,208)how (100838,202)you (101040,256)do (101296,196)it (101492,125)and (101617,173)god (101790,286)knows (102076,321)I (102397,131)never (102528,505)will(103033,354) 27 | [4]Went (103387,321)for (103708,220)me (103928,215)and (104143,232)not (104375,275)her(104650,739) 28 | [4]Cuz (106174,153)girls (106327,499)your (106826,416)age (107242,418)know (107660,400)better(108060,589) 29 | [4]I've (108649,389)made (109038,384)some (109422,200)real (109622,648)big (110270,400)mistakes(110670,1238) 30 | [4]But (111908,292)you (112200,226)make (112426,440)the (112866,238)worst (113104,495)one (113776,309)look (114085,423)fine(114508,975) 31 | [4]I (115483,392)should've (115875,848)known (116723,620)it (117343,180)was (117523,451)strange(117974,814) 32 | [4]You (118995,211)only (119206,1034)come (120240,400)out (120640,400)at (121040,416)night(121456,1045) 33 | [4]I (122501,432)used (122933,384)to (123317,216)think (123533,507)I (124281,236)was (124517,416)smart(124933,828) 34 | [4]But (125761,274)you (126035,232)made (126267,666)me (126933,446)look (127379,286)so (127665,439)naïve(128104,1338) 35 | [4]The (129442,360)way (129802,403)you (130205,432)sold (130637,466)me (131103,400)for (131503,416)parts(131919,798) 36 | [4]As (132717,238)you (132955,274)sunk (133229,618)your (133847,363)teeth (134210,560)into (134770,799)me, (135569,862)oh(136431,1038) 37 | [4]Blood(137469,959)sucker, (138428,670)fame(139246,915)fucker(140161,773) 38 | [4]Bleeding (141153,571)me (141724,345)dry (142069,366)like (142435,342)a (142777,183)god(142960,315)damn (143275,419)vam(143694,416)pire(144110,909) 39 | [4]You (151847,149)said (151996,207)it (152203,119)was (152322,358)true (152680,281)love(152961,518) 40 | [4]But (153505,232)wouldn't (153737,286)that (154023,321)be (154344,262)hard(154606,600) 41 | [4]You (155264,214)can't (155478,238)love (155716,167)anyone(155883,710) 42 | [4]Cuz (156593,232)that (156825,244)would (157069,214)mean (157283,196)you (157479,232)had (157711,238)a (157949,197)heart(158146,589) 43 | [4]I (158735,262)tried (158997,213)to (159210,100)help (159310,323)you (159633,211)out(159844,515) 44 | [4]Now (160476,167)I (160643,100)know (160743,310)that (161053,143)I (161196,280)can't(161476,648) 45 | [4]Cuz (162124,256)how (162380,178)you (162558,161)think's (162719,333)the (163052,185)kind (163237,196)of (163433,119)thing(163552,377) 46 | [4]I'll (163929,203)never (164132,440)un(164572,382)der(164954,434)stand(165388,5667) 47 | [4]I've (171088,347)made (171435,416)some (171851,219)real (172070,600)big (172670,400)mistakes(173070,1145) 48 | [4]But (174264,168)you (174432,200)make (174632,584)the (175216,234)worst (175450,465)one (176217,364)look (176581,386)fine(176967,935) 49 | [4]I (177902,378)should've (178280,784)known (179064,501)it (179565,334)was (179899,416)strange(180315,895) 50 | [4]You (181410,250)only (181660,1089)come (182749,440)out (183189,500)at (183689,392)night(184081,792) 51 | [4]I (184887,350)used (185237,450)to (185687,216)think (185903,504)I (186635,234)was (186869,466)smart(187335,671) 52 | [4]But (188071,262)you (188333,232)made (188565,648)me (189213,464)look (189677,292)so (189969,433)naïve(190402,1310) 53 | [4]The (191712,408)way (192120,435)you (192555,334)sold (192889,447)me (193336,435)for (193771,416)parts(194187,808) 54 | [4]As (194995,202)you (195197,274)sunk (195471,613)your (196084,369)teeth (196453,547)into (197000,839)me, (197839,837)oh(198676,1096) 55 | [4]Blood(199772,1062)sucker, (200834,530)fame(201599,886)fucker(202485,745) 56 | [4]Bleeding (203364,529)me (203893,375)dry (204268,421)like (204689,347)a (205036,266)god(205302,349)damn (205651,435)vam(206086,432)pire(206518,818) -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Searchers/Helpers/MatchHelpers/NameMatch.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers.General; 2 | 3 | namespace Lyricify.Lyrics.Searchers.Helpers 4 | { 5 | public static partial class CompareHelper 6 | { 7 | /// 8 | /// 比较曲目名或专辑名匹配程度 9 | /// 10 | /// 原曲目名 11 | /// 搜索得到的曲目名 12 | /// 名称匹配程度 13 | public static NameMatchType? CompareName(string? name1, string? name2) 14 | { 15 | if (name1 == null || name2 == null) return null; 16 | 17 | name1 = name1.ToSC(true).ToLower().Trim(); 18 | name2 = name2.ToSC(true).ToLower().Trim(); 19 | 20 | if (name1 == name2) return NameMatchType.Perfect; 21 | 22 | name1 = name1 23 | .Replace('’', '\'') 24 | .Replace(',', ',') 25 | .Replace("(", " (") 26 | .Replace(")", " )") 27 | .Replace('[', '(') 28 | .Replace(']', ')') 29 | .RemoveDuoSpaces(); 30 | name2 = name2 31 | .Replace('’', '\'') 32 | .Replace(',', ',') 33 | .Replace("(", " (") 34 | .Replace(")", " )") 35 | .Replace('[', '(') 36 | .Replace(']', ')') 37 | .RemoveDuoSpaces(); 38 | name1 = name1.Replace("acoustic version", "acoustic"); 39 | name2 = name2.Replace("acoustic version", "acoustic"); 40 | 41 | if ((name1.Replace(" - ", " (").Trim() + ")").Remove(" ") 42 | == (name2.Replace(" - ", " (").Trim() + ")").Remove(" ")) 43 | return NameMatchType.VeryHigh; 44 | 45 | static bool SpecialCompare(string str1, string str2, string special) 46 | { 47 | special = "(" + special; 48 | bool c1 = str1.Contains(special); 49 | bool c2 = str2.Contains(special); 50 | if (c1 && !c2 && str1[..str1.IndexOf(special)].Trim() == str2) return true; 51 | if (c2 && !c1 && str2[..str2.IndexOf(special)].Trim() == str1) return true; 52 | return false; 53 | } 54 | 55 | static bool SingleSpecialCompare(string str1, string str2, string special) 56 | { 57 | special = "(" + special; 58 | if (str1.Contains(special) && str2.Contains(special) 59 | && str1[..str1.IndexOf(special)].Trim() == str2[..str2.IndexOf(special)].Trim()) return true; 60 | return false; 61 | } 62 | 63 | static bool DuoSpecialCompare(string str1, string str2, string special1, string special2) 64 | { 65 | special1 = "(" + special1; 66 | special2 = "(" + special2; 67 | if (str1.Contains(special1) && str2.Contains(special2) 68 | && str1[..str1.IndexOf(special1)].Trim() == str2[..str2.IndexOf(special2)].Trim()) return true; 69 | if (str1.Contains(special2) && str2.Contains(special1) 70 | && str1[..str1.IndexOf(special2)].Trim() == str2[..str2.IndexOf(special1)].Trim()) return true; 71 | return false; 72 | } 73 | 74 | static bool BracketsCompare(string str1, string str2) 75 | { 76 | if (str1.Contains('(') && !str2.Contains('(') 77 | && str1[..str1.IndexOf('(')].Trim() == str2) return true; 78 | if (str2.Contains('(') && !str2.Contains('(') 79 | && str2[..str2.IndexOf('(')].Trim() == str1) return true; 80 | return false; 81 | } 82 | 83 | if (SpecialCompare(name1, name2, "deluxe")) return NameMatchType.VeryHigh; 84 | if (SpecialCompare(name1, name2, "explicit")) return NameMatchType.VeryHigh; 85 | if (SpecialCompare(name1, name2, "special edition")) return NameMatchType.VeryHigh; 86 | if (SpecialCompare(name1, name2, "bonus track")) return NameMatchType.VeryHigh; 87 | if (SpecialCompare(name1, name2, "feat")) return NameMatchType.VeryHigh; 88 | if (SpecialCompare(name1, name2, "with")) return NameMatchType.VeryHigh; 89 | 90 | if (DuoSpecialCompare(name1, name2, "feat", "explicit")) return NameMatchType.High; 91 | if (DuoSpecialCompare(name1, name2, "with", "explicit")) return NameMatchType.High; 92 | if (SingleSpecialCompare(name1, name2, "feat")) return NameMatchType.High; 93 | if (SingleSpecialCompare(name1, name2, "with")) return NameMatchType.High; 94 | 95 | if (BracketsCompare(name1, name2)) return NameMatchType.Medium; 96 | 97 | // 在同长度的情况下,判断解决异体字的问题 98 | int count = 0; 99 | if (name1.Length == name2.Length) 100 | { 101 | for (int i = 0; i < name1.Length; i++) 102 | if (name1[i] == name2[i]) count++; 103 | 104 | if ((double)count / name1.Length >= 0.8 && name1.Length >= 4 105 | || (double)count / name1.Length >= 0.5 && name1.Length >= 2 && name1.Length <= 3) 106 | return NameMatchType.High; 107 | } 108 | 109 | if (StringHelper.ComputeTextSame(name1, name2, true) > 90) return NameMatchType.VeryHigh; 110 | if (StringHelper.ComputeTextSame(name1, name2, true) > 80) return NameMatchType.High; 111 | if (StringHelper.ComputeTextSame(name1, name2, true) > 68) return NameMatchType.Medium; 112 | if (StringHelper.ComputeTextSame(name1, name2, true) > 55) return NameMatchType.Low; 113 | 114 | return NameMatchType.NoMatch; 115 | } 116 | 117 | public static int GetMatchScore(this NameMatchType matchType) 118 | { 119 | return GetMatchScore(matchType); 120 | } 121 | 122 | public static int GetMatchScore(this NameMatchType? matchType) 123 | { 124 | return matchType switch 125 | { 126 | NameMatchType.Perfect => 7, 127 | NameMatchType.VeryHigh => 6, 128 | NameMatchType.High => 5, 129 | NameMatchType.Medium => 4, 130 | NameMatchType.Low => 2, 131 | NameMatchType.NoMatch => 0, 132 | _ => 0, 133 | }; 134 | } 135 | 136 | /// 137 | /// 名称匹配程度 138 | /// 139 | public enum NameMatchType 140 | { 141 | Perfect, 142 | VeryHigh, 143 | High, 144 | Medium, 145 | Low, 146 | NoMatch = -1, 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Lyricify.Lyrics.Helper/Parsers/LyricifySyllableParser.cs: -------------------------------------------------------------------------------- 1 | using Lyricify.Lyrics.Helpers; 2 | using Lyricify.Lyrics.Helpers.General; 3 | using Lyricify.Lyrics.Models; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Lyricify.Lyrics.Parsers 7 | { 8 | public static class LyricifySyllableParser 9 | { 10 | public static LyricsData Parse(string lyrics) 11 | { 12 | var lyricsLines = lyrics.Trim().Split('\n').ToList(); 13 | var data = new LyricsData 14 | { 15 | TrackMetadata = new TrackMetadata(), 16 | File = new() 17 | { 18 | Type = LyricsTypes.Qrc, 19 | SyncTypes = SyncTypes.SyllableSynced, 20 | AdditionalInfo = new GeneralAdditionalInfo() 21 | { 22 | Attributes = new(), 23 | } 24 | } 25 | }; 26 | 27 | // 处理 Attributes 28 | var offset = AttributesHelper.ParseGeneralAttributesToLyricsData(data, lyricsLines); 29 | 30 | // 处理歌词行 31 | var lines = ParseLyrics(lyricsLines, offset); 32 | 33 | data.Lines = lines; 34 | return data; 35 | } 36 | 37 | /// 38 | /// 解析 Lyricify Syllable 歌词 39 | /// 40 | public static List ParseLyrics(List lines, int? offset = null) 41 | { 42 | var list = new List(); 43 | 44 | foreach (var line in lines) 45 | { 46 | // 处理歌词行 47 | var item = ParseLyricsLine(line); 48 | if (item != null) 49 | { 50 | list.Add(item); 51 | } 52 | } 53 | 54 | // 将背景人声歌词放入主歌词 55 | var newList = SetBackgroundVocalsInfo(list); 56 | 57 | // 应用 Offset 58 | if (offset.HasValue && offset.Value != 0) 59 | { 60 | OffsetHelper.AddOffset(newList, offset.Value); 61 | } 62 | 63 | return newList; 64 | } 65 | 66 | /// 67 | /// 获取将背景人声歌词放入主歌词后的新列表 (会破坏原列表中的 items) 68 | /// 69 | public static List SetBackgroundVocalsInfo(List list) 70 | { 71 | // 将已有背景人声行属性的歌词放入主歌词 72 | for (int i = 1; i < list.Count; i++) 73 | { 74 | if (list[i].IsBackgroundVocals == true) 75 | { 76 | list[i - 1].SubLine = SyllableLineInfoWithSubLineState.GetSyllableLineInfo(list[i]); 77 | list.RemoveAt(i--); 78 | } 79 | } 80 | 81 | // 判断没有预设属性的行 82 | bool IsNotBackgroundVocals(SyllableLineInfoWithSubLineState line) => line.IsBackgroundVocals is null && !line.IsBracketedLyrics || line.IsBackgroundVocals == false; 83 | for (int i = 1; i < list.Count; i++) 84 | { 85 | if (list[i].IsBackgroundVocals == null && list[i].IsBracketedLyrics) // 是符合背景人声条件的歌词行 86 | { 87 | if (IsNotBackgroundVocals(list[i - 1]) && list[i - 1].SubLine is null) // 上一行符合主歌词行的条件 88 | { 89 | if (i >= list.Count || IsNotBackgroundVocals(list[i + 1])) // 下一行不是背景歌词行 90 | { 91 | list[i - 1].SubLine = SyllableLineInfoWithSubLineState.GetSyllableLineInfo(list[i]); 92 | list.RemoveAt(i--); 93 | } 94 | } 95 | } 96 | } 97 | 98 | var lines = new List(); 99 | // 移除 IsBackgroundVocals 属性 100 | for (int i = 0; i < list.Count; i++) 101 | { 102 | lines.Add(SyllableLineInfoWithSubLineState.GetSyllableLineInfo(list[i])); 103 | } 104 | return lines; 105 | } 106 | 107 | public class SyllableLineInfoWithSubLineState : SyllableLineInfo 108 | { 109 | public SyllableLineInfoWithSubLineState() { } 110 | 111 | public SyllableLineInfoWithSubLineState(IEnumerable syllables) : base(syllables) { } 112 | 113 | /// 114 | /// 是否是背景人声歌词 115 | /// 116 | public bool? IsBackgroundVocals { get; set; } = null; 117 | 118 | /// 119 | /// 头尾是括号的歌词 120 | /// 121 | public bool IsBracketedLyrics => (Text.StartsWith("(") || Text.StartsWith("(")) && (Text.EndsWith(")") || Text.EndsWith(")")); 122 | 123 | /// 124 | /// 创建一个 SyllableLineInfo 实例,以便与 SyllableLineInfoWithSubLineState 完全分离 125 | /// 126 | /// 127 | /// 128 | public static SyllableLineInfo GetSyllableLineInfo(SyllableLineInfoWithSubLineState syllableLineInfoWithSubLineState) 129 | { 130 | return new SyllableLineInfo(syllableLineInfoWithSubLineState.Syllables) 131 | { 132 | LyricsAlignment = syllableLineInfoWithSubLineState.LyricsAlignment, 133 | SubLine = syllableLineInfoWithSubLineState.SubLine, 134 | }; 135 | } 136 | } 137 | 138 | /// 139 | /// 解析 Lyricify Syllable 歌词行 140 | /// 141 | public static SyllableLineInfoWithSubLineState? ParseLyricsLine(string line) 142 | { 143 | List lyricItems = new(); 144 | var lineInfo = new SyllableLineInfoWithSubLineState(); 145 | 146 | if (line.IndexOf(']') != -1) 147 | { 148 | var properties = line[..line.IndexOf("]")]; 149 | if (properties.Length > 1 && properties[1..].IsNumber()) 150 | { 151 | int p = int.Parse(properties[1..]); 152 | 153 | // 读取预设的背景人声 154 | if (p >= 6) 155 | { 156 | lineInfo.IsBackgroundVocals = true; 157 | } 158 | else if (p >= 3) 159 | { 160 | lineInfo.IsBackgroundVocals = false; 161 | } 162 | 163 | // 读取预设的对唱视图 164 | switch (p % 3) 165 | { 166 | case 0: 167 | lineInfo.LyricsAlignment = LyricsAlignment.Unspecified; 168 | break; 169 | case 1: 170 | lineInfo.LyricsAlignment = LyricsAlignment.Left; 171 | break; 172 | case 2: 173 | lineInfo.LyricsAlignment = LyricsAlignment.Right; 174 | break; 175 | } 176 | } 177 | line = line[(line.IndexOf("]") + 1)..]; 178 | } 179 | 180 | MatchCollection matches = Regex.Matches(line, @"(.*?)\((\d+),(\d+)\)"); 181 | 182 | foreach (Match match in matches.Cast()) 183 | { 184 | if (match.Groups.Count == 4) 185 | { 186 | string text = match.Groups[1].Value; 187 | int startTime = int.Parse(match.Groups[2].Value); 188 | int duration = int.Parse(match.Groups[3].Value); 189 | 190 | int endTime = startTime + duration; 191 | 192 | lyricItems.Add(new() { Text = text, StartTime = startTime, EndTime = endTime }); 193 | } 194 | } 195 | 196 | lineInfo.Syllables = lyricItems.Cast().ToList(); 197 | return lineInfo; 198 | } 199 | } 200 | } 201 | --------------------------------------------------------------------------------