diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 3b41b429a..64d303f4d 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -211,6 +211,7 @@ public class MangaParsingTests [InlineData("Max Level Returner เล่มที่ 5", "Max Level Returner")] [InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")] [InlineData("不安の種\uff0b - 01", "不安の種\uff0b")] + [InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); diff --git a/API/API.csproj b/API/API.csproj index 4940379da..994bbc46f 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -54,7 +54,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -66,7 +66,7 @@ - + @@ -95,7 +95,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index a2fcec81a..33b422d70 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -237,18 +237,40 @@ public class ImageService : IImageService using var sourceImage = Image.NewFromStream(stream); if (stream.CanSeek) stream.Position = 0; + var scalingSize = GetSizeForDimensions(sourceImage, targetWidth, targetHeight); + var scalingCrop = GetCropForDimensions(sourceImage, targetWidth, targetHeight); + using var thumbnail = sourceImage.ThumbnailImage(targetWidth, targetHeight, - size: GetSizeForDimensions(sourceImage, targetWidth, targetHeight), - crop: GetCropForDimensions(sourceImage, targetWidth, targetHeight)); + size: scalingSize, + crop: scalingCrop); var filename = fileName + encodeFormat.GetExtension(); _directoryService.ExistOrCreate(outputDirectory); + try { _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); - return filename; + + try + { + thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + + return filename; + } + catch (VipsException) + { + // NetVips Issue: https://github.com/kleisauke/net-vips/issues/234 + // Saving pdf covers from a stream can fail, so revert to old code + + if (stream.CanSeek) stream.Position = 0; + using var thumbnail2 = Image.ThumbnailStream(stream, targetWidth, height: targetHeight, + size: scalingSize, + crop: scalingCrop); + thumbnail2.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + + return filename; + } } public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default) diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 24bb3ef7a..840e7a6d8 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -235,7 +235,7 @@ public static class Parser // [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( - @"^(?.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+Vol(ume)?\.?(\d+|tbd|\s\d).+?", + @"^(?.+?)(?:\s*|_|\-\s*)+(?:Ch(?:apter|\.|)\s*\d+(?:\.\d+)?(?:\s*|_|\-\s*)+)?Vol(?:ume|\.|)\s*(?:\d+|tbd)(?:\s|_|\-\s*).+", MatchOptions, RegexTimeout), // Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip new Regex( @@ -764,7 +764,10 @@ public static class Parser var group = matches .Select(match => match.Groups["Series"]) .FirstOrDefault(group => group.Success && group != Match.Empty); - if (group != null) return CleanTitle(group.Value); + if (group != null) + { + return CleanTitle(group.Value); + } } return string.Empty; diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 916458762..98605c7fb 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/openapi.json b/openapi.json index fda248741..e8b4eaf3f 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.8.1.14" + "version": "0.8.1.15" }, "servers": [ { @@ -22251,6 +22251,729 @@ "description": "Responsible for all things Want To Read" } ] +} "object", + "properties": { + "seriesId": { + "type": "integer", + "format": "int32" + }, + "body": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "UpdateWantToReadDto": { + "type": "object", + "properties": { + "seriesIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "description": "List of Series Ids that will be Added/Removed", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A list of Series to pass when working with Want To Read APIs" + }, + "UploadFileDto": { + "required": [ + "id", + "url" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Id of the Entity", + "format": "int32" + }, + "url": { + "type": "string", + "description": "Base Url encoding of the file to upload from (can be null)", + "nullable": true + } + }, + "additionalProperties": false + }, + "UploadUrlDto": { + "required": [ + "url" + ], + "type": "object", + "properties": { + "url": { + "minLength": 1, + "type": "string", + "description": "External url" + } + }, + "additionalProperties": false + }, + "UserDto": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "token": { + "type": "string", + "nullable": true + }, + "refreshToken": { + "type": "string", + "nullable": true + }, + "apiKey": { + "type": "string", + "nullable": true + }, + "preferences": { + "$ref": "#/components/schemas/UserPreferencesDto" + }, + "ageRestriction": { + "$ref": "#/components/schemas/AgeRestrictionDto" + }, + "kavitaVersion": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "UserDtoICount": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/UserDto" + }, + "count": { + "type": "integer", + "format": "int64" + } + }, + "additionalProperties": false + }, + "UserPreferencesDto": { + "required": [ + "autoCloseMenu", + "backgroundColor", + "blurUnreadSummaries", + "bookReaderFontFamily", + "bookReaderFontSize", + "bookReaderImmersiveMode", + "bookReaderLayoutMode", + "bookReaderLineSpacing", + "bookReaderMargin", + "bookReaderReadingDirection", + "bookReaderTapToPaginate", + "bookReaderThemeName", + "bookReaderWritingStyle", + "collapseSeriesRelationships", + "emulateBook", + "globalPageLayoutMode", + "layoutMode", + "locale", + "noTransitions", + "pageSplitOption", + "pdfLayoutMode", + "pdfScrollMode", + "pdfSpreadMode", + "pdfTheme", + "promptForDownloadSize", + "readerMode", + "readingDirection", + "scalingOption", + "shareReviews", + "showScreenHints", + "swipeToPaginate", + "theme" + ], + "type": "object", + "properties": { + "readingDirection": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "Manga Reader Option: What direction should the next/prev page buttons go", + "format": "int32" + }, + "scalingOption": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "Manga Reader Option: How should the image be scaled to screen", + "format": "int32" + }, + "pageSplitOption": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "Manga Reader Option: Which side of a split image should we show first", + "format": "int32" + }, + "readerMode": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "Manga Reader Option: How the manga reader should perform paging or reading of the file\r\n\r\nWebtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging\r\nby clicking top/bottom sides of reader.\r\n", + "format": "int32" + }, + "layoutMode": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "Manga Reader Option: How many pages to display in the reader at once", + "format": "int32" + }, + "emulateBook": { + "type": "boolean", + "description": "Manga Reader Option: Emulate a book by applying a shadow effect on the pages" + }, + "backgroundColor": { + "minLength": 1, + "type": "string", + "description": "Manga Reader Option: Background color of the reader" + }, + "swipeToPaginate": { + "type": "boolean", + "description": "Manga Reader Option: Should swiping trigger pagination" + }, + "autoCloseMenu": { + "type": "boolean", + "description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction" + }, + "showScreenHints": { + "type": "boolean", + "description": "Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change" + }, + "bookReaderMargin": { + "type": "integer", + "description": "Book Reader Option: Override extra Margin", + "format": "int32" + }, + "bookReaderLineSpacing": { + "type": "integer", + "description": "Book Reader Option: Override line-height", + "format": "int32" + }, + "bookReaderFontSize": { + "type": "integer", + "description": "Book Reader Option: Override font size", + "format": "int32" + }, + "bookReaderFontFamily": { + "minLength": 1, + "type": "string", + "description": "Book Reader Option: Maps to the default Kavita font-family (inherit) or an override" + }, + "bookReaderTapToPaginate": { + "type": "boolean", + "description": "Book Reader Option: Allows tapping on side of screens to paginate" + }, + "bookReaderReadingDirection": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "Book Reader Option: What direction should the next/prev page buttons go", + "format": "int32" + }, + "bookReaderWritingStyle": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "Book Reader Option: What writing style should be used, horizontal or vertical.", + "format": "int32" + }, + "theme": { + "$ref": "#/components/schemas/SiteThemeDto" + }, + "bookReaderThemeName": { + "minLength": 1, + "type": "string" + }, + "bookReaderLayoutMode": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "bookReaderImmersiveMode": { + "type": "boolean", + "description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this." + }, + "globalPageLayoutMode": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "Global Site Option: If the UI should layout items as Cards or List items", + "format": "int32" + }, + "blurUnreadSummaries": { + "type": "boolean", + "description": "UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already" + }, + "promptForDownloadSize": { + "type": "boolean", + "description": "UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB." + }, + "noTransitions": { + "type": "boolean", + "description": "UI Site Global Setting: Should Kavita disable CSS transitions" + }, + "collapseSeriesRelationships": { + "type": "boolean", + "description": "When showing series, only parent series or series with no relationships will be returned" + }, + "shareReviews": { + "type": "boolean", + "description": "UI Site Global Setting: Should series reviews be shared with all users in the server" + }, + "locale": { + "minLength": 1, + "type": "string", + "description": "UI Site Global Setting: The language locale that should be used for the user" + }, + "pdfTheme": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "PDF Reader: Theme of the Reader", + "format": "int32" + }, + "pdfScrollMode": { + "enum": [ + 0, + 1, + 3 + ], + "type": "integer", + "description": "PDF Reader: Scroll mode of the reader", + "format": "int32" + }, + "pdfLayoutMode": { + "enum": [ + 0, + 2 + ], + "type": "integer", + "description": "PDF Reader: Layout Mode of the reader", + "format": "int32" + }, + "pdfSpreadMode": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "PDF Reader: Spread Mode of the reader", + "format": "int32" + } + }, + "additionalProperties": false + }, + "UserReadStatistics": { + "type": "object", + "properties": { + "totalPagesRead": { + "type": "integer", + "description": "Total number of pages read", + "format": "int64" + }, + "totalWordsRead": { + "type": "integer", + "description": "Total number of words read", + "format": "int64" + }, + "timeSpentReading": { + "type": "integer", + "description": "Total time spent reading based on estimates", + "format": "int64" + }, + "chaptersRead": { + "type": "integer", + "format": "int64" + }, + "lastActive": { + "type": "string", + "format": "date-time" + }, + "avgHoursPerWeekSpentReading": { + "type": "number", + "format": "double" + }, + "percentReadPerLibrary": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SingleStatCount" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "UserReviewDto": { + "type": "object", + "properties": { + "tagline": { + "type": "string", + "description": "A tagline for the review", + "nullable": true + }, + "body": { + "type": "string", + "description": "The main review", + "nullable": true + }, + "bodyJustText": { + "type": "string", + "description": "The main body with just text, for review preview", + "nullable": true + }, + "seriesId": { + "type": "integer", + "description": "The series this is for", + "format": "int32" + }, + "libraryId": { + "type": "integer", + "description": "The library this series belongs in", + "format": "int32" + }, + "username": { + "type": "string", + "description": "The user who wrote this", + "nullable": true + }, + "totalVotes": { + "type": "integer", + "format": "int32" + }, + "rating": { + "type": "number", + "format": "float" + }, + "rawBody": { + "type": "string", + "nullable": true + }, + "score": { + "type": "integer", + "description": "How many upvotes this review has gotten", + "format": "int32" + }, + "siteUrl": { + "type": "string", + "description": "If External, the url of the review", + "nullable": true + }, + "isExternal": { + "type": "boolean", + "description": "Does this review come from an external Source" + }, + "provider": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "If this review is External, which Provider did it come from", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Represents a User Review for a given Series" + }, + "Volume": { + "required": [ + "maxNumber", + "minNumber", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).", + "nullable": true + }, + "lookupName": { + "type": "string", + "description": "This is just the original Parsed volume number for lookups", + "nullable": true + }, + "number": { + "type": "integer", + "description": "The minimum number in the Name field in Int form", + "format": "int32", + "deprecated": true + }, + "minNumber": { + "type": "number", + "description": "The minimum number in the Name field", + "format": "float" + }, + "maxNumber": { + "type": "number", + "description": "The maximum number in the Name field (same as Minimum if Name isn't a range)", + "format": "float" + }, + "chapters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Chapter" + }, + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time" + }, + "lastModified": { + "type": "string", + "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, + "coverImage": { + "type": "string", + "description": "Absolute path to the (managed) image file", + "nullable": true + }, + "pages": { + "type": "integer", + "description": "Total pages of all chapters in this volume", + "format": "int32" + }, + "wordCount": { + "type": "integer", + "description": "Total Word count of all chapters in this volume.", + "format": "int64" + }, + "minHoursToRead": { + "type": "integer", + "format": "int32" + }, + "maxHoursToRead": { + "type": "integer", + "format": "int32" + }, + "avgHoursToRead": { + "type": "integer", + "format": "int32" + }, + "series": { + "$ref": "#/components/schemas/Series" + }, + "seriesId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "VolumeDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "minNumber": { + "type": "number", + "format": "float" + }, + "maxNumber": { + "type": "number", + "format": "float" + }, + "name": { + "type": "string", + "nullable": true + }, + "number": { + "type": "integer", + "description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14", + "format": "int32", + "deprecated": true + }, + "pages": { + "type": "integer", + "format": "int32" + }, + "pagesRead": { + "type": "integer", + "format": "int32" + }, + "lastModifiedUtc": { + "type": "string", + "format": "date-time" + }, + "createdUtc": { + "type": "string", + "format": "date-time" + }, + "created": { + "type": "string", + "description": "When chapter was created in local server time", + "format": "date-time" + }, + "lastModified": { + "type": "string", + "description": "When chapter was last modified in local server time", + "format": "date-time" + }, + "seriesId": { + "type": "integer", + "format": "int32" + }, + "chapters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChapterDto" + }, + "nullable": true + }, + "minHoursToRead": { + "type": "integer", + "format": "int32" + }, + "maxHoursToRead": { + "type": "integer", + "format": "int32" + }, + "avgHoursToRead": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + }, + "securitySchemes": { + "Bearer": { + "type": "apiKey", + "description": "Please insert JWT with Bearer into field", + "name": "Authorization", + "in": "header" + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ], + "tags": [ + { + "name": "Account", + "description": "All Account matters" + }, + { + "name": "Cbl", + "description": "Responsible for the CBL import flow" + }, + { + "name": "Collection", + "description": "APIs for Collections" + }, + { + "name": "Device", + "description": "Responsible interacting and creating Devices" + }, + { + "name": "Download", + "description": "All APIs related to downloading entities from the system. Requires Download Role or Admin Role." + }, + { + "name": "Filter", + "description": "This is responsible for Filter caching" + }, + { + "name": "Image", + "description": "Responsible for servicing up images stored in Kavita for entities" + }, + { + "name": "Panels", + "description": "For the Panels app explicitly" + }, + { + "name": "Rating", + "description": "Responsible for providing external ratings for Series" + }, + { + "name": "Reader", + "description": "For all things regarding reading, mainly focusing on non-Book related entities" + }, + { + "name": "Search", + "description": "Responsible for the Search interface from the UI" + }, + { + "name": "Stream", + "description": "Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream)" + }, + { + "name": "Tachiyomi", + "description": "All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any\r\nother purposes." + }, + { + "name": "Upload", + "description": "" + }, + { + "name": "WantToRead", + "description": "Responsible for all things Want To Read" + } + ] } "nullable": true } },