Kavita+ Tweaks (#2595)

This commit is contained in:
Joe Milazzo 2024-01-09 16:04:25 -06:00 committed by GitHub
parent e21144bf6b
commit 3dcf7750f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 501 additions and 414 deletions

View File

@ -1,6 +1,6 @@
name: Bug Report name: Bug Report
description: Create a report to help us improve description: Create a report to help us improve
title: "" title: "Bug Title Here" # Add a title field
labels: ["needs-triage"] labels: ["needs-triage"]
assignees: assignees:
body: body:
@ -16,7 +16,7 @@ body:
id: what-happened id: what-happened
attributes: attributes:
label: What happened? label: What happened?
description: Also tell us, what steps you took so we can try to reproduce. description: Also tell us what steps you took so we can try to reproduce.
placeholder: Tell us what you see! placeholder: Tell us what you see!
value: "" value: ""
validations: validations:
@ -52,7 +52,7 @@ body:
- type: dropdown - type: dropdown
id: desktop-OS id: desktop-OS
attributes: attributes:
label: If issue being seen on Desktop, what OS are you running where you see the issue? label: If the issue is being seen on Desktop, what OS are you running where you see the issue?
multiple: false multiple: false
options: options:
- Windows - Windows
@ -61,7 +61,7 @@ body:
- type: dropdown - type: dropdown
id: desktop-browsers id: desktop-browsers
attributes: attributes:
label: If issue being seen in the UI, what browsers are you seeing the problem on? label: If the issue is being seen in the UI, what browsers are you seeing the problem on?
multiple: true multiple: true
options: options:
- Firefox - Firefox
@ -71,7 +71,7 @@ body:
- type: dropdown - type: dropdown
id: mobile-OS id: mobile-OS
attributes: attributes:
label: If issue being seen on Mobile, what OS are you running where you see the issue? label: If the issue is being seen on Mobile, what OS are you running where you see the issue?
multiple: false multiple: false
options: options:
- Android - Android
@ -79,7 +79,7 @@ body:
- type: dropdown - type: dropdown
id: mobile-browsers id: mobile-browsers
attributes: attributes:
label: If issue being seen on UI, what browsers are you seeing the problem on? label: If the issue is being seen on the UI, what browsers are you seeing the problem on?
multiple: true multiple: true
options: options:
- Firefox - Firefox
@ -97,7 +97,7 @@ body:
attributes: attributes:
label: Additional Notes label: Additional Notes
description: Any other information about the issue not covered in this form? description: Any other information about the issue not covered in this form?
placeholder: e.g. Running Kavita on a raspberry pi, updating from X version, using LSIO container, etc placeholder: e.g. Running Kavita on a Raspberry Pi, updating from X version, using LSIO container, etc
value: "" value: ""
validations: validations:
required: true required: true

View File

@ -10,8 +10,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.11" /> <PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.12" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup> </ItemGroup>

View File

@ -6,13 +6,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" />
<PackageReference Include="xunit" Version="2.6.3" /> <PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -292,6 +292,7 @@ public class MangaParserTests
[InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Accel World Chapter 001 Volume 002", "1")]
[InlineData("Bleach 001-003", "1-3")] [InlineData("Bleach 001-003", "1-3")]
[InlineData("Accel World Volume 2", "0")] [InlineData("Accel World Volume 2", "0")]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View File

@ -53,7 +53,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -63,26 +63,26 @@
<PackageReference Include="ExCSS" Version="4.2.4" /> <PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="Flurl" Version="3.0.7" /> <PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.6" /> <PackageReference Include="Hangfire" Version="1.8.7" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" /> <PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" /> <PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" /> <PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.57" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" /> <PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.7" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" /> <PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" /> <PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="2.4.0" /> <PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.15.0" /> <PackageReference Include="NetVips.Native" Version="8.15.0" />
<PackageReference Include="NReco.Logging.File" Version="1.1.7" /> <PackageReference Include="NReco.Logging.File" Version="1.2.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
@ -92,17 +92,17 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" /> <PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.34.2" /> <PackageReference Include="SharpCompress" Version="0.35.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.4" /> <PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" /> <PackageReference Include="System.Drawing.Common" Version="8.0.1" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" /> <PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup> </ItemGroup>

View File

@ -39,22 +39,27 @@ public class ScrobblingController : BaseApiController
_localizationService = localizationService; _localizationService = localizationService;
} }
/// <summary>
/// Get the current user's AniList token
/// </summary>
/// <returns></returns>
[HttpGet("anilist-token")] [HttpGet("anilist-token")]
public async Task<ActionResult> GetAniListToken() public async Task<ActionResult> GetAniListToken()
{ {
// Validate the license
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
return Ok(user.AniListAccessToken); return Ok(user.AniListAccessToken);
} }
/// <summary>
/// Update the current user's AniList token
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-anilist-token")] [HttpPost("update-anilist-token")]
public async Task<ActionResult> UpdateAniListToken(AniListUpdateDto dto) public async Task<ActionResult> UpdateAniListToken(AniListUpdateDto dto)
{ {
// Validate the license
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
@ -71,6 +76,11 @@ public class ScrobblingController : BaseApiController
return Ok(); return Ok();
} }
/// <summary>
/// Checks if the current Scrobbling token for the given Provider has expired for the current user
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
[HttpGet("token-expired")] [HttpGet("token-expired")]
public async Task<ActionResult<bool>> HasTokenExpired(ScrobbleProvider provider) public async Task<ActionResult<bool>> HasTokenExpired(ScrobbleProvider provider)
{ {
@ -159,15 +169,20 @@ public class ScrobblingController : BaseApiController
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds);
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId))
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); return Ok(await _localizationService.Translate(user.Id, "nothing-to-do"));
var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build(); var seriesHold = new ScrobbleHoldBuilder()
.WithSeriesId(seriesId)
.Build();
user.ScrobbleHolds.Add(seriesHold); user.ScrobbleHolds.Add(seriesHold);
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
try try
{ {
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
// When a hold is placed on a series, clear any pre-existing Scrobble Events
await _scrobblingService.ClearEventsForSeries(user.Id, seriesId);
return Ok(); return Ok();
} }
catch (DbUpdateConcurrencyException ex) catch (DbUpdateConcurrencyException ex)

