Misc Bugfixes (#2216)

* Folder watching will now appropriately ignore changes that occur in blacklisted folders.

* Fixed up recently updated from dashboard not opening a pre-sorted page. There were issues with how encoding and decoding was done plus missing code.

* Fixed up all streams from Dashboard opening to correctly filtered pages.

* All search linking now works.

* Rating tooltip and stars are bigger on mobile.

* A bit of cleanup

* Added day breakdown to user stats page.

* Removed Token checks before we write events to the history table for scrobbling.

Refactored so series holds will prevent writing events for reviews, ratings, etc.

* Fixed a potential bug where series name could be taken from a chapter that isn't the first ordered (very unlikely) for epubs.

Fixed a bug where Volume 1.5 could be selected for series-level metadata over Volume 1.

* Optimized the license check code so that users without any license entered would still take advantage of the cache layer.

* Sped up an API that checks if the library allows scrobbling

* Cleaned up the mobile CSS a bit for filters.
This commit is contained in:
Joe Milazzo 2023-08-15 16:33:39 -05:00 committed by GitHub
parent ef3e76e3e5
commit c84a3294e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 324 additions and 246 deletions

View file

@ -128,15 +128,18 @@ public class LicenseService : ILicenseService
/// <remarks>Expected to be called at startup and on reoccurring basis</remarks>
public async Task ValidateLicenseStatus()
{
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
try
{
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
if (string.IsNullOrEmpty(license.Value)) return;
if (string.IsNullOrEmpty(license.Value)) {
await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
return;
}
_logger.LogInformation("Validating Kavita+ License");
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
await provider.FlushAsync();
await provider.FlushAsync();
var isValid = await IsLicenseValid(license.Value);
await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout);
@ -145,6 +148,7 @@ public class LicenseService : ILicenseService
catch (Exception ex)
{
_logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins");
await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30));
}
}

View file

@ -104,7 +104,7 @@ public class ScrobblingService : IScrobblingService
/// <summary>
///
/// An automated job that will run against all user's tokens and validate if they are still active
/// </summary>
/// <remarks>This service can validate without license check as the task which calls will be guarded</remarks>
/// <returns></returns>
@ -115,6 +115,7 @@ public class ScrobblingService : IScrobblingService
foreach (var user in users)
{
if (string.IsNullOrEmpty(user.AniListAccessToken) || !_tokenService.HasTokenExpired(user.AniListAccessToken)) continue;
_logger.LogInformation("User {UserName}'s AniList token has expired! They need to regenerate it for scrobbling to work", user.UserName);
await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired,
MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), user.Id);
}
@ -184,17 +185,13 @@ public class ScrobblingService : IScrobblingService
public async Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody)
{
if (!await _licenseService.HasActiveLicense()) return;
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException(await _localizationService.Translate(userId, "unable-to-register-k+"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
_logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
ScrobbleEventType.Review);
@ -229,17 +226,12 @@ public class ScrobblingService : IScrobblingService
public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating)
{
if (!await _licenseService.HasActiveLicense()) return;
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
_logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
ScrobbleEventType.ScoreUpdated);
@ -273,22 +265,12 @@ public class ScrobblingService : IScrobblingService
public async Task ScrobbleReadingUpdate(int userId, int seriesId)
{
if (!await _licenseService.HasActiveLicense()) return;
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
{
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, userId);
return;
}
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
_logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id,
ScrobbleEventType.ChapterRead);
@ -338,17 +320,12 @@ public class ScrobblingService : IScrobblingService
public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead)
{
if (!await _licenseService.HasActiveLicense()) return;
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
_logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name);
if (await CheckIfCanScrobble(userId, seriesId, series)) return;
var existing = await _unitOfWork.ScrobbleRepository.Exists(userId, series.Id,
onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead);
@ -369,6 +346,21 @@ public class ScrobblingService : IScrobblingService
_logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId);
}
private async Task<bool> CheckIfCanScrobble(int userId, int seriesId, Series series)
{
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
{
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name,
userId);
return true;
}
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return true;
if (library.Type == LibraryType.Comic) return true;
return false;
}
private async Task<int> GetRateLimit(string license, string aniListToken)
{
if (string.IsNullOrWhiteSpace(aniListToken)) return 0;

View file

@ -65,16 +65,19 @@ public class SeriesService : ISeriesService
/// <returns></returns>
public static Chapter? GetFirstChapterForMetadata(Series series)
{
var sortedVolumes = series.Volumes.OrderBy(v => v.Number, ChapterSortComparer.Default);
var sortedVolumes = series.Volumes
.Where(v => float.TryParse(v.Name, out var parsedValue) && parsedValue != 0.0f)
.OrderBy(v => float.TryParse(v.Name, out var parsedValue) ? parsedValue : float.MaxValue);
var minVolumeNumber = sortedVolumes
.Where(v => v.Number != 0)
.MinBy(v => v.Number);
.MinBy(v => float.Parse(v.Name));
var minChapter = series.Volumes
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default))
var allChapters = series.Volumes
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList();
var minChapter = allChapters
.FirstOrDefault();
if (minVolumeNumber != null && minChapter != null && float.Parse(minChapter.Number) > minVolumeNumber.Number)
if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, out var chapNum) && chapNum >= minVolumeNumber.Number)
{
return minVolumeNumber.Chapters.MinBy(c => float.Parse(c.Number), ChapterSortComparer.Default);
}

