Jelajahi Sumber

Add support for lyric provider plugins

Niels van Velzen 1 tahun lalu
induk
melakukan
6de56f0518

+ 6 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Activity;
+using MediaBrowser.Providers.Lyric;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -93,6 +94,11 @@ namespace Jellyfin.Server
                 serviceCollection.AddSingleton(typeof(ILyricProvider), type);
             }
 
+            foreach (var type in GetExportTypes<ILyricParser>())
+            {
+                serviceCollection.AddSingleton(typeof(ILyricParser), type);
+            }
+
             base.RegisterServices(serviceCollection);
         }
 

+ 28 - 0
MediaBrowser.Controller/Lyrics/ILyricParser.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Providers.Lyric;
+
+namespace MediaBrowser.Controller.Lyrics;
+
+/// <summary>
+/// Interface ILyricParser.
+/// </summary>
+public interface ILyricParser
+{
+    /// <summary>
+    /// Gets a value indicating the provider name.
+    /// </summary>
+    string Name { get; }
+
+    /// <summary>
+    /// Gets the priority.
+    /// </summary>
+    /// <value>The priority.</value>
+    ResolverPriority Priority { get; }
+
+    /// <summary>
+    /// Parses the raw lyrics into a response.
+    /// </summary>
+    /// <param name="lyrics">The raw lyrics content.</param>
+    /// <returns>The parsed lyrics or null if invalid.</returns>
+    LyricResponse? ParseLyrics(LyricFile lyrics);
+}

+ 28 - 0
MediaBrowser.Controller/Lyrics/LyricFile.cs

@@ -0,0 +1,28 @@
+namespace MediaBrowser.Providers.Lyric;
+
+/// <summary>
+/// The information for a raw lyrics file before parsing.
+/// </summary>
+public class LyricFile
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="LyricFile"/> class.
+    /// </summary>
+    /// <param name="name">The name.</param>
+    /// <param name="content">The content.</param>
+    public LyricFile(string name, string content)
+    {
+        Name = name;
+        Content = content;
+    }
+
+    /// <summary>
+    /// Gets or sets the name of the lyrics file. This must include the file extension.
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    /// Gets or sets the contents of the file.
+    /// </summary>
+    public string Content { get; set; }
+}

+ 0 - 49
MediaBrowser.Controller/Lyrics/LyricInfo.cs

@@ -1,49 +0,0 @@
-using System;
-using System.IO;
-using Jellyfin.Extensions;
-
-namespace MediaBrowser.Controller.Lyrics;
-
-/// <summary>
-/// Lyric helper methods.
-/// </summary>
-public static class LyricInfo
-{
-    /// <summary>
-    /// Gets matching lyric file for a requested item.
-    /// </summary>
-    /// <param name="lyricProvider">The lyricProvider interface to use.</param>
-    /// <param name="itemPath">Path of requested item.</param>
-    /// <returns>Lyric file path if passed lyric provider's supported media type is found; otherwise, null.</returns>
-    public static string? GetLyricFilePath(this ILyricProvider lyricProvider, string itemPath)
-    {
-        // Ensure we have a provider
-        if (lyricProvider is null)
-        {
-            return null;
-        }
-
-        // Ensure the path to the item is not null
-        string? itemDirectoryPath = Path.GetDirectoryName(itemPath);
-        if (itemDirectoryPath is null)
-        {
-            return null;
-        }
-
-        // Ensure the directory path exists
-        if (!Directory.Exists(itemDirectoryPath))
-        {
-            return null;
-        }
-
-        foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(itemPath)}.*"))
-        {
-            if (lyricProvider.SupportedMediaTypes.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
-            {
-                return lyricFilePath;
-            }
-        }
-
-        return null;
-    }
-}

+ 66 - 0
MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs

@@ -0,0 +1,66 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Resolvers;
+
+namespace MediaBrowser.Providers.Lyric;
+
+/// <inheritdoc />
+public class DefaultLyricProvider : ILyricProvider
+{
+    private static readonly string[] _lyricExtensions = { "lrc", "elrc", "txt", "elrc" };
+
+    /// <inheritdoc />
+    public string Name => "DefaultLyricProvider";
+
+    /// <inheritdoc />
+    public ResolverPriority Priority => ResolverPriority.First;
+
+    /// <inheritdoc />
+    public bool HasLyrics(BaseItem item)
+    {
+        var path = GetLyricsPath(item);
+        return path is not null;
+    }
+
+    /// <inheritdoc />
+    public async Task<LyricFile?> GetLyrics(BaseItem item)
+    {
+        var path = GetLyricsPath(item);
+        if (path is not null)
+        {
+            var content = await File.ReadAllTextAsync(path).ConfigureAwait(false);
+            return new LyricFile(path, content);
+        }
+
+        return null;
+    }
+
+    private string? GetLyricsPath(BaseItem item)
+    {
+        // Ensure the path to the item is not null
+        string? itemDirectoryPath = Path.GetDirectoryName(item.Path);
+        if (itemDirectoryPath is null)
+        {
+            return null;
+        }
+
+        // Ensure the directory path exists
+        if (!Directory.Exists(itemDirectoryPath))
+        {
+            return null;
+        }
+
+        foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(item.Path)}.*"))
+        {
+            if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
+            {
+                return lyricFilePath;
+            }
+        }
+
+        return null;
+    }
+}