View File

@ -27,7 +27,7 @@ public interface IScrobbleRepository
Task ClearScrobbleErrors(); Task ClearScrobbleErrors();
Task<bool> HasErrorForSeries(int seriesId); Task<bool> HasErrorForSeries(int seriesId);
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType); Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
Task<IEnumerable<ScrobbleEventDto>> GetUserEvents(int userId); Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
} }
@ -127,16 +127,17 @@ public class ScrobbleRepository : IScrobbleRepository
return await _context.ScrobbleEvent.FirstOrDefaultAsync(e => return await _context.ScrobbleEvent.FirstOrDefaultAsync(e =>
e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType);
} }
public async Task<IEnumerable<ScrobbleEventDto>> GetUserEvents(int userId)
public async Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId)
{ {
return await _context.ScrobbleEvent return await _context.ScrobbleEvent
.Where(e => e.AppUserId == userId) .Where(e => e.AppUserId == userId && !e.IsProcessed)
.Include(e => e.Series) .Include(e => e.Series)
.OrderBy(e => e.LastModifiedUtc) .OrderBy(e => e.LastModifiedUtc)
.AsSplitQuery() .AsSplitQuery()
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
} }
public async Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) public async Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination)
{ {
var query = _context.ScrobbleEvent var query = _context.ScrobbleEvent
@ -146,6 +147,7 @@ public class ScrobbleRepository : IScrobbleRepository
.WhereIf(!string.IsNullOrEmpty(filter.Query), s => .WhereIf(!string.IsNullOrEmpty(filter.Query), s =>
EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") EF.Functions.Like(s.Series.Name, $"%{filter.Query}%")
) )
.WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review)
.AsSplitQuery() .AsSplitQuery()
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider); .ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider);

View File

@ -15,4 +15,8 @@ public class ScrobbleEventFilter
/// A query to search against /// A query to search against
/// </summary> /// </summary>
public string Query { get; set; } public string Query { get; set; }
/// <summary>
/// Include reviews in the result - Note: Review Scrobbling is disabled
/// </summary>
public bool IncludeReviews { get; set; } = false;
} }

View File

