Skip to content

Commit 1978836

Browse files
Fesaamajora2007
andcommitted
Custom keybinds, Default language per Library, and bugfixes (#4162)
Co-authored-by: Joseph Milazzo <[email protected]>
1 parent 8a01b02 commit 1978836

File tree

73 files changed

+6037
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+6037
-440
lines changed

API.Tests/Repository/SeriesRepositoryTests.cs

Lines changed: 15 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -17,109 +17,19 @@
1717
using Microsoft.Extensions.Logging;
1818
using NSubstitute;
1919
using Xunit;
20+
using Xunit.Abstractions;
2021

2122
namespace API.Tests.Repository;
2223

2324
#nullable enable
2425

25-
public class SeriesRepositoryTests
26+
public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper): AbstractDbTest(testOutputHelper)
2627
{
27-
private readonly IUnitOfWork _unitOfWork;
2828

29-
private readonly DbConnection? _connection;
30-
private readonly DataContext _context;
31-
32-
private const string CacheDirectory = "C:/kavita/config/cache/";
33-
private const string CoverImageDirectory = "C:/kavita/config/covers/";
34-
private const string BackupDirectory = "C:/kavita/config/backups/";
35-
private const string DataDirectory = "C:/data/";
36-
37-
public SeriesRepositoryTests()
38-
{
39-
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
40-
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
41-
42-
_context = new DataContext(contextOptions);
43-
Task.Run(SeedDb).GetAwaiter().GetResult();
44-
45-
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
46-
var mapper = config.CreateMapper();
47-
_unitOfWork = new UnitOfWork(_context, mapper, null!);
48-
}
49-
50-
#region Setup
51-
52-
private static DbConnection CreateInMemoryDatabase()
53-
{
54-
var connection = new SqliteConnection("Filename=:memory:");
55-
56-
connection.Open();
57-
58-
return connection;
59-
}
60-
61-
private async Task<bool> SeedDb()
62-
{
63-
await _context.Database.MigrateAsync();
64-
var filesystem = CreateFileSystem();
65-
66-
await Seed.SeedSettings(_context,
67-
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
68-
69-
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
70-
setting.Value = CacheDirectory;
71-
72-
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
73-
setting.Value = BackupDirectory;
74-
75-
_context.ServerSetting.Update(setting);
76-
77-
var lib = new LibraryBuilder("Manga")
78-
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
79-
.Build();
80-
81-
_context.AppUser.Add(new AppUser()
82-
{
83-
UserName = "majora2007",
84-
Libraries = new List<Library>()
85-
{
86-
lib
87-
}
88-
});
89-
90-
return await _context.SaveChangesAsync() > 0;
91-
}
92-
93-
private async Task ResetDb()
94-
{
95-
_context.Series.RemoveRange(_context.Series.ToList());
96-
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
97-
_context.Genre.RemoveRange(_context.Genre.ToList());
98-
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
99-
_context.Person.RemoveRange(_context.Person.ToList());
100-
101-
await _context.SaveChangesAsync();
102-
}
103-
104-
private static MockFileSystem CreateFileSystem()
105-
{
106-
var fileSystem = new MockFileSystem();
107-
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
108-
fileSystem.AddDirectory("C:/kavita/config/");
109-
fileSystem.AddDirectory(CacheDirectory);
110-
fileSystem.AddDirectory(CoverImageDirectory);
111-
fileSystem.AddDirectory(BackupDirectory);
112-
fileSystem.AddDirectory(DataDirectory);
113-
114-
return fileSystem;
115-
}
116-
117-
#endregion
118-
119-
private async Task SetupSeriesData()
29+
private async Task SetupSeriesData(IUnitOfWork unitOfWork)
12030
{
12131
var library = new LibraryBuilder("GetFullSeriesByAnyName Manga", LibraryType.Manga)
122-
.WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build())
32+
.WithFolderPath(new FolderPathBuilder(DataDirectory+"manga/").Build())
12333
.WithSeries(new SeriesBuilder("The Idaten Deities Know Only Peace")
12434
.WithLocalizedName("Heion Sedai no Idaten-tachi")
12535
.WithFormat(MangaFormat.Archive)
@@ -130,8 +40,8 @@ private async Task SetupSeriesData()
13040
.Build())
13141
.Build();
13242

133-
_unitOfWork.LibraryRepository.Add(library);
134-
await _unitOfWork.CommitAsync();
43+
unitOfWork.LibraryRepository.Add(library);
44+
await unitOfWork.CommitAsync();
13545
}
13646

13747

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

14858
var series =
149-
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
59+
await unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
15060
2, format, false);
15161
if (expected == null)
15262
{
@@ -165,8 +75,8 @@ await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedN
16575
[InlineData(0, "", null)] // Case 3: Return null if neither exist
16676
public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int externalAniListId, string? webLinks, int? expectedAniListId)
16777
{
168-
// Arrange
169-
await ResetDb();
78+
var (unitOfWork, _, _) = await CreateDatabase();
79+
await SetupSeriesData(unitOfWork);
17080

17181
var series = new SeriesBuilder("Test Series")
17282
.WithFormat(MangaFormat.Archive)
@@ -195,12 +105,12 @@ public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int exte
195105
ReleaseYear = 2021
196106
};
197107

