diff options
| author | 1hitsong <3330318+1hitsong@users.noreply.github.com> | 2022-09-10 14:58:03 -0400 |
|---|---|---|
| committer | 1hitsong <3330318+1hitsong@users.noreply.github.com> | 2022-09-11 15:50:27 -0400 |
| commit | 9d5cf67dfe2d5871d42a55a5e114c5ead1036ff0 (patch) | |
| tree | b89a9f63b2d76314d6ae2ee8aeb43991b1958a76 | |
| parent | 23ec35d3965e950f710c7cf6294145c601a6885b (diff) | |
Create ILyricsProvider
| -rw-r--r-- | Emby.Server.Implementations/Dto/DtoService.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/ItemHelper.cs | 117 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/UserDtos/ILyricsProvider.cs | 34 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/UserDtos/LrcLyricsProvider.cs | 117 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/UserDtos/Lyric.cs | 18 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/UserDtos/Lyrics.cs | 23 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/UserDtos/TxtLyricsProvider.cs | 81 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dto/BaseItemDto.cs | 2 |
8 files changed, 325 insertions, 72 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 09ba36851..96717cff5 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -139,6 +140,10 @@ namespace Emby.Server.Implementations.Dto { LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } + else if (item is Audio) + { + dto.HasLocalLyricsFile = ItemHelper.HasLyricFile(item.Path); + } if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) diff --git a/Jellyfin.Api/Helpers/ItemHelper.cs b/Jellyfin.Api/Helpers/ItemHelper.cs index c1b5ea6cc..622eb0b9f 100644 --- a/Jellyfin.Api/Helpers/ItemHelper.cs +++ b/Jellyfin.Api/Helpers/ItemHelper.cs @@ -23,79 +23,98 @@ namespace Jellyfin.Api.Helpers /// <returns>Collection of Lyrics.</returns> internal static object? GetLyricData(BaseItem item) { - List<Lyrics> lyricsList = new List<Lyrics>(); + List<ILyricsProvider> providerList = new List<ILyricsProvider>(); - string lrcFilePath = @Path.ChangeExtension(item.Path, "lrc"); + // Find all classes that implement ILyricsProvider Interface + var foundLyricProviders = System.Reflection.Assembly.GetExecutingAssembly() + .GetTypes() + .Where(type => typeof(ILyricsProvider).IsAssignableFrom(type) && !type.IsInterface); - // LRC File not found, fallback to TXT file - if (!System.IO.File.Exists(lrcFilePath)) + if (!foundLyricProviders.Any()) { - string txtFilePath = @Path.ChangeExtension(item.Path, "txt"); - if (!System.IO.File.Exists(txtFilePath)) - { - return null; - } + return null; + } - var lyricTextData = System.IO.File.ReadAllText(txtFilePath); - string[] lyricTextLines = lyricTextData.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + foreach (var provider in foundLyricProviders) + { + providerList.Add((ILyricsProvider)Activator.CreateInstance(provider)); + } - foreach (var lyricLine in lyricTextLines) + foreach (ILyricsProvider provider in providerList) + { + provider.Process(item); + if (provider.HasData) { - lyricsList.Add(new Lyrics { Text = lyricLine }); + return provider.Data; } - - return new { lyrics = lyricsList }; } - // Process LRC File - Song lyricData; - List<Lyric> sortedLyricData = new List<Lyric>(); - var metaData = new ExpandoObject() as IDictionary<string, object>; - string lrcFileContent = System.IO.File.ReadAllText(lrcFilePath); - - try - { - LyricParser lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); - lyricData = lrcLyricParser.Decode(lrcFileContent); - var _metaData = lyricData.Lyrics - .Where(x => x.TimeTags.Count == 0) - .Where(x => x.Text.StartsWith("[", StringComparison.Ordinal) && x.Text.EndsWith("]", StringComparison.Ordinal)) - .Select(x => x.Text) - .ToList(); - - foreach (string dataRow in _metaData) - { - var data = dataRow.Split(":"); + return null; + } - string newPropertyName = data[0].Replace("[", string.Empty, StringComparison.Ordinal); - string newPropertyValue = data[1].Replace("]", string.Empty, StringComparison.Ordinal); + /// <summary> + /// Checks if requested item has a matching lyric file. + /// </summary> + /// <param name="itemPath">Path of requested item.</param> + /// <returns>True if item has a matching lyrics file.</returns> + public static string? GetLyricFilePath(string itemPath) + { + List<string> supportedLyricFileExtensions = new List<string>(); - metaData.Add(newPropertyName, newPropertyValue); - } + // Find all classes that implement ILyricsProvider Interface + var foundLyricProviders = System.Reflection.Assembly.GetExecutingAssembly() + .GetTypes() + .Where(type => typeof(ILyricsProvider).IsAssignableFrom(type) && !type.IsInterface); - sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.ToArray()[0].Value).ToList(); - } - catch + if (!foundLyricProviders.Any()) { return null; } - if (lyricData == null) + // Iterate over all found lyric providers + foreach (var provider in foundLyricProviders) { - return null; + var foundProvider = (ILyricsProvider)Activator.CreateInstance(provider); + if (foundProvider?.FileExtensions is null) + { + continue; + } + + if (foundProvider.FileExtensions.Any()) + { + // Gather distinct list of handled file extentions + foreach (string lyricFileExtension in foundProvider.FileExtensions) + { + if (!supportedLyricFileExtensions.Contains(lyricFileExtension)) + { + supportedLyricFileExtensions.Add(lyricFileExtension); + } + } + } } - for (int i = 0; i < sortedLyricData.Count; i++) + foreach (string lyricFileExtension in supportedLyricFileExtensions) { - if (sortedLyricData[i].TimeTags.Count > 0) + string lyricFilePath = @Path.ChangeExtension(itemPath, lyricFileExtension); + if (System.IO.File.Exists(lyricFilePath)) { - var timeData = sortedLyricData[i].TimeTags.ToArray()[0].Value; - double ticks = Convert.ToDouble(timeData, new NumberFormatInfo()) * 10000; - lyricsList.Add(new Lyrics { Start = Math.Ceiling(ticks), Text = sortedLyricData[i].Text }); + return lyricFilePath; } } - return new { MetaData = metaData, lyrics = lyricsList }; + return null; + } + + + /// <summary> + /// Checks if requested item has a matching local lyric file. + /// </summary> + /// <param name="itemPath">Path of requested item.</param> + /// <returns>True if item has a matching lyrics file; otherwise false.</returns> + public static bool HasLyricFile(string itemPath) + { + string? lyricFilePath = GetLyricFilePath(itemPath); + return !string.IsNullOrEmpty(lyricFilePath); } } } diff --git a/Jellyfin.Api/Models/UserDtos/ILyricsProvider.cs b/Jellyfin.Api/Models/UserDtos/ILyricsProvider.cs new file mode 100644 index 000000000..37f1f5bbe --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/ILyricsProvider.cs @@ -0,0 +1,34 @@ +using System.Collections.ObjectModel; +using MediaBrowser.Controller.Entities; + +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// Interface ILyricsProvider. + /// </summary> + public interface ILyricsProvider + { + /// <summary> + /// Gets a value indicating the File Extenstions this provider works with. + /// </summary> + public Collection<string>? FileExtensions { get; } + + /// <summary> + /// Gets a value indicating whether Process() generated data. + /// </summary> + /// <returns><c>true</c> if data generated; otherwise, <c>false</c>.</returns> + bool HasData { get; } + + /// <summary> + /// Gets Data object generated by Process() method. + /// </summary> + /// <returns><c>Object</c> with data if no error occured; otherwise, <c>null</c>.</returns> + object? Data { get; } + + /// <summary> + /// Opens lyric file for [the specified item], and processes it for API return. + /// </summary> + /// <param name="item">The item to to process.</param> + void Process(BaseItem item); + } +} diff --git a/Jellyfin.Api/Models/UserDtos/LrcLyricsProvider.cs b/Jellyfin.Api/Models/UserDtos/LrcLyricsProvider.cs new file mode 100644 index 000000000..029acd6ca --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/LrcLyricsProvider.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Globalization; +using System.Linq; +using LrcParser.Model; +using LrcParser.Parser; +using MediaBrowser.Controller.Entities; + +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// LRC File Lyric Provider. + /// </summary> + public class LrcLyricsProvider : ILyricsProvider + { + /// <summary> + /// Initializes a new instance of the <see cref="LrcLyricsProvider"/> class. + /// </summary> + public LrcLyricsProvider() + { + FileExtensions = new Collection<string> + { + "lrc" + }; + } + + /// <summary> + /// Gets a value indicating the File Extenstions this provider works with. + /// </summary> + public Collection<string>? FileExtensions { get; } + + /// <summary> + /// Gets or Sets a value indicating whether Process() generated data. + /// </summary> + /// <returns><c>true</c> if data generated; otherwise, <c>false</c>.</returns> + public bool HasData { get; set; } + + /// <summary> + /// Gets or Sets Data object generated by Process() method. + /// </summary> + /// <returns><c>Object</c> with data if no error occured; otherwise, <c>null</c>.</returns> + public object? Data { get; set; } + + /// <summary> + /// Opens lyric file for [the specified item], and processes it for API return. + /// </summary> + /// <param name="item">The item to to process.</param> + public void Process(BaseItem item) + { + string? lyricFilePath = Helpers.ItemHelper.GetLyricFilePath(item.Path); + + if (string.IsNullOrEmpty(lyricFilePath)) + { + return; + } + + List<Lyric> lyricsList = new List<Lyric>(); + + List<LrcParser.Model.Lyric> sortedLyricData = new List<LrcParser.Model.Lyric>(); + var metaData = new ExpandoObject() as IDictionary<string, object>; + string lrcFileContent = System.IO.File.ReadAllText(lyricFilePath); + + try + { + // Parse and sort lyric rows + LyricParser lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); + Song lyricData = lrcLyricParser.Decode(lrcFileContent); + sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.ToArray()[0].Value).ToList(); + + // Parse metadata rows + var metaDataRows = lyricData.Lyrics + .Where(x => x.TimeTags.Count == 0) + .Where(x => x.Text.StartsWith("[", StringComparison.Ordinal) && x.Text.EndsWith("]", StringComparison.Ordinal)) + .Select(x => x.Text) + .ToList(); + + foreach (string metaDataRow in metaDataRows) + { + var metaDataField = metaDataRow.Split(":"); + + string metaDataFieldName = metaDataField[0].Replace("[", string.Empty, StringComparison.Ordinal); + string metaDataFieldValue = metaDataField[1].Replace("]", string.Empty, StringComparison.Ordinal); + + metaData.Add(metaDataFieldName, metaDataFieldValue); + } + } + catch + { + return; + } + + if (!sortedLyricData.Any()) + { + return; + } + + for (int i = 0; i < sortedLyricData.Count; i++) + { + var timeData = sortedLyricData[i].TimeTags.ToArray()[0].Value; + double ticks = Convert.ToDouble(timeData, new NumberFormatInfo()) * 10000; + lyricsList.Add(new Lyric { Start = Math.Ceiling(ticks), Text = sortedLyricData[i].Text }); + } + + this.HasData = true; + if (metaData.Any()) + { + this.Data = new { MetaData = metaData, lyrics = lyricsList }; + } + else + { + this.Data = new { lyrics = lyricsList }; + } + } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/Lyric.cs b/Jellyfin.Api/Models/UserDtos/Lyric.cs new file mode 100644 index 000000000..2794cd78a --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/Lyric.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// Lyric dto. + /// </summary> + public class Lyric + { + /// <summary> + /// Gets or sets the start time (ticks). + /// </summary> + public double Start { get; set; } + + /// <summary> + /// Gets or sets the text. + /// </summary> + public string Text { get; set; } = string.Empty; + } +} diff --git a/Jellyfin.Api/Models/UserDtos/Lyrics.cs b/Jellyfin.Api/Models/UserDtos/Lyrics.cs deleted file mode 100644 index cd548eb03..000000000 --- a/Jellyfin.Api/Models/UserDtos/Lyrics.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Jellyfin.Api.Models.UserDtos -{ - /// <summary> - /// Lyric dto. - /// </summary> - public class Lyrics - { - /// <summary> - /// Gets or sets the start. - /// </summary> - public double? Start { get; set; } - - /// <summary> - /// Gets or sets the text. - /// </summary> - public string? Text { get; set; } - - /// <summary> - /// Gets or sets the error. - /// </summary> - public string? Error { get; set; } - } -} diff --git a/Jellyfin.Api/Models/UserDtos/TxtLyricsProvider.cs b/Jellyfin.Api/Models/UserDtos/TxtLyricsProvider.cs new file mode 100644 index 000000000..03cce1ffb --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/TxtLyricsProvider.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.Globalization; +using System.Linq; +using LrcParser.Model; +using LrcParser.Parser; +using MediaBrowser.Controller.Entities; + +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// TXT File Lyric Provider. + /// </summary> + public class TxtLyricsProvider : ILyricsProvider + { + /// <summary> + /// Initializes a new instance of the <see cref="TxtLyricsProvider"/> class. + /// </summary> + public TxtLyricsProvider() + { + FileExtensions = new Collection<string> + { + "lrc", "txt" + }; + } + + /// <summary> + /// Gets a value indicating the File Extenstions this provider works with. + /// </summary> + public Collection<string>? FileExtensions { get; } + + /// <summary> + /// Gets or Sets a value indicating whether Process() generated data. + /// </summary> + /// <returns><c>true</c> if data generated; otherwise, <c>false</c>.</returns> + public bool HasData { get; set; } + + /// <summary> + /// Gets or Sets Data object generated by Process() method. + /// </summary> + /// <returns><c>Object</c> with data if no error occured; otherwise, <c>null</c>.</returns> + public object? Data { get; set; } + + /// <summary> + /// Opens lyric file for [the specified item], and processes it for API return. + /// </summary> + /// <param name="item">The item to to process.</param> + public void Process(BaseItem item) + { + string? lyricFilePath = Helpers.ItemHelper.GetLyricFilePath(item.Path); + + if (string.IsNullOrEmpty(lyricFilePath)) + { + return; + } + + List<Lyric> lyricsList = new List<Lyric>(); + + string lyricData = System.IO.File.ReadAllText(lyricFilePath); + + // Splitting on Environment.NewLine caused some new lines to be missed in Windows. + char[] newLinedelims = new[] { '\r', '\n' }; + string[] lyricTextLines = lyricData.Split(newLinedelims, StringSplitOptions.RemoveEmptyEntries); + + if (!lyricTextLines.Any()) + { + return; + } + + foreach (string lyricLine in lyricTextLines) + { + lyricsList.Add(new Lyric { Text = lyricLine }); + } + + this.HasData = true; + this.Data = new { lyrics = lyricsList }; + } + } +} diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index fdb84fa32..b40a0210a 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -76,6 +76,8 @@ namespace MediaBrowser.Model.Dto public bool? CanDownload { get; set; } + public bool? HasLocalLyricsFile { get; set; } + public bool? HasSubtitles { get; set; } public string PreferredMetadataLanguage { get; set; } |