View file

@ -27,7 +27,7 @@ public interface IStatisticService
Task<IEnumerable<TopReadDto>> GetTopUsers(int days);
Task<IEnumerable<ReadHistoryEvent>> GetReadingHistory(int userId);
Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0, int days = 0);
IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown();
IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown(int userId = 0);
IEnumerable<StatCount<int>> GetPagesReadCountByYear(int userId = 0);
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
Task UpdateServerStatistics();
@ -411,11 +411,12 @@ public class StatisticService : IStatisticService
return results.OrderBy(r => r.Value);
}
public IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown()
public IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown(int userId)
{
return _context.AppUserProgresses
.AsSplitQuery()
.AsNoTracking()
.WhereIf(userId > 0, p => p.AppUserId == userId)
.GroupBy(p => p.LastModified.DayOfWeek)
.OrderBy(g => g.Key)
.Select(g => new StatCount<DayOfWeek>{ Value = g.Key, Count = g.Count() })

View file

@ -229,14 +229,20 @@ public class LibraryWatcher : ILibraryWatcher
public async Task ProcessChange(string filePath, bool isDirectoryChange = false)
{
var sw = Stopwatch.StartNew();
_logger.LogDebug("[LibraryWatcher] Processing change of {FilePath}", filePath);
_logger.LogTrace("[LibraryWatcher] Processing change of {FilePath}", filePath);
try
{
// If the change occurs in a blacklisted folder path, then abort processing
if (Parser.Parser.HasBlacklistedFolderInPath(filePath))
{
return;
}
// If not a directory change AND file is not an archive or book, ignore
if (!isDirectoryChange &&
!(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath)))
{
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath);
_logger.LogTrace("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath);
return;
}
@ -248,10 +254,10 @@ public class LibraryWatcher : ILibraryWatcher
.ToList();
var fullPath = GetFolder(filePath, libraryFolders);
_logger.LogDebug("Folder path: {FolderPath}", fullPath);
_logger.LogTrace("Folder path: {FolderPath}", fullPath);
if (string.IsNullOrEmpty(fullPath))
{
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
_logger.LogTrace("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
return;
}

View file

@ -146,7 +146,6 @@ public class ProcessSeries : IProcessSeries
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
// parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort)
// BUG: This check doesn't work for Books, as books usually have metadata on all files. (#2167)
var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo);
UpdateVolumes(series, parsedInfos, forceUpdate);