+ 6 - 6
MediaBrowser.Controller/Lyrics/ILyricProvider.cs → MediaBrowser.Providers/Lyric/ILyricProvider.cs

@@ -1,9 +1,8 @@
-using System.Collections.Generic;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Resolvers;
 
-namespace MediaBrowser.Controller.Lyrics;
+namespace MediaBrowser.Providers.Lyric;
 
 /// <summary>
 /// Interface ILyricsProvider.
@@ -22,15 +21,16 @@ public interface ILyricProvider
     ResolverPriority Priority { get; }
 
     /// <summary>
-    /// Gets the supported media types for this provider.
+    /// Checks if an item has lyrics available.
     /// </summary>
-    /// <value>The supported media types.</value>
-    IReadOnlyCollection<string> SupportedMediaTypes { get; }
+    /// <param name="item">The media item.</param>
+    /// <returns>Whether lyrics where found or not.</returns>
+    bool HasLyrics(BaseItem item);
 
     /// <summary>
     /// Gets the lyrics.
     /// </summary>
     /// <param name="item">The media item.</param>
     /// <returns>A task representing found lyrics.</returns>
-    Task<LyricResponse?> GetLyrics(BaseItem item);
+    Task<LyricFile?> GetLyrics(BaseItem item);
 }

+ 15 - 38
MediaBrowser.Providers/Lyric/LrcLyricProvider.cs → MediaBrowser.Providers/Lyric/LrcLyricParser.cs

@@ -3,34 +3,29 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using LrcParser.Model;
 using LrcParser.Parser;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Resolvers;
-using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Lyric;
 
 /// <summary>
-/// LRC Lyric Provider.
+/// LRC Lyric Parser.
 /// </summary>
-public class LrcLyricProvider : ILyricProvider
+public class LrcLyricParser : ILyricParser
 {
-    private readonly ILogger<LrcLyricProvider> _logger;
-
     private readonly LyricParser _lrcLyricParser;
 
+    private static readonly string[] _supportedMediaTypes = { "lrc", "elrc" };
     private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" };
 
     /// <summary>
-    /// Initializes a new instance of the <see cref="LrcLyricProvider"/> class.
+    /// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
     /// </summary>
-    /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
-    public LrcLyricProvider(ILogger<LrcLyricProvider> logger)
+    public LrcLyricParser()
     {
-        _logger = logger;
         _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser();
     }
 
@@ -41,37 +36,25 @@ public class LrcLyricProvider : ILyricProvider
     /// Gets the priority.
     /// </summary>
     /// <value>The priority.</value>
-    public ResolverPriority Priority => ResolverPriority.First;
+    public ResolverPriority Priority => ResolverPriority.Fourth;
 
     /// <inheritdoc />
-    public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc" };
-
-    /// <summary>
-    /// Opens lyric file for the requested item, and processes it for API return.
-    /// </summary>
-    /// <param name="item">The item to to process.</param>
-    /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/> with or without metadata; otherwise, null.</returns>
-    public async Task<LyricResponse?> GetLyrics(BaseItem item)
+    public LyricResponse? ParseLyrics(LyricFile lyrics)
     {
-        string? lyricFilePath = this.GetLyricFilePath(item.Path);
-
-        if (string.IsNullOrEmpty(lyricFilePath))
+        if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
         {
             return null;
         }
 
-        var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-        string lrcFileContent = await File.ReadAllTextAsync(lyricFilePath).ConfigureAwait(false);
-
         Song lyricData;
 
         try
         {
-            lyricData = _lrcLyricParser.Decode(lrcFileContent);
+            lyricData = _lrcLyricParser.Decode(lyrics.Content);
         }
-        catch (Exception ex)
+        catch (Exception)
         {
-            _logger.LogError(ex, "Error parsing lyric file {LyricFilePath} from {Provider}", lyricFilePath, Name);
+            // Failed to parse, return null so the next parser will be tried
             return null;
         }
 
@@ -84,6 +67,7 @@ public class LrcLyricProvider : ILyricProvider
             .Select(x => x.Text)
             .ToList();
 
+        var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         foreach (string metaDataRow in metaDataRows)
         {
             var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase);
@@ -130,17 +114,10 @@ public class LrcLyricProvider : ILyricProvider
             // Map metaData values from LRC file to LyricMetadata properties
             LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
 
-            return new LyricResponse
-            {
-                Metadata = lyricMetadata,
-                Lyrics = lyricList
-            };
+            return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList };
         }
 
