using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
using API.Helpers;
using API.Helpers.Builders;
using API.Services.Plus;
using Flurl.Http;
using HtmlAgilityPack;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services;
internal class MediaReviewDto
{
public string Body { get; set; }
public string Tagline { get; set; }
public int Rating { get; set; }
public int TotalVotes { get; set; }
///
/// The media's overall Score
///
public int Score { get; set; }
public string SiteUrl { get; set; }
///
/// In Markdown
///
public string RawBody { get; set; }
public string Username { get; set; }
public ScrobbleProvider Provider { get; set; }
}
public interface IReviewService
{
Task> GetReviewsForSeries(int userId, int seriesId);
}
public class ReviewService : IReviewService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger _logger;
public ReviewService(IUnitOfWork unitOfWork, ILogger logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
public async Task> GetReviewsForSeries(int userId, int seriesId)
{
var series =
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null || series == null) return new List();
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
var ret = (await GetReviews(license.Value, series)).Select(r => new UserReviewDto()
{
Body = r.Body,
Tagline = r.Tagline,
Score = r.Score,
Username = r.Username,
LibraryId = series.LibraryId,
SeriesId = series.Id,
IsExternal = true,
Provider = r.Provider,
BodyJustText = GetCharacters(r.Body),
ExternalUrl = r.SiteUrl
});
return ret.OrderByDescending(r => r.Score);
}
private static string GetCharacters(string body)
{
if (string.IsNullOrEmpty(body)) return body;
var doc = new HtmlDocument();
doc.LoadHtml(body);
var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]");
if (textNodes == null) return string.Empty;
var plainText = string.Join(" ", textNodes
.Select(node => node.InnerText)
.Where(s => !s.Equals("\n")));
// Clean any leftover markdown out
plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty);
plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty);
plainText = Regex.Replace(plainText, @"~~~(.*?)~~~", "$1");
plainText = Regex.Replace(plainText, @"\+{3}(.*?)\+{3}", "$1");
plainText = Regex.Replace(plainText, @"~~(.*?)~~", "$1");
plainText = Regex.Replace(plainText, @"__(.*?)__", "$1");
plainText = Regex.Replace(plainText, @"#\s(.*?)", "$1");
// Just strip symbols
plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty);
plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty);
plainText = Regex.Replace(plainText, @"~~~", string.Empty);
plainText = Regex.Replace(plainText, @"\+", string.Empty);
plainText = Regex.Replace(plainText, @"~~", string.Empty);
plainText = Regex.Replace(plainText, @"__", string.Empty);
// Take the first 100 characters
plainText = plainText.Length > 100 ? plainText.Substring(0, 100) : plainText;
return plainText + "…";
}
private async Task> GetReviews(string license, Series series)
{
_logger.LogDebug("Fetching external reviews for Series: {SeriesName}", series.Name);
try
{
return await (Configuration.KavitaPlusApiUrl + "/api/review")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)
.WithHeader("x-installId", HashUtil.ServerToken())
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.ReceiveJson>();
}
catch (Exception e)
{
_logger.LogError(e, "An error happened during the request to Kavita+ API");
}
return new List();
}
}