Save embedded lyrics when probing audio
This commit is contained in:
parent
ac33d1593a
commit
169e0dcb11
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
Loading…
Reference in New Issue
Block a user