Save embedded lyrics when probing audio

This commit is contained in:
Cody Robibero 2024-02-28 17:29:44 -07:00
parent ac33d1593a
commit 169e0dcb11
5 changed files with 77 additions and 37 deletions

View File

@ -146,13 +146,11 @@ public class LyricsController : BaseJellyfinApiController
await using (stream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
await Request.Body.CopyToAsync(stream).ConfigureAwait(false); await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
var uploadedLyric = await _lyricManager.UploadLyricAsync( var uploadedLyric = await _lyricManager.SaveLyricAsync(
audio, audio,
new LyricResponse format,
{ stream)
Format = format, .ConfigureAwait(false);
Stream = stream
}).ConfigureAwait(false);
if (uploadedLyric is null) if (uploadedLyric is null)
{ {

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -69,12 +70,22 @@ public interface ILyricManager
CancellationToken cancellationToken); CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Upload new lyrics. /// Saves new lyrics.
/// </summary> /// </summary>
/// <param name="audio">The audio file the lyrics belong to.</param> /// <param name="audio">The audio file the lyrics belong to.</param>
/// <param name="lyricResponse">The lyric response.</param> /// <param name="format">The lyrics format.</param>
/// <param name="lyrics">The lyrics.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse); Task<LyricDto?> SaveLyricAsync(Audio audio, string format, string lyrics);
/// <summary>
/// Saves new lyrics.
/// </summary>
/// <param name="audio">The audio file the lyrics belong to.</param>
/// <param name="format">The lyrics format.</param>
/// <param name="lyrics">The lyrics.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<LyricDto?> SaveLyricAsync(Audio audio, string format, Stream lyrics);
/// <summary> /// <summary>
/// Get the remote lyrics. /// Get the remote lyrics.

View File

@ -155,13 +155,13 @@ public class LyricManager : ILyricManager
return null; return null;
} }
var parsedLyrics = await InternalParseRemoteLyricsAsync(response, cancellationToken).ConfigureAwait(false); var parsedLyrics = await InternalParseRemoteLyricsAsync(response.Format, response.Stream, cancellationToken).ConfigureAwait(false);
if (parsedLyrics is null) if (parsedLyrics is null)
{ {
return null; return null;
} }
await TrySaveLyric(audio, libraryOptions, response).ConfigureAwait(false); await TrySaveLyric(audio, libraryOptions, response.Format, response.Stream).ConfigureAwait(false);
return parsedLyrics; return parsedLyrics;
} }
catch (RateLimitExceededException) catch (RateLimitExceededException)
@ -182,19 +182,33 @@ public class LyricManager : ILyricManager
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse) public async Task<LyricDto?> SaveLyricAsync(Audio audio, string format, string lyrics)
{ {
ArgumentNullException.ThrowIfNull(audio); ArgumentNullException.ThrowIfNull(audio);
ArgumentNullException.ThrowIfNull(lyricResponse); ArgumentException.ThrowIfNullOrEmpty(format);
ArgumentException.ThrowIfNullOrEmpty(lyrics);
var bytes = Encoding.UTF8.GetBytes(lyrics);
using var lyricStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
return await SaveLyricAsync(audio, format, lyricStream).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<LyricDto?> SaveLyricAsync(Audio audio, string format, Stream lyrics)
{
ArgumentNullException.ThrowIfNull(audio);
ArgumentException.ThrowIfNullOrEmpty(format);
ArgumentNullException.ThrowIfNull(lyrics);
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(audio); var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(audio);
var parsed = await InternalParseRemoteLyricsAsync(lyricResponse, CancellationToken.None).ConfigureAwait(false); var parsed = await InternalParseRemoteLyricsAsync(format, lyrics, CancellationToken.None).ConfigureAwait(false);
if (parsed is null) if (parsed is null)
{ {
return null; return null;
} }
await TrySaveLyric(audio, libraryOptions, lyricResponse).ConfigureAwait(false); await TrySaveLyric(audio, libraryOptions, format, lyrics).ConfigureAwait(false);
return parsed; return parsed;
} }
@ -209,7 +223,7 @@ public class LyricManager : ILyricManager
return null; return null;
} }
return await InternalParseRemoteLyricsAsync(lyricResponse, cancellationToken).ConfigureAwait(false); return await InternalParseRemoteLyricsAsync(lyricResponse.Format, lyricResponse.Stream, cancellationToken).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -289,12 +303,12 @@ public class LyricManager : ILyricManager
private string GetProviderId(string name) private string GetProviderId(string name)
=> name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); => name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
private async Task<LyricDto?> InternalParseRemoteLyricsAsync(LyricResponse lyricResponse, CancellationToken cancellationToken) private async Task<LyricDto?> InternalParseRemoteLyricsAsync(string format, Stream lyricStream, CancellationToken cancellationToken)
{ {
lyricResponse.Stream.Seek(0, SeekOrigin.Begin); lyricStream.Seek(0, SeekOrigin.Begin);
using var streamReader = new StreamReader(lyricResponse.Stream, leaveOpen: true); using var streamReader = new StreamReader(lyricStream, leaveOpen: true);
var lyrics = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var lyrics = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
var lyricFile = new LyricFile($"lyric.{lyricResponse.Format}", lyrics); var lyricFile = new LyricFile($"lyric.{format}", lyrics);
foreach (var parser in _lyricParsers) foreach (var parser in _lyricParsers)
{ {
var parsedLyrics = parser.ParseLyrics(lyricFile); var parsedLyrics = parser.ParseLyrics(lyricFile);
@ -334,7 +348,7 @@ public class LyricManager : ILyricManager
var parsedResults = new List<RemoteLyricInfoDto>(); var parsedResults = new List<RemoteLyricInfoDto>();
foreach (var result in searchResults) foreach (var result in searchResults)
{ {
var parsedLyrics = await InternalParseRemoteLyricsAsync(result.Lyrics, cancellationToken).ConfigureAwait(false); var parsedLyrics = await InternalParseRemoteLyricsAsync(result.Lyrics.Format, result.Lyrics.Stream, cancellationToken).ConfigureAwait(false);
if (parsedLyrics is null) if (parsedLyrics is null)
{ {
continue; continue;
@ -361,24 +375,23 @@ public class LyricManager : ILyricManager
private async Task TrySaveLyric( private async Task TrySaveLyric(
Audio audio, Audio audio,
LibraryOptions libraryOptions, LibraryOptions libraryOptions,
LyricResponse lyricResponse) string format,
Stream lyricStream)
{ {
var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia; var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia;
var memoryStream = new MemoryStream(); var memoryStream = new MemoryStream();
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
var stream = lyricResponse.Stream; await using (lyricStream.ConfigureAwait(false))
await using (stream.ConfigureAwait(false))
{ {
stream.Seek(0, SeekOrigin.Begin); lyricStream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(memoryStream).ConfigureAwait(false); await lyricStream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin); memoryStream.Seek(0, SeekOrigin.Begin);
} }
var savePaths = new List<string>(); var savePaths = new List<string>();
var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + lyricResponse.Format.ReplaceLineEndings(string.Empty).ToLowerInvariant(); var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + format.ReplaceLineEndings(string.Empty).ToLowerInvariant();
if (saveInMediaFolder) if (saveInMediaFolder)
{ {

View File

@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities; 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.Lyrics;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -36,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly LyricResolver _lyricResolver; private readonly LyricResolver _lyricResolver;
private readonly ILyricManager _lyricManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AudioFileProber"/> class. /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
@ -46,13 +48,15 @@ namespace MediaBrowser.Providers.MediaInfo
/// <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> /// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> 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) LyricResolver lyricResolver,
ILyricManager lyricManager)
{ {
_logger = logger; _logger = logger;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
@ -60,6 +64,7 @@ namespace MediaBrowser.Providers.MediaInfo
_libraryManager = libraryManager; _libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_lyricResolver = lyricResolver; _lyricResolver = lyricResolver;
_lyricManager = lyricManager;
} }
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
@ -107,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Fetch(item, result, options, cancellationToken); await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
} }
var libraryOptions = _libraryManager.GetLibraryOptions(item); var libraryOptions = _libraryManager.GetLibraryOptions(item);
@ -211,7 +216,8 @@ namespace MediaBrowser.Providers.MediaInfo
/// <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="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( /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task FetchAsync(
Audio audio, Audio audio,
Model.MediaInfo.MediaInfo mediaInfo, Model.MediaInfo.MediaInfo mediaInfo,
MetadataRefreshOptions options, MetadataRefreshOptions options,
@ -225,7 +231,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.IsLocked) if (!audio.IsLocked)
{ {
FetchDataFromTags(audio, options); await FetchDataFromTags(audio, options).ConfigureAwait(false);
} }
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams); var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
@ -241,9 +247,9 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary> /// </summary>
/// <param name="audio">The <see cref="Audio"/>.</param> /// <param name="audio">The <see cref="Audio"/>.</param>
/// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param> /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
private void FetchDataFromTags(Audio audio, MetadataRefreshOptions options) private async Task FetchDataFromTags(Audio audio, MetadataRefreshOptions options)
{ {
var file = TagLib.File.Create(audio.Path); using var file = TagLib.File.Create(audio.Path);
var tagTypes = file.TagTypesOnDisk; var tagTypes = file.TagTypesOnDisk;
Tag? tags = null; Tag? tags = null;
@ -398,6 +404,14 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId); audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId);
} }
// Save extracted lyrics if they exist,
// and if we are replacing all metadata or the audio doesn't yet have lyrics.
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
&& (options.ReplaceAllMetadata || audio.GetMediaStreams().All(s => s.Type != MediaStreamType.Lyric)))
{
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
}
} }
} }

View File

@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -64,6 +65,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
public ProbeProvider( public ProbeProvider(
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
@ -77,7 +79,8 @@ namespace MediaBrowser.Providers.MediaInfo
ILibraryManager libraryManager, ILibraryManager libraryManager,
IFileSystem fileSystem, IFileSystem fileSystem,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
NamingOptions namingOptions) NamingOptions namingOptions,
ILyricManager lyricManager)
{ {
_logger = loggerFactory.CreateLogger<ProbeProvider>(); _logger = loggerFactory.CreateLogger<ProbeProvider>();
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
@ -105,7 +108,8 @@ namespace MediaBrowser.Providers.MediaInfo
mediaEncoder, mediaEncoder,
itemRepo, itemRepo,
libraryManager, libraryManager,
_lyricResolver); _lyricResolver,
lyricManager);
} }
/// <inheritdoc /> /// <inheritdoc />