mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-19 20:54:20 +01:00
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
This commit is contained in:
@@ -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 MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
|
||||
namespace MediaBrowser.Providers.Lyric;
|
||||
|
||||
@@ -18,8 +19,8 @@ public class LrcLyricParser : ILyricParser
|
||||
{
|
||||
private readonly LyricParser _lrcLyricParser;
|
||||
|
||||
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[] _supportedMediaTypes = [".lrc", ".elrc"];
|
||||
private static readonly string[] _acceptedTimeFormats = ["HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss"];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
|
||||
@@ -39,7 +40,7 @@ public class LrcLyricParser : ILyricParser
|
||||
public ResolverPriority Priority => ResolverPriority.Fourth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LyricResponse? ParseLyrics(LyricFile lyrics)
|
||||
public LyricDto? ParseLyrics(LyricFile lyrics)
|
||||
{
|
||||
if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -95,7 +96,7 @@ public class LrcLyricParser : ILyricParser
|
||||
return null;
|
||||
}
|
||||
|
||||
List<LyricLine> lyricList = new();
|
||||
List<LyricLine> lyricList = [];
|
||||
|
||||
for (int i = 0; i < sortedLyricData.Count; i++)
|
||||
{
|
||||
@@ -106,7 +107,7 @@ public class LrcLyricParser : ILyricParser
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -114,10 +115,10 @@ public class LrcLyricParser : ILyricParser
|
||||
// Map metaData values from LRC file to LyricMetadata properties
|
||||
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>
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
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;
|
||||
|
||||
@@ -11,37 +28,246 @@ namespace MediaBrowser.Providers.Lyric;
|
||||
/// </summary>
|
||||
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 ILyricParser[] _lyricParsers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LyricManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lyricProviders">All found lyricProviders.</param>
|
||||
/// <param name="lyricParsers">All found lyricParsers.</param>
|
||||
public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers)
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{LyricManager}"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <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();
|
||||
_lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray();
|
||||
_logger = logger;
|
||||
_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 />
|
||||
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);
|
||||
if (lyrics is null)
|
||||
MediaPath = audio.Path,
|
||||
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);
|
||||
if (result is not null)
|
||||
_logger.LogDebug("Unable to download lyrics for {LyricId}", lyricId);
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasLyricFile(BaseItem item)
|
||||
private ILyricProvider? GetProvider(string providerId)
|
||||
{
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_logger.LogWarning("Unknown provider id: {ProviderId}", providerId.ReplaceLineEndings(string.Empty));
|
||||
}
|
||||
|
||||
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 MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
|
||||
namespace MediaBrowser.Providers.Lyric;
|
||||
|
||||
@@ -11,8 +12,8 @@ namespace MediaBrowser.Providers.Lyric;
|
||||
/// </summary>
|
||||
public class TxtLyricParser : ILyricParser
|
||||
{
|
||||
private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc", ".txt" };
|
||||
private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" };
|
||||
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc", ".txt"];
|
||||
private static readonly string[] _lineBreakCharacters = ["\r\n", "\r", "\n"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "TxtLyricProvider";
|
||||
@@ -24,7 +25,7 @@ public class TxtLyricParser : ILyricParser
|
||||
public ResolverPriority Priority => ResolverPriority.Fifth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LyricResponse? ParseLyrics(LyricFile lyrics)
|
||||
public LyricDto? ParseLyrics(LyricFile lyrics)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]);
|
||||
lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex].Trim());
|
||||
}
|
||||
|
||||
return new LyricResponse { Lyrics = lyricList };
|
||||
return new LyricDto { Lyrics = lyricList };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.BaseItemManager;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -22,6 +21,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
@@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly ILyricManager _lyricManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IBaseItemManager _baseItemManager;
|
||||
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
|
||||
@@ -79,6 +80,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
/// <param name="appPaths">The server application paths.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="baseItemManager">The BaseItem manager.</param>
|
||||
/// <param name="lyricManager">The lyric manager.</param>
|
||||
public ProviderManager(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ISubtitleManager subtitleManager,
|
||||
@@ -88,7 +90,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
IFileSystem fileSystem,
|
||||
IServerApplicationPaths appPaths,
|
||||
ILibraryManager libraryManager,
|
||||
IBaseItemManager baseItemManager)
|
||||
IBaseItemManager baseItemManager,
|
||||
ILyricManager lyricManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
@@ -99,6 +102,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
_libraryManager = libraryManager;
|
||||
_subtitleManager = subtitleManager;
|
||||
_baseItemManager = baseItemManager;
|
||||
_lyricManager = lyricManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -504,15 +508,22 @@ namespace MediaBrowser.Providers.Manager
|
||||
AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
|
||||
AddImagePlugins(pluginList, imageProviders);
|
||||
|
||||
var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
|
||||
|
||||
// Subtitle fetchers
|
||||
var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
|
||||
pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
|
||||
{
|
||||
Name = i.Name,
|
||||
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();
|
||||
|
||||
var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
|
||||
@@ -1025,7 +1036,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case Folder folder:
|
||||
await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await folder.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1047,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await child.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,7 +1069,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
.Select(i => i.MusicArtist)
|
||||
.Where(i => i is not null);
|
||||
|
||||
var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken));
|
||||
var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, cancellationToken));
|
||||
|
||||
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
private readonly IItemRepository _itemRepo;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly LyricResolver _lyricResolver;
|
||||
|
||||
/// <summary>
|
||||
/// 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="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>
|
||||
public AudioFileProber(
|
||||
ILogger<AudioFileProber> logger,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepo,
|
||||
ILibraryManager libraryManager)
|
||||
ILibraryManager libraryManager,
|
||||
LyricResolver lyricResolver)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepo = itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_lyricResolver = lyricResolver;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
||||
@@ -207,7 +211,11 @@ 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(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
protected void Fetch(
|
||||
Audio audio,
|
||||
Model.MediaInfo.MediaInfo mediaInfo,
|
||||
MetadataRefreshOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
audio.Container = mediaInfo.Container;
|
||||
audio.TotalBitrate = mediaInfo.Bitrate;
|
||||
@@ -220,7 +228,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
FetchDataFromTags(audio, options);
|
||||
}
|
||||
|
||||
_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>
|
||||
@@ -369,5 +382,17 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.ExternalFiles;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
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>
|
||||
@@ -209,6 +252,58 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
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>
|
||||
/// Returns the media info of the given file.
|
||||
/// </summary>
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
private readonly ILogger<ProbeProvider> _logger;
|
||||
private readonly AudioResolver _audioResolver;
|
||||
private readonly SubtitleResolver _subtitleResolver;
|
||||
private readonly LyricResolver _lyricResolver;
|
||||
private readonly FFProbeVideoInfo _videoProber;
|
||||
private readonly AudioFileProber _audioProber;
|
||||
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
|
||||
@@ -79,9 +80,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ProbeProvider>();
|
||||
_audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
|
||||
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), 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(
|
||||
loggerFactory.CreateLogger<FFProbeVideoInfo>(),
|
||||
mediaSourceManager,
|
||||
@@ -96,6 +98,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
libraryManager,
|
||||
_audioResolver,
|
||||
_subtitleResolver);
|
||||
|
||||
_audioProber = new AudioFileProber(
|
||||
loggerFactory.CreateLogger<AudioFileProber>(),
|
||||
mediaSourceManager,
|
||||
mediaEncoder,
|
||||
itemRepo,
|
||||
libraryManager,
|
||||
_lyricResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -123,23 +133,37 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
if (item.SupportsLocalMetadata && video is not null && !video.IsPlaceHolder
|
||||
&& !video.SubtitleFiles.SequenceEqual(
|
||||
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
StringComparer.Ordinal))
|
||||
if (video is not null
|
||||
&& item.SupportsLocalMetadata
|
||||
&& !video.IsPlaceHolder)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
||||
return true;
|
||||
if (!video.SubtitleFiles.SequenceEqual(
|
||||
_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
|
||||
&& !video.AudioFiles.SequenceEqual(
|
||||
_audioResolver.GetExternalFiles(video, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
if (item is Audio audio
|
||||
&& item.SupportsLocalMetadata
|
||||
&& !audio.LyricFiles.SequenceEqual(
|
||||
_lyricResolver.GetExternalFiles(audio, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
.Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(i =>
|
||||
{
|
||||
var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name);
|
||||
var index = request.SubtitleFetcherOrder.IndexOf(i.Name);
|
||||
return index == -1 ? int.MaxValue : index;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
Reference in New Issue
Block a user