198-
_unitOfWork.LibraryRepository.Add(library);
199-
_unitOfWork.SeriesRepository.Add(series);
200-
await _unitOfWork.CommitAsync();
108+
unitOfWork.LibraryRepository.Add(library);
109+
unitOfWork.SeriesRepository.Add(series);
110+
await unitOfWork.CommitAsync();
201111

202112
// Act
203-
var result = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id);
113+
var result = await unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id);
204114

205115
// Assert
206116
Assert.NotNull(result);

API/Controllers/LibraryController.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,18 +182,15 @@ public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string? path)
182182
/// <returns></returns>
183183
[Authorize(Policy = "RequireAdminRole")]
184184
[HttpPost("has-files-at-root")]
185-
public ActionResult<IDictionary<string, bool>> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto)
185+
public ActionResult<IList<string>> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto)
186186
{
187-
var results = new Dictionary<string, bool>();
188-
foreach (var root in dto.Roots)
189-
{
190-
results.TryAdd(root,
191-
_directoryService
192-
.GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly)
193-
.Any());
194-
}
187+
var foldersWithFilesAtRoot = dto.Roots
188+
.Where(root => _directoryService
189+
.GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly)
190+
.Any())
191+
.ToList();
195192

196-
return Ok(results);
193+
return Ok(foldersWithFilesAtRoot);
197194
}
198195

199196
/// <summary>
@@ -658,6 +655,7 @@ private void UpdateLibrarySettings(UpdateLibraryDto dto, Library library, bool u
658655
library.EnableMetadata = dto.EnableMetadata;
659656
library.RemovePrefixForSortName = dto.RemovePrefixForSortName;
660657
library.InheritWebLinksFromFirstChapter = dto.InheritWebLinksFromFirstChapter;
658+
library.DefaultLanguage = dto.DefaultLanguage;
661659

662660
library.LibraryFileTypes = dto.FileGroupTypes
663661
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})

API/Controllers/UploadController.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Flurl.Http;
1515
using Microsoft.AspNetCore.Authorization;
1616
using Microsoft.AspNetCore.Mvc;
17+
using Microsoft.AspNetCore.StaticFiles;
1718
using Microsoft.Extensions.Logging;
1819

