diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index feaed4fe05..6aefef8304 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -29,7 +29,6 @@
-
diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs
index 771ba940ce..d902ae3532 100644
--- a/API.Tests/Extensions/QueryableExtensionsTests.cs
+++ b/API.Tests/Extensions/QueryableExtensionsTests.cs
@@ -126,28 +126,45 @@ public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(
[InlineData(false, 1)]
public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
{
- var items = new List()
+ // Arrange
+ var items = new List
{
- new PersonBuilder("Test", PersonRole.Character)
- .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
- .Build(),
- new PersonBuilder("Test", PersonRole.Character)
- .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
- .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
- .Build(),
- new PersonBuilder("Test", PersonRole.Character)
- .WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
- .Build(),
+ CreatePersonWithSeriesMetadata("Test1", AgeRating.Teen),
+ CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, AgeRating.Teen),
+ CreatePersonWithSeriesMetadata("Test3", AgeRating.X18Plus)
};
- var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
+ var ageRestriction = new AgeRestriction
{
AgeRating = AgeRating.Teen,
IncludeUnknowns = includeUnknowns
- });
+ };
+
+ // Act
+ var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(ageRestriction);
+
+ // Assert
Assert.Equal(expectedCount, filtered.Count());
}
+ private static Person CreatePersonWithSeriesMetadata(string name, params AgeRating[] ageRatings)
+ {
+ var person = new PersonBuilder(name).Build();
+
+ foreach (var ageRating in ageRatings)
+ {
+ var seriesMetadata = new SeriesMetadataBuilder().WithAgeRating(ageRating).Build();
+ person.SeriesMetadataPeople.Add(new SeriesMetadataPeople
+ {
+ SeriesMetadata = seriesMetadata,
+ Person = person,
+ Role = PersonRole.Character // Role is now part of the relationship
+ });
+ }
+
+ return person;
+ }
+
[Theory]
[InlineData(true, 2)]
[InlineData(false, 1)]
diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs
index 38e5f00013..adaecfba52 100644
--- a/API.Tests/Extensions/SeriesExtensionsTests.cs
+++ b/API.Tests/Extensions/SeriesExtensionsTests.cs
@@ -185,6 +185,35 @@ public void GetCoverImage_JustVolumes()
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
}
+ [Fact]
+ public void GetCoverImage_JustVolumes_ButVolume0()
+ {
+ var series = new SeriesBuilder("Test 1")
+ .WithFormat(MangaFormat.Archive)
+
+ .WithVolume(new VolumeBuilder("0")
+ .WithName("Volume 0")
+ .WithChapter(new ChapterBuilder(Parser.DefaultChapter)
+ .WithCoverImage("Volume 0")
+ .Build())
+ .Build())
+
+ .WithVolume(new VolumeBuilder("1")
+ .WithName("Volume 1")
+ .WithChapter(new ChapterBuilder(Parser.DefaultChapter)
+ .WithCoverImage("Volume 1")
+ .Build())
+ .Build())
+ .Build();
+
+ foreach (var vol in series.Volumes)
+ {
+ vol.CoverImage = vol.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
+ }
+
+ Assert.Equal("Volume 1", series.GetCoverImage());
+ }
+
[Fact]
public void GetCoverImage_JustSpecials_WithDecimal()
{
diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs
index ed59a958ff..cf11f0f1f5 100644
--- a/API.Tests/Helpers/PersonHelperTests.cs
+++ b/API.Tests/Helpers/PersonHelperTests.cs
@@ -13,403 +13,403 @@ namespace API.Tests.Helpers;
public class PersonHelperTests
{
- #region UpdatePeople
- [Fact]
- public void UpdatePeople_ShouldAddNewPeople()
- {
- var allPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- };
- var peopleAdded = new List();
-
- PersonHelper.UpdatePeople(allPeople, new[] {"Joseph Shmo", "Sally Ann"}, PersonRole.Writer, person =>
- {
- peopleAdded.Add(person);
- });
-
- Assert.Equal(2, peopleAdded.Count);
- Assert.Equal(4, allPeople.Count);
- }
-
- [Fact]
- public void UpdatePeople_ShouldNotAddDuplicatePeople()
- {
- var allPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- new PersonBuilder("Sally Ann", PersonRole.CoverArtist).Build(),
-
- };
- var peopleAdded = new List();
-
- PersonHelper.UpdatePeople(allPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.CoverArtist, person =>
- {
- peopleAdded.Add(person);
- });
-
- Assert.Equal(3, allPeople.Count);
- }
- #endregion
-
- #region UpdatePeopleList
-
- [Fact]
- public void UpdatePeopleList_NullTags_NoChanges()
- {
- // Arrange
- ICollection tags = null;
- var series = new SeriesBuilder("Test Series").Build();
- var allTags = new List();
- var handleAddCalled = false;
- var onModifiedCalled = false;
-
- // Act
- PersonHelper.UpdatePeopleList(PersonRole.Writer, tags, series, allTags, p => handleAddCalled = true, () => onModifiedCalled = true);
-
- // Assert
- Assert.False(handleAddCalled);
- Assert.False(onModifiedCalled);
- }
-
- [Fact]
- public void UpdatePeopleList_AddNewTag_TagAddedAndOnModifiedCalled()
- {
- // Arrange
- const PersonRole role = PersonRole.Writer;
- var tags = new List
- {
- new PersonDto { Id = 1, Name = "John Doe", Role = role }
- };
- var series = new SeriesBuilder("Test Series").Build();
- var allTags = new List();
- var handleAddCalled = false;
- var onModifiedCalled = false;
-
- // Act
- PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
- {
- handleAddCalled = true;
- series.Metadata.People.Add(p);
- }, () => onModifiedCalled = true);
-
- // Assert
- Assert.True(handleAddCalled);
- Assert.True(onModifiedCalled);
- Assert.Single(series.Metadata.People);
- Assert.Equal("John Doe", series.Metadata.People.First().Name);
- }
-
- [Fact]
- public void UpdatePeopleList_RemoveExistingTag_TagRemovedAndOnModifiedCalled()
- {
- // Arrange
- const PersonRole role = PersonRole.Writer;
- var tags = new List();
- var series = new SeriesBuilder("Test Series").Build();
- var person = new PersonBuilder("John Doe", role).Build();
- person.Id = 1;
- series.Metadata.People.Add(person);
- var allTags = new List
- {
- person
- };
- var handleAddCalled = false;
- var onModifiedCalled = false;
-
- // Act
- PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
- {
- handleAddCalled = true;
- series.Metadata.People.Add(p);
- }, () => onModifiedCalled = true);
-
- // Assert
- Assert.False(handleAddCalled);
- Assert.True(onModifiedCalled);
- Assert.Empty(series.Metadata.People);
- }
-
- [Fact]
- public void UpdatePeopleList_UpdateExistingTag_OnModifiedCalled()
- {
- // Arrange
- const PersonRole role = PersonRole.Writer;
- var tags = new List
- {
- new PersonDto { Id = 1, Name = "John Doe", Role = role }
- };
- var series = new SeriesBuilder("Test Series").Build();
- var person = new PersonBuilder("John Doe", role).Build();
- person.Id = 1;
- series.Metadata.People.Add(person);
- var allTags = new List
- {
- person
- };
- var handleAddCalled = false;
- var onModifiedCalled = false;
-
- // Act
- PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
- {
- handleAddCalled = true;
- series.Metadata.People.Add(p);
- }, () => onModifiedCalled = true);
-
- // Assert
- Assert.False(handleAddCalled);
- Assert.False(onModifiedCalled);
- Assert.Single(series.Metadata.People);
- Assert.Equal("John Doe", series.Metadata.People.First().Name);
- }
-
- [Fact]
- public void UpdatePeopleList_NoChanges_HandleAddAndOnModifiedNotCalled()
- {
- // Arrange
- const PersonRole role = PersonRole.Writer;
- var tags = new List
- {
- new PersonDto { Id = 1, Name = "John Doe", Role = role }
- };
- var series = new SeriesBuilder("Test Series").Build();
- var person = new PersonBuilder("John Doe", role).Build();
- person.Id = 1;
- series.Metadata.People.Add(person);
- var allTags = new List
- {
- new PersonBuilder("John Doe", role).Build()
- };
- var handleAddCalled = false;
- var onModifiedCalled = false;
-
- // Act
- PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
- {
- handleAddCalled = true;
- series.Metadata.People.Add(p);
- }, () => onModifiedCalled = true);
-
- // Assert
- Assert.False(handleAddCalled);
- Assert.False(onModifiedCalled);
- Assert.Single(series.Metadata.People);
- Assert.Equal("John Doe", series.Metadata.People.First().Name);
- }
-
-
-
- #endregion
-
- #region RemovePeople
- [Fact]
- public void RemovePeople_ShouldRemovePeopleOfSameRole()
- {
- var existingPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- };
- var peopleRemoved = new List();
- PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
- {
- peopleRemoved.Add(person);
- });
-
- Assert.NotEqual(existingPeople, peopleRemoved);
- Assert.Single(peopleRemoved);
- }
-
- [Fact]
- public void RemovePeople_ShouldRemovePeopleFromBothRoles()
- {
- var existingPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- };
- var peopleRemoved = new List();
- PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
- {
- peopleRemoved.Add(person);
- });
-
- Assert.NotEqual(existingPeople, peopleRemoved);
- Assert.Single(peopleRemoved);
-
- PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo"}, PersonRole.CoverArtist, person =>
- {
- peopleRemoved.Add(person);
- });
-
- Assert.Empty(existingPeople);
- Assert.Equal(2, peopleRemoved.Count);
- }
-
- [Fact]
- public void RemovePeople_ShouldRemovePeopleOfSameRole_WhenNothingPassed()
- {
- var existingPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- };
- var peopleRemoved = new List();
- PersonHelper.RemovePeople(existingPeople, new List(), PersonRole.Writer, person =>
- {
- peopleRemoved.Add(person);
- });
-
- Assert.NotEqual(existingPeople, peopleRemoved);
- Assert.Equal(2, peopleRemoved.Count);
- }
-
-
- #endregion
-
- #region KeepOnlySamePeopleBetweenLists
- [Fact]
- public void KeepOnlySamePeopleBetweenLists()
- {
- var existingPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- new PersonBuilder("Sally", PersonRole.Writer).Build(),
- };
-
- var peopleFromChapters = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- };
-
- var peopleRemoved = new List();
- PersonHelper.KeepOnlySamePeopleBetweenLists(existingPeople,
- peopleFromChapters, person =>
- {
- peopleRemoved.Add(person);
- });
-
- Assert.Equal(2, peopleRemoved.Count);
- }
- #endregion
-
- #region AddPeople
-
- [Fact]
- public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonDoesNotExist()
- {
- // Arrange
- var metadataPeople = new List();
- var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
-
- // Act
- PersonHelper.AddPersonIfNotExists(metadataPeople, person);
-
- // Assert
- Assert.Single(metadataPeople);
- Assert.Contains(person, metadataPeople);
- }
-
- [Fact]
- public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonAlreadyExists()
- {
- // Arrange
- var metadataPeople = new List
- {
- new PersonBuilder("John Smith", PersonRole.Character)
- .WithId(1)
- .Build()
- };
- var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
- // Act
- PersonHelper.AddPersonIfNotExists(metadataPeople, person);
-
- // Assert
- Assert.Single(metadataPeople);
- Assert.NotNull(metadataPeople.SingleOrDefault(p =>
- p.Name.Equals(person.Name) && p.Role == person.Role && p.NormalizedName == person.NormalizedName));
- Assert.Equal(1, metadataPeople.First().Id);
- }
-
- [Fact]
- public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonNameIsNullOrEmpty()
- {
- // Arrange
- var metadataPeople = new List();
- var person2 = new PersonBuilder(string.Empty, PersonRole.Character).Build();
-
- // Act
- PersonHelper.AddPersonIfNotExists(metadataPeople, person2);
-
- // Assert
- Assert.Empty(metadataPeople);
- }
-
- [Fact]
- public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsDifferentButRoleIsSame()
- {
- // Arrange
- var metadataPeople = new List
- {
- new PersonBuilder("John Smith", PersonRole.Character).Build()
- };
- var person = new PersonBuilder("John Doe", PersonRole.Character).Build();
-
- // Act
- PersonHelper.AddPersonIfNotExists(metadataPeople, person);
-
- // Assert
- Assert.Equal(2, metadataPeople.Count);
- Assert.Contains(person, metadataPeople);
- }
-
- [Fact]
- public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsSameButRoleIsDifferent()
- {
- // Arrange
- var metadataPeople = new List
- {
- new PersonBuilder("John Doe", PersonRole.Writer).Build()
- };
- var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
-
- // Act
- PersonHelper.AddPersonIfNotExists(metadataPeople, person);
-
- // Assert
- Assert.Equal(2, metadataPeople.Count);
- Assert.Contains(person, metadataPeople);
- }
-
-
-
-
- [Fact]
- public void AddPeople_ShouldAddOnlyNonExistingPeople()
- {
- var existingPeople = new List
- {
- new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
- new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
- new PersonBuilder("Sally", PersonRole.Writer).Build(),
- };
-
-
- PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build());
- Assert.Equal(3, existingPeople.Count);
-
- PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.Writer).Build());
- Assert.Equal(3, existingPeople.Count);
-
- PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo Two", PersonRole.CoverArtist).Build());
- Assert.Equal(4, existingPeople.Count);
- }
-
- #endregion
+ // #region UpdatePeople
+ // [Fact]
+ // public void UpdatePeople_ShouldAddNewPeople()
+ // {
+ // var allPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // };
+ // var peopleAdded = new List();
+ //
+ // PersonHelper.UpdatePeople(allPeople, new[] {"Joseph Shmo", "Sally Ann"}, PersonRole.Writer, person =>
+ // {
+ // peopleAdded.Add(person);
+ // });
+ //
+ // Assert.Equal(2, peopleAdded.Count);
+ // Assert.Equal(4, allPeople.Count);
+ // }
+ //
+ // [Fact]
+ // public void UpdatePeople_ShouldNotAddDuplicatePeople()
+ // {
+ // var allPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // new PersonBuilder("Sally Ann", PersonRole.CoverArtist).Build(),
+ //
+ // };
+ // var peopleAdded = new List();
+ //
+ // PersonHelper.UpdatePeople(allPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.CoverArtist, person =>
+ // {
+ // peopleAdded.Add(person);
+ // });
+ //
+ // Assert.Equal(3, allPeople.Count);
+ // }
+ // #endregion
+ //
+ // #region UpdatePeopleList
+ //
+ // [Fact]
+ // public void UpdatePeopleList_NullTags_NoChanges()
+ // {
+ // // Arrange
+ // ICollection tags = null;
+ // var series = new SeriesBuilder("Test Series").Build();
+ // var allTags = new List();
+ // var handleAddCalled = false;
+ // var onModifiedCalled = false;
+ //
+ // // Act
+ // PersonHelper.UpdatePeopleList(PersonRole.Writer, tags, series, allTags, p => handleAddCalled = true, () => onModifiedCalled = true);
+ //
+ // // Assert
+ // Assert.False(handleAddCalled);
+ // Assert.False(onModifiedCalled);
+ // }
+ //
+ // [Fact]
+ // public void UpdatePeopleList_AddNewTag_TagAddedAndOnModifiedCalled()
+ // {
+ // // Arrange
+ // const PersonRole role = PersonRole.Writer;
+ // var tags = new List
+ // {
+ // new PersonDto { Id = 1, Name = "John Doe", Role = role }
+ // };
+ // var series = new SeriesBuilder("Test Series").Build();
+ // var allTags = new List();
+ // var handleAddCalled = false;
+ // var onModifiedCalled = false;
+ //
+ // // Act
+ // PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
+ // {
+ // handleAddCalled = true;
+ // series.Metadata.People.Add(p);
+ // }, () => onModifiedCalled = true);
+ //
+ // // Assert
+ // Assert.True(handleAddCalled);
+ // Assert.True(onModifiedCalled);
+ // Assert.Single(series.Metadata.People);
+ // Assert.Equal("John Doe", series.Metadata.People.First().Name);
+ // }
+ //
+ // [Fact]
+ // public void UpdatePeopleList_RemoveExistingTag_TagRemovedAndOnModifiedCalled()
+ // {
+ // // Arrange
+ // const PersonRole role = PersonRole.Writer;
+ // var tags = new List();
+ // var series = new SeriesBuilder("Test Series").Build();
+ // var person = new PersonBuilder("John Doe", role).Build();
+ // person.Id = 1;
+ // series.Metadata.People.Add(person);
+ // var allTags = new List
+ // {
+ // person
+ // };
+ // var handleAddCalled = false;
+ // var onModifiedCalled = false;
+ //
+ // // Act
+ // PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
+ // {
+ // handleAddCalled = true;
+ // series.Metadata.People.Add(p);
+ // }, () => onModifiedCalled = true);
+ //
+ // // Assert
+ // Assert.False(handleAddCalled);
+ // Assert.True(onModifiedCalled);
+ // Assert.Empty(series.Metadata.People);
+ // }
+ //
+ // [Fact]
+ // public void UpdatePeopleList_UpdateExistingTag_OnModifiedCalled()
+ // {
+ // // Arrange
+ // const PersonRole role = PersonRole.Writer;
+ // var tags = new List
+ // {
+ // new PersonDto { Id = 1, Name = "John Doe", Role = role }
+ // };
+ // var series = new SeriesBuilder("Test Series").Build();
+ // var person = new PersonBuilder("John Doe", role).Build();
+ // person.Id = 1;
+ // series.Metadata.People.Add(person);
+ // var allTags = new List
+ // {
+ // person
+ // };
+ // var handleAddCalled = false;
+ // var onModifiedCalled = false;
+ //
+ // // Act
+ // PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
+ // {
+ // handleAddCalled = true;
+ // series.Metadata.People.Add(p);
+ // }, () => onModifiedCalled = true);
+ //
+ // // Assert
+ // Assert.False(handleAddCalled);
+ // Assert.False(onModifiedCalled);
+ // Assert.Single(series.Metadata.People);
+ // Assert.Equal("John Doe", series.Metadata.People.First().Name);
+ // }
+ //
+ // [Fact]
+ // public void UpdatePeopleList_NoChanges_HandleAddAndOnModifiedNotCalled()
+ // {
+ // // Arrange
+ // const PersonRole role = PersonRole.Writer;
+ // var tags = new List
+ // {
+ // new PersonDto { Id = 1, Name = "John Doe", Role = role }
+ // };
+ // var series = new SeriesBuilder("Test Series").Build();
+ // var person = new PersonBuilder("John Doe", role).Build();
+ // person.Id = 1;
+ // series.Metadata.People.Add(person);
+ // var allTags = new List
+ // {
+ // new PersonBuilder("John Doe", role).Build()
+ // };
+ // var handleAddCalled = false;
+ // var onModifiedCalled = false;
+ //
+ // // Act
+ // PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
+ // {
+ // handleAddCalled = true;
+ // series.Metadata.People.Add(p);
+ // }, () => onModifiedCalled = true);
+ //
+ // // Assert
+ // Assert.False(handleAddCalled);
+ // Assert.False(onModifiedCalled);
+ // Assert.Single(series.Metadata.People);
+ // Assert.Equal("John Doe", series.Metadata.People.First().Name);
+ // }
+ //
+ //
+ //
+ // #endregion
+ //
+ // #region RemovePeople
+ // [Fact]
+ // public void RemovePeople_ShouldRemovePeopleOfSameRole()
+ // {
+ // var existingPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // };
+ // var peopleRemoved = new List();
+ // PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
+ // {
+ // peopleRemoved.Add(person);
+ // });
+ //
+ // Assert.NotEqual(existingPeople, peopleRemoved);
+ // Assert.Single(peopleRemoved);
+ // }
+ //
+ // [Fact]
+ // public void RemovePeople_ShouldRemovePeopleFromBothRoles()
+ // {
+ // var existingPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // };
+ // var peopleRemoved = new List();
+ // PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
+ // {
+ // peopleRemoved.Add(person);
+ // });
+ //
+ // Assert.NotEqual(existingPeople, peopleRemoved);
+ // Assert.Single(peopleRemoved);
+ //
+ // PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo"}, PersonRole.CoverArtist, person =>
+ // {
+ // peopleRemoved.Add(person);
+ // });
+ //
+ // Assert.Empty(existingPeople);
+ // Assert.Equal(2, peopleRemoved.Count);
+ // }
+ //
+ // [Fact]
+ // public void RemovePeople_ShouldRemovePeopleOfSameRole_WhenNothingPassed()
+ // {
+ // var existingPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // };
+ // var peopleRemoved = new List();
+ // PersonHelper.RemovePeople(existingPeople, new List(), PersonRole.Writer, person =>
+ // {
+ // peopleRemoved.Add(person);
+ // });
+ //
+ // Assert.NotEqual(existingPeople, peopleRemoved);
+ // Assert.Equal(2, peopleRemoved.Count);
+ // }
+ //
+ //
+ // #endregion
+ //
+ // #region KeepOnlySamePeopleBetweenLists
+ // [Fact]
+ // public void KeepOnlySamePeopleBetweenLists()
+ // {
+ // var existingPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // new PersonBuilder("Sally", PersonRole.Writer).Build(),
+ // };
+ //
+ // var peopleFromChapters = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // };
+ //
+ // var peopleRemoved = new List();
+ // PersonHelper.KeepOnlySamePeopleBetweenLists(existingPeople,
+ // peopleFromChapters, person =>
+ // {
+ // peopleRemoved.Add(person);
+ // });
+ //
+ // Assert.Equal(2, peopleRemoved.Count);
+ // }
+ // #endregion
+ //
+ // #region AddPeople
+ //
+ // [Fact]
+ // public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonDoesNotExist()
+ // {
+ // // Arrange
+ // var metadataPeople = new List();
+ // var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
+ //
+ // // Act
+ // PersonHelper.AddPersonIfNotExists(metadataPeople, person);
+ //
+ // // Assert
+ // Assert.Single(metadataPeople);
+ // Assert.Contains(person, metadataPeople);
+ // }
+ //
+ // [Fact]
+ // public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonAlreadyExists()
+ // {
+ // // Arrange
+ // var metadataPeople = new List
+ // {
+ // new PersonBuilder("John Smith", PersonRole.Character)
+ // .WithId(1)
+ // .Build()
+ // };
+ // var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
+ // // Act
+ // PersonHelper.AddPersonIfNotExists(metadataPeople, person);
+ //
+ // // Assert
+ // Assert.Single(metadataPeople);
+ // Assert.NotNull(metadataPeople.SingleOrDefault(p =>
+ // p.Name.Equals(person.Name) && p.Role == person.Role && p.NormalizedName == person.NormalizedName));
+ // Assert.Equal(1, metadataPeople.First().Id);
+ // }
+ //
+ // [Fact]
+ // public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonNameIsNullOrEmpty()
+ // {
+ // // Arrange
+ // var metadataPeople = new List();
+ // var person2 = new PersonBuilder(string.Empty, PersonRole.Character).Build();
+ //
+ // // Act
+ // PersonHelper.AddPersonIfNotExists(metadataPeople, person2);
+ //
+ // // Assert
+ // Assert.Empty(metadataPeople);
+ // }
+ //
+ // [Fact]
+ // public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsDifferentButRoleIsSame()
+ // {
+ // // Arrange
+ // var metadataPeople = new List
+ // {
+ // new PersonBuilder("John Smith", PersonRole.Character).Build()
+ // };
+ // var person = new PersonBuilder("John Doe", PersonRole.Character).Build();
+ //
+ // // Act
+ // PersonHelper.AddPersonIfNotExists(metadataPeople, person);
+ //
+ // // Assert
+ // Assert.Equal(2, metadataPeople.Count);
+ // Assert.Contains(person, metadataPeople);
+ // }
+ //
+ // [Fact]
+ // public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsSameButRoleIsDifferent()
+ // {
+ // // Arrange
+ // var metadataPeople = new List
+ // {
+ // new PersonBuilder("John Doe", PersonRole.Writer).Build()
+ // };
+ // var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
+ //
+ // // Act
+ // PersonHelper.AddPersonIfNotExists(metadataPeople, person);
+ //
+ // // Assert
+ // Assert.Equal(2, metadataPeople.Count);
+ // Assert.Contains(person, metadataPeople);
+ // }
+ //
+ //
+ //
+ //
+ // [Fact]
+ // public void AddPeople_ShouldAddOnlyNonExistingPeople()
+ // {
+ // var existingPeople = new List
+ // {
+ // new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
+ // new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
+ // new PersonBuilder("Sally", PersonRole.Writer).Build(),
+ // };
+ //
+ //
+ // PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build());
+ // Assert.Equal(3, existingPeople.Count);
+ //
+ // PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.Writer).Build());
+ // Assert.Equal(3, existingPeople.Count);
+ //
+ // PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo Two", PersonRole.CoverArtist).Build());
+ // Assert.Equal(4, existingPeople.Count);
+ // }
+ //
+ // #endregion
}
diff --git a/API.Tests/Parsers/BasicParserTests.cs b/API.Tests/Parsers/BasicParserTests.cs
index d47ebb8d29..ad040d59ee 100644
--- a/API.Tests/Parsers/BasicParserTests.cs
+++ b/API.Tests/Parsers/BasicParserTests.cs
@@ -138,13 +138,31 @@ public void Parse_MangaLibrary_SpecialMarkerInFilename()
[Fact]
public void Parse_MangaLibrary_SpecialInFilename()
{
- var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Volume Omake.cbr",
+ var actual = _parser.Parse("C:/Books/Summer Time Rendering/Volume SP01.cbr",
"C:/Books/Summer Time Rendering/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Summer Time Rendering", actual.Series);
- Assert.Equal("Volume Omake", actual.Title);
+ Assert.Equal("Volume SP01", actual.Title);
+ Assert.Equal(Parser.SpecialVolume, actual.Volumes);
+ Assert.Equal(Parser.DefaultChapter, actual.Chapters);
+ Assert.True(actual.IsSpecial);
+ }
+
+ ///
+ /// Tests that when the filename parses as a speical, it appropriately parses
+ ///
+ [Fact]
+ public void Parse_MangaLibrary_SpecialInFilename2()
+ {
+ var actual = _parser.Parse("M:/Kimi wa Midara na Boku no Joou/Specials/[Renzokusei] Special 1 SP02.zip",
+ "M:/Kimi wa Midara na Boku no Joou/",
+ RootDirectory, LibraryType.Manga, null);
+ Assert.NotNull(actual);
+
+ Assert.Equal("Kimi wa Midara na Boku no Joou", actual.Series);
+ Assert.Equal("[Renzokusei] Special 1 SP02", actual.Title);
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.True(actual.IsSpecial);
diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/API.Tests/Parsers/DefaultParserTests.cs
index 9dc926ef5c..733b55d624 100644
--- a/API.Tests/Parsers/DefaultParserTests.cs
+++ b/API.Tests/Parsers/DefaultParserTests.cs
@@ -408,7 +408,7 @@ public void Parse_ParseInfo_Manga_WithSpecialsFolder()
expected = new ParserInfo
{
Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, IsSpecial = true,
- Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
+ Chapters = Parser.DefaultChapter, Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};
diff --git a/API.Tests/Parsing/BookParsingTests.cs b/API.Tests/Parsing/BookParsingTests.cs
index 443d55b6db..9b02eff630 100644
--- a/API.Tests/Parsing/BookParsingTests.cs
+++ b/API.Tests/Parsing/BookParsingTests.cs
@@ -21,24 +21,4 @@ public void ParseVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Book));
}
-
- // [Theory]
- // [InlineData("@font-face{font-family:'syyskuu_repaleinen';src:url(data:font/opentype;base64,AAEAAAA", "@font-face{font-family:'syyskuu_repaleinen';src:url(data:font/opentype;base64,AAEAAAA")]
- // [InlineData("@font-face{font-family:'syyskuu_repaleinen';src:url('fonts/font.css')", "@font-face{font-family:'syyskuu_repaleinen';src:url('TEST/fonts/font.css')")]
- // public void ReplaceFontSrcUrl(string input, string expected)
- // {
- // var apiBase = "TEST/";
- // var actual = API.Parser.Parser.FontSrcUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
- // Assert.Equal(expected, actual);
- // }
- //
- // [Theory]
- // [InlineData("@import url('font.css');", "@import url('TEST/font.css');")]
- // public void ReplaceImportSrcUrl(string input, string expected)
- // {
- // var apiBase = "TEST/";
- // var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
- // Assert.Equal(expected, actual);
- // }
-
}
diff --git a/API.Tests/Parsing/ComicParsingTests.cs b/API.Tests/Parsing/ComicParsingTests.cs
index 2d2e3d12db..ad28e80a9c 100644
--- a/API.Tests/Parsing/ComicParsingTests.cs
+++ b/API.Tests/Parsing/ComicParsingTests.cs
@@ -1,11 +1,6 @@
-using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
-using API.Services;
using API.Services.Tasks.Scanner.Parser;
-using Microsoft.Extensions.Logging;
-using NSubstitute;
using Xunit;
-using Xunit.Abstractions;
namespace API.Tests.Parsing;
@@ -73,41 +68,41 @@ public class ComicParsingTests
[InlineData("SKY WORLD สกายเวิลด์ เล่มที่ 1", "SKY WORLD สกายเวิลด์")]
public void ParseComicSeriesTest(string filename, string expected)
{
- Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
+ Assert.Equal(expected, Parser.ParseComicSeries(filename));
}
[Theory]
- [InlineData("01 Spider-Man & Wolverine 01.cbr", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman & Catwoman - Trail of the Gun 01", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman & Daredevil - King of New York", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman & Robin the Teen Wonder #0", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman & Wildcat (1 of 3)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman And Superman World's Finest #01", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Babe 01", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("01 Spider-Man & Wolverine 01.cbr", Parser.LooseLeafVolume)]
+ [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", Parser.LooseLeafVolume)]
+ [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", Parser.LooseLeafVolume)]
+ [InlineData("Batman & Catwoman - Trail of the Gun 01", Parser.LooseLeafVolume)]
+ [InlineData("Batman & Daredevil - King of New York", Parser.LooseLeafVolume)]
+ [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", Parser.LooseLeafVolume)]
+ [InlineData("Batman & Robin the Teen Wonder #0", Parser.LooseLeafVolume)]
+ [InlineData("Batman & Wildcat (1 of 3)", Parser.LooseLeafVolume)]
+ [InlineData("Batman And Superman World's Finest #01", Parser.LooseLeafVolume)]
+ [InlineData("Babe 01", Parser.LooseLeafVolume)]
+ [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", Parser.LooseLeafVolume)]
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
- [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", Parser.LooseLeafVolume)]
[InlineData("Superman v1 024 (09-10 1943)", "1")]
[InlineData("Superman v1.5 024 (09-10 1943)", "1.5")]
- [InlineData("Amazing Man Comics chapter 25", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("spawn-123", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("spawn-chapter-123", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman Beyond 04 (of 6) (1999)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman Beyond 001 (2012)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman Beyond 2.0 001 (2013)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Amazing Man Comics chapter 25", Parser.LooseLeafVolume)]
+ [InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", Parser.LooseLeafVolume)]
+ [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", Parser.LooseLeafVolume)]
+ [InlineData("spawn-123", Parser.LooseLeafVolume)]
+ [InlineData("spawn-chapter-123", Parser.LooseLeafVolume)]
+ [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", Parser.LooseLeafVolume)]
+ [InlineData("Batman Beyond 04 (of 6) (1999)", Parser.LooseLeafVolume)]
+ [InlineData("Batman Beyond 001 (2012)", Parser.LooseLeafVolume)]
+ [InlineData("Batman Beyond 2.0 001 (2013)", Parser.LooseLeafVolume)]
+ [InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", Parser.LooseLeafVolume)]
[InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "1")]
- [InlineData("Chew Script Book (2011) (digital-Empire) SP04", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Chew Script Book (2011) (digital-Empire) SP04", Parser.LooseLeafVolume)]
[InlineData("Batgirl Vol.2000 #57 (December, 2004)", "2000")]
[InlineData("Batgirl V2000 #57", "2000")]
- [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
- [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", Parser.LooseLeafVolume)]
+ [InlineData("2000 AD 0366 [1984-04-28] (flopbie)", Parser.LooseLeafVolume)]
[InlineData("Daredevil - v6 - 10 - (2019)", "6")]
[InlineData("Daredevil - v6.5", "6.5")]
// Tome Tests
@@ -117,25 +112,25 @@ public void ParseComicSeriesTest(string filename, string expected)
[InlineData("Conquistador_Tome_2", "2")]
[InlineData("Max_l_explorateur-_Tome_0", "0")]
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")]
- [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", Parser.LooseLeafVolume)]
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")]
// Russian Tests
[InlineData("Kebab Том 1 Глава 3", "1")]
- [InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("Манга Глава 2", Parser.LooseLeafVolume)]
[InlineData("ย้อนเวลากลับมาร้าย เล่ม 1", "1")]
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "1")]
- [InlineData("วิวาห์รัก เดิมพันชีวิต ตอนที่ 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)]
+ [InlineData("วิวาห์รัก เดิมพันชีวิต ตอนที่ 2", Parser.LooseLeafVolume)]
public void ParseComicVolumeTest(string filename, string expected)
{
- Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
+ Assert.Equal(expected, Parser.ParseComicVolume(filename));
}
[Theory]
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
- [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
- [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
+ [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", Parser.DefaultChapter)]
+ [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", Parser.DefaultChapter)]
[InlineData("Batman & Catwoman - Trail of the Gun 01", "1")]
- [InlineData("Batman & Daredevil - King of New York", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
+ [InlineData("Batman & Daredevil - King of New York", Parser.DefaultChapter)]
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")]
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
[InlineData("Batman & Wildcat (1 of 3)", "1")]
@@ -159,8 +154,8 @@ public void ParseComicVolumeTest(string filename, string expected)
[InlineData("Batman Beyond 001 (2012)", "1")]
[InlineData("Batman Beyond 2.0 001 (2013)", "1")]
[InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "1")]
- [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
- [InlineData("Chew Script Book (2011) (digital-Empire) SP04", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
+ [InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", Parser.DefaultChapter)]
+ [InlineData("Chew Script Book (2011) (digital-Empire) SP04", Parser.DefaultChapter)]
[InlineData("Batgirl Vol.2000 #57 (December, 2004)", "57")]
[InlineData("Batgirl V2000 #57", "57")]
[InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "21")]
@@ -169,7 +164,7 @@ public void ParseComicVolumeTest(string filename, string expected)
[InlineData("Daredevil - v6 - 10 - (2019)", "10")]
[InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")]
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")]
- [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
+ [InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", Parser.DefaultChapter)]
[InlineData("Kebab Том 1 Глава 3", "3")]
[InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")]
@@ -179,35 +174,35 @@ public void ParseComicVolumeTest(string filename, string expected)
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseComicChapterTest(string filename, string expected)
{
- Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Comic));
+ Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Comic));
}
[Theory]
- [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", true)]
- [InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", true)]
+ [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", false)]
+ [InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", false)]
[InlineData("Baldwin the Brave & Other Tales Special SP1.cbr", true)]
- [InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", true)]
- [InlineData("Boule et Bill - THS -Bill à disparu", true)]
- [InlineData("Asterix - HS - Les 12 travaux d'Astérix", true)]
- [InlineData("Sillage Hors Série - Le Collectionneur - Concordance-DKFR", true)]
+ [InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", false)]
+ [InlineData("Boule et Bill - THS -Bill à disparu", false)]
+ [InlineData("Asterix - HS - Les 12 travaux d'Astérix", false)]
+ [InlineData("Sillage Hors Série - Le Collectionneur - Concordance-DKFR", false)]
[InlineData("laughs", false)]
- [InlineData("Annual Days of Summer", true)]
- [InlineData("Adventure Time 2013 Annual #001 (2013)", true)]
- [InlineData("Adventure Time 2013_Annual_#001 (2013)", true)]
- [InlineData("Adventure Time 2013_-_Annual #001 (2013)", true)]
+ [InlineData("Annual Days of Summer", false)]
+ [InlineData("Adventure Time 2013 Annual #001 (2013)", false)]
+ [InlineData("Adventure Time 2013_Annual_#001 (2013)", false)]
+ [InlineData("Adventure Time 2013_-_Annual #001 (2013)", false)]
[InlineData("G.I. Joe - A Real American Hero Yearbook 004 Reprint (2021)", false)]
[InlineData("Mazebook 001", false)]
- [InlineData("X-23 One Shot (2010)", true)]
- [InlineData("Casus Belli v1 Hors-Série 21 - Mousquetaires et Sorcellerie", true)]
- [InlineData("Batman Beyond Annual", true)]
- [InlineData("Batman Beyond Bonus", true)]
- [InlineData("Batman Beyond OneShot", true)]
- [InlineData("Batman Beyond Specials", true)]
- [InlineData("Batman Beyond Omnibus (1999)", true)]
- [InlineData("Batman Beyond Omnibus", true)]
- [InlineData("01 Annual Batman Beyond", true)]
- [InlineData("Blood Syndicate Annual #001", true)]
+ [InlineData("X-23 One Shot (2010)", false)]
+ [InlineData("Casus Belli v1 Hors-Série 21 - Mousquetaires et Sorcellerie", false)]
+ [InlineData("Batman Beyond Annual", false)]
+ [InlineData("Batman Beyond Bonus", false)]
+ [InlineData("Batman Beyond OneShot", false)]
+ [InlineData("Batman Beyond Specials", false)]
+ [InlineData("Batman Beyond Omnibus (1999)", false)]
+ [InlineData("Batman Beyond Omnibus", false)]
+ [InlineData("01 Annual Batman Beyond", false)]
+ [InlineData("Blood Syndicate Annual #001", false)]
public void IsComicSpecialTest(string input, bool expected)
{
Assert.Equal(expected, Parser.IsSpecial(input, LibraryType.Comic));
diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs
index 64d303f4d6..852eedb9eb 100644
--- a/API.Tests/Parsing/MangaParsingTests.cs
+++ b/API.Tests/Parsing/MangaParsingTests.cs
@@ -326,18 +326,18 @@ public void ParseEditionTest(string input, string expected)
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseEdition(input));
}
[Theory]
- [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)]
- [InlineData("Beelzebub_Omake_June_2012_RHS", true)]
+ [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)]
+ [InlineData("Beelzebub_Omake_June_2012_RHS", false)]
[InlineData("Beelzebub_Side_Story_02_RHS.zip", false)]
- [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", true)]
- [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)]
- [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)]
- [InlineData("Ani-Hina Art Collection.cbz", true)]
- [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", true)]
- [InlineData("A Town Where You Live - Bonus Chapter.zip", true)]
+ [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", false)]
+ [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", false)]
+ [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", false)]
+ [InlineData("Ani-Hina Art Collection.cbz", false)]
+ [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", false)]
+ [InlineData("A Town Where You Live - Bonus Chapter.zip", false)]
[InlineData("Yuki Merry - 4-Komga Anthology", false)]
- [InlineData("Beastars - SP01", false)]
- [InlineData("Beastars SP01", false)]
+ [InlineData("Beastars - SP01", true)]
+ [InlineData("Beastars SP01", true)]
[InlineData("The League of Extraordinary Gentlemen", false)]
[InlineData("The League of Extra-ordinary Gentlemen", false)]
[InlineData("Dr. Ramune - Mysterious Disease Specialist v01 (2020) (Digital) (danke-Empire)", false)]
diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs
index 9844e7766f..737779f0f5 100644
--- a/API.Tests/Services/DirectoryServiceTests.cs
+++ b/API.Tests/Services/DirectoryServiceTests.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading.Tasks;
using API.Services;
+using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@@ -745,6 +746,12 @@ public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string
[InlineData(new [] {"/manga"},
new [] {"/manga/Love Hina/Vol. 01.cbz", "/manga/Love Hina/Specials/Sp01.cbz"},
"/manga/Love Hina")]
+ [InlineData(new [] {"/manga"},
+ new [] {"/manga/Love Hina/Hina/Vol. 01.cbz", "/manga/Love Hina/Specials/Sp01.cbz"},
+ "/manga/Love Hina")]
+ [InlineData(new [] {"/manga"},
+ new [] {"/manga/Dress Up Darling/Dress Up Darling Ch 01.cbz", "/manga/Dress Up Darling/Dress Up Darling/Dress Up Darling Vol 01.cbz"},
+ "/manga/Dress Up Darling")]
public void FindLowestDirectoriesFromFilesTest(string[] rootDirectories, string[] files, string expectedDirectory)
{
var fileSystem = new MockFileSystem();
@@ -920,8 +927,9 @@ public Task ScanFiles_ShouldFindNoFiles_AllAreIgnored()
var ds = new DirectoryService(Substitute.For>(), fileSystem);
-
- var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions);
+ var globMatcher = new GlobMatcher();
+ globMatcher.AddExclude("*.*");
+ var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher);
Assert.Empty(allFiles);
@@ -945,7 +953,9 @@ public Task ScanFiles_ShouldFindNoNestedFiles_IgnoreNestedFiles()
var ds = new DirectoryService(Substitute.For>(), fileSystem);
- var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions);
+ var globMatcher = new GlobMatcher();
+ globMatcher.AddExclude("**/Accel World/*");
+ var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher);
Assert.Single(allFiles); // Ignore files are not counted in files, only valid extensions
@@ -974,7 +984,10 @@ public Task ScanFiles_NestedIgnore_IgnoreNestedFilesInOneDirectoryOnly()
var ds = new DirectoryService(Substitute.For>(), fileSystem);
- var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions);
+ var globMatcher = new GlobMatcher();
+ globMatcher.AddExclude("**/Accel World/*");
+ globMatcher.AddExclude("**/ArtBooks/*");
+ var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher);
Assert.Equal(2, allFiles.Count); // Ignore files are not counted in files, only valid extensions
diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs
index 9fbb76ec36..ff4868a8c1 100644
--- a/API.Tests/Services/ParseScannedFilesTests.cs
+++ b/API.Tests/Services/ParseScannedFilesTests.cs
@@ -206,24 +206,6 @@ public async Task ScanLibrariesForSeries_ShouldFindFiles()
var psf = new ParseScannedFiles(Substitute.For>(), ds,
new MockReadingItemService(ds, Substitute.For()), Substitute.For());
- // var parsedSeries = new Dictionary>();
- //
- // Task TrackFiles(Tuple> parsedInfo)
- // {
- // var skippedScan = parsedInfo.Item1;
- // var parsedFiles = parsedInfo.Item2;
- // if (parsedFiles.Count == 0) return Task.CompletedTask;
- //
- // var foundParsedSeries = new ParsedSeries()
- // {
- // Name = parsedFiles.First().Series,
- // NormalizedName = parsedFiles.First().Series.ToNormalized(),
- // Format = parsedFiles.First().Format
- // };
- //
- // parsedSeries.Add(foundParsedSeries, parsedFiles);
- // return Task.CompletedTask;
- // }
var library =
await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
@@ -273,7 +255,7 @@ public async Task ProcessFiles_ForLibraryMode_OnlyCallsFolderActionForEachTopLev
var directoriesSeen = new HashSet();
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
- var scanResults = await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
+ var scanResults = await psf.ScanFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
foreach (var scanResult in scanResults)
{
directoriesSeen.Add(scanResult.Folder);
@@ -295,7 +277,7 @@ public async Task ProcessFiles_ForNonLibraryMode_CallsFolderActionOnce()
Assert.NotNull(library);
var directoriesSeen = new HashSet();
- var scanResults = await psf.ProcessFiles("C:/Data/", false,
+ var scanResults = await psf.ScanFiles("C:/Data/", false,
await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
foreach (var scanResult in scanResults)
@@ -328,7 +310,7 @@ public async Task ProcessFiles_ShouldCallFolderActionTwice()
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
- var scanResults = await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
+ var scanResults = await psf.ScanFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
Assert.Equal(2, scanResults.Count);
}
@@ -357,7 +339,7 @@ public async Task ProcessFiles_ShouldCallFolderActionOnce()
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
- var scanResults = await psf.ProcessFiles("C:/Data", false,
+ var scanResults = await psf.ScanFiles("C:/Data", false,
await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
Assert.Single(scanResults);
diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs
index 67b93273bc..fcbfe82602 100644
--- a/API.Tests/Services/ScannerServiceTests.cs
+++ b/API.Tests/Services/ScannerServiceTests.cs
@@ -51,85 +51,83 @@ protected override async Task ResetDb()
}
[Fact]
- public void FindSeriesNotOnDisk_Should_Remove1()
+ public async Task ScanLibrary_ComicVine_PublisherFolder()
{
- var infos = new Dictionary>();
-
- ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive});
- //AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub});
+ var testcase = "Publisher - ComicVine.json";
+ var library = await GenerateScannerData(testcase);
+ var scanner = CreateServices();
+ await scanner.ScanLibrary(library.Id);
+ var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
- var existingSeries = new List
- {
- new SeriesBuilder("Darker Than Black")
- .WithFormat(MangaFormat.Epub)
+ Assert.NotNull(postLib);
+ Assert.Equal(4, postLib.Series.Count);
+ }
- .WithVolume(new VolumeBuilder("1")
- .WithName("1")
- .Build())
- .WithLocalizedName("Darker Than Black")
- .Build()
- };
+ [Fact]
+ public async Task ScanLibrary_ShouldCombineNestedFolder()
+ {
+ var testcase = "Series and Series-Series Combined - Manga.json";
+ var library = await GenerateScannerData(testcase);
+ var scanner = CreateServices();
+ await scanner.ScanLibrary(library.Id);
+ var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
- Assert.Single(ScannerService.FindSeriesNotOnDisk(existingSeries, infos));
+ Assert.NotNull(postLib);
+ Assert.Single(postLib.Series);
+ Assert.Equal(2, postLib.Series.First().Volumes.Count);
}
+
[Fact]
- public void FindSeriesNotOnDisk_Should_RemoveNothing_Test()
+ public async Task ScanLibrary_FlatSeries()
{
- var infos = new Dictionary>();
+ var testcase = "Flat Series - Manga.json";
+ var library = await GenerateScannerData(testcase);
+ var scanner = CreateServices();
+ await scanner.ScanLibrary(library.Id);
+ var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
- ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Format = MangaFormat.Archive});
- ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Cage of Eden", Volumes = "1", Format = MangaFormat.Archive});
- ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Cage of Eden", Volumes = "10", Format = MangaFormat.Archive});
+ Assert.NotNull(postLib);
+ Assert.Single(postLib.Series);
+ Assert.Equal(3, postLib.Series.First().Volumes.Count);
- var existingSeries = new List
- {
- new SeriesBuilder("Cage of Eden")
- .WithFormat(MangaFormat.Archive)
-
- .WithVolume(new VolumeBuilder("1")
- .WithName("1")
- .Build())
- .WithLocalizedName("Darker Than Black")
- .Build(),
- new SeriesBuilder("Darker Than Black")
- .WithFormat(MangaFormat.Archive)
- .WithVolume(new VolumeBuilder("1")
- .WithName("1")
- .Build())
- .WithLocalizedName("Darker Than Black")
- .Build(),
- };
-
- Assert.Empty(ScannerService.FindSeriesNotOnDisk(existingSeries, infos));
+ // TODO: Trigger a deletion of ch 10
}
[Fact]
- public async Task ScanLibrary_ComicVine_PublisherFolder()
+ public async Task ScanLibrary_FlatSeriesWithSpecialFolder()
{
- var testcase = "Publisher - ComicVine.json";
- var postLib = await GenerateScannerData(testcase);
+ var testcase = "Flat Series with Specials Folder - Manga.json";
+ var library = await GenerateScannerData(testcase);
+ var scanner = CreateServices();
+ await scanner.ScanLibrary(library.Id);
+ var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
Assert.NotNull(postLib);
- Assert.Equal(4, postLib.Series.Count);
+ Assert.Single(postLib.Series);
+ Assert.Equal(4, postLib.Series.First().Volumes.Count);
+ Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null));
}
[Fact]
- public async Task ScanLibrary_ShouldCombineNestedFolder()
+ public async Task ScanLibrary_FlatSeriesWithSpecial()
{
- var testcase = "Series and Series-Series Combined - Manga.json";
- var postLib = await GenerateScannerData(testcase);
+ const string testcase = "Flat Special - Manga.json";
+
+ var library = await GenerateScannerData(testcase);
+ var scanner = CreateServices();
+ await scanner.ScanLibrary(library.Id);
+ var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
Assert.NotNull(postLib);
Assert.Single(postLib.Series);
- Assert.Single(postLib.Series);
- Assert.Equal(2, postLib.Series.First().Volumes.Count);
+ Assert.Equal(3, postLib.Series.First().Volumes.Count);
+ Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null));
}
private async Task GenerateScannerData(string testcase)
{
var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase));
- _testOutputHelper.WriteLine($"Test Directory Path: {testDirectoryPath}");
var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase));
@@ -145,25 +143,26 @@ private async Task GenerateScannerData(string testcase)
_unitOfWork.LibraryRepository.Add(library);
await _unitOfWork.CommitAsync();
+ return library;
+ }
+
+ private ScannerService CreateServices()
+ {
var ds = new DirectoryService(Substitute.For>(), new FileSystem());
var mockReadingService = new MockReadingItemService(ds, Substitute.For());
var processSeries = new ProcessSeries(_unitOfWork, Substitute.For>(),
Substitute.For(),
ds, Substitute.For(), mockReadingService, Substitute.For(),
Substitute.For(),
- Substitute.For(), Substitute.For(),
+ Substitute.For(),
Substitute.For(),
- Substitute.For(), new TagManagerService(_unitOfWork, Substitute.For>()));
+ Substitute.For());
var scanner = new ScannerService(_unitOfWork, Substitute.For>(),
Substitute.For(),
Substitute.For(), Substitute.For(), ds,
mockReadingService, processSeries, Substitute.For());
-
- await scanner.ScanLibrary(library.Id);
-
- var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
- return postLib;
+ return scanner;
}
private static (string Publisher, LibraryType Type) SplitPublisherAndLibraryType(string input)
@@ -209,6 +208,8 @@ private async Task GenerateTestDirectory(string mapPath)
// Generate the files and folders
await Scaffold(testDirectory, filePaths);
+ _testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}");
+
return testDirectory;
}
diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs
index 67a541b050..7196c16fa8 100644
--- a/API.Tests/Services/SeriesServiceTests.cs
+++ b/API.Tests/Services/SeriesServiceTests.cs
@@ -817,12 +817,17 @@ public async Task UpdateSeriesMetadata_ShouldRemoveExistingTags()
public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople()
{
await ResetDb();
+ var g = new PersonBuilder("Existing Person").Build();
+ await _context.SaveChangesAsync();
+
var s = new SeriesBuilder("Test")
- .WithMetadata(new SeriesMetadataBuilder().Build())
+ .WithMetadata(new SeriesMetadataBuilder()
+ .WithPerson(g, PersonRole.Publisher)
+ .Build())
.Build();
s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
- var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build();
+
_context.Series.Add(s);
_context.Person.Add(g);
@@ -833,7 +838,7 @@ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople()
SeriesMetadata = new SeriesMetadataDto
{
SeriesId = 1,
- Publishers = new List {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
+ Publishers = new List {new () {Id = 0, Name = "Existing Person"}},
},
});
@@ -842,7 +847,7 @@ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople()
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
- Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
+ Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked
}
@@ -854,10 +859,14 @@ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople()
.WithMetadata(new SeriesMetadataBuilder().Build())
.Build();
s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
- var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build();
- s.Metadata.People = new List
- {new PersonBuilder("Existing Writer", PersonRole.Writer).Build(),
- new PersonBuilder("Existing Translator", PersonRole.Translator).Build(), new PersonBuilder("Existing Publisher 2", PersonRole.Publisher).Build()};
+ var g = new PersonBuilder("Existing Person").Build();
+ s.Metadata.People = new List
+ {
+ new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Writer").Build(), Role = PersonRole.Writer},
+ new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Translator").Build(), Role = PersonRole.Translator},
+ new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Publisher 2").Build(), Role = PersonRole.Publisher}
+ };
+
_context.Series.Add(s);
_context.Person.Add(g);
@@ -868,7 +877,7 @@ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople()
SeriesMetadata = new SeriesMetadataDto
{
SeriesId = 1,
- Publishers = new List {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
+ Publishers = new List {new () {Id = 0, Name = "Existing Person"}},
PublisherLocked = true
},
@@ -878,7 +887,7 @@ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople()
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
- Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
+ Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
Assert.True(series.Metadata.PublisherLocked);
}
@@ -891,7 +900,7 @@ public async Task UpdateSeriesMetadata_ShouldRemoveExistingPerson()
.WithMetadata(new SeriesMetadataBuilder().Build())
.Build();
s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
- var g = new PersonBuilder("Existing Person", PersonRole.Publisher).Build();
+ var g = new PersonBuilder("Existing Person").Build();
_context.Series.Add(s);
_context.Person.Add(g);
diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json
new file mode 100644
index 0000000000..6b4b701604
--- /dev/null
+++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json
@@ -0,0 +1,5 @@
+[
+ "My Dress-Up Darling/My Dress-Up Darling v01.cbz",
+ "My Dress-Up Darling/My Dress-Up Darling v02.cbz",
+ "My Dress-Up Darling/My Dress-Up Darling ch 10.cbz"
+]
\ No newline at end of file
diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json
new file mode 100644
index 0000000000..12e80ea958
--- /dev/null
+++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json
@@ -0,0 +1,6 @@
+[
+ "My Dress-Up Darling/My Dress-Up Darling v01.cbz",
+ "My Dress-Up Darling/My Dress-Up Darling v02.cbz",
+ "My Dress-Up Darling/My Dress-Up Darling ch 10.cbz",
+ "My Dress-Up Darling/Specials/Official Anime Fanbook SP05 (2024) (Digital).cbz"
+]
\ No newline at end of file
diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json
new file mode 100644
index 0000000000..d864da284e
--- /dev/null
+++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json
@@ -0,0 +1,5 @@
+[
+ "Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz",
+ "Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz",
+ "Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz"
+]
\ No newline at end of file
diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json
new file mode 100644
index 0000000000..586ae90f5c
--- /dev/null
+++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json
@@ -0,0 +1,4 @@
+[
+ "My Dress-Up Darling/Chapter 1/01.cbz",
+ "My Dress-Up Darling/Chapter 2/02.cbz"
+]
\ No newline at end of file
diff --git a/API/API.csproj.DotSettings b/API/API.csproj.DotSettings
index c7410bba23..ced14c1540 100644
--- a/API/API.csproj.DotSettings
+++ b/API/API.csproj.DotSettings
@@ -1,3 +1,4 @@
True
+ True
True
\ No newline at end of file
diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs
index a9f19a9514..b2c65282ca 100644
--- a/API/Controllers/ChapterController.cs
+++ b/API/Controllers/ChapterController.cs
@@ -79,7 +79,8 @@ public async Task> DeleteChapter(int chapterId)
[HttpPost("update")]
public async Task UpdateChapterMetadata(UpdateChapterDto dto)
{
- var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags);
+ var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id,
+ ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags);
if (chapter == null)
return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
@@ -135,105 +136,130 @@ public async Task UpdateChapterMetadata(UpdateChapterDto dto)
#region Genres
- if (dto.Genres != null &&
- dto.Genres.Count != 0)
+ if (dto.Genres is {Count: > 0})
{
- var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(dto.Genres.Select(t => Parser.Normalize(t.Title)))).ToList();
chapter.Genres ??= new List();
- GenreHelper.UpdateGenreList(dto.Genres, chapter, allGenres, genre =>
- {
- chapter.Genres.Add(genre);
- }, () => chapter.GenresLocked = true);
+ await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork);
}
#endregion
#region Tags
if (dto.Tags is {Count: > 0})
{
- var allTags = (await _unitOfWork.TagRepository
- .GetAllTagsByNameAsync(dto.Tags.Select(t => Parser.Normalize(t.Title))))
- .ToList();
chapter.Tags ??= new List();
- TagHelper.UpdateTagList(dto.Tags, chapter, allTags, tag =>
- {
- chapter.Tags.Add(tag);
- }, () => chapter.TagsLocked = true);
+ await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork);
}
#endregion
#region People
if (PersonHelper.HasAnyPeople(dto))
{
- void HandleAddPerson(Person person)
- {
- PersonHelper.AddPersonIfNotExists(chapter.People, person);
- }
-
- chapter.People ??= new List();
- var allWriters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Writer,
- dto.Writers.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Writer, dto.Writers, chapter, allWriters.AsReadOnly(),
- HandleAddPerson, () => chapter.WriterLocked = true);
-
- var allCharacters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Character,
- dto.Characters.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Character, dto.Characters, chapter, allCharacters.AsReadOnly(),
- HandleAddPerson, () => chapter.CharacterLocked = true);
-
- var allColorists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Colorist,
- dto.Colorists.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Colorist, dto.Colorists, chapter, allColorists.AsReadOnly(),
- HandleAddPerson, () => chapter.ColoristLocked = true);
-
- var allEditors = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Editor,
- dto.Editors.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Editor, dto.Editors, chapter, allEditors.AsReadOnly(),
- HandleAddPerson, () => chapter.EditorLocked = true);
-
- var allInkers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Inker,
- dto.Inkers.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Inker, dto.Inkers, chapter, allInkers.AsReadOnly(),
- HandleAddPerson, () => chapter.InkerLocked = true);
-
- var allLetterers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Letterer,
- dto.Letterers.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Letterer, dto.Letterers, chapter, allLetterers.AsReadOnly(),
- HandleAddPerson, () => chapter.LettererLocked = true);
-
- var allPencillers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Penciller,
- dto.Pencillers.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Penciller, dto.Pencillers, chapter, allPencillers.AsReadOnly(),
- HandleAddPerson, () => chapter.PencillerLocked = true);
-
- var allPublishers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Publisher,
- dto.Publishers.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Publisher, dto.Publishers, chapter, allPublishers.AsReadOnly(),
- HandleAddPerson, () => chapter.PublisherLocked = true);
-
- var allImprints = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Imprint,
- dto.Imprints.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Imprint, dto.Imprints, chapter, allImprints.AsReadOnly(),
- HandleAddPerson, () => chapter.ImprintLocked = true);
-
- var allTeams = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Team,
- dto.Imprints.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Team, dto.Teams, chapter, allTeams.AsReadOnly(),
- HandleAddPerson, () => chapter.TeamLocked = true);
-
- var allLocations = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Location,
- dto.Imprints.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Location, dto.Locations, chapter, allLocations.AsReadOnly(),
- HandleAddPerson, () => chapter.LocationLocked = true);
-
- var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator,
- dto.Translators.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.Translator, dto.Translators, chapter, allTranslators.AsReadOnly(),
- HandleAddPerson, () => chapter.TranslatorLocked = true);
-
- var allCoverArtists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.CoverArtist,
- dto.CoverArtists.Select(p => Parser.Normalize(p.Name)));
- PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, dto.CoverArtists, chapter, allCoverArtists.AsReadOnly(),
- HandleAddPerson, () => chapter.CoverArtistLocked = true);
+ chapter.People ??= new List();
+
+
+ // Update writers
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Writers.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Writer,
+ _unitOfWork
+ );
+
+ // Update characters
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Characters.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Character,
+ _unitOfWork
+ );
+
+ // Update pencillers
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Pencillers.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Penciller,
+ _unitOfWork
+ );
+
+ // Update inkers
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Inkers.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Inker,
+ _unitOfWork
+ );
+
+ // Update colorists
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Colorists.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Colorist,
+ _unitOfWork
+ );
+
+ // Update letterers
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Letterers.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Letterer,
+ _unitOfWork
+ );
+
+ // Update cover artists
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.CoverArtists.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.CoverArtist,
+ _unitOfWork
+ );
+
+ // Update editors
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Editors.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Editor,
+ _unitOfWork
+ );
+
+ // Update publishers
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Publishers.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Publisher,
+ _unitOfWork
+ );
+
+ // Update translators
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Translators.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Translator,
+ _unitOfWork
+ );
+
+ // Update imprints
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Imprints.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Imprint,
+ _unitOfWork
+ );
+
+ // Update teams
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Teams.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Team,
+ _unitOfWork
+ );
+
+ // Update locations
+ await PersonHelper.UpdateChapterPeopleAsync(
+ chapter,
+ dto.Locations.Select(p => Parser.Normalize(p.Name)).ToList(),
+ PersonRole.Location,
+ _unitOfWork
+ );
}
#endregion
diff --git a/API/Controllers/ColorScapeController.cs b/API/Controllers/ColorScapeController.cs
index 415f4aad8f..04827658dd 100644
--- a/API/Controllers/ColorScapeController.cs
+++ b/API/Controllers/ColorScapeController.cs
@@ -55,7 +55,6 @@ public async Task> GetColorScapeForChapter(int id)
}
-
private ActionResult GetColorSpaceDto(IHasCoverImage entity)
{
if (entity == null) return Ok(ColorScapeDto.Empty);
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index 6275c6d4cd..011ae471f4 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -45,7 +45,7 @@ public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryServic
///
///
[HttpGet("chapter-cover")]
- [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["chapterId", "apiKey"])]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
public async Task GetChapterCoverImage(int chapterId, string apiKey)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
@@ -130,6 +130,7 @@ public async Task GetCollectionCoverImage(int collectionTagId, str
{
var destFile = await GenerateCollectionCoverImage(collectionTagId);
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
+
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)),
_directoryService.FileSystem.Path.GetFileName(destFile));
}
@@ -170,6 +171,7 @@ private async Task GenerateCollectionCoverImage(int collectionId)
ImageService.GetCollectionTagFormat(collectionId));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
destFile += settings.EncodeMediaAs.GetExtension();
+
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
ImageService.CreateMergedImage(
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
@@ -282,6 +284,43 @@ public async Task GetPublisherImage(string publisherName, string a
return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName));
}
+ ///
+ /// Returns cover image for Person
+ ///
+ ///
+ ///
+ [HttpGet("person-cover")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["personId", "apiKey"])]
+ public async Task GetPersonCoverImage(int personId, string apiKey)
+ {
+ var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
+ if (userId == 0) return BadRequest();
+ var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.PersonRepository.GetCoverImageAsync(personId));
+ if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
+ var format = _directoryService.FileSystem.Path.GetExtension(path);
+
+ return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
+ }
+
+ ///
+ /// Returns cover image for Person
+ ///
+ ///
+ ///
+ [HttpGet("person-cover-by-name")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = ["personId", "apiKey"])]
+ public async Task GetPersonCoverImageByName(string name, string apiKey)
+ {
+ var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
+ if (userId == 0) return BadRequest();
+
+ var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.PersonRepository.GetCoverImageByNameAsync(name));
+ if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
+ var format = _directoryService.FileSystem.Path.GetExtension(path);
+
+ return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
+ }
+
///
/// Returns a temp coverupload image
///
diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs
new file mode 100644
index 0000000000..fb18156bab
--- /dev/null
+++ b/API/Controllers/PersonController.cs
@@ -0,0 +1,116 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.Data;
+using API.DTOs;
+using API.Entities.Enums;
+using API.Extensions;
+using API.Helpers;
+using API.Services;
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Nager.ArticleNumber;
+
+namespace API.Controllers;
+
+public class PersonController : BaseApiController
+{
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly ILocalizationService _localizationService;
+ private readonly IMapper _mapper;
+
+ public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _localizationService = localizationService;
+ _mapper = mapper;
+ }
+
+
+ [HttpGet]
+ public async Task> GetPersonByName(string name)
+ {
+ return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId()));
+ }
+
+ [HttpGet("roles")]
+ public async Task>> GetRolesForPersonByName(string name)
+ {
+ return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(name, User.GetUserId()));
+ }
+
+ ///
+ /// Returns a list of authors for browsing
+ ///
+ ///
+ ///
+ [HttpPost("authors")]
+ public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams)
+ {
+ userParams ??= UserParams.Default;
+ var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams);
+ Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
+ return Ok(list);
+ }
+
+ ///
+ /// Updates the Person
+ ///
+ ///
+ ///
+ [HttpPost("update")]
+ public async Task> UpdatePerson(UpdatePersonDto dto)
+ {
+ // This needs to get all people and update them equally
+ var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id);
+ if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist"));
+
+ dto.Description ??= string.Empty;
+ person.Description = dto.Description;
+ person.CoverImageLocked = dto.CoverImageLocked;
+
+ if (dto.MalId is > 0)
+ {
+ person.MalId = (long) dto.MalId;
+ }
+ if (dto.AniListId is > 0)
+ {
+ person.AniListId = (int) dto.AniListId;
+ }
+
+ if (!string.IsNullOrEmpty(dto.HardcoverId?.Trim()))
+ {
+ person.HardcoverId = dto.HardcoverId.Trim();
+ }
+
+ var asin = dto.Asin?.Trim();
+ if (!string.IsNullOrEmpty(asin) &&
+ (ArticleNumberHelper.IsValidIsbn10(asin) || ArticleNumberHelper.IsValidIsbn13(asin)))
+ {
+ person.Asin = asin;
+ }
+
+ _unitOfWork.PersonRepository.Update(person);
+ await _unitOfWork.CommitAsync();
+
+ return Ok(_mapper.Map(person));
+ }
+
+ ///
+ /// Returns the top 20 series that the "person" is known for. This will use Average Rating when applicable (Kavita+ field), else it's a random sort
+ ///
+ ///
+ ///
+ [HttpGet("series-known-for")]
+ public async Task>> GetKnownSeries(int personId)
+ {
+ return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId));
+ }
+
+ [HttpGet("chapters-by-role")]
+ public async Task>> GetChaptersByRole(int personId, PersonRole role)
+ {
+ return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role));
+ }
+
+
+}
diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs
index e89904deb4..333d4b45fd 100644
--- a/API/Controllers/UploadController.cs
+++ b/API/Controllers/UploadController.cs
@@ -480,5 +480,54 @@ public async Task ResetChapterLock(UploadFileDto uploadFileDto)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
}
+ ///
+ /// Replaces person tag cover image and locks it with a base64 encoded image
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
+ [HttpPost("person")]
+ public async Task UploadPersonCoverImageFromUrl(UploadFileDto uploadFileDto)
+ {
+ // Check if Url is non-empty, request the image and place in temp, then ask image service to handle it.
+ // See if we can do this all in memory without touching underlying system
+ if (string.IsNullOrEmpty(uploadFileDto.Url))
+ {
+ return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
+ }
+
+ try
+ {
+ var person = await _unitOfWork.PersonRepository.GetPersonById(uploadFileDto.Id);
+ if (person == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-doesnt-exist"));
+ var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetPersonFormat(uploadFileDto.Id)}");
+
+ if (!string.IsNullOrEmpty(filePath))
+ {
+ person.CoverImage = filePath;
+ person.CoverImageLocked = true;
+ _imageService.UpdateColorScape(person);
+ _unitOfWork.PersonRepository.Update(person);
+ }
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false);
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue uploading cover image for Person {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-person-save"));
+ }
+
}
diff --git a/API/DTOs/Person/BrowsePersonDto.cs b/API/DTOs/Person/BrowsePersonDto.cs
new file mode 100644
index 0000000000..8d69999737
--- /dev/null
+++ b/API/DTOs/Person/BrowsePersonDto.cs
@@ -0,0 +1,16 @@
+namespace API.DTOs;
+
+///
+/// Used to browse writers and click in to see their series
+///
+public class BrowsePersonDto : PersonDto
+{
+ ///
+ /// Number of Series this Person is the Writer for
+ ///
+ public int SeriesCount { get; set; }
+ ///
+ /// Number or Issues this Person is the Writer for
+ ///
+ public int IssueCount { get; set; }
+}
diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs
new file mode 100644
index 0000000000..aa0f0680cb
--- /dev/null
+++ b/API/DTOs/Person/PersonDto.cs
@@ -0,0 +1,37 @@
+namespace API.DTOs;
+
+public class PersonDto
+{
+ public int Id { get; set; }
+ public required string Name { get; set; }
+
+ public bool CoverImageLocked { get; set; }
+ public string PrimaryColor { get; set; }
+ public string SecondaryColor { get; set; }
+
+ public string? CoverImage { get; set; }
+
+ public string Description { get; set; }
+ ///
+ /// ASIN for person
+ ///
+ /// Can be used for Amazon author lookup
+ public string? Asin { get; set; }
+
+ ///
+ /// https://anilist.co/staff/{AniListId}/
+ ///
+ /// Kavita+ Only
+ public int AniListId { get; set; } = 0;
+ ///
+ /// https://myanimelist.net/people/{MalId}/
+ /// https://myanimelist.net/character/{MalId}/CharacterName
+ ///
+ /// Kavita+ Only
+ public long MalId { get; set; } = 0;
+ ///
+ /// https://hardcover.app/authors/{HardcoverId}
+ ///
+ /// Kavita+ Only
+ public string? HardcoverId { get; set; }
+}
diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs
new file mode 100644
index 0000000000..fe57632577
--- /dev/null
+++ b/API/DTOs/Person/UpdatePersonDto.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace API.DTOs;
+
+public class UpdatePersonDto
+{
+ [Required]
+ public int Id { get; init; }
+ [Required]
+ public bool CoverImageLocked { get; set; }
+ public string? Description { get; set; }
+
+ public int? AniListId { get; set; }
+ public long? MalId { get; set; }
+ public string? HardcoverId { get; set; }
+ public string? Asin { get; set; }
+}
diff --git a/API/DTOs/PersonDto.cs b/API/DTOs/PersonDto.cs
deleted file mode 100644
index 85cc72bb02..0000000000
--- a/API/DTOs/PersonDto.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using API.Entities.Enums;
-
-namespace API.DTOs;
-
-public class PersonDto
-{
- public int Id { get; set; }
- public required string Name { get; set; }
- public PersonRole Role { get; set; }
-}
diff --git a/API/DTOs/StandaloneChapterDto.cs b/API/DTOs/StandaloneChapterDto.cs
new file mode 100644
index 0000000000..6d8b5423d4
--- /dev/null
+++ b/API/DTOs/StandaloneChapterDto.cs
@@ -0,0 +1,14 @@
+using API.Entities.Enums;
+
+namespace API.DTOs;
+
+///
+/// Used on Person Profile page
+///
+public class StandaloneChapterDto : ChapterDto
+{
+ public int SeriesId { get; set; }
+ public int LibraryId { get; set; }
+ public LibraryType LibraryType { get; set; }
+ public string VolumeTitle { get; set; }
+}
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 4af1652495..21b7c26c80 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -66,6 +66,8 @@ public DataContext(DbContextOptions options) : base(options)
public DbSet ManualMigrationHistory { get; set; } = null!;
public DbSet SeriesBlacklist { get; set; } = null!;
public DbSet AppUserCollection { get; set; } = null!;
+ public DbSet ChapterPeople { get; set; } = null!;
+ public DbSet SeriesMetadataPeople { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
@@ -155,6 +157,36 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity()
.Property(b => b.AgeRating)
.HasDefaultValue(AgeRating.Unknown);
+
+ // Configure the many-to-many relationship for Movie and Person
+ builder.Entity()
+ .HasKey(cp => new { cp.ChapterId, cp.PersonId, cp.Role });
+
+ builder.Entity()
+ .HasOne(cp => cp.Chapter)
+ .WithMany(c => c.People)
+ .HasForeignKey(cp => cp.ChapterId);
+
+ builder.Entity()
+ .HasOne(cp => cp.Person)
+ .WithMany(p => p.ChapterPeople)
+ .HasForeignKey(cp => cp.PersonId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+
+ builder.Entity()
+ .HasKey(smp => new { smp.SeriesMetadataId, smp.PersonId, smp.Role });
+
+ builder.Entity()
+ .HasOne(smp => smp.SeriesMetadata)
+ .WithMany(sm => sm.People)
+ .HasForeignKey(smp => smp.SeriesMetadataId);
+
+ builder.Entity()
+ .HasOne(smp => smp.Person)
+ .WithMany(p => p.SeriesMetadataPeople)
+ .HasForeignKey(smp => smp.PersonId)
+ .OnDelete(DeleteBehavior.Cascade);
}
#nullable enable
diff --git a/API/Data/ManualMigrations/MigrateLowestSeriesFolderPath2.cs b/API/Data/ManualMigrations/MigrateLowestSeriesFolderPath2.cs
new file mode 100644
index 0000000000..bb79c33597
--- /dev/null
+++ b/API/Data/ManualMigrations/MigrateLowestSeriesFolderPath2.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using API.Entities;
+using API.Services.Tasks.Scanner.Parser;
+using Kavita.Common.EnvironmentInfo;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace API.Data.ManualMigrations;
+
+///
+/// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users.
+///
+public static class MigrateLowestSeriesFolderPath2
+{
+ public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger logger)
+ {
+ if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLowestSeriesFolderPath2"))
+ {
+ return;
+ }
+
+ logger.LogCritical(
+ "Running MigrateLowestSeriesFolderPath2 migration - Please be patient, this may take some time. This is not an error");
+
+ var series = await dataContext.Series.Where(s => !string.IsNullOrEmpty(s.LowestFolderPath)).ToListAsync();
+ foreach (var s in series)
+ {
+ s.LowestFolderPath = string.Empty;
+ unitOfWork.SeriesRepository.Update(s);
+ }
+
+ // Save changes after processing all series
+ if (dataContext.ChangeTracker.HasChanges())
+ {
+ await dataContext.SaveChangesAsync();
+ }
+
+ dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
+ {
+ Name = "MigrateLowestSeriesFolderPath2",
+ ProductVersion = BuildInfo.Version.ToString(),
+ RanAt = DateTime.UtcNow
+ });
+
+ await dataContext.SaveChangesAsync();
+ logger.LogCritical(
+ "Running MigrateLowestSeriesFolderPath2 migration - Completed. This is not an error");
+ }
+}
diff --git a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs b/API/Data/Migrations/20240704144224_PersonFields.Designer.cs
new file mode 100644
index 0000000000..ddc41d8113
--- /dev/null
+++ b/API/Data/Migrations/20240704144224_PersonFields.Designer.cs
@@ -0,0 +1,3064 @@
+//
+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("20240704144224_PersonFields")]
+ partial class PersonFields
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
+
+ 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("AniListAccessToken")
+ .HasColumnType("TEXT");
+
+ 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("MalAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("MalUserName")
+ .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.AppUserCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ 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("LastSyncUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("MissingSeriesFromSource")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Source")
+ .HasColumnType("INTEGER");
+
+ b.Property("SourceUrl")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalSourceCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserCollection");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsProvided")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartFilterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(4);
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SmartFilterId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("AppUserDashboardStream");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserExternalSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Host")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserExternalSource");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserOnDeckRemoval");
+ });
+
+ 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("BookReaderWritingStyle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("CollapseSeriesRelationships")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("Locale")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("en");
+
+ b.Property("NoTransitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfScrollMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfSpreadMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfTheme")
+ .HasColumnType("INTEGER");
+
+ b.Property("PromptForDownloadSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShareReviews")
+ .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