Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
00bb47b
Fix location locking on any person update. Fixes #4133
Fesaa Oct 25, 2025
4e8f196
Use seperate env var for local K+ server
Fesaa Oct 25, 2025
009e0f0
Fix person cover images not being cleaned up. Fixes #4134
Fesaa Oct 25, 2025
a5c5ec6
Fix missed localised date
Fesaa Oct 26, 2025
e98f64f
Update external IDs tooltip in person id if they're only a character
Fesaa Oct 26, 2025
45d3eac
Fix chapters by role not being sorted correctly when they're volumes …
Fesaa Oct 26, 2025
84b23d2
Fix changing person cover via the UI not doing anything if the new co…
Fesaa Oct 26, 2025
af5158e
Make files at root warning a lot more obvious
Fesaa Oct 26, 2025
dc995f8
Show release year for volume (first chapter) and chapters. Closes #4145
Fesaa Oct 26, 2025
e678ff1
Use ISO 8601 to denote time in backups. Closes #4012
Fesaa Oct 26, 2025
f06f1cf
Add support for default language per library. Closes #4085
Fesaa Oct 26, 2025
771fb5f
Create quick setup function for language type ahead
Fesaa Oct 26, 2025
ad8e722
Rough implementation of custom keybinds
Fesaa Oct 26, 2025
311eeeb
Support several combo's per target, proper codes, better modifier sup…
Fesaa Oct 27, 2025
e30e7c8
Add reserved combo's display warnings/errors
Fesaa Oct 27, 2025
6866201
Document KeyBindService better
Fesaa Oct 27, 2025
f2418e8
Better type safety, filter out default mappings when saving to prefer…
Fesaa Oct 27, 2025
7257f26
Save keybinds smarter, use keys instead of codes
Fesaa Oct 27, 2025
fb9c7e5
Small global notice if there are issus
Fesaa Oct 27, 2025
d1502c1
Use different display string if not on macOS for meta
Fesaa Oct 27, 2025
bc24936
Group keybinds in settings, fix major bugs, extra options in KeyBindE…
Fesaa Oct 27, 2025
8e0eb39
Add error for overlapping keybinds
Fesaa Oct 27, 2025
0aa42b2
Aria labels
Fesaa Oct 27, 2025
72c39e4
Improve performance by only checking for targets that are currently b…
Fesaa Oct 27, 2025
72625cd
Filtering on avaible shortcuts, scrobbling shortcut
Fesaa Oct 27, 2025
b017879
listener options, conditional filtering
Fesaa Oct 27, 2025
62b4c67
Migrate a few image reader options
Fesaa Oct 27, 2025
84fd857
Migrate final key binds in manga reader
Fesaa Oct 27, 2025
4ad6ddb
Migrate other readers
Fesaa Oct 27, 2025
00cbdfb
Fix side nav inplace reordering going haywire if non visible items ar…
Fesaa Oct 27, 2025
3a740b2
Warn for duplicate key binds, change texts a little bit
Fesaa Oct 28, 2025
0fd0166
Disable keybind system while picking new keybinds
Fesaa Oct 28, 2025
d63f80d
Support gamepad keybinds
Fesaa Oct 28, 2025
860537d
Use settings keybind for in reader settings as well
Fesaa Oct 28, 2025
acc3bff
Only check OIDC authority if a value is given. Fixes #4157
Fesaa Oct 29, 2025
5035226
Do not display magic numbers in the publiction status tooltip. Closes…
Fesaa Oct 29, 2025
347ef5b
Fix language typeahead using outdated settings. Fixes #4159
Fesaa Oct 29, 2025
d9155db
Fixed localization keys using incorrect notation to project.
majora2007 Oct 29, 2025
e01f8cc
Update behaviour of keybind settings inputs
Fesaa Oct 30, 2025
a20cffd
Auto focus when adding a new keybind, disable edits when read only
Fesaa Oct 30, 2025
e16a473
Fix tests
Fesaa Oct 30, 2025
2995d88
A bit of cleanup
Fesaa Oct 30, 2025
b14f581
Smallest amount of cleanup
majora2007 Oct 31, 2025
d9e8e1f
Update translation key
Fesaa Oct 31, 2025
4fe05ab
Auto unfocus on valid keybinds after some time has passed without cha…
Fesaa Oct 31, 2025
172b660
Small tweak to help reinforce how to remove a keybind
majora2007 Nov 1, 2025
a5dcc7e
Small tweak
majora2007 Nov 1, 2025
4decc4f
PR Comments
majora2007 Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 15 additions & 105 deletions API.Tests/Repository/SeriesRepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,109 +17,19 @@
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;

namespace API.Tests.Repository;

#nullable enable

