ソースを参照

Adapt LrcLyricParser to new LrcParser version (#14263)

Max Rumpf 3 日 前
コミット
9b8c12d433

+ 2 - 2
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>
+</Project>

+ 10 - 3
MediaBrowser.Model/Lyrics/LyricLineCue.cs

@@ -8,21 +8,28 @@ 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>

+ 36 - 26
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();
 }

+ 11 - 5
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);
     }
 }