mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Store lyrics in the database as media streams (#9951)
This commit is contained in:
parent
59f50ae8b2
commit
0bc41c015f
@ -173,6 +173,13 @@ namespace Emby.Naming.Common
|
|||||||
".vtt",
|
".vtt",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LyricFileExtensions = new[]
|
||||||
|
{
|
||||||
|
".lrc",
|
||||||
|
".elrc",
|
||||||
|
".txt"
|
||||||
|
};
|
||||||
|
|
||||||
AlbumStackingPrefixes = new[]
|
AlbumStackingPrefixes = new[]
|
||||||
{
|
{
|
||||||
"cd",
|
"cd",
|
||||||
@ -791,6 +798,11 @@ namespace Emby.Naming.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] SubtitleFileExtensions { get; set; }
|
public string[] SubtitleFileExtensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of lyric file extensions.
|
||||||
|
/// </summary>
|
||||||
|
public string[] LyricFileExtensions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of episode regular expressions.
|
/// Gets or sets list of episode regular expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -45,7 +45,8 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
|
|
||||||
var extension = Path.GetExtension(path.AsSpan());
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ using MediaBrowser.Controller.Entities;
|
|||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Lyrics;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@ -53,7 +52,6 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||||
|
|
||||||
private readonly ILyricManager _lyricManager;
|
|
||||||
private readonly ITrickplayManager _trickplayManager;
|
private readonly ITrickplayManager _trickplayManager;
|
||||||
|
|
||||||
public DtoService(
|
public DtoService(
|
||||||
@ -67,7 +65,6 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
IApplicationHost appHost,
|
IApplicationHost appHost,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||||
ILyricManager lyricManager,
|
|
||||||
ITrickplayManager trickplayManager)
|
ITrickplayManager trickplayManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -80,7 +77,6 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_livetvManagerFactory = livetvManagerFactory;
|
_livetvManagerFactory = livetvManagerFactory;
|
||||||
_lyricManager = lyricManager;
|
|
||||||
_trickplayManager = trickplayManager;
|
_trickplayManager = trickplayManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,10 +148,6 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
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
|
if (item is IItemByName itemByName
|
||||||
&& options.ContainsField(ItemFields.ItemCounts))
|
&& options.ContainsField(ItemFields.ItemCounts))
|
||||||
@ -275,6 +267,11 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item is Audio audio)
|
||||||
|
{
|
||||||
|
dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
|
||||||
|
}
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1232,6 +1232,19 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public T GetItemById<T>(Guid id)
|
||||||
|
where T : BaseItem
|
||||||
|
{
|
||||||
|
var item = GetItemById(id);
|
||||||
|
if (item is T typedItem)
|
||||||
|
{
|
||||||
|
return typedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
|
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
|
||||||
{
|
{
|
||||||
if (query.Recursive && !query.ParentId.IsEmpty())
|
if (query.Recursive && !query.ParentId.IsEmpty())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -25,16 +26,28 @@ namespace Jellyfin.Api.Auth.UserPermissionPolicy
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(context.User.GetUserId());
|
// Api keys have global permissions, so just succeed the requirement.
|
||||||
if (user is null)
|
if (context.User.GetIsApiKey())
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.HasPermission(requirement.RequiredPermission))
|
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
267
Jellyfin.Api/Controllers/LyricsController.cs
Normal file
267
Jellyfin.Api/Controllers/LyricsController.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lyrics controller.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LyricsController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||||
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
public LyricsController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILyricManager lyricManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IUserManager userManager)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_lyricManager = lyricManager;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an item's lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">Item id.</param>
|
||||||
|
/// <response code="200">Lyrics returned.</response>
|
||||||
|
/// <response code="404">Something went wrong. No Lyrics will be returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
|
||||||
|
[HttpGet("Audio/{itemId}/Lyrics")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var isApiKey = User.GetIsApiKey();
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
if (!isApiKey && userId.IsEmpty())
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||||
|
if (audio is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isApiKey)
|
||||||
|
{
|
||||||
|
var user = _userManager.GetUserById(userId);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the item is visible for the user
|
||||||
|
if (!audio.IsVisible(user))
|
||||||
|
{
|
||||||
|
return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (result is not null)
|
||||||
|
{
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload an external lyric file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item the lyric belongs to.</param>
|
||||||
|
/// <param name="fileName">Name of the file being uploaded.</param>
|
||||||
|
/// <response code="200">Lyrics uploaded.</response>
|
||||||
|
/// <response code="400">Error processing upload.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>The uploaded lyric.</returns>
|
||||||
|
[HttpPost("Audio/{itemId}/Lyrics")]
|
||||||
|
[Authorize(Policy = Policies.LyricManagement)]
|
||||||
|
[AcceptsFile(MediaTypeNames.Text.Plain)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<LyricDto>> UploadLyrics(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromQuery, Required] string fileName)
|
||||||
|
{
|
||||||
|
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||||
|
if (audio is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Request.ContentLength.GetValueOrDefault(0) == 0)
|
||||||
|
{
|
||||||
|
return BadRequest("No lyrics uploaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilize Path.GetExtension as it provides extra path validation.
|
||||||
|
var format = Path.GetExtension(fileName.AsSpan()).RightPart('.').ToString();
|
||||||
|
if (string.IsNullOrEmpty(format))
|
||||||
|
{
|
||||||
|
return BadRequest("Extension is required on filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
await using (stream.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
|
||||||
|
var uploadedLyric = await _lyricManager.UploadLyricAsync(
|
||||||
|
audio,
|
||||||
|
new LyricResponse
|
||||||
|
{
|
||||||
|
Format = format,
|
||||||
|
Stream = stream
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (uploadedLyric is null)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
|
return Ok(uploadedLyric);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an external lyric file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <response code="204">Lyric deleted.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpDelete("Audio/{itemId}/Lyrics")]
|
||||||
|
[Authorize(Policy = Policies.LyricManagement)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult> DeleteLyrics(
|
||||||
|
[FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||||
|
if (audio is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search remote lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <response code="200">Lyrics retrieved.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>An array of <see cref="RemoteLyricInfo"/>.</returns>
|
||||||
|
[HttpGet("Audio/{itemId}/RemoteSearch/Lyrics")]
|
||||||
|
[Authorize(Policy = Policies.LyricManagement)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||||
|
if (audio is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads a remote lyric.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <param name="lyricId">The lyric id.</param>
|
||||||
|
/// <response code="200">Lyric downloaded.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpPost("Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}")]
|
||||||
|
[Authorize(Policy = Policies.LyricManagement)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<LyricDto>> DownloadRemoteLyrics(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromRoute, Required] string lyricId)
|
||||||
|
{
|
||||||
|
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||||
|
if (audio is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (downloadedLyrics is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
|
return Ok(downloadedLyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the remote lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lyricId">The remote provider item id.</param>
|
||||||
|
/// <response code="200">File returned.</response>
|
||||||
|
/// <response code="404">Lyric not found.</response>
|
||||||
|
/// <returns>A <see cref="FileStreamResult"/> with the lyric file.</returns>
|
||||||
|
[HttpGet("Providers/Lyrics/{lyricId}")]
|
||||||
|
[Authorize(Policy = Policies.LyricManagement)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<LyricDto>> GetRemoteLyrics([FromRoute, Required] string lyricId)
|
||||||
|
{
|
||||||
|
var result = await _lyricManager.GetRemoteLyricsAsync(lyricId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,6 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Models.SubtitleDtos;
|
using Jellyfin.Api.Models.SubtitleDtos;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
@ -407,22 +406,29 @@ public class SubtitleController : BaseJellyfinApiController
|
|||||||
[FromBody, Required] UploadSubtitleDto body)
|
[FromBody, Required] UploadSubtitleDto body)
|
||||||
{
|
{
|
||||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||||
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
|
||||||
await using (stream.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
await _subtitleManager.UploadSubtitle(
|
|
||||||
video,
|
|
||||||
new SubtitleResponse
|
|
||||||
{
|
|
||||||
Format = body.Format,
|
|
||||||
Language = body.Language,
|
|
||||||
IsForced = body.IsForced,
|
|
||||||
IsHearingImpaired = body.IsHearingImpaired,
|
|
||||||
Stream = stream
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
|
||||||
|
|
||||||
return NoContent();
|
var bytes = Encoding.UTF8.GetBytes(body.Data);
|
||||||
|
var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
|
||||||
|
await using (memoryStream.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
using var transform = new FromBase64Transform();
|
||||||
|
var stream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read);
|
||||||
|
await using (stream.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await _subtitleManager.UploadSubtitle(
|
||||||
|
video,
|
||||||
|
new SubtitleResponse
|
||||||
|
{
|
||||||
|
Format = body.Format,
|
||||||
|
Language = body.Language,
|
||||||
|
IsForced = body.IsForced,
|
||||||
|
IsHearingImpaired = body.IsHearingImpaired,
|
||||||
|
Stream = stream
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ using MediaBrowser.Controller.Providers;
|
|||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -539,48 +540,4 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
|
|
||||||
return _userDataRepository.GetUserDataDto(item, user);
|
return _userDataRepository.GetUserDataDto(item, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an item's lyrics.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">User id.</param>
|
|
||||||
/// <param name="itemId">Item id.</param>
|
|
||||||
/// <response code="200">Lyrics returned.</response>
|
|
||||||
/// <response code="404">Something went wrong. No Lyrics will be returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
|
|
||||||
[HttpGet("Users/{userId}/Items/{itemId}/Lyrics")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<ActionResult<LyricResponse>> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
|
||||||
{
|
|
||||||
var user = _userManager.GetUserById(userId);
|
|
||||||
|
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = itemId.IsEmpty()
|
|
||||||
? _libraryManager.GetUserRootFolder()
|
|
||||||
: _libraryManager.GetItemById(itemId);
|
|
||||||
|
|
||||||
if (item is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is not UserRootFolder
|
|
||||||
// Check the item is visible for the user
|
|
||||||
&& !item.IsVisible(user))
|
|
||||||
{
|
|
||||||
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
|
|
||||||
if (result is not null)
|
|
||||||
{
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -506,6 +506,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
|
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
|
||||||
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
|
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
|
||||||
Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
|
Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
|
||||||
|
Permissions.Add(new Permission(PermissionKind.EnableLyricManagement, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -118,6 +118,11 @@ namespace Jellyfin.Data.Enums
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the user can edit subtitles.
|
/// Whether the user can edit subtitles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
EnableSubtitleManagement = 22
|
EnableSubtitleManagement = 22,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the user can edit lyrics.
|
||||||
|
/// </summary>
|
||||||
|
EnableLyricManagement = 23,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Events;
|
||||||
|
using MediaBrowser.Controller.Lyrics;
|
||||||
|
using MediaBrowser.Model.Activity;
|
||||||
|
using MediaBrowser.Model.Globalization;
|
||||||
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Events.Consumers.Library;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an entry in the activity log whenever a lyric download fails.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricDownloadFailureLogger : IEventConsumer<LyricDownloadFailureEventArgs>
|
||||||
|
{
|
||||||
|
private readonly ILocalizationManager _localizationManager;
|
||||||
|
private readonly IActivityManager _activityManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LyricDownloadFailureLogger"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizationManager">The localization manager.</param>
|
||||||
|
/// <param name="activityManager">The activity manager.</param>
|
||||||
|
public LyricDownloadFailureLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
|
||||||
|
{
|
||||||
|
_localizationManager = localizationManager;
|
||||||
|
_activityManager = activityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task OnEvent(LyricDownloadFailureEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
await _activityManager.CreateAsync(new ActivityLog(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
_localizationManager.GetLocalizedString("LyricDownloadFailureFromForItem"),
|
||||||
|
eventArgs.Provider,
|
||||||
|
GetItemName(eventArgs.Item)),
|
||||||
|
"LyricDownloadFailure",
|
||||||
|
Guid.Empty)
|
||||||
|
{
|
||||||
|
ItemId = eventArgs.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||||
|
ShortOverview = eventArgs.Exception.Message
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetItemName(BaseItem item)
|
||||||
|
{
|
||||||
|
var name = item.Name;
|
||||||
|
if (item is Episode episode)
|
||||||
|
{
|
||||||
|
if (episode.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
name = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Ep{0} - {1}",
|
||||||
|
episode.IndexNumber.Value,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episode.ParentIndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
name = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"S{0}, {1}",
|
||||||
|
episode.ParentIndexNumber.Value,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is IHasSeries hasSeries)
|
||||||
|
{
|
||||||
|
name = hasSeries.SeriesName + " - " + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||||
|
{
|
||||||
|
var artists = hasAlbumArtist.AlbumArtists;
|
||||||
|
|
||||||
|
if (artists.Count > 0)
|
||||||
|
{
|
||||||
|
name = artists[0] + " - " + name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item is IHasArtist hasArtist)
|
||||||
|
{
|
||||||
|
var artists = hasArtist.Artists;
|
||||||
|
|
||||||
|
if (artists.Count > 0)
|
||||||
|
{
|
||||||
|
name = artists[0] + " - " + name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ using MediaBrowser.Controller.Events.Authentication;
|
|||||||
using MediaBrowser.Controller.Events.Session;
|
using MediaBrowser.Controller.Events.Session;
|
||||||
using MediaBrowser.Controller.Events.Updates;
|
using MediaBrowser.Controller.Events.Updates;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -30,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Events
|
|||||||
public static void AddEventServices(this IServiceCollection collection)
|
public static void AddEventServices(this IServiceCollection collection)
|
||||||
{
|
{
|
||||||
// Library consumers
|
// Library consumers
|
||||||
|
collection.AddScoped<IEventConsumer<LyricDownloadFailureEventArgs>, LyricDownloadFailureLogger>();
|
||||||
collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
|
collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
|
||||||
|
|
||||||
// Security consumers
|
// Security consumers
|
||||||
|
@ -688,6 +688,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
|
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
|
||||||
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
|
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
|
||||||
user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
|
user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
|
||||||
|
user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
|
||||||
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
|
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
|
||||||
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
|
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ using Microsoft.OpenApi.Interfaces;
|
|||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
||||||
using IPNetwork = System.Net.IPNetwork;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Extensions
|
namespace Jellyfin.Server.Extensions
|
||||||
{
|
{
|
||||||
@ -83,6 +82,7 @@ namespace Jellyfin.Server.Extensions
|
|||||||
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
|
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
|
||||||
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
|
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
|
||||||
options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
|
options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
|
||||||
|
options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricManagement));
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.RequiresElevation,
|
Policies.RequiresElevation,
|
||||||
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
|
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
|
||||||
|
@ -89,4 +89,9 @@ public static class Policies
|
|||||||
/// Policy name for accessing subtitles management.
|
/// Policy name for accessing subtitles management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SubtitleManagement = "SubtitleManagement";
|
public const string SubtitleManagement = "SubtitleManagement";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for accessing lyric management.
|
||||||
|
/// </summary>
|
||||||
|
public const string LyricManagement = "LyricManagement";
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
@ -27,6 +28,7 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
{
|
{
|
||||||
Artists = Array.Empty<string>();
|
Artists = Array.Empty<string>();
|
||||||
AlbumArtists = Array.Empty<string>();
|
AlbumArtists = Array.Empty<string>();
|
||||||
|
LyricFiles = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -65,6 +67,16 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override MediaType MediaType => MediaType.Audio;
|
public override MediaType MediaType => MediaType.Audio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this audio has lyrics.
|
||||||
|
/// </summary>
|
||||||
|
public bool? HasLyrics { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of lyric paths.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> LyricFiles { get; set; }
|
||||||
|
|
||||||
public override double GetDefaultPrimaryImageAspectRatio()
|
public override double GetDefaultPrimaryImageAspectRatio()
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -168,6 +168,15 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns>BaseItem.</returns>
|
/// <returns>BaseItem.</returns>
|
||||||
BaseItem GetItemById(Guid id);
|
BaseItem GetItemById(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item by id, as T.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The item id.</param>
|
||||||
|
/// <typeparam name="T">The type of item.</typeparam>
|
||||||
|
/// <returns>The item.</returns>
|
||||||
|
T GetItemById<T>(Guid id)
|
||||||
|
where T : BaseItem;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the intros.
|
/// Gets the intros.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
using MediaBrowser.Model.Providers;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Lyrics;
|
namespace MediaBrowser.Controller.Lyrics;
|
||||||
|
|
||||||
@ -9,16 +16,93 @@ namespace MediaBrowser.Controller.Lyrics;
|
|||||||
public interface ILyricManager
|
public interface ILyricManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the lyrics.
|
/// Occurs when a lyric download fails.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The media item.</param>
|
event EventHandler<LyricDownloadFailureEventArgs> LyricDownloadFailure;
|
||||||
/// <returns>A task representing found lyrics the passed item.</returns>
|
|
||||||
Task<LyricResponse?> GetLyrics(BaseItem item);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if requested item has a matching local lyric file.
|
/// Search for lyrics for the specified song.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The media item.</param>
|
/// <param name="audio">The song.</param>
|
||||||
/// <returns>True if item has a matching lyric file; otherwise false.</returns>
|
/// <param name="isAutomated">Whether the request is automated.</param>
|
||||||
bool HasLyricFile(BaseItem item);
|
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||||
|
/// <returns>The list of lyrics.</returns>
|
||||||
|
Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(
|
||||||
|
Audio audio,
|
||||||
|
bool isAutomated,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The search request.</param>
|
||||||
|
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||||
|
/// <returns>The list of lyrics.</returns>
|
||||||
|
Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(
|
||||||
|
LyricSearchRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The audio.</param>
|
||||||
|
/// <param name="lyricId">The remote lyric id.</param>
|
||||||
|
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||||
|
/// <returns>The downloaded lyrics.</returns>
|
||||||
|
Task<LyricDto?> DownloadLyricsAsync(
|
||||||
|
Audio audio,
|
||||||
|
string lyricId,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The audio.</param>
|
||||||
|
/// <param name="libraryOptions">The library options to use.</param>
|
||||||
|
/// <param name="lyricId">The remote lyric id.</param>
|
||||||
|
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||||
|
/// <returns>The downloaded lyrics.</returns>
|
||||||
|
Task<LyricDto?> DownloadLyricsAsync(
|
||||||
|
Audio audio,
|
||||||
|
LibraryOptions libraryOptions,
|
||||||
|
string lyricId,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload new lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The audio file the lyrics belong to.</param>
|
||||||
|
/// <param name="lyricResponse">The lyric response.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the remote lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The remote lyrics id.</param>
|
||||||
|
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||||
|
/// <returns>The lyric response.</returns>
|
||||||
|
Task<LyricDto?> GetRemoteLyricsAsync(string id, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The audio file to remove lyrics from.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task DeleteLyricsAsync(Audio audio);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the list of lyric providers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
/// <returns>Lyric providers.</returns>
|
||||||
|
IReadOnlyList<LyricProviderInfo> GetSupportedProviders(BaseItem item);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the existing lyric for the audio.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The audio item.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>The parsed lyric model.</returns>
|
||||||
|
Task<LyricDto?> GetLyricsAsync(Audio audio, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Providers.Lyric;
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Lyrics;
|
namespace MediaBrowser.Controller.Lyrics;
|
||||||
|
|
||||||
@ -24,5 +24,5 @@ public interface ILyricParser
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lyrics">The raw lyrics content.</param>
|
/// <param name="lyrics">The raw lyrics content.</param>
|
||||||
/// <returns>The parsed lyrics or null if invalid.</returns>
|
/// <returns>The parsed lyrics or null if invalid.</returns>
|
||||||
LyricResponse? ParseLyrics(LyricFile lyrics);
|
LyricDto? ParseLyrics(LyricFile lyrics);
|
||||||
}
|
}
|
||||||
|
34
MediaBrowser.Controller/Lyrics/ILyricProvider.cs
Normal file
34
MediaBrowser.Controller/Lyrics/ILyricProvider.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
using MediaBrowser.Model.Providers;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface ILyricsProvider.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILyricProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the provider name.
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The search request.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>The list of remote lyrics.</returns>
|
||||||
|
Task<IEnumerable<RemoteLyricInfo>> SearchAsync(LyricSearchRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The remote lyric id.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>The lyric response.</returns>
|
||||||
|
Task<LyricResponse?> GetLyricsAsync(string id, CancellationToken cancellationToken);
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Lyrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event that occurs when subtitle downloading fails.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricDownloadFailureEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the item.
|
||||||
|
/// </summary>
|
||||||
|
public required BaseItem Item { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the provider.
|
||||||
|
/// </summary>
|
||||||
|
public required string Provider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the exception.
|
||||||
|
/// </summary>
|
||||||
|
public required Exception Exception { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Configuration
|
namespace MediaBrowser.Model.Configuration
|
||||||
{
|
{
|
||||||
@ -20,6 +21,7 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
AutomaticallyAddToCollection = false;
|
AutomaticallyAddToCollection = false;
|
||||||
EnablePhotos = true;
|
EnablePhotos = true;
|
||||||
SaveSubtitlesWithMedia = true;
|
SaveSubtitlesWithMedia = true;
|
||||||
|
SaveLyricsWithMedia = true;
|
||||||
PathInfos = Array.Empty<MediaPathInfo>();
|
PathInfos = Array.Empty<MediaPathInfo>();
|
||||||
EnableAutomaticSeriesGrouping = true;
|
EnableAutomaticSeriesGrouping = true;
|
||||||
SeasonZeroDisplayName = "Specials";
|
SeasonZeroDisplayName = "Specials";
|
||||||
@ -92,6 +94,9 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
|
|
||||||
public bool SaveSubtitlesWithMedia { get; set; }
|
public bool SaveSubtitlesWithMedia { get; set; }
|
||||||
|
|
||||||
|
[DefaultValue(true)]
|
||||||
|
public bool SaveLyricsWithMedia { get; set; }
|
||||||
|
|
||||||
public bool AutomaticallyAddToCollection { get; set; }
|
public bool AutomaticallyAddToCollection { get; set; }
|
||||||
|
|
||||||
public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
|
public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
|
||||||
|
@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
LocalMetadataProvider,
|
LocalMetadataProvider,
|
||||||
MetadataFetcher,
|
MetadataFetcher,
|
||||||
MetadataSaver,
|
MetadataSaver,
|
||||||
SubtitleFetcher
|
SubtitleFetcher,
|
||||||
|
LyricFetcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
Audio = 0,
|
Audio = 0,
|
||||||
Video = 1,
|
Video = 1,
|
||||||
Photo = 2,
|
Photo = 2,
|
||||||
Subtitle = 3
|
Subtitle = 3,
|
||||||
|
Lyric = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,11 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data.
|
/// The data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Data
|
Data,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The lyric.
|
||||||
|
/// </summary>
|
||||||
|
Lyric
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Lyrics;
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// LyricResponse model.
|
/// LyricResponse model.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LyricResponse
|
public class LyricDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets Metadata for the lyrics.
|
/// Gets or sets Metadata for the lyrics.
|
||||||
@ -16,5 +15,5 @@ public class LyricResponse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a collection of individual lyric lines.
|
/// Gets or sets a collection of individual lyric lines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<LyricLine> Lyrics { get; set; } = Array.Empty<LyricLine>();
|
public IReadOnlyList<LyricLine> Lyrics { get; set; } = [];
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace MediaBrowser.Providers.Lyric;
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The information for a raw lyrics file before parsing.
|
/// The information for a raw lyrics file before parsing.
|
@ -1,4 +1,4 @@
|
|||||||
namespace MediaBrowser.Controller.Lyrics;
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lyric model.
|
/// Lyric model.
|
@ -1,4 +1,4 @@
|
|||||||
namespace MediaBrowser.Controller.Lyrics;
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// LyricMetadata model.
|
/// LyricMetadata model.
|
||||||
@ -49,4 +49,9 @@ public class LyricMetadata
|
|||||||
/// Gets or sets the version of the creator used.
|
/// Gets or sets the version of the creator used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Version { get; set; }
|
public string? Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this lyric is synced.
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsSynced { get; set; }
|
||||||
}
|
}
|
19
MediaBrowser.Model/Lyrics/LyricResponse.cs
Normal file
19
MediaBrowser.Model/Lyrics/LyricResponse.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LyricResponse model.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the lyric stream.
|
||||||
|
/// </summary>
|
||||||
|
public required Stream Stream { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the lyric format.
|
||||||
|
/// </summary>
|
||||||
|
public required string Format { get; set; }
|
||||||
|
}
|
59
MediaBrowser.Model/Lyrics/LyricSearchRequest.cs
Normal file
59
MediaBrowser.Model/Lyrics/LyricSearchRequest.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lyric search request.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricSearchRequest : IHasProviderIds
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the media path.
|
||||||
|
/// </summary>
|
||||||
|
public string? MediaPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the artist name.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string>? ArtistNames { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the album name.
|
||||||
|
/// </summary>
|
||||||
|
public string? AlbumName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the song name.
|
||||||
|
/// </summary>
|
||||||
|
public string? SongName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the track duration in ticks.
|
||||||
|
/// </summary>
|
||||||
|
public long? Duration { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, string> ProviderIds { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to search all providers.
|
||||||
|
/// </summary>
|
||||||
|
public bool SearchAllProviders { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of disabled lyric fetcher names.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> DisabledLyricFetchers { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the order of lyric fetchers.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> LyricFetcherOrder { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this request is automated.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAutomated { get; set; }
|
||||||
|
}
|
22
MediaBrowser.Model/Lyrics/RemoteLyricInfoDto.cs
Normal file
22
MediaBrowser.Model/Lyrics/RemoteLyricInfoDto.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The remote lyric info dto.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteLyricInfoDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id for the lyric.
|
||||||
|
/// </summary>
|
||||||
|
public required string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the provider name.
|
||||||
|
/// </summary>
|
||||||
|
public required string ProviderName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
public required LyricDto Lyrics { get; init; }
|
||||||
|
}
|
16
MediaBrowser.Model/Lyrics/UploadLyricDto.cs
Normal file
16
MediaBrowser.Model/Lyrics/UploadLyricDto.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload lyric dto.
|
||||||
|
/// </summary>
|
||||||
|
public class UploadLyricDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the lyrics file.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public IFormFile Lyrics { get; set; } = null!;
|
||||||
|
}
|
17
MediaBrowser.Model/Providers/LyricProviderInfo.cs
Normal file
17
MediaBrowser.Model/Providers/LyricProviderInfo.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace MediaBrowser.Model.Providers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lyric provider info.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricProviderInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the provider name.
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the provider id.
|
||||||
|
/// </summary>
|
||||||
|
public required string Id { get; init; }
|
||||||
|
}
|
29
MediaBrowser.Model/Providers/RemoteLyricInfo.cs
Normal file
29
MediaBrowser.Model/Providers/RemoteLyricInfo.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Model.Providers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The remote lyric info.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteLyricInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id for the lyric.
|
||||||
|
/// </summary>
|
||||||
|
public required string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the provider name.
|
||||||
|
/// </summary>
|
||||||
|
public required string ProviderName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the lyric metadata.
|
||||||
|
/// </summary>
|
||||||
|
public required LyricMetadata Metadata { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the lyrics.
|
||||||
|
/// </summary>
|
||||||
|
public required LyricResponse Lyrics { get; init; }
|
||||||
|
}
|
@ -92,6 +92,12 @@ namespace MediaBrowser.Model.Users
|
|||||||
[DefaultValue(false)]
|
[DefaultValue(false)]
|
||||||
public bool EnableSubtitleManagement { get; set; }
|
public bool EnableSubtitleManagement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this user can manage lyrics.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool EnableLyricManagement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this instance is disabled.
|
/// Gets or sets a value indicating whether this instance is disabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Resolvers;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Lyric;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class DefaultLyricProvider : ILyricProvider
|
|
||||||
{
|
|
||||||
private static readonly string[] _lyricExtensions = { ".lrc", ".elrc", ".txt" };
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Name => "DefaultLyricProvider";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ResolverPriority Priority => ResolverPriority.First;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool HasLyrics(BaseItem item)
|
|
||||||
{
|
|
||||||
var path = GetLyricsPath(item);
|
|
||||||
return path is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<LyricFile?> GetLyrics(BaseItem item)
|
|
||||||
{
|
|
||||||
var path = GetLyricsPath(item);
|
|
||||||
if (path is not null)
|
|
||||||
{
|
|
||||||
var content = await File.ReadAllTextAsync(path).ConfigureAwait(false);
|
|
||||||
if (!string.IsNullOrEmpty(content))
|
|
||||||
{
|
|
||||||
return new LyricFile(path, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetLyricsPath(BaseItem item)
|
|
||||||
{
|
|
||||||
// Ensure the path to the item is not null
|
|
||||||
string? itemDirectoryPath = Path.GetDirectoryName(item.Path);
|
|
||||||
if (itemDirectoryPath is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the directory path exists
|
|
||||||
if (!Directory.Exists(itemDirectoryPath))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(item.Path)}.*"))
|
|
||||||
{
|
|
||||||
if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return lyricFilePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Resolvers;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Lyric;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interface ILyricsProvider.
|
|
||||||
/// </summary>
|
|
||||||
public interface ILyricProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating the provider name.
|
|
||||||
/// </summary>
|
|
||||||
string Name { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the priority.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The priority.</value>
|
|
||||||
ResolverPriority Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if an item has lyrics available.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The media item.</param>
|
|
||||||
/// <returns>Whether lyrics where found or not.</returns>
|
|
||||||
bool HasLyrics(BaseItem item);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the lyrics.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The media item.</param>
|
|
||||||
/// <returns>A task representing found lyrics.</returns>
|
|
||||||
Task<LyricFile?> GetLyrics(BaseItem item);
|
|
||||||
}
|
|
@ -8,6 +8,7 @@ using LrcParser.Model;
|
|||||||
using LrcParser.Parser;
|
using LrcParser.Parser;
|
||||||
using MediaBrowser.Controller.Lyrics;
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Lyric;
|
namespace MediaBrowser.Providers.Lyric;
|
||||||
|
|
||||||
@ -18,8 +19,8 @@ public class LrcLyricParser : ILyricParser
|
|||||||
{
|
{
|
||||||
private readonly LyricParser _lrcLyricParser;
|
private readonly LyricParser _lrcLyricParser;
|
||||||
|
|
||||||
private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc" };
|
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc"];
|
||||||
private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" };
|
private static readonly string[] _acceptedTimeFormats = ["HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
|
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
|
||||||
@ -39,7 +40,7 @@ public class LrcLyricParser : ILyricParser
|
|||||||
public ResolverPriority Priority => ResolverPriority.Fourth;
|
public ResolverPriority Priority => ResolverPriority.Fourth;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LyricResponse? ParseLyrics(LyricFile lyrics)
|
public LyricDto? ParseLyrics(LyricFile lyrics)
|
||||||
{
|
{
|
||||||
if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -95,7 +96,7 @@ public class LrcLyricParser : ILyricParser
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LyricLine> lyricList = new();
|
List<LyricLine> lyricList = [];
|
||||||
|
|
||||||
for (int i = 0; i < sortedLyricData.Count; i++)
|
for (int i = 0; i < sortedLyricData.Count; i++)
|
||||||
{
|
{
|
||||||
@ -106,7 +107,7 @@ public class LrcLyricParser : ILyricParser
|
|||||||
}
|
}
|
||||||
|
|
||||||
long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks;
|
long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks;
|
||||||
lyricList.Add(new LyricLine(sortedLyricData[i].Text, ticks));
|
lyricList.Add(new LyricLine(sortedLyricData[i].Text.Trim(), ticks));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileMetaData.Count != 0)
|
if (fileMetaData.Count != 0)
|
||||||
@ -114,10 +115,10 @@ public class LrcLyricParser : ILyricParser
|
|||||||
// Map metaData values from LRC file to LyricMetadata properties
|
// Map metaData values from LRC file to LyricMetadata properties
|
||||||
LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
|
LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
|
||||||
|
|
||||||
return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList };
|
return new LyricDto { Metadata = lyricMetadata, Lyrics = lyricList };
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LyricResponse { Lyrics = lyricList };
|
return new LyricDto { Lyrics = lyricList };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,8 +1,25 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Lyrics;
|
using MediaBrowser.Controller.Lyrics;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
using MediaBrowser.Model.Providers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Lyric;
|
namespace MediaBrowser.Providers.Lyric;
|
||||||
|
|
||||||
@ -11,37 +28,246 @@ namespace MediaBrowser.Providers.Lyric;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class LyricManager : ILyricManager
|
public class LyricManager : ILyricManager
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<LyricManager> _logger;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILibraryMonitor _libraryMonitor;
|
||||||
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
|
||||||
private readonly ILyricProvider[] _lyricProviders;
|
private readonly ILyricProvider[] _lyricProviders;
|
||||||
private readonly ILyricParser[] _lyricParsers;
|
private readonly ILyricParser[] _lyricParsers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LyricManager"/> class.
|
/// Initializes a new instance of the <see cref="LyricManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lyricProviders">All found lyricProviders.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{LyricManager}"/> interface.</param>
|
||||||
/// <param name="lyricParsers">All found lyricParsers.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers)
|
/// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
|
||||||
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
|
/// <param name="lyricProviders">The list of <see cref="ILyricProvider"/>.</param>
|
||||||
|
/// <param name="lyricParsers">The list of <see cref="ILyricParser"/>.</param>
|
||||||
|
public LyricManager(
|
||||||
|
ILogger<LyricManager> logger,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ILibraryMonitor libraryMonitor,
|
||||||
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
IEnumerable<ILyricProvider> lyricProviders,
|
||||||
|
IEnumerable<ILyricParser> lyricParsers)
|
||||||
{
|
{
|
||||||
_lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray();
|
_logger = logger;
|
||||||
_lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray();
|
_fileSystem = fileSystem;
|
||||||
|
_libraryMonitor = libraryMonitor;
|
||||||
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_lyricProviders = lyricProviders
|
||||||
|
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
|
||||||
|
.ToArray();
|
||||||
|
_lyricParsers = lyricParsers
|
||||||
|
.OrderBy(l => l.Priority)
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<LyricResponse?> GetLyrics(BaseItem item)
|
public event EventHandler<LyricDownloadFailureEventArgs>? LyricDownloadFailure;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(Audio audio, bool isAutomated, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (ILyricProvider provider in _lyricProviders)
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
|
||||||
|
var request = new LyricSearchRequest
|
||||||
{
|
{
|
||||||
var lyrics = await provider.GetLyrics(item).ConfigureAwait(false);
|
MediaPath = audio.Path,
|
||||||
if (lyrics is null)
|
SongName = audio.Name,
|
||||||
|
AlbumName = audio.Album,
|
||||||
|
ArtistNames = audio.GetAllArtists().ToList(),
|
||||||
|
Duration = audio.RunTimeTicks,
|
||||||
|
IsAutomated = isAutomated
|
||||||
|
};
|
||||||
|
|
||||||
|
return SearchLyricsAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(LyricSearchRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
var providers = _lyricProviders
|
||||||
|
.Where(i => !request.DisabledLyricFetchers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(i =>
|
||||||
{
|
{
|
||||||
continue;
|
var index = request.LyricFetcherOrder.IndexOf(i.Name);
|
||||||
|
return index == -1 ? int.MaxValue : index;
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// If not searching all, search one at a time until something is found
|
||||||
|
if (!request.SearchAllProviders)
|
||||||
|
{
|
||||||
|
foreach (var provider in providers)
|
||||||
|
{
|
||||||
|
var providerResult = await InternalSearchProviderAsync(provider, request, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (providerResult.Count > 0)
|
||||||
|
{
|
||||||
|
return providerResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ILyricParser parser in _lyricParsers)
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = providers.Select(async provider => await InternalSearchProviderAsync(provider, request, cancellationToken).ConfigureAwait(false));
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return results.SelectMany(i => i).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<LyricDto?> DownloadLyricsAsync(Audio audio, string lyricId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(lyricId);
|
||||||
|
|
||||||
|
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(audio);
|
||||||
|
|
||||||
|
return DownloadLyricsAsync(audio, libraryOptions, lyricId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<LyricDto?> DownloadLyricsAsync(Audio audio, LibraryOptions libraryOptions, string lyricId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
ArgumentNullException.ThrowIfNull(libraryOptions);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(lyricId);
|
||||||
|
|
||||||
|
var provider = GetProvider(lyricId.AsSpan().LeftPart('_').ToString());
|
||||||
|
if (provider is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await InternalGetRemoteLyricsAsync(lyricId, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (response is null)
|
||||||
{
|
{
|
||||||
var result = parser.ParseLyrics(lyrics);
|
_logger.LogDebug("Unable to download lyrics for {LyricId}", lyricId);
|
||||||
if (result is not null)
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedLyrics = await InternalParseRemoteLyricsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (parsedLyrics is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await TrySaveLyric(audio, libraryOptions, response).ConfigureAwait(false);
|
||||||
|
return parsedLyrics;
|
||||||
|
}
|
||||||
|
catch (RateLimitExceededException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LyricDownloadFailure?.Invoke(this, new LyricDownloadFailureEventArgs
|
||||||
|
{
|
||||||
|
Item = audio,
|
||||||
|
Exception = ex,
|
||||||
|
Provider = provider.Name
|
||||||
|
});
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
ArgumentNullException.ThrowIfNull(lyricResponse);
|
||||||
|
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(audio);
|
||||||
|
|
||||||
|
var parsed = await InternalParseRemoteLyricsAsync(lyricResponse, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (parsed is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await TrySaveLyric(audio, libraryOptions, lyricResponse).ConfigureAwait(false);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<LyricDto?> GetRemoteLyricsAsync(string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||||
|
|
||||||
|
var lyricResponse = await InternalGetRemoteLyricsAsync(id, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (lyricResponse is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await InternalParseRemoteLyricsAsync(lyricResponse, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task DeleteLyricsAsync(Audio audio)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
var streams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
|
||||||
|
{
|
||||||
|
ItemId = audio.Id,
|
||||||
|
Type = MediaStreamType.Lyric
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var stream in streams)
|
||||||
|
{
|
||||||
|
var path = stream.Path;
|
||||||
|
_libraryMonitor.ReportFileSystemChangeBeginning(path);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_libraryMonitor.ReportFileSystemChangeComplete(path, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio.RefreshMetadata(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<LyricProviderInfo> GetSupportedProviders(BaseItem item)
|
||||||
|
{
|
||||||
|
if (item is not Audio)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _lyricProviders.Select(p => new LyricProviderInfo { Name = p.Name, Id = GetProviderId(p.Name) }).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<LyricDto?> GetLyricsAsync(Audio audio, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(audio);
|
||||||
|
|
||||||
|
var lyricStreams = audio.GetMediaStreams().Where(s => s.Type == MediaStreamType.Lyric);
|
||||||
|
foreach (var lyricStream in lyricStreams)
|
||||||
|
{
|
||||||
|
var lyricContents = await File.ReadAllTextAsync(lyricStream.Path, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var lyricFile = new LyricFile(Path.GetFileName(lyricStream.Path), lyricContents);
|
||||||
|
foreach (var parser in _lyricParsers)
|
||||||
|
{
|
||||||
|
var parsedLyrics = parser.ParseLyrics(lyricFile);
|
||||||
|
if (parsedLyrics is not null)
|
||||||
{
|
{
|
||||||
return result;
|
return parsedLyrics;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,22 +275,180 @@ public class LyricManager : ILyricManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private ILyricProvider? GetProvider(string providerId)
|
||||||
public bool HasLyricFile(BaseItem item)
|
|
||||||
{
|
{
|
||||||
foreach (ILyricProvider provider in _lyricProviders)
|
var provider = _lyricProviders.FirstOrDefault(p => string.Equals(providerId, GetProviderId(p.Name), StringComparison.Ordinal));
|
||||||
|
if (provider is null)
|
||||||
{
|
{
|
||||||
if (item is null)
|
_logger.LogWarning("Unknown provider id: {ProviderId}", providerId.ReplaceLineEndings(string.Empty));
|
||||||
{
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.HasLyrics(item))
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetProviderId(string name)
|
||||||
|
=> name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
private async Task<LyricDto?> InternalParseRemoteLyricsAsync(LyricResponse lyricResponse, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
lyricResponse.Stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
using var streamReader = new StreamReader(lyricResponse.Stream, leaveOpen: true);
|
||||||
|
var lyrics = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var lyricFile = new LyricFile($"lyric.{lyricResponse.Format}", lyrics);
|
||||||
|
foreach (var parser in _lyricParsers)
|
||||||
|
{
|
||||||
|
var parsedLyrics = parser.ParseLyrics(lyricFile);
|
||||||
|
if (parsedLyrics is not null)
|
||||||
{
|
{
|
||||||
return true;
|
return parsedLyrics;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<LyricResponse?> InternalGetRemoteLyricsAsync(string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||||
|
var parts = id.Split('_', 2);
|
||||||
|
var provider = GetProvider(parts[0]);
|
||||||
|
if (provider is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
id = parts[^1];
|
||||||
|
|
||||||
|
return await provider.GetLyricsAsync(id, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<RemoteLyricInfoDto>> InternalSearchProviderAsync(
|
||||||
|
ILyricProvider provider,
|
||||||
|
LyricSearchRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var providerId = GetProviderId(provider.Name);
|
||||||
|
var searchResults = await provider.SearchAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
var parsedResults = new List<RemoteLyricInfoDto>();
|
||||||
|
foreach (var result in searchResults)
|
||||||
|
{
|
||||||
|
var parsedLyrics = await InternalParseRemoteLyricsAsync(result.Lyrics, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (parsedLyrics is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedLyrics.Metadata = result.Metadata;
|
||||||
|
parsedResults.Add(new RemoteLyricInfoDto
|
||||||
|
{
|
||||||
|
Id = $"{providerId}_{result.Id}",
|
||||||
|
ProviderName = result.ProviderName,
|
||||||
|
Lyrics = parsedLyrics
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedResults;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error downloading lyrics from {Provider}", provider.Name);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TrySaveLyric(
|
||||||
|
Audio audio,
|
||||||
|
LibraryOptions libraryOptions,
|
||||||
|
LyricResponse lyricResponse)
|
||||||
|
{
|
||||||
|
var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia;
|
||||||
|
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
await using (memoryStream.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var stream = lyricResponse.Stream;
|
||||||
|
|
||||||
|
await using (stream.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
var savePaths = new List<string>();
|
||||||
|
var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + lyricResponse.Format.ReplaceLineEndings(string.Empty).ToLowerInvariant();
|
||||||
|
|
||||||
|
if (saveInMediaFolder)
|
||||||
|
{
|
||||||
|
var mediaFolderPath = Path.GetFullPath(Path.Combine(audio.ContainingFolderPath, saveFileName));
|
||||||
|
// TODO: Add some error handling to the API user: return BadRequest("Could not save lyric, bad path.");
|
||||||
|
if (mediaFolderPath.StartsWith(audio.ContainingFolderPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
savePaths.Add(mediaFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var internalPath = Path.GetFullPath(Path.Combine(audio.GetInternalMetadataPath(), saveFileName));
|
||||||
|
|
||||||
|
// TODO: Add some error to the user: return BadRequest("Could not save lyric, bad path.");
|
||||||
|
if (internalPath.StartsWith(audio.GetInternalMetadataPath(), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
savePaths.Add(internalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savePaths.Count > 0)
|
||||||
|
{
|
||||||
|
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("An uploaded lyric could not be saved because the resulting paths were invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
|
||||||
|
{
|
||||||
|
List<Exception>? exs = null;
|
||||||
|
|
||||||
|
foreach (var savePath in savePaths)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Saving lyrics to {SavePath}", savePath.ReplaceLineEndings(string.Empty));
|
||||||
|
|
||||||
|
_libraryMonitor.ReportFileSystemChangeBeginning(savePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||||
|
|
||||||
|
var fileOptions = AsyncFile.WriteOptions;
|
||||||
|
fileOptions.Mode = FileMode.Create;
|
||||||
|
fileOptions.PreallocationSize = stream.Length;
|
||||||
|
var fs = new FileStream(savePath, fileOptions);
|
||||||
|
await using (fs.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(fs).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
(exs ??= []).Add(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_libraryMonitor.ReportFileSystemChangeComplete(savePath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exs is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(exs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Lyrics;
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
|
using MediaBrowser.Model.Lyrics;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Lyric;
|
namespace MediaBrowser.Providers.Lyric;
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ namespace MediaBrowser.Providers.Lyric;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TxtLyricParser : ILyricParser
|
public class TxtLyricParser : ILyricParser
|
||||||
{
|
{
|
||||||
private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc", ".txt" };
|
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc", ".txt"];
|
||||||
private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" };
|
private static readonly string[] _lineBreakCharacters = ["\r\n", "\r", "\n"];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "TxtLyricProvider";
|
public string Name => "TxtLyricProvider";
|
||||||
@ -24,7 +25,7 @@ public class TxtLyricParser : ILyricParser
|
|||||||
public ResolverPriority Priority => ResolverPriority.Fifth;
|
public ResolverPriority Priority => ResolverPriority.Fifth;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LyricResponse? ParseLyrics(LyricFile lyrics)
|
public LyricDto? ParseLyrics(LyricFile lyrics)
|
||||||
{
|
{
|
||||||
if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -36,9 +37,9 @@ public class TxtLyricParser : ILyricParser
|
|||||||
|
|
||||||
for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++)
|
for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++)
|
||||||
{
|
{
|
||||||
lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]);
|
lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex].Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LyricResponse { Lyrics = lyricList };
|
return new LyricDto { Lyrics = lyricList };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ using MediaBrowser.Controller.Entities;
|
|||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
@ -52,6 +53,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
private readonly IServerApplicationPaths _appPaths;
|
private readonly IServerApplicationPaths _appPaths;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ISubtitleManager _subtitleManager;
|
private readonly ISubtitleManager _subtitleManager;
|
||||||
|
private readonly ILyricManager _lyricManager;
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
private readonly IBaseItemManager _baseItemManager;
|
private readonly IBaseItemManager _baseItemManager;
|
||||||
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
|
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
|
||||||
@ -78,6 +80,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
/// <param name="appPaths">The server application paths.</param>
|
/// <param name="appPaths">The server application paths.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
/// <param name="baseItemManager">The BaseItem manager.</param>
|
/// <param name="baseItemManager">The BaseItem manager.</param>
|
||||||
|
/// <param name="lyricManager">The lyric manager.</param>
|
||||||
public ProviderManager(
|
public ProviderManager(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ISubtitleManager subtitleManager,
|
ISubtitleManager subtitleManager,
|
||||||
@ -87,7 +90,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IServerApplicationPaths appPaths,
|
IServerApplicationPaths appPaths,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IBaseItemManager baseItemManager)
|
IBaseItemManager baseItemManager,
|
||||||
|
ILyricManager lyricManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
@ -98,6 +102,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_subtitleManager = subtitleManager;
|
_subtitleManager = subtitleManager;
|
||||||
_baseItemManager = baseItemManager;
|
_baseItemManager = baseItemManager;
|
||||||
|
_lyricManager = lyricManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -503,15 +508,22 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
|
AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
|
||||||
AddImagePlugins(pluginList, imageProviders);
|
AddImagePlugins(pluginList, imageProviders);
|
||||||
|
|
||||||
var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
|
|
||||||
|
|
||||||
// Subtitle fetchers
|
// Subtitle fetchers
|
||||||
|
var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
|
||||||
pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
|
pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
|
||||||
{
|
{
|
||||||
Name = i.Name,
|
Name = i.Name,
|
||||||
Type = MetadataPluginType.SubtitleFetcher
|
Type = MetadataPluginType.SubtitleFetcher
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Lyric fetchers
|
||||||
|
var lyricProviders = _lyricManager.GetSupportedProviders(dummy);
|
||||||
|
pluginList.AddRange(lyricProviders.Select(i => new MetadataPlugin
|
||||||
|
{
|
||||||
|
Name = i.Name,
|
||||||
|
Type = MetadataPluginType.LyricFetcher
|
||||||
|
}));
|
||||||
|
|
||||||
summary.Plugins = pluginList.ToArray();
|
summary.Plugins = pluginList.ToArray();
|
||||||
|
|
||||||
var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
|
var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
|
||||||
|
@ -35,6 +35,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
|
private readonly LyricResolver _lyricResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
||||||
@ -44,18 +45,21 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
||||||
public AudioFileProber(
|
public AudioFileProber(
|
||||||
ILogger<AudioFileProber> logger,
|
ILogger<AudioFileProber> logger,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
ILibraryManager libraryManager)
|
ILibraryManager libraryManager,
|
||||||
|
LyricResolver lyricResolver)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
_lyricResolver = lyricResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
||||||
@ -103,7 +107,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Fetch(item, result, cancellationToken);
|
Fetch(item, result, options, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||||
@ -205,8 +209,13 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="audio">The <see cref="Audio"/>.</param>
|
/// <param name="audio">The <see cref="Audio"/>.</param>
|
||||||
/// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
|
/// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
|
||||||
|
/// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||||
protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
|
protected void Fetch(
|
||||||
|
Audio audio,
|
||||||
|
Model.MediaInfo.MediaInfo mediaInfo,
|
||||||
|
MetadataRefreshOptions options,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
audio.Container = mediaInfo.Container;
|
audio.Container = mediaInfo.Container;
|
||||||
audio.TotalBitrate = mediaInfo.Bitrate;
|
audio.TotalBitrate = mediaInfo.Bitrate;
|
||||||
@ -219,7 +228,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
FetchDataFromTags(audio);
|
FetchDataFromTags(audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
|
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
|
||||||
|
AddExternalLyrics(audio, mediaStreams, options);
|
||||||
|
|
||||||
|
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||||
|
|
||||||
|
_itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -333,5 +347,17 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId);
|
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddExternalLyrics(
|
||||||
|
Audio audio,
|
||||||
|
List<MediaStream> currentStreams,
|
||||||
|
MetadataRefreshOptions options)
|
||||||
|
{
|
||||||
|
var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
|
||||||
|
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
||||||
|
|
||||||
|
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||||
|
currentStreams.AddRange(externalLyricFiles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
MediaBrowser.Providers/MediaInfo/LyricResolver.cs
Normal file
39
MediaBrowser.Providers/MediaInfo/LyricResolver.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Emby.Naming.Common;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
|
using MediaBrowser.Model.Globalization;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Providers.MediaInfo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves external lyric files for <see cref="Audio"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class LyricResolver : MediaInfoResolver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LyricResolver"/> class for external subtitle file processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="localizationManager">The localization manager.</param>
|
||||||
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
|
/// <param name="fileSystem">The file system.</param>
|
||||||
|
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||||
|
public LyricResolver(
|
||||||
|
ILogger<LyricResolver> logger,
|
||||||
|
ILocalizationManager localizationManager,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
NamingOptions namingOptions)
|
||||||
|
: base(
|
||||||
|
logger,
|
||||||
|
localizationManager,
|
||||||
|
mediaEncoder,
|
||||||
|
fileSystem,
|
||||||
|
namingOptions,
|
||||||
|
DlnaProfileType.Lyric)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.ExternalFiles;
|
using Emby.Naming.ExternalFiles;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
@ -148,7 +149,49 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaStreams.AsReadOnly();
|
return mediaStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the external streams for the provided audio.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The <see cref="Audio"/> object to search external streams for.</param>
|
||||||
|
/// <param name="startIndex">The stream index to start adding external streams at.</param>
|
||||||
|
/// <param name="directoryService">The directory service to search for files.</param>
|
||||||
|
/// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
|
||||||
|
/// <returns>The external streams located.</returns>
|
||||||
|
public IReadOnlyList<MediaStream> GetExternalStreams(
|
||||||
|
Audio audio,
|
||||||
|
int startIndex,
|
||||||
|
IDirectoryService directoryService,
|
||||||
|
bool clearCache)
|
||||||
|
{
|
||||||
|
if (!audio.IsFileProtocol)
|
||||||
|
{
|
||||||
|
return Array.Empty<MediaStream>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathInfos = GetExternalFiles(audio, directoryService, clearCache);
|
||||||
|
|
||||||
|
if (pathInfos.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<MediaStream>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaStreams = new MediaStream[pathInfos.Count];
|
||||||
|
|
||||||
|
for (var i = 0; i < pathInfos.Count; i++)
|
||||||
|
{
|
||||||
|
mediaStreams[i] = new MediaStream
|
||||||
|
{
|
||||||
|
Type = MediaStreamType.Lyric,
|
||||||
|
Path = pathInfos[i].Path,
|
||||||
|
Language = pathInfos[i].Language,
|
||||||
|
Index = startIndex++
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -209,6 +252,58 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
return externalPathInfos;
|
return externalPathInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the external file infos for the given audio.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audio">The <see cref="Audio"/> object to search external files for.</param>
|
||||||
|
/// <param name="directoryService">The directory service to search for files.</param>
|
||||||
|
/// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
|
||||||
|
/// <returns>The external file paths located.</returns>
|
||||||
|
public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
|
||||||
|
Audio audio,
|
||||||
|
IDirectoryService directoryService,
|
||||||
|
bool clearCache)
|
||||||
|
{
|
||||||
|
if (!audio.IsFileProtocol)
|
||||||
|
{
|
||||||
|
return Array.Empty<ExternalPathParserResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
string folder = audio.ContainingFolderPath;
|
||||||
|
var files = directoryService.GetFilePaths(folder, clearCache, true).ToList();
|
||||||
|
files.Remove(audio.Path);
|
||||||
|
var internalMetadataPath = audio.GetInternalMetadataPath();
|
||||||
|
if (_fileSystem.DirectoryExists(internalMetadataPath))
|
||||||
|
{
|
||||||
|
files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<ExternalPathParserResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var externalPathInfos = new List<ExternalPathParserResult>();
|
||||||
|
ReadOnlySpan<char> prefix = audio.FileNameWithoutExtension;
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
|
||||||
|
if (fileNameWithoutExtension.Length >= prefix.Length
|
||||||
|
&& prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
|
||||||
|
{
|
||||||
|
var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());
|
||||||
|
|
||||||
|
if (externalPathInfo is not null)
|
||||||
|
{
|
||||||
|
externalPathInfos.Add(externalPathInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return externalPathInfos;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the media info of the given file.
|
/// Returns the media info of the given file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -43,6 +43,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
private readonly ILogger<ProbeProvider> _logger;
|
private readonly ILogger<ProbeProvider> _logger;
|
||||||
private readonly AudioResolver _audioResolver;
|
private readonly AudioResolver _audioResolver;
|
||||||
private readonly SubtitleResolver _subtitleResolver;
|
private readonly SubtitleResolver _subtitleResolver;
|
||||||
|
private readonly LyricResolver _lyricResolver;
|
||||||
private readonly FFProbeVideoInfo _videoProber;
|
private readonly FFProbeVideoInfo _videoProber;
|
||||||
private readonly AudioFileProber _audioProber;
|
private readonly AudioFileProber _audioProber;
|
||||||
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
|
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
|
||||||
@ -79,9 +80,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
NamingOptions namingOptions)
|
NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger<ProbeProvider>();
|
_logger = loggerFactory.CreateLogger<ProbeProvider>();
|
||||||
_audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
|
|
||||||
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
|
_lyricResolver = new LyricResolver(loggerFactory.CreateLogger<LyricResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
|
|
||||||
_videoProber = new FFProbeVideoInfo(
|
_videoProber = new FFProbeVideoInfo(
|
||||||
loggerFactory.CreateLogger<FFProbeVideoInfo>(),
|
loggerFactory.CreateLogger<FFProbeVideoInfo>(),
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
@ -96,6 +98,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
libraryManager,
|
libraryManager,
|
||||||
_audioResolver,
|
_audioResolver,
|
||||||
_subtitleResolver);
|
_subtitleResolver);
|
||||||
|
|
||||||
|
_audioProber = new AudioFileProber(
|
||||||
|
loggerFactory.CreateLogger<AudioFileProber>(),
|
||||||
|
mediaSourceManager,
|
||||||
|
mediaEncoder,
|
||||||
|
itemRepo,
|
||||||
|
libraryManager,
|
||||||
|
_lyricResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -123,23 +133,37 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.SupportsLocalMetadata && video is not null && !video.IsPlaceHolder
|
if (video is not null
|
||||||
&& !video.SubtitleFiles.SequenceEqual(
|
&& item.SupportsLocalMetadata
|
||||||
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
&& !video.IsPlaceHolder)
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
if (!video.SubtitleFiles.SequenceEqual(
|
||||||
return true;
|
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
||||||
|
.Select(info => info.Path).ToList(),
|
||||||
|
StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!video.AudioFiles.SequenceEqual(
|
||||||
|
_audioResolver.GetExternalFiles(video, directoryService, false)
|
||||||
|
.Select(info => info.Path).ToList(),
|
||||||
|
StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.SupportsLocalMetadata && video is not null && !video.IsPlaceHolder
|
if (item is Audio audio
|
||||||
&& !video.AudioFiles.SequenceEqual(
|
&& item.SupportsLocalMetadata
|
||||||
_audioResolver.GetExternalFiles(video, directoryService, false)
|
&& !audio.LyricFiles.SequenceEqual(
|
||||||
.Select(info => info.Path).ToList(),
|
_lyricResolver.GetExternalFiles(audio, directoryService, false)
|
||||||
|
.Select(info => info.Path).ToList(),
|
||||||
StringComparer.Ordinal))
|
StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
.Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
|
.Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
.OrderBy(i =>
|
.OrderBy(i =>
|
||||||
{
|
{
|
||||||
var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name);
|
var index = request.SubtitleFetcherOrder.IndexOf(i.Name);
|
||||||
return index == -1 ? int.MaxValue : index;
|
return index == -1 ? int.MaxValue : index;
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
@ -61,6 +61,11 @@ namespace Jellyfin.Extensions
|
|||||||
/// <returns>The part left of the <paramref name="needle" />.</returns>
|
/// <returns>The part left of the <paramref name="needle" />.</returns>
|
||||||
public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
|
public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
|
||||||
{
|
{
|
||||||
|
if (haystack.IsEmpty)
|
||||||
|
{
|
||||||
|
return ReadOnlySpan<char>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
var pos = haystack.IndexOf(needle);
|
var pos = haystack.IndexOf(needle);
|
||||||
return pos == -1 ? haystack : haystack[..pos];
|
return pos == -1 ? haystack : haystack[..pos];
|
||||||
}
|
}
|
||||||
@ -73,6 +78,11 @@ namespace Jellyfin.Extensions
|
|||||||
/// <returns>The part right of the <paramref name="needle" />.</returns>
|
/// <returns>The part right of the <paramref name="needle" />.</returns>
|
||||||
public static ReadOnlySpan<char> RightPart(this ReadOnlySpan<char> haystack, char needle)
|
public static ReadOnlySpan<char> RightPart(this ReadOnlySpan<char> haystack, char needle)
|
||||||
{
|
{
|
||||||
|
if (haystack.IsEmpty)
|
||||||
|
{
|
||||||
|
return ReadOnlySpan<char>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
var pos = haystack.LastIndexOf(needle);
|
var pos = haystack.LastIndexOf(needle);
|
||||||
if (pos == -1)
|
if (pos == -1)
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
@ -570,7 +571,8 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
Mock.Of<IFileSystem>(),
|
Mock.Of<IFileSystem>(),
|
||||||
Mock.Of<IServerApplicationPaths>(),
|
Mock.Of<IServerApplicationPaths>(),
|
||||||
libraryManager.Object,
|
libraryManager.Object,
|
||||||
baseItemManager!);
|
baseItemManager!,
|
||||||
|
Mock.Of<ILyricManager>());
|
||||||
|
|
||||||
return providerManager;
|
return providerManager;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user