1920
namespace API.Controllers;
@@ -62,9 +63,15 @@ public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILog
6263
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
6364
{
6465
var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace('/', '_').Replace(':', '_');
65-
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
6666
try
6767
{
68+
var format = await dto.Url.GetFileFormatAsync();
69+
if (string.IsNullOrEmpty(format))
70+
{
71+
// Fallback to unreliable parsing if needed
72+
format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
73+
}
74+
6875
var path = await dto.Url
6976
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
7077

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

502-
await _coverDbService.SetPersonCoverByUrl(person, uploadFileDto.Url, true);
509+
await _coverDbService.SetPersonCoverByUrl(person, uploadFileDto.Url, chooseBetterImage: false);
503510
return Ok();
504511
}
505512
catch (Exception e)

API/Controllers/UsersController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPrefer
121121
existingPreferences.ColorScapeEnabled = preferencesDto.ColorScapeEnabled;
122122
existingPreferences.BookReaderHighlightSlots = preferencesDto.BookReaderHighlightSlots;
123123
existingPreferences.DataSaver = preferencesDto.DataSaver;
124+
existingPreferences.CustomKeyBinds = preferencesDto.CustomKeyBinds;
124125

125126
var allLibs = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id))
126127
.Select(l => l.Id).ToList();

API/DTOs/Dashboard/UpdateStreamPositionDto.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
public sealed record UpdateStreamPositionDto
44
{
5+
public string StreamName { get; set; }
6+
public int Id { get; set; }
57
public int FromPosition { get; set; }
68
public int ToPosition { get; set; }
7-
public int Id { get; set; }
8-
public string StreamName { get; set; }
9+
/// <summary>
10+
/// If the <see cref="ToPosition"/> has taken into account non-visible items
11+
/// </summary>
12+
public bool PositionIncludesInvisible { get; set; }
913
}

API/DTOs/LibraryDto.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,6 @@ public sealed record LibraryDto
7777
public bool RemovePrefixForSortName { get; set; } = false;
7878
/// <inheritdoc cref="Library.InheritWebLinksFromFirstChapter"/>
7979
public bool InheritWebLinksFromFirstChapter { get; init; }
80+
/// <inheritdoc cref="Library.DefaultLanguage"/>
81+
public string DefaultLanguage { get; init; }
8082
}

API/DTOs/UpdateLibraryDto.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public sealed record UpdateLibraryDto
4848
/// <inheritdoc cref="Library.InheritWebLinksFromFirstChapter"/>
4949
[Required]
5050
public bool InheritWebLinksFromFirstChapter { get; init; }
51+
/// <inheritdoc cref="Library.DefaultLanguage"/>
52+
public string DefaultLanguage { get; init; }
5153
/// <summary>
5254
/// What types of files to allow the scanner to pickup
5355
/// </summary>

API/DTOs/UserPreferencesDto.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public sealed record UserPreferencesDto
4040
/// <inheritdoc cref="API.Entities.AppUserPreferences.DataSaver"/>
4141
[Required]
4242
public bool DataSaver { get; set; } = false;
43+
/// <inheritdoc cref="API.Entities.AppUserPreferences.CustomKeyBinds"/>
44+
[Required]
45+
public Dictionary<KeyBindTarget, IList<KeyBind>> CustomKeyBinds { get; set; } = [];
4346

4447
/// <inheritdoc cref="API.Entities.AppUserPreferences.AniListScrobblingEnabled"/>
4548
public bool AniListScrobblingEnabled { get; set; }

API/Data/DataContext.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ protected override void OnModelCreating(ModelBuilder builder)
155155
builder.Entity<Library>()
156156
.Property(b => b.EnableMetadata)
157157
.HasDefaultValue(true);
158+
builder.Entity<Library>()
159+
.Property(l => l.DefaultLanguage)
160+
.HasDefaultValue(string.Empty);
158161

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

299+
builder.Entity<AppUserPreferences>()
300+
.Property(p => p.CustomKeyBinds)
301+
.HasJsonConversion([])
302+
.HasColumnType("TEXT")
303+
.HasDefaultValue(new Dictionary<KeyBindTarget, IList<KeyBind>>());
304+
296305
builder.Entity<AppUser>()
297306
.Property(user => user.IdentityProvider)
298307
.HasDefaultValue(IdentityProvider.Kavita);

0 commit comments

Comments
 (0)