diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index b63c8f10e..4bd226d95 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -173,6 +173,13 @@ namespace Emby.Naming.Common
".vtt",
};
+ LyricFileExtensions = new[]
+ {
+ ".lrc",
+ ".elrc",
+ ".txt"
+ };
+
AlbumStackingPrefixes = new[]
{
"cd",
@@ -791,6 +798,11 @@ namespace Emby.Naming.Common
///
public string[] SubtitleFileExtensions { get; set; }
+ ///
+ /// Gets the list of lyric file extensions.
+ ///
+ public string[] LyricFileExtensions { get; }
+
///
/// Gets or sets list of episode regular expressions.
///
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 4080ba10d..9d54533c2 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -45,7 +45,8 @@ namespace Emby.Naming.ExternalFiles
var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
- && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
+ && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ && !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
return null;
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index d372277e0..7812687ea 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -18,7 +18,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
@@ -53,7 +52,6 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy _livetvManagerFactory;
- private readonly ILyricManager _lyricManager;
private readonly ITrickplayManager _trickplayManager;
public DtoService(
@@ -67,7 +65,6 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy livetvManagerFactory,
- ILyricManager lyricManager,
ITrickplayManager trickplayManager)
{
_logger = logger;
@@ -80,7 +77,6 @@ namespace Emby.Server.Implementations.Dto
_appHost = appHost;
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
- _lyricManager = lyricManager;
_trickplayManager = trickplayManager;
}
@@ -152,10 +148,6 @@ namespace Emby.Server.Implementations.Dto
{
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
}
- else if (item is Audio)
- {
- dto.HasLyrics = _lyricManager.HasLyricFile(item);
- }
if (item is IItemByName itemByName
&& options.ContainsField(ItemFields.ItemCounts))
@@ -275,6 +267,11 @@ namespace Emby.Server.Implementations.Dto
LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
}
+ if (item is Audio audio)
+ {
+ dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
+ }
+
return dto;
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 7998ce34a..13a381060 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1232,6 +1232,19 @@ namespace Emby.Server.Implementations.Library
return item;
}
+ ///
+ public T GetItemById(Guid id)
+ where T : BaseItem
+ {
+ var item = GetItemById(id);
+ if (item is T typedItem)
+ {
+ return typedItem;
+ }
+
+ return null;
+ }
+
public List GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
index e72bec46f..764c0a435 100644
--- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -25,16 +26,28 @@ namespace Jellyfin.Api.Auth.UserPermissionPolicy
///
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
{
- var user = _userManager.GetUserById(context.User.GetUserId());
- if (user is null)
- {
- throw new ResourceNotFoundException();
- }
-
- if (user.HasPermission(requirement.RequiredPermission))
+ // Api keys have global permissions, so just succeed the requirement.
+ if (context.User.GetIsApiKey())
{
context.Succeed(requirement);
}
+ else
+ {
+ var userId = context.User.GetUserId();
+ if (!userId.IsEmpty())
+ {
+ var user = _userManager.GetUserById(context.User.GetUserId());
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ if (user.HasPermission(requirement.RequiredPermission))
+ {
+ context.Succeed(requirement);
+ }
+ }
+ }
return Task.CompletedTask;
}
diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs
new file mode 100644
index 000000000..4fccf2cb4
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LyricsController.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Lyrics;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Lyrics controller.
+///
+[Route("")]
+public class LyricsController : BaseJellyfinApiController
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILyricManager _lyricManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly IUserManager _userManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public LyricsController(
+ ILibraryManager libraryManager,
+ ILyricManager lyricManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ IUserManager userManager)
+ {
+ _libraryManager = libraryManager;
+ _lyricManager = lyricManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _userManager = userManager;
+ }
+
+ ///
+ /// Gets an item's lyrics.
+ ///
+ /// Item id.
+ /// Lyrics returned.
+ /// Something went wrong. No Lyrics will be returned.
+ /// An containing the item's lyrics.
+ [HttpGet("Audio/{itemId}/Lyrics")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task> GetLyrics([FromRoute, Required] Guid itemId)
+ {
+ var isApiKey = User.GetIsApiKey();
+ var userId = User.GetUserId();
+ if (!isApiKey && userId.IsEmpty())
+ {
+ return BadRequest();
+ }
+
+ var audio = _libraryManager.GetItemById