Customized Scheduler + Saved Kavita+ Details (#2644)

This commit is contained in:
Joe Milazzo 2024-01-22 12:10:57 -06:00 committed by GitHub
parent 2092e120c3
commit ad74871623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 6076 additions and 3370 deletions

View file

@ -10,8 +10,10 @@ using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Extensions;
using API.Helpers.Builders;
using AutoMapper;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
@ -39,6 +41,8 @@ internal class SeriesDetailPlusAPIDto
public IEnumerable<MediaRecommendationDto> Recommendations { get; set; }
public IEnumerable<UserReviewDto> Reviews { get; set; }
public IEnumerable<RatingDto> Ratings { get; set; }
public int? AniListId { get; set; }
public long? MalId { get; set; }
}
public interface IExternalMetadataService
@ -51,11 +55,14 @@ public class ExternalMetadataService : IExternalMetadataService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ExternalMetadataService> _logger;
private readonly IMapper _mapper;
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(14);
public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger<ExternalMetadataService> logger)
public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger<ExternalMetadataService> logger, IMapper mapper)
{
_unitOfWork = unitOfWork;
_logger = logger;
_mapper = mapper;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
@ -76,8 +83,12 @@ public class ExternalMetadataService : IExternalMetadataService
throw new KavitaException("Unable to find valid information from url for External Load");
}
// This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
return await GetSeriesDetail(license, aniListId, malId, seriesId);
var details = await GetSeriesDetail(license, aniListId, malId, seriesId);
return details;
}
@ -87,13 +98,22 @@ public class ExternalMetadataService : IExternalMetadataService
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Volumes | SeriesIncludes.Chapters);
if (series == null || series.Library.Type == LibraryType.Comic) return new SeriesDetailPlusDto();
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return new SeriesDetailPlusDto();
// Let's try to get SeriesDetailPlusDto from the local DB.
var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series);
var needsRefresh = externalSeriesMetadata.LastUpdatedUtc <= DateTime.UtcNow.Subtract(_externalSeriesMetadataCache);
if (!needsRefresh)
{
// Convert into DTOs and return
return await SerializeExternalSeriesDetail(seriesId, externalSeriesMetadata, user, series);
}
try
{
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/series-detail")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
@ -106,13 +126,49 @@ public class ExternalMetadataService : IExternalMetadataService
.ReceiveJson<SeriesDetailPlusAPIDto>();
var recs = await ProcessRecommendations(series, user!, result.Recommendations);
return new SeriesDetailPlusDto()
// Clear out existing results
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings);
_unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations);
externalSeriesMetadata.ExternalReviews = result.Reviews.Select(r =>
{
var review = _mapper.Map<ExternalReview>(r);
review.SeriesId = externalSeriesMetadata.SeriesId;
return review;
}).ToList();
externalSeriesMetadata.ExternalRatings = result.Ratings.Select(r =>
{
var rating = _mapper.Map<ExternalRating>(r);
rating.SeriesId = externalSeriesMetadata.SeriesId;
return rating;
}).ToList();
// Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
var recs = await ProcessRecommendations(series, user!, result.Recommendations, externalSeriesMetadata);
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
externalSeriesMetadata.AverageExternalRating = (int) externalSeriesMetadata.ExternalRatings
.Where(r => r.AverageScore > 0)
.Average(r => r.AverageScore);
if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value;
if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value;
await _unitOfWork.CommitAsync();
var ret = new SeriesDetailPlusDto()
{
Recommendations = recs,
Ratings = result.Ratings,
Reviews = result.Reviews
};
return ret;
}
catch (FlurlHttpException ex)
{
@ -129,7 +185,63 @@ public class ExternalMetadataService : IExternalMetadataService
return null;
}
private async Task<RecommendationDto> ProcessRecommendations(Series series, AppUser user, IEnumerable<MediaRecommendationDto> recs)
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, ExternalSeriesMetadata externalSeriesMetadata,
AppUser user, Series series)
{
var seriesIdsOnServer = externalSeriesMetadata.ExternalRecommendations
.Where(r => r.SeriesId is > 0)
.Select(s => (int) s.SeriesId!)
.ToList();
var ownedSeries = (await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(seriesIdsOnServer, user.Id))
.ToList();
var canSeeExternalSeries = user is {AgeRestriction: AgeRating.NotApplicable} &&
await _unitOfWork.UserRepository.IsUserAdminAsync(user);
var externalSeries = new List<ExternalSeriesDto>();
if (canSeeExternalSeries)
{
externalSeries = externalSeriesMetadata.ExternalRecommendations
.Where(r => r.SeriesId is null or 0)
.Select(r => _mapper.Map<ExternalSeriesDto>(r))
.ToList();
}
var ret = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, series.LibraryId, user);
return new SeriesDetailPlusDto()
{
Ratings = externalSeriesMetadata.ExternalRatings.Select(r => _mapper.Map<RatingDto>(r)),
Reviews = externalSeriesMetadata.ExternalReviews.OrderByDescending(r => r.Score).Select(r =>
{
var review = _mapper.Map<UserReviewDto>(r);
review.SeriesId = seriesId;
review.LibraryId = series.LibraryId;
review.IsExternal = true;
return review;
}),
Recommendations = new RecommendationDto()
{
ExternalSeries = externalSeries,
OwnedSeries = ownedSeries
}
};
}
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
{
var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
if (externalSeriesMetadata == null)
{
externalSeriesMetadata = new ExternalSeriesMetadata();
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
}
return externalSeriesMetadata;
}
private async Task<RecommendationDto> ProcessRecommendations(Series series, AppUser user, IEnumerable<MediaRecommendationDto> recs, ExternalSeriesMetadata externalSeriesMetadata)
{
var recDto = new RecommendationDto()
{
@ -139,6 +251,7 @@ public class ExternalMetadataService : IExternalMetadataService
var canSeeExternalSeries = user is {AgeRestriction: AgeRating.NotApplicable} &&
await _unitOfWork.UserRepository.IsUserAdminAsync(user);
// NOTE: This can result in a series being recommended that shares the same name but different format
foreach (var rec in recs)
{
// Find the series based on name and type and that the user has access too
@ -149,6 +262,17 @@ public class ExternalMetadataService : IExternalMetadataService
if (seriesForRec != null)
{
recDto.OwnedSeries.Add(seriesForRec);
externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation()
{
SeriesId = seriesForRec.Id,
AniListId = rec.AniListId,
MalId = rec.MalId,
Name = seriesForRec.Name,
Url = rec.SiteUrl,
CoverUrl = rec.CoverUrl,
Summary = rec.Summary,
Provider = rec.Provider
});
continue;
}
@ -164,6 +288,17 @@ public class ExternalMetadataService : IExternalMetadataService
AniListId = rec.AniListId,
MalId = rec.MalId
});
externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation()
{
SeriesId = null,
AniListId = rec.AniListId,
MalId = rec.MalId,
Name = rec.Name,
Url = rec.SiteUrl,
CoverUrl = rec.CoverUrl,
Summary = rec.Summary,
Provider = rec.Provider
});
}
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, recDto.OwnedSeries);
@ -184,9 +319,10 @@ public class ExternalMetadataService : IExternalMetadataService
SeriesName = string.Empty,
LocalizedSeriesName = string.Empty
};
if (seriesId is > 0)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews);
if (series != null)
{
if (payload.AniListId <= 0)

View file

@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Helpers;
using API.Helpers.Builders;
using EasyCaching.Core;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
public interface IRatingService
{
Task<IEnumerable<RatingDto>> GetRatings(int seriesId);
}
public class RatingService : IRatingService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<RatingService> _logger;
private readonly IEasyCachingProvider _cacheProvider;
public const string CacheKey = "rating_";
public RatingService(IUnitOfWork unitOfWork, ILogger<RatingService> logger, IEasyCachingProviderFactory cachingProviderFactory)
{
_unitOfWork = unitOfWork;
_logger = logger;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
}
/// <summary>
/// Fetches Ratings for a given Series. Will check cache first
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<IEnumerable<RatingDto>> GetRatings(int seriesId)
{
var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<IEnumerable<RatingDto>>(cacheKey);
if (results.HasValue)
{
return results.Value;
}
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes);
// Don't send any ratings back for Comic libraries as Kavita+ doesn't have any providers for that
if (series == null || series.Library.Type == LibraryType.Comic)
{
await _cacheProvider.SetAsync(cacheKey, ImmutableList<RatingDto>.Empty, TimeSpan.FromHours(24));
return ImmutableList<RatingDto>.Empty;
}
var ratings = (await GetRatings(license.Value, series)).ToList();
await _cacheProvider.SetAsync(cacheKey, ratings, TimeSpan.FromHours(24));
_logger.LogDebug("Caching external rating for {Key}", cacheKey);
return ratings;
}
private async Task<IEnumerable<RatingDto>> GetRatings(string license, Series series)
{
try
{
return await (Configuration.KavitaPlusApiUrl + "/api/rating")
.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<IEnumerable<RatingDto>>();
}
catch (Exception e)
{
_logger.LogError(e, "An error happened during the request to Kavita+ API");
}
return new List<RatingDto>();
}
}