@ -53,6 +53,7 @@ public interface IScrobblingService
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
Task ProcessUpdatesSinceLastSync(); Task ProcessUpdatesSinceLastSync();
Task CreateEventsFromExistingHistory(int userId = 0); Task CreateEventsFromExistingHistory(int userId = 0);
Task ClearEventsForSeries(int userId, int seriesId);
} }
public class ScrobblingService : IScrobblingService public class ScrobblingService : IScrobblingService
@ -542,6 +543,26 @@ public class ScrobblingService : IScrobblingService
} }
} }
/// <summary>
/// Removes all events (active) that are tied to a now-on hold series
/// </summary>
/// <param name="userId"></param>
/// <param name="seriesId"></param>
public async Task ClearEventsForSeries(int userId, int seriesId)
{
_logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {UserId} as Series is now on hold list", seriesId, userId);
var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId);
foreach (var scrobble in events)
{
_unitOfWork.ScrobbleRepository.Remove(scrobble);
}
await _unitOfWork.CommitAsync();
}
/// <summary>
/// Removes all events that have been processed that are 7 days old
/// </summary>
[DisableConcurrentExecution(60 * 60 * 60)] [DisableConcurrentExecution(60 * 60 * 60)]
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ClearProcessedEvents() public async Task ClearProcessedEvents()
@ -594,10 +615,10 @@ public class ScrobblingService : IScrobblingService
.Where(e => librariesWithScrobbling.Contains(e.LibraryId)) .Where(e => librariesWithScrobbling.Contains(e.LibraryId))
.Where(e => !errors.Contains(e.SeriesId)) .Where(e => !errors.Contains(e.SeriesId))
.ToList(); .ToList();
var reviewEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.Review)) // var reviewEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.Review))
.Where(e => librariesWithScrobbling.Contains(e.LibraryId)) // .Where(e => librariesWithScrobbling.Contains(e.LibraryId))
.Where(e => !errors.Contains(e.SeriesId)) // .Where(e => !errors.Contains(e.SeriesId))
.ToList(); // .ToList();
var decisions = addToWantToRead var decisions = addToWantToRead
.GroupBy(item => new { item.SeriesId, item.AppUserId }) .GroupBy(item => new { item.SeriesId, item.AppUserId })
.Select(group => new .Select(group => new
@ -624,7 +645,7 @@ public class ScrobblingService : IScrobblingService
await SetAndCheckRateLimit(userRateLimits, user, license.Value); await SetAndCheckRateLimit(userRateLimits, user, license.Value);
} }
var totalProgress = readEvents.Count + decisions.Count + ratingEvents.Count + decisions.Count + reviewEvents.Count; var totalProgress = readEvents.Count + decisions.Count + ratingEvents.Count + decisions.Count;// + reviewEvents.Count;
_logger.LogInformation("Found {TotalEvents} Scrobble Events", totalProgress); _logger.LogInformation("Found {TotalEvents} Scrobble Events", totalProgress);
try try
@ -671,21 +692,21 @@ public class ScrobblingService : IScrobblingService
Year = evt.Series.Metadata.ReleaseYear Year = evt.Series.Metadata.ReleaseYear
})); }));
progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, // progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter,
totalProgress, evt => Task.FromResult(new ScrobbleDto() // totalProgress, evt => Task.FromResult(new ScrobbleDto()
{ // {
Format = evt.Format, // Format = evt.Format,
AniListId = evt.AniListId, // AniListId = evt.AniListId,
MALId = (int?) evt.MalId, // MALId = (int?) evt.MalId,
ScrobbleEventType = evt.ScrobbleEventType, // ScrobbleEventType = evt.ScrobbleEventType,
AniListToken = evt.AppUser.AniListAccessToken, // AniListToken = evt.AppUser.AniListAccessToken,
SeriesName = evt.Series.Name, // SeriesName = evt.Series.Name,
LocalizedSeriesName = evt.Series.LocalizedName, // LocalizedSeriesName = evt.Series.LocalizedName,
Rating = evt.Rating, // Rating = evt.Rating,
Year = evt.Series.Metadata.ReleaseYear, // Year = evt.Series.Metadata.ReleaseYear,
ReviewBody = evt.ReviewBody, // ReviewBody = evt.ReviewBody,
ReviewTitle = evt.ReviewTitle // ReviewTitle = evt.ReviewTitle
})); // }));
progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter,
totalProgress, evt => Task.FromResult(new ScrobbleDto() totalProgress, evt => Task.FromResult(new ScrobbleDto()

View File

@ -49,6 +49,7 @@ public class DefaultParser : IDefaultParser
// If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism // If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism
if (type == LibraryType.Image || Parser.IsImage(filePath)) if (type == LibraryType.Image || Parser.IsImage(filePath))
{ {
// TODO: We can move this up one level
return ParseImage(filePath, rootPath, ret); return ParseImage(filePath, rootPath, ret);
} }
@ -78,7 +79,7 @@ public class DefaultParser : IDefaultParser
var edition = Parser.ParseEdition(fileName); var edition = Parser.ParseEdition(fileName);
if (!string.IsNullOrEmpty(edition)) if (!string.IsNullOrEmpty(edition))
{ {
ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic); ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic);
ret.Edition = edition; ret.Edition = edition;
} }

