aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels van Velzen <git@ndat.nl>2023-06-20 16:51:07 +0200
committerNiels van Velzen <git@ndat.nl>2023-06-23 21:13:20 +0200
commit6de56f05186b77042a611112d82208b8fa8675fb (patch)
treefc240188be55246860d6b091cccbb4a81813fd66
parenta1eb2f6ea8cd78d527f1ae395378419f016208ab (diff)
Add support for lyric provider plugins
-rw-r--r--Jellyfin.Server/CoreAppHost.cs6
-rw-r--r--MediaBrowser.Controller/Lyrics/ILyricParser.cs28
-rw-r--r--MediaBrowser.Controller/Lyrics/LyricFile.cs28
-rw-r--r--MediaBrowser.Controller/Lyrics/LyricInfo.cs49
-rw-r--r--MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs66
-rw-r--r--MediaBrowser.Providers/Lyric/ILyricProvider.cs (renamed from MediaBrowser.Controller/Lyrics/ILyricProvider.cs)12
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs (renamed from MediaBrowser.Providers/Lyric/LrcLyricProvider.cs)53
-rw-r--r--MediaBrowser.Providers/Lyric/LyricManager.cs22
-rw-r--r--MediaBrowser.Providers/Lyric/TxtLyricParser.cs49
-rw-r--r--MediaBrowser.Providers/Lyric/TxtLyricProvider.cs60
10 files changed, 215 insertions, 158 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 939376dd8..0c6315c66 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/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);
}
diff --git a/MediaBrowser.Controller/Lyrics/ILyricParser.cs b/MediaBrowser.Controller/Lyrics/ILyricParser.cs
new file mode 100644
index 000000000..65a9471a3
--- /dev/null
+++ b/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);
+}
diff --git a/MediaBrowser.Controller/Lyrics/LyricFile.cs b/MediaBrowser.Controller/Lyrics/LyricFile.cs
new file mode 100644
index 000000000..21096797a
--- /dev/null
+++ b/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; }
+}
diff --git a/MediaBrowser.Controller/Lyrics/LyricInfo.cs b/MediaBrowser.Controller/Lyrics/LyricInfo.cs
deleted file mode 100644
index 6ec6df582..000000000
--- a/MediaBrowser.Controller/Lyrics/LyricInfo.cs
+++ /dev/null
@@ -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;
- }
-}
diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs
new file mode 100644
index 000000000..f828ec26b
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs b/MediaBrowser.Providers/Lyric/ILyricProvider.cs
index 2a04c6152..27ceba72b 100644
--- a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs
+++ b/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);
}
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 7b108921b..01a0dddf1 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs
+++ b/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>
diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs
index f9547e0f0..6da811927 100644
--- a/MediaBrowser.Providers/Lyric/LyricManager.cs
+++ b/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;
}
diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs
new file mode 100644
index 000000000..2ed0a6d8a
--- /dev/null
+++ b/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 };
+ }
+}
diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs
deleted file mode 100644
index a9099d192..000000000
--- a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs
+++ /dev/null
@@ -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
- };
- }
-}