diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8740c4e13..cdd72de1c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists. multiple: false options: - - 0.8.5.11 - Stable + - 0.8.6.2 - Stable - Nightly Testing Branch validations: required: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a77338866..7ce4276bc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,7 +13,7 @@ name: "CodeQL" on: push: - branches: [ "develop", "main" ] + branches: [ "develop"] pull_request: # The branches below must be a subset of the branches above branches: [ "develop" ] diff --git a/API/API.csproj b/API/API.csproj index 52c390f4e..80f371b7a 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -115,6 +115,7 @@ + diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs index 7181f6eef..db1381d9d 100644 --- a/API/Controllers/VolumeController.cs +++ b/API/Controllers/VolumeController.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; +using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs; @@ -54,4 +56,29 @@ public class VolumeController : BaseApiController return Ok(false); } + + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("multiple")] + public async Task> DeleteMultipleVolumes(int[] volumesIds) + { + var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds); + if (volumes.Count != volumesIds.Length) + { + return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); + } + + _unitOfWork.VolumeRepository.Remove(volumes); + + if (!await _unitOfWork.CommitAsync()) + { + return Ok(false); + } + + foreach (var volume in volumes) + { + await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + } + + return Ok(true); + } } diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index cb0783a89..4b07ade96 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -35,6 +35,7 @@ public interface IVolumeRepository void Add(Volume volume); void Update(Volume volume); void Remove(Volume volume); + void Remove(IList volumes); Task> GetFilesForVolume(int volumeId); Task GetVolumeCoverImageAsync(int volumeId); Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); @@ -43,6 +44,7 @@ public interface IVolumeRepository Task GetVolumeDtoAsync(int volumeId, int userId); Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); Task> GetVolumes(int seriesId); + Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None); Task GetVolumeByIdAsync(int volumeId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task> GetCoverImagesForLockedVolumesAsync(); @@ -72,6 +74,10 @@ public class VolumeRepository : IVolumeRepository { _context.Volume.Remove(volume); } + public void Remove(IList volumes) + { + _context.Volume.RemoveRange(volumes); + } /// /// Returns a list of non-tracked files for a given volume. @@ -180,6 +186,15 @@ public class VolumeRepository : IVolumeRepository .OrderBy(vol => vol.MinNumber) .ToListAsync(); } + public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None) + { + return await _context.Volume + .Where(vol => volumeIds.Contains(vol.Id)) + .Includes(includes) + .AsSplitQuery() + .OrderBy(vol => vol.MinNumber) + .ToListAsync(); + } /// /// Returns a single volume with Chapter and Files diff --git a/API/I18N/pl.json b/API/I18N/pl.json index db3fa5063..68a4a1a4f 100644 --- a/API/I18N/pl.json +++ b/API/I18N/pl.json @@ -204,8 +204,8 @@ "person-image-doesnt-exist": "Osoba nie istnieje w CoversDB", "email-taken": "Adres e-mail jest już używany", "kavitaplus-restricted": "Jest to dostępne tylko dla Kavita+", - "smart-filter-name-required": "Strona internetowa", - "sidenav-stream-only-delete-smart-filter": "Jedynie filtry filtrowe mogą zostać usunięte z SideNav", - "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów mogą zostać usunięte z rozdzielczości", + "smart-filter-name-required": "Inteligentny filtr wymaga nazwy", + "sidenav-stream-only-delete-smart-filter": "Tylko inteligentne filtry mogą zostać usunięte z panelu bocznego", + "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów może zostać usunięte z głównego panelu", "smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem" } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index d008ab5f5..a2acc538b 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -186,6 +186,7 @@ public class CacheService : ICacheService } else { + // Potential BUG: If the folder is left here and there are no files within, this could theoretically return without proper cache return chapter; } } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 2e3a0f43c..5d5df6647 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -178,7 +178,6 @@ public class StatsService : IStatsService var sw = Stopwatch.StartNew(); var response = await (Configuration.StatsApiUrl + "/api/health/") .WithBasicHeaders(ApiKey) - .WithTimeout(TimeSpan.FromSeconds(30)) .GetAsync(); if (response.StatusCode == StatusCodes.Status200OK) @@ -197,7 +196,7 @@ public class StatsService : IStatsService private async Task MaxSeriesInAnyLibrary() { - // If first time flow, just return 0 + // If first time flow, return 0 if (!await _context.Series.AnyAsync()) return 0; return await _context.Series .Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count()) diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 0c6352d06..ad2d89fa5 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -1,7 +1,7 @@ { "TokenKey": "super secret unguessable key that is longer because we require it", "Port": 5000, - "IpAddresses": "0.0.0.0,::", + "IpAddresses": "", "BaseUrl": "/", "Cache": 75, "AllowIFraming": false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43cd2e208..292217862 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit - HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc) - [Git](https://git-scm.com/downloads) - [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher) -- .NET 8.0+ +- .NET 9.0+ - dotnet tool install -g Swashbuckle.AspNetCore.Cli ### Getting started ### diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 419748032..8b6730ba3 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.0 + 0.8.6.2 en true @@ -20,4 +20,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 1ea5a94ca..bff8f0f5c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ your reading collection with your friends and family! ## Demo If you want to try out Kavita, a demo is available: -[https://demo.kavitareader.com/](https://demo.kavitareader.com/) +[https://demo.kavitareader.com/](https://demo.kavitareader.com/login?apiKey=9003cf99-9213-4206-a787-af2fe4cc5f1f) ``` Username: demouser Password: Demouser64 diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 8bf6cdacd..1cf4e448e 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -472,8 +472,19 @@ export class ActionService { }); } + async deleteMultipleVolumes(volumes: Array, callback?: BooleanActionCallback) { + // TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes" + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return; + + this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => { + if (callback) { + callback(success); + } + }) + } + async deleteMultipleChapters(seriesId: number, chapterIds: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters'))) return; + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: chapterIds.length}))) return; this.chapterService.deleteMultipleChapters(seriesId, chapterIds.map(c => c.id)).subscribe(() => { if (callback) { diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts index 16857b3d2..f53a20543 100644 --- a/UI/Web/src/app/_services/volume.service.ts +++ b/UI/Web/src/app/_services/volume.service.ts @@ -21,6 +21,10 @@ export class VolumeService { return this.httpClient.delete(this.baseUrl + 'volume?volumeId=' + volumeId); } + deleteMultipleVolumes(volumeIds: number[]) { + return this.httpClient.post(this.baseUrl + "volume/multiple", volumeIds) + } + updateVolume(volume: any) { return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse); } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index d9aa1decb..8546ad921 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from 'rxjs'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {translate, TranslocoModule} from "@jsverse/transloco"; @@ -46,15 +46,21 @@ export class ManageEmailSettingsComponent implements OnInit { // Automatically save settings as we edit them this.settingsForm.valueChanges.pipe( - debounceTime(300), distinctUntilChanged(), + debounceTime(300), filter(_ => this.settingsForm.valid), takeUntilDestroyed(this.destroyRef), switchMap(_ => { const data = this.packData(); - return this.settingsService.updateServerSettings(data); + return this.settingsService.updateServerSettings(data).pipe(catchError(err => { + console.error(err); + return of(null); + })); }), tap(settings => { + if (!settings) { + return; + } this.serverSettings = settings; this.cdRef.markForCheck(); }) diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index fbfb256dc..ac62e2038 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {debounceTime, distinctUntilChanged, filter, switchMap, take, tap} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, take, tap} from 'rxjs'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {DirectoryPickerComponent, DirectoryPickerResult} from '../_modals/directory-picker/directory-picker.component'; @@ -55,9 +55,15 @@ export class ManageMediaSettingsComponent implements OnInit { takeUntilDestroyed(this.destroyRef), switchMap(_ => { const data = this.packData(); - return this.settingsService.updateServerSettings(data); + return this.settingsService.updateServerSettings(data).pipe(catchError(err => { + console.error(err); + return of(null); + })); }), tap(settings => { + if (!settings) { + return; + } const encodingChanged = this.serverSettings.encodeMediaAs !== settings.encodeMediaAs; if (encodingChanged) { diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index d7a2e8c6c..7e0b86f87 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -64,7 +64,7 @@ @if (settingsForm.get('ipAddresses'); as formControl) { - {{formControl.value}} + {{formControl.value | defaultValue}}
@@ -75,7 +75,7 @@ @if(settingsForm.dirty || !settingsForm.untouched) {
- @if (formControl.errors?.pattern) { + @if (formControl.errors?.emptyOrPattern) {
{{t('ip-address-validation')}}
}
diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 33941f768..8c5f3f17a 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -1,5 +1,5 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; -import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; import {take} from 'rxjs/operators'; import {ServerService} from 'src/app/_services/server.service'; @@ -10,7 +10,7 @@ import {WikiLink} from "../../_models/wiki"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; import {ConfirmService} from "../../shared/confirm.service"; -import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from "rxjs"; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {EnterBlurDirective} from "../../_directives/enter-blur.directive"; @@ -40,6 +40,7 @@ export class ManageSettingsComponent implements OnInit { settingsForm: FormGroup = new FormGroup({}); taskFrequencies: Array = []; logLevels: Array = []; + isDocker: boolean = false; allowStatsTooltip = translate('manage-settings.allow-stats-tooltip-part-1') + ' - -
- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) { - {{volume.name}} - } @else { - {{volume.chapters[0].titleName}} - } -
- - @if (actions && actions.length > 0) { - - - + @if ((libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book)) { + @if (volume.name) { +
+
+ {{volume.name}} +
+
} -
+ } @else if (volume.chapters[0].titleName) { +
+
+ {{volume.chapters[0].titleName}} +
+
+ } }
diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts index 3a3087632..22405364f 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts @@ -82,7 +82,7 @@ export class DraggableOrderedListComponent { get BufferAmount() { - return Math.min(this.items.length / 20, 20); + return Math.floor(Math.min(this.items.length / 20, 20)); } constructor() { diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 890237be4..68d2ac4dd 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -115,6 +115,7 @@ import {CoverImageComponent} from "../../../_single-module/cover-image/cover-ima import {DefaultModalOptions} from "../../../_models/default-modal-options"; import {LicenseService} from "../../../_services/license.service"; import {PageBookmark} from "../../../_models/readers/page-bookmark"; +import {VolumeRemovedEvent} from "../../../_models/events/volume-removed-event"; enum TabID { @@ -353,11 +354,25 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.cdRef.markForCheck(); break; case Action.Delete: - await this.actionService.deleteMultipleChapters(seriesId, chapters, () => { - // No need to update the page as the backend will spam volume/chapter deletions - this.bulkSelectionService.deselectAll(); - this.cdRef.markForCheck(); - }); + if (chapters.length > 0) { + await this.actionService.deleteMultipleChapters(seriesId, chapters, () => { + // No need to update the page as the backend will spam volume/chapter deletions + this.bulkSelectionService.deselectAll(); + this.cdRef.markForCheck(); + }); + + // It's not possible to select both chapters and volumes + break; + } + + if (selectedVolumeIds.length > 0) { + await this.actionService.deleteMultipleVolumes(selectedVolumeIds, () => { + // No need to update the page as the backend will spam volume deletions + this.bulkSelectionService.deselectAll(); + this.cdRef.markForCheck(); + }); + } + break; } } @@ -486,6 +501,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { const removedEvent = event.payload as ChapterRemovedEvent; if (removedEvent.seriesId !== this.seriesId) return; this.loadPageSource.next(false); + } else if (event.event === EVENTS.VolumeRemoved) { + const volumeRemoveEvent = event.payload as VolumeRemovedEvent; + if (volumeRemoveEvent.seriesId === this.seriesId) { + this.loadPageSource.next(false); + } } }); @@ -660,7 +680,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { case (Action.Delete): await this.actionService.deleteChapter(chapter.id, (success) => { if (!success) return; - + this.chapters = this.chapters.filter(c => c.id != chapter.id); this.cdRef.markForCheck(); }); diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 2b9d6644a..4d22e44bb 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -2640,6 +2640,7 @@ "alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.", "confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.", "confirm-delete-multiple-chapters": "Are you sure you want to delete {{count}} chapter/volumes? It will not modify files on disk.", + "confirm-delete-multiple-volumes": "Are you sure you want to delete {{count}} volumes? It will not modify files on disk.", "confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.", "confirm-delete-chapter": "Are you sure you want to delete this chapter? It will not modify files on disk.", "confirm-delete-volume": "Are you sure you want to delete this volume? It will not modify files on disk.", diff --git a/UI/Web/src/assets/langs/fi.json b/UI/Web/src/assets/langs/fi.json index edcd8166f..9a316f77a 100644 --- a/UI/Web/src/assets/langs/fi.json +++ b/UI/Web/src/assets/langs/fi.json @@ -10,7 +10,7 @@ "devices-tab": "{{tabs.devices-tab}}", "smart-filters-tab": "{{tabs.smart-filters-tab}}", "success-toast": "Käyttäjäasetukset päivitetty", - "global-settings-title": "Laajamittaiset asetukset", + "global-settings-title": "Yleisesti pätevät asetukset", "page-layout-mode-label": "Sivun asettelutila", "page-layout-mode-tooltip": "Näytä kohteet kortteina tai luettelonäkymänä Sarjojen yksityiskohdat -sivulla.", "locale-label": "Kieliasetus", diff --git a/UI/Web/src/assets/langs/zh_Hans.json b/UI/Web/src/assets/langs/zh_Hans.json index d3a8496e0..a81869f69 100644 --- a/UI/Web/src/assets/langs/zh_Hans.json +++ b/UI/Web/src/assets/langs/zh_Hans.json @@ -2219,12 +2219,12 @@ "delete-device": "您确定要删除该设备吗?", "confirm-regen-covers": "刷新封面将强制重新生成所有封面图片。这是一项繁重的运算。您确定执行,而不想使用扫描操作代替吗?", "alert-long-running": "这是一个长时间运行的任务。请等待其完成后再次进行操作。", - "confirm-delete-multiple-series": "您确定要删除{{count}}个系列吗?这不会修改磁盘上的文件。", + "confirm-delete-multiple-series": "您确定要删除这 {{count}} 个系列吗?这不会修改磁盘上的文件。", "confirm-delete-series": "您确定要删除此系列吗?这不会修改磁盘上的文件。", "confirm-delete-chapter": "您确定要删除此章节吗?它不会修改磁盘上的文件。", "confirm-delete-volume": "您确定要删除此卷吗?它不会修改磁盘上的文件。", "alert-bad-theme": "主题中存在无效或不安全的CSS。请联系管理员进行修正。将默认为暗色主题。", - "confirm-library-delete": "您确定要删除{{name}}资料库吗?此操作无法撤销。", + "confirm-library-delete": "您确定要删除 {{name}} 资料库吗?此操作无法撤销。", "confirm-library-type-change": "更改资料库类型将触发具有不同解析规则的新扫描,并可能导致重新创建系列,因此您可能会丢失进度和书签。您应该在执行此操作之前进行备份。您确定要继续吗?", "confirm-download-size": "{{entityType}}的大小为{{size}}。确定要继续吗?", "confirm-download-size-ios": "iOS 在下载大于 200MB 的文件时出现问题,此下载可能无法完成。", diff --git a/openapi.json b/openapi.json index db02f4511..e71ecfd55 100644 --- a/openapi.json +++ b/openapi.json @@ -1,8 +1,8 @@ { "openapi": "3.0.4", "info": { - "title": "Kavita (v0.8.6.0)", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.0", + "title": "Kavita (v0.8.6.1)", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.1", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" @@ -14508,6 +14508,66 @@ } } }, + "/api/Volume/multiple": { + "post": { + "tags": [ + "Volume" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "boolean" + } + }, + "application/json": { + "schema": { + "type": "boolean" + } + }, + "text/json": { + "schema": { + "type": "boolean" + } + } + } + } + } + } + }, "/api/want-to-read": { "post": { "tags": [