View File

@ -543,7 +543,7 @@ public static class Parser
{ {
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
new Regex( new Regex(
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)", @"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-\d+(\.\d)?)?)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex( new Regex(

View File

@ -14,7 +14,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.16.0.82469">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

636
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,17 +13,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.6", "@angular/animations": "^17.0.8",
"@angular/cdk": "^17.0.4", "@angular/cdk": "^17.0.4",
"@angular/common": "^17.0.6", "@angular/common": "^17.0.8",
"@angular/compiler": "^17.0.6", "@angular/compiler": "^17.0.8",
"@angular/core": "^17.0.6", "@angular/core": "^17.0.8",
"@angular/forms": "^17.0.6", "@angular/forms": "^17.0.8",
"@angular/localize": "^17.0.6", "@angular/localize": "^17.0.8",
"@angular/platform-browser": "^17.0.6", "@angular/platform-browser": "^17.0.8",
"@angular/platform-browser-dynamic": "^17.0.6", "@angular/platform-browser-dynamic": "^17.0.8",
"@angular/router": "^17.0.6", "@angular/router": "^17.0.8",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.5.1",
"@iharbeck/ngx-virtual-scroller": "^17.0.0", "@iharbeck/ngx-virtual-scroller": "^17.0.0",
"@iplab/ngx-file-upload": "^17.0.0", "@iplab/ngx-file-upload": "^17.0.0",
"@microsoft/signalr": "^7.0.12", "@microsoft/signalr": "^7.0.12",
@ -42,7 +42,7 @@
"luxon": "^3.4.4", "luxon": "^3.4.4",
"ng-circle-progress": "^1.7.1", "ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3", "ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^13.0.9", "ng-select2-component": "^14.0.0",
"ngx-color-picker": "^16.0.0", "ngx-color-picker": "^16.0.0",
"ngx-extended-pdf-viewer": "^18.1.9", "ngx-extended-pdf-viewer": "^18.1.9",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
@ -58,17 +58,17 @@
"zone.js": "^0.14.2" "zone.js": "^0.14.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.7", "@angular-devkit/build-angular": "^17.0.9",
"@angular-eslint/builder": "^17.1.1", "@angular-eslint/builder": "^17.2.0",
"@angular-eslint/eslint-plugin": "^17.1.1", "@angular-eslint/eslint-plugin": "^17.2.0",
"@angular-eslint/eslint-plugin-template": "^17.1.1", "@angular-eslint/eslint-plugin-template": "^17.2.0",
"@angular-eslint/schematics": "^17.1.1", "@angular-eslint/schematics": "^17.2.0",
"@angular-eslint/template-parser": "^17.1.1", "@angular-eslint/template-parser": "^17.2.0",
"@angular/cli": "^17.0.7", "@angular/cli": "^17.0.9",
"@angular/compiler-cli": "^17.0.6", "@angular/compiler-cli": "^17.0.8",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.3.7", "@types/luxon": "^3.4.0",
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0", "@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0", "@typescript-eslint/parser": "^6.13.0",

View File

@ -6,6 +6,8 @@ import {environment} from "../../environments/environment";
import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {SideNavStream} from "../_models/sidenav/sidenav-stream";
import {TextResonse} from "../_types/text-response"; import {TextResonse} from "../_types/text-response";
import {DashboardStream} from "../_models/dashboard/dashboard-stream"; import {DashboardStream} from "../_models/dashboard/dashboard-stream";
import {AccountService} from "./account.service";
import {tap} from "rxjs/operators";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -34,9 +36,16 @@ export class NavService {
private renderer: Renderer2; private renderer: Renderer2;
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient) { constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient, private accountService: AccountService) {
this.renderer = rendererFactory.createRenderer(null, null); this.renderer = rendererFactory.createRenderer(null, null);
this.showNavBar();
// To avoid flashing, let's check if we are authenticated before we show
this.accountService.currentUser$.subscribe(u => {
if (u) {
this.showNavBar();
}
});
const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false; const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false;
this.sideNavCollapseSource.next(sideNavState); this.sideNavCollapseSource.next(sideNavState);
this.showSideNav(); this.showSideNav();

View File

@ -84,12 +84,19 @@ export class BookLineOverlayComponent implements OnInit {
const selection = window.getSelection(); const selection = window.getSelection();
if (!event.target) return; if (!event.target) return;
if ((selection === null || selection === undefined || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) { if ((selection === null || selection === undefined || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
if (this.selectedText !== '') { if (this.selectedText !== '') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
this.reset();
const isRightClick = (event instanceof MouseEvent && event.button === 2);
if (!isRightClick) {
this.reset();
}
return; return;
} }

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import { Validators, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { Validators, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -6,6 +6,7 @@ import { AccountService } from 'src/app/_services/account.service';
import { NgIf } from '@angular/common'; import { NgIf } from '@angular/common';
import { SplashContainerComponent } from '../splash-container/splash-container.component'; import { SplashContainerComponent } from '../splash-container/splash-container.component';
import {TranslocoDirective} from "@ngneat/transloco"; import {TranslocoDirective} from "@ngneat/transloco";
import {NavService} from "../../../_services/nav.service";
@Component({ @Component({
selector: 'app-reset-password', selector: 'app-reset-password',
@ -17,12 +18,19 @@ import {TranslocoDirective} from "@ngneat/transloco";
}) })
export class ResetPasswordComponent { export class ResetPasswordComponent {
private readonly router = inject(Router);
private readonly accountService = inject(AccountService);
private readonly toastr = inject(ToastrService);
private readonly navService = inject(NavService);
registerForm: FormGroup = new FormGroup({ registerForm: FormGroup = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]), email: new FormControl('', [Validators.required, Validators.email]),
}); });
constructor(private router: Router, private accountService: AccountService, constructor() {
private toastr: ToastrService) {} this.navService.hideNavBar();
this.navService.hideSideNav();
}
submit() { submit() {
const model = this.registerForm.get('email')?.value; const model = this.registerForm.get('email')?.value;

View File

@ -1,5 +1,5 @@
<div class="mx-auto login"> <div class="mx-auto login" [ngStyle]="{'height': (navService.navbarVisible$ | async) ? 'calc(var(--vh, 1vh) * 100 - 57px)' : 'calc(var(--vh, 1vh) * 100)'}">
<div class="row row-cols-4 row-cols-md-4 row-cols-sm-2 row-cols-xs-2"> <div class="row row-cols-4 row-cols-md-4 row-cols-sm-2 row-cols-xs-2">
<div class="col align-self-center card p-3"> <div class="col align-self-center card p-3">
<span> <span>
@ -17,4 +17,4 @@
</div> </div>
</div> </div>

View File

@ -1,10 +1,18 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import {AsyncPipe, NgStyle} from "@angular/common";
import {NavService} from "../../../_services/nav.service";
@Component({ @Component({
selector: 'app-splash-container', selector: 'app-splash-container',
templateUrl: './splash-container.component.html', templateUrl: './splash-container.component.html',
styleUrls: ['./splash-container.component.scss'], styleUrls: ['./splash-container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true imports: [
NgStyle,
AsyncPipe
],
standalone: true
}) })
export class SplashContainerComponent {} export class SplashContainerComponent {
protected readonly navService = inject(NavService);
}

View File

@ -26,6 +26,4 @@ export class UserHoldsComponent {
private readonly scrobblingService = inject(ScrobblingService); private readonly scrobblingService = inject(ScrobblingService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
holds$ = this.scrobblingService.getHolds().pipe(takeUntilDestroyed(this.destroyRef), shareReplay()); holds$ = this.scrobblingService.getHolds().pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
constructor() {}
} }

View File

@ -7335,6 +7335,7 @@
"tags": [ "tags": [
"Scrobbling" "Scrobbling"
], ],
"summary": "Get the current user's AniList token",
"responses": { "responses": {
"200": { "200": {
"description": "Success" "description": "Success"
@ -7347,7 +7348,9 @@
"tags": [ "tags": [
"Scrobbling" "Scrobbling"
], ],
"summary": "Update the current user's AniList token",
"requestBody": { "requestBody": {
"description": "",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -7378,10 +7381,12 @@
"tags": [ "tags": [
"Scrobbling" "Scrobbling"
], ],
"summary": "Checks if the current Scrobbling token for the given Provider has expired for the current user",
"parameters": [ "parameters": [
{ {
"name": "provider", "name": "provider",
"in": "query", "in": "query",
"description": "",
"schema": { "schema": {
"enum": [ "enum": [
0, 0,
@ -17178,6 +17183,10 @@
"type": "string", "type": "string",
"description": "A query to search against", "description": "A query to search against",
"nullable": true "nullable": true
},
"includeReviews": {
"type": "boolean",
"description": "Include reviews in the result - Note: Review Scrobbling is disabled"
} }
}, },
"additionalProperties": false "additionalProperties": false