瀏覽代碼

Save embedded lyrics when probing audio

Cody Robibero 1 年之前
父節點
當前提交
169e0dcb11

+ 5 - 7
Jellyfin.Api/Controllers/LyricsController.cs

@@ -146,13 +146,11 @@ public class LyricsController : BaseJellyfinApiController
         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);
+            var uploadedLyric = await _lyricManager.SaveLyricAsync(
+                    audio,
+                    format,
+                    stream)
+                .ConfigureAwait(false);
 
             if (uploadedLyric is null)
             {

+ 14 - 3
MediaBrowser.Controller/Lyrics/ILyricManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
@@ -69,12 +70,22 @@ public interface ILyricManager
         CancellationToken cancellationToken);
 
     /// <summary>
-    /// Upload new lyrics.
+    /// Saves new lyrics.
     /// </summary>
     /// <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>
-    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>
     /// Get the remote lyrics.

+ 32 - 19
MediaBrowser.Providers/Lyric/LyricManager.cs

@@ -155,13 +155,13 @@ public class LyricManager : ILyricManager
                 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)
             {
                 return null;
             }
 
-            await TrySaveLyric(audio, libraryOptions, response).ConfigureAwait(false);
+            await TrySaveLyric(audio, libraryOptions, response.Format, response.Stream).ConfigureAwait(false);
             return parsedLyrics;
         }
         catch (RateLimitExceededException)
@@ -182,19 +182,33 @@ public class LyricManager : ILyricManager
     }
 
     /// <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(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 parsed = await InternalParseRemoteLyricsAsync(lyricResponse, CancellationToken.None).ConfigureAwait(false);
+        var parsed = await InternalParseRemoteLyricsAsync(format, lyrics, CancellationToken.None).ConfigureAwait(false);
         if (parsed is null)
         {
             return null;
         }
 
-        await TrySaveLyric(audio, libraryOptions, lyricResponse).ConfigureAwait(false);
+        await TrySaveLyric(audio, libraryOptions, format, lyrics).ConfigureAwait(false);
         return parsed;
     }
 
@@ -209,7 +223,7 @@ public class LyricManager : ILyricManager
             return null;
         }
 
-        return await InternalParseRemoteLyricsAsync(lyricResponse, cancellationToken).ConfigureAwait(false);
+        return await InternalParseRemoteLyricsAsync(lyricResponse.Format, lyricResponse.Stream, cancellationToken).ConfigureAwait(false);
     }
 
     /// <inheritdoc />
@@ -289,12 +303,12 @@ public class LyricManager : ILyricManager
     private string GetProviderId(string name)
         => 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);
-        using var streamReader = new StreamReader(lyricResponse.Stream, leaveOpen: true);
+        lyricStream.Seek(0, SeekOrigin.Begin);
+        using var streamReader = new StreamReader(lyricStream, leaveOpen: true);
         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)
         {
             var parsedLyrics = parser.ParseLyrics(lyricFile);
@@ -334,7 +348,7 @@ public class LyricManager : ILyricManager
             var parsedResults = new List<RemoteLyricInfoDto>();
             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)
                 {
                     continue;
@@ -361,24 +375,23 @@ public class LyricManager : ILyricManager
     private async Task TrySaveLyric(
         Audio audio,
         LibraryOptions libraryOptions,
-        LyricResponse lyricResponse)
+        string format,
+        Stream lyricStream)
     {
         var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia;
 
         var memoryStream = new MemoryStream();
         await using (memoryStream.ConfigureAwait(false))
         {
-            var stream = lyricResponse.Stream;
-
-            await using (stream.ConfigureAwait(false))
+            await using (lyricStream.ConfigureAwait(false))
             {
-                stream.Seek(0, SeekOrigin.Begin);
-                await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+                lyricStream.Seek(0, SeekOrigin.Begin);
+                await lyricStream.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();
+            var saveFileName = Path.GetFileNameWithoutExtension(audio.Path) + "." + format.ReplaceLineEndings(string.Empty).ToLowerInvariant();
 
             if (saveInMediaFolder)
             {

+ 20 - 6
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
@@ -36,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILibraryManager _libraryManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly LyricResolver _lyricResolver;
+        private readonly ILyricManager _lyricManager;
 
         /// <summary>
         /// 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="libraryManager">Instance of the <see cref="ILibraryManager"/> 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(
             ILogger<AudioFileProber> logger,
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
             ILibraryManager libraryManager,
-            LyricResolver lyricResolver)
+            LyricResolver lyricResolver,
+            ILyricManager lyricManager)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
@@ -60,6 +64,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _libraryManager = libraryManager;
             _mediaSourceManager = mediaSourceManager;
             _lyricResolver = lyricResolver;
+            _lyricManager = lyricManager;
         }
 
         [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
@@ -107,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                Fetch(item, result, options, cancellationToken);
+                await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
             }
 
             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="options">The <see cref="MetadataRefreshOptions"/>.</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,
             Model.MediaInfo.MediaInfo mediaInfo,
             MetadataRefreshOptions options,
@@ -225,7 +231,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
             if (!audio.IsLocked)
             {
-                FetchDataFromTags(audio, options);
+                await FetchDataFromTags(audio, options).ConfigureAwait(false);
             }
 
             var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
@@ -241,9 +247,9 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// <param name="audio">The <see cref="Audio"/>.</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;
             Tag? tags = null;
 
@@ -398,6 +404,14 @@ namespace MediaBrowser.Providers.MediaInfo
                 {
                     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);
+                }
             }
         }
 

+ 6 - 2
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

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