Hotfix Prep (#2484)

This commit is contained in:
Joe Milazzo 2023-12-10 12:47:25 -06:00 committed by GitHub
parent 8c16b87ff0
commit bd4cbeb393
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 114 additions and 87 deletions

View File

@ -44,6 +44,17 @@ public class SmartFilterHelperTests
AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95"); AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95");
} }
[Fact]
public void Test_Decode2()
{
const string encoded = """
name=Test%202&stmts=comparison%253D10%25C2%25A6field%253D1%25C2%25A6value%253DA%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D11&sortOptions=sortField%3D1%C2%A6isAscending%3DTrue&limitTo=0&combination=1
""";
var filter = SmartFilterHelper.Decode(encoded);
Assert.True(filter.SortOptions.IsAscending);
}
[Fact] [Fact]
public void Test_EncodeDecode() public void Test_EncodeDecode()
{ {

View File

@ -83,6 +83,7 @@ public class MangaParserTests
[InlineData("시즌34삽화2", "34")] [InlineData("시즌34삽화2", "34")]
[InlineData("Accel World Chapter 001 Volume 002", "2")] [InlineData("Accel World Chapter 001 Volume 002", "2")]
[InlineData("Accel World Volume 2", "2")] [InlineData("Accel World Volume 2", "2")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
@ -204,6 +205,7 @@ public class MangaParserTests
[InlineData("죠시라쿠! 2년 후 1권", "죠시라쿠! 2년 후")] [InlineData("죠시라쿠! 2년 후 1권", "죠시라쿠! 2년 후")]
[InlineData("test 2 years 1권", "test 2 years")] [InlineData("test 2 years 1권", "test 2 years")]
[InlineData("test 2 years 1화", "test 2 years")] [InlineData("test 2 years 1화", "test 2 years")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));

View File

@ -2,13 +2,23 @@
using Xunit; using Xunit;
namespace API.Tests.Services; namespace API.Tests.Services;
#nullable enable
public class ScrobblingServiceTests public class ScrobblingServiceTests
{ {
[Theory] [Theory]
[InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)]
public void CanParseWeblink(string link, long expectedId) [InlineData("https://anilist.co/manga/30105", 30105)]
[InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)]
public void CanParseWeblink_AniList(string link, int? expectedId)
{ {
Assert.Equal(ScrobblingService.ExtractId<long>(link, ScrobblingService.AniListWeblinkWebsite), expectedId); Assert.Equal(ScrobblingService.ExtractId<int?>(link, ScrobblingService.AniListWeblinkWebsite), expectedId);
}
[Theory]
[InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")]
public void CanParseWeblink_MangaDex(string link, string expectedId)
{
Assert.Equal(ScrobblingService.ExtractId<string?>(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId);
} }
} }

View File

@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharpCompress;
namespace API.Controllers; namespace API.Controllers;
@ -137,8 +138,7 @@ public class AccountController : BaseApiController
if (!result.Succeeded) return BadRequest(result.Errors); if (!result.Succeeded) return BadRequest(result.Errors);
// Assign default streams // Assign default streams
user.DashboardStreams = Seed.DefaultStreams.ToList(); AddDefaultStreamsToUser(user);
user.SideNavStreams = Seed.DefaultSideNavStreams.ToList();
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen"));
@ -608,7 +608,8 @@ public class AccountController : BaseApiController
} }
// Create a new user // Create a new user
var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); var user = new AppUserBuilder(dto.Email, dto.Email,
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
_unitOfWork.UserRepository.Add(user); _unitOfWork.UserRepository.Add(user);
try try
{ {
@ -616,9 +617,7 @@ public class AccountController : BaseApiController
if (!result.Succeeded) return BadRequest(result.Errors); if (!result.Succeeded) return BadRequest(result.Errors);
// Assign default streams // Assign default streams
user.DashboardStreams = Seed.DefaultStreams.ToList(); AddDefaultStreamsToUser(user);
user.SideNavStreams = Seed.DefaultSideNavStreams.ToList();
// Assign Roles // Assign Roles
var roles = dto.Roles; var roles = dto.Roles;
@ -657,7 +656,6 @@ public class AccountController : BaseApiController
user.CreateSideNavFromLibrary(lib); user.CreateSideNavFromLibrary(lib);
} }
_unitOfWork.UserRepository.Update(user);
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
@ -669,6 +667,7 @@ public class AccountController : BaseApiController
} }
user.ConfirmationToken = token; user.ConfirmationToken = token;
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
} }
catch (Exception ex) catch (Exception ex)
@ -702,7 +701,7 @@ public class AccountController : BaseApiController
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto() BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
{ {
EmailAddress = dto.Email, EmailAddress = dto.Email,
InvitingUser = adminUser.UserName!, InvitingUser = adminUser.UserName,
ServerConfirmationLink = emailLink ServerConfirmationLink = emailLink
})); }));
} }
@ -721,6 +720,19 @@ public class AccountController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user")); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
} }
private void AddDefaultStreamsToUser(AppUser user)
{
foreach (var newStream in Seed.DefaultStreams.Select(stream => _mapper.Map<AppUserDashboardStream, AppUserDashboardStream>(stream)))
{
user.DashboardStreams.Add(newStream);
}
foreach (var stream in Seed.DefaultSideNavStreams.Select(stream => _mapper.Map<AppUserSideNavStream, AppUserSideNavStream>(stream)))
{
user.SideNavStreams.Add(stream);
}
}
/// <summary> /// <summary>
/// Last step in authentication flow, confirms the email token for email /// Last step in authentication flow, confirms the email token for email
/// </summary> /// </summary>