public class SeriesRepositoryTests
public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper): AbstractDbTest(testOutputHelper)
{
private readonly IUnitOfWork _unitOfWork;

private readonly DbConnection? _connection;
private readonly DataContext _context;

private const string CacheDirectory = "C:/kavita/config/cache/";
private const string CoverImageDirectory = "C:/kavita/config/covers/";
private const string BackupDirectory = "C:/kavita/config/backups/";
private const string DataDirectory = "C:/data/";

public SeriesRepositoryTests()
{
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;

_context = new DataContext(contextOptions);
Task.Run(SeedDb).GetAwaiter().GetResult();

var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
var mapper = config.CreateMapper();
_unitOfWork = new UnitOfWork(_context, mapper, null!);
}

#region Setup

private static DbConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("Filename=:memory:");

connection.Open();

return connection;
}

private async Task<bool> SeedDb()
{
await _context.Database.MigrateAsync();
var filesystem = CreateFileSystem();

await Seed.SeedSettings(_context,
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));

var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
setting.Value = CacheDirectory;

setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
setting.Value = BackupDirectory;

_context.ServerSetting.Update(setting);

var lib = new LibraryBuilder("Manga")
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
.Build();

_context.AppUser.Add(new AppUser()
{
UserName = "majora2007",
Libraries = new List<Library>()
{
lib
}
});

return await _context.SaveChangesAsync() > 0;
}

private async Task ResetDb()
{
_context.Series.RemoveRange(_context.Series.ToList());
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
_context.Genre.RemoveRange(_context.Genre.ToList());
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
_context.Person.RemoveRange(_context.Person.ToList());

await _context.SaveChangesAsync();
}

private static MockFileSystem CreateFileSystem()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
fileSystem.AddDirectory("C:/kavita/config/");
fileSystem.AddDirectory(CacheDirectory);
fileSystem.AddDirectory(CoverImageDirectory);
fileSystem.AddDirectory(BackupDirectory);
fileSystem.AddDirectory(DataDirectory);

return fileSystem;
}

#endregion

private async Task SetupSeriesData()
private async Task SetupSeriesData(IUnitOfWork unitOfWork)
{
var library = new LibraryBuilder("GetFullSeriesByAnyName Manga", LibraryType.Manga)
.WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build())
.WithFolderPath(new FolderPathBuilder(DataDirectory+"manga/").Build())
.WithSeries(new SeriesBuilder("The Idaten Deities Know Only Peace")
.WithLocalizedName("Heion Sedai no Idaten-tachi")
.WithFormat(MangaFormat.Archive)
Expand All @@ -130,8 +40,8 @@ private async Task SetupSeriesData()
.Build())
.Build();

_unitOfWork.LibraryRepository.Add(library);
await _unitOfWork.CommitAsync();
unitOfWork.LibraryRepository.Add(library);
await unitOfWork.CommitAsync();
}


Expand All @@ -142,11 +52,11 @@ private async Task SetupSeriesData()
[InlineData("Hitomi-chan wa Hitomishiri", MangaFormat.Archive, "", "Hitomi-chan is Shy With Strangers")]
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
{
await ResetDb();
await SetupSeriesData();
var (unitOfWork, _, _) = await CreateDatabase();
await SetupSeriesData(unitOfWork);

var series =
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
await unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
2, format, false);
if (expected == null)
{
Expand All @@ -165,8 +75,8 @@ await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedN
[InlineData(0, "", null)] // Case 3: Return null if neither exist
public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int externalAniListId, string? webLinks, int? expectedAniListId)
{
// Arrange
await ResetDb();
var (unitOfWork, _, _) = await CreateDatabase();
await SetupSeriesData(unitOfWork);

var series = new SeriesBuilder("Test Series")
.WithFormat(MangaFormat.Archive)
Expand Down Expand Up @@ -195,12 +105,12 @@ public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int exte
ReleaseYear = 2021
};

_unitOfWork.LibraryRepository.Add(library);
_unitOfWork.SeriesRepository.Add(series);
await _unitOfWork.CommitAsync();
unitOfWork.LibraryRepository.Add(library);
unitOfWork.SeriesRepository.Add(series);
await unitOfWork.CommitAsync();

// Act
var result = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id);
var result = await unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id);

// Assert
Assert.NotNull(result);
Expand Down
18 changes: 8 additions & 10 deletions API/Controllers/LibraryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,15 @@ public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string? path)
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("has-files-at-root")]
public ActionResult<IDictionary<string, bool>> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto)
public ActionResult<IList<string>> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto)
{
var results = new Dictionary<string, bool>();
foreach (var root in dto.Roots)
{
results.TryAdd(root,
_directoryService
.GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly)
.Any());
}
var foldersWithFilesAtRoot = dto.Roots
.Where(root => _directoryService
.GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly)
.Any())
.ToList();

return Ok(results);
return Ok(foldersWithFilesAtRoot);
}