-        return new LyricResponse
-        {
-            Lyrics = lyricList
-        };
+        return new LyricResponse { Lyrics = lyricList };
     }
 
     /// <summary>

+ 17 - 5
MediaBrowser.Providers/Lyric/LyricManager.cs

@@ -12,14 +12,17 @@ namespace MediaBrowser.Providers.Lyric;
 public class LyricManager : ILyricManager
 {
     private readonly ILyricProvider[] _lyricProviders;
+    private readonly ILyricParser[] _lyricParsers;
 
     /// <summary>
     /// Initializes a new instance of the <see cref="LyricManager"/> class.
     /// </summary>
     /// <param name="lyricProviders">All found lyricProviders.</param>
-    public LyricManager(IEnumerable<ILyricProvider> lyricProviders)
+    /// <param name="lyricParsers">All found lyricParsers.</param>
+    public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers)
     {
         _lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray();
+        _lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray();
     }
 
     /// <inheritdoc />
@@ -27,10 +30,19 @@ public class LyricManager : ILyricManager
     {
         foreach (ILyricProvider provider in _lyricProviders)
         {
-            var results = await provider.GetLyrics(item).ConfigureAwait(false);
-            if (results is not null)
+            var lyrics = await provider.GetLyrics(item).ConfigureAwait(false);
+            if (lyrics is null)
             {
-                return results;
+                continue;
+            }
+
+            foreach (ILyricParser parser in _lyricParsers)
+            {
+                var result = parser.ParseLyrics(lyrics);
+                if (result is not null)
+                {
+                    return result;
+                }
             }
         }
 
@@ -47,7 +59,7 @@ public class LyricManager : ILyricManager
                 continue;
             }
 
-            if (provider.GetLyricFilePath(item.Path) is not null)
+            if (provider.HasLyrics(item))
             {
                 return true;
             }

+ 49 - 0
MediaBrowser.Providers/Lyric/TxtLyricParser.cs

@@ -0,0 +1,49 @@
+using System;
+using System.IO;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.Resolvers;
+
+namespace MediaBrowser.Providers.Lyric;
+
+/// <summary>
+/// TXT Lyric Parser.
+/// </summary>
+public class TxtLyricParser : ILyricParser
+{
+    private static readonly string[] _supportedMediaTypes = { "lrc", "elrc", "txt" };
+
+    /// <inheritdoc />
+    public string Name => "TxtLyricProvider";
+
+    /// <summary>
+    /// Gets the priority.
+    /// </summary>
+    /// <value>The priority.</value>
+    public ResolverPriority Priority => ResolverPriority.Fifth;
+
+    /// <inheritdoc />
+    public LyricResponse? ParseLyrics(LyricFile lyrics)
+    {
+        if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
+        {
+            return null;
+        }
+
+        string[] lyricTextLines = lyrics.Content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+
+        if (lyricTextLines.Length == 0)
+        {
+            return null;
+        }
+
+        LyricLine[] lyricList = new LyricLine[lyricTextLines.Length];
+
+        for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++)
+        {
+            lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]);
+        }
+
+        return new LyricResponse { Lyrics = lyricList };
+    }
+}

+ 0 - 60
MediaBrowser.Providers/Lyric/TxtLyricProvider.cs

@@ -1,60 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Lyrics;
-using MediaBrowser.Controller.Resolvers;
-
-namespace MediaBrowser.Providers.Lyric;
-
-/// <summary>
-/// TXT Lyric Provider.
-/// </summary>
-public class TxtLyricProvider : ILyricProvider
-{
-    /// <inheritdoc />
-    public string Name => "TxtLyricProvider";
-
-    /// <summary>
-    /// Gets the priority.
-    /// </summary>
-    /// <value>The priority.</value>
-    public ResolverPriority Priority => ResolverPriority.Second;
-
-    /// <inheritdoc />
-    public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc", "txt" };
-
-    /// <summary>
-    /// Opens lyric file for the requested item, and processes it for API return.
-    /// </summary>
-    /// <param name="item">The item to to process.</param>
-    /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/>; otherwise, null.</returns>
-    public async Task<LyricResponse?> GetLyrics(BaseItem item)
-    {
-        string? lyricFilePath = this.GetLyricFilePath(item.Path);
-
-        if (string.IsNullOrEmpty(lyricFilePath))
-        {
-            return null;
-        }
-
-        string[] lyricTextLines = await File.ReadAllLinesAsync(lyricFilePath).ConfigureAwait(false);
-
-        if (lyricTextLines.Length == 0)
-        {
-            return null;
-        }
-
-        LyricLine[] lyricList = new LyricLine[lyricTextLines.Length];
-
-        for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++)
-        {
-            lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]);
-        }
-
-        return new LyricResponse
-        {
-            Lyrics = lyricList
-        };
-    }
-}