Personal Table of Contents (#2148)

* Fixed a bad default setting for token key

* Changed the payment link to support Google Pay

* Fixed duplicate events occurring on newly added series from a scan.

Fixed the version update code from not firing and made it check every 4-6 hours (random per user per restart)

* Check for new releases on startup as well.

Added Personal Table of Contents (called Bookmarks on epub and pdf reader). The idea is that sometimes you want to bookmark certain parts of pages to get back to quickly later. This mechanism will allow you to do that without having to edit the underlying ToC.

* Added a button to update modal to show how to update for those unaware.

* Darkened the link text within tables to be more visible.

* Update link for how to update now is dynamic for docker users

* Refactored to send proper star/end dates for scrobble read events for upcoming changes in the API.

Added GoogleBooks Rating UI code if I go forward with API changes.

* When Scrobbling, send when the first and last progress for the series was.

Added OpenLibrary icon for upcoming enhancements for Kavita+.

Changed the Update checker to execute at start.

* Fixed backups not saving favicons in the correct place

* Refactored the layout code for Personal ToC

* More bugfixes around toc

* Box alignment

* Fixed up closing the overlay when bookmark mode is active

* Fixed up closing the overlay when bookmark mode is active

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2023-07-21 17:29:35 -05:00 committed by GitHub
parent f3b8074b3a
commit a0a6da9c60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 3538 additions and 244 deletions

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using API.Data.Metadata;
using API.DTOs.Reader;
using API.Entities;
@ -898,7 +897,7 @@ public class BookService : IBookService
/// <param name="mappings">Epub mappings</param>
/// <param name="page">Page number we are loading</param>
/// <returns></returns>
public async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
private async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
{
await InlineStyles(doc, book, apiBase, body);

View file

@ -26,7 +26,6 @@ public class StartupTasksHostedService : IHostedService
taskScheduler.ScheduleUpdaterTasks();
try
{
// These methods will automatically check if stat collection is disabled to prevent sending any data regardless

View file

@ -9,6 +9,7 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Helpers;
using API.Helpers.Builders;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
@ -59,19 +60,7 @@ public class RatingService : IRatingService
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDto()
{
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
VolumeCount = series.Volumes.Count,
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
Year = series.Metadata.ReleaseYear
})
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.ReceiveJson<IEnumerable<RatingDto>>();
}
catch (Exception e)

View file

@ -11,6 +11,7 @@ using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
using Flurl.Http;
using Kavita.Common;
@ -24,6 +25,7 @@ public record PlusSeriesDto
{
public int? AniListId { get; set; }
public long? MalId { get; set; }
public string? GoogleBooksId { get; set; }
public string SeriesName { get; set; }
public string? AltSeriesName { get; set; }
public MediaFormat MediaFormat { get; set; }
@ -134,19 +136,7 @@ public class RecommendationService : IRecommendationService
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDto()
{
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
VolumeCount = series.Volumes.Count,
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
Year = series.Metadata.ReleaseYear
})
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.ReceiveJson<IEnumerable<MediaRecommendationDto>>();
}

View file

