diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 77fa9ac1f..84e4d5fd5 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -448,8 +448,9 @@ public class CleanupServiceTests : AbstractDbTest // Delete the Chapter _context.Chapter.Remove(c); await _unitOfWork.CommitAsync(); - Assert.Single(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + // NOTE: This may not be needed, the underlying DB structure seems fixed as of v0.7 await cleanupService.CleanupDbEntries(); Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a9229fa7c..9f05add96 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -41,7 +41,6 @@ public class AccountController : BaseApiController private readonly IMapper _mapper; private readonly IAccountService _accountService; private readonly IEmailService _emailService; - private readonly IHostEnvironment _environment; private readonly IEventHub _eventHub; /// @@ -50,8 +49,7 @@ public class AccountController : BaseApiController ITokenService tokenService, IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, IAccountService accountService, - IEmailService emailService, IHostEnvironment environment, - IEventHub eventHub) + IEmailService emailService, IEventHub eventHub) { _userManager = userManager; _signInManager = signInManager; @@ -61,7 +59,6 @@ public class AccountController : BaseApiController _mapper = mapper; _accountService = accountService; _emailService = emailService; - _environment = environment; _eventHub = eventHub; } @@ -202,7 +199,7 @@ public class AccountController : BaseApiController } // Update LastActive on account - user.LastActive = DateTime.Now; + user.UpdateLastActive(); user.UserPreferences ??= new AppUserPreferences { Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme() diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index cd9402fe7..0800f3977 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -61,8 +61,7 @@ public class CollectionController : BaseApiController queryString = queryString.Replace(@"%", string.Empty); if (queryString.Length == 0) return await GetAllTags(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, user.Id); + return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId()); } /// diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 98fa072e9..6c44c2659 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -201,19 +201,20 @@ public class DownloadController : BaseApiController if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty"); // We know that all bookmarks will be for one single seriesId - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); + var username = User.GetUsername(); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); var filename = $"{series.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F)); var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct()); var filePath = _archiveService.CreateZipForDownload(files, - $"download_{user.Id}_{seriesIds}_bookmarks"); + $"download_{userId}_{seriesIds}_bookmarks"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 1F)); return PhysicalFile(filePath, DefaultContentType, filename, true); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 800743766..79948ce60 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -244,8 +244,7 @@ public class ReaderController : BaseApiController [HttpGet("bookmark-info")] public async Task> GetBookmarkInfo(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - var totalPages = await _cacheService.CacheBookmarkForSeries(user.Id, seriesId); + var totalPages = await _cacheService.CacheBookmarkForSeries(User.GetUserId(), seriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); return Ok(new BookmarkInfoDto() @@ -451,25 +450,15 @@ public class ReaderController : BaseApiController [HttpGet("get-progress")] public async Task> GetProgress(int chapterId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); - var progressBookmark = new ProgressDto() + var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, User.GetUserId()); + if (progress == null) return Ok(new ProgressDto() { PageNum = 0, ChapterId = chapterId, VolumeId = 0, SeriesId = 0 - }; - if (user.Progresses == null) return Ok(progressBookmark); - var progress = user.Progresses.FirstOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId); - - if (progress != null) - { - progressBookmark.SeriesId = progress.SeriesId; - progressBookmark.VolumeId = progress.VolumeId; - progressBookmark.PageNum = progress.PagesRead; - progressBookmark.BookScrollId = progress.BookScrollId; - } - return Ok(progressBookmark); + }); + return Ok(progress); } /// @@ -480,9 +469,7 @@ public class ReaderController : BaseApiController [HttpPost("progress")] public async Task BookmarkProgress(ProgressDto progressDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - - if (await _readerService.SaveReadingProgress(progressDto, user.Id)) return Ok(true); + if (await _readerService.SaveReadingProgress(progressDto, User.GetUserId())) return Ok(true); return BadRequest("Could not save progress"); } @@ -506,7 +493,7 @@ public class ReaderController : BaseApiController /// /// [HttpGet("has-progress")] - public async Task> HasProgress(int seriesId) + public async Task> HasProgress(int seriesId) { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId)); diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 72678780c..1ecebf23c 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -82,8 +82,7 @@ public class ReadingListController : BaseApiController [HttpGet("items")] public async Task>> GetListForUser(int readingListId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId); + var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, User.GetUserId()); return Ok(items); } diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index 893cb852a..f945c735d 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -26,10 +26,8 @@ public class RecommendedController : BaseApiController [HttpGet("quick-reads")] public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - userParams ??= new UserParams(); - var series = await _unitOfWork.SeriesRepository.GetQuickReads(user.Id, libraryId, userParams); + var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -44,10 +42,8 @@ public class RecommendedController : BaseApiController [HttpGet("quick-catchup-reads")] public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - userParams ??= new UserParams(); - var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(user.Id, libraryId, userParams); + var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -62,11 +58,10 @@ public class RecommendedController : BaseApiController [HttpGet("highly-rated")] public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - + var userId = User.GetUserId(); userParams ??= new UserParams(); - var series = await _unitOfWork.SeriesRepository.GetHighlyRated(user.Id, libraryId, userParams); - await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); + var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); } @@ -81,11 +76,11 @@ public class RecommendedController : BaseApiController [HttpGet("more-in")] public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); userParams ??= new UserParams(); - var series = await _unitOfWork.SeriesRepository.GetMoreIn(user.Id, libraryId, genreId, userParams); - await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series); + var series = await _unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -100,10 +95,8 @@ public class RecommendedController : BaseApiController [HttpGet("rediscover")] public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams userParams) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - userParams ??= new UserParams(); - var series = await _unitOfWork.SeriesRepository.GetRediscover(user.Id, libraryId, userParams); + var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index c93e93fe9..c0059e0e5 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -7,6 +7,7 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.Filtering; +using API.DTOs.Metadata; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; @@ -121,11 +122,12 @@ public class SeriesController : BaseApiController [HttpGet("chapter")] public async Task> GetChapter(int chapterId) { - return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId)); + var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); + return Ok(await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter)); } [HttpGet("chapter-metadata")] - public async Task> GetChapterMetadata(int chapterId) + public async Task> GetChapterMetadata(int chapterId) { return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); } diff --git a/API/Controllers/TachiyomiController.cs b/API/Controllers/TachiyomiController.cs index 77f32764d..84bde35d1 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/API/Controllers/TachiyomiController.cs @@ -32,8 +32,7 @@ public class TachiyomiController : BaseApiController public async Task> GetLatestChapter(int seriesId) { if (seriesId < 1) return BadRequest("seriesId must be greater than 0"); - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _tachiyomiService.GetLatestChapter(seriesId, userId)); + return Ok(await _tachiyomiService.GetLatestChapter(seriesId, User.GetUserId())); } /// diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 3a52866e0..434117567 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -49,7 +49,7 @@ public class UploadController : BaseApiController [HttpPost("upload-by-url")] public async Task> GetImageFromFile(UploadUrlDto dto) { - var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace('/', '_').Replace(':', '_'); + var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace('/', '_').Replace(':', '_'); var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty); try { diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 12bbeaa85..424681cf4 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -35,8 +35,7 @@ public class WantToReadController : BaseApiController public async Task>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto) { userParams ??= new UserParams(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, userParams, filterDto); + var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); return Ok(pagedList); } @@ -44,8 +43,7 @@ public class WantToReadController : BaseApiController [HttpGet] public async Task> IsSeriesInWantToRead([FromQuery] int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.IsSeriesInWantToRead(user.Id, seriesId)); + return Ok(await _unitOfWork.SeriesRepository.IsSeriesInWantToRead(User.GetUserId(), seriesId)); } /// diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 60e08b554..b93bd124a 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Reader; using API.Entities.Enums; using API.Entities.Interfaces; @@ -11,7 +9,7 @@ namespace API.DTOs; /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying /// file (abstracted from type). /// -public class ChapterDto : IHasReadTimeEstimate +public class ChapterDto : IHasReadTimeEstimate, IEntityDate { public int Id { get; init; } /// @@ -43,6 +41,10 @@ public class ChapterDto : IHasReadTimeEstimate /// public int PagesRead { get; set; } /// + /// The last time a chapter was read by current authenticated user + /// + public DateTime LastReadingProgressUtc { get; set; } + /// /// If the Cover Image is locked for this entity /// public bool CoverImageLocked { get; set; } @@ -53,7 +55,10 @@ public class ChapterDto : IHasReadTimeEstimate /// /// When chapter was created /// - public DateTime Created { get; init; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } /// /// When the chapter was released. /// @@ -77,7 +82,6 @@ public class ChapterDto : IHasReadTimeEstimate /// Total words in a Chapter (books only) /// public long WordCount { get; set; } = 0L; - /// /// Formatted Volume title ie) Volume 2. /// diff --git a/API/DTOs/Device/DeviceDto.cs b/API/DTOs/Device/DeviceDto.cs index e5344f31e..c05671113 100644 --- a/API/DTOs/Device/DeviceDto.cs +++ b/API/DTOs/Device/DeviceDto.cs @@ -30,4 +30,8 @@ public class DeviceDto /// Last time this device was used to send a file /// public DateTime LastUsed { get; set; } + /// + /// Last time this device was used to send a file + /// + public DateTime LastUsedUtc { get; set; } } diff --git a/API/DTOs/Jobs/JobDto.cs b/API/DTOs/Jobs/JobDto.cs index 5af700528..dc566961e 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/API/DTOs/Jobs/JobDto.cs @@ -20,5 +20,13 @@ public class JobDto /// Last time the job was run /// public DateTime? LastExecution { get; set; } + /// + /// When the job was created + /// + public DateTime? CreatedAtUtc { get; set; } + /// + /// Last time the job was run + /// + public DateTime? LastExecutionUtc { get; set; } public string Cron { get; set; } } diff --git a/API/DTOs/ProgressDto.cs b/API/DTOs/ProgressDto.cs index b3ffd2914..e5f796bcc 100644 --- a/API/DTOs/ProgressDto.cs +++ b/API/DTOs/ProgressDto.cs @@ -16,12 +16,8 @@ public class ProgressDto [Required] public int LibraryId { get; set; } /// - /// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position + /// For EPUB reader, this can be an optional string of the id of a part marker, to help resume reading position /// on pages that combine multiple "chapters". /// public string BookScrollId { get; set; } - /// - /// Last time the progress was synced from UI or external app - /// - public DateTime LastModified { get; set; } } diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/API/DTOs/ReadingLists/ReadingListItemDto.cs index 91c436264..79fdd9d8f 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/API/DTOs/ReadingLists/ReadingListItemDto.cs @@ -19,6 +19,7 @@ public class ReadingListItemDto public int VolumeId { get; set; } public int LibraryId { get; set; } public LibraryType LibraryType { get; set; } + public string LibraryName { get; set; } public string Title { get; set; } /// /// Release Date from Chapter @@ -28,4 +29,13 @@ public class ReadingListItemDto /// Used internally only /// public int ReadingListId { get; set; } + /// + /// The last time a reading list item (underlying chapter) was read by current authenticated user + /// + public DateTime LastReadingProgressUtc { get; set; } + /// + /// File size of underlying item + /// + /// This is only used for CDisplayEx + public long FileSize { get; set; } } diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/API/DTOs/Theme/SiteThemeDto.cs index 7c44a1cd0..6e3650e21 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/API/DTOs/Theme/SiteThemeDto.cs @@ -1,5 +1,6 @@ using System; using API.Entities.Enums.Theme; +using API.Entities.Interfaces; using API.Services; namespace API.DTOs.Theme; @@ -7,7 +8,7 @@ namespace API.DTOs.Theme; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui /// -public class SiteThemeDto +public class SiteThemeDto : IEntityDate { public int Id { get; set; } /// @@ -29,5 +30,7 @@ public class SiteThemeDto public ThemeProvider Provider { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } public string Selector => "bg-" + Name.ToLower(); } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 1606f9c71..9a6af8b3e 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -112,18 +112,19 @@ public sealed class DataContext : IdentityDbContext +/// Introduced in v0.6.1.38 or v0.7.0, +/// +public static class MigrateToUtcDates +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + // if current version is > 0.6.1.38, then we can exit and not perform + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (Version.Parse(settings.InstallVersion) > new Version(0, 6, 1, 38)) + { + return; + } + logger.LogCritical("Running MigrateToUtcDates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); + + #region Series + logger.LogInformation("Updating Dates on Series..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Series SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastChapterAddedUtc] = datetime([LastChapterAdded], 'utc'), + [LastFolderScannedUtc] = datetime([LastFolderScanned], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Series...Done"); + #endregion + + #region Library + logger.LogInformation("Updating Dates on Libraries..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Library SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Libraries...Done"); + #endregion + + #region Volume + try + { + logger.LogInformation("Updating Dates on Volumes..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Volume SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'); + "); + logger.LogInformation("Updating Dates on Volumes...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Volumes...Failed"); + } + #endregion + + #region Chapter + try + { + logger.LogInformation("Updating Dates on Chapters..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Chapter SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Chapters...Done"); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Updating Dates on Chapters...Failed"); + } + #endregion + + #region AppUserBookmark + logger.LogInformation("Updating Dates on Bookmarks..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserBookmark SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Bookmarks...Done"); + #endregion + + #region AppUserProgress + logger.LogInformation("Updating Dates on Progress..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE AppUserProgresses SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Progress...Done"); + #endregion + + #region Device + logger.LogInformation("Updating Dates on Device..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE Device SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastUsedUtc] = datetime([LastUsed], 'utc') + ; + "); + logger.LogInformation("Updating Dates on Device...Done"); + #endregion + + #region MangaFile + logger.LogInformation("Updating Dates on MangaFile..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE MangaFile SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc'), + [LastFileAnalysisUtc] = datetime([LastFileAnalysis], 'utc') + ; + "); + logger.LogInformation("Updating Dates on MangaFile...Done"); + #endregion + + #region ReadingList + logger.LogInformation("Updating Dates on ReadingList..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE ReadingList SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on ReadingList...Done"); + #endregion + + #region SiteTheme + logger.LogInformation("Updating Dates on SiteTheme..."); + await dataContext.Database.ExecuteSqlRawAsync(@" + UPDATE SiteTheme SET + [LastModifiedUtc] = datetime([LastModified], 'utc'), + [CreatedUtc] = datetime([Created], 'utc') + ; + "); + logger.LogInformation("Updating Dates on SiteTheme...Done"); + #endregion + + logger.LogInformation("MigrateToUtcDates migration finished"); + + } +} diff --git a/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs b/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs new file mode 100644 index 000000000..ff9394649 --- /dev/null +++ b/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs @@ -0,0 +1,1836 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230210153842_UtcTimes")] + partial class UtcTimes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInDashboard") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IncludeInSearch") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NameLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Devices"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20230210153842_UtcTimes.cs b/API/Data/Migrations/20230210153842_UtcTimes.cs new file mode 100644 index 000000000..9354cded7 --- /dev/null +++ b/API/Data/Migrations/20230210153842_UtcTimes.cs @@ -0,0 +1,323 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class UtcTimes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "Volume", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "Volume", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "SiteTheme", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "SiteTheme", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastChapterAddedUtc", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastFolderScannedUtc", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "ReadingList", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "ReadingList", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "MangaFile", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastFileAnalysisUtc", + table: "MangaFile", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "MangaFile", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "Library", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "Library", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "Device", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "Device", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastUsedUtc", + table: "Device", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "Chapter", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "Chapter", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "AspNetUsers", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastActiveUtc", + table: "AspNetUsers", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "AppUserProgresses", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "AppUserProgresses", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedUtc", + table: "AppUserBookmark", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModifiedUtc", + table: "AppUserBookmark", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgresses_ChapterId", + table: "AppUserProgresses", + column: "ChapterId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgresses_Chapter_ChapterId", + table: "AppUserProgresses", + column: "ChapterId", + principalTable: "Chapter", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgresses_Chapter_ChapterId", + table: "AppUserProgresses"); + + migrationBuilder.DropIndex( + name: "IX_AppUserProgresses_ChapterId", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "SiteTheme"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "SiteTheme"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "Series"); + + migrationBuilder.DropColumn( + name: "LastChapterAddedUtc", + table: "Series"); + + migrationBuilder.DropColumn( + name: "LastFolderScannedUtc", + table: "Series"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "Series"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "ReadingList"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "ReadingList"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "MangaFile"); + + migrationBuilder.DropColumn( + name: "LastFileAnalysisUtc", + table: "MangaFile"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "MangaFile"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "Library"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "Library"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "Device"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "Device"); + + migrationBuilder.DropColumn( + name: "LastUsedUtc", + table: "Device"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LastActiveUtc", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "CreatedUtc", + table: "AppUserBookmark"); + + migrationBuilder.DropColumn( + name: "LastModifiedUtc", + table: "AppUserBookmark"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 13a553b30..56a8cacbd 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -72,6 +72,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -82,6 +85,9 @@ namespace API.Data.Migrations b.Property("LastActive") .HasColumnType("TEXT"); + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + b.Property("LockoutEnabled") .HasColumnType("INTEGER"); @@ -146,12 +152,18 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("FileName") .HasColumnType("TEXT"); b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("Page") .HasColumnType("INTEGER"); @@ -283,9 +295,15 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("LibraryId") .HasColumnType("INTEGER"); @@ -302,6 +320,8 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); + b.HasIndex("ChapterId"); + b.HasIndex("SeriesId"); b.ToTable("AppUserProgresses"); @@ -373,6 +393,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("IsSpecial") .HasColumnType("INTEGER"); @@ -382,6 +405,9 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); @@ -475,6 +501,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("EmailAddress") .HasColumnType("TEXT"); @@ -484,9 +513,15 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("LastUsed") .HasColumnType("TEXT"); + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); @@ -554,6 +589,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("FolderWatching") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -577,6 +615,9 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("LastScanned") .HasColumnType("TEXT"); @@ -611,6 +652,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("Extension") .HasColumnType("TEXT"); @@ -623,9 +667,15 @@ namespace API.Data.Migrations b.Property("LastFileAnalysis") .HasColumnType("TEXT"); + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("Pages") .HasColumnType("INTEGER"); @@ -797,9 +847,15 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("NormalizedTitle") .HasColumnType("TEXT"); @@ -874,6 +930,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("FolderPath") .HasColumnType("TEXT"); @@ -883,12 +942,21 @@ namespace API.Data.Migrations b.Property("LastChapterAdded") .HasColumnType("TEXT"); + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + b.Property("LastFolderScanned") .HasColumnType("TEXT"); + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("LibraryId") .HasColumnType("INTEGER"); @@ -1004,6 +1072,9 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("FileName") .HasColumnType("TEXT"); @@ -1013,6 +1084,9 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); @@ -1062,9 +1136,15 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); @@ -1333,6 +1413,12 @@ namespace API.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("API.Entities.Series", null) .WithMany("Progress") .HasForeignKey("SeriesId") @@ -1707,6 +1793,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Chapter", b => { b.Navigation("Files"); + + b.Navigation("UserProgress"); }); modelBuilder.Entity("API.Entities.Library", b => diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 172ba8648..af442081e 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.DTOs; using API.Entities; using API.Entities.Enums; +using AutoMapper; +using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; @@ -21,15 +24,18 @@ public interface IAppUserProgressRepository Task GetAnyProgress(); Task> GetUserProgressForSeriesAsync(int seriesId, int userId); Task> GetAllProgress(); + Task GetUserProgressDtoAsync(int chapterId, int userId); } public class AppUserProgressRepository : IAppUserProgressRepository { private readonly DataContext _context; + private readonly IMapper _mapper; - public AppUserProgressRepository(DataContext context) + public AppUserProgressRepository(DataContext context, IMapper mapper) { _context = context; + _mapper = mapper; } public void Update(AppUserProgress userProgress) @@ -114,6 +120,14 @@ public class AppUserProgressRepository : IAppUserProgressRepository return await _context.AppUserProgresses.ToListAsync(); } + public async Task GetUserProgressDtoAsync(int chapterId, int userId) + { + return await _context.AppUserProgresses + .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + } + public async Task GetUserProgressAsync(int chapterId, int userId) { return await _context.AppUserProgresses diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 1c5d9099f..82944fe77 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -18,7 +18,7 @@ public enum ChapterIncludes { None = 1, Volumes = 2, - Files = 4 + Files = 4, } public interface IChapterRepository @@ -28,8 +28,8 @@ public interface IChapterRepository Task GetChapterInfoDtoAsync(int chapterId); Task GetChapterTotalPagesAsync(int chapterId); Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterDtoAsync(int chapterId); - Task GetChapterMetadataDtoAsync(int chapterId); + Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); + Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task> GetFilesForChapterAsync(int chapterId); Task> GetChaptersAsync(int volumeId); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); @@ -37,6 +37,7 @@ public interface IChapterRepository Task> GetAllCoverImagesAsync(); Task> GetAllChaptersWithNonWebPCovers(); Task> GetCoverImagesForLockedChaptersAsync(); + Task AddChapterModifiers(int userId, ChapterDto chapter); } public class ChapterRepository : IChapterRepository { @@ -121,24 +122,24 @@ public class ChapterRepository : IChapterRepository return _context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.Pages) - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); } - public async Task GetChapterDtoAsync(int chapterId) + public async Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter - .Include(c => c.Files) + .Includes(includes) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(c => c.Id == chapterId); + .FirstOrDefaultAsync(c => c.Id == chapterId); return chapter; } - public async Task GetChapterMetadataDtoAsync(int chapterId) + public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter - .Include(c => c.Files) + .Includes(includes) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() @@ -168,14 +169,9 @@ public class ChapterRepository : IChapterRepository /// public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { - var query = _context.Chapter - .AsSplitQuery(); - - if (includes.HasFlag(ChapterIncludes.Files)) query = query.Include(c => c.Files); - if (includes.HasFlag(ChapterIncludes.Volumes)) query = query.Include(c => c.Volume); - - return await query - .SingleOrDefaultAsync(c => c.Id == chapterId); + return await _context.Chapter + .Includes(includes) + .FirstOrDefaultAsync(c => c.Id == chapterId); } /// @@ -247,4 +243,24 @@ public class ChapterRepository : IChapterRepository .AsNoTracking() .ToListAsync(); } + + public async Task AddChapterModifiers(int userId, ChapterDto chapter) + { + var progress = await _context.AppUserProgresses.Where(x => + x.AppUserId == userId && x.ChapterId == chapter.Id) + .AsNoTracking() + .FirstOrDefaultAsync(); + if (progress != null) + { + chapter.PagesRead = progress.PagesRead ; + chapter.LastReadingProgressUtc = progress.LastModifiedUtc; + } + else + { + chapter.PagesRead = 0; + chapter.LastReadingProgressUtc = DateTime.MinValue; + } + + return chapter; + } } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index bae5d9591..5d9f041e2 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -149,6 +149,7 @@ public class ReadingListRepository : IReadingListRepository chapter.ReleaseDate, ReadingListItem = data, ChapterTitleName = chapter.TitleName, + FileSize = chapter.Files.Sum(f => f.Bytes) }) .Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new @@ -158,6 +159,7 @@ public class ReadingListRepository : IReadingListRepository data.ChapterNumber, data.ReleaseDate, data.ChapterTitleName, + data.FileSize, VolumeId = volume.Id, VolumeNumber = volume.Name, }) @@ -174,6 +176,8 @@ public class ReadingListRepository : IReadingListRepository data.VolumeId, data.ReleaseDate, data.ChapterTitleName, + data.FileSize, + LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(), LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single() }) .Select(data => new ReadingListItemDto() @@ -192,7 +196,9 @@ public class ReadingListRepository : IReadingListRepository ReadingListId = data.ReadingListItem.ReadingListId, ReleaseDate = data.ReleaseDate, LibraryType = data.LibraryType, - ChapterTitleName = data.ChapterTitleName + ChapterTitleName = data.ChapterTitleName, + LibraryName = data.LibraryName, + FileSize = data.FileSize }) .Where(o => userLibraries.Contains(o.LibraryId)) .OrderBy(rli => rli.Order) @@ -218,6 +224,7 @@ public class ReadingListRepository : IReadingListRepository if (progressItem == null) continue; progressItem.PagesRead = progress.PagesRead; + progressItem.LastReadingProgressUtc = progress.LastModifiedUtc; } return items; @@ -233,7 +240,7 @@ public class ReadingListRepository : IReadingListRepository public async Task> AddReadingProgressModifiers(int userId, IList items) { - var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList(); + var chapterIds = items.Select(i => i.ChapterId).Distinct(); var userProgress = await _context.AppUserProgresses .Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId)) .AsNoTracking() @@ -241,8 +248,10 @@ public class ReadingListRepository : IReadingListRepository foreach (var item in items) { - var progress = userProgress.Where(p => p.ChapterId == item.ChapterId); - item.PagesRead = progress.Sum(p => p.PagesRead); + var progress = userProgress.Where(p => p.ChapterId == item.ChapterId).ToList(); + if (progress.Count == 0) continue; + item.PagesRead = progress.Sum(p => p.PagesRead); + item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc); } return items; diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index 04a91c95c..c46110dd4 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -218,7 +218,10 @@ public class VolumeRepository : IVolumeRepository { foreach (var c in v.Chapters) { - c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead); + var progresses = userProgress.Where(p => p.ChapterId == c.Id).ToList(); + if (progresses.Count == 0) continue; + c.PagesRead = progresses.Sum(p => p.PagesRead); + c.LastReadingProgressUtc = progresses.Max(p => p.LastModifiedUtc); } v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 50aadf421..a62ac4552 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -51,7 +51,7 @@ public class UnitOfWork : IUnitOfWork public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper); - public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context); + public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context, _mapper); public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper); public IChapterRepository ChapterRepository => new ChapterRepository(_context, _mapper); public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper); diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 8a603ba57..37db2687a 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -11,7 +11,9 @@ namespace API.Entities; public class AppUser : IdentityUser, IHasConcurrencyToken { public DateTime Created { get; set; } = DateTime.Now; + public DateTime CreatedUtc { get; set; } = DateTime.UtcNow; public DateTime LastActive { get; set; } + public DateTime LastActiveUtc { get; set; } public ICollection Libraries { get; set; } public ICollection UserRoles { get; set; } public ICollection Progresses { get; set; } @@ -60,4 +62,10 @@ public class AppUser : IdentityUser, IHasConcurrencyToken RowVersion++; } + public void UpdateLastActive() + { + LastActive = DateTime.Now; + LastActiveUtc = DateTime.UtcNow; + } + } diff --git a/API/Entities/AppUserBookmark.cs b/API/Entities/AppUserBookmark.cs index faaf431b3..f0c8dfaea 100644 --- a/API/Entities/AppUserBookmark.cs +++ b/API/Entities/AppUserBookmark.cs @@ -27,4 +27,6 @@ public class AppUserBookmark : IEntityDate public int AppUserId { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } } diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 4486d73af..3d7ca6ee4 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -47,6 +47,9 @@ public class AppUserProgress : IEntityDate /// public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + // Relationships /// /// Navigational Property for EF. Links to a unique AppUser diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index 311f769dc..7e0821bf4 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -24,6 +24,9 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate public ICollection Files { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + /// /// Relative path to the (managed) image file representing the cover image /// @@ -101,6 +104,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); + public ICollection UserProgress { get; set; } diff --git a/API/Entities/Device.cs b/API/Entities/Device.cs index e4ceabff5..4e7ca32dd 100644 --- a/API/Entities/Device.cs +++ b/API/Entities/Device.cs @@ -41,6 +41,15 @@ public class Device : IEntityDate /// Last time this device was used to send a file /// public DateTime LastUsed { get; set; } + public DateTime LastUsedUtc { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + + public void UpdateLastUsed() + { + LastUsed = DateTime.Now; + LastUsedUtc = DateTime.UtcNow; + } } diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs index fe0e73493..98b6e503e 100644 --- a/API/Entities/FolderPath.cs +++ b/API/Entities/FolderPath.cs @@ -16,4 +16,16 @@ public class FolderPath // Relationship public Library Library { get; set; } public int LibraryId { get; set; } + + public void UpdateLastScanned(DateTime? time) + { + if (time == null) + { + LastScanned = DateTime.Now; + } + else + { + LastScanned = (DateTime) time; + } + } } diff --git a/API/Entities/Interfaces/IEntityDate.cs b/API/Entities/Interfaces/IEntityDate.cs index 11b4e8969..fa08ffdda 100644 --- a/API/Entities/Interfaces/IEntityDate.cs +++ b/API/Entities/Interfaces/IEntityDate.cs @@ -6,4 +6,6 @@ public interface IEntityDate { DateTime Created { get; set; } DateTime LastModified { get; set; } + DateTime CreatedUtc { get; set; } + DateTime LastModifiedUtc { get; set; } } diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 18eb69b0f..4a3369c6d 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -33,6 +33,9 @@ public class Library : IEntityDate public bool ManageCollections { get; set; } = true; public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + /// /// Last time Library was scanned /// @@ -42,4 +45,21 @@ public class Library : IEntityDate public ICollection AppUsers { get; set; } public ICollection Series { get; set; } + public void UpdateLastModified() + { + LastModified = DateTime.Now; + LastModifiedUtc = DateTime.UtcNow; + } + + public void UpdateLastScanned(DateTime? time) + { + if (time == null) + { + LastScanned = DateTime.Now; + } + else + { + LastScanned = (DateTime) time; + } + } } diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index 9377a86a7..888548863 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -36,10 +36,15 @@ public class MangaFile : IEntityDate /// /// This gets updated anytime the file is scanned public DateTime LastModified { get; set; } + + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + /// /// Last time file analysis ran on this file /// public DateTime LastFileAnalysis { get; set; } + public DateTime LastFileAnalysisUtc { get; set; } // Relationship Mapping @@ -53,5 +58,12 @@ public class MangaFile : IEntityDate public void UpdateLastModified() { LastModified = File.GetLastWriteTime(FilePath); + LastModifiedUtc = File.GetLastWriteTimeUtc(FilePath); + } + + public void UpdateLastFileAnalysis() + { + LastFileAnalysis = DateTime.Now; + LastFileAnalysisUtc = DateTime.UtcNow; } } diff --git a/API/Entities/ReadingList.cs b/API/Entities/ReadingList.cs index 6712fe923..0d710728f 100644 --- a/API/Entities/ReadingList.cs +++ b/API/Entities/ReadingList.cs @@ -37,6 +37,8 @@ public class ReadingList : IEntityDate public ICollection Items { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } // Relationships public int AppUserId { get; set; } diff --git a/API/Entities/ReadingListItem.cs b/API/Entities/ReadingListItem.cs index a68042d3d..bba133df7 100644 --- a/API/Entities/ReadingListItem.cs +++ b/API/Entities/ReadingListItem.cs @@ -19,5 +19,4 @@ public class ReadingListItem public Series Series { get; set; } public Volume Volume { get; set; } public Chapter Chapter { get; set; } - } diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 7fa02f67b..91c469fb8 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -41,6 +41,10 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc /// public DateTime LastModified { get; set; } + + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + /// /// Absolute path to the (managed) image file /// @@ -64,6 +68,10 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// public DateTime LastFolderScanned { get; set; } /// + /// Last time the folder was scanned in Utc + /// + public DateTime LastFolderScannedUtc { get; set; } + /// /// The type of all the files attached to this series /// public MangaFormat Format { get; set; } = MangaFormat.Unknown; @@ -76,6 +84,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// When a Chapter was last added onto the Series /// public DateTime LastChapterAdded { get; set; } + public DateTime LastChapterAddedUtc { get; set; } /// /// Total Word count of all chapters in this chapter. @@ -104,4 +113,16 @@ public class Series : IEntityDate, IHasReadTimeEstimate public List Volumes { get; set; } public Library Library { get; set; } public int LibraryId { get; set; } + + public void UpdateLastFolderScanned() + { + LastFolderScanned = DateTime.Now; + LastFolderScannedUtc = DateTime.UtcNow; + } + + public void UpdateLastChapterAdded() + { + LastChapterAdded = DateTime.Now; + LastChapterAddedUtc = DateTime.UtcNow; + } } diff --git a/API/Entities/SiteTheme.cs b/API/Entities/SiteTheme.cs index a4847a7d6..79424014f 100644 --- a/API/Entities/SiteTheme.cs +++ b/API/Entities/SiteTheme.cs @@ -35,4 +35,6 @@ public class SiteTheme : IEntityDate, ITheme public ThemeProvider Provider { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 2caddbb73..36747f1d7 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -19,6 +19,9 @@ public class Volume : IEntityDate, IHasReadTimeEstimate public IList Chapters { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime LastModifiedUtc { get; set; } + /// /// Absolute path to the (managed) image file /// diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/API/Extensions/ClaimsPrincipalExtensions.cs index f351aea42..07d94b23f 100644 --- a/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/API/Extensions/ClaimsPrincipalExtensions.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Kavita.Common; +using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace API.Extensions; @@ -7,8 +8,15 @@ public static class ClaimsPrincipalExtensions { public static string GetUsername(this ClaimsPrincipal user) { - var userClaim = user.FindFirst(ClaimTypes.NameIdentifier); + var userClaim = user.FindFirst(JwtRegisteredClaimNames.Name); if (userClaim == null) throw new KavitaException("User is not authenticated"); return userClaim.Value; } + + public static int GetUserId(this ClaimsPrincipal user) + { + var userClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userClaim == null) throw new KavitaException("User is not authenticated"); + return int.Parse(userClaim.Value); + } } diff --git a/API/Extensions/QueryableExtensions.cs b/API/Extensions/QueryableExtensions.cs index efae46cd2..6cd6cb532 100644 --- a/API/Extensions/QueryableExtensions.cs +++ b/API/Extensions/QueryableExtensions.cs @@ -133,6 +133,13 @@ public static class QueryableExtensions queryable = queryable.Include(v => v.Volume); } + if (includes.HasFlag(ChapterIncludes.Files)) + { + queryable = queryable + .Include(c => c.Files); + } + + return queryable.AsSplitQuery(); } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index bef39e90d..3fd23b709 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -34,6 +34,11 @@ public class AutoMapperProfiles : Profile CreateMap(); CreateMap(); + CreateMap() + .ForMember(dest => dest.PageNum, + opt => + opt.MapFrom( + src => src.PagesRead)); CreateMap() .ForMember(dest => dest.Writers, opt => diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 6a8d8676f..751f9a303 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -278,7 +278,7 @@ public class ArchiveService : IArchiveService /// public string CreateZipForDownload(IEnumerable files, string tempFolder) { - var dateString = DateTime.Now.ToShortDateString().Replace("/", "_"); + var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); var potentialExistingFile = _directoryService.FileSystem.FileInfo.FromFileName(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs index dad63ad27..ffab5a858 100644 --- a/API/Services/DeviceService.cs +++ b/API/Services/DeviceService.cs @@ -114,7 +114,7 @@ public class DeviceService : IDeviceService throw new KavitaException("Cannot Send non Epub or Pdf to devices as not supported on Kindle"); - device.LastUsed = DateTime.Now; + device.UpdateLastUsed(); _unitOfWork.DeviceRepository.Update(device); await _unitOfWork.CommitAsync(); var success = await _emailService.SendFilesToEmail(new SendToDto() diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 3eabe5b78..7ca9523e4 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -231,6 +231,7 @@ public class ReaderService : IReaderService var userProgress = await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId); + if (userProgress == null) { // Create a user object diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 1dffcd1ac..42cb77049 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -436,7 +436,7 @@ public class SeriesService : ISeriesService var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds); foreach (var library in libraries) { - library.LastModified = DateTime.Now; + library.UpdateLastModified(); _unitOfWork.LibraryRepository.Update(library); } diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index c62e49ce2..134a82f90 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -92,7 +92,7 @@ public class BackupService : IBackupService await SendProgress(0F, "Started backup"); await SendProgress(0.1F, "Copying core files"); - var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_"); + var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace("/", "_").Replace(":", "_"); var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip"); if (File.Exists(zipPath)) diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index 65f4d427e..24075abf3 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -233,7 +233,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService private void UpdateFileAnalysis(MangaFile file) { - file.LastFileAnalysis = DateTime.Now; + file.UpdateLastFileAnalysis(); _unitOfWork.MangaFileRepository.Update(file); } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 527cc89ec..e568a4f5b 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -164,7 +164,7 @@ public class ProcessSeries : IProcessSeries // Update series FolderPath here await UpdateSeriesFolderPath(parsedInfos, library, series); - series.LastFolderScanned = DateTime.Now; + series.UpdateLastFolderScanned(); if (_unitOfWork.HasChanges()) { @@ -562,7 +562,7 @@ public class ProcessSeries : IProcessSeries "[ScannerService] Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters); chapter = DbFactory.Chapter(info); volume.Chapters.Add(chapter); - series.LastChapterAdded = DateTime.Now; + series.UpdateLastChapterAdded(); } else { diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 5eedb6734..26962183d 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -528,13 +528,13 @@ public class ScannerService : IScannerService _logger.LogInformation("[ScannerService] Finished file scan in {ScanAndUpdateTime} milliseconds. Updating database", scanElapsedTime); - var time = DateTime.Now; + var time = DateTime.UtcNow; foreach (var folderPath in library.Folders) { - folderPath.LastScanned = time; + folderPath.UpdateLastScanned(time); } - library.LastScanned = time; + library.UpdateLastScanned(time); _unitOfWork.LibraryRepository.Update(library); diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 927b15907..923c3b1d7 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -10,6 +10,7 @@ using API.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; +using static System.Security.Claims.ClaimTypes; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; @@ -22,6 +23,7 @@ public interface ITokenService Task CreateRefreshToken(AppUser user); } + public class TokenService : ITokenService { private readonly UserManager _userManager; @@ -38,19 +40,20 @@ public class TokenService : ITokenService { var claims = new List { - new Claim(JwtRegisteredClaimNames.NameId, user.UserName) + new Claim(JwtRegisteredClaimNames.Name, user.UserName), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), }; var roles = await _userManager.GetRolesAsync(user); - claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + claims.AddRange(roles.Select(role => new Claim(Role, role))); var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); var tokenDescriptor = new SecurityTokenDescriptor() { Subject = new ClaimsIdentity(claims), - Expires = DateTime.Now.AddDays(14), + Expires = DateTime.UtcNow.AddDays(14), SigningCredentials = creds }; diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index 5cf847c6e..0760cff8a 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -57,7 +57,7 @@ public class PresenceTracker : IPresenceTracker } // Update the last active for the user - user.LastActive = DateTime.Now; + user.UpdateLastActive(); await _unitOfWork.CommitAsync(); } diff --git a/API/Startup.cs b/API/Startup.cs index 6989b13c9..785decd89 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -225,6 +225,7 @@ public class Startup // v0.6.8 or v0.7 await MigrateUserProgressLibraryId.Migrate(unitOfWork, logger); + await MigrateToUtcDates.Migrate(unitOfWork, dataContext, logger); // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); diff --git a/UI/Web/src/app/_models/reading-list.ts b/UI/Web/src/app/_models/reading-list.ts index 9e21f90d9..10903da0e 100644 --- a/UI/Web/src/app/_models/reading-list.ts +++ b/UI/Web/src/app/_models/reading-list.ts @@ -16,6 +16,7 @@ export interface ReadingListItem { releaseDate: string; title: string; libraryType: LibraryType; + libraryName: string; } export interface ReadingList { diff --git a/openapi.json b/openapi.json index 1eb30f82b..8fe95ff3b 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.6.1.34" + "version": "0.6.1.37" }, "servers": [ { @@ -4048,17 +4048,17 @@ "content": { "text/plain": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "type": "boolean" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "type": "boolean" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "type": "boolean" } } } @@ -6152,17 +6152,17 @@ "content": { "text/plain": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "$ref": "#/components/schemas/ChapterMetadataDto" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "$ref": "#/components/schemas/ChapterMetadataDto" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/ChapterDto" + "$ref": "#/components/schemas/ChapterMetadataDto" } } } @@ -9339,10 +9339,18 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, "lastActive": { "type": "string", "format": "date-time" }, + "lastActiveUtc": { + "type": "string", + "format": "date-time" + }, "libraries": { "type": "array", "items": { @@ -9470,6 +9478,14 @@ "lastModified": { "type": "string", "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" } }, "additionalProperties": false, @@ -9633,6 +9649,14 @@ "description": "Last date this was updated", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "appUser": { "$ref": "#/components/schemas/AppUser" }, @@ -9888,6 +9912,14 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "coverImage": { "type": "string", "description": "Relative path to the (managed) image file representing the cover image", @@ -9988,6 +10020,13 @@ }, "nullable": true }, + "userProgress": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AppUserProgress" + }, + "nullable": true + }, "volume": { "$ref": "#/components/schemas/Volume" }, @@ -10042,6 +10081,11 @@ "description": "Calculated at API time. Number of pages read for this Chapter for logged in user.", "format": "int32" }, + "lastReadingProgressUtc": { + "type": "string", + "description": "The last time a chapter was read by current authenticated user", + "format": "date-time" + }, "coverImageLocked": { "type": "boolean", "description": "If the Cover Image is locked for this entity" @@ -10056,6 +10100,18 @@ "description": "When chapter was created", "format": "date-time" }, + "lastModified": { + "type": "string", + "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "releaseDate": { "type": "string", "description": "When the chapter was released.", @@ -10189,6 +10245,145 @@ "additionalProperties": false, "description": "Information about the Chapter for the Reader to render" }, + "ChapterMetadataDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "chapterId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "writers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "coverArtists": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "publishers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "characters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "pencillers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "inkers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "colorists": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "letterers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "editors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "translators": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + }, + "nullable": true + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GenreTagDto" + }, + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDto" + }, + "description": "Collection of all Tags from underlying chapters for a Series", + "nullable": true + }, + "ageRating": { + "$ref": "#/components/schemas/AgeRating" + }, + "releaseDate": { + "type": "string", + "nullable": true + }, + "publicationStatus": { + "$ref": "#/components/schemas/PublicationStatus" + }, + "summary": { + "type": "string", + "description": "Summary for the Chapter/Issue", + "nullable": true + }, + "language": { + "type": "string", + "description": "Language for the Chapter/Issue", + "nullable": true + }, + "count": { + "type": "integer", + "description": "Number in the TotalCount of issues", + "format": "int32" + }, + "totalCount": { + "type": "integer", + "description": "Total number of issues for the series", + "format": "int32" + }, + "wordCount": { + "type": "integer", + "description": "Number of Words for this chapter. Only applies to Epub", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "Exclusively metadata about a given chapter" + }, "CollectionTag": { "type": "object", "properties": { @@ -10533,6 +10728,10 @@ "description": "Last time this device was used to send a file", "format": "date-time" }, + "lastUsedUtc": { + "type": "string", + "format": "date-time" + }, "created": { "type": "string", "format": "date-time" @@ -10540,6 +10739,14 @@ "lastModified": { "type": "string", "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" } }, "additionalProperties": false, @@ -10571,6 +10778,11 @@ "type": "string", "description": "Last time this device was used to send a file", "format": "date-time" + }, + "lastUsedUtc": { + "type": "string", + "description": "Last time this device was used to send a file", + "format": "date-time" } }, "additionalProperties": false, @@ -11075,6 +11287,18 @@ "format": "date-time", "nullable": true }, + "createdAtUtc": { + "type": "string", + "description": "When the job was created", + "format": "date-time", + "nullable": true + }, + "lastExecutionUtc": { + "type": "string", + "description": "Last time the job was run", + "format": "date-time", + "nullable": true + }, "cron": { "type": "string", "nullable": true @@ -11173,6 +11397,14 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "lastScanned": { "type": "string", "description": "Last time Library was scanned", @@ -11331,11 +11563,23 @@ "description": "Last time underlying file was modified", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "lastFileAnalysis": { "type": "string", "description": "Last time file analysis ran on this file", "format": "date-time" }, + "lastFileAnalysisUtc": { + "type": "string", + "format": "date-time" + }, "chapter": { "$ref": "#/components/schemas/Chapter" }, @@ -11652,13 +11896,8 @@ }, "bookScrollId": { "type": "string", - "description": "For Book reader, this can be an optional string of the id of a part marker, to help resume reading position\r\non pages that combine multiple \"chapters\".", + "description": "For EPUB reader, this can be an optional string of the id of a part marker, to help resume reading position\r\non pages that combine multiple \"chapters\".", "nullable": true - }, - "lastModified": { - "type": "string", - "description": "Last time the progress was synced from UI or external app", - "format": "date-time" } }, "additionalProperties": false @@ -11809,6 +12048,14 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "appUserId": { "type": "integer", "format": "int32" @@ -11950,6 +12197,10 @@ "libraryType": { "$ref": "#/components/schemas/LibraryType" }, + "libraryName": { + "type": "string", + "nullable": true + }, "title": { "type": "string", "nullable": true @@ -11963,6 +12214,16 @@ "type": "integer", "description": "Used internally only", "format": "int32" + }, + "lastReadingProgressUtc": { + "type": "string", + "description": "The last time a reading list item (underlying chapter) was read by current authenticated user", + "format": "date-time" + }, + "fileSize": { + "type": "integer", + "description": "File size of underlying item", + "format": "int64" } }, "additionalProperties": false @@ -12418,6 +12679,14 @@ "description": "Whenever a modification occurs. Ie) New volumes, removed volumes, title update, etc", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "coverImage": { "type": "string", "description": "Absolute path to the (managed) image file", @@ -12442,6 +12711,11 @@ "description": "Last time the folder was scanned", "format": "date-time" }, + "lastFolderScannedUtc": { + "type": "string", + "description": "Last time the folder was scanned in Utc", + "format": "date-time" + }, "format": { "$ref": "#/components/schemas/MangaFormat" }, @@ -12459,6 +12733,10 @@ "description": "When a Chapter was last added onto the Series", "format": "date-time" }, + "lastChapterAddedUtc": { + "type": "string", + "format": "date-time" + }, "wordCount": { "type": "integer", "description": "Total Word count of all chapters in this chapter.", @@ -13473,6 +13751,14 @@ "lastModified": { "type": "string", "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" } }, "additionalProperties": false, @@ -13510,6 +13796,14 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "selector": { "type": "string", "nullable": true, @@ -14488,6 +14782,14 @@ "type": "string", "format": "date-time" }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, "coverImage": { "type": "string", "description": "Absolute path to the (managed) image file",