View file

@ -20,43 +20,13 @@ using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
public record PlusSeriesDto
{
public int? AniListId { get; set; }
public long? MalId { get; set; }
public string? GoogleBooksId { get; set; }
public string? MangaDexId { get; set; }
public string SeriesName { get; set; }
public string? AltSeriesName { get; set; }
public MediaFormat MediaFormat { get; set; }
/// <summary>
/// Optional but can help with matching
/// </summary>
public int? ChapterCount { get; set; }
/// <summary>
/// Optional but can help with matching
/// </summary>
public int? VolumeCount { get; set; }
public int? Year { get; set; }
}
public record MediaRecommendationDto
{
public int Rating { get; set; }
public IEnumerable<string> RecommendationNames { get; set; } = null!;
public string Name { get; set; }
public string CoverUrl { get; set; }
public string SiteUrl { get; set; }
public string? Summary { get; set; }
public int? AniListId { get; set; }
public long? MalId { get; set; }
}
public interface IRecommendationService
{
Task<RecommendationDto> GetRecommendationsForSeries(int userId, int seriesId);
//Task<RecommendationDto> GetRecommendationsForSeries(int userId, int seriesId);
}
public class RecommendationService : IRecommendationService
{
private readonly IUnitOfWork _unitOfWork;

View file

@ -1,114 +1,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.Constants;
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 EasyCaching.Core;
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 static class ReviewService
{
public string Body { get; set; }
public string Tagline { get; set; }
public int Rating { get; set; }
public int TotalVotes { get; set; }
/// <summary>
/// The media's overall Score
/// </summary>
public int Score { get; set; }
public string SiteUrl { get; set; }
/// <summary>
/// In Markdown
/// </summary>
public string RawBody { get; set; }
public string Username { get; set; }
public ScrobbleProvider Provider { get; set; }
}
public interface IReviewService
{
Task<IEnumerable<UserReviewDto>> GetReviewsForSeries(int userId, int seriesId);
}
public class ReviewService : IReviewService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReviewService> _logger;
private readonly ILicenseService _licenseService;
private readonly IEasyCachingProvider _cacheProvider;
public const string CacheKey = "review_";
public ReviewService(IUnitOfWork unitOfWork, ILogger<ReviewService> logger, ILicenseService licenseService,
IEasyCachingProviderFactory cachingProviderFactory)
{
_unitOfWork = unitOfWork;
_logger = logger;
_licenseService = licenseService;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
}
public async Task<IEnumerable<UserReviewDto>> GetReviewsForSeries(int userId, int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return ImmutableList<UserReviewDto>.Empty;
var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId))
.Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
.ToList();
if (!await _licenseService.HasActiveLicense())
{
return userRatings;
}
var cacheKey = CacheKey + seriesId;
IList<UserReviewDto> externalReviews;
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
if (result.HasValue)
{
externalReviews = result.Value.ToList();
}
else
{
var reviews = (await GetExternalReviews(userId, seriesId)).ToList();
externalReviews = SelectSpectrumOfReviews(reviews);
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
_logger.LogDebug("Caching external reviews for {Key}", cacheKey);
}
// Fetch external reviews and splice them in
userRatings.AddRange(externalReviews);
return userRatings;
}
private static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
public static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
{
IList<UserReviewDto> externalReviews;
var totalReviews = reviews.Count;
@ -142,33 +44,7 @@ public class ReviewService : IReviewService
return externalReviews;
}
private async Task<IEnumerable<UserReviewDto>> GetExternalReviews(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<UserReviewDto>();
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)
public static string GetCharacters(string body)
{
if (string.IsNullOrEmpty(body)) return body;
@ -204,29 +80,4 @@ public class ReviewService : IReviewService
return plainText + "…";
}
private async Task<IEnumerable<MediaReviewDto>> 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<IEnumerable<MediaReviewDto>>();
}
catch (Exception e)
{
_logger.LogError(e, "An error happened during the request to Kavita+ API");
}
return new List<MediaReviewDto>();
}
}