/// <summary>
Expand Down Expand Up @@ -658,6 +655,7 @@ private void UpdateLibrarySettings(UpdateLibraryDto dto, Library library, bool u
library.EnableMetadata = dto.EnableMetadata;
library.RemovePrefixForSortName = dto.RemovePrefixForSortName;
library.InheritWebLinksFromFirstChapter = dto.InheritWebLinksFromFirstChapter;
library.DefaultLanguage = dto.DefaultLanguage;

library.LibraryFileTypes = dto.FileGroupTypes
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
Expand Down
11 changes: 9 additions & 2 deletions API/Controllers/UploadController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Flurl.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;

namespace API.Controllers;
Expand Down Expand Up @@ -62,9 +63,15 @@ public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILog
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
{
var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace('/', '_').Replace(':', '_');
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
try
{
var format = await dto.Url.GetFileFormatAsync();
if (string.IsNullOrEmpty(format))
{
// Fallback to unreliable parsing if needed
format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
}

var path = await dto.Url
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");

Expand Down Expand Up @@ -499,7 +506,7 @@ public async Task<ActionResult> UploadPersonCoverImageFromUrl(UploadFileDto uplo
var person = await _unitOfWork.PersonRepository.GetPersonById(uploadFileDto.Id);
if (person == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-doesnt-exist"));

await _coverDbService.SetPersonCoverByUrl(person, uploadFileDto.Url, true);
await _coverDbService.SetPersonCoverByUrl(person, uploadFileDto.Url, chooseBetterImage:false);
return Ok();
}
catch (Exception e)
Expand Down
1 change: 1 addition & 0 deletions API/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPrefer
existingPreferences.ColorScapeEnabled = preferencesDto.ColorScapeEnabled;
existingPreferences.BookReaderHighlightSlots = preferencesDto.BookReaderHighlightSlots;
existingPreferences.DataSaver = preferencesDto.DataSaver;
existingPreferences.CustomKeyBinds = preferencesDto.CustomKeyBinds;

var allLibs = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id))
.Select(l => l.Id).ToList();
Expand Down
8 changes: 6 additions & 2 deletions API/DTOs/Dashboard/UpdateStreamPositionDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

public sealed record UpdateStreamPositionDto
{
public string StreamName { get; set; }
public int Id { get; set; }
public int FromPosition { get; set; }
public int ToPosition { get; set; }
public int Id { get; set; }
public string StreamName { get; set; }
/// <summary>
/// If the <see cref="ToPosition"/> has taken into account non-visible items
/// </summary>
public bool PositionIncludesInvisible { get; set; }
}
2 changes: 2 additions & 0 deletions API/DTOs/LibraryDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,6 @@ public sealed record LibraryDto
public bool RemovePrefixForSortName { get; set; } = false;
/// <inheritdoc cref="Library.InheritWebLinksFromFirstChapter"/>
public bool InheritWebLinksFromFirstChapter { get; init; }
/// <inheritdoc cref="Library.DefaultLanguage"/>
public string DefaultLanguage { get; init; }
}
2 changes: 2 additions & 0 deletions API/DTOs/UpdateLibraryDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public sealed record UpdateLibraryDto
/// <inheritdoc cref="Library.InheritWebLinksFromFirstChapter"/>
[Required]
public bool InheritWebLinksFromFirstChapter { get; init; }
/// <inheritdoc cref="Library.DefaultLanguage"/>
public string DefaultLanguage { get; init; }
/// <summary>
/// What types of files to allow the scanner to pickup
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions API/DTOs/UserPreferencesDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public sealed record UserPreferencesDto
/// <inheritdoc cref="API.Entities.AppUserPreferences.DataSaver"/>
[Required]
public bool DataSaver { get; set; } = false;
/// <inheritdoc cref="API.Entities.AppUserPreferences.CustomKeyBinds"/>
[Required]
public Dictionary<KeyBindTarget, IList<KeyBind>> CustomKeyBinds { get; set; } = [];

/// <inheritdoc cref="API.Entities.AppUserPreferences.AniListScrobblingEnabled"/>
public bool AniListScrobblingEnabled { get; set; }
Expand Down
9 changes: 9 additions & 0 deletions API/Data/DataContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<Library>()
.Property(b => b.EnableMetadata)
.HasDefaultValue(true);
builder.Entity<Library>()
.Property(l => l.DefaultLanguage)
.HasDefaultValue(string.Empty);

builder.Entity<Chapter>()
.Property(b => b.WebLinks)
Expand Down Expand Up @@ -293,6 +296,12 @@ protected override void OnModelCreating(ModelBuilder builder)
.HasColumnType("TEXT")
.HasDefaultValue(new List<HighlightSlot>());

builder.Entity<AppUserPreferences>()
.Property(p => p.CustomKeyBinds)
.HasJsonConversion([])
.HasColumnType("TEXT")
.HasDefaultValue(new Dictionary<KeyBindTarget, IList<KeyBind>>());

builder.Entity<AppUser>()
.Property(user => user.IdentityProvider)
.HasDefaultValue(IdentityProvider.Kavita);
Expand Down
Loading
Loading