diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index b63c8f10e5..4bd226d95e 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 4080ba10d3..9d54533c24 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 d372277e0a..7812687ea3 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 7998ce34a7..13a3810600 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 e72bec46fd..764c0a435f 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,15 +26,27 @@ namespace Jellyfin.Api.Auth.UserPermissionPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement) { - var user = _userManager.GetUserById(context.User.GetUserId()); - if (user is null) + // Api keys have global permissions, so just succeed the requirement. + if (context.User.GetIsApiKey()) { - throw new ResourceNotFoundException(); + context.Succeed(requirement); } - - if (user.HasPermission(requirement.RequiredPermission)) + else { - context.Succeed(requirement); + 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 0000000000..4fccf2cb42 --- /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