View file

@ -5,6 +5,8 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Constants;
using API.Controllers;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
@ -19,6 +21,7 @@ using API.Helpers.Builders;
using API.Services.Plus;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR;
using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Microsoft.Extensions.Logging;
@ -29,7 +32,7 @@ namespace API.Services;
public interface ISeriesService
{
Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId);
Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto);
Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, int userId = 0);
Task<bool> UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto);
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
@ -51,6 +54,7 @@ public class SeriesService : ISeriesService
private readonly ILogger<SeriesService> _logger;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _cacheProvider;
private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto
{
@ -60,7 +64,8 @@ public class SeriesService : ISeriesService
};
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService,
IEasyCachingProviderFactory cachingProviderFactory)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
@ -69,6 +74,8 @@ public class SeriesService : ISeriesService
_scrobblingService = scrobblingService;
_localizationService = localizationService;
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusSeriesDetail);
}
/// <summary>
@ -100,8 +107,15 @@ public class SeriesService : ISeriesService
return minChapter;
}
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
/// <summary>
/// Updates the Series Metadata.
/// </summary>
/// <param name="updateSeriesMetadataDto"></param>
/// <param name="userId">If 0, does not bust any cache</param>
/// <returns></returns>
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, int userId = 0)
{
var hasWebLinksChanged = false;
try
{
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
@ -157,6 +171,8 @@ public class SeriesService : ISeriesService
series.Metadata.WebLinks = string.Empty;
} else
{
hasWebLinksChanged =
series.Metadata.WebLinks.Equals(updateSeriesMetadataDto.SeriesMetadata?.WebLinks);
series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks
.Split(",")
.Where(s => !string.IsNullOrEmpty(s))
@ -299,13 +315,18 @@ public class SeriesService : ISeriesService
_logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work");
}
if (hasWebLinksChanged && userId > 0)
{
_logger.LogDebug("Clearing cache as series weblinks may have changed");
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId + userId);
}
if (updateSeriesMetadataDto.CollectionTags == null) return true;
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
{
await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection,
MessageFactory.SeriesAddedToCollectionEvent(tag.Id,
updateSeriesMetadataDto.SeriesMetadata.SeriesId), false);
MessageFactory.SeriesAddedToCollectionEvent(tag.Id, seriesId), false);
}
return true;
}

