diff options
| -rw-r--r-- | Directory.Packages.props | 4 | ||||
| -rw-r--r-- | MediaBrowser.Model/Lyrics/LyricLineCue.cs | 13 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Lyric/LrcLyricParser.cs | 62 | ||||
| -rw-r--r-- | tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs | 16 |
4 files changed, 59 insertions, 36 deletions
diff --git a/Directory.Packages.props b/Directory.Packages.props index 532202634..61093ad99 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ <PackageVersion Include="Ignore" Version="0.2.1" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="4.0.12" /> - <PackageVersion Include="LrcParser" Version="2025.228.1" /> + <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.6" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" /> @@ -92,4 +92,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" /> <PackageVersion Include="xunit" Version="2.9.3" /> </ItemGroup> -</Project>
\ No newline at end of file +</Project> diff --git a/MediaBrowser.Model/Lyrics/LyricLineCue.cs b/MediaBrowser.Model/Lyrics/LyricLineCue.cs index 1172a0231..291553361 100644 --- a/MediaBrowser.Model/Lyrics/LyricLineCue.cs +++ b/MediaBrowser.Model/Lyrics/LyricLineCue.cs @@ -8,22 +8,29 @@ public class LyricLineCue /// <summary> /// Initializes a new instance of the <see cref="LyricLineCue"/> class. /// </summary> - /// <param name="position">The start of the character index of the lyric.</param> + /// <param name="position">The start character index of the cue.</param> + /// <param name="endPosition">The end character index of the cue.</param> /// <param name="start">The start of the timestamp the lyric is synced to in ticks.</param> /// <param name="end">The end of the timestamp the lyric is synced to in ticks.</param> - public LyricLineCue(int position, long start, long? end) + public LyricLineCue(int position, int endPosition, long start, long? end) { Position = position; + EndPosition = endPosition; Start = start; End = end; } /// <summary> - /// Gets the character index of the lyric. + /// Gets the start character index of the cue. /// </summary> public int Position { get; } /// <summary> + /// Gets the end character index of the cue. + /// </summary> + public int EndPosition { get; } + + /// <summary> /// Gets the timestamp the lyric is synced to in ticks. /// </summary> public long Start { get; } diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 27d17b535..fa711eb28 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using Jellyfin.Extensions; using LrcParser.Model; @@ -66,47 +67,56 @@ public partial class LrcLyricParser : ILyricParser } List<LyricLine> lyricList = []; - for (var l = 0; l < sortedLyricData.Count; l++) + for (var lineIndex = 0; lineIndex < sortedLyricData.Count; lineIndex++) { - var cues = new List<LyricLineCue>(); - var lyric = sortedLyricData[l]; + var lyric = sortedLyricData[lineIndex]; - if (lyric.TimeTags.Count != 0) + // Extract cues from time tags + var cues = new List<LyricLineCue>(); + if (lyric.TimeTags.Count > 0) { var keys = lyric.TimeTags.Keys.ToList(); - int current = 0, next = 1; - while (next < keys.Count) + for (var tagIndex = 0; tagIndex < keys.Count - 1; tagIndex++) { - var currentKey = keys[current]; - var currentMs = lyric.TimeTags[currentKey] ?? 0; - var nextMs = lyric.TimeTags[keys[next]] ?? 0; - - cues.Add(new LyricLineCue( - position: Math.Max(currentKey.Index, 0), - start: TimeSpan.FromMilliseconds(currentMs).Ticks, - end: TimeSpan.FromMilliseconds(nextMs).Ticks)); + var currentKey = keys[tagIndex]; + var nextKey = keys[tagIndex + 1]; - current++; - next++; + var currentPos = currentKey.State == IndexState.End ? currentKey.Index + 1 : currentKey.Index; + var nextPos = nextKey.State == IndexState.End ? nextKey.Index + 1 : nextKey.Index; + var currentMs = lyric.TimeTags[currentKey] ?? 0; + var nextMs = lyric.TimeTags[keys[tagIndex + 1]] ?? 0; + var currentSlice = lyric.Text[currentPos..nextPos]; + var currentSliceTrimmed = currentSlice.Trim(); + if (currentSliceTrimmed.Length > 0) + { + cues.Add(new LyricLineCue( + position: currentPos, + endPosition: nextPos, + start: TimeSpan.FromMilliseconds(currentMs).Ticks, + end: TimeSpan.FromMilliseconds(nextMs).Ticks)); + } } - var lastKey = keys[current]; + var lastKey = keys[^1]; + var lastPos = lastKey.State == IndexState.End ? lastKey.Index + 1 : lastKey.Index; var lastMs = lyric.TimeTags[lastKey] ?? 0; + var lastSlice = lyric.Text[lastPos..]; + var lastSliceTrimmed = lastSlice.Trim(); - cues.Add(new LyricLineCue( - position: Math.Max(lastKey.Index, 0), - start: TimeSpan.FromMilliseconds(lastMs).Ticks, - end: l + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[l + 1].StartTime).Ticks : null)); + if (lastSliceTrimmed.Length > 0) + { + cues.Add(new LyricLineCue( + position: lastPos, + endPosition: lyric.Text.Length, + start: TimeSpan.FromMilliseconds(lastMs).Ticks, + end: lineIndex + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[lineIndex + 1].StartTime).Ticks : null)); + } } long lyricStartTicks = TimeSpan.FromMilliseconds(lyric.StartTime).Ticks; - lyricList.Add(new LyricLine(WhitespaceRegex().Replace(lyric.Text.Trim(), " "), lyricStartTicks, cues)); + lyricList.Add(new LyricLine(lyric.Text, lyricStartTicks, cues)); } return new LyricDto { Lyrics = lyricList }; } - - // Replacement is required until https://github.com/karaoke-dev/LrcParser/issues/83 is resolved. - [GeneratedRegex(@"\s+")] - private static partial Regex WhitespaceRegex(); } diff --git a/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs index 756a688ab..a1fc067cc 100644 --- a/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs +++ b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs @@ -20,22 +20,28 @@ public static class LrcLyricParserTests var line1 = parsed.Lyrics[0]; Assert.Equal("Every night that goes between", line1.Text); Assert.NotNull(line1.Cues); - Assert.Equal(9, line1.Cues.Count); + Assert.Equal(5, line1.Cues.Count); Assert.Equal(68400000, line1.Cues[0].Start); Assert.Equal(72000000, line1.Cues[0].End); + Assert.Equal(0, line1.Cues[0].Position); + Assert.Equal(5, line1.Cues[0].EndPosition); + Assert.Equal(6, line1.Cues[1].Position); + Assert.Equal(11, line1.Cues[1].EndPosition); + Assert.Equal(12, line1.Cues[2].Position); var line5 = parsed.Lyrics[4]; Assert.Equal("Every night you do not come", line5.Text); Assert.NotNull(line5.Cues); - Assert.Equal(11, line5.Cues.Count); - Assert.Equal(377300000, line5.Cues[5].Start); - Assert.Equal(380000000, line5.Cues[5].End); + Assert.Equal(6, line5.Cues.Count); + Assert.Equal(375200000, line5.Cues[2].Start); + Assert.Equal(377300000, line5.Cues[2].End); var lastLine = parsed.Lyrics[^1]; Assert.Equal("I have always been a storm", lastLine.Text); Assert.NotNull(lastLine.Cues); - Assert.Equal(11, lastLine.Cues.Count); + Assert.Equal(6, lastLine.Cues.Count); Assert.Equal(2358000000, lastLine.Cues[^1].Start); + Assert.Equal(26, lastLine.Cues[^1].EndPosition); Assert.Null(lastLine.Cues[^1].End); } } |
