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

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

View File

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

View File

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

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