View File

@ -115,10 +115,6 @@ public class ReaderController : BaseApiController
try try
{ {
if (new Random().Next(1, 10) > 5)
{
await Task.Delay(1000);
}
var chapter = await _cacheService.Ensure(chapterId, extractPdf); var chapter = await _cacheService.Ensure(chapterId, extractPdf);
if (chapter == null) return NoContent(); if (chapter == null) return NoContent();
_logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId); _logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId);

View File

@ -76,48 +76,42 @@ public static class Seed
}, },
}.ToArray()); }.ToArray());
public static readonly ImmutableArray<AppUserSideNavStream> DefaultSideNavStreams = ImmutableArray.Create(new[] public static readonly ImmutableArray<AppUserSideNavStream> DefaultSideNavStreams = ImmutableArray.Create(
new AppUserSideNavStream()
{ {
new AppUserSideNavStream() Name = "want-to-read",
{ StreamType = SideNavStreamType.WantToRead,
Name = "want-to-read", Order = 1,
StreamType = SideNavStreamType.WantToRead, IsProvided = true,
Order = 1, Visible = true
IsProvided = true, }, new AppUserSideNavStream()
Visible = true {
}, Name = "collections",
new AppUserSideNavStream() StreamType = SideNavStreamType.Collections,
{ Order = 2,
Name = "collections", IsProvided = true,
StreamType = SideNavStreamType.Collections, Visible = true
Order = 2, }, new AppUserSideNavStream()
IsProvided = true, {
Visible = true Name = "reading-lists",
}, StreamType = SideNavStreamType.ReadingLists,
new AppUserSideNavStream() Order = 3,
{ IsProvided = true,
Name = "reading-lists", Visible = true
StreamType = SideNavStreamType.ReadingLists, }, new AppUserSideNavStream()
Order = 3, {
IsProvided = true, Name = "bookmarks",
Visible = true StreamType = SideNavStreamType.Bookmarks,
}, Order = 4,
new AppUserSideNavStream() IsProvided = true,
{ Visible = true
Name = "bookmarks", }, new AppUserSideNavStream()
StreamType = SideNavStreamType.Bookmarks, {
Order = 4, Name = "all-series",
IsProvided = true, StreamType = SideNavStreamType.AllSeries,
Visible = true Order = 5,
}, IsProvided = true,
new AppUserSideNavStream() Visible = true
{
Name = "all-series",
StreamType = SideNavStreamType.AllSeries,
Order = 5,
IsProvided = true,
Visible = true
}
}); });

View File

@ -240,9 +240,10 @@ public class AutoMapperProfiles : Profile
CreateMap<AppUserSmartFilter, SmartFilterDto>(); CreateMap<AppUserSmartFilter, SmartFilterDto>();
CreateMap<AppUserDashboardStream, DashboardStreamDto>(); CreateMap<AppUserDashboardStream, DashboardStreamDto>();
// CreateMap<AppUserDashboardStream, DashboardStreamDto>()
// .ForMember(dest => dest.SmartFilterEncoded, // This is for cloning to ensure the records don't get overwritten when setting from SeedData
// opt => opt.MapFrom(src => src.SmartFilter)); CreateMap<AppUserDashboardStream, AppUserDashboardStream>();
CreateMap<AppUserSideNavStream, AppUserSideNavStream>();
} }
} }

View File

@ -27,6 +27,8 @@ public class PlusSeriesDtoBuilder : IEntityBuilder<PlusSeriesDto>
ScrobblingService.MalWeblinkWebsite), ScrobblingService.MalWeblinkWebsite),
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks, GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.GoogleBooksWeblinkWebsite), ScrobblingService.GoogleBooksWeblinkWebsite),
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.MangaDexWeblinkWebsite),
VolumeCount = series.Volumes.Count, VolumeCount = series.Volumes.Count,
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
Year = series.Metadata.ReleaseYear Year = series.Metadata.ReleaseYear

