diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 050882052..dbfe1129d 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -1203,10 +1203,67 @@ public class ReadingListServiceTests #region CreateReadingListsFromSeries + private async Task> SetupData() + { + // Setup 2 series, only do this once tho + if (await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary("Series 1", 1, MangaFormat.Archive)) + { + return new Tuple(await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(1), + await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(2)); + } + + var library = + await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Series | LibraryIncludes.AppUser); + var user = new AppUserBuilder("majora2007", "majora2007@fake.com").Build(); + library!.AppUsers.Add(user); + library.ManageReadingLists = true; + + // Setup the series for CreateReadingListsFromSeries + var series1 = new SeriesBuilder("Series 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithStoryArc("CreateReadingListsFromSeries") + .WithStoryArcNumber("1") + .Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .Build(); + + var series2 = new SeriesBuilder("Series 2") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .Build(); + + library!.Series.Add(series1); + library!.Series.Add(series2); + + await _unitOfWork.CommitAsync(); + + return new Tuple(series1, series2); + } + // [Fact] // public async Task CreateReadingListsFromSeries_ShouldCreateFromSinglePair() // { + // //await SetupData(); // + // var series1 = new SeriesBuilder("Series 1") + // .WithFormat(MangaFormat.Archive) + // .WithVolume(new VolumeBuilder("1") + // .WithChapter(new ChapterBuilder("1") + // .WithStoryArc("CreateReadingListsFromSeries") + // .WithStoryArcNumber("1") + // .Build()) + // .WithChapter(new ChapterBuilder("2").Build()) + // .Build()) + // .Build(); + // + // _readingListService.CreateReadingListsFromSeries(series.Item1) // } #endregion diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 0f6b5dc99..8e58805d8 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -228,7 +228,7 @@ public class UserRepository : IUserRepository public async Task GetDefaultAdminUser() { return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)) - .OrderByDescending(u => u.Created) + .OrderBy(u => u.Created) .First(); } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 90eadba5d..188ed9692 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -13,7 +13,7 @@ public class SeriesMetadata : IHasConcurrencyToken public string Summary { get; set; } = string.Empty; - public ICollection CollectionTags { get; set; } = null!; + public ICollection CollectionTags { get; set; } = new List(); public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index 69e0f5eef..35b115998 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -17,7 +17,7 @@ public class ChapterBuilder : IEntityBuilder { Range = string.IsNullOrEmpty(range) ? number : range, Title = string.IsNullOrEmpty(range) ? number : range, - Number = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(number) + string.Empty, + Number = Parser.MinNumberFromRange(number) + string.Empty, Files = new List(), Pages = 1 }; @@ -42,12 +42,24 @@ public class ChapterBuilder : IEntityBuilder return this; } - private ChapterBuilder WithNumber(string number) + public ChapterBuilder WithNumber(string number) { _chapter.Number = number; return this; } + public ChapterBuilder WithStoryArc(string arc) + { + _chapter.StoryArc = arc; + return this; + } + + public ChapterBuilder WithStoryArcNumber(string number) + { + _chapter.StoryArcNumber = number; + return this; + } + private ChapterBuilder WithRange(string range) { _chapter.Range = range; diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index 1689340a3..ff73c6a74 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -103,6 +103,7 @@ public static class PersonHelper if (string.IsNullOrEmpty(person.Name)) return; var existingPerson = metadataPeople.FirstOrDefault(p => p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role); + if (existingPerson == null) { metadataPeople.Add(person); diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index c3d1bb57a..8c99da39f 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -13,9 +13,11 @@ using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Builders; +using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Kavita.Common; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; namespace API.Services; @@ -447,7 +449,7 @@ public class ReadingListService : IReadingListService series.Metadata ??= new SeriesMetadataBuilder().Build(); foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) { - List> pairs = new List>(); + var pairs = new List>(); if (!string.IsNullOrEmpty(chapter.StoryArc)) { pairs.AddRange(GeneratePairs(chapter.Files.FirstOrDefault()!.FilePath, chapter.StoryArc, chapter.StoryArcNumber)); @@ -459,7 +461,6 @@ public class ReadingListService : IReadingListService foreach (var arcPair in pairs) { - var order = int.Parse(arcPair.Item2); var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id); if (readingList == null) { @@ -471,19 +472,36 @@ public class ReadingListService : IReadingListService } var items = readingList.Items.ToList(); - var readingListItem = items.FirstOrDefault(item => item.Order == order); + var order = int.Parse(arcPair.Item2); + var readingListItem = items.FirstOrDefault(item => item.Order == order || item.ChapterId == chapter.Id); if (readingListItem == null) { + // If no number was provided in the reading list, we default to MaxValue and hence we should insert the item at the end of the list + if (order == int.MaxValue) + { + order = items.Count > 0 ? items.Max(item => item.Order) + 1 : 0; + } items.Add(new ReadingListItemBuilder(order, series.Id, chapter.VolumeId, chapter.Id).Build()); } else { - ReorderItems(items, readingListItem.Id, order); + if (order == int.MaxValue) + { + _logger.LogWarning("{Filename} has a missing StoryArcNumber/AlternativeNumber but list already exists with this item. Skipping item", chapter.Files.FirstOrDefault()?.FilePath); + } + else + { + ReorderItems(items, readingListItem.Id, order); + } } readingList.Items = items; await CalculateReadingListAgeRating(readingList); - await _unitOfWork.CommitAsync(); + await CalculateStartAndEndDates(readingList); + if (_unitOfWork.HasChanges()) + { + await _unitOfWork.CommitAsync(); + } } } } @@ -495,14 +513,19 @@ public class ReadingListService : IReadingListService var arcs = storyArc.Split(","); var arcNumbers = storyArcNumbers.Split(","); - if (arcNumbers.Length != arcs.Length) + if (arcNumbers.Count(s => !string.IsNullOrEmpty(s)) != arcs.Length) { - _logger.LogError("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename); + _logger.LogWarning("There is a mismatch on StoryArc and StoryArcNumber for {FileName}. Def", filename); } var maxPairs = Math.Min(arcs.Length, arcNumbers.Length); for (var i = 0; i < maxPairs; i++) { + // When there is a mismatch on arcs and arc numbers, then we should default to a high number + if (string.IsNullOrEmpty(arcNumbers[i]) && !string.IsNullOrEmpty(arcs[i])) + { + arcNumbers[i] = int.MaxValue.ToString(); + } if (string.IsNullOrEmpty(arcs[i]) || !int.TryParse(arcNumbers[i], out _)) continue; data.Add(new Tuple(arcs[i], arcNumbers[i])); } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 785bc2960..d70dea58f 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -196,8 +196,9 @@ public class ProcessSeries : IProcessSeries { await _unitOfWork.RollbackAsync(); _logger.LogCritical(ex, - "[ScannerService] There was an issue writing to the database for series {@SeriesName}", + "[ScannerService] There was an issue writing to the database for series {SeriesName}", series.Name); + _logger.LogTrace("[ScannerService] Full Series Dump: {@Series}", series); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series}", @@ -222,7 +223,7 @@ public class ProcessSeries : IProcessSeries _logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name); } - await _metadataService.GenerateCoversForSeries(series, false); + await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP); EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id); } @@ -726,7 +727,18 @@ public class ProcessSeries : IProcessSeries void AddPerson(Person person) { - PersonHelper.AddPersonIfNotExists(chapter.People, person); + // TODO: Temp have code inlined to help debug foreign key constraint issue + //PersonHelper.AddPersonIfNotExists(chapter.People, person); + if (string.IsNullOrEmpty(person.Name)) return; + var existingPerson = chapter.People.FirstOrDefault(p => + p.NormalizedName == person.Name.ToNormalized() && p.Role == person.Role); + _logger.LogTrace("[PersonHelper] Attempting to add {@Person} to {FileName} with ChapterID {ChapterId}, adding if not null: {@ExistingPerson}", + person, chapter.Files.FirstOrDefault()?.FilePath, chapter.Id, existingPerson); + + if (existingPerson == null) + { + chapter.People.Add(person); + } } void AddGenre(Genre genre, bool newTag) @@ -823,15 +835,19 @@ public class ProcessSeries : IProcessSeries lock (_peopleLock) { var allPeopleTypeRole = _people.Where(p => p.Role == role).ToList(); + _logger.LogTrace("[UpdatePeople] for {Role} and Names of {Names}", role, names); + _logger.LogTrace("[UpdatePeople] for {Role} found {@People}", role, allPeopleTypeRole.Select(p => new {p.Id, p.Name, SeriesMetadataIds = p.SeriesMetadatas?.Select(m => m.Id).ToList()})); foreach (var name in names) { var normalizedName = name.ToNormalized(); var person = allPeopleTypeRole.FirstOrDefault(p => p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); + if (person == null) { person = new PersonBuilder(name, role).Build(); + _logger.LogTrace("[UpdatePeople] for {Role} no one found, adding to _people", role); _people.Add(person); } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index f9eca3991..492107829 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -531,7 +531,7 @@ public class ScannerService : IScannerService _logger.LogInformation("[ScannerService] Finished file scan in {ScanAndUpdateTime} milliseconds. Updating database", scanElapsedTime); - var time = DateTime.UtcNow; + var time = DateTime.Now; foreach (var folderPath in library.Folders) { folderPath.UpdateLastScanned(time); diff --git a/UI/Web/src/app/pipe/time-ago.pipe.ts b/UI/Web/src/app/pipe/time-ago.pipe.ts index 2bbb5abe6..d44a483e8 100644 --- a/UI/Web/src/app/pipe/time-ago.pipe.ts +++ b/UI/Web/src/app/pipe/time-ago.pipe.ts @@ -33,11 +33,12 @@ and modified }) export class TimeAgoPipe implements PipeTransform, OnDestroy { - private timer: number | null = null; + private timer: number | null = null; constructor(private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone) {} transform(value: string) { this.removeTimer(); const d = new Date(value); + console.log('date: ', d); const now = new Date(); const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000)); const timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) * 1000; @@ -61,37 +62,37 @@ export class TimeAgoPipe implements PipeTransform, OnDestroy { return ''; } - if (seconds <= 45) { - return 'just now'; - } - if (seconds <= 90) { - return 'a minute ago'; - } - if (minutes <= 45) { - return minutes + ' minutes ago'; - } - if (minutes <= 90) { - return 'an hour ago'; - } - if (hours <= 22) { - return hours + ' hours ago'; - } - if (hours <= 36) { - return 'a day ago'; - } - if (days <= 25) { - return days + ' days ago'; - } - if (days <= 45) { - return 'a month ago'; - } - if (days <= 345) { - return months + ' months ago'; - } - if (days <= 545) { - return 'a year ago'; - } - return years + ' years ago'; + if (seconds <= 45) { + return 'just now'; + } + if (seconds <= 90) { + return 'a minute ago'; + } + if (minutes <= 45) { + return minutes + ' minutes ago'; + } + if (minutes <= 90) { + return 'an hour ago'; + } + if (hours <= 22) { + return hours + ' hours ago'; + } + if (hours <= 36) { + return 'a day ago'; + } + if (days <= 25) { + return days + ' days ago'; + } + if (days <= 45) { + return 'a month ago'; + } + if (days <= 345) { + return months + ' months ago'; + } + if (days <= 545) { + return 'a year ago'; + } + return years + ' years ago'; } ngOnDestroy(): void { diff --git a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.html b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.html index abfa81ef3..d77d722a8 100644 --- a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.html +++ b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.html @@ -58,7 +58,7 @@ aria-describedby="starting-year-header">
- Must be between 1 and 12 or blank + Must be greater than 1000, 0 or blank
@@ -84,7 +84,7 @@ aria-describedby="ending-year-header">
- Must be between 1 and 12 or blank + Must be greater than 1000, 0 or blank
@@ -109,7 +109,7 @@ diff --git a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts index 0c871bd17..ee625c2ed 100644 --- a/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts +++ b/UI/Web/src/app/reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component.ts @@ -116,6 +116,8 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy { updateSelectedIndex(index: number) { this.coverImageIndex = index; + console.log(this.coverImageIndex) + this.cdRef.detectChanges(); } updateSelectedImage(url: string) { diff --git a/UI/Web/src/app/registration/user-login/user-login.component.html b/UI/Web/src/app/registration/user-login/user-login.component.html index ffbd27bae..f8d2f85f5 100644 --- a/UI/Web/src/app/registration/user-login/user-login.component.html +++ b/UI/Web/src/app/registration/user-login/user-login.component.html @@ -12,7 +12,7 @@
+ id="password" type="password" pattern="^.{6,32}$" placeholder="Password">
Password must be between 6 and 32 characters in length