Add support for lyric provider plugins

This commit is contained in:
Niels van Velzen 2023-06-20 16:51:07 +02:00
parent a1eb2f6ea8
commit 6de56f0518
10 changed files with 215 additions and 158 deletions

View File

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

View File

@ -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);
}

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

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

View File

@ -3,34 +3,29 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using Jellyfin.Extensions;
using LrcParser.Model; using LrcParser.Model;
using LrcParser.Parser; using LrcParser.Parser;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Lyric; namespace MediaBrowser.Providers.Lyric;
/// <summary> /// <summary>
/// LRC Lyric Provider. /// LRC Lyric Parser.
/// </summary> /// </summary>
public class LrcLyricProvider : ILyricProvider public class LrcLyricParser : ILyricParser
{ {
private readonly ILogger<LrcLyricProvider> _logger;
private readonly LyricParser _lrcLyricParser; 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" }; private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" };
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LrcLyricProvider"/> class. /// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
/// </summary> /// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> public LrcLyricParser()
public LrcLyricProvider(ILogger<LrcLyricProvider> logger)
{ {
_logger = logger;
_lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser();
} }
@ -41,37 +36,25 @@ public class LrcLyricProvider : ILyricProvider
/// Gets the priority. /// Gets the priority.
/// </summary> /// </summary>
/// <value>The priority.</value> /// <value>The priority.</value>
public ResolverPriority Priority => ResolverPriority.First; public ResolverPriority Priority => ResolverPriority.Fourth;
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc" }; public LyricResponse? ParseLyrics(LyricFile lyrics)
/// <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)
{ {
string? lyricFilePath = this.GetLyricFilePath(item.Path); if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(lyricFilePath))
{ {
return null; return null;
} }
var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string lrcFileContent = await File.ReadAllTextAsync(lyricFilePath).ConfigureAwait(false);
Song lyricData; Song lyricData;
try 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; return null;
} }
@ -84,6 +67,7 @@ public class LrcLyricProvider : ILyricProvider
.Select(x => x.Text) .Select(x => x.Text)
.ToList(); .ToList();
var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (string metaDataRow in metaDataRows) foreach (string metaDataRow in metaDataRows)
{ {
var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase); var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase);
@ -130,17 +114,10 @@ public class LrcLyricProvider : ILyricProvider
// Map metaData values from LRC file to LyricMetadata properties // Map metaData values from LRC file to LyricMetadata properties
LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData); LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
return new LyricResponse return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList };
{
Metadata = lyricMetadata,
Lyrics = lyricList
};
} }
return new LyricResponse return new LyricResponse { Lyrics = lyricList };
{
Lyrics = lyricList
};
} }
/// <summary> /// <summary>

View File

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

View File

@ -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 };
}
}

View File

@ -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
};
}
}