From 5f5347aee3209383248a6055318ec8883291d406 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Fri, 9 Sep 2022 20:14:23 -0400 Subject: [PATCH] Add Lyrics API Endpoint --- .gitignore | 2 + .../Controllers/UserLibraryController.cs | 49 ++++++++++ Jellyfin.Api/Helpers/ItemHelper.cs | 87 ++++++++++++++++++ Jellyfin.Api/Jellyfin.Api.csproj | 12 +++ Jellyfin.Api/Libraries/LrcParser.dll | Bin 0 -> 12288 bytes Jellyfin.Api/Models/UserDtos/Lyrics.cs | 23 +++++ 6 files changed, 173 insertions(+) create mode 100644 Jellyfin.Api/Helpers/ItemHelper.cs create mode 100644 Jellyfin.Api/Libraries/LrcParser.dll create mode 100644 Jellyfin.Api/Models/UserDtos/Lyrics.cs diff --git a/.gitignore b/.gitignore index c2ae76c1e3..3f80ffcf7d 100644 --- a/.gitignore +++ b/.gitignore @@ -281,3 +281,5 @@ apiclient/generated # Omnisharp crash logs mono_crash.*.json + +!LrcParser.dll \ No newline at end of file diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index e45f9b58cd..89fb56744c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -1,14 +1,18 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using Kfstorm.LrcParser; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -381,5 +385,50 @@ namespace Jellyfin.Api.Controllers return _userDataRepository.GetUserDataDto(item, user); } + + /// + /// Gets an item's lyrics. + /// + /// User id. + /// Item id. + /// Lyrics returned. + /// Something went wrong. No Lyrics will be returned. + /// An containing the intros to play. + [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + if (user == null) + { + List lyricsList = new List + { + new Lyrics { Error = "User Not Found" } + }; + return NotFound(new { Results = lyricsList.ToArray() }); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item == null) + { + List lyricsList = new List + { + new Lyrics { Error = "Requested Item Not Found" } + }; + return NotFound(new { Results = lyricsList.ToArray() }); + } + + List result = ItemHelper.GetLyricData(item); + if (string.IsNullOrEmpty(result.ElementAt(0).Error)) + { + return Ok(new { Results = result }); + } + + return NotFound(new { Results = result.ToArray() }); + } } } diff --git a/Jellyfin.Api/Helpers/ItemHelper.cs b/Jellyfin.Api/Helpers/ItemHelper.cs new file mode 100644 index 0000000000..43ef7aa093 --- /dev/null +++ b/Jellyfin.Api/Helpers/ItemHelper.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Jellyfin.Api.Models.UserDtos; +using Kfstorm.LrcParser; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Item helper. + /// + public static class ItemHelper + { + /// + /// Opens lyrics file, converts to a List of Lyrics, and returns it. + /// + /// Requested Item. + /// Collection of Lyrics. + internal static List GetLyricData(BaseItem item) + { + List lyricsList = new List(); + + string lrcFilePath = @Path.ChangeExtension(item.Path, "lrc"); + + // LRC File not found, fallback to TXT file + if (!System.IO.File.Exists(lrcFilePath)) + { + string txtFilePath = @Path.ChangeExtension(item.Path, "txt"); + if (!System.IO.File.Exists(txtFilePath)) + { + lyricsList.Add(new Lyrics { Error = "Lyric File Not Found" }); + return lyricsList; + } + + var lyricTextData = System.IO.File.ReadAllText(txtFilePath); + string[] lyricTextLines = lyricTextData.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + + foreach (var lyricLine in lyricTextLines) + { + lyricsList.Add(new Lyrics { Text = lyricLine }); + } + + return lyricsList; + } + + // Process LRC File + ILrcFile lyricData; + string lrcFileContent = System.IO.File.ReadAllText(lrcFilePath); + try + { + lrcFileContent = lrcFileContent.Replace('<', '['); + lrcFileContent = lrcFileContent.Replace('>', ']'); + lyricData = Kfstorm.LrcParser.LrcFile.FromText(lrcFileContent); + } + catch + { + lyricsList.Add(new Lyrics { Error = "No Lyrics Data" }); + return lyricsList; + } + + if (lyricData == null) + { + lyricsList.Add(new Lyrics { Error = "No Lyrics Data" }); + return lyricsList; + } + + foreach (var lyricLine in lyricData.Lyrics) + { + double ticks = lyricLine.Timestamp.TotalSeconds * 10000000; + lyricsList.Add(new Lyrics { Start = Math.Ceiling(ticks), Text = lyricLine.Content }); + } + + return lyricsList; + } + } +} diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 894d871383..1b78bb1074 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -46,4 +46,16 @@ + + + Libraries\LrcParser.dll + + + + + + Always + + + diff --git a/Jellyfin.Api/Libraries/LrcParser.dll b/Jellyfin.Api/Libraries/LrcParser.dll new file mode 100644 index 0000000000000000000000000000000000000000..46e4fb703335931a91bdb2bcce2a24c62b6b8dda GIT binary patch literal 12288 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 Y+Hfv^i;kV{{|xhgd;RYC?aRRb02;KsP5=M^ literal 0 HcmV?d00001 diff --git a/Jellyfin.Api/Models/UserDtos/Lyrics.cs b/Jellyfin.Api/Models/UserDtos/Lyrics.cs new file mode 100644 index 0000000000..df1b5d08a2 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/Lyrics.cs @@ -0,0 +1,23 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// Lyric dto. + /// + public class Lyrics + { + /// + /// Gets or sets the start. + /// + public double? Start { get; set; } + + /// + /// Gets or sets the test. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the error. + /// + public string? Error { get; set; } + } +}