View File

@ -133,7 +133,7 @@ public static class SmartFilterHelper
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey)); var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey));
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey)); var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey));
var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; var isAscending = isAscendingPart?.Trim().Replace(IsAscendingKey, string.Empty).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
if (sortFieldPart == null) if (sortFieldPart == null)
{ {
return new SortOptions(); return new SortOptions();

View File

@ -25,6 +25,7 @@ public record PlusSeriesDto
public int? AniListId { get; set; } public int? AniListId { get; set; }
public long? MalId { get; set; } public long? MalId { get; set; }
public string? GoogleBooksId { get; set; } public string? GoogleBooksId { get; set; }
public string? MangaDexId { get; set; }
public string SeriesName { get; set; } public string SeriesName { get; set; }
public string? AltSeriesName { get; set; } public string? AltSeriesName { get; set; }
public MediaFormat MediaFormat { get; set; } public MediaFormat MediaFormat { get; set; }

View File

@ -67,13 +67,14 @@ public class ScrobblingService : IScrobblingService
public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id=";
public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/";
private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>() private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
{ {
{AniListWeblinkWebsite, 0}, {AniListWeblinkWebsite, 0},
{MalWeblinkWebsite, 0}, {MalWeblinkWebsite, 0},
{GoogleBooksWeblinkWebsite, 0}, {GoogleBooksWeblinkWebsite, 0},
{MangaDexWeblinkWebsite, 0},
}; };
private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
@ -829,12 +830,12 @@ public class ScrobblingService : IScrobblingService
if (!webLink.StartsWith(website)) continue; if (!webLink.StartsWith(website)) continue;
var tokens = webLink.Split(website)[1].Split('/'); var tokens = webLink.Split(website)[1].Split('/');
var value = tokens[index]; var value = tokens[index];
if (typeof(T) == typeof(int)) if (typeof(T) == typeof(int?))
{ {
if (int.TryParse(value, out var intValue)) if (int.TryParse(value, out var intValue))
return (T)(object)intValue; return (T)(object)intValue;
} }
else if (typeof(T) == typeof(long)) else if (typeof(T) == typeof(long?))
{ {
if (long.TryParse(value, out var longValue)) if (long.TryParse(value, out var longValue))
return (T)(object)longValue; return (T)(object)longValue;

View File

@ -34,7 +34,6 @@ public interface ITaskScheduler
void ScanSiteThemes(); void ScanSiteThemes();
void CovertAllCoversToEncoding(); void CovertAllCoversToEncoding();
Task CleanupDbEntries(); Task CleanupDbEntries();
Task ScrobbleUpdates(int userId);
} }
public class TaskScheduler : ITaskScheduler public class TaskScheduler : ITaskScheduler
@ -141,7 +140,6 @@ public class TaskScheduler : ITaskScheduler
} }
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions); RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions);
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, RecurringJobOptions);
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions); RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions);
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions); RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions);
@ -272,16 +270,6 @@ public class TaskScheduler : ITaskScheduler
await _cleanupService.CleanupDbEntries(); await _cleanupService.CleanupDbEntries();
} }
/// <summary>
/// TODO: Remove this for Release
/// </summary>
/// <returns></returns>
public async Task ScrobbleUpdates(int userId)
{
if (!await _licenseService.HasActiveLicense()) return;
BackgroundJob.Enqueue(() => _scrobblingService.ProcessUpdatesSinceLastSync());
}
/// <summary> /// <summary>
/// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future. /// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future.
/// </summary> /// </summary>

View File

@ -9,6 +9,7 @@ using API.Entities.Enums;
using API.Logging; using API.Logging;
using API.SignalR; using API.SignalR;
using Hangfire; using Hangfire;
using Kavita.Common.EnvironmentInfo;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services.Tasks; namespace API.Services.Tasks;
@ -91,7 +92,7 @@ public class BackupService : IBackupService
await SendProgress(0.1F, "Copying core files"); await SendProgress(0.1F, "Copying core files");
var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace("/", "_").Replace(":", "_"); var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip"); var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}_v{BuildInfo.Version}.zip");
if (File.Exists(zipPath)) if (File.Exists(zipPath))
{ {

View File

@ -92,6 +92,8 @@ public class CleanupService : ICleanupService
await CleanupLogs(); await CleanupLogs();
await SendProgress(0.9F, "Cleaning progress events that exceed 100%"); await SendProgress(0.9F, "Cleaning progress events that exceed 100%");
await EnsureChapterProgressIsCapped(); await EnsureChapterProgressIsCapped();
await SendProgress(0.95F, "Cleaning abandoned database rows");
await CleanupDbEntries();
await SendProgress(1F, "Cleanup finished"); await SendProgress(1F, "Cleanup finished");
_logger.LogInformation("Cleanup finished"); _logger.LogInformation("Cleanup finished");
} }

