Misc Enhancements (#1525)

* Moved the data connection for the Database out of appsettings.json and hardcoded it. This will allow for more customization and cleaner update process.

* Removed unneeded code

* Updated pdf viewer to 15.0.0 (pdf 2.6), which now supports east-asian fonts

* Fixed up some regex parsing for volumes that have a float number.

* Fixed a bug where the tooltip for Publication Status wouldn't show

* Fixed some weird parsing rules where v1.1 would parse as volume 1 chapter 1

* Fixed a bug where bookmarking button was hidden for admins without bookmark role (due to migration)

* Unified the star rating component in series detail to match metadata filter.

* Fixed a bug in the bulk selection code when using shift selection, where the inverse of what was selected would be toggled.

* Fixed some old code where if on all series page, only English as a language would return. We now return all languages of all libraries.

* Updated api/metadata/languages documentation

* Refactored some bookmark api names: get-bookmarks -> chapter-bookmarks, get-all-bookmarks -> all-bookmarks, get-series-bookmarks -> series-bookmarks, etc.

* Refactored all cases of createSeriesFilter to filterUtiltityService.

Added ability to search for a series on Bookmarks page.

Fixed a bug where people filters wouldn't respect the disable flag froms ettings.

* Cleaned up a bit of the circular downloader code.

* Implemented Russian Parsing

* Fixed an issue where some users that had a missing theme entry wouldn't be able to update their user preferences.

* Refactored normalization to exclude !, thus allowing series with ! to be different from each other.

* Fixed a migration exit case

* Fixed broken unit test
This commit is contained in:
Joseph Milazzo 2022-09-13 18:59:26 -05:00 committed by GitHub
parent b7d88f08d8
commit 00f0ad5a3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 508 additions and 419 deletions

View file

@ -115,8 +115,9 @@ public class MetadataController : BaseApiController
}
/// <summary>
/// Fetches all age ratings from the instance
/// Fetches all age languages from the libraries passed (or if none passed, all in the server)
/// </summary>
/// <remarks>This does not perform RBS for the user if they have Library access due to the non-sensitive nature of languages</remarks>
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
/// <returns></returns>
[HttpGet("languages")]
@ -128,15 +129,8 @@ public class MetadataController : BaseApiController
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
}
var englishTag = CultureInfo.GetCultureInfo("en");
return Ok(new List<LanguageDto>()
{
new ()
{
Title = englishTag.DisplayName,
IsoCode = englishTag.IetfLanguageTag
}
});
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync());
}
[HttpGet("all-languages")]

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Reader;
using API.Entities;
using API.Entities.Enums;
@ -529,7 +530,7 @@ public class ReaderController : BaseApiController
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("get-bookmarks")]
[HttpGet("chapter-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
@ -540,13 +541,15 @@ public class ReaderController : BaseApiController
/// <summary>
/// Returns a list of all bookmarked pages for a User
/// </summary>
/// <param name="filterDto">Only supports SeriesNameQuery</param>
/// <returns></returns>
[HttpGet("get-all-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
[HttpPost("all-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
}
/// <summary>
@ -629,7 +632,7 @@ public class ReaderController : BaseApiController
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[HttpGet("get-volume-bookmarks")]
[HttpGet("volume-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
@ -642,7 +645,7 @@ public class ReaderController : BaseApiController
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("get-series-bookmarks")]
[HttpGet("series-bookmarks")]
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);

View file

@ -78,6 +78,8 @@ public class UsersController : BaseApiController
AppUserIncludes.UserPreferences);
var existingPreferences = user.UserPreferences;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
@ -92,7 +94,6 @@ public class UsersController : BaseApiController
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;

View file

@ -118,5 +118,4 @@ public class ServerInfoDto
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool UsingSeriesRelationships { get; set; }
}

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using API.Data;
using API.DTOs.Theme;
using API.Entities;
using API.Entities.Enums;
@ -83,11 +84,11 @@ public class UserPreferencesDto
/// </summary>
[Required]
public ReadingDirection BookReaderReadingDirection { get; set; }
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
[Required]
public SiteTheme Theme { get; set; }
[Required]
public string BookReaderThemeName { get; set; }

View file

@ -1,181 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Helpers;
using API.Services;
using Microsoft.EntityFrameworkCore;
namespace API.Data;
/// <summary>
/// A data structure to migrate Cover Images from byte[] to files.
/// </summary>
internal class CoverMigration
{
public string Id { get; set; }
public byte[] CoverImage { get; set; }
public string ParentId { get; set; }
}
/// <summary>
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
/// </summary>
public static class MigrateCoverImages
{
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
/// <summary>
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
/// </summary>
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
{
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
Console.WriteLine("Extracting cover images for Series");
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = "0"
});
foreach (var series in lockedSeries)
{
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(series.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Chapters");
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = x[2] + string.Empty
});
foreach (var chapter in chapters)
{
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
try
{
var stream = new MemoryStream(chapter.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Collection Tags");
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1] ,
ParentId = "0"
});
foreach (var tag in tags)
{
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(tag.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
/// <summary>
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
/// </summary>
/// <param name="context"></param>
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
{
Console.WriteLine("Updating Series entities");
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
foreach (var series in seriesCovers)
{
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Chapter entities");
var chapters = await context.Chapter.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var chapter in chapters)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
{
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Volume entities");
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
foreach (var volume in volumes)
{
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
if (firstChapter == null) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
{
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Collection Tag entities");
var tags = await context.CollectionTag.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var tag in tags)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
{
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Cover Image Migration completed");
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data;
/// <summary>
/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated
/// </summary>
public static class MigrateNormalizedEverything
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
// if current version is > 0.5.6.3, then we can exit and not perform
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 3))
{
return;
}
logger.LogCritical("Running MigrateNormalizedEverything migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database");
logger.LogInformation("Updating Normalization on Series...");
foreach (var series in await dataContext.Series.ToListAsync())
{
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty);
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name ?? string.Empty);
logger.LogInformation("Updated Series: {SeriesName}", series.Name);
unitOfWork.SeriesRepository.Update(series);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Series...Done");
// Genres
logger.LogInformation("Updating Normalization on Genres...");
foreach (var genre in await dataContext.Genre.ToListAsync())
{
genre.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title ?? string.Empty);
logger.LogInformation("Updated Genre: {Genre}", genre.Title);
unitOfWork.GenreRepository.Attach(genre);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Genres...Done");
// Tags
logger.LogInformation("Updating Normalization on Tags...");
foreach (var tag in await dataContext.Tag.ToListAsync())
{
tag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title ?? string.Empty);
logger.LogInformation("Updated Tag: {Tag}", tag.Title);
unitOfWork.TagRepository.Attach(tag);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Tags...Done");
// People
logger.LogInformation("Updating Normalization on People...");
foreach (var person in await dataContext.Person.ToListAsync())
{
person.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name ?? string.Empty);
logger.LogInformation("Updated Person: {Person}", person.Name);
unitOfWork.PersonRepository.Attach(person);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on People...Done");
// Collections
logger.LogInformation("Updating Normalization on Collections...");
foreach (var collection in await dataContext.CollectionTag.ToListAsync())
{
collection.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(collection.Title ?? string.Empty);
logger.LogInformation("Updated Collection: {Collection}", collection.Title);
unitOfWork.CollectionTagRepository.Update(collection);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Collections...Done");
// Reading Lists
logger.LogInformation("Updating Normalization on Reading Lists...");
foreach (var readingList in await dataContext.ReadingList.ToListAsync())
{
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title ?? string.Empty);
logger.LogInformation("Updated Reading List: {ReadingList}", readingList.Title);
unitOfWork.ReadingListRepository.Update(readingList);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogInformation("Updating Normalization on Reading Lists...Done");
logger.LogInformation("MigrateNormalizedEverything migration finished");
}
}

View file

@ -44,6 +44,7 @@ public interface ILibraryRepository
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync();
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
Library GetLibraryByFolder(string folder);
@ -311,6 +312,26 @@ public class LibraryRepository : ILibraryRepository
.ToList();
}
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync()
{
var ret = await _context.Series
.Select(s => s.Metadata.Language)
.AsSplitQuery()
.AsNoTracking()
.Distinct()
.ToListAsync();
return ret
.Where(s => !string.IsNullOrEmpty(s))
.Select(s => new LanguageDto()
{
Title = CultureInfo.GetCultureInfo(s).DisplayName,
IsoCode = s
})
.OrderBy(s => s.Title)
.ToList();
}
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
{
return _context.Series

View file

@ -5,12 +5,14 @@ using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Reader;
using API.Entities;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp.PixelFormats;
namespace API.Data.Repositories;
@ -44,7 +46,7 @@ public interface IUserRepository
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId, FilterDto filter);
Task<IEnumerable<AppUserBookmark>> GetAllBookmarksAsync();
Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId);
Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId);
@ -309,12 +311,63 @@ public class UserRepository : IUserRepository
.ToListAsync();
}
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
/// <summary>
/// Get all bookmarks for the user
/// </summary>
/// <param name="userId"></param>
/// <param name="filter">Only supports SeriesNameQuery</param>
/// <returns></returns>
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId, FilterDto filter)
{
return await _context.AppUserBookmark
var query = _context.AppUserBookmark
.Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page)
.AsNoTracking()
.AsNoTracking();
if (!string.IsNullOrEmpty(filter.SeriesNameQuery))
{
var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery);
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new
{
bookmark,
series
})
.Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
);
// This doesn't work on bookmarks themselves, only the series. For now, I don't think there is much value add
// if (filter.SortOptions != null)
// {
// if (filter.SortOptions.IsAscending)
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderBy(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderBy(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderBy(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// else
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderByDescending(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// }
query = filterSeriesQuery.Select(o => o.bookmark);
}
return await query
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}

View file

@ -59,7 +59,6 @@ public static class ApplicationServiceExtensions
services.AddScoped<IEventHub, EventHub>();
services.AddSqLite(config, env);
services.AddLogging(config);
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
}
@ -68,18 +67,9 @@ public static class ApplicationServiceExtensions
{
services.AddDbContext<DataContext>(options =>
{
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.UseSqlite("Data source=config/kavita.db");
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging(env.IsDevelopment());
});
}
private static void AddLogging(this IServiceCollection services, IConfiguration config)
{
services.AddLogging(loggingBuilder =>
{
var loggingSection = config.GetSection("Logging");
loggingBuilder.AddFile(loggingSection);
});
}
}

