| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 | 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;/// <summary>/// Lyric Manager./// </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="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)    {        _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 event EventHandler<LyricDownloadFailureEventArgs>? LyricDownloadFailure;    /// <inheritdoc />    public Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(Audio audio, bool isAutomated, CancellationToken cancellationToken)    {        ArgumentNullException.ThrowIfNull(audio);        var request = new LyricSearchRequest        {            MediaPath = audio.Path,            SongName = audio.Name,            AlbumName = audio.Album,            AlbumArtistsNames = audio.AlbumArtists,            ArtistNames = audio.Artists,            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 =>            {                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;                }            }            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)            {                _logger.LogDebug("Unable to download lyrics for {LyricId}", lyricId);                return null;            }            var parsedLyrics = await InternalParseRemoteLyricsAsync(response.Format, response.Stream, cancellationToken).ConfigureAwait(false);            if (parsedLyrics is null)            {                return null;            }            await TrySaveLyric(audio, libraryOptions, response.Format, response.Stream).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?> SaveLyricAsync(Audio audio, string format, string lyrics)    {        ArgumentNullException.ThrowIfNull(audio);        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(format, lyrics, CancellationToken.None).ConfigureAwait(false);        if (parsed is null)        {            return null;        }        await TrySaveLyric(audio, libraryOptions, format, lyrics).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.Format, lyricResponse.Stream, 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 parsedLyrics;                }            }        }        return null;    }    private ILyricProvider? GetProvider(string providerId)    {        var provider = _lyricProviders.FirstOrDefault(p => string.Equals(providerId, GetProviderId(p.Name), StringComparison.Ordinal));        if (provider is null)        {            _logger.LogWarning("Unknown provider id: {ProviderId}", providerId.ReplaceLineEndings(string.Empty));        }        return provider;    }    private string GetProviderId(string name)        => name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);    private async Task<LyricDto?> InternalParseRemoteLyricsAsync(string format, Stream lyricStream, CancellationToken cancellationToken)    {        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.{format}", lyrics);        foreach (var parser in _lyricParsers)        {            var parsedLyrics = parser.ParseLyrics(lyricFile);            if (parsedLyrics is not null)            {                return parsedLyrics;            }        }        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.Format, result.Lyrics.Stream, 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,        string format,        Stream lyricStream)    {        var saveInMediaFolder = libraryOptions.SaveLyricsWithMedia;        var memoryStream = new MemoryStream();        await using (memoryStream.ConfigureAwait(false))        {            await using (lyricStream.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) + "." + 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);        }    }}
 |