├── 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 |
--------------------------------------------------------------------------------