View File

@ -109,9 +109,9 @@ public static class Parser
new Regex( new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)", @"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar // Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])", @"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?<Volume>\d+(\.\d+)?)(.+?|$)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex( new Regex(
@ -137,6 +137,7 @@ public static class Parser
new Regex( new Regex(
@"(vol_)(?<Volume>\d+(\.\d)?)", @"(vol_)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册 // Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
new Regex( new Regex(
@"第(?<Volume>\d+)(卷|册)", @"第(?<Volume>\d+)(卷|册)",
@ -197,16 +198,17 @@ public static class Parser
new Regex( new Regex(
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d", @"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz
new Regex(
@"^(?<Series>.*)( |_)Vol\.?(\d+|tbd)",
MatchOptions, RegexTimeout),
// Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex), // Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex),
// The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake // The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake
new Regex( new Regex(
@"(?<Series>.+?)(\s|_|-)+(?:Vol(ume|\.)?(\s|_|-)+\d+)(\s|_|-)+(?:(Ch|Chapter|Ch)\.?)(\s|_|-)+(?<Chapter>\d+)", @"(?<Series>.+?)(\s|_|-)+(?:Vol(ume|\.)?(\s|_|-)+\d+)(\s|_|-)+(?:(Ch|Chapter|Ch)\.?)(\s|_|-)+(?<Chapter>\d+)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz,
// Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake
new Regex(
@"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+Vol(ume)?\.?(\d+|tbd|\s\d).+?",
MatchOptions, RegexTimeout),
// Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip // Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip
new Regex( new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d*)(\s|_|-)", @"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d*)(\s|_|-)",
@ -233,6 +235,7 @@ public static class Parser
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)", @"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
new Regex( new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)", @"(?<Series>.+?):? (\b|_|-)(vol)(ume)",

Binary file not shown.

View File

@ -55,7 +55,7 @@ install methods and platforms.
**Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.** **Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.**
## Feature Requests ## Feature Requests
Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features before you submit an idea. Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects?type=classic) first for a list of planned features before you submit an idea.
## Notice ## Notice
Kavita is being actively developed and should be considered beta software until the 1.0 release. Kavita is being actively developed and should be considered beta software until the 1.0 release.

View File

@ -15,7 +15,8 @@ type UtcToLocalTimeFormat = 'full' | 'short' | 'shortDate' | 'shortTime';
}) })
export class UtcToLocalTimePipe implements PipeTransform { export class UtcToLocalTimePipe implements PipeTransform {
transform(utcDate: string, format: UtcToLocalTimeFormat = 'short'): string { transform(utcDate: string | undefined | null, format: UtcToLocalTimeFormat = 'short'): string {
if (utcDate === undefined || utcDate === null) return '';
const browserLanguage = navigator.language; const browserLanguage = navigator.language;
const dateTime = DateTime.fromISO(utcDate, { zone: 'utc' }).toLocal().setLocale(browserLanguage); const dateTime = DateTime.fromISO(utcDate, { zone: 'utc' }).toLocal().setLocale(browserLanguage);

View File

@ -58,7 +58,9 @@
<td> <td>
{{task.title | titlecase}} {{task.title | titlecase}}
</td> </td>
<td>{{task.lastExecutionUtc | utcToLocalTime | defaultValue }}</td> <td>
{{task.lastExecutionUtc | utcToLocalTime | defaultValue }}
</td>
<td>{{task.cron}}</td> <td>{{task.cron}}</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -36,7 +36,7 @@
</div> </div>
</div> </div>
</div> </div>
<app-loading [loading]="isLoading || !(currentImage$ | async)?.complete" [absolute]="true"></app-loading> <app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
<div class="reading-area" <div class="reading-area"
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)" ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea> [ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>

View File

@ -51,7 +51,7 @@
</div> </div>
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image> <app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
<ng-container *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial"> <ng-container *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
<div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) | number:'1.0-1'}}% Read"> <div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) * 100 | number:'1.0-1'}}% Read">
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar> <ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
</div> </div>
<div class="under-image"> <div class="under-image">

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.11.0" "version": "0.7.11.1"
}, },
"servers": [ "servers": [
{ {