Skip to content

Commit

Permalink
Switched the code to find a Series for a relationship to be more robust.
Browse files Browse the repository at this point in the history
Fixed a bug where something went wrong due to a Kavita+ call happening for non-admins.
  • Loading branch information
majora2007 committed Feb 13, 2025
1 parent f2b49d3 commit 719eebf
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 24 deletions.
2 changes: 2 additions & 0 deletions API.Tests/AbstractDbTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ protected AbstractDbTest()
{
var contextOptions = new DbContextOptionsBuilder<DataContext>()
.UseSqlite(CreateInMemoryDatabase())
.EnableSensitiveDataLogging()
.Options;

_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
Expand Down Expand Up @@ -93,6 +94,7 @@ private async Task<bool> SeedDb()


_context.Library.Add(new LibraryBuilder("Manga")
.WithAllowMetadataMatching(true)
.WithFolderPath(new FolderPathBuilder(DataDirectory).Build())
.Build());

Expand Down
247 changes: 244 additions & 3 deletions API.Tests/Services/ExternalMetadataServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using API.Data.Repositories;
using API.DTOs.KavitaPlus.Metadata;
using API.DTOs.Recommendation;
using API.DTOs.Scrobbling;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
Expand All @@ -15,6 +16,7 @@
using API.Services.Tasks.Metadata;
using API.SignalR;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
Expand Down Expand Up @@ -1974,8 +1976,246 @@ await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesD

#region Relationships

// Not enabled

// Non-Sequel

[Fact]
public async Task Relationships_NonSequel()
{
await ResetDb();

const string seriesName = "Test - Relationships Side Story";
var series = new SeriesBuilder(seriesName)
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series);

var series2 = new SeriesBuilder("Test - Relationships Side Story - Target")
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.WithExternalMetadata(new ExternalSeriesMetadata()
{
AniListId = 10
})
.Build();
_context.Series.Attach(series2);
await _context.SaveChangesAsync();

var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings();
metadataSettings.Enabled = true;
metadataSettings.EnableRelationships = true;
_context.MetadataSettings.Update(metadataSettings);
await _context.SaveChangesAsync();

await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto()
{
Name = seriesName,
Relations = [new SeriesRelationship()
{
Relation = RelationKind.SideStory,
SeriesName = new ALMediaTitle()
{
PreferredTitle = series2.Name,
EnglishTitle = null,
NativeTitle = series2.Name,
RomajiTitle = series2.Name,
},
AniListId = 10,
PlusMediaFormat = PlusMediaFormat.Manga
}]
}, 1);

// Repull Series and validate what is overwritten
var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related);
Assert.NotNull(sourceSeries);
Assert.Single(sourceSeries.Relations);
Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name);
}

[Fact]
public async Task Relationships_NonSequel_LocalizedName()
{
await ResetDb();

const string seriesName = "Test - Relationships Side Story";
var series = new SeriesBuilder(seriesName)
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series);

var series2 = new SeriesBuilder("Test - Relationships Side Story - Target")
.WithLibraryId(1)
.WithLocalizedName("School bus")
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series2);
await _context.SaveChangesAsync();

var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings();
metadataSettings.Enabled = true;
metadataSettings.EnableRelationships = true;
_context.MetadataSettings.Update(metadataSettings);
await _context.SaveChangesAsync();

await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto()
{
Name = seriesName,
Relations = [new SeriesRelationship()
{
Relation = RelationKind.SideStory,
SeriesName = new ALMediaTitle()
{
PreferredTitle = "School bus",
EnglishTitle = null,
NativeTitle = series2.Name,
RomajiTitle = series2.Name,
},
AniListId = 10,
PlusMediaFormat = PlusMediaFormat.Manga
}]
}, 1);

// Repull Series and validate what is overwritten
var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related);
Assert.NotNull(sourceSeries);
Assert.Single(sourceSeries.Relations);
Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name);
}

// Non-Sequel with no match due to Format difference
[Fact]
public async Task Relationships_NonSequel_FormatDifference()
{
await ResetDb();

const string seriesName = "Test - Relationships Side Story";
var series = new SeriesBuilder(seriesName)
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series);

var series2 = new SeriesBuilder("Test - Relationships Side Story - Target")
.WithLibraryId(1)
.WithLocalizedName("School bus")
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series2);
await _context.SaveChangesAsync();

var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings();
metadataSettings.Enabled = true;
metadataSettings.EnableRelationships = true;
_context.MetadataSettings.Update(metadataSettings);
await _context.SaveChangesAsync();

await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto()
{
Name = seriesName,
Relations = [new SeriesRelationship()
{
Relation = RelationKind.SideStory,
SeriesName = new ALMediaTitle()
{
PreferredTitle = "School bus",
EnglishTitle = null,
NativeTitle = series2.Name,
RomajiTitle = series2.Name,
},
AniListId = 10,
PlusMediaFormat = PlusMediaFormat.Book
}]
}, 1);

// Repull Series and validate what is overwritten
var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related);
Assert.NotNull(sourceSeries);
Assert.Empty(sourceSeries.Relations);
}