View file

@ -141,7 +141,10 @@ public class TaskScheduler : ITaskScheduler
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions);
}
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions);
setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value;
_logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting);
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), CronConverter.ConvertToCronNotation(setting), RecurringJobOptions);
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions);
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions);

View file

@ -122,16 +122,28 @@ public class ProcessSeries : IProcessSeries
}
catch (Exception ex)
{
var series2 = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format, false);
var details = $"Series 1: {firstInfo.Series} Series 2: {series2.Name}" + "\n" +
$"Localized: {firstInfo.LocalizedSeries} Localized: {series2.LocalizedName}" + "\n" +
$"Filename: {_directoryService.FileSystem.FileInfo.New(firstInfo.FullFilePath).Directory} Filename: {series2.FolderPath}";
_logger.LogError(ex, "Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct",
firstInfo.Series, firstInfo.LocalizedSeries, library.Name);
var seriesCollisions = await _unitOfWork.SeriesRepository.GetAllSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format);
await _eventHub.SendMessageAsync(MessageFactory.Error,
MessageFactory.ErrorEvent($"Scanner found a Series {firstInfo.Series} which matched another Series {firstInfo.LocalizedSeries} in a different folder parallel to Library {library.Name} root folder. This is not allowed. Please correct",
details));
seriesCollisions = seriesCollisions.Where(collision =>
collision.Name != firstInfo.Series || collision.LocalizedName != firstInfo.LocalizedSeries).ToList();
if (seriesCollisions.Any())
{
var tableRows = seriesCollisions.Select(collision =>
$"<tr><td>Name: {firstInfo.Series}</td><td>Name: {collision.Name}</td></tr>" +
$"<tr><td>Localized: {firstInfo.LocalizedSeries}</td><td>Localized: {collision.LocalizedName}</td></tr>" +
$"<tr><td>Filename: {Parser.Parser.NormalizePath(_directoryService.FileSystem.FileInfo.New(firstInfo.FullFilePath).Directory?.ToString())}</td><td>Filename: {Parser.Parser.NormalizePath(collision.FolderPath)}</td></tr>"
);
var htmlTable = $"<table class='table table-striped'><thead><tr><th>Series 1</th><th>Series 2</th></tr></thead><tbody>{string.Join(string.Empty, tableRows)}</tbody></table>";
_logger.LogError(ex, "Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct",
firstInfo.Series, firstInfo.LocalizedSeries, library.Name);
await _eventHub.SendMessageAsync(MessageFactory.Error,
MessageFactory.ErrorEvent($"Library {library.Name} Series collision on {firstInfo.Series}",
htmlTable));
}
return;
}
@ -223,6 +235,9 @@ public class ProcessSeries : IProcessSeries
if (seriesAdded)
{
// See if any recommendations can link up to the series
_logger.LogInformation("Linking up External Recommendations new series (if applicable)");
await _unitOfWork.ExternalSeriesMetadataRepository.LinkRecommendationsToSeries(series);
await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded,
MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false);
}