@ -63,11 +63,14 @@ public class ScrobblingService : IScrobblingService
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id=";
private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
{
{AniListWeblinkWebsite, 0},
{MalWeblinkWebsite, 0},
{GoogleBooksWeblinkWebsite, 0},
};
private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
@ -208,8 +211,8 @@ public class ScrobblingService : IScrobblingService
SeriesId = series.Id,
LibraryId = series.LibraryId,
ScrobbleEventType = ScrobbleEventType.Review,
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
AppUserId = userId,
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
ReviewBody = reviewBody,
@ -253,8 +256,8 @@ public class ScrobblingService : IScrobblingService
SeriesId = series.Id,
LibraryId = series.LibraryId,
ScrobbleEventType = ScrobbleEventType.ScoreUpdated,
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
AppUserId = userId,
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
Rating = rating
@ -310,8 +313,8 @@ public class ScrobblingService : IScrobblingService
SeriesId = series.Id,
LibraryId = series.LibraryId,
ScrobbleEventType = ScrobbleEventType.ChapterRead,
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
AppUserId = userId,
VolumeNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId),
@ -353,8 +356,8 @@ public class ScrobblingService : IScrobblingService
SeriesId = series.Id,
LibraryId = series.LibraryId,
ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead,
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
AppUserId = userId,
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
};
@ -542,7 +545,7 @@ public class ScrobblingService : IScrobblingService
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ProcessUpdatesSinceLastSync()
{
// Check how many scrobbles we have available then only do those.
// Check how many scrobble events we have available then only do those.
_logger.LogInformation("Starting Scrobble Processing");
var userRateLimits = new Dictionary<int, int>();
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
@ -623,7 +626,7 @@ public class ScrobblingService : IScrobblingService
readEvt.AppUser.Id);
_unitOfWork.ScrobbleRepository.Update(readEvt);
}
progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, async evt => new ScrobbleDto()
{
Format = evt.Format,
AniListId = evt.AniListId,
@ -634,12 +637,14 @@ public class ScrobblingService : IScrobblingService
AniListToken = evt.AppUser.AniListAccessToken,
SeriesName = evt.Series.Name,
LocalizedSeriesName = evt.Series.LocalizedName,
StartedReadingDateUtc = evt.CreatedUtc,
ScrobbleDateUtc = evt.LastModifiedUtc,
Year = evt.Series.Metadata.ReleaseYear
Year = evt.Series.Metadata.ReleaseYear,
StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id),
LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id),
});
progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter,
totalProgress, evt => Task.FromResult(new ScrobbleDto()
{
Format = evt.Format,
AniListId = evt.AniListId,
@ -650,9 +655,10 @@ public class ScrobblingService : IScrobblingService
LocalizedSeriesName = evt.Series.LocalizedName,
Rating = evt.Rating,
Year = evt.Series.Metadata.ReleaseYear
});
}));
progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter,
totalProgress, evt => Task.FromResult(new ScrobbleDto()
{
Format = evt.Format,
AniListId = evt.AniListId,
@ -665,21 +671,22 @@ public class ScrobblingService : IScrobblingService
Year = evt.Series.Metadata.ReleaseYear,
ReviewBody = evt.ReviewBody,
ReviewTitle = evt.ReviewTitle
});
}));
progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
{
Format = evt.Format,
AniListId = evt.AniListId,
MALId = (int?) evt.MalId,
ScrobbleEventType = evt.ScrobbleEventType,
ChapterNumber = evt.ChapterNumber,
VolumeNumber = evt.VolumeNumber,
AniListToken = evt.AppUser.AniListAccessToken,
SeriesName = evt.Series.Name,
LocalizedSeriesName = evt.Series.LocalizedName,
Year = evt.Series.Metadata.ReleaseYear
});
progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter,
totalProgress, evt => Task.FromResult(new ScrobbleDto()
{
Format = evt.Format,
AniListId = evt.AniListId,
MALId = (int?) evt.MalId,
ScrobbleEventType = evt.ScrobbleEventType,
ChapterNumber = evt.ChapterNumber,
VolumeNumber = evt.VolumeNumber,
AniListToken = evt.AppUser.AniListAccessToken,
SeriesName = evt.Series.Name,
LocalizedSeriesName = evt.Series.LocalizedName,
Year = evt.Series.Metadata.ReleaseYear
}));
}
catch (FlurlHttpException)
{
@ -693,7 +700,7 @@ public class ScrobblingService : IScrobblingService
}
private async Task<int> ProcessEvents(IEnumerable<ScrobbleEvent> events, IDictionary<int, int> userRateLimits,
int usersToScrobble, int progressCounter, int totalProgress, Func<ScrobbleEvent, ScrobbleDto> createEvent)
int usersToScrobble, int progressCounter, int totalProgress, Func<ScrobbleEvent, Task<ScrobbleDto>> createEvent)
{
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
foreach (var evt in events)
@ -714,7 +721,7 @@ public class ScrobblingService : IScrobblingService
try
{
var data = createEvent(evt);
var data = await createEvent(evt);
userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt);
evt.IsProcessed = true;
evt.ProcessDateUtc = DateTime.UtcNow;
@ -784,17 +791,31 @@ public class ScrobblingService : IScrobblingService
/// <param name="webLinks"></param>
/// <param name="website"></param>
/// <returns></returns>
public static long? ExtractId(string webLinks, string website)
public static T? ExtractId<T>(string webLinks, string website)
{
var index = WeblinkExtractionMap[website];
foreach (var webLink in webLinks.Split(','))
{
if (!webLink.StartsWith(website)) continue;
var tokens = webLink.Split(website)[1].Split('/');
return long.Parse(tokens[index]);
var value = tokens[index];
if (typeof(T) == typeof(int))
{
if (int.TryParse(value, out var intValue))
return (T)(object)intValue;
}
else if (typeof(T) == typeof(long))
{
if (long.TryParse(value, out var longValue))
return (T)(object)longValue;
}
else if (typeof(T) == typeof(string))
{
return (T)(object)value;
}
}
return 0;
return default(T?);
}
private async Task<int> SetAndCheckRateLimit(IDictionary<int, int> userRateLimits, AppUser user, string license)

View file

@ -9,6 +9,7 @@ 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;
@ -133,19 +134,7 @@ public class ReviewService : IReviewService
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDto()
{
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
VolumeCount = series.Volumes.Count,
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
Year = series.Metadata.ReleaseYear
})
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.ReceiveJson<IEnumerable<MediaReviewDto>>();
}

View file

@ -61,6 +61,7 @@ public class TaskScheduler : ITaskScheduler
public const string DefaultQueue = "default";
public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read";
public const string UpdateYearlyStatsTaskId = "update-yearly-stats";
public const string CheckForUpdateId = "check-updates";
public const string CleanupDbTaskId = "cleanup-db";
public const string CleanupTaskId = "cleanup";
public const string BackupTaskId = "backup";
@ -226,10 +227,8 @@ public class TaskScheduler : ITaskScheduler
public void ScheduleUpdaterTasks()
{
_logger.LogInformation("Scheduling Auto-Update tasks");
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(5, 23)), new RecurringJobOptions()
{
TimeZone = TimeZoneInfo.Local
});
RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions);
BackgroundJob.Enqueue(() => CheckForUpdate());
}
public void ScanFolder(string folderPath, TimeSpan delay)

View file

@ -145,7 +145,7 @@ public class BackupService : IBackupService
private void CopyFaviconsToBackupDirectory(string tempDirectory)
{
_directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, tempDirectory);
_directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons"));
}
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)

View file

@ -72,13 +72,11 @@ public class VersionUpdaterService : IVersionUpdaterService
/// <summary>
/// Fetches the latest release from Github
/// </summary>
/// <returns>Latest update or null if current version is greater than latest update</returns>
public async Task<UpdateNotificationDto?> CheckForUpdate()
/// <returns>Latest update</returns>
public async Task<UpdateNotificationDto> CheckForUpdate()
{
var update = await GetGithubRelease();
var dto = CreateDto(update);
if (dto == null) return null;
return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto;
return CreateDto(update);
}
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()