diff --git a/API/API.csproj b/API/API.csproj
index 1658d7be3..e5276ac0d 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -205,7 +205,6 @@
Always
-
diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs
index 469bb8ebb..3a06fb06c 100644
--- a/API/Controllers/PluginController.cs
+++ b/API/Controllers/PluginController.cs
@@ -1,11 +1,16 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
+using API.DTOs.Misc;
using API.Entities.Enums;
using API.Middleware;
using API.Services;
+using API.Services.Tasks.Scanner.Parser;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -91,4 +96,85 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService
return Ok(new { ExpiresAt = exp?.ToUniversalTime() });
}
+
+
+ ///
+ /// Parse a string and return Parsed information from it. Does not support any directory fallback parsing
+ ///
+ /// String to parse
+ /// Determines the set of pattern matching to use
+ ///
+ [HttpGet("parse")]
+ public ActionResult Parse([FromQuery] [StringLength(1000)] string name, [FromQuery] LibraryType libraryType)
+ {
+ try
+ {
+ var result = ParseText(name, libraryType);
+
+ return Ok(result);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return BadRequest("Input could not be parsed in allowed time");
+ }
+ catch (Exception)
+ {
+ return BadRequest("Failed to parse input");
+ }
+ }
+
+ private static ParseResultDto ParseText(string name, LibraryType libraryType)
+ {
+ var result = new ParseResultDto
+ {
+ SeriesName = Parser.ParseSeries(name, libraryType),
+ SeriesYear = Parser.ParseYear(name)
+ };
+ var chapterRange = Parser.ParseChapter(name, libraryType);
+ result.MinChapterNumber = Parser.MinNumberFromRange(chapterRange);
+ result.MaxChapterNumber = Parser.MaxNumberFromRange(chapterRange);
+ var volumeRange = Parser.ParseVolume(name, libraryType);
+ result.MinVolumeNumber = Parser.MinNumberFromRange(volumeRange);
+ result.MaxVolumeNumber = Parser.MaxNumberFromRange(volumeRange);
+ return result;
+ }
+
+ [HttpPost("parse-bulk")]
+ public ActionResult ParseBulk(ParseBulkRequestDto dto, CancellationToken cancellationToken)
+ {
+ if (dto.Names.Count > 100)
+ {
+ return BadRequest("Only 100 items can be processed at once");
+ }
+
+ var result = new ParseBulkResponseDto();
+
+ var successfulParses = result.Results;
+ var errorParses = result.Errors;
+ foreach (var name in dto.Names)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (name.Length > 1000)
+ {
+ errorParses.Add(name, "Length > 1000 characters");
+ continue;
+ }
+
+ try
+ {
+ successfulParses.Add(name, ParseText(name, dto.LibraryType));
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ errorParses.Add(name, "Input could not be parsed in allowed time");
+ }
+ catch (Exception)
+ {
+ errorParses.Add(name, "Failed to parse input");
+ }
+ }
+
+ return Ok(result);
+ }
}
diff --git a/API/DTOs/Misc/ParseBulkRequestDto.cs b/API/DTOs/Misc/ParseBulkRequestDto.cs
new file mode 100644
index 000000000..7e529e9ed
--- /dev/null
+++ b/API/DTOs/Misc/ParseBulkRequestDto.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using API.Entities.Enums;
+
+namespace API.DTOs.Misc;
+
+public sealed record ParseBulkRequestDto
+{
+ public ICollection Names { get; set; }
+ public LibraryType LibraryType { get; set; }
+}
diff --git a/API/DTOs/Misc/ParseBulkResponseDto.cs b/API/DTOs/Misc/ParseBulkResponseDto.cs
new file mode 100644
index 000000000..c46f1b78b
--- /dev/null
+++ b/API/DTOs/Misc/ParseBulkResponseDto.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace API.DTOs.Misc;
+
+public record ParseBulkResponseDto
+{
+ ///
+ /// The requested name to the parsed result. Does not include errored items
+ ///
+ public Dictionary Results { get; set; } = new();
+ ///
+ /// The requested name to parse maps to the Error exception
+ ///
+ public Dictionary Errors { get; set; } = new();
+
+ ///
+ /// Count of errored items
+ ///
+ public int ErrorCounts => Errors.Count;
+}
diff --git a/API/DTOs/Misc/ParseResultDto.cs b/API/DTOs/Misc/ParseResultDto.cs
new file mode 100644
index 000000000..dd2b8771e
--- /dev/null
+++ b/API/DTOs/Misc/ParseResultDto.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace API.DTOs.Misc;
+
+public sealed record ParseResultDto
+{
+ public string SeriesName { get; set; }
+ public string SeriesYear { get; set; }
+ public float MinChapterNumber { get; set; }
+ public float MaxChapterNumber { get; set; }
+ public float MinVolumeNumber { get; set; }
+ public float MaxVolumeNumber { get; set; }
+
+
+}