// Non-Sequel existing relationship with new link, both exist
[Fact]
public async Task Relationships_NonSequel_ExistingLink_DifferentType()
{
await ResetDb();

var existingRelationshipSeries = new SeriesBuilder("Existing")
.WithLibraryId(1)
.Build();
_context.Series.Attach(existingRelationshipSeries);
await _context.SaveChangesAsync();

const string seriesName = "Test - Relationships Side Story";
var series = new SeriesBuilder(seriesName)
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithRelationship(existingRelationshipSeries.Id, RelationKind.Annual)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.Build();
_context.Series.Attach(series);

var series2 = new SeriesBuilder("Test - Relationships Side Story - Target")
.WithLibraryId(1)
.WithFormat(MangaFormat.Archive)
.WithMetadata(new SeriesMetadataBuilder()
.Build())
.WithExternalMetadata(new ExternalSeriesMetadata()
{
AniListId = 10
})
.Build();
_context.Series.Attach(series2);
await _context.SaveChangesAsync();

var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings();
metadataSettings.Enabled = true;
metadataSettings.EnableRelationships = true;
_context.MetadataSettings.Update(metadataSettings);
await _context.SaveChangesAsync();

await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto()
{
Name = seriesName,
Relations = [new SeriesRelationship()
{
Relation = RelationKind.SideStory,
SeriesName = new ALMediaTitle()
{
PreferredTitle = series2.Name,
EnglishTitle = null,
NativeTitle = series2.Name,
RomajiTitle = series2.Name,
},
AniListId = 10,
PlusMediaFormat = PlusMediaFormat.Manga
}]
}, 1);

// Repull Series and validate what is overwritten
var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related);
Assert.NotNull(sourceSeries);
Assert.Contains(sourceSeries.Relations, r => r.RelationKind == RelationKind.Annual);
Assert.Contains(sourceSeries.Relations, r => r.RelationKind == RelationKind.SideStory);
}



// Sequel/Prequel


Expand Down Expand Up @@ -2433,8 +2673,6 @@ await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesD





protected override async Task ResetDb()
{
_context.Series.RemoveRange(_context.Series);
Expand All @@ -2458,7 +2696,10 @@ protected override async Task ResetDb()

await _context.SaveChangesAsync();

_context.AppUser.Add(new AppUserBuilder("Joe", "Joe").WithRole(PolicyConstants.AdminRole).Build());
_context.AppUser.Add(new AppUserBuilder("Joe", "Joe")
.WithRole(PolicyConstants.AdminRole)
.WithLibrary(await _context.Library.FirstAsync(l => l.Id == 1))
.Build());

// Create a bunch of Genres for this test and store their string in _genreLookup
_genreLookup.Clear();
Expand Down
9 changes: 8 additions & 1 deletion API/Controllers/LicenseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ public async Task<ActionResult<bool>> HasLicense()
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
public async Task<ActionResult<LicenseInfoDto?>> GetLicenseInfo(bool forceCheck = false)
{
return Ok(await licenseService.GetLicenseInfo(forceCheck));
try
{
return Ok(await licenseService.GetLicenseInfo(forceCheck));
}
catch (Exception)
{
return Ok(null);
}
}

[Authorize("RequireAdminRole")]
Expand Down
38 changes: 38 additions & 0 deletions API/Data/Repositories/SeriesRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ public interface ISeriesRepository
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
int userId, SeriesIncludes includes = SeriesIncludes.None);
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);

Task<Series?> GetSeriesByAnyName(IList<string> names, IList<MangaFormat> formats,
int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None);
Task<Series?> GetSeriesByAnyName(string seriesName, string localizedName, IList<MangaFormat> formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None);
public Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
MangaFormat format);
Expand Down Expand Up @@ -1757,6 +1760,41 @@ public async Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> nor
.FirstOrDefaultAsync();
}


public async Task<Series?> GetSeriesByAnyName(IList<string> names, IList<MangaFormat> formats,
int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None)
{
var libraryIds = GetLibraryIdsForUser(userId);
names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList();
var normalizedNames = names.Select(s => s.ToNormalized()).ToList();


var query = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Where(s => formats.Contains(s.Format));

if (aniListId.HasValue && aniListId.Value > 0)
{
// If AniList ID is provided, override name checks
query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value ||
normalizedNames.Contains(s.NormalizedName)
|| normalizedNames.Contains(s.NormalizedLocalizedName)
|| names.Contains(s.OriginalName));
}
else
{
// Otherwise, use name checks
query = query.Where(s =>
normalizedNames.Contains(s.NormalizedName)
|| normalizedNames.Contains(s.NormalizedLocalizedName)
|| names.Contains(s.OriginalName));
}

return await query
.Includes(includes)
.FirstOrDefaultAsync();
}

public async Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
MangaFormat format)
{
Expand Down
12 changes: 12 additions & 0 deletions API/Helpers/Builders/SeriesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,16 @@ public SeriesBuilder WithExternalMetadata(ExternalSeriesMetadata metadata)
}


public SeriesBuilder WithRelationship(int targetSeriesId, RelationKind kind)
{
_series.Relations ??= [];
_series.Relations.Add(new SeriesRelation()
{
SeriesId = _series.Id,
RelationKind = kind,
TargetSeriesId = targetSeriesId
});

return this;
}
}
Loading

0 comments on commit 719eebf

Please sign in to comment.