diff options
| author | Claus Vium <cvium@users.noreply.github.com> | 2022-10-07 15:21:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-07 15:21:42 +0200 |
| commit | 05c20001c8767bee90b6f45cb9cc163722ee4fa8 (patch) | |
| tree | 9811f8c5e2760df1db5c8fd19afcc5d9ea0b85f4 /MediaBrowser.Providers | |
| parent | b137d0cc2bae86ffed23bdfcf6988c04a3b4d365 (diff) | |
| parent | c36785724444f651c1c24a72fed750e42a4d7b68 (diff) | |
Merge pull request #8381 from 1hitsong/lyric-lrc-file-support
Diffstat (limited to 'MediaBrowser.Providers')
| -rw-r--r-- | MediaBrowser.Providers/Lyric/LrcLyricProvider.cs | 220 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Lyric/LyricManager.cs | 58 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Lyric/TxtLyricProvider.cs | 61 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaBrowser.Providers.csproj | 1 |
4 files changed, 340 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs b/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs new file mode 100644 index 0000000000..7b108921b3 --- /dev/null +++ b/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +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. +/// </summary> +public class LrcLyricProvider : ILyricProvider +{ + private readonly ILogger<LrcLyricProvider> _logger; + + private readonly LyricParser _lrcLyricParser; + + 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. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + public LrcLyricProvider(ILogger<LrcLyricProvider> logger) + { + _logger = logger; + _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); + } + + /// <inheritdoc /> + public string Name => "LrcLyricProvider"; + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public ResolverPriority Priority => ResolverPriority.First; + + /// <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) + { + string? lyricFilePath = this.GetLyricFilePath(item.Path); + + if (string.IsNullOrEmpty(lyricFilePath)) + { + 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); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error parsing lyric file {LyricFilePath} from {Provider}", lyricFilePath, Name); + return null; + } + + List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.First().Value).ToList(); + + // Parse metadata rows + var metaDataRows = lyricData.Lyrics + .Where(x => x.TimeTags.Count == 0) + .Where(x => x.Text.StartsWith('[') && x.Text.EndsWith(']')) + .Select(x => x.Text) + .ToList(); + + foreach (string metaDataRow in metaDataRows) + { + var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase); + if (index == -1) + { + continue; + } + + // Remove square bracket before field name, and after field value + // Example 1: [au: 1hitsong] + // Example 2: [ar: Calabrese] + var metaDataFieldName = GetMetadataFieldName(metaDataRow, index); + var metaDataFieldValue = GetMetadataValue(metaDataRow, index); + + if (string.IsNullOrEmpty(metaDataFieldName) || string.IsNullOrEmpty(metaDataFieldValue)) + { + continue; + } + + fileMetaData[metaDataFieldName] = metaDataFieldValue; + } + + if (sortedLyricData.Count == 0) + { + return null; + } + + List<LyricLine> lyricList = new(); + + for (int i = 0; i < sortedLyricData.Count; i++) + { + var timeData = sortedLyricData[i].TimeTags.First().Value; + if (timeData is null) + { + continue; + } + + long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks; + lyricList.Add(new LyricLine(sortedLyricData[i].Text, ticks)); + } + + if (fileMetaData.Count != 0) + { + // Map metaData values from LRC file to LyricMetadata properties + LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData); + + return new LyricResponse + { + Metadata = lyricMetadata, + Lyrics = lyricList + }; + } + + return new LyricResponse + { + Lyrics = lyricList + }; + } + + /// <summary> + /// Converts metadata from an LRC file to LyricMetadata properties. + /// </summary> + /// <param name="metaData">The metadata from the LRC file.</param> + /// <returns>A lyricMetadata object with mapped property data.</returns> + private static LyricMetadata MapMetadataValues(IDictionary<string, string> metaData) + { + LyricMetadata lyricMetadata = new(); + + if (metaData.TryGetValue("ar", out var artist) && !string.IsNullOrEmpty(artist)) + { + lyricMetadata.Artist = artist; + } + + if (metaData.TryGetValue("al", out var album) && !string.IsNullOrEmpty(album)) + { + lyricMetadata.Album = album; + } + + if (metaData.TryGetValue("ti", out var title) && !string.IsNullOrEmpty(title)) + { + lyricMetadata.Title = title; + } + + if (metaData.TryGetValue("au", out var author) && !string.IsNullOrEmpty(author)) + { + lyricMetadata.Author = author; + } + + if (metaData.TryGetValue("length", out var length) && !string.IsNullOrEmpty(length)) + { + if (DateTime.TryParseExact(length, _acceptedTimeFormats, null, DateTimeStyles.None, out var value)) + { + lyricMetadata.Length = value.TimeOfDay.Ticks; + } + } + + if (metaData.TryGetValue("by", out var by) && !string.IsNullOrEmpty(by)) + { + lyricMetadata.By = by; + } + + if (metaData.TryGetValue("offset", out var offset) && !string.IsNullOrEmpty(offset)) + { + if (int.TryParse(offset, out var value)) + { + lyricMetadata.Offset = TimeSpan.FromMilliseconds(value).Ticks; + } + } + + if (metaData.TryGetValue("re", out var creator) && !string.IsNullOrEmpty(creator)) + { + lyricMetadata.Creator = creator; + } + + if (metaData.TryGetValue("ve", out var version) && !string.IsNullOrEmpty(version)) + { + lyricMetadata.Version = version; + } + + return lyricMetadata; + } + + private static string GetMetadataFieldName(string metaDataRow, int index) + { + var metadataFieldName = metaDataRow.AsSpan(1, index - 1).Trim(); + return metadataFieldName.IsEmpty ? string.Empty : metadataFieldName.ToString(); + } + + private static string GetMetadataValue(string metaDataRow, int index) + { + var metadataValue = metaDataRow.AsSpan(index + 1, metaDataRow.Length - index - 2).Trim(); + return metadataValue.IsEmpty ? string.Empty : metadataValue.ToString(); + } +} diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs new file mode 100644 index 0000000000..f9547e0f05 --- /dev/null +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Lyrics; + +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// Lyric Manager. +/// </summary> +public class LyricManager : ILyricManager +{ + private readonly ILyricProvider[] _lyricProviders; + + /// <summary> + /// Initializes a new instance of the <see cref="LyricManager"/> class. + /// </summary> + /// <param name="lyricProviders">All found lyricProviders.</param> + public LyricManager(IEnumerable<ILyricProvider> lyricProviders) + { + _lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray(); + } + + /// <inheritdoc /> + public async Task<LyricResponse?> GetLyrics(BaseItem item) + { + foreach (ILyricProvider provider in _lyricProviders) + { + var results = await provider.GetLyrics(item).ConfigureAwait(false); + if (results is not null) + { + return results; + } + } + + return null; + } + + /// <inheritdoc /> + public bool HasLyricFile(BaseItem item) + { + foreach (ILyricProvider provider in _lyricProviders) + { + if (item is null) + { + continue; + } + + if (provider.GetLyricFilePath(item.Path) is not null) + { + return true; + } + } + + return false; + } +} diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs new file mode 100644 index 0000000000..96a9e9dcf3 --- /dev/null +++ b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +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 + }; + } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index dd43ba9cb0..3a0e9a225b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,6 +16,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="LrcParser" Version="2022.529.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> |
