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