View file

@ -37,7 +37,6 @@ public class Program
public static async Task Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
@ -87,7 +86,8 @@ public class Program
await Seed.SeedThemes(context);
await Seed.SeedUserApiKeys(context);
// NOTE: This check is from v0.4.8 (Nov 04, 2021). We can likely remove this
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
if (isDocker && new FileInfo("data/appsettings.json").Exists)
{
logger.LogCritical("WARNING! Mount point is incorrect, nothing here will persist. Please change your container mount from /kavita/data to /kavita/config");

View file

@ -58,7 +58,7 @@ public static class Parser
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])",
MatchOptions, RegexTimeout);
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+!]",
MatchOptions, RegexTimeout);
/// <summary>
@ -67,6 +67,8 @@ public static class Parser
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
MatchOptions, RegexTimeout);
private const string Number = @"\d+(\.\d)?";
private const string NumberRange = Number + @"(-" + Number + @")?";
private static readonly Regex[] MangaVolumeRegex = new[]
{
@ -78,9 +80,10 @@ public static class Parser
new Regex(
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
MatchOptions, RegexTimeout),
// TODO: In .NET 7, update this to use raw literal strings and apply the NumberRange everywhere
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex(
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>\d+(-\d+)?)(?!\])",
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
MatchOptions, RegexTimeout),
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
new Regex(
@ -130,10 +133,34 @@ public static class Parser
new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] MangaSeriesRegex = new[]
{
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(?<Series>.+?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?<Series>.+?)(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(?<Series>.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Grand Blue Dreaming - SP02
new Regex(
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
@ -280,10 +307,27 @@ public static class Parser
new Regex(
@"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] ComicSeriesRegex = new[]
{
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"(?<Series>.+?)Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(?<Series>.+?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?<Series>.+?)(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(?<Series>.+?)(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Tintin - T22 Vol 714 pour Sydney
new Regex(
@"(?<Series>.+?)\s?(\b|_|-)\s?((vol|tome|t)\.?)(?<Volume>\d+(-\d+)?)",
@ -380,6 +424,14 @@ public static class Parser
new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] ComicChapterRegex = new[]
@ -417,11 +469,18 @@ public static class Parser
@"^(?<Series>.+?)(?:vol\.?\d+)\s#(?<Chapter>\d+)",
MatchOptions,
RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex(
@"^(?<Series>.+?)(?: (?<Chapter>\d+))",
MatchOptions, RegexTimeout),
// Saga 001 (2012) (Digital) (Empire-Zone)
new Regex(
@"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}",
@ -438,7 +497,6 @@ public static class Parser
new Regex(
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] ReleaseGroupRegex = new[]
@ -459,7 +517,7 @@ public static class Parser
MatchOptions, RegexTimeout),
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex(
@"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)",
@"v\d+\.(\s|_)(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove)
new Regex(
@ -469,6 +527,10 @@ public static class Parser
new Regex(
@"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)",
MatchOptions, RegexTimeout),
// Russian Chapter: Главы n -> Chapter n
new Regex(
@"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, RegexTimeout),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
new Regex(
@"^(?!Vol)(?<Series>.+?)(?<!Vol)(?<!Vol.)\s(\d\s)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
@ -503,9 +565,14 @@ public static class Parser
MatchOptions, RegexTimeout),
// Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル 高校生のSMごっこ 第1話
new Regex(
@"第?(?<Chapter>\d+(?:.\d+|-\d+)?)話",
@"第?(?<Chapter>\d+(?:\.\d+|-\d+)?)話",
MatchOptions, RegexTimeout),
// Russian Chapter: n Главa -> Chapter n
new Regex(
@"(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] MangaEditionRegex = {
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
new Regex(
@ -760,12 +827,10 @@ public static class Parser
var matches = regex.Matches(filename);
foreach (Match match in matches)
{
if (match.Groups["Chapter"].Success && match.Groups["Chapter"] != Match.Empty)
{
var value = match.Groups["Chapter"].Value;
var hasPart = match.Groups["Part"].Success;
return FormatValue(value, hasPart);
}
if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
var value = match.Groups["Chapter"].Value;
var hasPart = match.Groups["Part"].Success;
return FormatValue(value, hasPart);
}
}

View file

@ -5,7 +5,6 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Entities;
using API.Entities.Enums;
@ -18,12 +17,10 @@ using API.Services.Tasks;
using API.SignalR;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.Storage.SQLite;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
@ -193,8 +190,8 @@ public class Startup
await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
// Only needed for v0.5.5.x and v0.5.6
await MigrateNormalizedLocalizedName.Migrate(unitOfWork, dataContext, logger);
// only needed for v0.5.4 and v0.6.0
await MigrateNormalizedEverything.Migrate(unitOfWork, dataContext, logger);
// Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);

View file

@ -1,7 +1,4 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data source=config//kavita.db"
},
"TokenKey": "super secret unguessable key",
"Port": 5000
}

View file

@ -1,7 +1,4 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data source=config/kavita.db"
},
"TokenKey": "super secret unguessable key",
"Port": 5000
}