using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Jellyfin.Plugin.Lyrics.Models;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Model.Lyrics;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Lyrics;
///
/// Lyric provider for Lyrics.
///
public class LyricsProvider : ILyricProvider
{
private const string BaseUrl = "https://lrclib.net";
private const string SyncedSuffix = "synced";
private const string PlainSuffix = "plain";
private const string SyncedFormat = "lrc";
private const string PlainFormat = "txt";
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
/// Instance of the .
public LyricsProvider(IHttpClientFactory httpClientFactory, ILogger logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
private static bool UseStrictSearch => LyricsPlugin.Instance?.Configuration.UseStrictSearch ?? true;
private static bool ExcludeArtistName => LyricsPlugin.Instance?.Configuration.ExcludeArtistName ?? false;
private static bool ExcludeAlbumName => LyricsPlugin.Instance?.Configuration.ExcludeAlbumName ?? false;
///
public string Name => LyricsPlugin.Instance!.Name;
///
public async Task> SearchAsync(
LyricSearchRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
return UseStrictSearch
? await GetExactMatch(request, cancellationToken).ConfigureAwait(false)
: await GetFuzzyMatch(request, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_logger.LogDebug(
ex,
"Unable to get results for {Artist} - {Album} - {Song}",
request.ArtistNames?[0],
request.AlbumName,
request.SongName);
return Enumerable.Empty();
}
}
///
public async Task GetLyricsAsync(string id, CancellationToken cancellationToken)
{
var splitId = id.Split('_', 2);
try
{
var requestUri = new UriBuilder(BaseUrl)
{
Path = $"/api/get/{splitId[0]}"
};
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetFromJsonAsync(requestUri.Uri, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (response is null)
{
throw new ResourceNotFoundException("Unable to get results for id {Id}");
}
if (string.Equals(splitId[1], SyncedSuffix, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(response.SyncedLyrics))
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.SyncedLyrics));
return new LyricResponse
{
Format = SyncedFormat,
Stream = stream
};
}
if (string.Equals(splitId[1], PlainSuffix, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(response.PlainLyrics))
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.PlainLyrics));
return new LyricResponse
{
Format = PlainFormat,
Stream = stream
};
}
throw new ResourceNotFoundException("Unable to get results for id {Id}");
}
catch (HttpRequestException ex)
{
_logger.LogDebug(
ex,
"Unable to get results for id {Id}",
id);
throw new ResourceNotFoundException("Unable to get results for id {Id}");
}
}
private async Task> GetExactMatch(
LyricSearchRequest request,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.SongName))
{
_logger.LogInformation("Song name is required");
return Enumerable.Empty();
}
string artist;
if (request.ArtistNames is not null
&& request.ArtistNames.Count > 0)
{
artist = request.ArtistNames[0];
}
else
{
_logger.LogInformation("Artist name is required");
return Enumerable.Empty();
}
if (string.IsNullOrEmpty(request.AlbumName))
{
_logger.LogInformation("Album name is required");
return Enumerable.Empty();
}
if (request.Duration is null)
{
_logger.LogInformation("Duration is required");
return Enumerable.Empty();
}
var queryStringBuilder = new StringBuilder()
.Append("track_name=")
.Append(HttpUtility.UrlEncode(request.SongName))
.Append("&artist_name=")
.Append(HttpUtility.UrlEncode(artist))
.Append("&album_name=")
.Append(HttpUtility.UrlEncode(request.AlbumName))
.Append("&duration=")
.Append(TimeSpan.FromTicks(request.Duration.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture));
var requestUri = new UriBuilder(BaseUrl)
{
Path = "/api/get",
Query = queryStringBuilder.ToString()
};
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
var response = await httpClient
.GetFromJsonAsync(requestUri.Uri, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (response is null)
{
return Enumerable.Empty();
}
return GetRemoteLyrics(response);
}
private async Task> GetFuzzyMatch(
LyricSearchRequest request,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.SongName))
{
_logger.LogInformation("Song name is required");
return Enumerable.Empty();
}
var queryStringBuilder = new StringBuilder()
.Append("track_name=")
.Append(HttpUtility.UrlEncode(request.SongName));
if (!ExcludeArtistName)
{
string artist;
if (request.ArtistNames is not null
&& request.ArtistNames.Count > 0)
{
artist = request.ArtistNames[0];
}
else
{
_logger.LogInformation("Artist name is required");
return Enumerable.Empty();
}
queryStringBuilder
.Append("&artist_name=")
.Append(HttpUtility.UrlEncode(artist));
}
if (!ExcludeAlbumName)
{
if (string.IsNullOrEmpty(request.AlbumName))
{
_logger.LogInformation("Album name is required");
return Enumerable.Empty();
}
queryStringBuilder
.Append("&album_name=")
.Append(HttpUtility.UrlEncode(request.AlbumName));
}
var requestUri = new UriBuilder(BaseUrl)
{
Path = "/api/search",
Query = queryStringBuilder.ToString()
};
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
var response = await httpClient
.GetFromJsonAsync>(requestUri.Uri, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (response is null)
{
return Enumerable.Empty();
}
var results = new List();
foreach (var item in response)
{
results.AddRange(GetRemoteLyrics(item));
}
var sortedResults = results.OrderByDescending(x => x.Metadata.IsSynced);
return sortedResults;
}
private List GetRemoteLyrics(LyricsSearchResponse response)
{
var results = new List();
if (!string.IsNullOrEmpty(response.SyncedLyrics))
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.SyncedLyrics));
results.Add(new RemoteLyricInfo
{
Id = $"{response.Id}_{SyncedSuffix}",
ProviderName = Name,
Metadata = new LyricMetadata
{
Album = response.AlbumName,
Artist = response.ArtistName,
Title = response.TrackName,
Length = TimeSpan.FromSeconds(response.Duration ?? 0).Ticks,
IsSynced = true
},
Lyrics = new LyricResponse
{
Format = SyncedFormat,
Stream = stream
}
});
}
if (!string.IsNullOrEmpty(response.PlainLyrics))
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.PlainLyrics));
results.Add(new RemoteLyricInfo
{
Id = $"{response.Id}_{PlainSuffix}",
ProviderName = Name,
Metadata = new LyricMetadata
{
Album = response.AlbumName,
Artist = response.ArtistName,
Title = response.TrackName,
Length = TimeSpan.FromSeconds(response.Duration ?? 0).Ticks,
IsSynced = false
},
Lyrics = new LyricResponse
{
Format = PlainFormat,
Stream = stream
}
});
}
return results;
}
}